MCPNode.jsAnthropicAI Agents

Building Your First MCP Server in Node.js

A complete walkthrough of the Model Context Protocol — what it is, why it matters, and how to build a production-ready MCP server in Node.js that exposes tools and resources to Claude Desktop and Claude Code.

Deepak Kaushal··11 min read

The Model Context Protocol (MCP) is quietly becoming the standard way to connect AI models to external systems. Instead of writing one-off integrations for every AI application, MCP defines a universal interface: your tools and data live in an MCP server, and any MCP-compatible client — Claude Desktop, Claude Code, or your own app — can discover and use them without additional integration work. This guide builds a complete, working MCP server in Node.js from scratch.

How MCP Works

MCP has three participants. The host is the AI application (Claude Desktop, Claude Code, or your custom app). The client is built into the host and manages connections to MCP servers. The server is a process you write that exposes tools, resources, and prompts. The host initiates a connection, discovers what the server offers, and then calls into the server whenever the model needs to use a tool or read a resource. The model never talks to MCP directly — the host orchestrates everything.

Project Setup

Create a new Node.js project and install the MCP SDK. Use ES modules (type: module in package.json) — the SDK is ESM-only.

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
# Set "type": "module" in package.json

Defining Your First Tool

Create index.js and build a minimal MCP server with one tool. Tools are defined with a name, a Zod schema for their input arguments, and an async handler that returns the result. The SDK validates inputs against the schema before calling your handler — bad inputs are rejected before your code runs.

// index.js
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',
});

// A tool that searches listings
server.tool(
  'search_listings',
  'Search marketplace listings by keyword, category, and location',
  {
    query: z.string().describe('Search keyword or phrase'),
    category: z
      .enum(['services', 'rentals', 'products'])
      .optional()
      .describe('Filter by category'),
    location: z.string().optional().describe('City or region'),
    maxPrice: z.number().optional().describe('Maximum price in USD'),
  },
  async ({ query, category, location, maxPrice }) => {
    // Replace with your actual data source
    const results = await searchDatabase({ query, category, location, maxPrice });

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  },
);

// Connect via stdio transport and start listening
const transport = new StdioServerTransport();
await server.connect(transport);

Adding a Resource

Resources expose readable data — think of them as MCP's version of GET endpoints. A resource has a URI, a MIME type, and a handler that returns content. Resources are ideal for data that the model should read as context: configuration, documentation, current system state, or live database records.

// Expose the full listing catalogue as a resource
server.resource(
  'listing-catalogue',
  'listings://all',
  { mimeType: 'application/json' },
  async (uri) => {
    const listings = await getAllListings();
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify(listings, null, 2),
        },
      ],
    };
  },
);

// Dynamic resource with a URI template
server.resource(
  'listing-detail',
  new ResourceTemplate('listings://{id}', { list: undefined }),
  { mimeType: 'application/json' },
  async (uri, { id }) => {
    const listing = await getListingById(id);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify(listing, null, 2),
        },
      ],
    };
  },
);

A Practical Multi-Tool Server

Real MCP servers expose multiple related tools. Here's a complete marketplace assistant server with search, listing detail, and booking availability tools — the same set you'd use with Claude's tool use API, but now usable from Claude Desktop and Claude Code without any additional configuration.

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: 'marketplace-mcp', version: '1.0.0' });

server.tool(
  'search_listings',
  'Search marketplace listings by keyword, category, price range, and location.',
  {
    query: z.string(),
    category: z.enum(['services', 'rentals', 'products']).optional(),
    maxPrice: z.number().optional(),
    location: z.string().optional(),
  },
  async (args) => ({
    content: [{ type: 'text', text: JSON.stringify(await searchListings(args)) }],
  }),
);

server.tool(
  'get_listing',
  'Get full details for a single listing including description, pricing, and provider info.',
  { id: z.string().describe('Listing ID from search results') },
  async ({ id }) => ({
    content: [{ type: 'text', text: JSON.stringify(await getListingById(id)) }],
  }),
);

