---
title: "Build Your Own MCP, minimal server skeleton in TypeScript | Recipe — StudioMeyer Academy"
description: "From npm init to a working MCP server with one tool, in 60 lines of TypeScript. The skeleton you can fork for everything in Phase 6-10."
canonical: "https://studiomeyer.academy/en/recipes/6.1-server-skeleton"
language: en
markdown_versions: ["https://studiomeyer.academy/en/recipes/6.1-server-skeleton.md"]
publisher: "StudioMeyer Academy, https://studiomeyer.academy (llms.txt: https://studiomeyer.academy/llms.txt)"
---

[← Alle Recipes](https://studiomeyer.academy/recipes)

Phase 6 · Build Your Own MCP Server·6 steps

# Build Your Own MCP, minimal server skeleton in TypeScript

From npm init to a working MCP server with one tool, in 60 lines of TypeScript. The skeleton you can fork for everything in Phase 6-10.

6 steps0%

Du liest ohne Account. Mit [Login](https://studiomeyer.academy/login?next=/recipes/6.1-server-skeleton) speichern wir Step-Fortschritt + Notes.

# Build Your Own MCP, minimal server skeleton

You don't need a framework. The official `@modelcontextprotocol/sdk` is enough. By the end of this recipe you have a working MCP server with one tool, runnable via `npx -y your-package` from any MCP client.

This recipe is a tutorial, you will read it, your AI will write the code with you. There are no installable skills here. Phase 6-10 is about teaching, not pasting. Phase 11+ ships ready-to-deploy code patterns.

### Step 1: Bootstrap the package

```bash
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install '@modelcontextprotocol/sdk@^1' 'zod@^3'
npm install -D typescript @types/node tsx vitest
```

**About version pinning:** The MCP SDK is on `1.x` as of mid-2026 (`1.29.x` was current at this recipe's last update). A `2.x` line is on the SDK's roadmap and will likely be a breaking change, pinning to `^1` keeps you on the current API surface and lets you upgrade deliberately when v2 ships. Same idea for Zod: `^3` because Zod 4 made the input-validation API around `.strict()` / `.passthrough()` slightly different.

Then create `tsconfig.json`:

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": false
  },
  "include": ["src/**/*"]
}
```

And in `package.json`, add:

```json
{
  "type": "module",
  "main": "dist/server.js",
  "bin": { "my-mcp-server": "./dist/server.js" },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/server.ts",
    "test": "vitest run"
  }
}
```

The `bin` field makes `npx -y my-mcp-server` work after publish. The `type: module` tells Node to use ESM.

### Step 2: Write the server

`src/server.ts`:

```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  type Tool,
} from '@modelcontextprotocol/sdk/types.js';

const helloTool: Tool = {
  name: 'hello',
  description: 'Say hello to a name. Returns a friendly greeting.',
  inputSchema: {
    type: 'object',
    properties: {
      name: { type: 'string', description: 'Who to greet.' },
    },
    required: ['name'],
  },
};

