A minimal TypeScript MCP server registers tools and connects a transport:
ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "invoices", version: "1.0.0" });
server.tool(
"search_invoices",
"Search invoices by customer and status. Returns at most 20 rows.",
{ customer: z.string(), status: z.enum(["open", "paid", "void"]).optional() },
async ({ customer, status }) => {
const rows = await db.invoices.find({ customer, status, limit: 20 });
return { content: [{ type: "text", text: JSON.stringify(rows) }] };
},
);
server.resource("invoice", "invoice://{id}", async (uri, { id }) => ({
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(await db.invoices.get(id)) }],
}));
await server.connect(new StdioServerTransport());Design rules: return compact results (agents pay per token), never expose an unbounded query, and authorise inside each handler — the transport does not know who the end user is.
Exercise: add a `void_invoice` tool that requires a reason argument and refuses invoices older than 90 days, then confirm the refusal message is understandable enough for the agent to recover.