Mcp.Net is a .NET implementation of the Model Context Protocol (MCP) - a standardized way for apps to talk to AI models and execute tools. Think of it as the "HTTP of AI tool usage" - a clean, consistent way for your app to give AI models the ability to:
- π§° Use tools like search, weather lookup, database access
- π Access web resources and fetch web content
- π Work with predefined prompts and templates
β οΈ Pre-1.0 NoticeThis is version 0.9.0 - the core is stable but some features are still in development. See Current Status for details.
Experience MCP (with web-search, scraping, twilio, various demo tools) with OpenAI or Anthropic models in just two steps:
# 1. Start the server with demo tools
dotnet run --project Mcp.Net.Examples.SimpleServer/Mcp.Net.Examples.SimpleServer.csproj
# 2. In a new terminal, run the LLM chat app (requires OpenAI or Anthropic API key)
dotnet run --project Mcp.Net.LLM/Mcp.Net.LLM.csprojSee the LLM demo documentation for more details.
# For building a server (the thing that provides tools)
dotnet add package Mcp.Net.Server
# For building a client (the thing that talks to AI models)
dotnet add package Mcp.Net.Client# Terminal 1 β start the demo server (SSE on http://localhost:5000)
dotnet run --project Mcp.Net.Examples.SimpleServer/Mcp.Net.Examples.SimpleServer.csproj
# Terminal 2 β launch the client (performs dynamic registration + PKCE)
dotnet run --project Mcp.Net.Examples.SimpleClient -- --url http://localhost:5000 --auth-mode pkceβΉοΈ The first SSE GET returns
401 Unauthorizedby design. The client follows theWWW-Authenticatechallenge, registers itself at/oauth/register, completes the PKCE handshake, and reconnects with a bearer token. Watch the logs to see resources, prompts, and tools (including the new elicitation flow) being exercised end-to-end. When the Warhammer inquisitor tool runs youβll be prompted to accept/decline/cancel and optionally override fields via the console handler in the sample client.
The client advertises the elicitation capability only after you register a handler. Call
SetElicitationHandler (or the builder helper) before Initialize:
var client = new McpClientBuilder()
.UseSseTransport(serverUrl)
.WithElicitationHandler(async (context, ct) =>
{
// Render UI, validate against context.RequestedSchema, then respond
return ElicitationClientResponse.Decline();
})
.Build();
await client.Initialize();Handlers can accept, decline, or cancel and receive strongly typed schema details via
ElicitationRequestContext. The SimpleClient console demo in Mcp.Net.Examples.SimpleClient
shows a full end-to-end implementation.
using Mcp.Net.Core.Attributes;
using Mcp.Net.Server;
using Mcp.Net.Server.ConnectionManagers;
using Mcp.Net.Server.Extensions;
using Microsoft.Extensions.Logging;
// 1. Create a simple stdio server
var server = new McpServer(
new ServerInfo { Name = "QuickStart Server", Version = "1.0" },
new InMemoryConnectionManager(new LoggerFactory())
);
// 2. Define tools using simple attributes and POCOs
[McpTool("Calculator", "Math operations")]
public class CalculatorTools
{
// Simple synchronous tool that returns a plain string
[McpTool("add", "Add two numbers")]
public string Add(
[McpParameter(required: true, description: "First number")] double a,
[McpParameter(required: true, description: "Second number")] double b)
{
return $"The sum of {a} and {b} is {a + b}";
}
// Async tool with a POCO return type - easiest approach!
[McpTool("getWeather", "Get weather for a location")]
public async Task<WeatherResponse> GetWeatherAsync(
[McpParameter(required: true, description: "Location")] string location)
{
// Simulate API call
await Task.Delay(100);
// Just return a POCO - no need to deal with ToolCallResult!
return new WeatherResponse
{
Location = location,
Temperature = "72Β°F",
Conditions = "Sunny",
Forecast = new[] { "Clear", "Partly cloudy", "Clear" }
};
}
}
// Simple POCO class
public class WeatherResponse
{
public string Location { get; set; }
public string Temperature { get; set; }
public string Conditions { get; set; }
public string[] Forecast { get; set; }
}
// 3. Connect to stdio transport and start
await server.ConnectAsync(new StdioTransport());
// Server is now running and ready to process requests!For more control, you can also register tools directly:
using System.Text.Json;
using Mcp.Net.Core.Models.Content;
using Mcp.Net.Core.Models.Tools;
using Mcp.Net.Server;
using Mcp.Net.Server.ConnectionManagers;
using Microsoft.Extensions.Logging;
// Create server
var server = new McpServer(
new ServerInfo { Name = "Manual Server", Version = "1.0" },
new InMemoryConnectionManager(new LoggerFactory())
);
// Register tool with explicit schema and handler
server.RegisterTool(
name: "multiply",
description: "Multiply two numbers",
inputSchema: JsonDocument.Parse(@"
{
""type"": ""object"",
""properties"": {
""x"": { ""type"": ""number"" },
""y"": { ""type"": ""number"" }
},
""required"": [""x"", ""y""]
}
").RootElement,
handler: async (args) =>
{
var x = args?.GetProperty("x").GetDouble() ?? 0;
var y = args?.GetProperty("y").GetDouble() ?? 0;
var result = x * y;
// For full control, you can explicitly use ToolCallResult
return new ToolCallResult
{
Content = new[] { new TextContent { Text = $"{x} * {y} = {result}" } }
};
}
);using Mcp.Net.Client;
// Connect to a stdio server (like Claude or a local MCP server)
var client = new StdioMcpClient("MyApp", "1.0");
await client.Initialize();
// List available tools
var tools = await client.ListTools();
Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}");
// Call the add tool
var result = await client.CallTool("add", new { a = 5, b = 3 });
Console.WriteLine(((TextContent)result.Content.First()).Text); // "The sum is 8"
// Call the weather tool
var weatherResult = await client.CallTool("getWeather", new { location = "San Francisco" });
Console.WriteLine(((TextContent)weatherResult.Content.First()).Text);
// "The weather in San Francisco is sunny and 72Β°F"- Mcp.Net.Core: Models, interfaces, and base protocol components
- Mcp.Net.Server: Server-side implementation with transports (SSE and stdio)
- Mcp.Net.Client: Client libraries for connecting to MCP servers
- Mcp.Net.Examples.SimpleServer: Simple example server with calculator and themed tools
- Mcp.Net.Examples.SimpleClient: Simple example client that connects to MCP servers
- Mcp.Net.LLM: Interactive LLM demo integrating OpenAI/Anthropic models with MCP tools
- Mcp.Net.Examples.ExternalTools: Standalone tool library that can be loaded by any MCP server
-
Two Transport Options:
- β¨οΈ stdio: Perfect for CLI tools and direct model interaction
- π SSE: Ideal for web apps and browser integrations
-
Tool Management:
- β Dynamic tool discovery
- β JSON Schema validation for parameters
- β Both synchronous and async tool support
- β Error handling and result formatting
-
OAuth 2.1 Reference Flow:
- β
Dynamic client registration (
/oauth/register) with in-memory persistence - β
Authorization code + PKCE with enforced
resourceindicators - β Refresh-token rotation and audience validation in the demo server/client samples
- β
Dynamic client registration (
-
Flexible Hosting:
- β Use as standalone server
- β Embed in ASP.NET Core applications
- β Run as background service
The MCP server provides multiple ways to configure your server, especially for controlling network settings when using the SSE transport:
// Configure the server with the builder pattern
var builder = new McpServerBuilder()
.WithName("My MCP Server")
.WithVersion("1.0.0")
.WithInstructions("This server provides helpful tools")
// Configure network settings
.UsePort(8080) // Default is 5000
.UseHostname("0.0.0.0") // Default is localhost
// Configure transport mode
.UseSseTransport(); // Uses the port and hostname configured aboveWhen running the server from the command line:
# Run with custom port and hostname
dotnet run --project Mcp.Net.Server --port 8080 --hostname 0.0.0.0
# For cloud environments, binding to 0.0.0.0 is usually required
dotnet run --project Mcp.Net.Server --hostname 0.0.0.0
# Run with stdio transport instead of SSE
dotnet run --project Mcp.Net.Server --stdio
# or use the shorthand
dotnet run --project Mcp.Net.Server -s
# Enable debug-level logging
dotnet run --project Mcp.Net.Server --debug
# or use the shorthand
dotnet run --project Mcp.Net.Server -d
# Specify a custom log file path
dotnet run --project Mcp.Net.Server --log-path /path/to/logfile.log
# Use a specific URL scheme (http or https)
dotnet run --project Mcp.Net.Server --scheme https
# Combine multiple options
dotnet run --project Mcp.Net.Server --stdio --debug --port 8080 --hostname 0.0.0.0The ServerConfiguration and CommandLineOptions classes handle these arguments:
// CommandLineOptions.cs parses command-line arguments
public static CommandLineOptions Parse(string[] args)
{
var options = new CommandLineOptions(args)
{
UseStdio = args.Contains("--stdio") || args.Contains("-s"),
DebugMode = args.Contains("--debug") || args.Contains("-d"),
LogPath = GetArgumentValue(args, "--log-path") ?? "mcp-server.log",
Port = GetArgumentValue(args, "--port"),
Hostname = GetArgumentValue(args, "--hostname"),
Scheme = GetArgumentValue(args, "--scheme")
};
return options;
}# Set standard environment variables before running
export MCP_SERVER_PORT=8080
export MCP_SERVER_HOSTNAME=0.0.0.0
export MCP_SERVER_SCHEME=http
# Cloud platform compatibility - many cloud platforms use PORT
export PORT=8080
dotnet run --project Mcp.Net.ServerThe ServerConfiguration class handles these environment variables with a priority-based approach:
// ServerConfiguration.cs handles environment variables:
private void LoadFromEnvironmentVariables()
{
// Standard MCP hostname variable
string? envHostname = Environment.GetEnvironmentVariable("MCP_SERVER_HOSTNAME");
if (!string.IsNullOrEmpty(envHostname))
{
Hostname = envHostname;
}
// Cloud platform compatibility - PORT is standard on platforms like Google Cloud Run
string? cloudRunPort = Environment.GetEnvironmentVariable("PORT");
if (!string.IsNullOrEmpty(cloudRunPort) && int.TryParse(cloudRunPort, out int parsedCloudPort))
{
Port = parsedCloudPort;
}
else
{
// Fall back to MCP-specific environment variable
string? envPort = Environment.GetEnvironmentVariable("MCP_SERVER_PORT");
if (!string.IsNullOrEmpty(envPort) && int.TryParse(envPort, out int parsedEnvPort))
{
Port = parsedEnvPort;
}
}
// HTTPS configuration
string? envScheme = Environment.GetEnvironmentVariable("MCP_SERVER_SCHEME");
if (!string.IsNullOrEmpty(envScheme))
{
Scheme = envScheme.ToLowerInvariant();
}
}The server also reads settings from appsettings.json:
{
"Server": {
"Port": 8080,
"Hostname": "0.0.0.0",
"Scheme": "http"
}
}The configuration is loaded with a tiered priority approach:
// SseServerBuilder automatically loads from configuration files:
private void ConfigureAppSettings(WebApplicationBuilder builder, string[] args)
{
// Add configuration from multiple sources with priority:
// 1. Command line args (highest)
// 2. Environment variables
// 3. appsettings.json (lowest)
builder.Configuration.AddJsonFile("appsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables("MCP_");
builder.Configuration.AddCommandLine(args);
}The server uses this priority order when resolving configuration:
- Command line arguments (highest priority)
- Environment variables
- appsettings.json configuration
- Default values (lowest priority)
This allows for flexible deployment in various environments, from local development to cloud platforms.
The SSE server includes built-in health check endpoints:
/health- Overall health status/health/ready- Readiness check for load balancers/health/live- Liveness check for container orchestrators
- HTTPS/TLS support enhancements
- Advanced metrics and telemetry
- Authentication integration
- Resource quota management
Perfect for web applications, the SSE transport:
- Maintains a persistent HTTP connection
- Uses standard event streaming
- Supports browser-based clients
- Enables multiple concurrent connections
Ideal for CLI tools and AI model integration:
- Communicates via standard input/output
- Works great with Claude, GPT tools
- Simple line-based protocol
- Lightweight and efficient
- Requests now honour newline-delimited framing and expose a configurable
StdioClientTransport.RequestTimeout(default 60s, set toTimeout.InfiniteTimeSpanto disable). Pending requests are cancelled automatically when the transport closes so callers can surface clean shutdown errors.
var builder = WebApplication.CreateBuilder(args);
// Add MCP server to services
builder.Services.AddMcpServer(b =>
{
b.WithName("My MCP Server")
.WithVersion("1.0.0")
.WithInstructions("Server providing math and weather tools")
.UsePort(8080) // Configure port (default: 5000)
.UseHostname("0.0.0.0") // Configure hostname (default: localhost)
.UseSseTransport(); // Uses the port and hostname configured above
});
// Configure middleware
var app = builder.Build();
app.UseCors(); // If needed
app.UseMcpServer();
await app.RunAsync();// Return both text and an image
return new ToolCallResult
{
Content = new IContent[]
{
new TextContent { Text = "Here's the chart you requested:" },
new ImageContent
{
MimeType = "image/png",
Data = Convert.ToBase64String(imageBytes)
}
}
};This implementation is currently at version 0.9.0:
- β Core JSON-RPC message exchange
- β Dual transport support (SSE and stdio)
- β Tool registration and discovery
- β Tool invocation with parameter validation
- β Error handling and propagation
- β Text-based content responses
- β Client connection and initialization flow
- β Configurable server port and hostname
- β Resource catalogue (list/read) with sample markdown content
- β Prompt catalogue (list/get) demonstrated by SimpleServer
- β Elicitation (server + client) with console UX helpers and schema validation
β οΈ Advanced content types (Image, Resource, Embedded)β οΈ XML documentation
dotnet build Mcp.Net.sln
dotnet test Mcp.Net.Tests/Mcp.Net.Tests.csprojThe test suite covers happy-path tool invocation and negative-path OAuth scenarios (dynamic registration failures, PKCE mismatches, resource-indicator validation, and refresh-token replays). The sample server seeds markdown resources and reusable prompts so integration runs exercise the entire capability surface.
- The demo server includes a lightweight OAuth 2.1 resource server with dynamic registration, authorization code + PKCE, refresh tokens, and resource indicators. Use it for local testing; production systems should wire in a dedicated identity provider (e.g., Supabase, Auth0, Azure AD).
- Clients must include
Authorization: Bearer <token>plus the negotiatedMcp-Session-IdandMCP-Protocol-Versionheaders on every POST/GET to/mcp. - SimpleClient defaults to PKCE (
--auth-mode pkce) but still supports legacy client-credentials mode (--auth-mode client) when interacting with static client IDs. - Tool code can read the authenticated principal from
context.Items["AuthenticatedUserId"](and other claims viaAuthResult) to enforce user-level authorization before touching downstream APIs or databases.
The server now accepts production configuration via appsettings.json (or environment variables):
- Auth0 / Entra ID: point
Authorityat the OpenID metadata endpoint, add the MCP endpoint as an API/audience, and ensure issued tokens include the userβs stable identifier (sub). The server will fetch signing keys from JWKS automatically. - Supabase: either mark Supabase as the authority (if JWT signing enabled) or run a lightweight
exchange service that mints MCP-scoped tokens using Supabase session claims. Map
subto your database row IDs and store the Supabase signing secret inSigningKeyswhen JWKS is not exposed. - Clerk: register an OAuth application that scopes tokens to the MCP resource, then configure
Clerkβs issuer/JWKS URLs. Tokens arrive with the Clerk user ID in the
subclaim, which your tools can trust after validation. - HTTP failures now raise
McpClientHttpException, which captures the status code, response body, and originating request so host applications can surface actionable error messages to users.
When writing MCP tools that touch per-user resources, compare the incoming request parameters to the authenticated subject (or enforce role claims) before executing queriesβthis prevents a caller from mutating data for another account even if the tool input is tampered.
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ by Sam Fold
{ "Server": { "Authentication": { "OAuth": { "Authority": "https://your-idp.example", "Resource": "https://your-mcp.example/mcp", "ResourceMetadataPath": "/.well-known/oauth-protected-resource", "AuthorizationServers": [ "https://your-idp.example/.well-known/oauth-authorization-server" ], "ValidAudiences": [ "https://your-mcp.example/mcp" ], "ValidIssuers": [ "https://your-idp.example" ], "SigningKeys": [ "base64-or-base64url-encoded-HS256-key-if-jwks-not-available" ] } } } }