# 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:
--------------------------------------------------------------------------------
```
[submodule "src/Inspector.Aspire.Hosting/inspector"]
path = src/Inspector.Aspire.Hosting/inspector
url = https://github.com/modelcontextprotocol/inspector.git
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp
Artefacts
```
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
```
##########################################
# Common Settings
##########################################
# This file is the top-most EditorConfig file
root = true
# All Files
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length=100
##########################################
# File Extension Settings
##########################################
# Visual Studio Solution Files
[*.sln]
indent_style = tab
# Visual Studio XML Project Files
[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# XML Configuration Files
[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}]
indent_size = 2
# JSON Files
[*.{json,json5,webmanifest}]
indent_size = 2
# YAML Files
[*.{yml,yaml}]
indent_size = 2
# Markdown Files
[*.md]
trim_trailing_whitespace = false
# Web Files
[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}]
indent_size = 2
# Batch Files
[*.{cmd,bat}]
end_of_line = crlf
# Bash Files
[*.sh]
end_of_line = lf
# Makefiles
[Makefile]
indent_style = tab
##########################################
# Default .NET Code Style Severities
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/configuration-options#scope
##########################################
[*.{cs,csx,cake,vb,vbx}]
# Default Severity for all .NET Code Style rules below
dotnet_analyzer_diagnostic.severity = warning
csharp_style_expression_bodied_methods=true:suggestion
csharp_style_expression_bodied_constructors=true:suggestion
csharp_style_namespace_declarations=file_scoped:silent
##########################################
# TODO: this is already present in this file and section below is inteded to force this on editor level.
# IDE Code Style IDE_XXXX
# Motivation https://github.com/dotnet/roslyn/blob/9f87b444da9c48a4d492b19f8337339056bf2b95/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs
##########################################
# IDE0055 Fix formatting
dotnet_diagnostic.IDE0055.severity = suggestion
# IDE005_gen: Remove unnecessary usings in generated code
dotnet_diagnostic.IDE0005.severity = error
# IDE0065: Using directives must be placed outside of a namespace declaration
dotnet_diagnostic.IDE0065.severity = error
# IDE0059: Unnecessary assignment
dotnet_diagnostic.IDE0059.severity = error
# IDE0007 UseImplicitType
dotnet_diagnostic.IDE0007.severity = error
# IDE0008
dotnet_diagnostic.IDE0008.severity = error
# IDE0011 UseExplicitType
dotnet_diagnostic.IDE0011.severity = error
# IDE0040 AddAccessibilityModifiers
dotnet_diagnostic.IDE0040.severity = error
# IDE0060 UnusedParameter
dotnet_diagnostic.IDE0060.severity = error
# IDE0036 Order modifiers
dotnet_diagnostic.IDE0036.severity = error
# IDE0059 Remove unnecessary value assignment
dotnet_diagnostic.IDE0059.severity = error
# IDE0016 Use throw expression
dotnet_diagnostic.IDE0016.severity = suggestion
# CA1056: URI properties should not be strings
dotnet_diagnostic.CA1056.severity = suggestion
dotnet_diagnostic.CA1812.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1307.severity = none
dotnet_diagnostic.CA2234.severity = none
dotnet_diagnostic.CA1054.severity = none
dotnet_diagnostic.CA1032.severity = none
dotnet_diagnostic.CA1724.severity = none
dotnet_diagnostic.CA1055.severity = none
dotnet_diagnostic.CA1510.severity = suggestion
dotnet_diagnostic.CA2227.severity = suggestion
dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1019.severity = none
##########################################
# File Header (Uncomment to support file headers)
# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
##########################################
# [*.{cs,csx,cake,vb,vbx}]
# file_header_template = <copyright file="{fileName}" company="PROJECT-AUTHOR">\n© PROJECT-AUTHOR\n</copyright>
# SA1636: File header copyright text should match
# 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.
# dotnet_diagnostic.SA1636.severity = warning
[*.{cs,csx,cake,vb,vbx}]
# IDE0073: The file header is missing or not located at the top of the file
dotnet_diagnostic.IDE0073.severity = error
##########################################
# .NET Language Conventions
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
##########################################
# .NET Code Style Settings
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings
[*.{cs,csx,cake,vb,vbx}]
# "this." and "Me." qualifiers
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me
dotnet_style_qualification_for_field = true:warning
dotnet_style_qualification_for_property = true:warning
dotnet_style_qualification_for_method = true:warning
dotnet_style_qualification_for_event = true:warning
# Language keywords instead of framework type names for type references
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# Modifier preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
dotnet_style_require_accessibility_modifiers = always:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
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
dotnet_style_readonly_field = true:warning
# Parentheses preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = always_for_clarity:suggestion
# Expression-level preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
dotnet_style_object_initializer = true:warning
dotnet_style_collection_initializer = true:warning
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_style_prefer_conditional_expression_over_return = false:suggestion
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_style_prefer_compound_assignment = true:warning
dotnet_diagnostic.CA1848.severity = suggestion
# Null-checking preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences
dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning
# Parameter preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences
dotnet_code_quality_unused_parameters = all:warning
# More style options (Undocumented)
# https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641
dotnet_style_operator_placement_when_wrapping = end_of_line
# https://github.com/dotnet/roslyn/pull/40070
dotnet_style_prefer_simplified_interpolation = true:warning
# C# Code Style Settings
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
[*.{cs,csx,cake}]
# Implicit and explicit types
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = true:warning
# Expression-bodied members
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_constructors = true:warning
csharp_style_expression_bodied_operators = true:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning
csharp_style_expression_bodied_lambdas = true:warning
csharp_style_expression_bodied_local_functions = true:warning
# Pattern matching
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
# Inlined variable declarations
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations
csharp_style_inlined_variable_declaration = true:warning
# Expression-level preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
csharp_prefer_simple_default_expression = true:warning
# "Null" checking preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning
# Code block preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences
csharp_prefer_braces = true:warning
# Unused value preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences
csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
dotnet_diagnostic.IDE0058.severity = suggestion
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
dotnet_diagnostic.IDE0059.severity = suggestion
# Index and range preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning
# Miscellaneous preferences
# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_pattern_local_over_anonymous_function = true:warning
csharp_using_directive_placement = inside_namespace:warning
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:suggestion
dotnet_diagnostic.IDE0063.severity = suggestion
##########################################
# .NET Formatting Conventions
# https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions
##########################################
# Organize usings
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives
dotnet_sort_system_directives_first = true
# Newline options
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation options
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = no_change
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents_when_block = false
# Spacing options
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_after_comma = true
csharp_space_before_comma = false
csharp_space_after_dot = false
csharp_space_before_dot = false
csharp_space_after_semicolon_in_for_statement = true
csharp_space_before_semicolon_in_for_statement = false
csharp_space_around_declaration_statements = false
csharp_space_before_open_square_brackets = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_square_brackets = false
# Wrapping options
# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options
csharp_preserve_single_line_statements = false
csharp_preserve_single_line_blocks = true
##########################################
# .NET Naming Conventions
# https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions
##########################################
[*.{cs,csx,cake,vb,vbx}]
##########################################
# Styles
##########################################
# camel_case_style - Define the camelCase style
dotnet_naming_style.camel_case_style.capitalization = camel_case
# pascal_case_style - Define the PascalCase style
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# first_upper_style - The first character must start with an upper-case character
dotnet_naming_style.first_upper_style.capitalization = first_word_upper
# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I'
dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T'
dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
# disallowed_style - Anything that has this style applied is marked as disallowed
dotnet_naming_style.disallowed_style.capitalization = pascal_case
dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file
dotnet_naming_style.internal_error_style.capitalization = pascal_case
dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____
##########################################
# .NET Design Guideline Field Naming Rules
# Naming rules for fields follow the .NET Framework design guidelines
# https://docs.microsoft.com/dotnet/standard/design-guidelines/index
##########################################
# All public/protected/protected_internal constant fields must be PascalCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal
dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const
dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field
dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group
dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning
# All public/protected/protected_internal static readonly fields must be PascalCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal
dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly
dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field
dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group
dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning
# No other public/protected/protected_internal fields are allowed
# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal
dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field
dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group
dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style
dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error
##########################################
# StyleCop Field Naming Rules
# Naming rules for fields follow the StyleCop analyzers
# This does not override any rules using disallowed_style above
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
##########################################
# All constant fields must be PascalCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md
dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const
dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group
dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning
# All static readonly fields must be PascalCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md
dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly
dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group
dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning
# No non-private instance fields are allowed
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md
dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field
dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group
dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style
dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error
# Private fields must be camelCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md
dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private
dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style
dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning
# Local variables must be camelCase
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local
dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local
dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group
dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style
dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent
# This rule should never fire. However, it's included for at least two purposes:
# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers.
# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#).
dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = *
dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field
dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group
dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style
dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error
##########################################
# Other Naming Rules
##########################################
# All of the following must be PascalCase:
# - Namespaces
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
# - Classes and Enumerations
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
# - Delegates
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types
# - Constructors, Properties, Events, Methods
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members
dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
dotnet_naming_rule.element_rule.symbols = element_group
dotnet_naming_rule.element_rule.style = pascal_case_style
dotnet_naming_rule.element_rule.severity = warning
# Interfaces use PascalCase and are prefixed with uppercase 'I'
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
dotnet_naming_symbols.interface_group.applicable_kinds = interface
dotnet_naming_rule.interface_rule.symbols = interface_group
dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style
dotnet_naming_rule.interface_rule.severity = warning
# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
dotnet_naming_rule.type_parameter_rule.severity = warning
# Function parameters use camelCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters
dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
dotnet_naming_rule.parameters_rule.symbols = parameters_group
dotnet_naming_rule.parameters_rule.style = camel_case_style
dotnet_naming_rule.parameters_rule.severity = warning
# IDE0130: Namespace does not match folder structure
dotnet_diagnostic.IDE0130.severity = suggestion
# CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1062.severity = suggestion
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = suggestion
# IDE0022: Use expression body for method
dotnet_diagnostic.IDE0022.severity = silent
# CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = suggestion
# https://github.com/dennisdoomen/CSharpGuidelines/tree/5.7.0/_rules
dotnet_diagnostic.AV1505.severity=none
dotnet_diagnostic.AV2305.severity=none
dotnet_diagnostic.AV1008.severity=none
dotnet_diagnostic.AV1555.severity=none
dotnet_diagnostic.AV1553.severity=none
dotnet_diagnostic.AV1568.severity=none
dotnet_diagnostic.AV1745.severity=none
dotnet_diagnostic.AV1708.severity=none
dotnet_diagnostic.AV1580.severity=none
dotnet_diagnostic.AV1564.severity=warning
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Hangfire MCP [](https://github.com/NikiforovAll/hangfire-mcp/actions/workflows/build.yml)
Enqueue background jobs using Hangfire MCP server.
## Motivation
Interaction with *Hangfire* using *Hangfire MCP Server* allows you to enqueue jobs from any client that supports MCP protocol.
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.
<video src="https://github.com/user-attachments/assets/e6abc036-b1f9-4691-a829-65292db5b5e6" controls="controls"></video>
Here is MCP Server configuration for VS Code:
```json
{
"servers": {
"hangfire-mcp": {
"url": "http://localhost:3001"
}
}
}
```
## Code Example
Here is how it works:
```mermaid
sequenceDiagram
participant User as User
participant MCPHangfire as MCP Hangfire
participant IBackgroundJobClient as IBackgroundJobClient
participant Database as Database
participant HangfireServer as Hangfire Server
User->>MCPHangfire: Enqueue Job
MCPHangfire->>IBackgroundJobClient: Send Job Message
IBackgroundJobClient->>Database: Store Job Message
HangfireServer->>Database: Fetch Job Message
HangfireServer->>HangfireServer: Process Job
```
## Standalone Mode
It is a regular MCP packaged as .NET global tool. Here is how to setup it as an MCP server in VSCode.
```bash
dotnet tool install --global --add-source Nall.HangfireMCP
```
Configuration:
```json
{
"servers": {
"hangfire-mcp-standalone": {
"type": "stdio",
"command": "HangfireMCP",
"args": [
"--stdio"
],
"env": {
"HANGFIRE_JOBS_ASSEMBLY": "path/to/Jobs.dll",
"HANGFIRE_JOBS_MATCH_EXPRESSION": "[?IsInterface && contains(Name, 'Job')]",
"HANGFIRE_CONNECTION_STRING": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=hangfire"
}
}
}
}
```
### Aspire
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgresServer = builder
.AddPostgres("postgres-server")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent);
var postgresDatabase = postgresServer.AddDatabase("hangfire");
builder.AddProject<Projects.Web>("server")
.WithReference(postgresDatabase)
.WaitFor(postgresDatabase);
var mcp = builder
.AddProject<Projects.HangfireMCP_Standalone>("hangfire-mcp")
.WithEnvironment("HANGFIRE_JOBS_ASSEMBLY", "path/to/Jobs.dll")
.WithEnvironment("HANGFIRE_JOBS_MATCH_EXPRESSION", "[?IsInterface && contains(Name, 'Job')]")
.WithReference(postgresDatabase)
.WaitFor(postgresDatabase);
builder
.AddMCPInspector()
.WithSSE(mcp)
.WaitFor(mcp);
builder.Build().Run();
```
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.
## Custom Setup (as Code) Mode
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.
### Aspire
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgresServer = builder
.AddPostgres("postgres-server")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent);
var postgresDatabase = postgresServer.AddDatabase("hangfire");
builder.AddProject<Projects.Web>("server")
.WithReference(postgresDatabase)
.WaitFor(postgresDatabase);
var mcp = builder
.AddProject<Projects.HangfireMCP>("hangfire-mcp")
.WithReference(postgresDatabase)
.WaitFor(postgresDatabase);
builder
.AddMCPInspector()
.WithSSE(mcp)
.WaitFor(mcp);
builder.Build().Run();
```

### MCP Server
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.WithMcpServer(args).WithToolsFromAssembly();
builder.Services.AddHangfire(cfg => cfg.UsePostgreSqlStorage(options =>
options.UseNpgsqlConnection(builder.Configuration.GetConnectionString("hangfire")))
);
builder.Services.AddHangfireMcp();
builder.Services.AddTransient<HangfireTool>();
var app = builder.Build();
app.MapMcpServer(args);
app.Run();
```
Here is an example of the Hangfire tool:
```csharp
[McpServerToolType]
public class HangfireTool(IHangfireDynamicScheduler scheduler)
{
[McpServerTool(Name = "RunJob")]
public string Run(
[Required] string jobName,
[Required] string methodName,
Dictionary<string, object>? parameters = null
)
{
var descriptor = new JobDescriptor(jobName, methodName, parameters);
return scheduler.Enqueue(descriptor, typeof(ITimeJob).Assembly);
}
}
```
## Tools

