# Why Your .NET MCP Tool Classes Crash at Runtime (And the Two-Line Fix)

> Source: <https://dev.to/tekspry/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix-lcb>
> Published: 2026-09-13 19:28:37+00:00

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:

```
System.Reflection.TargetException: Non-static method requires a target.
```

No warning at startup. No compile-time error. Just a crash the moment an AI model calls your tool in production.

This 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.

The framework lets you organize your MCP tools into classes and scan them with one line:

```
builder.WithComponentsFrom(Assembly.GetExecutingAssembly());
```

This scans your assembly, finds every method decorated with `[McpTool]`, and registers it. Clean, simple, plug-and-play.

Except — 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:

```
System.Reflection.TargetException: Non-static method requires a target.
```

At runtime. In production. With no warning during startup.

The 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.

The workaround was to manually add every tool class:

```
// ❌ v2.0: manual registration required for every non-static tool class
builder.Services.AddTransient<ProductSearchTool>();
builder.Services.AddTransient<ImageGenerationTool>();
builder.Services.AddTransient<InventoryTool>();
// ... one line per class, forever
```

This is exactly the opposite of what "plug-and-play" should mean.

When 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:

```
{
  "name": "search_products",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query":  { "type": "string" },
      "limit":  { "type": "integer" },
      "filter": { "type": "string" }
    },
    "required": ["query"]
  }
}
```

No descriptions. Just types and names. The model had to guess what `filter` means. Is it a category? A regex? A SQL WHERE clause?

Both issues are fixed in v2.1. Install the update:

```
dotnet add package DotnetFastMCP --version 2.1.1
```

Starting in v2.1, `WithComponentsFrom` automatically registers every non-static tool class in the DI container using `TryAddTransient`:

```
public class ProductSearchTool
{
    private readonly IProductRepository _repo;
    private readonly ILogger<ProductSearchTool> _logger;

    // Constructor injection — works automatically in v2.1
    public ProductSearchTool(IProductRepository repo, ILogger<ProductSearchTool> logger)
    {
        _repo = repo;
        _logger = logger;
    }

    [McpTool("search_products", Description = "Searches the product catalog")]
    public async Task<string> SearchAsync(string query, int limit = 10)
    {
        _logger.LogInformation("Searching for '{Query}'", query);
        var results = await _repo.SearchAsync(query, limit);
        return JsonSerializer.Serialize(results);
    }
}
```

Your `Program.cs` stays clean — register only the *dependencies*, not the tool class:

``` js
var mcpServer = new FastMCPServer("my-server");
var builder = McpServerBuilder.Create(mcpServer, args);
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
builder.WithComponentsFrom(Assembly.GetExecutingAssembly());
var app = builder.Build();
await app.RunMcpAsync(args);
```

`TryAddTransient` means existing registrations (like `AddHttpClient<T>`) are preserved — no collisions.

`[McpDescription]` — Tell the AI What Each Parameter Means

```
[McpTool("search_products", Description = "Searches the product catalog")]
public async Task<string> SearchAsync(
    [McpDescription("The search query string, e.g. 'red cotton saree'")] string query,
    [McpDescription("Maximum number of results to return (1–100)")] int limit = 10,
    [McpDescription("Filter by category: 'clothing', 'accessories', 'footwear'")] string? category = null)
{
    ...
}
```

Now `tools/list` returns descriptions in the schema. The model knows exactly what to pass without asking the user.

Framework-injected parameters (`McpContext`, `CancellationToken`, `ClaimsPrincipal`) are always excluded automatically.

```
git clone https://github.com/tekspry/DotnetFastMCP.git
cd DotnetFastMCP/examples/BasicServer
dotnet run -- --urls http://localhost:5100
```

Then call the non-static tool:

```
curl -s -X POST http://localhost:5100/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"greet_user","arguments":{"name":"Alice","style":"formal"}}}' | jq .
```

Response: `"Good day, Alice. How may I assist you today?"`

No manual DI registration. No runtime crash. Constructor injection just works.

No breaking changes. Update the package version, remove manual `AddTransient<MyToolClass>()` calls (now redundant), and add `[McpDescription]` to your parameters.

*DotnetFastMCP is MIT-licensed and open for contributions.*
