.NET MCP 服务器中构造函数依赖注入的工具类导致 TargetException 运行时崩溃,DotnetFastMCP v2.1 给出两行修复方案并优化了首次调用准确率。
如果你在 .NET 中使用基于实例的工具类来构建 MCP 服务器——这类类通过构造函数注入 ILogger<T>、HttpClient 或 repository——那么在运行时很可能已经遇到过这个错误:
System.Reflection.TargetException: Non-static method requires a target.
启动时没有任何警告。没有编译时错误。只有在生产环境中 AI 模型调用你的工具时才会崩溃。
这篇文章解释了这个问题的原因,展示了 DotnetFastMCP v2.1 中的两行修复方案,并介绍了一个相关的改进——让 AI 模型在第一次尝试时就能正确调用你的工具,而不需要向用户澄清。
框架允许你将 MCP 工具组织成类,用一行代码扫描注册:
builder.WithComponentsFrom(Assembly.GetExecutingAssembly());
这会扫描你的程序集,找到所有用 [McpTool] 装饰的方法并注册它们。干净、简单、即插即用。
然而——如果你的工具类有构造函数注入,你会得到一个静默的注册。类会出现在工具列表中。但当 AI 模型调用它时,服务器会抛出:
System.Reflection.TargetException: Non-static method requires a target.
发生在运行时。生产环境中。启动时没有任何警告。
原因很简单:WithComponentsFrom 在工具字典中注册了方法,但从未在 ASP.NET Core 的 DI 容器中注册声明类。当处理器尝试解析一个实例来调用时,没有东西可以解析。
变通方案是手动添加每个工具类:
// ❌ v2.0: 每个非静态工具类都需要手动注册
builder.Services.AddTransient<ProductSearchTool>();
builder.Services.AddTransient<ImageGenerationTool>();
builder.Services.AddTransient<InventoryTool>();
// ... 每个类一行,永无止境
这恰恰与"即插即用"的含义背道而驰。
当 AI 模型调用 tools/list 时,服务器返回每个工具参数的 JSON Schema。以下是 v2.0 中这个 schema 的样子:
{
"name": "search_products",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer" },
"filter": { "type": "string" }
},
"required": ["query"]
}
}
没有描述。只有类型和名称。模型只能猜测 filter 是什么意思。它是一个分类?一个正则表达式?一个 SQL WHERE 子句?
两个问题在 v2.1 中都已修复。安装更新:
dotnet add package DotnetFastMCP --version 2.1.1
从 v2.1 开始,WithComponentsFrom 使用 TryAddTransient 自动在 DI 容器中注册每个非静态工具类:
public class ProductSearchTool
{
private readonly IProductRepository _repo;
private readonly ILogger<ProductSearchTool> _logger;
// 构造函数注入 — 在 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);
}
}
你的 Program.cs 保持干净——只需注册依赖项,不需要注册工具类:
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 意味着现有的注册(如 AddHttpClient<T>)会被保留——不会冲突。
[McpDescription] — 告诉 AI 每个参数的含义[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)
{
...
}
现在 tools/list 在 schema 中返回描述。模型确切知道该传递什么,而不需要询问用户。
框架注入的参数(McpContext、CancellationToken、ClaimsPrincipal)始终自动排除。
git clone https://github.com/tekspry/DotnetFastMCP.git
cd DotnetFastMCP/examples/BasicServer
dotnet run -- --urls http://localhost:5100
然后调用非静态工具:
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 .
响应:"Good day, Alice. How may I assist you today?"
无需手动 DI 注册。无运行时崩溃。构造函数注入自动生效。
无破坏性变更。更新包版本,移除手动 AddTransient<MyToolClass>() 调用(现在已是冗余的),并在参数上添加 [McpDescription]。
📦 DotnetFastMCP on NuGet
💻 GitHub: tekspry/DotnetFastMCP
🧪 真实案例:FashionAccessoryPipeline
DotnetFastMCP 采用 MIT 许可,欢迎贡献。