{"slug": "why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix", "title": "Why Your .NET MCP Tool Classes Crash at Runtime (And the Two-Line Fix)", "summary": "A developer detailed a runtime crash in .NET MCP servers where instance-based tool classes using constructor injection threw a System.Reflection.TargetException because the WithComponentsFrom method registered tool methods but never registered their declaring classes in ASP.NET Core's DI container. The fix, shipped in DotnetFastMCP v2.1, automatically registers every non-static tool class via TryAddTransient and adds parameter descriptions to the JSON Schema so AI models call tools correctly on the first attempt.", "body_md": "If you're building MCP servers in .NET with instance-based tool classes — the kind that take `ILogger<T>`, `HttpClient`, or a repository through constructor injection — there's a good chance you've already hit this at runtime:\n\n```\nSystem.Reflection.TargetException: Non-static method requires a target.\n```\n\nNo warning at startup. No compile-time error. Just a crash the moment an AI model calls your tool in production.\n\nThis post explains why it happens, shows the two-line fix in [DotnetFastMCP](https://github.com/tekspry/DotnetFastMCP) v2.1, and covers a related improvement that makes AI models call your tools correctly on the first attempt — without asking the user for clarification.\n\nThe framework lets you organize your MCP tools into classes and scan them with one line:\n\n```\nbuilder.WithComponentsFrom(Assembly.GetExecutingAssembly());\n```\n\nThis scans your assembly, finds every method decorated with `[McpTool]`, and registers it. Clean, simple, plug-and-play.\n\nExcept — if your tool class had **constructor injection**, you'd get a silent registration. The class would appear in the tool list. But the moment an AI model called it, the server would throw:\n\n```\nSystem.Reflection.TargetException: Non-static method requires a target.\n```\n\nAt runtime. In production. With no warning during startup.\n\nThe cause was straightforward: `WithComponentsFrom` registered the *methods* in the tool dictionary, but never registered the *declaring class* in ASP.NET Core's DI container. When the handler tried to resolve an instance for invocation, there was nothing to resolve.\n\nThe workaround was to manually add every tool class:\n\n```\n// ❌ v2.0: manual registration required for every non-static tool class\nbuilder.Services.AddTransient<ProductSearchTool>();\nbuilder.Services.AddTransient<ImageGenerationTool>();\nbuilder.Services.AddTransient<InventoryTool>();\n// ... one line per class, forever\n```\n\nThis is exactly the opposite of what \"plug-and-play\" should mean.\n\nWhen an AI model calls `tools/list`, the server returns a JSON Schema for each tool's parameters. Here's what that schema looked like in v2.0:\n\n```\n{\n  \"name\": \"search_products\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"query\":  { \"type\": \"string\" },\n      \"limit\":  { \"type\": \"integer\" },\n      \"filter\": { \"type\": \"string\" }\n    },\n    \"required\": [\"query\"]\n  }\n}\n```\n\nNo descriptions. Just types and names. The model had to guess what `filter` means. Is it a category? A regex? A SQL WHERE clause?\n\nBoth issues are fixed in v2.1. Install the update:\n\n```\ndotnet add package DotnetFastMCP --version 2.1.1\n```\n\nStarting in v2.1, `WithComponentsFrom` automatically registers every non-static tool class in the DI container using `TryAddTransient`:\n\n```\npublic class ProductSearchTool\n{\n    private readonly IProductRepository _repo;\n    private readonly ILogger<ProductSearchTool> _logger;\n\n    // Constructor injection — works automatically in v2.1\n    public ProductSearchTool(IProductRepository repo, ILogger<ProductSearchTool> logger)\n    {\n        _repo = repo;\n        _logger = logger;\n    }\n\n    [McpTool(\"search_products\", Description = \"Searches the product catalog\")]\n    public async Task<string> SearchAsync(string query, int limit = 10)\n    {\n        _logger.LogInformation(\"Searching for '{Query}'\", query);\n        var results = await _repo.SearchAsync(query, limit);\n        return JsonSerializer.Serialize(results);\n    }\n}\n```\n\nYour `Program.cs` stays clean — register only the *dependencies*, not the tool class:\n\n``` js\nvar mcpServer = new FastMCPServer(\"my-server\");\nvar builder = McpServerBuilder.Create(mcpServer, args);\nbuilder.Services.AddScoped<IProductRepository, SqlProductRepository>();\nbuilder.WithComponentsFrom(Assembly.GetExecutingAssembly());\nvar app = builder.Build();\nawait app.RunMcpAsync(args);\n```\n\n`TryAddTransient` means existing registrations (like `AddHttpClient<T>`) are preserved — no collisions.\n\n`[McpDescription]` — Tell the AI What Each Parameter Means\n\n```\n[McpTool(\"search_products\", Description = \"Searches the product catalog\")]\npublic async Task<string> SearchAsync(\n    [McpDescription(\"The search query string, e.g. 'red cotton saree'\")] string query,\n    [McpDescription(\"Maximum number of results to return (1–100)\")] int limit = 10,\n    [McpDescription(\"Filter by category: 'clothing', 'accessories', 'footwear'\")] string? category = null)\n{\n    ...\n}\n```\n\nNow `tools/list` returns descriptions in the schema. The model knows exactly what to pass without asking the user.\n\nFramework-injected parameters (`McpContext`, `CancellationToken`, `ClaimsPrincipal`) are always excluded automatically.\n\n```\ngit clone https://github.com/tekspry/DotnetFastMCP.git\ncd DotnetFastMCP/examples/BasicServer\ndotnet run -- --urls http://localhost:5100\n```\n\nThen call the non-static tool:\n\n```\ncurl -s -X POST http://localhost:5100/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"greet_user\",\"arguments\":{\"name\":\"Alice\",\"style\":\"formal\"}}}' | jq .\n```\n\nResponse: `\"Good day, Alice. How may I assist you today?\"`\n\nNo manual DI registration. No runtime crash. Constructor injection just works.\n\nNo breaking changes. Update the package version, remove manual `AddTransient<MyToolClass>()` calls (now redundant), and add `[McpDescription]` to your parameters.\n\n*DotnetFastMCP is MIT-licensed and open for contributions.*", "url": "https://wpnews.pro/news/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix", "canonical_source": "https://dev.to/tekspry/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix-lcb", "published_at": "2026-09-13 19:28:37+00:00", "updated_at": "2026-09-13 19:50:31.897384+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["DotnetFastMCP", "ASP.NET Core", ".NET", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix", "markdown": "https://wpnews.pro/news/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix.md", "text": "https://wpnews.pro/news/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix.txt", "jsonld": "https://wpnews.pro/news/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix.jsonld"}}