How to Create Your Own MCP Server
An MCP server is a process that Claude can talk to. You define tools (functions Claude can call), resources (data Claude can read), and prompts (reusable templates).
This tutorial builds a minimal but real MCP server in TypeScript.
Prerequisites
- Node.js 18+
- npm or pnpm
- Basic TypeScript knowledge
Step 1 — Scaffold the Project
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsxCreate tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}Step 2 — Write the Server
Create 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-mcp-server',
version: '1.0.0',
})
// --- Define a tool ---
server.tool(
'get_exchange_rate',
'Get the current exchange rate between two currencies',
{
from: z.string().describe('Source currency code, e.g. MYR'),
to: z.string().describe('Target currency code, e.g. USD'),
},
async ({ from, to }) => {
// Replace with a real API call
const rate = from === 'MYR' && to === 'USD' ? 0.22 : 1.0
return {
content: [
{
type: 'text',
text: `1 ${from} = ${rate} ${to}`,
},
],
}
}
)
// --- Start the server ---
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
console.error('MCP server running on stdio')
}
main().catch(console.error)Step 3 — Add a Build Script
In package.json:
{
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts"
}
}Test it runs:
npm run devYou should see: MCP server running on stdio
Step 4 — Connect it to Claude Code
Add to .claude/mcp_servers.json in your project:
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
}
}
}Or use tsx for development (no build step):
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/my-mcp-server/src/index.ts"]
}
}
}Start Claude Code. Ask it:
What exchange rate tools do you have?
Claude will find get_exchange_rate and be able to call it.
Adding a Resource
Resources are data Claude can read (like a file or a DB record):
server.resource(
'config',
'mcp://my-mcp-server/config',
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({ environment: 'production', version: '1.0' }),
},
],
})
)Adding a Prompt
Prompts are reusable templates Claude can invoke:
server.prompt(
'review_payment',
'Generate a payment review checklist',
{
amount: z.string().describe('Transaction amount'),
currency: z.string().describe('Currency code'),
},
({ amount, currency }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Review this ${currency} ${amount} transaction for PCI compliance and flag any anomalies.`,
},
},
],
})
)MCP Server Patterns
| Pattern | When to use |
|---|---|
| Tool | Claude needs to call a function and get a result (API calls, DB queries, calculations) |
| Resource | Claude needs to read data (configs, docs, current state) |
| Prompt | Claude needs a structured starting point for a task |
Debugging
MCP servers communicate over stdio. Anything logged to stderr appears in Claude's debug output — always use console.error() for logs, not console.log() (which would corrupt the JSON protocol on stdout).
# Watch the raw MCP traffic
MCP_DEBUG=1 claudeNext Steps
In the next tutorial, we build a specific MCP server: one that stores and retrieves Claude's session memory across conversations.