```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
```markdown
MIT License
Copyright (c) 2024 Oleksii Nikiforov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
```markdown
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[email protected].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {}
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {}
}
}
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
{
"[csharp]": {
"editor.defaultFormatter": "csharpier.csharpier-vscode"
}
}
```
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
```json
{
"sdk": {
"version": "9.0.201",
"rollForward": "latestMajor",
"allowPrerelease": false
}
}
```
--------------------------------------------------------------------------------
/samples/Web/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/appsettings.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/appsettings.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
--------------------------------------------------------------------------------
/samples/Web/appsettings.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
--------------------------------------------------------------------------------
/samples/AppHost/appsettings.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
```
--------------------------------------------------------------------------------
/samples/AppHost/appsettings.Development.json:
--------------------------------------------------------------------------------
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Parameters": {
"pg-username": "postgres",
"pg-password": "postgres"
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/Program.cs:
--------------------------------------------------------------------------------
```csharp
using HangfireMCP;
using Nall.Hangfire.Mcp;
var builder = WebApplication.CreateBuilder(args);
builder.WithMcpServer(args).WithToolsFromAssembly();
builder.AddHangfire();
builder.Services.AddHangfireMcp();
builder.Services.AddTransient<HangfireTool>();
var app = builder.Build();
app.MapMcpServer(args);
app.Run();
```
--------------------------------------------------------------------------------
/.config/dotnet-tools.json:
--------------------------------------------------------------------------------
```json
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "5.0.0",
"commands": [
"dotnet-cake"
],
"rollForward": false
},
"csharpier": {
"version": "1.0.1",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/JobDescriptor.cs:
--------------------------------------------------------------------------------
```csharp
namespace Nall.Hangfire.Mcp;
public class JobDescriptor(
string jobName,
string methodName,
IDictionary<string, object>? parameters = null
)
{
public string JobName { get; } = jobName;
public string MethodName { get; } = methodName;
public IDictionary<string, object>? Parameters { get; } = parameters;
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/Program.cs:
--------------------------------------------------------------------------------
```csharp
using HangfireMCP;
using Nall.Hangfire.Mcp;
var builder = WebApplication.CreateBuilder(args);
builder.WithMcpServer(args).WithToolsFromAssembly();
builder.AddHangfire();
builder.Services.AddHangfireMcp();
builder.Services.AddTransient<HangfireTool>();
builder.Services.AddHostedService<DynamicJobLoaderHostedService>();
var app = builder.Build();
app.MapMcpServer(args);
app.Run();
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
```csharp
namespace Nall.Hangfire.Mcp;
using Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddHangfireMcp(this IServiceCollection services)
{
services.AddSingleton<IHangfireDynamicScheduler, HangfireDynamicScheduler>();
services.AddSingleton<DynamicJobLoader>();
return services;
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
```yaml
name: Release Drafter
on:
push:
branches:
- main
pull_request_target:
types:
- edited
- opened
- reopened
- synchronize
workflow_dispatch:
jobs:
update_release_draft:
permissions:
contents: write
pull-requests: read
runs-on: ubuntu-latest
steps:
- name: "Draft Release"
uses: release-drafter/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
--------------------------------------------------------------------------------
/samples/HangfireJobs/ITimeJob.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireJobs;
using System.Globalization;
using Microsoft.Extensions.Logging;
public interface ITimeJob
{
public Task ExecuteAsync();
}
public class TimeJob(TimeProvider timeProvider, ILogger<TimeJob> logger) : ITimeJob
{
public Task ExecuteAsync()
{
logger.LogInformation(
"Current time: {CurrentTime}",
timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
);
return Task.CompletedTask;
}
}
```
--------------------------------------------------------------------------------
/mcp_example.json:
--------------------------------------------------------------------------------
```json
{
"servers": {
"hangfire-mcp-standalone": {
"type": "stdio",
"command": "HangfireMCP",
"args": [
"--stdio"
],
"env": {
"HANGFIRE_JOBS_ASSEMBLY": "C:\\Users\\Oleksii_Nikiforov\\dev\\hangfire-mcp\\samples\\HangfireMCP.Standalone\\bin\\Debug\\net9.0\\HangfireJobs.dll",
"HANGFIRE_JOBS_MATCH_EXPRESSION": "[?IsInterface && contains(Name, 'Job')]",
"HANGFIRE_CONNECTION_STRING": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=hangfire"
}
}
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7133;http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7133;http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
```
--------------------------------------------------------------------------------
/samples/Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5296",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7211;http://localhost:5296",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireJobs/ISendMessageJob.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireJobs;
using Microsoft.Extensions.Logging;
public interface ISendMessageJob
{
public Task ExecuteAsync(Message message);
public Task ExecuteAsync(string text);
}
public class Message
{
public string Subject { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
public class SendMessageJob(ILogger<SendMessageJob> logger) : ISendMessageJob
{
public Task ExecuteAsync(string text)
{
logger.LogInformation("Text: {Text}", text);
return Task.CompletedTask;
}
public Task ExecuteAsync(Message message)
{
logger.LogInformation("Subject: {Subject}, Text: {Text}", message.Subject, message.Text);
return Task.CompletedTask;
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/HangfireExtensions.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireMCP;
using System.Globalization;
using Hangfire;
using Hangfire.PostgreSql;
using HangfireMCP;
public static class HangfireExtensions
{
public static void AddHangfire(this WebApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
var defaultCulture = CultureInfo.InvariantCulture;
GlobalConfiguration.Configuration.UseDefaultCulture(
culture: defaultCulture,
uiCulture: defaultCulture,
captureDefault: false
);
builder.Services.AddHangfire(globalConfiguration =>
globalConfiguration.UsePostgreSqlStorage(options =>
options.UseNpgsqlConnection(builder.Configuration.GetConnectionString("hangfire"))
)
);
}
}
```
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
```
<Project>
<PropertyGroup>
<PackageProjectUrl>https://github.com/NikiforovAll/hangfire-mcp</PackageProjectUrl>
<RepositoryUrl>https://github.com/NikiforovAll/hangfire-mcp</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>logo.png</PackageIcon>
<Authors>Nikiforov Oleksii</Authors>
<Description>Hangfire MCP Server</Description>
<PackageTags>mcp;hangfire;ai</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<PropertyGroup>
<RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))</RepoRoot>
</PropertyGroup>
<ItemGroup>
<None Include="$(RepoRoot)\assets\logo.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<None Include="$(RepoRoot)\README.md" Pack="true" PackagePath="" />
</ItemGroup>
</Project>
```
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
```yaml
name-template: "$RESOLVED_VERSION"
tag-template: "$RESOLVED_VERSION"
change-template: "- $TITLE by @$AUTHOR (#$NUMBER)"
no-changes-template: "- No changes"
categories:
- title: "📚 Documentation"
labels:
- "documentation"
- title: "🚀 New Features"
labels:
- "enhancement"
- title: "🐛 Bug Fixes"
labels:
- "bug"
- title: "🧰 Maintenance"
labels:
- "maintenance"
version-resolver:
major:
labels:
- "major"
minor:
labels:
- "minor"
patch:
labels:
- "patch"
default: patch
template: |
$CHANGES
## 👨🏼💻 Contributors
$CONTRIBUTORS
autolabeler:
- label: "documentation"
files:
- "**/*.md"
- label: "enhancement"
files:
- "Source/**/*"
- label: "maintenance"
files:
- ".github/**/*"
- "Benchmarks/**/*"
- "Images/**/*"
- "Tests/**/*"
- "Tools/**/*"
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/HangfireExtensions.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireMCP;
using System.Globalization;
using Hangfire;
using Hangfire.PostgreSql;
using HangfireMCP;
public static class HangfireExtensions
{
public static void AddHangfire(this WebApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
var defaultCulture = CultureInfo.InvariantCulture;
GlobalConfiguration.Configuration.UseDefaultCulture(
culture: defaultCulture,
uiCulture: defaultCulture,
captureDefault: false
);
var connectionString =
Environment.GetEnvironmentVariable("HANGFIRE_CONNECTION_STRING")
?? builder.Configuration.GetConnectionString("hangfire");
builder.Services.AddHangfire(globalConfiguration =>
globalConfiguration.UsePostgreSqlStorage(options =>
options.UseNpgsqlConnection(connectionString)
)
);
}
}
```
--------------------------------------------------------------------------------
/samples/Web/HangfireExtensions.cs:
--------------------------------------------------------------------------------
```csharp
namespace Web;
using System.Globalization;
using Hangfire;
using Hangfire.PostgreSql;
using Web;
public static class HangfireExtensions
{
public static void AddHangfireServer(this WebApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
var defaultCulture = CultureInfo.InvariantCulture;
GlobalConfiguration.Configuration.UseDefaultCulture(
culture: defaultCulture,
uiCulture: defaultCulture,
captureDefault: false
);
builder.Services.AddHangfireServer();
builder.Services.AddHangfire(globalConfiguration =>
globalConfiguration
.UseFilter(new AutomaticRetryAttribute { Attempts = 0 })
.UsePostgreSqlStorage(options =>
options.UseNpgsqlConnection(
builder.Configuration.GetConnectionString("hangfire")
)
)
);
}
}
```
--------------------------------------------------------------------------------
/samples/AppHost/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:18888;http://localhost:18887",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21141",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22060"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:18887",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19176",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20005"
}
}
}
}
```
--------------------------------------------------------------------------------
/samples/AppHost/Program.cs:
--------------------------------------------------------------------------------
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var pgUser = builder.AddParameter("pg-username");
var pgPassword = builder.AddParameter("pg-password", secret: true);
var postgresServer = builder
.AddPostgres("postgres-server", pgUser, pgPassword, 5432)
.WithImageTag("15.7")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent);
var postgresDatabase = postgresServer
.AddDatabase("hangfire")
.WithCreationScript(
"""
CREATE DATABASE hangfire;
"""
);
builder
.AddProject<Projects.Web>("server")
.WithReference(postgresDatabase)
.WaitFor(postgresDatabase);
var mcp = builder
.AddProject<Projects.HangfireMCP_Standalone>("hangfire-mcp")
.WithEnvironment(
"HANGFIRE_JOBS_ASSEMBLY",
@"C:\Users\Oleksii_Nikiforov\dev\hangfire-mcp\samples\HangfireMCP.Standalone\bin\Debug\net9.0\HangfireJobs.dll"
)
.WithEnvironment("HANGFIRE_JOBS_MATCH_EXPRESSION", "[?IsInterface && contains(Name, 'Job')]")
.WithReference(postgresDatabase)
.WaitFor(postgresDatabase);
builder.AddMCPInspector().WithSSE(mcp).WithParentRelationship(mcp);
builder.Build().Run();
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/McpServerExtensions.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireMCP;
public static class McpServerExtensions
{
public static IMcpServerBuilder WithMcpServer(this WebApplicationBuilder builder, string[] args)
{
var isStdio = args.Contains("--stdio");
if (isStdio)
{
builder.WebHost.UseUrls("http://*:0"); // random port
// logs from stderr are shown in the inspector
builder.Services.AddLogging(builder =>
builder
.AddConsole(consoleBuilder =>
{
consoleBuilder.LogToStandardErrorThreshold = LogLevel.Trace;
consoleBuilder.FormatterName = "json";
})
.AddFilter(null, LogLevel.Warning)
);
}
var mcpBuilder = isStdio
? builder.Services.AddMcpServer().WithStdioServerTransport()
: builder.Services.AddMcpServer().WithHttpTransport();
return mcpBuilder;
}
public static WebApplication MapMcpServer(this WebApplication app, string[] args)
{
var isSse = !args.Contains("--stdio");
if (isSse)
{
app.MapMcp();
}
return app;
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/McpServerExtensions.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireMCP;
public static class McpServerExtensions
{
public static IMcpServerBuilder WithMcpServer(this WebApplicationBuilder builder, string[] args)
{
var isStdio = args.Contains("--stdio");
if (isStdio)
{
builder.WebHost.UseUrls("http://*:0"); // random port
// logs from stderr are shown in the inspector
builder.Services.AddLogging(builder =>
builder
.AddConsole(consoleBuilder =>
{
consoleBuilder.LogToStandardErrorThreshold = LogLevel.Trace;
consoleBuilder.FormatterName = "json";
})
.AddFilter(null, LogLevel.Warning)
);
}
var mcpBuilder = isStdio
? builder.Services.AddMcpServer().WithStdioServerTransport()
: builder.Services.AddMcpServer().WithHttpTransport();
return mcpBuilder;
}
public static WebApplication MapMcpServer(this WebApplication app, string[] args)
{
var isSse = !args.Contains("--stdio");
if (isSse)
{
app.MapMcp();
}
return app;
}
}
```
--------------------------------------------------------------------------------
/samples/Web/Program.cs:
--------------------------------------------------------------------------------
```csharp
using Hangfire;
using HangfireJobs;
using Nall.Hangfire.Mcp;
using Web;
var builder = WebApplication.CreateBuilder(args);
builder.AddHangfireServer();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddTransient<ITimeJob, TimeJob>();
builder.Services.AddTransient<ISendMessageJob, SendMessageJob>();
builder.Services.AddHangfireMcp();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseHttpsRedirection();
app.MapHangfireDashboard(string.Empty);
app.MapPost(
"/jobs",
(JobDescriptor jobDescriptor, IHangfireDynamicScheduler scheduler) =>
{
var jobId = scheduler.Enqueue(jobDescriptor, typeof(ITimeJob).Assembly);
return Results.Ok($"Job executed successfully with ID: {jobId}");
}
);
app.MapGet(
"/jobs",
(IHangfireDynamicScheduler scheduler, string? searchTerm) =>
{
var jobs = scheduler.DiscoverJobs(
type =>
type.IsInterface && type.Name.EndsWith("Job", StringComparison.OrdinalIgnoreCase),
(type, method) =>
string.IsNullOrEmpty(searchTerm)
|| $"{type.Name}.{method.Name}".Contains(
searchTerm,
StringComparison.OrdinalIgnoreCase
),
typeof(ITimeJob).Assembly
);
return Results.Ok(jobs);
}
);
app.Run();
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/DynamicJobLoaderHostedService.cs:
--------------------------------------------------------------------------------
```csharp
namespace Nall.Hangfire.Mcp;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
/// <summary>
/// Hosted service that initializes and manages the dynamic job loader.
/// </summary>
public class DynamicJobLoaderHostedService(
DynamicJobLoader dynamicJobLoader,
ILogger<DynamicJobLoaderHostedService> logger
) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Starting dynamic job loader service");
try
{
var initialized = dynamicJobLoader.Initialize();
if (initialized)
{
logger.LogInformation("Dynamic job loader initialized successfully");
// Discover jobs on startup
var jobs = dynamicJobLoader.DiscoverJobs();
logger.LogInformation("Discovered {Count} jobs on startup", jobs.Count());
}
else
{
logger.LogWarning("Dynamic job loader initialization failed or was not configured");
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error during dynamic job loader startup");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Stopping dynamic job loader service");
return Task.CompletedTask;
}
}
```
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
```json
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"inputs": [],
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "shell",
"args": [
"build"
],
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "always",
"revealProblems": "onProblem"
},
// "problemMatcher": "$msCompile",
"detail": "Builds the solution ⚙️",
"icon": {
"color": "terminal.ansiGreen"
}
},
{
"label": "cake:build",
"command": "dotnet",
"type": "shell",
"args": [
"cake",
"--target",
"build"
],
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"reveal": "always",
"revealProblems": "onProblem"
},
"problemMatcher": "$msCompile",
"detail": "Builds the solution ⚙️",
"icon": {
"color": "terminal.ansiGreen"
}
},
{
"label": "cake:test",
"command": "dotnet",
"type": "shell",
"args": [
"cake",
"--target",
"test"
],
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"revealProblems": "onProblem"
},
"problemMatcher": "$msCompile",
"detail": "Tests the solution 🧪",
"icon": {
"color": "terminal.ansiYellow"
}
},
{
"label": "cake:pack",
"command": "dotnet",
"type": "shell",
"args": [
"cake",
"--target",
"pack"
],
"group": {
"kind": "none",
"isDefault": false
},
"presentation": {
"reveal": "always",
"revealProblems": "onProblem"
},
"problemMatcher": "$msCompile",
"detail": "Packs the solution 📦",
"icon": {
"color": "terminal.ansiBlue"
}
}
]
}
```
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
```yaml
name: Build
on:
push:
branches:
- main
pull_request:
release:
types:
- published
workflow_dispatch:
env:
# Disable the .NET logo in the console output.
DOTNET_NOLOGO: true
# Disable the .NET first time experience to skip caching NuGet packages and speed up the build.
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# Disable sending .NET CLI telemetry to Microsoft.
DOTNET_CLI_TELEMETRY_OPTOUT: true
# Set the build number in MinVer.
MINVERBUILDMETADATA: build.${{github.run_number}}
jobs:
build:
name: Build-${{matrix.os}}
runs-on: ${{matrix.os}}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: "Checkout"
uses: actions/checkout@v4
with:
lfs: true
fetch-depth: 0
- name: "Install .NET SDK"
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.201
- name: "Dotnet Tool Restore"
run: dotnet tool restore
shell: pwsh
- name: "Dotnet Cake Build"
run: dotnet cake --target=Build
shell: pwsh
# - name: "Dotnet Cake Test"
# run: dotnet cake --target=Test
# shell: pwsh
# - name: "Dotnet Cake Pack"
# run: dotnet cake --target=Pack
# shell: pwsh
# - name: "Publish Artefacts"
# uses: actions/upload-artifact@v4
# with:
# name: ${{matrix.os}}
# path: "./Artefacts"
# push-github-packages:
# name: "Push GitHub Packages"
# needs: build
# if: github.ref == 'refs/heads/main' || github.event_name == 'release'
# environment:
# name: "GitHub Packages"
# url: https://github.com/NikiforovAll/hangfire-mcp/packages/
# permissions:
# packages: write
# runs-on: windows-latest
# steps:
# - name: "Download Artefact"
# uses: actions/download-artifact@v4
# with:
# name: "windows-latest"
# - name: "Dotnet NuGet Add Source"
# run: dotnet nuget add source https://nuget.pkg.github.com/nikiforovall/index.json --name GitHub --username nikiforovall --password ${{secrets.GITHUB_TOKEN}}
# shell: pwsh
# - name: "Dotnet NuGet Push"
# run: dotnet nuget push .\*.nupkg --api-key ${{ github.token }} --source GitHub --skip-duplicate
# shell: pwsh
# push-nuget:
# name: "Push NuGet Packages"
# needs: build
# if: github.event_name == 'release'
# environment:
# name: "NuGet"
# url: https://www.nuget.org/packages/Nall.Hangfire.Mcp
# runs-on: windows-latest
# steps:
# - name: "Download Artefact"
# uses: actions/download-artifact@v4
# with:
# name: "windows-latest"
# - name: "Dotnet NuGet Push"
# run: |
# Get-ChildItem .\ -Filter *.nupkg |
# Where-Object { !$_.Name.Contains('preview') } |
# ForEach-Object { dotnet nuget push $_ --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{secrets.NUGET_API_KEY}} }
# shell: pwsh
```
--------------------------------------------------------------------------------
/samples/HangfireMCP/HangfireTool.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireMCP;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Hangfire;
using HangfireJobs;
using Nall.Hangfire.Mcp;
[McpServerToolType]
public class HangfireTool(
IHangfireDynamicScheduler scheduler,
IBackgroundJobClient backgroundJobClient
)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
[
McpServerTool(Name = "RunJob"),
Description("Invokes a job with the given jobName, methodName, and parameters")
]
[return: Description("The job ID of the enqueued job")]
public string Run(
[Required] string jobName,
[Required] string methodName,
Dictionary<string, object>? parameters = null
)
{
ArgumentNullException.ThrowIfNull(jobName);
ArgumentNullException.ThrowIfNull(methodName);
var descriptor = new JobDescriptor(jobName, methodName, parameters);
return scheduler.Enqueue(descriptor, typeof(ITimeJob).Assembly);
}
[McpServerTool(Name = "ListJobs"), Description("Lists all jobs")]
[return: Description("An array of job descriptors in JSON format")]
public string ListJobs(
[Description(
"The search term to filter job names, optional parameter, use it only when users want to search for jobs"
)]
string? searchTerm = null
)
{
var jobs = scheduler.DiscoverJobs(
type =>
type.IsInterface && type.Name.EndsWith("Job", StringComparison.OrdinalIgnoreCase),
(type, method) =>
string.IsNullOrEmpty(searchTerm)
|| $"{type.Name}.{method.Name}".Contains(
searchTerm,
StringComparison.OrdinalIgnoreCase
),
typeof(ITimeJob).Assembly
);
return JsonSerializer.Serialize(
jobs.Select(job => new
{
job.JobName,
job.MethodName,
job.Parameters,
}),
JsonOptions
);
}
[McpServerTool(Name = "GetJobById"), Description("Gets job details by job ID")]
[return: Description("The job details as JSON, or null if not found")]
public string? GetJobById([Required, Description("The job ID")] string jobId)
{
ArgumentNullException.ThrowIfNull(jobId);
using var connection = JobStorage.Current.GetConnection();
var jobData = connection.GetJobData(jobId);
if (jobData == null)
{
return null;
}
var result = new
{
JobId = jobId,
jobData.State,
jobData.CreatedAt,
Arguments = jobData.Job?.Args,
Method = jobData.Job?.Method?.Name,
Type = jobData.Job?.Type?.FullName,
};
return JsonSerializer.Serialize(result, JsonOptions);
}
[McpServerTool(Name = "RequeueJob"), Description("Requeues an existing job by ID")]
[return: Description("The new job ID if successful, null if the original job was not found")]
public string? RequeueJob([Required, Description("The job ID to requeue")] string jobId)
{
ArgumentNullException.ThrowIfNull(jobId);
backgroundJobClient.Requeue(jobId);
return this.GetJobById(jobId);
}
}
```
--------------------------------------------------------------------------------
/samples/HangfireMCP.Standalone/HangfireTool.cs:
--------------------------------------------------------------------------------
```csharp
namespace HangfireMCP;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Hangfire;
using Nall.Hangfire.Mcp;
[McpServerToolType]
public class HangfireTool(
IHangfireDynamicScheduler scheduler,
IBackgroundJobClient backgroundJobClient,
DynamicJobLoader dynamicJobLoader,
ILogger<HangfireTool> logger
)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
[
McpServerTool(Name = "RunJob"),
Description("Invokes a job with the given jobName, methodName, and parameters")
]
[return: Description("The job ID of the enqueued job")]
public string Run(
[Required] string jobName,
[Required] string methodName,
Dictionary<string, object>? parameters = null
)
{
ArgumentNullException.ThrowIfNull(jobName);
ArgumentNullException.ThrowIfNull(methodName);
var descriptor = new JobDescriptor(jobName, methodName, parameters);
var assembly =
dynamicJobLoader.GetAssembly()
?? throw new InvalidOperationException(
"Dynamic job loader is not initialized or assembly is not loaded."
);
return scheduler.Enqueue(descriptor, assembly);
}
[McpServerTool(Name = "ListJobs"), Description("Lists all jobs")]
[return: Description("An array of job descriptors in JSON format")]
public string ListJobs(
[Description(
"The search term to filter job names, optional parameter, use it only when users want to search for jobs"
)]
string? searchTerm = null
)
{
var jobsList = new List<JobDescriptor>();
try
{
var dynamicJobs = dynamicJobLoader.DiscoverJobs();
if (dynamicJobs.Any())
{
// Filter dynamic jobs by search term if provided
if (!string.IsNullOrEmpty(searchTerm))
{
dynamicJobs = dynamicJobs.Where(job =>
$"{job.JobName}.{job.MethodName}".Contains(
searchTerm,
StringComparison.OrdinalIgnoreCase
)
);
}
jobsList.AddRange(dynamicJobs);
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error discovering dynamic jobs");
}
return JsonSerializer.Serialize(
jobsList.Select(job => new
{
job.JobName,
job.MethodName,
job.Parameters,
}),
JsonOptions
);
}
[McpServerTool(Name = "GetJobById"), Description("Gets job details by job ID")]
[return: Description("The job details as JSON, or null if not found")]
public string? GetJobById([Required, Description("The job ID")] string jobId)
{
ArgumentNullException.ThrowIfNull(jobId);
using var connection = JobStorage.Current.GetConnection();
var jobData = connection.GetJobData(jobId);
if (jobData == null)
{
return null;
}
var result = new
{
JobId = jobId,
jobData.State,
jobData.CreatedAt,
Arguments = jobData.Job?.Args,
Method = jobData.Job?.Method?.Name,
Type = jobData.Job?.Type?.FullName,
};
return JsonSerializer.Serialize(result, JsonOptions);
}
[McpServerTool(Name = "RequeueJob"), Description("Requeues an existing job by ID")]
[return: Description("The new job ID if successful, null if the original job was not found")]
public string? RequeueJob([Required, Description("The job ID to requeue")] string jobId)
{
ArgumentNullException.ThrowIfNull(jobId);
backgroundJobClient.Requeue(jobId);
return this.GetJobById(jobId);
}
}
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/DynamicJobLoader.cs:
--------------------------------------------------------------------------------
```csharp
namespace Nall.Hangfire.Mcp;
using System.Reflection;
using DevLab.JmesPath;
using Microsoft.Extensions.Logging;
/// <summary>
/// Provides functionality for dynamically loading and filtering job types from external assemblies.
/// </summary>
public class DynamicJobLoader(IHangfireDynamicScheduler scheduler, ILogger<DynamicJobLoader> logger)
{
private string? assemblyPath;
public Assembly? LoadedAssembly { get; set; }
/// <summary>
/// Initializes the dynamic job loader with assembly path and JMESPath expression from environment variables.
/// </summary>
/// <returns>True if initialization succeeded, false otherwise.</returns>
public bool Initialize()
{
try
{
// Get assembly path from environment variable
this.assemblyPath = Environment.GetEnvironmentVariable("HANGFIRE_JOBS_ASSEMBLY");
if (string.IsNullOrWhiteSpace(this.assemblyPath))
{
logger.LogWarning("HANGFIRE_JOBS_ASSEMBLY environment variable not set or empty");
return false;
}
// Attempt to load the assembly
if (!this.LoadAssembly())
{
return false;
}
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to initialize dynamic job loader");
return false;
}
}
/// <summary>
/// Loads the assembly from the configured path.
/// </summary>
/// <returns>True if assembly loaded successfully, false otherwise.</returns>
private bool LoadAssembly()
{
if (string.IsNullOrEmpty(this.assemblyPath))
{
return false;
}
try
{
if (!File.Exists(this.assemblyPath))
{
logger.LogError("Assembly file not found: {Path}", this.assemblyPath);
return false;
}
// Load the assembly
this.LoadedAssembly = Assembly.LoadFrom(this.assemblyPath);
logger.LogInformation(
"Successfully loaded assembly: {AssemblyName} from {Path}",
this.LoadedAssembly.FullName,
this.assemblyPath
);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to load assembly from {Path}", this.assemblyPath);
this.LoadedAssembly = null;
return false;
}
}
/// <summary>
/// Discovers job types in the loaded assembly that match the JMESPath expression.
/// </summary>
/// <returns>A collection of job descriptors for the matched types.</returns>
public IEnumerable<JobDescriptor> DiscoverJobs()
{
if (this.LoadedAssembly == null)
{
logger.LogWarning("No assembly loaded. Attempting to initialize...");
if (!this.Initialize() || this.LoadedAssembly == null)
{
logger.LogError("Failed to initialize or load assembly. Aborting job discovery.");
return [];
}
}
try
{
// Get JMESPath expression from environment variable
var expressionString = Environment.GetEnvironmentVariable(
"HANGFIRE_JOBS_MATCH_EXPRESSION"
);
if (string.IsNullOrWhiteSpace(expressionString))
{
logger.LogWarning(
"HANGFIRE_JOBS_MATCH_EXPRESSION environment variable not set or empty"
);
return [];
}
// Create JmesPath expression
var jmesPath = new JmesPath();
// Build a collection of type information as JObject to use with JMESPath
var types = this
.LoadedAssembly.GetTypes()
.Select(type => new
{
type.Name,
type.FullName,
type.Namespace,
type.IsPublic,
type.IsClass,
type.IsInterface,
})
.ToList();
// Serialize to JSON for JmesPath to process
var typesJson = System.Text.Json.JsonSerializer.Serialize(types);
// Apply JmesPath expression to filter the types
var result = jmesPath.Transform(typesJson, expressionString);
if (string.IsNullOrEmpty(result) || result == "null" || result == "[]")
{
logger.LogWarning(
"JMESPath expression '{Expression}' did not match any types",
expressionString
);
return [];
}
// Deserialize the filtered results
var filteredTypeInfo = System.Text.Json.JsonSerializer.Deserialize<
List<System.Text.Json.JsonElement>
>(result);
if (filteredTypeInfo == null || filteredTypeInfo.Count == 0)
{
logger.LogWarning("No types matched the JMESPath expression");
return [];
}
// Get the full names of the matched types
var matchedFullNames = filteredTypeInfo
.Select(element => element.GetProperty("FullName").GetString())
.Where(name => !string.IsNullOrEmpty(name))
.ToList();
// Discover jobs from these types
var jobs = scheduler.DiscoverJobs(
type => matchedFullNames.Contains(type.FullName),
assembly: this.LoadedAssembly
);
logger.LogInformation(
"Discovered {Count} jobs from {AssemblyName}",
jobs.Count(),
this.LoadedAssembly.GetName().Name
);
return jobs;
}
catch (Exception ex)
{
logger.LogError(ex, "Error discovering jobs with JMESPath expression");
return [];
}
}
/// <summary>
/// Gets the loaded assembly.
/// </summary>
/// <returns>The loaded assembly or null if no assembly is loaded.</returns>
public Assembly? GetAssembly() => this.LoadedAssembly;
}
```
--------------------------------------------------------------------------------
/src/Nall.Hangfire.Mcp/HangfireDynamicScheduler.cs:
--------------------------------------------------------------------------------
```csharp
namespace Nall.Hangfire.Mcp;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;
using global::Hangfire;
public interface IHangfireDynamicScheduler
{
/// <summary>
/// Enqueues a job based on the provided <see cref="JobDescriptor"/>.
/// </summary>
public string Enqueue(JobDescriptor descriptor, Assembly? assembly = null);
/// <summary>
/// Discovers jobs in the specified assembly.
/// </summary>
public IEnumerable<JobDescriptor> DiscoverJobs(
Func<Type, bool> nameSelector,
Func<Type, MethodInfo, bool>? methodSelector = null,
Assembly? assembly = null
);
}
public class HangfireDynamicScheduler(IBackgroundJobClient client) : IHangfireDynamicScheduler
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
/// <summary>
/// Enqueues a job based on the provided <see cref="JobDescriptor"/>.
/// </summary>
public string Enqueue(JobDescriptor descriptor, Assembly? assembly = null)
{
ArgumentNullException.ThrowIfNull(descriptor);
assembly ??= Assembly.GetExecutingAssembly();
var type =
assembly.GetType(descriptor.JobName)
?? throw new InvalidOperationException($"Type '{descriptor.JobName}' not found.");
return type.IsInterface
? this.EnqueueByInterfaceName(type, descriptor.MethodName, descriptor.Parameters)
: this.EnqueueByTypeName(type, descriptor.MethodName, descriptor.Parameters);
}
private string EnqueueByInterfaceName(
Type iface,
string methodName,
IDictionary<string, object>? parameters
)
{
// Find the correct method overload based on parameter types
var method =
FindMethodByNameAndParameters(iface, methodName, parameters)
?? throw new InvalidOperationException(
$"Method '{methodName}' not found on '{iface.FullName}' with matching parameters."
);
var param = Expression.Parameter(iface, "x");
var methodParams = method.GetParameters();
var arguments = BuildMethodArguments(methodParams, parameters);
var call = Expression.Call(param, method, arguments);
var lambdaType = typeof(Func<,>).MakeGenericType(iface, typeof(Task));
var lambda = Expression.Lambda(lambdaType, call, param);
var enqueueMethod = GetEnqueueMethod();
var genericEnqueue = enqueueMethod.MakeGenericMethod(iface);
return (string)(
genericEnqueue.Invoke(null, [client, lambda])
?? throw new InvalidOperationException("Failed to enqueue job")
);
}
private static MethodInfo? FindMethodByNameAndParameters(
Type type,
string methodName,
IDictionary<string, object>? parameters
)
{
// Get all methods with the given name
var candidateMethods = type.GetMethods(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
)
.Where(m => m.Name == methodName)
.ToList();
if (candidateMethods.Count == 0)
{
return null;
}
// If only one method with this name exists, return it
if (candidateMethods.Count == 1)
{
return candidateMethods[0];
}
// If we have parameters, try to find the best match by parameter names
if (parameters != null && parameters.Count > 0)
{
var paramNames = parameters.Keys.ToHashSet(StringComparer.OrdinalIgnoreCase);
// Find methods where all parameters have matching names
var matchingMethods = candidateMethods
.Where(m =>
{
var methodParamNames = m.GetParameters()
.Select(p => p.Name)
.Where(n => n != null)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
// Check if all required parameters (non-optional) have values
var requiredParams = m.GetParameters()
.Where(p => !p.HasDefaultValue)
.Select(p => p.Name)
.Where(n => n != null);
return requiredParams.All(rp => rp != null && paramNames.Contains(rp));
})
.ToList();
if (matchingMethods.Count == 1)
{
return matchingMethods[0];
}
// If multiple methods match, select the one with the most matching parameters
if (matchingMethods.Count > 1)
{
return matchingMethods
.OrderByDescending(m =>
{
var methodParamNames = m.GetParameters()
.Select(p => p.Name)
.Where(n => n != null);
return methodParamNames.Count(mpn =>
mpn != null && paramNames.Contains(mpn)
);
})
.FirstOrDefault();
}
}
// If we can't find by parameter names, just return the first method
return candidateMethods.FirstOrDefault();
}
private static Expression[] BuildMethodArguments(
ParameterInfo[] methodParams,
IDictionary<string, object>? parameters
)
{
var arguments = new Expression[methodParams.Length];
for (var i = 0; i < methodParams.Length; i++)
{
var methodParam = methodParams[i];
if (parameters != null && parameters.TryGetValue(methodParam.Name!, out var value))
{
var convertedValue = value;
if (value is JsonElement jsonElement)
{
convertedValue = GetValue(methodParam, jsonElement);
}
arguments[i] = Expression.Constant(convertedValue, methodParam.ParameterType);
}
else if (methodParam.HasDefaultValue)
{
arguments[i] = Expression.Constant(
methodParam.DefaultValue,
methodParam.ParameterType
);
}
else
{
throw new ArgumentException(
$"Required parameter '{methodParam.Name}' was not provided"
);
}
}
return arguments;
}
private static object? GetValue(ParameterInfo methodParam, JsonElement jsonElement) =>
jsonElement.ValueKind switch
{
JsonValueKind.String => jsonElement.GetString(),
JsonValueKind.Number when methodParam.ParameterType == typeof(int) =>
jsonElement.GetInt32(),
JsonValueKind.Number when methodParam.ParameterType == typeof(long) =>
jsonElement.GetInt64(),
JsonValueKind.Number when methodParam.ParameterType == typeof(double) =>
jsonElement.GetDouble(),
JsonValueKind.True or JsonValueKind.False => jsonElement.GetBoolean(),
JsonValueKind.Object => JsonSerializer.Deserialize(
jsonElement.GetRawText(),
methodParam.ParameterType,
JsonSerializerOptions
),
JsonValueKind.Array => JsonSerializer.Deserialize(
jsonElement.GetRawText(),
methodParam.ParameterType,
JsonSerializerOptions
),
_ => throw new ArgumentException(
$"Unsupported parameter type '{methodParam.ParameterType}' for '{methodParam.Name}'"
),
};
private static MethodInfo GetEnqueueMethod()
{
return typeof(BackgroundJobClientExtensions)
.GetMethods()
.First(m =>
{
var p = m.GetParameters()[1].ParameterType;
if (!p.IsGenericType || p.GetGenericTypeDefinition() != typeof(Expression<>))
{
return false;
}
var inner = p.GetGenericArguments()[0];
return inner.IsGenericType
&& inner.GetGenericTypeDefinition() == typeof(Func<,>)
&& inner.GetGenericArguments()[1] == typeof(Task);
});
}
private string EnqueueByTypeName(
Type type,
string methodName,
IDictionary<string, object>? parameters
)
{
// Find the correct method overload based on parameter types
var method =
FindMethodByNameAndParameters(type, methodName, parameters)
?? throw new InvalidOperationException(
$"Method '{methodName}' not found on type '{type.FullName}' with matching parameters."
);
var param = Expression.Parameter(type, "x");
var methodParams = method.GetParameters();
var arguments = BuildMethodArguments(methodParams, parameters);
var call = Expression.Call(param, method, arguments);
var lambdaType = typeof(Func<,>).MakeGenericType(type, typeof(Task));
var lambda = Expression.Lambda(lambdaType, call, param);
var enqueueMethod = GetEnqueueMethod();
var genericEnqueue = enqueueMethod.MakeGenericMethod(type);
return (string)(
genericEnqueue.Invoke(null, [client, lambda])
?? throw new InvalidOperationException("Failed to enqueue job")
);
}
public IEnumerable<JobDescriptor> DiscoverJobs(
Func<Type, bool> nameSelector,
Func<Type, MethodInfo, bool>? methodSelector = null,
Assembly? assembly = null
)
{
assembly ??= Assembly.GetExecutingAssembly();
var results = new List<JobDescriptor>();
// Get all types from the assembly that match the name selector
var types = assembly
.GetTypes()
.Where(t => (t.IsClass || t.IsInterface) && nameSelector(t))
.ToList();
foreach (var type in types)
{ // Get all public methods from the type, excluding standard Object methods
var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(m =>
!m.IsSpecialName
&& // Exclude property getters/setters
!IsStandardObjectMethod(m)
&& (methodSelector?.Invoke(type, m) ?? true)
) // Exclude standard Object methods
.ToList();
foreach (var method in methods)
{
// Create parameter dictionary with default values
var paramDict = new Dictionary<string, object>();
foreach (var param in method.GetParameters())
{
if (param.Name != null)
{
paramDict[param.Name] = CreateDefaultValue(param.ParameterType);
}
}
results.Add(new JobDescriptor(type.FullName!, method.Name, paramDict));
}
}
return results;
}
private static bool IsStandardObjectMethod(MethodInfo method)
{
return method.DeclaringType == typeof(object)
|| method.Name is "ToString" or "GetType" or "Equals" or "GetHashCode";
}
private static object CreateDefaultValue(Type type)
{
if (type == typeof(string))
{
return string.Empty;
}
if (type == typeof(int) || type == typeof(long) || type == typeof(short))
{
return 0;
}
if (type == typeof(double) || type == typeof(float) || type == typeof(decimal))
{
return 0.0;
}
if (type == typeof(bool))
{
return false;
}
if (type == typeof(Guid))
{
return Guid.Empty;
}
if (type == typeof(DateTime))
{
return DateTime.Now;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
return Activator.CreateInstance(type)!;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
return Activator.CreateInstance(type)!;
}
if (type.IsClass && !type.IsAbstract && type != typeof(object))
{
try
{
return Activator.CreateInstance(type) ?? new object();
}
catch
{
return new object();
}
}
return new object();
}
}
```