I’ve been making great use of the Model Context Protocol (MCP) as part of our AI stack at work, using it to expose functionality within our platform as MCP tools that can then be used by our LLM. In particular, I’ve been leveraging the official Model Context Protocol C# SDK to build out our MCP server. Unfortunately, this project is still fairly young so there hasn’t been a lot of knowledge shared out about how to do more “exotic” things with the SDK when it comes to building MCP servers.
An interesting problem came up recently on a project I’m working on where we want to basically have multiple “lists” of tools that the MCP server can expose based on a route value in the URL that the MCP server is mapped to. I came up with what I consider to be a nice solution to the problem and wanted to help add to the body of knowledge that is out there around the official C# SDK.
Overview
A little introduction to the protocol behind MCP will give some insight into what we need to do. When an MCP client connects to an MCP server that supports Streamable HTTP, it first sends an initialize
request that sets up the session with the server. Then from there, it can send several different messages, but for our use case it can either send a tools/list
message to retrieve the list of tools that are available or a tools/call
message to call a tool.
With our MCP server, we will want to tie in to the handling of the tools/list
and tools/call
messages and provide our own custom logic for how these operations will be handled. Luckily the C# SDK offers us an extension point to plug in our own handlers for these requests so that we can handle them.
Implementation
What we will be creating will be a generic IMcpHandler<TRequestParams, TResponse>
interface that we can inherit from, it will have a Handles(HttpRequest)
method that will be used to determine if the handler should be called for a given request. We will then create implementations of the IMcpHandler
interface that will be used to handle the tools/list
and tools/call
messages. Finally, we will create an IDispatcher<TRequestParams, TResponse>
interface and an implementation that can be used to dispatch to the correct handler and register two instances of it with the built-in .NET dependency injection framework. One instance will be used for dispatching to our tools/list
handlers, while the other will be used for dispatching to our tools/call
handlers.
We will then tie in to the extension points for message handling with the MCP server and use our dispatcher to find the correct handler to use for each request. To keep things simple, we have a single handler for the tools/list
and tools/call
messages but we could easily add additional implementations and wire those up with dependency injection and support multiple lists of tools like we are planning on doing.
Creating our MCP Call Handlers
This is our interface for presenting an MCP call that can be used to either list tools or perform a tool call. The Handles()
method is used to determine whether the handler should be invoked for a given request. We’ll allow only one handler to be executed for a given request.
/// <summary>
/// Interface implemented by MCP message handlers
/// </summary>
public interface IMcpCallHandler<TRequestParams, TResult> where TRequestParams : class where TResult : class
{
/// <summary>
/// Whether the handler implementation handles the request
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
bool Handles(HttpRequest request);
/// <summary>
/// Handles the request
/// </summary>
/// <param name="requestContext">The request context</param>
/// <param name="cancellationToken">The cancellation token to use</param>
/// <returns></returns>
ValueTask<TResult> ExecuteAsync(
RequestContext<TRequestParams> requestContext,
CancellationToken cancellationToken
);
}
We’ll then have one implementation of IMcpHandler
for taking an OpenAPI specification and exposing the various paths as a list of tools. This not a complete example so the implementation is left up to the reader.
/// <summary>
/// Implementation of the <see cref="IMcpCallHandler{ListToolsRequestParams, ListToolsResult}>"/> interface that downloads
/// an OpenAPI specification for the GitHub API and then exposes each API endpoint/path as an MCP tool
/// </summary>
public class OpenApiListToolsHandler : IMcpCallHandler<ListToolsRequestParams, ListToolsResult>
{
private readonly IMapAsync<Stream, IEnumerable<Tool>> _toolMapper;
/// <summary>
/// Constructor
/// </summary>
/// <param name="toolMapper">Mapper that maps an OpenApiDocument to an enumerable of MCP Tools</param>
public OpenApiListToolsHandler(IMapAsync<Stream, IEnumerable<Tool>> toolMapper)
{
_toolMapper = toolMapper;
}
/// <inheritdoc/>
public bool Handles(HttpRequest request)
=> request.RouteValues.GetValueOrDefault("serverId")?.ToString() == "github";
/// <inheritdoc/>
public async ValueTask<ListToolsResult> ExecuteAsync(
RequestContext<ListToolsRequestParams> requestContext,
CancellationToken cancellationToken)
{
const string openApiSpec = "https://raw.githubusercontent.com/github/rest-api-description/refs/heads/main/descriptions/api.github.com/api.github.com.2022-11-28.json";
// Read the OpenAPI spec URL into a MemoryStream
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Custom.Mcp.Server/1.0.0");
using var response = await httpClient.GetAsync(openApiSpec, cancellationToken);
response.EnsureSuccessStatusCode();
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);
var result = new ListToolsResult();
var tools = await _toolMapper.MapAsync(contentStream, cancellationToken);
result.Tools.AddAll(tools);
return result;
}
}
Then finally we’ll have a tool call handler implementation that invokes the associated API endpoint. This is also not a complete example so the implementation is also left up to the reader.
public class OpenApiCallToolHandler : IMcpCallHandler<CallToolRequestParams, CallToolResult>
{
private readonly IToolExecutor _toolExecutor;
/// <summary>
/// Constructor
/// </summary>
/// <param name="toolExecutor">The tool executor instance for invoking OpenAPI endpoints</param>
public OpenApiCallToolHandler(IToolExecutor toolExecutor)
{
_toolExecutor = toolExecutor;
}
public bool Handles(HttpRequest request) =>
request.RouteValues.GetValueOrDefault("serverId")?.ToString() == "github";
public async ValueTask<CallToolResult> ExecuteAsync(RequestContext<CallToolRequestParams> requestContext, CancellationToken cancellationToken)
{
var result = _toolExecutor.Execute(
toolName: requestContext.Params.Name,
arguments: requestContext.Params.Arguments
);
var callResult = new CallToolResult { IsError = false };
callResult.Contents.Add(new TextContentBlock { Text = result });
return callResult;
}
}
Creating the Dispatcher
Next we’ll create a dispatcher class that will determine the correct handler to execute based off the current request to the MCP server. This dispatcher can be used for both list tool calls and for call tool calls based off the generic parameters passed to the class.
/// <summary>
/// Interface for dispatcher that matches a request to the correct <see cref="IMcpHandler{TRequestParams, TResult}"/> implementation
/// for a given <see cref="HttpRequest"/>
public interface IMcpCallDispatcher<TRequestParams, TResult> where TRequestParams : class where TResult : class
{
IMcpCallHandler<TRequestParams, TResult>? Dispatch();
}
/// <summary>
/// Implementation of the <see cref="IMcpDispatcher{TRequestParams, TResult}"/> interface that will receive a list of all registered
/// implementations of the <see cref="IMcpHandler{TRequestParams, TResult}"/> interface with the dependency injection system and then use
/// the current <see cref="HttpContext"/> to determine the correct handler to execute
/// </summary>
public class McpCallDispatcher<TRequestParams, TResult> : IMcpCallDispatcher<TRequestParams, TResult> where TRequestParams : class where TResult : class
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<IMcpCallHandler<TRequestParams, TResult>> _handlers;
/// <summary>
/// Constructor
/// </summary>
/// <param name="httpContextAccessor">The accessor to use to get an HttpContext instance</param>
/// <param name="handlers">The <see cref="IMcpCallHandler{TRequestParams,TResult}"/> implementations to choose from to dispatch</param>
public McpCallDispatcher(
IHttpContextAccessor httpContextAccessor,
IEnumerable<IMcpCallHandler<TRequestParams, TResult>> handlers)
{
_httpContextAccessor = httpContextAccessor;
_handlers = handlers;
}
/// <inheritdoc/>
public IMcpCallHandler<TRequestParams, TResult>? Dispatch()
{
var request = _httpContextAccessor.HttpContext?.Request;
if (request == null)
{
throw new InvalidOperationException("HttpContext is null");
}
return _handlers.FirstOrDefault(strategy => strategy.Handles(request));
}
}
Wiring Up Dependency Injection
We’ll need to register all of our handlers and dispatchers with the IServiceCollection
for dependency injection. It should be noted that you can register multiple implementations for a single interface, you can then either inject a list of that interface into your service or inject a single instance and the last registered instance “wins.”
public static class ServiceExtensions
{
public static IServiceCollection AddMcpHandlers(this IServiceCollection services)
{
// List tool handlers
services.AddTransient<IMcpCallHandler<ListToolsRequestParams, ListToolsResult>, OpenApiListToolsHandler>();
services.AddTransient<IMcpCallDispatcher<ListToolsRequestParams, ListToolsResult>, McpCallDispatcher<ListToolsRequestParams, ListToolsResult>>();
// Call tool handlers
services.AddTransient<IMcpCallHandler<CallToolRequestParams, CallToolResult>, OpenApiCallToolHandler>();
services.AddTransient<IMcpCallDispatcher<CallToolRequestParams, CallToolResult>, McpCallDispatcher<CallToolRequestParams, CallToolResult>>();
return services;
}
}
Wiring Up Our Handlers with the Server
We’ll create an extension method on IMcpServerBuilder
that will allow us to register our handlers for the list tools
and call tools
requests using the extension points that the Model Context Protocol C# SDK provides.
public static class McpServerBuilderExtensions
{
public static IMcpServerBuilder WithMcpCallHandlers(this IMcpServerBuilder builder)
{
builder.WithListToolsHandler(McpCallHandler<ListToolsRequestParams, ListToolsResult>);
builder.WithCallToolHandler(McpCallHandler<CallToolRequestParams, CallToolResult>);
return builder;
}
private static async ValueTask<TResult> McpCallHandler<TRequestParams, TResult>(
RequestContext<TRequestParams> context,
CancellationToken cancellationToken
) where TRequestParams : class where TResult : class
{
var dispatcher = context.Services?.GetRequiredService<IMcpCallDispatcher<TRequestParams, TResult>>();
if (dispatcher == null)
{
throw new Exception("Unable to get dispatcher from services");
}
var handler = dispatcher.Dispatch();
if (handler == null)
{
throw new Exception("Unable to find strategy to handle request");
}
var result = await handler.ExecuteAsync(context, cancellationToken);
return result;
}
}
The code is pretty straight forward. We call the builder.WithListToolsHandler
and builder.WithCallToolHandler
methods and pass it our McpCallHandler
method with the correct generic arguments for the type of handlers we need to dispatch. The McpCallHandler
method then uses our IDispatcher
implementation to determine which handler to invoke for the request.
Creating the MCP Server
Now all we need to do is create our actual MCP server.
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddJsonConsole(options =>
{
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
options.UseUtcTimestamp = true;
});
var services = builder.Services;
var configurationRoot = new ConfigurationBuilder().BuildConfigurationWithSources(args).Build();
services
.AddMcpServer()
.WithHttpTransport(options =>
{
// Should fix issue with HttpContext being null, disables things like client sampling
// https://github.com/modelcontextprotocol/csharp-sdk/issues/365#issuecomment-2859953161
options.Stateless = true;
}
)
.WithMcpCallHandlers();
services.
.AddHttpContextAccessor()
.AddMcpHandlers()
app.MapMcp("server/{serverId}").AllowAnonymous();
app.Run();
Conclusion
I hope that this example proves useful about some of the more advanced capabilities that the C# SDK provides. Feel free to reach out on Twitter or LinkedIn if you have any questions or comments.