McpServe.Build
C# · package Toolnexus · SPEC §7C · McpServe.cs
public static McpServer BuildMcpServer( ITransport transport, IReadOnlyList<ITool> tools, MCPServeConfig? cfg = null, OnCall? onCall = null)The inbound mirror of A2AServer.Start — but where A2A advertises
skills and fulfils a Task through the whole client loop, this advertises the toolkit’s
unified tools (every source: mcp · skill · native · http · builtin · a2a) and dispatches each
tools/call straight to ITool.ExecuteAsync. There is no client, no Task, no TaskStore — the
calling MCP client is the LLM host. Built on the official ModelContextProtocol SDK’s own
server types (ITransport, McpServer); Toolkit.ServeAsync mounts the streamable-HTTP profile
(POST /mcp) on top of it when opts.Mcp (or a top-level mcpServer config block) is present.
When to use it
Section titled “When to use it”Turn your toolkit into a universal MCP gateway: aggregate N MCP servers, your skills, and your
own native/HTTP tools behind one Toolkit, then re-expose the union as a single MCP server any MCP
client (Claude Desktop, an IDE, another agent) can call. BuildMcpServer is the low-level
constructor — call it directly when you have your own ITransport (e.g. an in-process pipe for
tests, or your own hosting shell); most apps go through Toolkit.ServeAsync(addr, new Toolkit.ServeOptions { Mcp = ... }), which wires this to the shared streamable-HTTP endpoint.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — an in-process transport pair
Section titled “1. The smallest useful call — an in-process transport pair”using System.IO.Pipelines;using ModelContextProtocol;using ModelContextProtocol.Client;using ModelContextProtocol.Protocol;using ModelContextProtocol.Server;using Toolnexus;
var echo = NativeTool.Of("echo", "echo back text", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["text"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new List<object?> { "text" }, }, (IDictionary<string, object?> a) => a.TryGetValue("text", out var v) ? v?.ToString() ?? "" : "");
var clientToServer = new Pipe();var serverToClient = new Pipe();var serverTransport = new StreamServerTransport(clientToServer.Reader.AsStream(), serverToClient.Writer.AsStream());var server = McpServe.BuildMcpServer(serverTransport, new List<ITool> { echo }, new MCPServeConfig { Name = "gateway" });var cts = new CancellationTokenSource();var run = server.RunAsync(cts.Token);
var clientTransport = new StreamClientTransport(clientToServer.Writer.AsStream(), serverToClient.Reader.AsStream());var client = await McpClient.CreateAsync(clientTransport);
if (client.ServerInfo.Name != "gateway") throw new Exception(client.ServerInfo.Name);var tools = await client.ListToolsAsync();if (!tools.Any(t => t.Name == "echo")) throw new Exception("expected echo in tools/list");
await client.DisposeAsync();cts.Cancel();await server.DisposeAsync();try { await run; } catch { }
Console.WriteLine($"ok: {client.ServerInfo.Name} advertises {string.Join(",", tools.Select(t => t.Name))}");2. The realistic case — tools/call dispatches straight to ExecuteAsync
Section titled “2. The realistic case — tools/call dispatches straight to ExecuteAsync”using System.IO.Pipelines;using ModelContextProtocol.Client;using ModelContextProtocol.Protocol;using ModelContextProtocol.Server;using Toolnexus;
var echo = NativeTool.Of("echo", "echo back text", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["text"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new List<object?> { "text" }, }, (IDictionary<string, object?> a) => a.TryGetValue("text", out var v) ? v?.ToString() ?? "" : "");var boom = NativeTool.Of("boom", "always throws", null, (IDictionary<string, object?> _) => throw new Exception("kaboom"));
var calls = new List<OnCallEvent>();var clientToServer = new Pipe();var serverToClient = new Pipe();var serverTransport = new StreamServerTransport(clientToServer.Reader.AsStream(), serverToClient.Writer.AsStream());var server = McpServe.BuildMcpServer(serverTransport, new List<ITool> { echo, boom }, null, ev => { calls.Add(ev); return Task.CompletedTask; });var cts = new CancellationTokenSource();var run = server.RunAsync(cts.Token);var client = await McpClient.CreateAsync( new StreamClientTransport(clientToServer.Writer.AsStream(), serverToClient.Reader.AsStream()));
var ok = await client.CallToolAsync("echo", new Dictionary<string, object?> { ["text"] = "hi" });if (ok.IsError == true) throw new Exception("expected success");if (((TextContentBlock)ok.Content[0]).Text != "hi") throw new Exception("wrong echo");
// A throwing tool becomes an isError result — never crashes the server.var bad = await client.CallToolAsync("boom", new Dictionary<string, object?>());if (bad.IsError != true) throw new Exception("expected isError");if (!((TextContentBlock)bad.Content[0]).Text.Contains("kaboom")) throw new Exception("expected kaboom in output");
if (calls.Count != 2) throw new Exception($"expected 2 onCall events, got {calls.Count}");
await client.DisposeAsync();cts.Cancel();await server.DisposeAsync();try { await run; } catch { }
Console.WriteLine($"ok: echo succeeded, boom isError=true, onCall saw {calls.Count} calls");3. Full surface — the real streamable-HTTP endpoint via Toolkit.ServeAsync
Section titled “3. Full surface — the real streamable-HTTP endpoint via Toolkit.ServeAsync”using ModelContextProtocol.Client;using ModelContextProtocol.Protocol;using Toolnexus;
var echo = NativeTool.Of("echo", "echo back text", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["text"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new List<object?> { "text" }, }, (IDictionary<string, object?> a) => a.TryGetValue("text", out var v) ? v?.ToString() ?? "" : "");
await using var tk = await Toolkit.CreateAsync(new Toolkit.Options{ Builtins = false, ExtraTools = new List<ITool> { echo },});
// mcp.Tools narrows the advertised surface — even though the toolkit only has one tool here,// this is the filter a real gateway would use to expose a subset of a much larger toolkit.var srv = await tk.ServeAsync("127.0.0.1:0", new Toolkit.ServeOptions{ Mcp = new MCPServeConfig { Name = "http-gateway", Tools = new[] { "echo" } },});
var client = await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions{ Endpoint = new Uri(srv.Url + "/mcp"), TransportMode = HttpTransportMode.StreamableHttp,}));
if (client.ServerInfo.Name != "http-gateway") throw new Exception(client.ServerInfo.Name);var res = await client.CallToolAsync("echo", new Dictionary<string, object?> { ["text"] = "over http" });var text = ((TextContentBlock)res.Content[0]).Text;if (text != "over http") throw new Exception(text);
await client.DisposeAsync();await srv.StopAsync();Console.WriteLine($"ok: {client.ServerInfo.Name} -> {text}");Parameters
Section titled “Parameters”| Parameter | Type | What it is |
|---|---|---|
transport |
ITransport |
The SDK transport to serve over — an in-process stream pair for tests, or the streamable-HTTP transport Toolkit.ServeAsync builds per request. |
tools |
IReadOnlyList<ITool> |
The tools to expose. Use McpServe.ExposedMcpTools(tk.Tools(), cfg) to apply cfg.Tools filtering first. |
cfg |
MCPServeConfig? |
Name/Version for initialize; Tools narrows tools/list (unknown names ignored, never an error). |
onCall |
OnCall? |
Fires per inbound tools/call with {name, source, ms, isError}. Host callback errors are isolated. |
See also
Section titled “See also”A2AServer.Start— Publish an Agent Card and answer JSON-RPC over the client loop — your toolkit becomes someone else’s remote agent.A2AServer.BuildAgentCard— Construct the Agent Card that advertises your name, skills and endpoint.A2AServer.FileTaskStore— Persist inbound A2A tasks so a suspended request survives a restart.