Create src/index.ts and paste this in:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 1. Create the server
const server = new McpServer({
name: "my-first-mcp-server",
version: "1.0.0",
});
// 2. Register a tool
server.tool(
"say_hello", // tool name
"Returns a greeting for a given name", // description Claude reads
{ name: z.string().describe("The name to greet") }, // input schema
async ({ name }) => ({ // handler
content: [{ type: "text", text: `Hello, ${name}! MCP is working.` }],
})
);
// 3. Start the server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running"); // stderr only!
Run it:
npm run dev
If you see MCP server running with no errors — your server works. It's now sitting there waiting for a client to connect. Ctrl+C to stop it.
What each part does
McpServer — the main server object. Give it a name and version. That's it.
server.tool() — registers one tool. Takes four arguments:
- Tool name (snake_case by convention)
- Description — this is what Claude reads to decide when to call your tool. Be specific.
- Input schema — defined with Zod. Each field describes what Claude needs to pass.
- Handler — the async function that runs when Claude calls the tool. Always returns
{ content: [{ type: "text", text: "..." }] }.
StdioServerTransport — sets up the stdio communication channel. Claude Desktop will start your server as a subprocess and talk to it through this.
server.connect(transport) — starts listening. Your server is now alive.
That's the whole pattern. Everything you build from here is just adding more tools.