Are you the author? Sign in to claim
.NET MCP bridge: expose app methods/data as MCP tools, prompts, and resources via an in-app plugin + lightweight server
MCP Plugin for .NET is a comprehensive solution for integrating .NET applications with the Model Context Protocol (MCP). It allows you to easily expose methods and data from your .NET applications as Tools, Prompts, and Resources to AI assistants (like Claude) and other MCP clients.
Standard MCP servers are typically designed to be launched as subprocesses by the client (e.g., Claude Desktop spawns a Python script). This works well for lightweight scripts but creates challenges for complex .NET applications like Unity Engine, WPF Desktop Apps, or Game Servers:
This project solves this by decoupling the MCP Server from your application using a Bridge Architecture:
Why SignalR?
8080). No complex firewall rules.The system uses a hub-and-spoke architecture where McpPlugin.Server acts as the central gateway.
graph LR
subgraph "Your .NET Apps (SignalR Clients)"
A[Unity Editor] -- SignalR --> S
B[WPF Desktop] -- SignalR --> S
E[Game Server] -- SignalR --> S
end
subgraph "MCP Infrastructure (Bridge)"
S[McpPlugin.Server]
end
subgraph "AI / MCP Clients"
C[Claude Desktop] -- StdIO/HTTP --> S
D[MCP Inspector] -- StdIO/HTTP --> S
end
[AiTool], [AiPrompt], and [AiResource] attributes. (The legacy [McpPluginTool], [McpPluginPrompt], [McpPluginResource] names remain available as [Obsolete] aliases for backward compatibility.)stdio (for local AI agents like Claude Desktop) and streamableHttp (for remote connections).Microsoft.Extensions.DependencyInjection.A key feature of this architecture is the use of SignalR for the connection between your application (McpPlugin) and the bridge (McpPlugin.Server).
8080). No complex firewall rules or multiple socket connections are required.Default Connection:
http://localhost:8080/hub/mcp-serverhttp://localhost:8080/hub/mcp-serverMcpPlugin.Server)The server acts as a hub. You can run the provided DemoWebApp or host it in your own ASP.NET Core application.
Running the Demo Server:
cd DemoWebApp
dotnet run port=11111 client-transport=stdio
Note: Use client-transport=stdio if connecting from Claude Desktop, or client-transport=streamableHttp for HTTP-based clients.
Hosting in your own Web App:
// Program.cs
using com.IvanMurzak.McpPlugin.Common;
using com.IvanMurzak.McpPlugin.Common.Utils;
using com.IvanMurzak.McpPlugin.Server;
var builder = WebApplication.CreateBuilder(args);
// 1. Prepare arguments (or load from config)
var dataArguments = new DataArguments(args);
// 2. Register MCP Server services
builder.Services
.WithMcpServer(dataArguments) // Configures transport based on dataArguments.ClientTransport
.WithMcpPluginServer(dataArguments);
// 3. Configure Kestrel with separate IPv4/IPv6 bindings (avoids dual-stack issues on macOS)
builder.WebHost.UseKestrelForMcpPlugin(dataArguments.Port);
var app = builder.Build();
// 4. Use MCP Server middleware
app.UseMcpPluginServer(dataArguments);
app.Run();
McpPlugin)Add the com.IvanMurzak.McpPlugin package to your .NET application.
Defining Tools, Prompts, and Resources:
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.McpPlugin.Common.Model;
using System.ComponentModel;
[AiToolType]
public static class MyMcpComponents
{
// --- Tools ---
[AiTool("calculate-sum", "Adds two numbers")]
[Description("Adds two numbers")]
public static int Add(int a, int b) => a + b;
// --- Prompts ---
[AiPrompt(Name = "explain-code")]
public static string ExplainCode(string code) => $"The following code: {code} does X, Y, and Z.";
// --- Resources ---
[AiResource(Route = "logs://system", Name = "system-logs", Description = "Returns the latest system logs", ListResources = nameof(ListLogs))]
public static ResponseResourceContent[] GetLogs()
=> new[] { ResponseResourceContent.CreateText("logs://system", "Log entry 1: System started...") };
public static ResponseListResource[] ListLogs()
=> new[] { new ResponseListResource("logs://system", "system-logs") };
}
Connecting to the Server:
using com.IvanMurzak.McpPlugin;
using com.IvanMurzak.ReflectorNet;
// 1. Initialize Reflector (The core engine)
var reflector = new Reflector();
// 2. Configure and build the plugin
var version = new com.IvanMurzak.McpPlugin.Common.Version
{
Api = "1.0.0",
Plugin = "1.0.0"
};
var plugin = new McpPluginBuilder(version)
.WithConfig(config => {
config.Host = "http://localhost:11111"; // Match your server port
})
// Option A: Scan assemblies for [AiTool], [AiPrompt], [AiResource]
.WithToolsFromAssembly(typeof(MyMcpComponents).Assembly)
.WithPromptsFromAssembly(typeof(MyMcpComponents).Assembly)
.WithResourcesFromAssembly(typeof(MyMcpComponents).Assembly)
.Build(reflector);
// 3. Connect to the MCP server
await plugin.Connect();
Unlike standard MCP implementations that struggle with complex .NET types, this plugin handles them natively. You can pass nested objects or collections as tool parameters:
public class UserProfile {
public string Name { get; set; }
public List<string> Roles { get; set; }
}
[AiTool("update-user")]
public static void UpdateUser(UserProfile profile) {
// ReflectorNet automatically deserializes the JSON from the AI into this object
}
You can configure how strictly the AI must match your method names. This is useful when LLMs use slightly different terminology:
var plugin = new McpPluginBuilder(version)
// ...
.Build(reflector);
// Configure fuzzy matching level (1-6)
// 6: Exact, 3: StartsWith (Case-Insensitive), 1: Contains (Case-Insensitive)
plugin.MethodNameMatchLevel = 3;
McpPlugin.Server)Command-line arguments take priority over environment variables.
| Argument | Env Var | Description | Default |
|---|---|---|---|
port | MCP_PLUGIN_PORT | The port the SignalR hub listens on. | 8080 |
client-transport | MCP_PLUGIN_CLIENT_TRANSPORT | Transport method: stdio or streamableHttp. | streamableHttp |
plugin-timeout | MCP_PLUGIN_CLIENT_TIMEOUT | Timeout for plugin operations (ms). | 10000 |
idle-timeout-seconds | MCP_PLUGIN_IDLE_TIMEOUT_SECONDS | streamableHttp only: idle window before an MCP session is evicted from the in-memory tracker. Longer values reduce reconnect 404s / session-migration rehydrates at the cost of higher in-memory footprint (bounded by max-idle-session-count). Only sessions with no in-flight request and no open SSE stream are evicted, so this never interrupts a long-running call. The SDK's own default is 7200 (2 h). | 600 |
max-idle-session-count | MCP_PLUGIN_MAX_IDLE_SESSION_COUNT | streamableHttp only: hard ceiling on retained idle MCP sessions. When exceeded, the least-recently-active idle sessions are pruned (disposed, buffers returned to the pool) before the idle timeout elapses. Bounds worst-case per-session buffer memory under connection churn. Active sessions are never counted/pruned. The SDK's own default is 10000. | 1000 |
auth | MCP_AUTH | Authorization mode: none (anonymous — offline / local dev / CI) or oauth (OAuth 2.1 resource server; validates ES256 JWT / opaque PAT against the authorization server). The legacy authorization / MCP_AUTHORIZATION alias still parses none; the retired required shared-token mode was removed and now fails closed. | none |
auth-issuer | MCP_AUTH_ISSUER | auth=oauth only (required): the authorization server URL (e.g. https://ai-game.dev). | (none) |
public-url | MCP_PUBLIC_URL | auth=oauth only (required): this server's canonical resource id / public URL (used as the token audience). | (none) |
McpPlugin.Server can emit fire-and-forget HTTP POST notifications to external endpoints for observability and analytics. Each event category has an independent URL, so you can route tool, prompt, resource, and connection events to different systems.
| Argument | Env Var | Description | Default |
|---|---|---|---|
webhook-tool-url | MCP_PLUGIN_WEBHOOK_TOOL_URL | Endpoint to receive tool call events. | (none) |
webhook-prompt-url | MCP_PLUGIN_WEBHOOK_PROMPT_URL | Endpoint to receive prompt retrieval events. | (none) |
webhook-resource-url | MCP_PLUGIN_WEBHOOK_RESOURCE_URL | Endpoint to receive resource access events. | (none) |
webhook-connection-url | MCP_PLUGIN_WEBHOOK_CONNECTION_URL | Endpoint to receive client connect/disconnect events. | (none) |
webhook-token | MCP_PLUGIN_WEBHOOK_TOKEN | Security token sent in each webhook request header. | (none) |
webhook-header | MCP_PLUGIN_WEBHOOK_HEADER | Header name for the security token. | X-Webhook-Token |
webhook-timeout | MCP_PLUGIN_WEBHOOK_TIMEOUT | HTTP delivery timeout in milliseconds. | 10000 |
Example — enable tool and connection analytics:
dotnet run \
client-transport=stdio \
webhook-tool-url=https://analytics.example.com/hooks/tools \
webhook-connection-url=https://analytics.example.com/hooks/connections \
webhook-token=my-secret-token
Event payload structure (all events follow this envelope):
{
"schemaVersion": "1.0",
"eventType": "tool.call.completed",
"timestamp": "2026-03-01T12:34:56.789Z",
"data": {
"toolName": "add",
"requestSizeBytes": 42,
"responseSizeBytes": 18,
"status": "success",
"durationMs": 150
}
}
Supported event types:
| Event Type | Trigger |
|---|---|
tool.call.completed | Every MCP tool call (success or failure) |
prompt.retrieved | Every MCP prompt retrieval |
resource.accessed | Every MCP resource access |
connection.ai-agent.connected | AI agent (MCP client) connects |
connection.ai-agent.disconnected | AI agent (MCP client) disconnects |
connection.plugin.connected | McpPlugin (.NET client) connects via SignalR |
connection.plugin.disconnected | McpPlugin (.NET client) disconnects |
Notes:
McpPlugin.Server can be configured with a synchronous authorization webhook that gates connections from both MCP clients (AI agents via HTTP) and McpPlugin clients (.NET apps via SignalR). Unlike the fire-and-forget analytics webhooks above, authorization webhooks block the connection until your endpoint responds.
| Argument | Env Var | Description | Default |
|---|---|---|---|
webhook-authorization-url | MCP_PLUGIN_WEBHOOK_AUTHORIZATION_URL | Endpoint that authorizes/denies connections. | (none) |
webhook-authorization-fail-open | MCP_PLUGIN_WEBHOOK_AUTHORIZATION_FAIL_OPEN | When true, allow connections if webhook times out or errors. When false, deny on failure. | false |
Example — enable connection authorization with fail-closed behavior:
dotnet run \
client-transport=stdio \
webhook-authorization-url=https://auth.example.com/authorize \
webhook-token=my-secret-token \
webhook-authorization-fail-open=false
Request format (POST from server to your webhook):
For AI agent connections (authorization.ai-agent):
{
"schemaVersion": "1.0",
"eventType": "authorization.ai-agent",
"timestamp": "2025-03-04T22:45:30.1234567Z",
"connectionId": "trace-id-or-connection-id",
"clientType": "ai-agent",
"bearerToken": "<token-from-client>",
"remoteIpAddress": "192.168.1.100",
"userAgent": "claude-ai/1.0",
"requestPath": "/mcp",
"clientName": null,
"clientVersion": null,
"hmacSignature": "sha256=abc123..."
}
For plugin connections (authorization.plugin):
{
"schemaVersion": "1.0",
"eventType": "authorization.plugin",
"timestamp": "2025-03-04T22:45:30.1234567Z",
"connectionId": "trace-id-or-connection-id",
"clientType": "plugin",
"bearerToken": "<token-from-plugin>",
"remoteIpAddress": null,
"userAgent": null,
"requestPath": null,
"clientName": "my-unity-plugin",
"clientVersion": "1.2.0",
"hmacSignature": "sha256=def456..."
}
Note: The
hmacSignaturefield is only present whenwebhook-tokenis configured. It contains an HMAC-SHA256 signature of the request body (before the signature field is added), computed using the webhook token as the secret key.
Expected response format (from your webhook to server):
{ "allowed": true }
or
{ "allowed": false, "reason": "IP not in allowlist" }
Behavior:
allowed: true → Connection proceedsallowed: false → Connection denied (reason logged as warning)fail-open=true)Security:
webhook-header (default: X-Webhook-Token)Notes:
webhook-authorization-url is not configured, authorization is disabled (all connections allowed)McpPlugin)Command-line arguments and environment variables are parsed automatically via ConnectionConfig.BuildFromArgsOrEnv(). They can also be overridden programmatically via McpPluginBuilder.WithConfig(...).
| Argument | Env Var | Property | Description | Default |
|---|---|---|---|---|
mcp-server-endpoint | MCP_SERVER_ENDPOINT | Host | The URL of the bridge server. | http://localhost:8080 |
mcp-server-timeout | MCP_SERVER_TIMEOUT | TimeoutMs | Operation timeout (ms). | 10000 |
mcp-plugin-token | MCP_PLUGIN_TOKEN | Token | Bearer token sent to the server for authentication. | (none) |
mcp-skills-folder | MCP_SKILLS_FOLDER | SkillsPath | Path for generated skill markdown files. | SKILLS |
Programmatic-only properties (set via WithConfig(...)):
| Property | Description | Default |
|---|---|---|
KeepConnected | Automatically reconnect if the connection is lost. | true |
GenerateSkillFiles | Auto-generate skill markdown files for each registered tool. | true |
You can run the bridge server in a Docker container:
docker build -t mcp-bridge -f McpPlugin.Server/Dockerfile .
docker run -p 8080:8080 mcp-bridge port=8080 client-transport=streamableHttp
McpPlugin: The client library for .NET applications. Contains the core logic for managing tools, prompts, and resources.McpPlugin.Server: The server implementation that bridges SignalR clients to the MCP protocol.McpPlugin.Common: Shared data structures, interfaces, and protocol definitions.DemoConsoleApp: A sample client application demonstrating how to expose tools.DemoWebApp: A sample server application demonstrating how to host the MCP bridge.This project is licensed under the Apache-2.0 License. Copyright - Ivan Murzak.
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows