To build an MCP server, install the official TypeScript SDK, register a tool and a resource on an McpServer, and serve it with serveStdio. Test it with the MCP Inspector, then add it to a client such as Claude Code with one command. The whole server is about 50 lines.
We built it in a scratch folder with SDK 2.1.0, part of the v2 line built for the 2026-07-28 version of the protocol, on Node.js 26. Every command below ran as written, and the output shown is what the tools printed.
- An MCP server gives AI apps tools (actions the model can call) and resources (data the app can read).
- Use the official v2 SDK,
@modelcontextprotocol/server. It implements the 2026-07-28 spec and still serves older clients. - Serve over stdio with
serveStdio, and keep stdout for protocol messages only. - Test with the MCP Inspector’s CLI or a 20-line client before you connect a real app.
- Validate every input, keep each tool narrow, and treat what tools return as untrusted.
Before you start: MCP changed in July 2026
If MCP is new to you, start with the basics. This tutorial assumes you know that a server offers tools and resources to a client inside an AI app.
MCP connects one agent to tools and data. When you want agents to hand work to each other instead, that is the separate job of A2A.
The 2026-07-28 revision of the spec made the protocol stateless. The initialize handshake is gone: every request carries its own protocol version and client capabilities, and servers answer a new server/discover call. The official TypeScript SDK was rebuilt for it as version 2, split into @modelcontextprotocol/server and @modelcontextprotocol/client.
That makes many 2025 tutorials misleading. They wire a server with new StdioServerTransport() and server.connect(). In SDK v2, serveStdio replaces that wiring, and by default it still serves older clients that open with the 2025 handshake.
Step 1: set up the project
You need Node.js 22.19 or newer. Node runs TypeScript files directly from version 22.18, and the MCP Inspector needs 22.19. Then create the project:
mkdir notes-server && cd notes-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zodThe type=module line enables modern import syntax. Zod is the schema library the SDK’s examples use to describe and check tool inputs. We got @modelcontextprotocol/server 2.1.0 and zod 4.6.5.
Step 2: build the MCP server
Our server keeps notes in a JSON file. It has one tool, add_note, which the model can call to save a note. It has one resource, notes://all, which an app can read to show them. Save this as server.ts:
import { readFile, writeFile } from 'node:fs/promises';
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const NOTES_FILE = new URL('./notes.json', import.meta.url);
async function loadNotes(): Promise<string[]> {
try {
return JSON.parse(await readFile(NOTES_FILE, 'utf8'));
} catch {
return []; // no file yet
}
}
serveStdio(() => {
const server = new McpServer({ name: 'notes', version: '1.0.0' });
// A tool: an action the model can take
server.registerTool(
'add_note',
{
title: 'Add note',
description: 'Save a short note so it can be read later.',
inputSchema: z.object({ text: z.string().min(1).max(500) }),
},
async ({ text }) => {
const notes = await loadNotes();
notes.push(text);
await writeFile(NOTES_FILE, JSON.stringify(notes, null, 2));
return { content: [{ type: 'text', text: `Saved note ${notes.length}.` }] };
},
);
// A resource: data the app can read
server.registerResource(
'all-notes',
'notes://all',
{ title: 'All notes', mimeType: 'text/plain' },
async (uri) => {
const notes = await loadNotes();
const text = notes.map((n, i) => `${i + 1}. ${n}`).join('\n') || 'No notes yet.';
return { contents: [{ uri: uri.href, text }] };
},
);
return server;
});
console.error('notes server running on stdio'); // stderr, never stdoutFour things are worth knowing about this code:
serveStdiotakes a function that builds the server, and the SDK calls it for each connection. The same function serves new and older clients.inputSchemais checked before your code runs. When we sent an empty note, the server answered with a tool error (Too small: expected string to have >=1 characters) instead of crashing.A tool returns
contentfor the model to read. A resource returnscontentstagged with the URI that was asked for.The last line logs with
console.error. In a stdio server, stdout is the protocol channel, and the client parses every line there as a message.
Step 3: test it with the MCP Inspector
The MCP Inspector is the official test client. In CLI mode it starts your server, sends one request and prints the answer. The --protocol-era modern flag makes it speak the 2026-07-28 protocol:
npx @modelcontextprotocol/inspector --cli node server.ts --method tools/list --protocol-era modern
npx @modelcontextprotocol/inspector --cli node server.ts --method tools/call --tool-name add_note --tool-arg 'text=Renew the domain before Friday' --protocol-era modern
npx @modelcontextprotocol/inspector --cli node server.ts --method resources/read --uri notes://all --protocol-era modernThe first command listed add_note with its schema, and the second saved the note. The third read it back:
{
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "notes",
"version": "1.0.0"
}
},
"contents": [
{
"uri": "notes://all",
"text": "1. Renew the domain before Friday"
}
],
"ttlMs": 0,
"cacheScope": "private"
}The _meta block and the two cache fields are new in the 2026-07-28 spec: servers identify themselves in every result, and resource reads carry a freshness hint. We used Inspector 2.8.0. Drop --cli and it serves the same checks as a web page at http://127.0.0.1:6274.
Or test it with a small client
A client is the other half of MCP, and a short one is a useful test. Install the client package, then save this as client.ts:
npm install @modelcontextprotocol/clientimport { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const client = new Client(
{ name: 'notes-tester', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } }, // use the 2026-07-28 protocol if the server has it
);
await client.connect(new StdioClientTransport({ command: 'node', args: ['server.ts'] }));
console.log('Protocol:', client.getNegotiatedProtocolVersion());
const { tools } = await client.listTools();
console.log('Tools:', tools.map((t) => t.name));
const saved = await client.callTool({ name: 'add_note', arguments: { text: 'Ship the MCP post' } });
console.log('Tool result:', saved.content);
const read = await client.readResource({ uri: 'notes://all' });
console.log('Resource:', read.contents[0]);
await client.close();Protocol: 2026-07-28
notes server running on stdio
Tools: [ 'add_note' ]
Tool result: [ { type: 'text', text: 'Saved note 2.' } ]
Resource: {
uri: 'notes://all',
text: '1. Renew the domain before Friday\n2. Ship the MCP post'
}The v2 client speaks the 2025 handshake unless you opt in, which is what mode: 'auto' does. Without it, the same script connected on protocol 2025-11-25, and so did a client built on the v1 SDK. That is the backward compatibility serveStdio buys you. To turn a client like this into an agent that picks its own tools, see build your first AI agent.
Step 4: connect it to Claude Code
Claude Code adds a local server with one command. Everything after -- is the command that starts your server, passed through untouched. Use an absolute path, so it works from any folder:
claude mcp add --transport stdio notes -- node /absolute/path/to/notes-server/server.ts
claude mcp listWith Claude Code 2.1.276, claude mcp list checked the server and reported it as Connected. This adds the server for you, in the current project only.
To share a server with your team, commit a .mcp.json file at the project root instead:
{
"mcpServers": {
"notes": {
"command": "node",
"args": ["/absolute/path/to/notes-server/server.ts"]
}
}
}Claude Code asks each person to approve servers from a shared .mcp.json before it uses them. Until then, claude mcp list showed ours as pending approval. Other MCP clients need the same two facts, the command and its arguments, in their own settings format, so check each app’s MCP docs.
Security basics for your first MCP server
An MCP server is code an AI can trigger on your machine, so small choices matter:
Validate every input. The
max(500)limit stops a confused model from pouring a novel into your notes file.Keep each tool narrow.
add_notewrites exactly one file. A tool that accepts any path or any shell command can hand the model your whole computer.Pass secrets as environment variables.
claude mcp add -e API_KEY=...hands a key to the server without putting it in code or in tool results.Treat returned content as untrusted. A note, web page or email can contain instructions aimed at the model. That is prompt injection, and every server you add widens the door.
Sign-in before the internet. A stdio server is reachable only by the app that started it. Put a server on the web and it needs real authentication, which the spec builds on OAuth.
The same questions apply to servers other people wrote. Our guide to the MCP servers worth installing first shows how to vet one before you add it.
FAQ
Do I need TypeScript to build an MCP server?
No. MCP is a protocol, not a library, and there are official SDKs for other languages, including Python. We used TypeScript because its v2 SDK already implements the 2026-07-28 spec.
Will my server work with clients that still use the 2025 protocol?
Yes, if you serve it with serveStdio, which answers 2025-era clients by default. We tested ours with a v1 SDK client and with the v2 client in both modes.
What is the difference between an MCP tool and a resource?
The model decides when to call a tool, such as add_note. The app or the user decides when to read a resource, such as notes://all. Use tools for actions and resources for context.
How do I put my MCP server online?
Serve it over the Streamable HTTP transport instead of stdio, and require sign-in in front of it. Get the tools right locally first, because a remote server is harder to change and easier to attack.
Read next: agent skills explained, another way to teach an agent a job, or what AGENTS.md is.
- Key changes, specification 2026-07-28, Model Context Protocol, July 2026
- MCP TypeScript SDK, GitHub, accessed September 2026
- Serve over stdio, MCP TypeScript SDK v2 documentation, accessed September 2026
- MCP Inspector, GitHub, version 2.8.0
- Connect Claude Code to tools via MCP, Claude Code documentation, accessed September 2026
- Modules: TypeScript, Node.js documentation, accessed September 2026