server.tool(
  'check_availability',
  'Check whether a listing is available for a given date range.',
  {
    listingId: z.string(),
    startDate: z.string().describe('ISO 8601 date, e.g. 2026-07-10'),
    endDate: z.string().describe('ISO 8601 date, e.g. 2026-07-15'),
  },
  async ({ listingId, startDate, endDate }) => {
    const available = await checkAvailability(listingId, startDate, endDate);
    return {
      content: [
        {
          type: 'text',
          text: available
            ? 'Available for the requested dates.'
            : 'Not available for the requested dates.',
        },
      ],
    };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

Connecting to Claude Desktop

Add your server to Claude Desktop's config file. On macOS this is ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows it's %APPDATA%\Claude\claude_desktop_config.json. After saving, restart Claude Desktop — a hammer icon appears in the input bar showing your connected tools.

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

HTTP Transport for Remote Servers

stdio works only for locally-installed servers. For a server you deploy and share with a team, use the Streamable HTTP transport instead. It runs as an HTTP server with a single endpoint that handles both incoming requests and outgoing server-sent events.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';

const app = express();
app.use(express.json());

const server = new McpServer({ name: 'marketplace-mcp', version: '1.0.0' });
// ... register tools ...

app.all('/mcp', async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });
  res.on('close', () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.listen(3001, () => console.log('MCP server running on :3001'));

Testing with MCP Inspector

Before connecting to Claude Desktop, test your server with the MCP Inspector — a browser-based tool that lets you call tools directly and inspect the JSON responses. Run it with npx, point it at your server command, and you get a UI to invoke every tool and resource without a model in the loop.

npx @modelcontextprotocol/inspector node index.js
# Opens browser at localhost:6274
# Connect → Tools tab → call your tools directly

Error Handling in Tools

Return errors as content with isError: true rather than throwing. Thrown exceptions crash the tool call and produce an opaque error message. A structured error response lets Claude explain to the user what went wrong and potentially try a different approach.

server.tool('get_listing', '...', { id: z.string() }, async ({ id }) => {
  try {
    const listing = await getListingById(id);
    if (!listing) {
      return {
        content: [{ type: 'text', text: `No listing found with ID: ${id}` }],
        isError: true,
      };
    }
    return { content: [{ type: 'text', text: JSON.stringify(listing) }] };
  } catch (err) {
    return {
      content: [{ type: 'text', text: `Database error: ${err.message}` }],
      isError: true,
    };
  }
});

Calling Your MCP Server from the Claude API

The Anthropic API doesn't speak MCP natively, but you can bridge the two. Use the MCP client SDK to connect to your server, list its tools, convert the definitions to Anthropic's tool format, and pass them to the API. This gives you the best of both worlds: tools defined once in MCP, usable both from Claude Desktop and your own applications.

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import Anthropic from '@anthropic-ai/sdk';

const mcpClient = new Client({ name: 'my-app', version: '1.0.0' });
const transport = new StdioClientTransport({
  command: 'node',
  args: ['./mcp-server/index.js'],
});
await mcpClient.connect(transport);

// Discover tools from the MCP server
const { tools: mcpTools } = await mcpClient.listTools();

// Convert MCP tool definitions to Anthropic format
const anthropicTools = mcpTools.map((t) => ({
  name: t.name,
  description: t.description,
  input_schema: t.inputSchema,
}));

const anthropic = new Anthropic();
const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  tools: anthropicTools,
  messages: [{ role: 'user', content: 'Find photography services in London under £200' }],
});

// Execute any tool calls via the MCP server
if (response.stop_reason === 'tool_use') {
  for (const block of response.content.filter((b) => b.type === 'tool_use')) {
    const result = await mcpClient.callTool({ name: block.name, arguments: block.input });
    console.log(result.content);
  }
}

Further Reading

Frequently Asked Questions

What is the Model Context Protocol (MCP)?

MCP is an open protocol created by Anthropic that standardises how AI models connect to external tools and data sources. Instead of hard-coding API integrations into every AI application, MCP defines a universal client–server interface: your tool logic lives in an MCP server, and any MCP-compatible client (Claude Desktop, Claude Code, or your own app) can connect to it without custom integration work.

What can an MCP server expose?

An MCP server can expose three types of primitives: Tools (functions Claude can call, like querying a database or calling an API), Resources (readable data sources like files, database rows, or live feeds), and Prompts (reusable prompt templates with dynamic arguments). Most MCP servers focus on tools — they're the most flexible and widely supported primitive.

What transport does an MCP server use?

MCP supports two transports. stdio is the simplest — your server runs as a child process and communicates over standard input/output. Claude Desktop and Claude Code use stdio by default. Streamable HTTP is for remote servers — it uses HTTP with SSE for server-sent streaming. Use stdio for local tools and HTTP for servers you deploy and share.

How is MCP different from Claude's built-in tool use?

Claude's tool use API requires you to define tools inline in every API call and handle execution in your own code. MCP is a separate protocol layer — tools are defined once in a standalone server, and any MCP client can discover and use them without knowing their implementation. MCP tools can also be reused across Claude Desktop, Claude Code, and the API simultaneously.

Can I use MCP with the Claude API directly?

Not directly via the MCP protocol — the Anthropic API uses its own tool use format. However, you can programmatically connect to an MCP server using the MCP client SDK, list its available tools, convert them to Anthropic tool definitions, and pass them to the API. This lets you build apps that dynamically discover tools from any MCP server.

Is MCP only for Anthropic/Claude?

No. MCP is an open protocol and provider-agnostic. OpenAI, Google, and other AI providers are adding MCP support. An MCP server you build today for Claude Desktop will work with any other MCP-compatible client without changes.

How do I test an MCP server during development?

The fastest way is to add your server to Claude Desktop's config file (claude_desktop_config.json) and restart the app — the hammer icon in the UI shows connected tools. For automated testing, use the MCP Inspector (npx @modelcontextprotocol/inspector) which gives a browser UI to call your server's tools directly without a model in the loop.

What Node.js version do I need for the MCP SDK?

The @modelcontextprotocol/sdk package requires Node.js 18 or later due to its use of native fetch and the WebStreams API. Most projects on Node 20 LTS work without any compatibility concerns.

More Articles

Need help with this?

I'm available for Sharetribe Flex, Shopify, Next.js, and AI integration projects.

Get In Touch