Guides

Build your first MCP server#

This page turns the beginner concepts into a runnable local server. Keep the first pass deliberately small: one stdio server, one read-only tool, one structured result, and one Inspector call before touching host installs or agent-connector surfaces.

Reference baseline#

The implementation below follows the current official TypeScript SDK style: McpServer, registerTool, Zod-backed input schemas, optional outputSchema, and structuredContent beside text content. It also keeps the first transport local with stdio.

ReferenceUse it for
Build an MCP serverThe official quickstart for the TypeScript SDK and stdio server shape.
Tools specTool metadata, input schemas, optional output schemas, and result content.
MCP InspectorA protocol-aware test client. Use it before debugging a real host UI.

1. Create the project#

Use a new folder so you can tell package errors apart from host errors. The SDK version below was current when this guide was refreshed; re-run npm view @modelcontextprotocol/sdk version before changing a published tutorial package.

terminal
bash
mkdir acme-db-mcp
cd acme-db-mcp
npm init -y
npm install @modelcontextprotocol/sdk@^1.29.0 zod@^3

2. Write one read-only tool#

This example returns both text and structured JSON. Text is useful for the model's answer; structuredContent is useful when the result has fields that should remain machine-readable.

my-mcp-server.mjs
ts
// my-mcp-server.mjs
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: "acme-db", version: "0.1.0" });

server.registerTool(
  "schema_summary",
  {
    title: "Schema summary",
    description: "Return a short, read-only summary of the database schema.",
    inputSchema: {
      table: z.string().min(1).optional().describe("Optional table name"),
    },
    outputSchema: {
      summary: z.string(),
      table: z.string().optional(),
    },
  },
  async ({ table }) => {
    const structuredContent = {
      table,
      summary: table
        ? `Schema summary for ${table}: id, email, created_at`
        : "Schema summary for all tables: users, orders, invoices",
    };

    return {
      structuredContent,
      content: [{ type: "text", text: structuredContent.summary }],
    };
  },
);

await server.connect(new StdioServerTransport());

Why not start with resources, prompts, sampling, or elicitation?

They are important MCP features, but a first server needs one tight loop: advertise a capability, receive arguments, validate them, and return a predictable result. Add other protocol surfaces after this loop is boring.

3. Run with MCP Inspector#

A stdio MCP server waits for JSON-RPC messages on stdin, so running node my-mcp-server.mjs directly can look idle. Inspector starts the process as a host would and lets you list and call tools.

terminal
bash
npx -y @modelcontextprotocol/inspector node ./my-mcp-server.mjs

# In Inspector:
# 1. Connect to the stdio server.
# 2. Open Tools.
# 3. Call schema_summary with:
#    { "table": "users" }

4. What success looks like#

CheckExpected result
Tool listInspector shows schema_summary with the title, description, and input schema.
Valid callCalling with { "table": "users" } returns a short text result and structured JSON.
Invalid callBad input returns a clear validation error instead of corrupting the stdio stream.

After these checks pass, continue to Connect your first host.