# Directory Structure
```
├── .config
│ └── dotnet-tools.json
├── .editorconfig
├── .github
│ ├── release-drafter.yml
│ └── workflows
│ ├── build.yml
│ └── release-drafter.yml
├── .gitignore
├── .gitmodules
├── .vscode
│ ├── settings.json
│ └── tasks.json
├── assets
│ ├── aspire-dashboard.png
│ ├── inspector.png
│ └── logo.png
├── build.cake
├── CODE_OF_CONDUCT.md
├── Directory.Build.props
├── global.json
├── hangfire-mcp.slnx
├── LICENSE.md
├── mcp_example.json
├── README.md
├── samples
│ ├── AppHost
│ │ ├── AppHost.csproj
│ │ ├── appsettings.Development.json
│ │ ├── appsettings.json
│ │ ├── Program.cs
│ │ └── Properties
│ │ └── launchSettings.json
│ ├── HangfireJobs
│ │ ├── HangfireJobs.csproj
│ │ ├── ISendMessageJob.cs
│ │ └── ITimeJob.cs
│ ├── HangfireMCP
│ │ ├── appsettings.Development.json
│ │ ├── appsettings.json
│ │ ├── HangfireExtensions.cs
│ │ ├── HangfireMCP.csproj
│ │ ├── HangfireTool.cs
│ │ ├── McpServerExtensions.cs
│ │ ├── Program.cs
│ │ └── Properties
│ │ └── launchSettings.json
│ ├── HangfireMCP.Standalone
│ │ ├── appsettings.Development.json
│ │ ├── appsettings.json
│ │ ├── HangfireExtensions.cs
│ │ ├── HangfireMCP.Standalone.csproj
│ │ ├── HangfireTool.cs
│ │ ├── McpServerExtensions.cs
│ │ ├── Program.cs
│ │ └── Properties
│ │ └── launchSettings.json
│ └── Web
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── HangfireExtensions.cs
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Web.csproj
│ └── Web.http
└── src
└── Nall.Hangfire.Mcp
├── DynamicJobLoader.cs
├── DynamicJobLoaderHostedService.cs
├── HangfireDynamicScheduler.cs
├── JobDescriptor.cs
├── Nall.Hangfire.Mcp.csproj
└── ServiceCollectionExtensions.cs
```
# Files
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
```
1 | [submodule "src/Inspector.Aspire.Hosting/inspector"]
2 | path = src/Inspector.Aspire.Hosting/inspector
3 | url = https://github.com/modelcontextprotocol/inspector.git
4 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from `dotnet new gitignore`
5 |
6 | # dotenv files
7 | .env
8 |
9 | # User-specific files
10 | *.rsuser
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # User-specific files (MonoDevelop/Xamarin Studio)
17 | *.userprefs
18 |
19 | # Mono auto generated files
20 | mono_crash.*
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Ww][Ii][Nn]32/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # Tye
69 | .tye/
70 |
71 | # ASP.NET Scaffolding
72 | ScaffoldingReadMe.txt
73 |
74 | # StyleCop
75 | StyleCopReport.xml
76 |
77 | # Files built by Visual Studio
78 | *_i.c
79 | *_p.c
80 | *_h.h
81 | *.ilk
82 | *.meta
83 | *.obj
84 | *.iobj
85 | *.pch
86 | *.pdb
87 | *.ipdb
88 | *.pgc
89 | *.pgd
90 | *.rsp
91 | *.sbr
92 | *.tlb
93 | *.tli
94 | *.tlh
95 | *.tmp
96 | *.tmp_proj
97 | *_wpftmp.csproj
98 | *.log
99 | *.tlog
100 | *.vspscc
101 | *.vssscc
102 | .builds
103 | *.pidb
104 | *.svclog
105 | *.scc
106 |
107 | # Chutzpah Test files
108 | _Chutzpah*
109 |
110 | # Visual C++ cache files
111 | ipch/
112 | *.aps
113 | *.ncb
114 | *.opendb
115 | *.opensdf
116 | *.sdf
117 | *.cachefile
118 | *.VC.db
119 | *.VC.VC.opendb
120 |
121 | # Visual Studio profiler
122 | *.psess
123 | *.vsp
124 | *.vspx
125 | *.sap
126 |
127 | # Visual Studio Trace Files
128 | *.e2e
129 |
130 | # TFS 2012 Local Workspace
131 | $tf/
132 |
133 | # Guidance Automation Toolkit
134 | *.gpState
135 |
136 | # ReSharper is a .NET coding add-in
137 | _ReSharper*/
138 | *.[Rr]e[Ss]harper
139 | *.DotSettings.user
140 |
141 | # TeamCity is a build add-in
142 | _TeamCity*
143 |
144 | # DotCover is a Code Coverage Tool
145 | *.dotCover
146 |
147 | # AxoCover is a Code Coverage Tool
148 | .axoCover/*
149 | !.axoCover/settings.json
150 |
151 | # Coverlet is a free, cross platform Code Coverage Tool
152 | coverage*.json
153 | coverage*.xml
154 | coverage*.info
155 |
156 | # Visual Studio code coverage results
157 | *.coverage
158 | *.coveragexml
159 |
160 | # NCrunch
161 | _NCrunch_*
162 | .*crunch*.local.xml
163 | nCrunchTemp_*
164 |
165 | # MightyMoose
166 | *.mm.*
167 | AutoTest.Net/
168 |
169 | # Web workbench (sass)
170 | .sass-cache/
171 |
172 | # Installshield output folder
173 | [Ee]xpress/
174 |
175 | # DocProject is a documentation generator add-in
176 | DocProject/buildhelp/
177 | DocProject/Help/*.HxT
178 | DocProject/Help/*.HxC
179 | DocProject/Help/*.hhc
180 | DocProject/Help/*.hhk
181 | DocProject/Help/*.hhp
182 | DocProject/Help/Html2
183 | DocProject/Help/html
184 |
185 | # Click-Once directory
186 | publish/
187 |
188 | # Publish Web Output
189 | *.[Pp]ublish.xml
190 | *.azurePubxml
191 | # Note: Comment the next line if you want to checkin your web deploy settings,
192 | # but database connection strings (with potential passwords) will be unencrypted
193 | *.pubxml
194 | *.publishproj
195 |
196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
197 | # checkin your Azure Web App publish settings, but sensitive information contained
198 | # in these scripts will be unencrypted
199 | PublishScripts/
200 |
201 | # NuGet Packages
202 | *.nupkg
203 | # NuGet Symbol Packages
204 | *.snupkg
205 | # The packages folder can be ignored because of Package Restore
206 | **/[Pp]ackages/*
207 | # except build/, which is used as an MSBuild target.
208 | !**/[Pp]ackages/build/
209 | # Uncomment if necessary however generally it will be regenerated when needed
210 | #!**/[Pp]ackages/repositories.config
211 | # NuGet v3's project.json files produces more ignorable files
212 | *.nuget.props
213 | *.nuget.targets
214 |
215 | # Microsoft Azure Build Output
216 | csx/
217 | *.build.csdef
218 |
219 | # Microsoft Azure Emulator
220 | ecf/
221 | rcf/
222 |
223 | # Windows Store app package directories and files
224 | AppPackages/
225 | BundleArtifacts/
226 | Package.StoreAssociation.xml
227 | _pkginfo.txt
228 | *.appx
229 | *.appxbundle
230 | *.appxupload
231 |
232 | # Visual Studio cache files
233 | # files ending in .cache can be ignored
234 | *.[Cc]ache
235 | # but keep track of directories ending in .cache
236 | !?*.[Cc]ache/
237 |
238 | # Others
239 | ClientBin/
240 | ~$*
241 | *~
242 | *.dbmdl
243 | *.dbproj.schemaview
244 | *.jfm
245 | *.pfx
246 | *.publishsettings
247 | orleans.codegen.cs
248 |
249 | # Including strong name files can present a security risk
250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251 | #*.snk
252 |
253 | # Since there are multiple workflows, uncomment next line to ignore bower_components
254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255 | #bower_components/
256 |
257 | # RIA/Silverlight projects
258 | Generated_Code/
259 |
260 | # Backup & report files from converting an old project file
261 | # to a newer Visual Studio version. Backup files are not needed,
262 | # because we have git ;-)
263 | _UpgradeReport_Files/
264 | Backup*/
265 | UpgradeLog*.XML
266 | UpgradeLog*.htm
267 | ServiceFabricBackup/
268 | *.rptproj.bak
269 |
270 | # SQL Server files
271 | *.mdf
272 | *.ldf
273 | *.ndf
274 |
275 | # Business Intelligence projects
276 | *.rdl.data
277 | *.bim.layout
278 | *.bim_*.settings
279 | *.rptproj.rsuser
280 | *- [Bb]ackup.rdl
281 | *- [Bb]ackup ([0-9]).rdl
282 | *- [Bb]ackup ([0-9][0-9]).rdl
283 |
284 | # Microsoft Fakes
285 | FakesAssemblies/
286 |
287 | # GhostDoc plugin setting file
288 | *.GhostDoc.xml
289 |
290 | # Node.js Tools for Visual Studio
291 | .ntvs_analysis.dat
292 | node_modules/
293 |
294 | # Visual Studio 6 build log
295 | *.plg
296 |
297 | # Visual Studio 6 workspace options file
298 | *.opt
299 |
300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
301 | *.vbw
302 |
303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
304 | *.vbp
305 |
306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
307 | *.dsw
308 | *.dsp
309 |
310 | # Visual Studio 6 technical files
311 | *.ncb
312 | *.aps
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # CodeRush personal settings
330 | .cr/personal
331 |
332 | # Python Tools for Visual Studio (PTVS)
333 | __pycache__/
334 | *.pyc
335 |
336 | # Cake - Uncomment if you are using it
337 | # tools/**
338 | # !tools/packages.config
339 |
340 | # Tabs Studio
341 | *.tss
342 |
343 | # Telerik's JustMock configuration file
344 | *.jmconfig
345 |
346 | # BizTalk build output
347 | *.btp.cs
348 | *.btm.cs
349 | *.odx.cs
350 | *.xsd.cs
351 |
352 | # OpenCover UI analysis results
353 | OpenCover/
354 |
355 | # Azure Stream Analytics local run output
356 | ASALocalRun/
357 |
358 | # MSBuild Binary and Structured Log
359 | *.binlog
360 |
361 | # NVidia Nsight GPU debugger configuration file
362 | *.nvuser
363 |
364 | # MFractors (Xamarin productivity tool) working folder
365 | .mfractor/
366 |
367 | # Local History for Visual Studio
368 | .localhistory/
369 |
370 | # Visual Studio History (VSHistory) files
371 | .vshistory/
372 |
373 | # BeatPulse healthcheck temp database
374 | healthchecksdb
375 |
376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
377 | MigrationBackup/
378 |
379 | # Ionide (cross platform F# VS Code tools) working folder
380 | .ionide/
381 |
382 | # Fody - auto-generated XML schema
383 | FodyWeavers.xsd
384 |
385 | # VS Code files for those working on multiple tools
386 | .vscode/*
387 | !.vscode/settings.json
388 | !.vscode/tasks.json
389 | !.vscode/launch.json
390 | !.vscode/extensions.json
391 | *.code-workspace
392 |
393 | # Local History for Visual Studio Code
394 | .history/
395 |
396 | # Windows Installer files from build outputs
397 | *.cab
398 | *.msi
399 | *.msix
400 | *.msm
401 | *.msp
402 |
403 | # JetBrains Rider
404 | *.sln.iml
405 | .idea/
406 |
407 | ##
408 | ## Visual studio for Mac
409 | ##
410 |
411 |
412 | # globs
413 | Makefile.in
414 | *.userprefs
415 | *.usertasks
416 | config.make
417 | config.status
418 | aclocal.m4
419 | install-sh
420 | autom4te.cache/
421 | *.tar.gz
422 | tarballs/
423 | test-results/
424 |
425 | # Mac bundle stuff
426 | *.dmg
427 | *.app
428 |
429 | # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
430 | # General
431 | .DS_Store
432 | .AppleDouble
433 | .LSOverride
434 |
435 | # Icon must end with two \r
436 | Icon
437 |
438 |
439 | # Thumbnails
440 | ._*
441 |
442 | # Files that might appear in the root of a volume
443 | .DocumentRevisions-V100
444 | .fseventsd
445 | .Spotlight-V100
446 | .TemporaryItems
447 | .Trashes
448 | .VolumeIcon.icns
449 | .com.apple.timemachine.donotpresent
450 |
451 | # Directories potentially created on remote AFP share
452 | .AppleDB
453 | .AppleDesktop
454 | Network Trash Folder
455 | Temporary Items
456 | .apdisk
457 |
458 | # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
459 | # Windows thumbnail cache files
460 | Thumbs.db
461 | ehthumbs.db
462 | ehthumbs_vista.db
463 |
464 | # Dump file
465 | *.stackdump
466 |
467 | # Folder config file
468 | [Dd]esktop.ini
469 |
470 | # Recycle Bin used on file shares
471 | $RECYCLE.BIN/
472 |
473 | # Windows Installer files
474 | *.cab
475 | *.msi
476 | *.msix
477 | *.msm
478 | *.msp
479 |
480 | # Windows shortcuts
481 | *.lnk
482 |
483 | # Vim temporary swap files
484 | *.swp
485 |
486 | Artefacts
```
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
```
1 |
2 | ##########################################
3 | # Common Settings
4 | ##########################################
5 |
6 | # This file is the top-most EditorConfig file
7 | root = true
8 |
9 | # All Files
10 | [*]
11 | charset = utf-8
12 | indent_style = space
13 | indent_size = 4
14 | insert_final_newline = true
15 | trim_trailing_whitespace = true
16 | max_line_length=100
17 | ##########################################
18 | # File Extension Settings
19 | ##########################################
20 |
21 | # Visual Studio Solution Files
22 | [*.sln]
23 | indent_style = tab
24 |
25 | # Visual Studio XML Project Files
26 | [*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}]
27 | indent_size = 2
28 |
29 | # XML Configuration Files
30 | [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}]
31 | indent_size = 2
32 |
33 | # JSON Files
34 | [*.{json,json5,webmanifest}]
35 | indent_size = 2
36 |
37 | # YAML Files
38 | [*.{yml,yaml}]
39 | indent_size = 2
40 |
41 | # Markdown Files
42 | [*.md]
43 | trim_trailing_whitespace = false
44 |
45 | # Web Files
46 | [*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}]
47 | indent_size = 2
48 |
49 | # Batch Files
50 | [*.{cmd,bat}]
51 | end_of_line = crlf
52 |
53 | # Bash Files
54 | [*.sh]
55 | end_of_line = lf
56 |
57 | # Makefiles
58 | [Makefile]
59 | indent_style = tab
60 |
61 | ##########################################
62 | # Default .NET Code Style Severities
63 | # https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope
64 | ##########################################
65 |
66 | [*.{cs,csx,cake,vb,vbx}]
67 | # Default Severity for all .NET Code Style rules below
68 | dotnet_analyzer_diagnostic.severity = warning
69 |
70 | csharp_style_expression_bodied_methods=true:suggestion
71 | csharp_style_expression_bodied_constructors=true:suggestion
72 | csharp_style_namespace_declarations=file_scoped:silent
73 |
74 | ##########################################
75 | # TODO: this is already present in this file and section below is inteded to force this on editor level.
76 | # IDE Code Style IDE_XXXX
77 | # Motivation https://github.com/dotnet/roslyn/blob/9f87b444da9c48a4d492b19f8337339056bf2b95/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs
78 | ##########################################
79 | # IDE0055 Fix formatting
80 | dotnet_diagnostic.IDE0055.severity = suggestion
81 | # IDE005_gen: Remove unnecessary usings in generated code
82 | dotnet_diagnostic.IDE0005.severity = error
83 | # IDE0065: Using directives must be placed outside of a namespace declaration
84 | dotnet_diagnostic.IDE0065.severity = error
85 | # IDE0059: Unnecessary assignment
86 | dotnet_diagnostic.IDE0059.severity = error
87 | # IDE0007 UseImplicitType
88 | dotnet_diagnostic.IDE0007.severity = error
89 | # IDE0008
90 | dotnet_diagnostic.IDE0008.severity = error
91 | # IDE0011 UseExplicitType
92 | dotnet_diagnostic.IDE0011.severity = error
93 | # IDE0040 AddAccessibilityModifiers
94 | dotnet_diagnostic.IDE0040.severity = error
95 | # IDE0060 UnusedParameter
96 | dotnet_diagnostic.IDE0060.severity = error
97 | # IDE0036 Order modifiers
98 | dotnet_diagnostic.IDE0036.severity = error
99 | # IDE0059 Remove unnecessary value assignment
100 | dotnet_diagnostic.IDE0059.severity = error
101 | # IDE0016 Use throw expression
102 | dotnet_diagnostic.IDE0016.severity = suggestion
103 | # CA1056: URI properties should not be strings
104 | dotnet_diagnostic.CA1056.severity = suggestion
105 |
106 | dotnet_diagnostic.CA1812.severity = none
107 | dotnet_diagnostic.CA2007.severity = none
108 | dotnet_diagnostic.CA1307.severity = none
109 | dotnet_diagnostic.CA2234.severity = none
110 | dotnet_diagnostic.CA1054.severity = none
111 | dotnet_diagnostic.CA1032.severity = none
112 | dotnet_diagnostic.CA1724.severity = none
113 | dotnet_diagnostic.CA1055.severity = none
114 | dotnet_diagnostic.CA1510.severity = suggestion
115 | dotnet_diagnostic.CA2227.severity = suggestion
116 | dotnet_diagnostic.CA1819.severity = suggestion
117 | dotnet_diagnostic.CA1019.severity = none
118 |
119 | ##########################################
120 | # File Header (Uncomment to support file headers)
121 | # https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
122 | ##########################################
123 |
124 | # [*.{cs,csx,cake,vb,vbx}]
125 | # file_header_template = <copyright file="{fileName}" company="PROJECT-AUTHOR">\n© PROJECT-AUTHOR\n</copyright>
126 |
127 | # SA1636: File header copyright text should match
128 | # Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
129 | # dotnet_diagnostic.SA1636.severity = warning
130 |
131 | [*.{cs,csx,cake,vb,vbx}]
132 | # IDE0073: The file header is missing or not located at the top of the file
133 | dotnet_diagnostic.IDE0073.severity = error
134 |
135 | ##########################################
136 | # .NET Language Conventions
137 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
138 | ##########################################
139 |
140 | # .NET Code Style Settings
141 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings
142 | [*.{cs,csx,cake,vb,vbx}]
143 |
144 | # "this." and "Me." qualifiers
145 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me
146 | dotnet_style_qualification_for_field = true:warning
147 | dotnet_style_qualification_for_property = true:warning
148 | dotnet_style_qualification_for_method = true:warning
149 | dotnet_style_qualification_for_event = true:warning
150 | # Language keywords instead of framework type names for type references
151 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords
152 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning
153 | dotnet_style_predefined_type_for_member_access = true:warning
154 | # Modifier preferences
155 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
156 | dotnet_style_require_accessibility_modifiers = always:warning
157 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
158 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
159 | dotnet_style_readonly_field = true:warning
160 | # Parentheses preferences
161 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences
162 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
163 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
164 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
165 | dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion
166 | # Expression-level preferences
167 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
168 | dotnet_style_object_initializer = true:warning
169 | dotnet_style_collection_initializer = true:warning
170 | dotnet_style_explicit_tuple_names = true:warning
171 | dotnet_style_prefer_inferred_tuple_names = true:warning
172 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
173 | dotnet_style_prefer_auto_properties = true:warning
174 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
175 | dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
176 | dotnet_diagnostic.IDE0045.severity = suggestion
177 | dotnet_style_prefer_conditional_expression_over_return = false:suggestion
178 | dotnet_diagnostic.IDE0046.severity = suggestion
179 | dotnet_style_prefer_compound_assignment = true:warning
180 |
181 | dotnet_diagnostic.CA1848.severity = suggestion
182 | # Null-checking preferences
183 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences
184 | dotnet_style_coalesce_expression = true:warning
185 | dotnet_style_null_propagation = true:warning
186 | # Parameter preferences
187 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences
188 | dotnet_code_quality_unused_parameters = all:warning
189 | # More style options (Undocumented)
190 | # https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641
191 | dotnet_style_operator_placement_when_wrapping = end_of_line
192 | # https://github.com/dotnet/roslyn/pull/40070
193 | dotnet_style_prefer_simplified_interpolation = true:warning
194 |
195 | # C# Code Style Settings
196 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
197 | [*.{cs,csx,cake}]
198 | # Implicit and explicit types
199 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types
200 | csharp_style_var_for_built_in_types = true:warning
201 | csharp_style_var_when_type_is_apparent = true:warning
202 | csharp_style_var_elsewhere = true:warning
203 | # Expression-bodied members
204 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members
205 | csharp_style_expression_bodied_methods = true:silent
206 | csharp_style_expression_bodied_constructors = true:warning
207 | csharp_style_expression_bodied_operators = true:warning
208 | csharp_style_expression_bodied_properties = true:warning
209 | csharp_style_expression_bodied_indexers = true:warning
210 | csharp_style_expression_bodied_accessors = true:warning
211 | csharp_style_expression_bodied_lambdas = true:warning
212 | csharp_style_expression_bodied_local_functions = true:warning
213 | # Pattern matching
214 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching
215 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning
216 | csharp_style_pattern_matching_over_as_with_null_check = true:warning
217 | # Inlined variable declarations
218 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations
219 | csharp_style_inlined_variable_declaration = true:warning
220 | # Expression-level preferences
221 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
222 | csharp_prefer_simple_default_expression = true:warning
223 | # "Null" checking preferences
224 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences
225 | csharp_style_throw_expression = true:warning
226 | csharp_style_conditional_delegate_call = true:warning
227 | # Code block preferences
228 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences
229 | csharp_prefer_braces = true:warning
230 | # Unused value preferences
231 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences
232 | csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
233 | dotnet_diagnostic.IDE0058.severity = suggestion
234 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
235 | dotnet_diagnostic.IDE0059.severity = suggestion
236 | # Index and range preferences
237 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences
238 | csharp_style_prefer_index_operator = true:warning
239 | csharp_style_prefer_range_operator = true:warning
240 | # Miscellaneous preferences
241 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences
242 | csharp_style_deconstructed_variable_declaration = true:warning
243 | csharp_style_pattern_local_over_anonymous_function = true:warning
244 | csharp_using_directive_placement = inside_namespace:warning
245 | csharp_prefer_static_local_function = true:warning
246 | csharp_prefer_simple_using_statement = true:suggestion
247 | dotnet_diagnostic.IDE0063.severity = suggestion
248 | ##########################################
249 | # .NET Formatting Conventions
250 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions
251 | ##########################################
252 |
253 | # Organize usings
254 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives
255 | dotnet_sort_system_directives_first = true
256 | # Newline options
257 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options
258 | csharp_new_line_before_open_brace = all
259 | csharp_new_line_before_else = true
260 | csharp_new_line_before_catch = true
261 | csharp_new_line_before_finally = true
262 | csharp_new_line_before_members_in_object_initializers = true
263 | csharp_new_line_before_members_in_anonymous_types = true
264 | csharp_new_line_between_query_expression_clauses = true
265 | # Indentation options
266 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options
267 | csharp_indent_case_contents = true
268 | csharp_indent_switch_labels = true
269 | csharp_indent_labels = no_change
270 | csharp_indent_block_contents = true
271 | csharp_indent_braces = false
272 | csharp_indent_case_contents_when_block = false
273 | # Spacing options
274 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options
275 | csharp_space_after_cast = false
276 | csharp_space_after_keywords_in_control_flow_statements = true
277 | csharp_space_between_parentheses = false
278 | csharp_space_before_colon_in_inheritance_clause = true
279 | csharp_space_after_colon_in_inheritance_clause = true
280 | csharp_space_around_binary_operators = before_and_after
281 | csharp_space_between_method_declaration_parameter_list_parentheses = false
282 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
283 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
284 | csharp_space_between_method_call_parameter_list_parentheses = false
285 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
286 | csharp_space_between_method_call_name_and_opening_parenthesis = false
287 | csharp_space_after_comma = true
288 | csharp_space_before_comma = false
289 | csharp_space_after_dot = false
290 | csharp_space_before_dot = false
291 | csharp_space_after_semicolon_in_for_statement = true
292 | csharp_space_before_semicolon_in_for_statement = false
293 | csharp_space_around_declaration_statements = false
294 | csharp_space_before_open_square_brackets = false
295 | csharp_space_between_empty_square_brackets = false
296 | csharp_space_between_square_brackets = false
297 | # Wrapping options
298 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options
299 | csharp_preserve_single_line_statements = false
300 | csharp_preserve_single_line_blocks = true
301 |
302 | ##########################################
303 | # .NET Naming Conventions
304 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions
305 | ##########################################
306 |
307 | [*.{cs,csx,cake,vb,vbx}]
308 |
309 | ##########################################
310 | # Styles
311 | ##########################################
312 |
313 | # camel_case_style - Define the camelCase style
314 | dotnet_naming_style.camel_case_style.capitalization = camel_case
315 | # pascal_case_style - Define the PascalCase style
316 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
317 | # first_upper_style - The first character must start with an upper-case character
318 | dotnet_naming_style.first_upper_style.capitalization = first_word_upper
319 | # prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I'
320 | dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
321 | dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
322 | # prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T'
323 | dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
324 | dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
325 | # disallowed_style - Anything that has this style applied is marked as disallowed
326 | dotnet_naming_style.disallowed_style.capitalization = pascal_case
327 | dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
328 | dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
329 | # internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file
330 | dotnet_naming_style.internal_error_style.capitalization = pascal_case
331 | dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
332 | dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____
333 |
334 | ##########################################
335 | # .NET Design Guideline Field Naming Rules
336 | # Naming rules for fields follow the .NET Framework design guidelines
337 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/index
338 | ##########################################
339 |
340 | # All public/protected/protected_internal constant fields must be PascalCase
341 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field
342 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal
343 | dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const
344 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field
345 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group
346 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
347 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning
348 |
349 | # All public/protected/protected_internal static readonly fields must be PascalCase
350 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field
351 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal
352 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly
353 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field
354 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group
355 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
356 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning
357 |
358 | # No other public/protected/protected_internal fields are allowed
359 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field
360 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal
361 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field
362 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group
363 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style
364 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error
365 |
366 | ##########################################
367 | # StyleCop Field Naming Rules
368 | # Naming rules for fields follow the StyleCop analyzers
369 | # This does not override any rules using disallowed_style above
370 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers
371 | ##########################################
372 |
373 | # All constant fields must be PascalCase
374 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md
375 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
376 | dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const
377 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field
378 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group
379 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
380 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning
381 |
382 | # All static readonly fields must be PascalCase
383 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md
384 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
385 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly
386 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field
387 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group
388 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
389 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning
390 |
391 | # No non-private instance fields are allowed
392 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md
393 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
394 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field
395 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group
396 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style
397 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error
398 |
399 | # Private fields must be camelCase
400 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md
401 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private
402 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field
403 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group
404 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style
405 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning
406 |
407 | # Local variables must be camelCase
408 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
409 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local
410 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local
411 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group
412 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style
413 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent
414 |
415 | # This rule should never fire. However, it's included for at least two purposes:
416 | # First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers.
417 | # Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#).
418 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = *
419 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field
420 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group
421 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style
422 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error
423 |
424 |
425 | ##########################################
426 | # Other Naming Rules
427 | ##########################################
428 |
429 | # All of the following must be PascalCase:
430 | # - Namespaces
431 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces
432 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
433 | # - Classes and Enumerations
434 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
435 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
436 | # - Delegates
437 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types
438 | # - Constructors, Properties, Events, Methods
439 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members
440 | dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
441 | dotnet_naming_rule.element_rule.symbols = element_group
442 | dotnet_naming_rule.element_rule.style = pascal_case_style
443 | dotnet_naming_rule.element_rule.severity = warning
444 |
445 | # Interfaces use PascalCase and are prefixed with uppercase 'I'
446 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
447 | dotnet_naming_symbols.interface_group.applicable_kinds = interface
448 | dotnet_naming_rule.interface_rule.symbols = interface_group
449 | dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style
450 | dotnet_naming_rule.interface_rule.severity = warning
451 |
452 | # Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
453 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
454 | dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
455 | dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
456 | dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
457 | dotnet_naming_rule.type_parameter_rule.severity = warning
458 |
459 | # Function parameters use camelCase
460 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters
461 | dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
462 | dotnet_naming_rule.parameters_rule.symbols = parameters_group
463 | dotnet_naming_rule.parameters_rule.style = camel_case_style
464 | dotnet_naming_rule.parameters_rule.severity = warning
465 |
466 | # IDE0130: Namespace does not match folder structure
467 | dotnet_diagnostic.IDE0130.severity = suggestion
468 |
469 | # CA1062: Validate arguments of public methods
470 | dotnet_diagnostic.CA1062.severity = suggestion
471 |
472 | # CS1591: Missing XML comment for publicly visible type or member
473 | dotnet_diagnostic.CS1591.severity = suggestion
474 |
475 | # IDE0022: Use expression body for method
476 | dotnet_diagnostic.IDE0022.severity = silent
477 |
478 | # CA1822: Mark members as static
479 | dotnet_diagnostic.CA1822.severity = suggestion
480 |
481 | # https://github.com/dennisdoomen/CSharpGuidelines/tree/5.7.0/_rules
482 | dotnet_diagnostic.AV1505.severity=none
483 | dotnet_diagnostic.AV2305.severity=none
484 | dotnet_diagnostic.AV1008.severity=none
485 | dotnet_diagnostic.AV1555.severity=none
486 | dotnet_diagnostic.AV1553.severity=none
487 | dotnet_diagnostic.AV1568.severity=none
488 | dotnet_diagnostic.AV1745.severity=none
489 | dotnet_diagnostic.AV1708.severity=none
490 | dotnet_diagnostic.AV1580.severity=none
491 |
492 | dotnet_diagnostic.AV1564.severity=warning
493 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Hangfire MCP [](https://github.com/NikiforovAll/hangfire-mcp/actions/workflows/build.yml)
2 |
3 | Enqueue background jobs using Hangfire MCP server.
4 |
5 | ## Motivation
6 |
7 | Interaction with *Hangfire* using *Hangfire MCP Server* allows you to enqueue jobs from any client that supports MCP protocol.
8 | For example, you can use Hangfire MCP directly from *VS Code* in *Agent Mode* and enqueue jobs. It makes possible to execute any kind of code without writing additional code.
9 |
10 | <video src="https://github.com/user-attachments/assets/e6abc036-b1f9-4691-a829-65292db5b5e6" controls="controls"></video>
11 |
12 | Here is MCP Server configuration for VS Code:
13 |
14 | ```json
15 | {
16 | "servers": {
17 | "hangfire-mcp": {
18 | "url": "http://localhost:3001"
19 | }
20 | }
21 | }
22 | ```
23 |
24 | ## Code Example
25 |
26 | Here is how it works:
27 |
28 | ```mermaid
29 | sequenceDiagram
30 | participant User as User
31 | participant MCPHangfire as MCP Hangfire
32 | participant IBackgroundJobClient as IBackgroundJobClient
33 | participant Database as Database
34 | participant HangfireServer as Hangfire Server
35 |
36 | User->>MCPHangfire: Enqueue Job
37 | MCPHangfire->>IBackgroundJobClient: Send Job Message
38 | IBackgroundJobClient->>Database: Store Job Message
39 | HangfireServer->>Database: Fetch Job Message
40 | HangfireServer->>HangfireServer: Process Job
41 | ```
42 |
43 | ## Standalone Mode
44 |
45 | It is a regular MCP packaged as .NET global tool. Here is how to setup it as an MCP server in VSCode.
46 |
47 | ```bash
48 | dotnet tool install --global --add-source Nall.HangfireMCP
49 | ```
50 |
51 | Configuration:
52 |
53 | ```json
54 | {
55 | "servers": {
56 | "hangfire-mcp-standalone": {
57 | "type": "stdio",
58 | "command": "HangfireMCP",
59 | "args": [
60 | "--stdio"
61 | ],
62 | "env": {
63 | "HANGFIRE_JOBS_ASSEMBLY": "path/to/Jobs.dll",
64 | "HANGFIRE_JOBS_MATCH_EXPRESSION": "[?IsInterface && contains(Name, 'Job')]",
65 | "HANGFIRE_CONNECTION_STRING": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=hangfire"
66 | }
67 | }
68 | }
69 | }
70 | ```
71 |
72 | ### Aspire
73 |
74 | ```csharp
75 | var builder = DistributedApplication.CreateBuilder(args);
76 |
77 | var postgresServer = builder
78 | .AddPostgres("postgres-server")
79 | .WithDataVolume()
80 | .WithLifetime(ContainerLifetime.Persistent);
81 |
82 | var postgresDatabase = postgresServer.AddDatabase("hangfire");
83 |
84 | builder.AddProject<Projects.Web>("server")
85 | .WithReference(postgresDatabase)
86 | .WaitFor(postgresDatabase);
87 |
88 | var mcp = builder
89 | .AddProject<Projects.HangfireMCP_Standalone>("hangfire-mcp")
90 | .WithEnvironment("HANGFIRE_JOBS_ASSEMBLY", "path/to/Jobs.dll")
91 | .WithEnvironment("HANGFIRE_JOBS_MATCH_EXPRESSION", "[?IsInterface && contains(Name, 'Job')]")
92 | .WithReference(postgresDatabase)
93 | .WaitFor(postgresDatabase);
94 |
95 | builder
96 | .AddMCPInspector()
97 | .WithSSE(mcp)
98 | .WaitFor(mcp);
99 |
100 | builder.Build().Run();
101 | ```
102 |
103 | As result, the jobs are dynamically loaded from the specified assembly and can be enqueued using MCP protocol. The rules for matching job names can be specified using `HANGFIRE_JOBS_MATCH_EXPRESSION` environment variable. For example, the expression `[?IsInterface && contains(Name, 'Job')]` will match all interfaces that contain "Job" in their name. It is a [JMESPath](https://jmespath.org/tutorial.html) expression, so you can define how to match job names according to your needs.
104 |
105 | ## Custom Setup (as Code) Mode
106 |
107 | You can create your own MCP server and use this project as starting point. You can extend it with your own tools and features. Here is an example of how to set up Hangfire MCP server in a custom project.
108 |
109 | ### Aspire
110 |
111 | ```csharp
112 | var builder = DistributedApplication.CreateBuilder(args);
113 |
114 | var postgresServer = builder
115 | .AddPostgres("postgres-server")
116 | .WithDataVolume()
117 | .WithLifetime(ContainerLifetime.Persistent);
118 |
119 | var postgresDatabase = postgresServer.AddDatabase("hangfire");
120 |
121 | builder.AddProject<Projects.Web>("server")
122 | .WithReference(postgresDatabase)
123 | .WaitFor(postgresDatabase);
124 |
125 | var mcp = builder
126 | .AddProject<Projects.HangfireMCP>("hangfire-mcp")
127 | .WithReference(postgresDatabase)
128 | .WaitFor(postgresDatabase);
129 |
130 | builder
131 | .AddMCPInspector()
132 | .WithSSE(mcp)
133 | .WaitFor(mcp);
134 |
135 | builder.Build().Run();
136 | ```
137 |
138 | 
139 |
140 | ### MCP Server
141 |
142 | ```csharp
143 | var builder = WebApplication.CreateBuilder(args);
144 |
145 | builder.WithMcpServer(args).WithToolsFromAssembly();
146 | builder.Services.AddHangfire(cfg => cfg.UsePostgreSqlStorage(options =>
147 | options.UseNpgsqlConnection(builder.Configuration.GetConnectionString("hangfire")))
148 | );
149 | builder.Services.AddHangfireMcp();
150 | builder.Services.AddTransient<HangfireTool>();
151 | var app = builder.Build();
152 | app.MapMcpServer(args);
153 | app.Run();
154 | ```
155 |
156 | Here is an example of the Hangfire tool:
157 |
158 | ```csharp
159 | [McpServerToolType]
160 | public class HangfireTool(IHangfireDynamicScheduler scheduler)
161 | {
162 | [McpServerTool(Name = "RunJob")]
163 | public string Run(
164 | [Required] string jobName,
165 | [Required] string methodName,
166 | Dictionary<string, object>? parameters = null
167 | )
168 | {
169 | var descriptor = new JobDescriptor(jobName, methodName, parameters);
170 | return scheduler.Enqueue(descriptor, typeof(ITimeJob).Assembly);
171 | }
172 | }
173 | ```
174 |
175 | ## Tools
176 |
177 | 
178 |
```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
```markdown
1 | MIT License
2 |
3 | Copyright (c) 2024 Oleksii Nikiforov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
```
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | [email protected].
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {}
4 | }
5 | }
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {}
4 | }
5 | }
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "[csharp]": {
3 | "editor.defaultFormatter": "csharpier.csharpier-vscode"
4 | }
5 | }
6 |
```
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "sdk": {
3 | "version": "9.0.201",
4 | "rollForward": "latestMajor",
5 | "allowPrerelease": false
6 | }
7 | }
8 |
```
--------------------------------------------------------------------------------
/samples/Web/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/appsettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/appsettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
```
--------------------------------------------------------------------------------
/samples/Web/appsettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
```
--------------------------------------------------------------------------------
/samples/AppHost/appsettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning",
6 | "Aspire.Hosting.Dcp": "Warning"
7 | }
8 | }
9 | }
10 |
```
--------------------------------------------------------------------------------
/samples/AppHost/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "Parameters": {
9 | "pg-username": "postgres",
10 | "pg-password": "postgres"
11 | }
12 | }
13 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/Program.cs:
--------------------------------------------------------------------------------
```csharp
1 | using HangfireMCP;
2 | using Nall.Hangfire.Mcp;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 | builder.WithMcpServer(args).WithToolsFromAssembly();
6 | builder.AddHangfire();
7 | builder.Services.AddHangfireMcp();
8 | builder.Services.AddTransient<HangfireTool>();
9 | var app = builder.Build();
10 | app.MapMcpServer(args);
11 | app.Run();
12 |
```
--------------------------------------------------------------------------------
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "version": 1,
3 | "isRoot": true,
4 | "tools": {
5 | "cake.tool": {
6 | "version": "5.0.0",
7 | "commands": [
8 | "dotnet-cake"
9 | ],
10 | "rollForward": false
11 | },
12 | "csharpier": {
13 | "version": "1.0.1",
14 | "commands": [
15 | "csharpier"
16 | ],
17 | "rollForward": false
18 | }
19 | }
20 | }
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/JobDescriptor.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace Nall.Hangfire.Mcp;
2 |
3 | public class JobDescriptor(
4 | string jobName,
5 | string methodName,
6 | IDictionary<string, object>? parameters = null
7 | )
8 | {
9 | public string JobName { get; } = jobName;
10 | public string MethodName { get; } = methodName;
11 | public IDictionary<string, object>? Parameters { get; } = parameters;
12 | }
13 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/Program.cs:
--------------------------------------------------------------------------------
```csharp
1 | using HangfireMCP;
2 | using Nall.Hangfire.Mcp;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 | builder.WithMcpServer(args).WithToolsFromAssembly();
6 | builder.AddHangfire();
7 | builder.Services.AddHangfireMcp();
8 | builder.Services.AddTransient<HangfireTool>();
9 | builder.Services.AddHostedService<DynamicJobLoaderHostedService>();
10 |
11 | var app = builder.Build();
12 | app.MapMcpServer(args);
13 | app.Run();
14 |
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace Nall.Hangfire.Mcp;
2 |
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | public static class ServiceCollectionExtensions
6 | {
7 | public static IServiceCollection AddHangfireMcp(this IServiceCollection services)
8 | {
9 | services.AddSingleton<IHangfireDynamicScheduler, HangfireDynamicScheduler>();
10 | services.AddSingleton<DynamicJobLoader>();
11 |
12 | return services;
13 | }
14 | }
15 |
```
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request_target:
8 | types:
9 | - edited
10 | - opened
11 | - reopened
12 | - synchronize
13 | workflow_dispatch:
14 |
15 | jobs:
16 | update_release_draft:
17 | permissions:
18 | contents: write
19 | pull-requests: read
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: "Draft Release"
23 | uses: release-drafter/[email protected]
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 |
```
--------------------------------------------------------------------------------
/samples/HangfireJobs/ITimeJob.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireJobs;
2 |
3 | using System.Globalization;
4 | using Microsoft.Extensions.Logging;
5 |
6 | public interface ITimeJob
7 | {
8 | public Task ExecuteAsync();
9 | }
10 |
11 | public class TimeJob(TimeProvider timeProvider, ILogger<TimeJob> logger) : ITimeJob
12 | {
13 | public Task ExecuteAsync()
14 | {
15 | logger.LogInformation(
16 | "Current time: {CurrentTime}",
17 | timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
18 | );
19 | return Task.CompletedTask;
20 | }
21 | }
22 |
```
--------------------------------------------------------------------------------
/mcp_example.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "servers": {
3 | "hangfire-mcp-standalone": {
4 | "type": "stdio",
5 | "command": "HangfireMCP",
6 | "args": [
7 | "--stdio"
8 | ],
9 | "env": {
10 | "HANGFIRE_JOBS_ASSEMBLY": "C:\\Users\\Oleksii_Nikiforov\\dev\\hangfire-mcp\\samples\\HangfireMCP.Standalone\\bin\\Debug\\net9.0\\HangfireJobs.dll",
11 | "HANGFIRE_JOBS_MATCH_EXPRESSION": "[?IsInterface && contains(Name, 'Job')]",
12 | "HANGFIRE_CONNECTION_STRING": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=hangfire"
13 | }
14 | }
15 | }
16 | }
17 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "http://localhost:3001",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": false,
17 | "applicationUrl": "https://localhost:7133;http://localhost:3001",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "http://localhost:3001",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": false,
17 | "applicationUrl": "https://localhost:7133;http://localhost:3001",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
```
--------------------------------------------------------------------------------
/samples/Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "http": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "http://localhost:5296",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development"
11 | }
12 | },
13 | "https": {
14 | "commandName": "Project",
15 | "dotnetRunMessages": true,
16 | "launchBrowser": false,
17 | "applicationUrl": "https://localhost:7211;http://localhost:5296",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | }
22 | }
23 | }
24 |
```
--------------------------------------------------------------------------------
/samples/HangfireJobs/ISendMessageJob.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireJobs;
2 |
3 | using Microsoft.Extensions.Logging;
4 |
5 | public interface ISendMessageJob
6 | {
7 | public Task ExecuteAsync(Message message);
8 | public Task ExecuteAsync(string text);
9 | }
10 |
11 | public class Message
12 | {
13 | public string Subject { get; set; } = string.Empty;
14 | public string Text { get; set; } = string.Empty;
15 | }
16 |
17 | public class SendMessageJob(ILogger<SendMessageJob> logger) : ISendMessageJob
18 | {
19 | public Task ExecuteAsync(string text)
20 | {
21 | logger.LogInformation("Text: {Text}", text);
22 | return Task.CompletedTask;
23 | }
24 |
25 | public Task ExecuteAsync(Message message)
26 | {
27 | logger.LogInformation("Subject: {Subject}, Text: {Text}", message.Subject, message.Text);
28 | return Task.CompletedTask;
29 | }
30 | }
31 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/HangfireExtensions.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireMCP;
2 |
3 | using System.Globalization;
4 | using Hangfire;
5 | using Hangfire.PostgreSql;
6 | using HangfireMCP;
7 |
8 | public static class HangfireExtensions
9 | {
10 | public static void AddHangfire(this WebApplicationBuilder builder)
11 | {
12 | ArgumentNullException.ThrowIfNull(builder);
13 |
14 | var defaultCulture = CultureInfo.InvariantCulture;
15 | GlobalConfiguration.Configuration.UseDefaultCulture(
16 | culture: defaultCulture,
17 | uiCulture: defaultCulture,
18 | captureDefault: false
19 | );
20 |
21 | builder.Services.AddHangfire(globalConfiguration =>
22 | globalConfiguration.UsePostgreSqlStorage(options =>
23 | options.UseNpgsqlConnection(builder.Configuration.GetConnectionString("hangfire"))
24 | )
25 | );
26 | }
27 | }
28 |
```
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
```
1 | <Project>
2 | <PropertyGroup>
3 | <PackageProjectUrl>https://github.com/NikiforovAll/hangfire-mcp</PackageProjectUrl>
4 | <RepositoryUrl>https://github.com/NikiforovAll/hangfire-mcp</RepositoryUrl>
5 | <RepositoryType>git</RepositoryType>
6 | <PackageLicenseExpression>MIT</PackageLicenseExpression>
7 | <PackageIcon>logo.png</PackageIcon>
8 | <Authors>Nikiforov Oleksii</Authors>
9 | <Description>Hangfire MCP Server</Description>
10 | <PackageTags>mcp;hangfire;ai</PackageTags>
11 | <PackageReadmeFile>README.md</PackageReadmeFile>
12 | </PropertyGroup>
13 |
14 | <PropertyGroup>
15 | <RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))</RepoRoot>
16 | </PropertyGroup>
17 |
18 | <ItemGroup>
19 | <None Include="$(RepoRoot)\assets\logo.png" Pack="true" PackagePath="\" />
20 | </ItemGroup>
21 |
22 |
23 | <ItemGroup>
24 | <None Include="$(RepoRoot)\README.md" Pack="true" PackagePath="" />
25 | </ItemGroup>
26 | </Project>
27 |
```
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
```yaml
1 | name-template: "$RESOLVED_VERSION"
2 | tag-template: "$RESOLVED_VERSION"
3 | change-template: "- $TITLE by @$AUTHOR (#$NUMBER)"
4 | no-changes-template: "- No changes"
5 | categories:
6 | - title: "📚 Documentation"
7 | labels:
8 | - "documentation"
9 | - title: "🚀 New Features"
10 | labels:
11 | - "enhancement"
12 | - title: "🐛 Bug Fixes"
13 | labels:
14 | - "bug"
15 | - title: "🧰 Maintenance"
16 | labels:
17 | - "maintenance"
18 | version-resolver:
19 | major:
20 | labels:
21 | - "major"
22 | minor:
23 | labels:
24 | - "minor"
25 | patch:
26 | labels:
27 | - "patch"
28 | default: patch
29 | template: |
30 | $CHANGES
31 |
32 | ## 👨🏼💻 Contributors
33 |
34 | $CONTRIBUTORS
35 | autolabeler:
36 | - label: "documentation"
37 | files:
38 | - "**/*.md"
39 | - label: "enhancement"
40 | files:
41 | - "Source/**/*"
42 | - label: "maintenance"
43 | files:
44 | - ".github/**/*"
45 | - "Benchmarks/**/*"
46 | - "Images/**/*"
47 | - "Tests/**/*"
48 | - "Tools/**/*"
49 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/HangfireExtensions.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireMCP;
2 |
3 | using System.Globalization;
4 | using Hangfire;
5 | using Hangfire.PostgreSql;
6 | using HangfireMCP;
7 |
8 | public static class HangfireExtensions
9 | {
10 | public static void AddHangfire(this WebApplicationBuilder builder)
11 | {
12 | ArgumentNullException.ThrowIfNull(builder);
13 |
14 | var defaultCulture = CultureInfo.InvariantCulture;
15 | GlobalConfiguration.Configuration.UseDefaultCulture(
16 | culture: defaultCulture,
17 | uiCulture: defaultCulture,
18 | captureDefault: false
19 | );
20 |
21 | var connectionString =
22 | Environment.GetEnvironmentVariable("HANGFIRE_CONNECTION_STRING")
23 | ?? builder.Configuration.GetConnectionString("hangfire");
24 |
25 | builder.Services.AddHangfire(globalConfiguration =>
26 | globalConfiguration.UsePostgreSqlStorage(options =>
27 | options.UseNpgsqlConnection(connectionString)
28 | )
29 | );
30 | }
31 | }
32 |
```
--------------------------------------------------------------------------------
/samples/Web/HangfireExtensions.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace Web;
2 |
3 | using System.Globalization;
4 | using Hangfire;
5 | using Hangfire.PostgreSql;
6 | using Web;
7 |
8 | public static class HangfireExtensions
9 | {
10 | public static void AddHangfireServer(this WebApplicationBuilder builder)
11 | {
12 | ArgumentNullException.ThrowIfNull(builder);
13 |
14 | var defaultCulture = CultureInfo.InvariantCulture;
15 | GlobalConfiguration.Configuration.UseDefaultCulture(
16 | culture: defaultCulture,
17 | uiCulture: defaultCulture,
18 | captureDefault: false
19 | );
20 |
21 | builder.Services.AddHangfireServer();
22 |
23 | builder.Services.AddHangfire(globalConfiguration =>
24 | globalConfiguration
25 | .UseFilter(new AutomaticRetryAttribute { Attempts = 0 })
26 | .UsePostgreSqlStorage(options =>
27 | options.UseNpgsqlConnection(
28 | builder.Configuration.GetConnectionString("hangfire")
29 | )
30 | )
31 | );
32 | }
33 | }
34 |
```
--------------------------------------------------------------------------------
/samples/AppHost/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "https": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": true,
7 | "launchBrowser": false,
8 | "applicationUrl": "https://localhost:18888;http://localhost:18887",
9 | "environmentVariables": {
10 | "ASPNETCORE_ENVIRONMENT": "Development",
11 | "DOTNET_ENVIRONMENT": "Development",
12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21141",
13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22060"
14 | }
15 | },
16 | "http": {
17 | "commandName": "Project",
18 | "dotnetRunMessages": true,
19 | "launchBrowser": false,
20 | "applicationUrl": "http://localhost:18887",
21 | "environmentVariables": {
22 | "ASPNETCORE_ENVIRONMENT": "Development",
23 | "DOTNET_ENVIRONMENT": "Development",
24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19176",
25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20005"
26 | }
27 | }
28 | }
29 | }
30 |
```
--------------------------------------------------------------------------------
/samples/AppHost/Program.cs:
--------------------------------------------------------------------------------
```csharp
1 | var builder = DistributedApplication.CreateBuilder(args);
2 |
3 | var pgUser = builder.AddParameter("pg-username");
4 | var pgPassword = builder.AddParameter("pg-password", secret: true);
5 |
6 | var postgresServer = builder
7 | .AddPostgres("postgres-server", pgUser, pgPassword, 5432)
8 | .WithImageTag("15.7")
9 | .WithDataVolume()
10 | .WithLifetime(ContainerLifetime.Persistent);
11 |
12 | var postgresDatabase = postgresServer
13 | .AddDatabase("hangfire")
14 | .WithCreationScript(
15 | """
16 | CREATE DATABASE hangfire;
17 | """
18 | );
19 |
20 | builder
21 | .AddProject<Projects.Web>("server")
22 | .WithReference(postgresDatabase)
23 | .WaitFor(postgresDatabase);
24 |
25 | var mcp = builder
26 | .AddProject<Projects.HangfireMCP_Standalone>("hangfire-mcp")
27 | .WithEnvironment(
28 | "HANGFIRE_JOBS_ASSEMBLY",
29 | @"C:\Users\Oleksii_Nikiforov\dev\hangfire-mcp\samples\HangfireMCP.Standalone\bin\Debug\net9.0\HangfireJobs.dll"
30 | )
31 | .WithEnvironment("HANGFIRE_JOBS_MATCH_EXPRESSION", "[?IsInterface && contains(Name, 'Job')]")
32 | .WithReference(postgresDatabase)
33 | .WaitFor(postgresDatabase);
34 |
35 | builder.AddMCPInspector().WithSSE(mcp).WithParentRelationship(mcp);
36 |
37 | builder.Build().Run();
38 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/McpServerExtensions.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireMCP;
2 |
3 | public static class McpServerExtensions
4 | {
5 | public static IMcpServerBuilder WithMcpServer(this WebApplicationBuilder builder, string[] args)
6 | {
7 | var isStdio = args.Contains("--stdio");
8 |
9 | if (isStdio)
10 | {
11 | builder.WebHost.UseUrls("http://*:0"); // random port
12 |
13 | // logs from stderr are shown in the inspector
14 | builder.Services.AddLogging(builder =>
15 | builder
16 | .AddConsole(consoleBuilder =>
17 | {
18 | consoleBuilder.LogToStandardErrorThreshold = LogLevel.Trace;
19 | consoleBuilder.FormatterName = "json";
20 | })
21 | .AddFilter(null, LogLevel.Warning)
22 | );
23 | }
24 |
25 | var mcpBuilder = isStdio
26 | ? builder.Services.AddMcpServer().WithStdioServerTransport()
27 | : builder.Services.AddMcpServer().WithHttpTransport();
28 |
29 | return mcpBuilder;
30 | }
31 |
32 | public static WebApplication MapMcpServer(this WebApplication app, string[] args)
33 | {
34 | var isSse = !args.Contains("--stdio");
35 |
36 | if (isSse)
37 | {
38 | app.MapMcp();
39 | }
40 |
41 | return app;
42 | }
43 | }
44 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/McpServerExtensions.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireMCP;
2 |
3 | public static class McpServerExtensions
4 | {
5 | public static IMcpServerBuilder WithMcpServer(this WebApplicationBuilder builder, string[] args)
6 | {
7 | var isStdio = args.Contains("--stdio");
8 |
9 | if (isStdio)
10 | {
11 | builder.WebHost.UseUrls("http://*:0"); // random port
12 |
13 | // logs from stderr are shown in the inspector
14 | builder.Services.AddLogging(builder =>
15 | builder
16 | .AddConsole(consoleBuilder =>
17 | {
18 | consoleBuilder.LogToStandardErrorThreshold = LogLevel.Trace;
19 | consoleBuilder.FormatterName = "json";
20 | })
21 | .AddFilter(null, LogLevel.Warning)
22 | );
23 | }
24 |
25 | var mcpBuilder = isStdio
26 | ? builder.Services.AddMcpServer().WithStdioServerTransport()
27 | : builder.Services.AddMcpServer().WithHttpTransport();
28 |
29 | return mcpBuilder;
30 | }
31 |
32 | public static WebApplication MapMcpServer(this WebApplication app, string[] args)
33 | {
34 | var isSse = !args.Contains("--stdio");
35 |
36 | if (isSse)
37 | {
38 | app.MapMcp();
39 | }
40 |
41 | return app;
42 | }
43 | }
44 |
```
--------------------------------------------------------------------------------
/samples/Web/Program.cs:
--------------------------------------------------------------------------------
```csharp
1 | using Hangfire;
2 | using HangfireJobs;
3 | using Nall.Hangfire.Mcp;
4 | using Web;
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 | builder.AddHangfireServer();
8 | builder.Services.AddSingleton(TimeProvider.System);
9 | builder.Services.AddTransient<ITimeJob, TimeJob>();
10 | builder.Services.AddTransient<ISendMessageJob, SendMessageJob>();
11 | builder.Services.AddHangfireMcp();
12 | builder.Services.AddProblemDetails();
13 | var app = builder.Build();
14 | app.UseHttpsRedirection();
15 |
16 | app.MapHangfireDashboard(string.Empty);
17 |
18 | app.MapPost(
19 | "/jobs",
20 | (JobDescriptor jobDescriptor, IHangfireDynamicScheduler scheduler) =>
21 | {
22 | var jobId = scheduler.Enqueue(jobDescriptor, typeof(ITimeJob).Assembly);
23 |
24 | return Results.Ok($"Job executed successfully with ID: {jobId}");
25 | }
26 | );
27 |
28 | app.MapGet(
29 | "/jobs",
30 | (IHangfireDynamicScheduler scheduler, string? searchTerm) =>
31 | {
32 | var jobs = scheduler.DiscoverJobs(
33 | type =>
34 | type.IsInterface && type.Name.EndsWith("Job", StringComparison.OrdinalIgnoreCase),
35 | (type, method) =>
36 | string.IsNullOrEmpty(searchTerm)
37 | || $"{type.Name}.{method.Name}".Contains(
38 | searchTerm,
39 | StringComparison.OrdinalIgnoreCase
40 | ),
41 | typeof(ITimeJob).Assembly
42 | );
43 |
44 | return Results.Ok(jobs);
45 | }
46 | );
47 |
48 | app.Run();
49 |
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/DynamicJobLoaderHostedService.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace Nall.Hangfire.Mcp;
2 |
3 | using Microsoft.Extensions.Hosting;
4 | using Microsoft.Extensions.Logging;
5 |
6 | /// <summary>
7 | /// Hosted service that initializes and manages the dynamic job loader.
8 | /// </summary>
9 | public class DynamicJobLoaderHostedService(
10 | DynamicJobLoader dynamicJobLoader,
11 | ILogger<DynamicJobLoaderHostedService> logger
12 | ) : IHostedService
13 | {
14 | public Task StartAsync(CancellationToken cancellationToken)
15 | {
16 | logger.LogInformation("Starting dynamic job loader service");
17 |
18 | try
19 | {
20 | var initialized = dynamicJobLoader.Initialize();
21 | if (initialized)
22 | {
23 | logger.LogInformation("Dynamic job loader initialized successfully");
24 |
25 | // Discover jobs on startup
26 | var jobs = dynamicJobLoader.DiscoverJobs();
27 | logger.LogInformation("Discovered {Count} jobs on startup", jobs.Count());
28 | }
29 | else
30 | {
31 | logger.LogWarning("Dynamic job loader initialization failed or was not configured");
32 | }
33 | }
34 | catch (Exception ex)
35 | {
36 | logger.LogError(ex, "Error during dynamic job loader startup");
37 | }
38 |
39 | return Task.CompletedTask;
40 | }
41 |
42 | public Task StopAsync(CancellationToken cancellationToken)
43 | {
44 | logger.LogInformation("Stopping dynamic job loader service");
45 | return Task.CompletedTask;
46 | }
47 | }
48 |
```
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "inputs": [],
6 | "tasks": [
7 | {
8 | "label": "build",
9 | "command": "dotnet",
10 | "type": "shell",
11 | "args": [
12 | "build"
13 | ],
14 | "group": {
15 | "kind": "build",
16 | "isDefault": false
17 | },
18 | "presentation": {
19 | "reveal": "always",
20 | "revealProblems": "onProblem"
21 | },
22 | // "problemMatcher": "$msCompile",
23 | "detail": "Builds the solution ⚙️",
24 | "icon": {
25 | "color": "terminal.ansiGreen"
26 | }
27 | },
28 | {
29 | "label": "cake:build",
30 | "command": "dotnet",
31 | "type": "shell",
32 | "args": [
33 | "cake",
34 | "--target",
35 | "build"
36 | ],
37 | "group": {
38 | "kind": "build",
39 | "isDefault": true
40 | },
41 | "presentation": {
42 | "reveal": "always",
43 | "revealProblems": "onProblem"
44 | },
45 | "problemMatcher": "$msCompile",
46 | "detail": "Builds the solution ⚙️",
47 | "icon": {
48 | "color": "terminal.ansiGreen"
49 | }
50 | },
51 | {
52 | "label": "cake:test",
53 | "command": "dotnet",
54 | "type": "shell",
55 | "args": [
56 | "cake",
57 | "--target",
58 | "test"
59 | ],
60 | "group": {
61 | "kind": "test",
62 | "isDefault": true
63 | },
64 | "presentation": {
65 | "reveal": "always",
66 | "revealProblems": "onProblem"
67 | },
68 | "problemMatcher": "$msCompile",
69 | "detail": "Tests the solution 🧪",
70 | "icon": {
71 | "color": "terminal.ansiYellow"
72 | }
73 | },
74 | {
75 | "label": "cake:pack",
76 | "command": "dotnet",
77 | "type": "shell",
78 | "args": [
79 | "cake",
80 | "--target",
81 | "pack"
82 | ],
83 | "group": {
84 | "kind": "none",
85 | "isDefault": false
86 | },
87 | "presentation": {
88 | "reveal": "always",
89 | "revealProblems": "onProblem"
90 | },
91 | "problemMatcher": "$msCompile",
92 | "detail": "Packs the solution 📦",
93 | "icon": {
94 | "color": "terminal.ansiBlue"
95 | }
96 | }
97 | ]
98 | }
```
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | release:
9 | types:
10 | - published
11 | workflow_dispatch:
12 |
13 | env:
14 | # Disable the .NET logo in the console output.
15 | DOTNET_NOLOGO: true
16 | # Disable the .NET first time experience to skip caching NuGet packages and speed up the build.
17 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
18 | # Disable sending .NET CLI telemetry to Microsoft.
19 | DOTNET_CLI_TELEMETRY_OPTOUT: true
20 | # Set the build number in MinVer.
21 | MINVERBUILDMETADATA: build.${{github.run_number}}
22 |
23 | jobs:
24 | build:
25 | name: Build-${{matrix.os}}
26 | runs-on: ${{matrix.os}}
27 | strategy:
28 | matrix:
29 | os: [ubuntu-latest, windows-latest]
30 | steps:
31 | - name: "Checkout"
32 | uses: actions/checkout@v4
33 | with:
34 | lfs: true
35 | fetch-depth: 0
36 | - name: "Install .NET SDK"
37 | uses: actions/setup-dotnet@v4
38 | with:
39 | dotnet-version: 9.0.201
40 | - name: "Dotnet Tool Restore"
41 | run: dotnet tool restore
42 | shell: pwsh
43 | - name: "Dotnet Cake Build"
44 | run: dotnet cake --target=Build
45 | shell: pwsh
46 | # - name: "Dotnet Cake Test"
47 | # run: dotnet cake --target=Test
48 | # shell: pwsh
49 | # - name: "Dotnet Cake Pack"
50 | # run: dotnet cake --target=Pack
51 | # shell: pwsh
52 | # - name: "Publish Artefacts"
53 | # uses: actions/upload-artifact@v4
54 | # with:
55 | # name: ${{matrix.os}}
56 | # path: "./Artefacts"
57 |
58 | # push-github-packages:
59 | # name: "Push GitHub Packages"
60 | # needs: build
61 | # if: github.ref == 'refs/heads/main' || github.event_name == 'release'
62 | # environment:
63 | # name: "GitHub Packages"
64 | # url: https://github.com/NikiforovAll/hangfire-mcp/packages/
65 | # permissions:
66 | # packages: write
67 | # runs-on: windows-latest
68 | # steps:
69 | # - name: "Download Artefact"
70 | # uses: actions/download-artifact@v4
71 | # with:
72 | # name: "windows-latest"
73 | # - name: "Dotnet NuGet Add Source"
74 | # run: dotnet nuget add source https://nuget.pkg.github.com/nikiforovall/index.json --name GitHub --username nikiforovall --password ${{secrets.GITHUB_TOKEN}}
75 | # shell: pwsh
76 | # - name: "Dotnet NuGet Push"
77 | # run: dotnet nuget push .\*.nupkg --api-key ${{ github.token }} --source GitHub --skip-duplicate
78 | # shell: pwsh
79 |
80 | # push-nuget:
81 | # name: "Push NuGet Packages"
82 | # needs: build
83 | # if: github.event_name == 'release'
84 | # environment:
85 | # name: "NuGet"
86 | # url: https://www.nuget.org/packages/Nall.Hangfire.Mcp
87 | # runs-on: windows-latest
88 | # steps:
89 | # - name: "Download Artefact"
90 | # uses: actions/download-artifact@v4
91 | # with:
92 | # name: "windows-latest"
93 | # - name: "Dotnet NuGet Push"
94 | # run: |
95 | # Get-ChildItem .\ -Filter *.nupkg |
96 | # Where-Object { !$_.Name.Contains('preview') } |
97 | # ForEach-Object { dotnet nuget push $_ --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{secrets.NUGET_API_KEY}} }
98 | # shell: pwsh
99 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/HangfireTool.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireMCP;
2 |
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Text.Json;
5 | using Hangfire;
6 | using HangfireJobs;
7 | using Nall.Hangfire.Mcp;
8 |
9 | [McpServerToolType]
10 | public class HangfireTool(
11 | IHangfireDynamicScheduler scheduler,
12 | IBackgroundJobClient backgroundJobClient
13 | )
14 | {
15 | private static readonly JsonSerializerOptions JsonOptions = new()
16 | {
17 | WriteIndented = true,
18 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
19 | };
20 |
21 | [
22 | McpServerTool(Name = "RunJob"),
23 | Description("Invokes a job with the given jobName, methodName, and parameters")
24 | ]
25 | [return: Description("The job ID of the enqueued job")]
26 | public string Run(
27 | [Required] string jobName,
28 | [Required] string methodName,
29 | Dictionary<string, object>? parameters = null
30 | )
31 | {
32 | ArgumentNullException.ThrowIfNull(jobName);
33 | ArgumentNullException.ThrowIfNull(methodName);
34 |
35 | var descriptor = new JobDescriptor(jobName, methodName, parameters);
36 | return scheduler.Enqueue(descriptor, typeof(ITimeJob).Assembly);
37 | }
38 |
39 | [McpServerTool(Name = "ListJobs"), Description("Lists all jobs")]
40 | [return: Description("An array of job descriptors in JSON format")]
41 | public string ListJobs(
42 | [Description(
43 | "The search term to filter job names, optional parameter, use it only when users want to search for jobs"
44 | )]
45 | string? searchTerm = null
46 | )
47 | {
48 | var jobs = scheduler.DiscoverJobs(
49 | type =>
50 | type.IsInterface && type.Name.EndsWith("Job", StringComparison.OrdinalIgnoreCase),
51 | (type, method) =>
52 | string.IsNullOrEmpty(searchTerm)
53 | || $"{type.Name}.{method.Name}".Contains(
54 | searchTerm,
55 | StringComparison.OrdinalIgnoreCase
56 | ),
57 | typeof(ITimeJob).Assembly
58 | );
59 |
60 | return JsonSerializer.Serialize(
61 | jobs.Select(job => new
62 | {
63 | job.JobName,
64 | job.MethodName,
65 | job.Parameters,
66 | }),
67 | JsonOptions
68 | );
69 | }
70 |
71 | [McpServerTool(Name = "GetJobById"), Description("Gets job details by job ID")]
72 | [return: Description("The job details as JSON, or null if not found")]
73 | public string? GetJobById([Required, Description("The job ID")] string jobId)
74 | {
75 | ArgumentNullException.ThrowIfNull(jobId);
76 |
77 | using var connection = JobStorage.Current.GetConnection();
78 | var jobData = connection.GetJobData(jobId);
79 | if (jobData == null)
80 | {
81 | return null;
82 | }
83 |
84 | var result = new
85 | {
86 | JobId = jobId,
87 | jobData.State,
88 | jobData.CreatedAt,
89 | Arguments = jobData.Job?.Args,
90 | Method = jobData.Job?.Method?.Name,
91 | Type = jobData.Job?.Type?.FullName,
92 | };
93 | return JsonSerializer.Serialize(result, JsonOptions);
94 | }
95 |
96 | [McpServerTool(Name = "RequeueJob"), Description("Requeues an existing job by ID")]
97 | [return: Description("The new job ID if successful, null if the original job was not found")]
98 | public string? RequeueJob([Required, Description("The job ID to requeue")] string jobId)
99 | {
100 | ArgumentNullException.ThrowIfNull(jobId);
101 |
102 | backgroundJobClient.Requeue(jobId);
103 |
104 | return this.GetJobById(jobId);
105 | }
106 | }
107 |
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/HangfireTool.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace HangfireMCP;
2 |
3 | using System.ComponentModel.DataAnnotations;
4 | using System.Text.Json;
5 | using Hangfire;
6 | using Nall.Hangfire.Mcp;
7 |
8 | [McpServerToolType]
9 | public class HangfireTool(
10 | IHangfireDynamicScheduler scheduler,
11 | IBackgroundJobClient backgroundJobClient,
12 | DynamicJobLoader dynamicJobLoader,
13 | ILogger<HangfireTool> logger
14 | )
15 | {
16 | private static readonly JsonSerializerOptions JsonOptions = new()
17 | {
18 | WriteIndented = true,
19 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
20 | };
21 |
22 | [
23 | McpServerTool(Name = "RunJob"),
24 | Description("Invokes a job with the given jobName, methodName, and parameters")
25 | ]
26 | [return: Description("The job ID of the enqueued job")]
27 | public string Run(
28 | [Required] string jobName,
29 | [Required] string methodName,
30 | Dictionary<string, object>? parameters = null
31 | )
32 | {
33 | ArgumentNullException.ThrowIfNull(jobName);
34 | ArgumentNullException.ThrowIfNull(methodName);
35 |
36 | var descriptor = new JobDescriptor(jobName, methodName, parameters);
37 |
38 | var assembly =
39 | dynamicJobLoader.GetAssembly()
40 | ?? throw new InvalidOperationException(
41 | "Dynamic job loader is not initialized or assembly is not loaded."
42 | );
43 | return scheduler.Enqueue(descriptor, assembly);
44 | }
45 |
46 | [McpServerTool(Name = "ListJobs"), Description("Lists all jobs")]
47 | [return: Description("An array of job descriptors in JSON format")]
48 | public string ListJobs(
49 | [Description(
50 | "The search term to filter job names, optional parameter, use it only when users want to search for jobs"
51 | )]
52 | string? searchTerm = null
53 | )
54 | {
55 | var jobsList = new List<JobDescriptor>();
56 | try
57 | {
58 | var dynamicJobs = dynamicJobLoader.DiscoverJobs();
59 | if (dynamicJobs.Any())
60 | {
61 | // Filter dynamic jobs by search term if provided
62 | if (!string.IsNullOrEmpty(searchTerm))
63 | {
64 | dynamicJobs = dynamicJobs.Where(job =>
65 | $"{job.JobName}.{job.MethodName}".Contains(
66 | searchTerm,
67 | StringComparison.OrdinalIgnoreCase
68 | )
69 | );
70 | }
71 |
72 | jobsList.AddRange(dynamicJobs);
73 | }
74 | }
75 | catch (Exception ex)
76 | {
77 | logger.LogError(ex, "Error discovering dynamic jobs");
78 | }
79 |
80 | return JsonSerializer.Serialize(
81 | jobsList.Select(job => new
82 | {
83 | job.JobName,
84 | job.MethodName,
85 | job.Parameters,
86 | }),
87 | JsonOptions
88 | );
89 | }
90 |
91 | [McpServerTool(Name = "GetJobById"), Description("Gets job details by job ID")]
92 | [return: Description("The job details as JSON, or null if not found")]
93 | public string? GetJobById([Required, Description("The job ID")] string jobId)
94 | {
95 | ArgumentNullException.ThrowIfNull(jobId);
96 |
97 | using var connection = JobStorage.Current.GetConnection();
98 | var jobData = connection.GetJobData(jobId);
99 | if (jobData == null)
100 | {
101 | return null;
102 | }
103 |
104 | var result = new
105 | {
106 | JobId = jobId,
107 | jobData.State,
108 | jobData.CreatedAt,
109 | Arguments = jobData.Job?.Args,
110 | Method = jobData.Job?.Method?.Name,
111 | Type = jobData.Job?.Type?.FullName,
112 | };
113 | return JsonSerializer.Serialize(result, JsonOptions);
114 | }
115 |
116 | [McpServerTool(Name = "RequeueJob"), Description("Requeues an existing job by ID")]
117 | [return: Description("The new job ID if successful, null if the original job was not found")]
118 | public string? RequeueJob([Required, Description("The job ID to requeue")] string jobId)
119 | {
120 | ArgumentNullException.ThrowIfNull(jobId);
121 |
122 | backgroundJobClient.Requeue(jobId);
123 |
124 | return this.GetJobById(jobId);
125 | }
126 | }
127 |
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/DynamicJobLoader.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace Nall.Hangfire.Mcp;
2 |
3 | using System.Reflection;
4 | using DevLab.JmesPath;
5 | using Microsoft.Extensions.Logging;
6 |
7 | /// <summary>
8 | /// Provides functionality for dynamically loading and filtering job types from external assemblies.
9 | /// </summary>
10 | public class DynamicJobLoader(IHangfireDynamicScheduler scheduler, ILogger<DynamicJobLoader> logger)
11 | {
12 | private string? assemblyPath;
13 |
14 | public Assembly? LoadedAssembly { get; set; }
15 |
16 | /// <summary>
17 | /// Initializes the dynamic job loader with assembly path and JMESPath expression from environment variables.
18 | /// </summary>
19 | /// <returns>True if initialization succeeded, false otherwise.</returns>
20 | public bool Initialize()
21 | {
22 | try
23 | {
24 | // Get assembly path from environment variable
25 | this.assemblyPath = Environment.GetEnvironmentVariable("HANGFIRE_JOBS_ASSEMBLY");
26 | if (string.IsNullOrWhiteSpace(this.assemblyPath))
27 | {
28 | logger.LogWarning("HANGFIRE_JOBS_ASSEMBLY environment variable not set or empty");
29 | return false;
30 | }
31 |
32 | // Attempt to load the assembly
33 | if (!this.LoadAssembly())
34 | {
35 | return false;
36 | }
37 |
38 | return true;
39 | }
40 | catch (Exception ex)
41 | {
42 | logger.LogError(ex, "Failed to initialize dynamic job loader");
43 | return false;
44 | }
45 | }
46 |
47 | /// <summary>
48 | /// Loads the assembly from the configured path.
49 | /// </summary>
50 | /// <returns>True if assembly loaded successfully, false otherwise.</returns>
51 | private bool LoadAssembly()
52 | {
53 | if (string.IsNullOrEmpty(this.assemblyPath))
54 | {
55 | return false;
56 | }
57 |
58 | try
59 | {
60 | if (!File.Exists(this.assemblyPath))
61 | {
62 | logger.LogError("Assembly file not found: {Path}", this.assemblyPath);
63 | return false;
64 | }
65 |
66 | // Load the assembly
67 | this.LoadedAssembly = Assembly.LoadFrom(this.assemblyPath);
68 | logger.LogInformation(
69 | "Successfully loaded assembly: {AssemblyName} from {Path}",
70 | this.LoadedAssembly.FullName,
71 | this.assemblyPath
72 | );
73 |
74 | return true;
75 | }
76 | catch (Exception ex)
77 | {
78 | logger.LogError(ex, "Failed to load assembly from {Path}", this.assemblyPath);
79 | this.LoadedAssembly = null;
80 | return false;
81 | }
82 | }
83 |
84 | /// <summary>
85 | /// Discovers job types in the loaded assembly that match the JMESPath expression.
86 | /// </summary>
87 | /// <returns>A collection of job descriptors for the matched types.</returns>
88 | public IEnumerable<JobDescriptor> DiscoverJobs()
89 | {
90 | if (this.LoadedAssembly == null)
91 | {
92 | logger.LogWarning("No assembly loaded. Attempting to initialize...");
93 | if (!this.Initialize() || this.LoadedAssembly == null)
94 | {
95 | logger.LogError("Failed to initialize or load assembly. Aborting job discovery.");
96 | return [];
97 | }
98 | }
99 |
100 | try
101 | {
102 | // Get JMESPath expression from environment variable
103 | var expressionString = Environment.GetEnvironmentVariable(
104 | "HANGFIRE_JOBS_MATCH_EXPRESSION"
105 | );
106 | if (string.IsNullOrWhiteSpace(expressionString))
107 | {
108 | logger.LogWarning(
109 | "HANGFIRE_JOBS_MATCH_EXPRESSION environment variable not set or empty"
110 | );
111 | return [];
112 | }
113 |
114 | // Create JmesPath expression
115 | var jmesPath = new JmesPath();
116 |
117 | // Build a collection of type information as JObject to use with JMESPath
118 | var types = this
119 | .LoadedAssembly.GetTypes()
120 | .Select(type => new
121 | {
122 | type.Name,
123 | type.FullName,
124 | type.Namespace,
125 | type.IsPublic,
126 | type.IsClass,
127 | type.IsInterface,
128 | })
129 | .ToList();
130 |
131 | // Serialize to JSON for JmesPath to process
132 | var typesJson = System.Text.Json.JsonSerializer.Serialize(types);
133 |
134 | // Apply JmesPath expression to filter the types
135 | var result = jmesPath.Transform(typesJson, expressionString);
136 |
137 | if (string.IsNullOrEmpty(result) || result == "null" || result == "[]")
138 | {
139 | logger.LogWarning(
140 | "JMESPath expression '{Expression}' did not match any types",
141 | expressionString
142 | );
143 | return [];
144 | }
145 |
146 | // Deserialize the filtered results
147 | var filteredTypeInfo = System.Text.Json.JsonSerializer.Deserialize<
148 | List<System.Text.Json.JsonElement>
149 | >(result);
150 |
151 | if (filteredTypeInfo == null || filteredTypeInfo.Count == 0)
152 | {
153 | logger.LogWarning("No types matched the JMESPath expression");
154 | return [];
155 | }
156 |
157 | // Get the full names of the matched types
158 | var matchedFullNames = filteredTypeInfo
159 | .Select(element => element.GetProperty("FullName").GetString())
160 | .Where(name => !string.IsNullOrEmpty(name))
161 | .ToList();
162 |
163 | // Discover jobs from these types
164 | var jobs = scheduler.DiscoverJobs(
165 | type => matchedFullNames.Contains(type.FullName),
166 | assembly: this.LoadedAssembly
167 | );
168 |
169 | logger.LogInformation(
170 | "Discovered {Count} jobs from {AssemblyName}",
171 | jobs.Count(),
172 | this.LoadedAssembly.GetName().Name
173 | );
174 |
175 | return jobs;
176 | }
177 | catch (Exception ex)
178 | {
179 | logger.LogError(ex, "Error discovering jobs with JMESPath expression");
180 | return [];
181 | }
182 | }
183 |
184 | /// <summary>
185 | /// Gets the loaded assembly.
186 | /// </summary>
187 | /// <returns>The loaded assembly or null if no assembly is loaded.</returns>
188 | public Assembly? GetAssembly() => this.LoadedAssembly;
189 | }
190 |
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/HangfireDynamicScheduler.cs:
--------------------------------------------------------------------------------
```csharp
1 | namespace Nall.Hangfire.Mcp;
2 |
3 | using System.Linq.Expressions;
4 | using System.Reflection;
5 | using System.Text.Json;
6 | using global::Hangfire;
7 |
8 | public interface IHangfireDynamicScheduler
9 | {
10 | /// <summary>
11 | /// Enqueues a job based on the provided <see cref="JobDescriptor"/>.
12 | /// </summary>
13 | public string Enqueue(JobDescriptor descriptor, Assembly? assembly = null);
14 |
15 | /// <summary>
16 | /// Discovers jobs in the specified assembly.
17 | /// </summary>
18 | public IEnumerable<JobDescriptor> DiscoverJobs(
19 | Func<Type, bool> nameSelector,
20 | Func<Type, MethodInfo, bool>? methodSelector = null,
21 | Assembly? assembly = null
22 | );
23 | }
24 |
25 | public class HangfireDynamicScheduler(IBackgroundJobClient client) : IHangfireDynamicScheduler
26 | {
27 | private static readonly JsonSerializerOptions JsonSerializerOptions = new()
28 | {
29 | PropertyNameCaseInsensitive = false,
30 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
31 | };
32 |
33 | /// <summary>
34 | /// Enqueues a job based on the provided <see cref="JobDescriptor"/>.
35 | /// </summary>
36 | public string Enqueue(JobDescriptor descriptor, Assembly? assembly = null)
37 | {
38 | ArgumentNullException.ThrowIfNull(descriptor);
39 | assembly ??= Assembly.GetExecutingAssembly();
40 | var type =
41 | assembly.GetType(descriptor.JobName)
42 | ?? throw new InvalidOperationException($"Type '{descriptor.JobName}' not found.");
43 |
44 | return type.IsInterface
45 | ? this.EnqueueByInterfaceName(type, descriptor.MethodName, descriptor.Parameters)
46 | : this.EnqueueByTypeName(type, descriptor.MethodName, descriptor.Parameters);
47 | }
48 |
49 | private string EnqueueByInterfaceName(
50 | Type iface,
51 | string methodName,
52 | IDictionary<string, object>? parameters
53 | )
54 | {
55 | // Find the correct method overload based on parameter types
56 | var method =
57 | FindMethodByNameAndParameters(iface, methodName, parameters)
58 | ?? throw new InvalidOperationException(
59 | $"Method '{methodName}' not found on '{iface.FullName}' with matching parameters."
60 | );
61 |
62 | var param = Expression.Parameter(iface, "x");
63 | var methodParams = method.GetParameters();
64 | var arguments = BuildMethodArguments(methodParams, parameters);
65 | var call = Expression.Call(param, method, arguments);
66 |
67 | var lambdaType = typeof(Func<,>).MakeGenericType(iface, typeof(Task));
68 | var lambda = Expression.Lambda(lambdaType, call, param);
69 |
70 | var enqueueMethod = GetEnqueueMethod();
71 | var genericEnqueue = enqueueMethod.MakeGenericMethod(iface);
72 | return (string)(
73 | genericEnqueue.Invoke(null, [client, lambda])
74 | ?? throw new InvalidOperationException("Failed to enqueue job")
75 | );
76 | }
77 |
78 | private static MethodInfo? FindMethodByNameAndParameters(
79 | Type type,
80 | string methodName,
81 | IDictionary<string, object>? parameters
82 | )
83 | {
84 | // Get all methods with the given name
85 | var candidateMethods = type.GetMethods(
86 | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
87 | )
88 | .Where(m => m.Name == methodName)
89 | .ToList();
90 |
91 | if (candidateMethods.Count == 0)
92 | {
93 | return null;
94 | }
95 |
96 | // If only one method with this name exists, return it
97 | if (candidateMethods.Count == 1)
98 | {
99 | return candidateMethods[0];
100 | }
101 |
102 | // If we have parameters, try to find the best match by parameter names
103 | if (parameters != null && parameters.Count > 0)
104 | {
105 | var paramNames = parameters.Keys.ToHashSet(StringComparer.OrdinalIgnoreCase);
106 |
107 | // Find methods where all parameters have matching names
108 | var matchingMethods = candidateMethods
109 | .Where(m =>
110 | {
111 | var methodParamNames = m.GetParameters()
112 | .Select(p => p.Name)
113 | .Where(n => n != null)
114 | .ToHashSet(StringComparer.OrdinalIgnoreCase);
115 |
116 | // Check if all required parameters (non-optional) have values
117 | var requiredParams = m.GetParameters()
118 | .Where(p => !p.HasDefaultValue)
119 | .Select(p => p.Name)
120 | .Where(n => n != null);
121 |
122 | return requiredParams.All(rp => rp != null && paramNames.Contains(rp));
123 | })
124 | .ToList();
125 |
126 | if (matchingMethods.Count == 1)
127 | {
128 | return matchingMethods[0];
129 | }
130 |
131 | // If multiple methods match, select the one with the most matching parameters
132 | if (matchingMethods.Count > 1)
133 | {
134 | return matchingMethods
135 | .OrderByDescending(m =>
136 | {
137 | var methodParamNames = m.GetParameters()
138 | .Select(p => p.Name)
139 | .Where(n => n != null);
140 |
141 | return methodParamNames.Count(mpn =>
142 | mpn != null && paramNames.Contains(mpn)
143 | );
144 | })
145 | .FirstOrDefault();
146 | }
147 | }
148 |
149 | // If we can't find by parameter names, just return the first method
150 | return candidateMethods.FirstOrDefault();
151 | }
152 |
153 | private static Expression[] BuildMethodArguments(
154 | ParameterInfo[] methodParams,
155 | IDictionary<string, object>? parameters
156 | )
157 | {
158 | var arguments = new Expression[methodParams.Length];
159 | for (var i = 0; i < methodParams.Length; i++)
160 | {
161 | var methodParam = methodParams[i];
162 | if (parameters != null && parameters.TryGetValue(methodParam.Name!, out var value))
163 | {
164 | var convertedValue = value;
165 | if (value is JsonElement jsonElement)
166 | {
167 | convertedValue = GetValue(methodParam, jsonElement);
168 | }
169 | arguments[i] = Expression.Constant(convertedValue, methodParam.ParameterType);
170 | }
171 | else if (methodParam.HasDefaultValue)
172 | {
173 | arguments[i] = Expression.Constant(
174 | methodParam.DefaultValue,
175 | methodParam.ParameterType
176 | );
177 | }
178 | else
179 | {
180 | throw new ArgumentException(
181 | $"Required parameter '{methodParam.Name}' was not provided"
182 | );
183 | }
184 | }
185 | return arguments;
186 | }
187 |
188 | private static object? GetValue(ParameterInfo methodParam, JsonElement jsonElement) =>
189 | jsonElement.ValueKind switch
190 | {
191 | JsonValueKind.String => jsonElement.GetString(),
192 | JsonValueKind.Number when methodParam.ParameterType == typeof(int) =>
193 | jsonElement.GetInt32(),
194 | JsonValueKind.Number when methodParam.ParameterType == typeof(long) =>
195 | jsonElement.GetInt64(),
196 | JsonValueKind.Number when methodParam.ParameterType == typeof(double) =>
197 | jsonElement.GetDouble(),
198 | JsonValueKind.True or JsonValueKind.False => jsonElement.GetBoolean(),
199 | JsonValueKind.Object => JsonSerializer.Deserialize(
200 | jsonElement.GetRawText(),
201 | methodParam.ParameterType,
202 | JsonSerializerOptions
203 | ),
204 | JsonValueKind.Array => JsonSerializer.Deserialize(
205 | jsonElement.GetRawText(),
206 | methodParam.ParameterType,
207 | JsonSerializerOptions
208 | ),
209 | _ => throw new ArgumentException(
210 | $"Unsupported parameter type '{methodParam.ParameterType}' for '{methodParam.Name}'"
211 | ),
212 | };
213 |
214 | private static MethodInfo GetEnqueueMethod()
215 | {
216 | return typeof(BackgroundJobClientExtensions)
217 | .GetMethods()
218 | .First(m =>
219 | {
220 | var p = m.GetParameters()[1].ParameterType;
221 | if (!p.IsGenericType || p.GetGenericTypeDefinition() != typeof(Expression<>))
222 | {
223 | return false;
224 | }
225 |
226 | var inner = p.GetGenericArguments()[0];
227 | return inner.IsGenericType
228 | && inner.GetGenericTypeDefinition() == typeof(Func<,>)
229 | && inner.GetGenericArguments()[1] == typeof(Task);
230 | });
231 | }
232 |
233 | private string EnqueueByTypeName(
234 | Type type,
235 | string methodName,
236 | IDictionary<string, object>? parameters
237 | )
238 | {
239 | // Find the correct method overload based on parameter types
240 | var method =
241 | FindMethodByNameAndParameters(type, methodName, parameters)
242 | ?? throw new InvalidOperationException(
243 | $"Method '{methodName}' not found on type '{type.FullName}' with matching parameters."
244 | );
245 |
246 | var param = Expression.Parameter(type, "x");
247 | var methodParams = method.GetParameters();
248 | var arguments = BuildMethodArguments(methodParams, parameters);
249 | var call = Expression.Call(param, method, arguments);
250 |
251 | var lambdaType = typeof(Func<,>).MakeGenericType(type, typeof(Task));
252 | var lambda = Expression.Lambda(lambdaType, call, param);
253 |
254 | var enqueueMethod = GetEnqueueMethod();
255 | var genericEnqueue = enqueueMethod.MakeGenericMethod(type);
256 | return (string)(
257 | genericEnqueue.Invoke(null, [client, lambda])
258 | ?? throw new InvalidOperationException("Failed to enqueue job")
259 | );
260 | }
261 |
262 | public IEnumerable<JobDescriptor> DiscoverJobs(
263 | Func<Type, bool> nameSelector,
264 | Func<Type, MethodInfo, bool>? methodSelector = null,
265 | Assembly? assembly = null
266 | )
267 | {
268 | assembly ??= Assembly.GetExecutingAssembly();
269 | var results = new List<JobDescriptor>();
270 |
271 | // Get all types from the assembly that match the name selector
272 | var types = assembly
273 | .GetTypes()
274 | .Where(t => (t.IsClass || t.IsInterface) && nameSelector(t))
275 | .ToList();
276 |
277 | foreach (var type in types)
278 | { // Get all public methods from the type, excluding standard Object methods
279 | var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
280 | .Where(m =>
281 | !m.IsSpecialName
282 | && // Exclude property getters/setters
283 | !IsStandardObjectMethod(m)
284 | && (methodSelector?.Invoke(type, m) ?? true)
285 | ) // Exclude standard Object methods
286 | .ToList();
287 |
288 | foreach (var method in methods)
289 | {
290 | // Create parameter dictionary with default values
291 | var paramDict = new Dictionary<string, object>();
292 | foreach (var param in method.GetParameters())
293 | {
294 | if (param.Name != null)
295 | {
296 | paramDict[param.Name] = CreateDefaultValue(param.ParameterType);
297 | }
298 | }
299 |
300 | results.Add(new JobDescriptor(type.FullName!, method.Name, paramDict));
301 | }
302 | }
303 |
304 | return results;
305 | }
306 |
307 | private static bool IsStandardObjectMethod(MethodInfo method)
308 | {
309 | return method.DeclaringType == typeof(object)
310 | || method.Name is "ToString" or "GetType" or "Equals" or "GetHashCode";
311 | }
312 |
313 | private static object CreateDefaultValue(Type type)
314 | {
315 | if (type == typeof(string))
316 | {
317 | return string.Empty;
318 | }
319 |
320 | if (type == typeof(int) || type == typeof(long) || type == typeof(short))
321 | {
322 | return 0;
323 | }
324 |
325 | if (type == typeof(double) || type == typeof(float) || type == typeof(decimal))
326 | {
327 | return 0.0;
328 | }
329 |
330 | if (type == typeof(bool))
331 | {
332 | return false;
333 | }
334 |
335 | if (type == typeof(Guid))
336 | {
337 | return Guid.Empty;
338 | }
339 |
340 | if (type == typeof(DateTime))
341 | {
342 | return DateTime.Now;
343 | }
344 |
345 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
346 | {
347 | return Activator.CreateInstance(type)!;
348 | }
349 |
350 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
351 | {
352 | return Activator.CreateInstance(type)!;
353 | }
354 |
355 | if (type.IsClass && !type.IsAbstract && type != typeof(object))
356 | {
357 | try
358 | {
359 | return Activator.CreateInstance(type) ?? new object();
360 | }
361 | catch
362 | {
363 | return new object();
364 | }
365 | }
366 |
367 | return new object();
368 | }
369 | }
370 |
```