Build One
A complete tool server is genuinely about ten lines. Both SDKs build the tool definition from ordinary code (a schema in TypeScript, type hints and a docstring in Python), so you rarely write JSON by hand.
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
const server = new McpServer({ name: "greeter", version: "1.0.0" });
server.registerTool(
"greet",
{
// The model reads this description to decide whether to call the tool.
description: "Greet someone by name",
inputSchema: z.object({ name: z.string() }),
},
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
})
);
await server.connect(new StdioServerTransport());And the client side that connects to it:
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const mcp = new Client({ name: "demo", version: "1.0.0" });
// The transport starts the server process for you.
await mcp.connect(
new StdioClientTransport({ command: "node", args: ["greeter.js"] })
);
const { tools } = await mcp.listTools();
console.log(tools.map((t) => t.name)); // ["greet"]
const result = await mcp.callTool({
name: "greet",
arguments: { name: "Ada" },
});
console.log(result.content[0].text); // "Hello, Ada!"The payoff is in listTools(): it returns schemas you can hand straight to a model's tool-use API. Adding a server adds capabilities to your assistant without writing code for that server specifically.