McpServe.build
Java · package io.github.muthuishere:toolnexus · SPEC §7C · McpServe.java
// the ergonomic entry — Toolkit.ServeOptions.mcp(...) opts in:public A2AServer.ServeHandle serve(String addr, Toolkit.ServeOptions opts)
// the filtering helper used to build the exposed surface:public static List<Tool> exposedMcpTools(List<Tool> tools, McpServe.MCPServeConfig cfg)Mounts a streamable-HTTP MCP server at POST /mcp on the same server A2AServer.start
runs — built on the same official io.modelcontextprotocol.sdk:mcp the client side already
uses. Where the A2A profile advertises skills and fulfils a Task
through the whole LlmClient loop, the MCP profile advertises the toolkit’s unified tools
(every source — mcp · skill · native · http · builtin · a2a) and dispatches each tools/call
straight to Tool.execute. There is no client, no Task, and no TaskStore here — the calling
MCP client is the LLM host.
When to use it
Section titled “When to use it”Whenever you want toolnexus to act as a universal MCP gateway: aggregate N MCP servers plus
skills plus 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 toolnexus toolkit — can connect to.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — exposedMcpTools filters by name
Section titled “1. The smallest useful call — exposedMcpTools filters by name”import io.github.muthuishere.toolnexus.*;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { Tool echo = NativeTool.of("echo", "echoes back", Map.of("type", "object", "properties", Map.of("text", Map.of("type", "string"))), (Map<String, Object> a) -> a.get("text")); Tool ping = NativeTool.of("ping", "health check", Map.of("type", "object", "properties", Map.of()), (Map<String, Object> a) -> "pong"); List<Tool> tools = List.of(echo, ping);
// No config ⇒ every tool is exposed. List<Tool> all = McpServe.exposedMcpTools(tools, null); if (all.size() != 2) throw new AssertionError(all.size());
// A named allowlist narrows the surface; an unknown name is simply ignored, never an error. List<Tool> narrowed = McpServe.exposedMcpTools(tools, new McpServe.MCPServeConfig().tools(List.of("echo", "does-not-exist"))); if (narrowed.size() != 1 || !narrowed.get(0).name().equals("echo")) throw new AssertionError(narrowed);
System.out.println("ok: " + all.size() + " tools total, " + narrowed.size() + " exposed"); }}2. The realistic case — a real MCP client round trip over /mcp
Section titled “2. The realistic case — a real MCP client round trip over /mcp”The port’s own MCP client (the same SDK it uses to connect to remote servers) connects to a
toolkit.serve(...) instance and drives tools/list + tools/call — a genuine local
client↔server round trip, no external network.
import io.github.muthuishere.toolnexus.*;import io.modelcontextprotocol.client.McpClient;import io.modelcontextprotocol.client.McpSyncClient;import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapperSupplier;import io.modelcontextprotocol.spec.McpSchema;import java.util.Map;
public class Example { public static void main(String[] args) throws Exception { Tool echo = NativeTool.of("echo", "echoes back", Map.of("type", "object", "properties", Map.of("text", Map.of("type", "string")), "required", java.util.List.of("text")), (Map<String, Object> a) -> a.get("text"));
try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false).extraTools(echo))) { A2AServer.ServeHandle handle = tk.serve("127.0.0.1:0", new Toolkit.ServeOptions().mcp(new McpServe.MCPServeConfig().name("gateway").version("1.0.0"))); try { HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport .builder(handle.url() + "/mcp") .jsonMapper(new JacksonMcpJsonMapperSupplier().get()) .build(); McpSyncClient client = McpClient.sync(transport) .clientInfo(new McpSchema.Implementation("docs-example", "1.0.0")) .build(); client.initialize(); try { if (!"gateway".equals(client.getServerInfo().name())) throw new AssertionError(client.getServerInfo());
boolean hasEcho = client.listTools().tools().stream().anyMatch(t -> t.name().equals("echo")); if (!hasEcho) throw new AssertionError("echo should be advertised");
McpSchema.CallToolResult res = client.callTool( new McpSchema.CallToolRequest("echo", Map.of("text", "over mcp"))); String text = ((McpSchema.TextContent) res.content().get(0)).text(); if (!"over mcp".equals(text)) throw new AssertionError(text);
System.out.println("ok: " + text); } finally { client.closeGracefully(); } } finally { handle.stop(); } } }}3. The full surface — onCall telemetry, and an erroring tool never crashes the server
Section titled “3. The full surface — onCall telemetry, and an erroring tool never crashes the server”tools/call maps a thrown exception to isError:true text, and the server keeps serving —
onCall fires with the source and timing of every inbound call either way.
import io.github.muthuishere.toolnexus.*;import io.modelcontextprotocol.client.McpClient;import io.modelcontextprotocol.client.McpSyncClient;import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapperSupplier;import io.modelcontextprotocol.spec.McpSchema;import java.util.List;import java.util.Map;import java.util.concurrent.CopyOnWriteArrayList;
public class Example { public static void main(String[] args) throws Exception { Tool boom = NativeTool.of("boom", "always throws", Map.of("type", "object", "properties", Map.of()), (Map<String, Object> a) -> { throw new RuntimeException("kaboom"); }); List<McpServe.OnCallEvent> calls = new CopyOnWriteArrayList<>();
try (Toolkit tk = Toolkit.create(new Toolkit.Options().builtins(false).extraTools(boom))) { A2AServer.ServeHandle handle = tk.serve("127.0.0.1:0", new Toolkit.ServeOptions().mcp(new McpServe.MCPServeConfig()).onCall(calls::add)); try { HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport .builder(handle.url() + "/mcp") .jsonMapper(new JacksonMcpJsonMapperSupplier().get()) .build(); McpSyncClient client = McpClient.sync(transport) .clientInfo(new McpSchema.Implementation("docs-example", "1.0.0")) .build(); client.initialize(); try { McpSchema.CallToolResult res = client.callTool(new McpSchema.CallToolRequest("boom", Map.of())); if (!Boolean.TRUE.equals(res.isError())) throw new AssertionError("expected isError"); String text = ((McpSchema.TextContent) res.content().get(0)).text(); if (!text.contains("kaboom")) throw new AssertionError(text);
if (calls.size() != 1) throw new AssertionError(calls.size()); if (!calls.get(0).name().equals("boom") || !calls.get(0).isError()) throw new AssertionError(calls.get(0));
// the server survives — it's still answering after the error. if (client.listTools().tools().isEmpty()) throw new AssertionError("server should still be up");
System.out.println("ok: " + text + " (onCall fired " + calls.size() + "x)"); } finally { client.closeGracefully(); } } finally { handle.stop(); } } }}Fields and overloads
Section titled “Fields and overloads”| Member | Type | What it is |
|---|---|---|
Toolkit.ServeOptions.mcp |
MCPServeConfig |
Opting in mounts POST /mcp; null (and no config-block fallback) ⇒ no MCP surface. |
MCPServeConfig.name / .version |
String |
initialize serverInfo; defaults "toolnexus" / "0.1.0". |
MCPServeConfig.tools |
List<String> |
Allowlist by final exposed tool name; null ⇒ every toolkit tool. Unknown names ignored. |
exposedMcpTools(tools, cfg) |
List<Tool> |
The filtering step above, callable standalone. |
tools/list |
— | name used verbatim (already sanitized at registration — not re-sanitized like A2A skill ids); inputSchema = Tool.inputSchema(). |
tools/call |
— | Dispatches to Tool.execute; a throw becomes isError:true, never crashes the server. |
OnCall |
functional interface | accept(OnCallEvent{name, source, ms, isError}) per inbound tools/call. |
See also
Section titled “See also”A2AServer.start— the sibling inbound profile: skills over A2A, with a model in the loop.McpSource.load— the outbound mirror: connect to someone else’s MCP server.Toolkit.create— aggregates every tool source this profile re-exposes.