MCP
MCP server
An MCP server on SDK v2, one list and one call handler for every tool.
- title
- Text Slug
- hints
- readOnly, idempotent
- inputSchema
- text, separator
- failure
- isError: true
Access
- Import
import { createMcpServer } from "@agntn/tools/mcp" - Peer
@modelcontextprotocol/server >=2.2.0 <3 - Fails
isError: true
import { createMcpServer } from "@agntn/tools/mcp";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { tools } from "./tools.ts";
const server = createMcpServer({ name: "mypackage", version: "1.0.0" }, tools);
await server.connect(new StdioServerTransport());
createMcpServer(info, tools) gives you an unconnected server. One tools/list handler, one tools/call handler, every tool in the list. Connect it to stdio, to Streamable HTTP or to a pair in memory for tests. This site does the last one, in your tab.
What the client sees
The schema goes out exactly as you wrote it, additionalProperties: false and pattern included. The annotations come from effect:
readOnlyHintwhen the effect isreaddestructiveHintwhen it'sdestructiveidempotentHintfromidempotent, which defaults to readsopenWorldHintfromopenWorld, off unless you say so
details never reaches the client and structuredContent is never set. Clients that see structured output tend to prefer it over the text and hide the readable answer, so the text is the answer. Everything the next call needs goes in it.
Why not McpServer
SDK v2's McpServer.registerTool would take the schema. It also answers a call to toString, constructor or __proto__ with "Tool toString disabled", because its lookup reaches Object.prototype. It echoes a tool name with a newline and an escape code straight back to the client. It glues validation problems into one line and passes a thrown message through with its newline intact.
So the adapter sits on the low-level Server, which the SDK marks @deprecated, and does those parts itself. An unknown name, toString included, gets Unknown <server> tool: "<name>" with the name escaped. Validation gets one line per problem. A throw becomes <tool> failed: <message> on one line.
Errors
A bad input, a returned isError and a thrown error all come back as isError: true with text. Nothing escapes as a JSON-RPC error, so the model always gets something to read and a reason to try again.