Claude Code Academy

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 tsx

Create 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 dev

You 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

PatternWhen to use
ToolClaude needs to call a function and get a result (API calls, DB queries, calculations)
ResourceClaude needs to read data (configs, docs, current state)
PromptClaude 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 claude

Next Steps

In the next tutorial, we build a specific MCP server: one that stores and retrieves Claude's session memory across conversations.