TutorialsApr 4, 202612 minUpdated Apr 7, 2026

How to Build Your Own MCP Server: A Step-by-Step Tutorial

Learn how to build a custom MCP server from scratch using TypeScript. This step-by-step tutorial covers the MCP SDK, tool definitions, and connecting your server to Claude.

multiagent.tools team
mcp server tutorialbuild mcp servertypescriptclaudedeveloper guide

Want to give Claude custom capabilities? Building an MCP server is easier than you might think. In this tutorial, we'll create a fully functional MCP server from scratch using TypeScript and the official MCP SDK.

By the end, you'll have a working server that you can connect to Claude Desktop and use in your daily workflow.

What Do You Need Before Starting?

Make sure you have these installed:

  • Node.js 18+download from nodejs.org
  • TypeScript — We'll install it as a dev dependency
  • Claude Desktop — For testing your server (see our setup guide)
  • A code editor — VS Code, Cursor, or any editor you prefer

Basic TypeScript knowledge is helpful but not required — the code is straightforward.

How Do You Set Up the Project?

Create a new directory and initialize the project:

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node

Create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Update package.json with build scripts:

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

How Do You Create a Basic MCP Server?

Create src/index.ts with the minimal server structure:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

// Create the server instance
const server = new McpServer({
  name: 'my-mcp-server',
  version: '1.0.0',
})

// Define your first tool
server.tool(
  'greet',
  'Greet a person by name',
  { name: { type: 'string', description: 'The person\'s name' } },
  async ({ name }) => ({
    content: [{ type: 'text', text: `Hello, ${name}! Welcome to MCP.` }],
  })
)

// Start the server using stdio transport
const transport = new StdioServerTransport()
await server.connect(transport)
console.error('MCP server running on stdio')

This creates a minimal MCP server with one tool: greet. The server communicates over stdio (standard input/output), which is how Claude Desktop connects to local MCP servers.

Build and test:

npm run build

How Do You Add Useful Tools?

The greet tool is a starting point. Let's add tools that actually do something useful.

A Word Counter Tool

server.tool(
  'count_words',
  'Count words, characters, and lines in text',
  { text: { type: 'string', description: 'Text to analyze' } },
  async ({ text }) => {
    const words = text.trim().split(/\s+/).length
    const chars = text.length
    const lines = text.split('\n').length
    return {
      content: [{
        type: 'text',
        text: `Words: ${words}\nCharacters: ${chars}\nLines: ${lines}`,
      }],
    }
  }
)

A JSON Formatter Tool

server.tool(
  'format_json',
  'Pretty-print and validate a JSON string',
  { json: { type: 'string', description: 'JSON string to format' } },
  async ({ json }) => {
    try {
      const parsed = JSON.parse(json)
      return {
        content: [{
          type: 'text',
          text: JSON.stringify(parsed, null, 2),
        }],
      }
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Invalid JSON: ${error instanceof Error ? error.message : 'Unknown error'}`,
        }],
        isError: true,
      }
    }
  }
)

A Timestamp Tool

server.tool(
  'timestamp',
  'Get the current date and time in various formats',
  { timezone: { type: 'string', description: 'Timezone (e.g., UTC, America/New_York)', default: 'UTC' } },
  async ({ timezone }) => {
    const now = new Date()
    return {
      content: [{
        type: 'text',
        text: [
          `ISO: ${now.toISOString()}`,
          `Unix: ${Math.floor(now.getTime() / 1000)}`,
          `Local: ${now.toLocaleString('en-US', { timeZone: timezone || 'UTC' })}`,
        ].join('\n'),
      }],
    }
  }
)

How Do You Add Resources?

Resources let Claude read data from your server. Unlike tools (which perform actions), resources provide read-only context.

server.resource(
  'server-info',
  'info://server',
  async () => ({
    contents: [{
      uri: 'info://server',
      text: JSON.stringify({
        name: 'My MCP Server',
        version: '1.0.0',
        tools: ['greet', 'count_words', 'format_json', 'timestamp'],
        uptime: process.uptime(),
      }, null, 2),
      mimeType: 'application/json',
    }],
  })
)

Claude can read this resource to understand what your server provides.

How Do You Connect to Claude Desktop?

Build your server:

npm run build

Edit your Claude Desktop configuration file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

Add your server:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/absolute/path/to/my-mcp-server/dist/index.js"]
    }
  }
}

Restart Claude Desktop. You should see my-server in the MCP tools list. Try asking Claude:

  • "Count the words in this paragraph..."
  • "Format this JSON: {a:1,b:2}"
  • "What time is it in New York?"

For more configuration options, see: How to Set Up MCP Servers with Claude Desktop

What Are the Best Practices for MCP Servers?

1. Validate All Inputs

Never trust input data. Validate parameters before processing:

server.tool('example', 'Example tool', 
  { count: { type: 'number', description: 'Must be positive' } },
  async ({ count }) => {
    if (typeof count !== 'number' || count <= 0) {
      return { content: [{ type: 'text', text: 'Error: count must be a positive number' }], isError: true }
    }
    // ... process
  }
)

2. Handle Errors Gracefully

Return helpful error messages instead of crashing. Use isError: true to signal errors to the client:

try {
  // ... your logic
} catch (error) {
  return {
    content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown'}` }],
    isError: true,
  }
}

3. Keep Tools Focused

Each tool should do one thing well. Instead of a do_everything tool, create specific tools like read_file, write_file, search_files.

4. Write Clear Descriptions

The tool description is what Claude uses to decide when to use your tool. Be specific:

  • Bad: "Process data"
  • Good: "Count words, characters, and lines in a text string"

5. Use TypeScript

TypeScript catches bugs at compile time instead of runtime. The MCP SDK has excellent TypeScript support.

6. Log to stderr

MCP uses stdout for protocol communication. All logging must go to stderr:

console.error('This appears in logs, not in MCP protocol')

How Do You Publish Your Server?

Once your server is ready, share it with the community:

  1. Publish to npm:

    npm publish
    
  2. Add to GitHub with the mcp-server topic tag

  3. Submit to registries like Smithery and MCP.so

Your server will appear in directories like our MCP server catalog once indexed by our scrapers.

What Should You Build Next?

Now that you know the basics, here are ideas for useful MCP servers:

  • API wrapper — Give Claude access to your company's internal API
  • Database client — Let Claude query your PostgreSQL/MongoDB/Redis
  • Monitoring tool — Check server status, logs, metrics from Claude
  • Deployment tool — Deploy to Vercel/AWS/Docker from conversation
  • Content manager — CRUD operations on your CMS (Directus, Strapi, etc.)

For inspiration, browse existing MCP servers in our catalog — 500+ servers to learn from.


Need inspiration? Browse 500+ MCP servers in our catalog.