const server = new Server(
  { name: 'my-mcp-server', version: '0.1.0' },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [helloTool],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const args = (req.params.arguments ?? {}) as Record<string, unknown>;
  if (req.params.name === 'hello') {
    const name = String(args.name || 'world');
    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
  }
  return {
    content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
    isError: true,
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);
```

That's the whole server. 50 lines. List tools, handle a call, connect stdio. The MCP SDK does the wire protocol.

### Step 3: Build + test locally

```bash
npm run build
chmod +x dist/server.js
```

Now register it as a local MCP server in Claude Code:

```bash
claude mcp add my-server -s user -- node /absolute/path/to/dist/server.js
```

Open a fresh Claude Code session and ask "say hello to me with name=Matthias". Claude calls your `hello` tool. You see the response.

### Step 4: Add a second tool with input validation

The `hello` tool trusted whatever the LLM sent. Production tools validate. Add zod:

```typescript
import { z } from 'zod';

const greetSchema = z.object({
  name: z.string().min(1).max(80),
  language: z.enum(['en', 'de', 'es']).default('en'),
});

const greetTool: Tool = {
  name: 'greet',
  description: 'Greet someone in their language.',
  inputSchema: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      language: { type: 'string', enum: ['en', 'de', 'es'] },
    },
    required: ['name'],
  },
};
```

In the call handler:

```typescript
if (req.params.name === 'greet') {
  const parsed = greetSchema.safeParse(args);
  if (!parsed.success) {
    return {
      content: [{ type: 'text', text: `Bad input: ${parsed.error.message}` }],
      isError: true,
    };
  }
  const greetings = { en: 'Hello', de: 'Hallo', es: 'Hola' };
  return {
    content: [{ type: 'text', text: `${greetings[parsed.data.language]}, ${parsed.data.name}!` }],
  };
}
```

The pattern: Zod schema for runtime validation, JSON schema in the Tool definition for the LLM. They duplicate intentionally. JSON schema teaches the LLM how to call, Zod catches the rare bad call that slips through.

### Step 5: Verify

Run `academy_validate_step`. The validator checks `package.json` in cwd has `@modelcontextprotocol/sdk` as a dependency AND a `bin` or `main` entry. If you skipped the `bin` field, add it now, without it `npx -y` cannot find the entry point.

### Step 6: What the next 4 recipes cover

This is the foundation. The rest of Phase 6 layers in:

- **6.2 Tool design**, naming, descriptions, idempotency, error responses. The difference between a tool the LLM uses and one it ignores.
- **6.3 Input validation**. Zod patterns that don't fight the LLM. `.strip()` policy, error response shape.
- **6.4 Testing MCP tools**, vitest setup, in-memory transport for integration tests, smoke tests.
- **6.5 Logging pattern**, stderr-only, structured. **stdout corrupts the wire protocol**, this is the most common production bug in homemade MCP servers.

Phase 7 then adds dual-transport (stdio + HTTP), OAuth, multi-tenant. Phase 8 deploys it. Phase 9 publishes it. Phase 10 turns it into a SaaS.

If you want the production-grade code patterns directly (rate-limit middleware, idempotency tables, RLS schema, blue-green deploy scripts), those are **Phase 11-15** in the **Team plan**, every recipe ships with installable Skills + ready-to-deploy code. Phase 6-10 here is the teaching path; Phase 11-15 is the production library.

Client check · run on your machine

cat package.json 2>/dev/null | python3 -c "import json,sys; p=json.load(sys.stdin); deps=list((p.get(\\"dependencies\\") or {}).keys()); print(\\"sdk:\\", \\"@modelcontextprotocol/sdk\\" in deps); print(\\"bin:\\", bool(p.get(\\"bin\\"))); print(\\"main:\\", bool(p.get(\\"main\\")))" 2>/dev/null || echo "no package.json in cwd"

**Expect:** sdk: True, plus either bin or main is True.

**If stuck:** Run \`npm init -y && npm install @modelcontextprotocol/sdk zod\`, then add \`"bin": { "your-server": "./dist/server.js" }\` to package.json.

[← Weekly review, synthesize a we](https://studiomeyer.academy/recipes/5.5-weekly-review) [Tool design, what makes a good →](https://studiomeyer.academy/recipes/6.2-tool-design)

Phase 6

Server skeleton, tool design, validation, testing, logging

[Alle Phasen →](https://studiomeyer.academy/recipes)

Concepts

[MCP basics](https://studiomeyer.academy/concepts/mcp_basics) [Dual transport (stdio + HTTP)](https://studiomeyer.academy/concepts/dual_transport)

## Sitemap

Public pages of this site as Markdown: [sitemap](https://studiomeyer.academy/sitemap.md). Curated entry point: [llms.txt](https://studiomeyer.academy/llms.txt), full text: [llms-full.txt](https://studiomeyer.academy/llms-full.txt).
