This tutorial walks you through building a complete MCP server in TypeScript — from setup to connecting it to Claude Code and Cursor.
By the end, you’ll have a server that exposes tools, serves resources, and works with any MCP-compatible host.
Prerequisites
- Node.js 18+
- Basic TypeScript knowledge
- An MCP host (Claude Desktop, Claude Code, or Cursor)
Step 1: Project setup
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true
}
}
Update package.json:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Step 2: Create the server
// src/index.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: "my-dev-tools",
version: "1.0.0",
});
Step 3: Add a tool
Tools are actions the AI can execute. Let’s add a tool that checks if a website is up:
server.tool(
"check_website",
"Check if a website is responding",
{ url: z.string().url().describe("The URL to check") },
async ({ url }) => {
try {
const start = Date.now();
const response = await fetch(url);
const latency = Date.now() - start;
return {
content: [{
type: "text",
text: `${url}: ${response.status} (${latency}ms)`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `${url}: UNREACHABLE — ${error.message}`
}],
isError: true
};
}
}
);
Step 4: Add a resource
Resources are data the AI can read. Let’s expose the project’s package.json:
import { readFileSync } from "fs";
server.resource(
"package_json",
"file://package.json",
"The project's package.json",
async () => {
const content = readFileSync("package.json", "utf-8");
return {
contents: [{
uri: "file://package.json",
mimeType: "application/json",
text: content
}]
};
}
);
Step 5: Add a prompt template
Prompts are reusable templates users can invoke:
server.prompt(
"review_dependencies",
"Review project dependencies for security and updates",
{},
() => ({
messages: [{
role: "user",
content: "Review the package.json dependencies. Check for:\n1. Known security vulnerabilities\n2. Outdated packages\n3. Unnecessary dependencies\n4. Missing important packages\n\nProvide specific recommendations."
}]
})
);
Step 6: Connect the transport
// At the bottom of src/index.ts
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
Step 7: Build and test
npm run build
Connect to Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS):
{
"mcpServers": {
"my-dev-tools": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop. You should see your tools available in the tools menu.
Connect to Claude Code
claude mcp add my-dev-tools node /absolute/path/to/dist/index.js
Now you can ask Claude: “Check if aimadetools.com is up” and it will use your check_website tool.
Step 8: Add error handling
Production servers need proper error handling:
server.tool(
"query_database",
"Run a read-only SQL query",
{ query: z.string().describe("SQL SELECT query") },
async ({ query }) => {
// Validate: only allow SELECT
if (!query.trim().toUpperCase().startsWith("SELECT")) {
return {
content: [{ type: "text", text: "Error: Only SELECT queries are allowed" }],
isError: true
};
}
try {
const results = await db.query(query);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Query failed: ${error.message}` }],
isError: true
};
}
}
);
Common patterns
Environment variables for secrets
const API_KEY = process.env.MY_API_KEY;
if (!API_KEY) throw new Error("MY_API_KEY required");
Pass env vars in the host config:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["dist/index.js"],
"env": { "MY_API_KEY": "sk-..." }
}
}
}
Multiple tools in one server
Group related tools in a single server. A “dev-tools” server might have: check_website, query_database, read_logs, run_tests.
Publishing to npm
npm publish
Users install with:
npx my-mcp-server
Next steps
- Build an MCP Server in Python — same tutorial in Python
- MCP Security Risks — what can go wrong
- Best MCP Servers — community servers to learn from
- MCP Complete Guide — full architecture reference
Related: What is MCP? · MCP vs A2A vs ACP · What is Tool Calling?