{"slug": "build-your-first-mcp-server-in-30-minutes", "title": "Build Your First MCP Server in 30 Minutes", "summary": "A developer built an MCP (Model Context Protocol) server in 30 minutes using TypeScript and the official SDK. The server allows AI agents like Claude to connect and query external tools via a stateless HTTP transport. The project demonstrates how to register custom tools, such as a customer lookup function, with Zod schema validation.", "body_md": "MCP (Model Context Protocol) is an open standard that lets AI agents connect to external tools and data sources. Instead of building a custom integration for every AI client, you build one MCP server, and any compatible client (Claude, Cursor, Windsurf, custom agents) can plug into it.\n\nThis guide walks you through building an MCP server from scratch using TypeScript and the official SDK. By the end, you'll have a working server that Claude can connect to and query.\n\n**Prerequisites:**\n\nCreate a new project and install the MCP SDK:\n\n```\nmkdir my-mcp-server && cd my-mcp-server\nnpm init -y\nnpm install @modelcontextprotocol/sdk express zod\nnpm install -D typescript @types/express @types/node\nnpx tsc --init\n```\n\nUpdate your `tsconfig.json`\n\nwith these essentials:\n\n```\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"Node16\",\n    \"moduleResolution\": \"Node16\",\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true\n  }\n}\n```\n\nYour project structure will look like this:\n\n```\nmy-mcp-server/\n├── src/\n│   ├── server.ts          ← entry point\n│   └── tools/\n│       └── customers.ts   ← your first tool\n├── tsconfig.json\n└── package.json\n```\n\nThe MCP server is an Express app with the SDK wired into it. The SDK handles protocol negotiation, JSON-RPC framing, and tool discovery. You just register your tools and pick a transport.\n\n``` python\n// src/server.ts\nimport express from 'express'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport {\n  StreamableHTTPServerTransport\n} from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { registerTools } from './tools/customers.js'\n\nconst app = express()\napp.use(express.json())\n\n// Health check\napp.get('/health', (req, res) => res.sendStatus(200))\n\n// MCP endpoint\napp.post('/mcp', async (req, res) => {\n  // Create a fresh server + transport per request (stateless)\n  const server = new McpServer({\n    name: 'my-mcp-server',\n    version: '1.0.0',\n  })\n\n  // Register all tools on this server instance\n  registerTools(server)\n\n  const transport = new StreamableHTTPServerTransport({\n    sessionIdGenerator: undefined,  // stateless\n    enableJsonResponse: true,\n  })\n\n  await server.connect(transport)\n  await transport.handleRequest(req, res, req.body)\n\n  res.on('finish', () => {\n    transport.close()\n    server.close()\n  })\n})\n\n// Reject session-based requests (we're stateless)\napp.get('/mcp', (req, res) => res.sendStatus(405))\napp.delete('/mcp', (req, res) => res.sendStatus(405))\n\nconst PORT = process.env.PORT || 4006\napp.listen(PORT, () => {\n  console.log(`MCP server running on port ${PORT}`)\n})\n```\n\nWhy stateless?By creating a fresh server per request with no session tracking, any instance behind a load balancer can handle any request. No sticky sessions, no shared session store. Start here and only add sessions if you need server-initiated push.\n\nA tool is a function the AI can call. It has a name, a description (which the AI reads to decide when to call it), an input schema (validated with Zod), and a handler.\n\nLet's build a simple customer lookup tool:\n\n``` python\n// src/tools/customers.ts\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\n\n// Sample data — replace with your database\nconst customers = [\n  { id: '1', name: 'Alice Chen',   company: 'Acme Corp',  plan: 'enterprise' },\n  { id: '2', name: 'Bob Rivera',   company: 'StartupXYZ', plan: 'starter'    },\n  { id: '3', name: 'Carol Zhang',  company: 'BigCo Inc',  plan: 'pro'        },\n  { id: '4', name: 'David Park',   company: 'TechFlow',   plan: 'enterprise' },\n]\n\nexport function registerTools(server: McpServer) {\n\n  // Tool 1: Search customers by name\n  server.tool(\n    'searchCustomers',\n    'Search customers by name. Returns matching customers '\n    + 'with their company and plan.',\n    {\n      query: z.string().describe('Name or partial name to search'),\n    },\n    async ({ query }) => {\n      const results = customers.filter(c =>\n        c.name.toLowerCase().includes(query.toLowerCase())\n      )\n\n      if (!results.length) {\n        return { content: [{ type: 'text', text: 'No customers found.' }] }\n      }\n\n      const table = results\n        .map(c => `| ${c.name} | ${c.company} | ${c.plan} |`)\n        .join('\\n')\n\n      return {\n        content: [{\n          type: 'text',\n          text: `| Name | Company | Plan |\\n|---|---|---|\\n${table}`,\n        }],\n      }\n    }\n  )\n\n  // Tool 2: Get customer by ID\n  server.tool(\n    'getCustomer',\n    'Get detailed information about a specific customer by ID.',\n    {\n      customerId: z.string().describe('The customer ID'),\n    },\n    async ({ customerId }) => {\n      const customer = customers.find(c => c.id === customerId)\n\n      if (!customer) {\n        return { content: [{ type: 'text', text: 'Customer not found.' }] }\n      }\n\n      return {\n        content: [{\n          type: 'text',\n          text: [\n            `**${customer.name}**`,\n            `Company: ${customer.company}`,\n            `Plan: ${customer.plan}`,\n          ].join('\\n'),\n        }],\n      }\n    }\n  )\n}\n```\n\nA few things to notice:\n\n`content`\n\narray with text blocks. Markdown formatting works well because the AI renders it for the user.Add a build and start script to `package.json`\n\n:\n\n```\n{\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"start\": \"node dist/server.js\",\n    \"dev\": \"tsc --watch & node --watch dist/server.js\"\n  }\n}\nnpm run build && npm start\n# MCP server running on port 4006\n```\n\nTest it with a quick curl:\n\n```\ncurl -X POST http://localhost:4006/mcp \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\n      \"jsonrpc\": \"2.0\",\n      \"id\": 1,\n      \"method\": \"initialize\",\n      \"params\": {\n        \"protocolVersion\": \"2025-03-26\",\n        \"capabilities\": {},\n        \"clientInfo\": { \"name\": \"test\", \"version\": \"1.0\" }\n      }\n    }'\n```\n\nTo connect your server to Claude Desktop, add it to your Claude configuration file:\n\n```\n// ~/.claude/claude_desktop_config.json\n{\n  \"mcpServers\": {\n    \"my-server\": {\n      \"url\": \"http://localhost:4006/mcp\"\n    }\n  }\n}\n```\n\nRestart Claude Desktop. You should see your tools listed in the tools menu. Try asking Claude: *\"Search for a customer named Alice\"* and watch it call your `searchCustomers`\n\ntool.\n\nFor Claude Code (CLI), add it to your project's `.mcp.json`\n\n:\n\n```\n{\n  \"mcpServers\": {\n    \"my-server\": {\n      \"type\": \"url\",\n      \"url\": \"http://localhost:4006/mcp\"\n    }\n  }\n}\n```\n\nYou now have the pattern. To add more tools, create new files in `src/tools/`\n\nand register them on the server. Some guidelines from building production MCP servers:\n\nThe AI picks tools based on the description. A vague description like *\"Get data\"* will confuse the model. Be specific:\n\n```\n// Bad — the AI doesn't know when to use this\n'Get data from the system'\n\n// Good — clear about what, when, and what it returns\n'Search customers by name. Returns matching customers '\n+ 'with their company name and subscription plan. '\n+ 'Use this when the user asks about a specific customer '\n+ 'or wants to find customers by name.'\n```\n\nIt's tempting to create many small, focused tools. In practice, the AI is better at using fewer, broader tools. Start with one tool per entity (e.g. `getCustomer`\n\nreturns everything about a customer) and only split when responses are too large for the context window.\n\nMarkdown tables, bold text, and lists render well in AI clients. Structure your responses so the AI can present them clearly to the user. Include enough context that the AI doesn't need to call another tool to make sense of the data.\n\nThe server above has no auth. Anyone who can reach it can call your tools. For local development that's fine. For anything else, you need authentication.\n\nMCP uses OAuth 2.0. The SDK provides middleware for the token exchange. You provide the identity verification and consent flow.\n\n``` js\n// src/auth.ts\nimport {\n  mcpAuthRouter,\n  requireBearerAuth,\n} from '@modelcontextprotocol/sdk/server/auth/router.js'\n\n// The auth router handles /register, /authorize, /token\n// You provide the OAuthServerProvider implementation\napp.use(mcpAuthRouter({\n  provider: yourOAuthProvider,\n  issuerUrl: new URL('https://your-server.com'),\n}))\n\n// Protect MCP endpoints with bearer auth\napp.use(\n  '/mcp',\n  requireBearerAuth({ verifier: yourOAuthProvider })\n)\n```\n\nThe `OAuthServerProvider`\n\ninterface requires you to implement:\n\n`authorize()`\n\n`challengeForAuthorization()`\n\n`exchangeAuthorizationCode()`\n\n`exchangeRefreshToken()`\n\n`verifyAccessToken()`\n\nTip:Store OAuth state (auth codes, refresh tokens, client registrations) in Redis, not in memory. In-memory state doesn't survive restarts and isn't shared across instances. Use atomic GET-and-DELETE for code exchange to prevent replay attacks.\n\nBefore deploying, walk through these:\n\n`/register`\n\n, `/authorize`\n\n, and `/token`\n\nendpoints are public. Without rate limiting, they're open to abuse. A fixed-window limiter in Redis works well. Decide what happens when Redis is down (fail open? in-memory fallback?).This guide gets you to a working server. Taking it to production involves more decisions: two-token auth patterns, stateless transport tradeoffs, rate limiter fallback strategies, prompt injection protection, and multi-tenant security.\n\nI wrote a detailed post covering all of these based on building a production MCP server for a multi-tenant SaaS product:\n\n👉 [Building a Production MCP Server: Auth, Transport, and the Hard Parts](https://dev.to/kalpesh_parihar/building-a-production-mcp-server-auth-transport-and-the-hard-parts-3aph)\n\n*If you found this useful, follow me for more posts on building AI-powered products in production.*", "url": "https://wpnews.pro/news/build-your-first-mcp-server-in-30-minutes", "canonical_source": "https://dev.to/kalpesh_parihar/build-your-first-mcp-server-in-30-minutes-2o3o", "published_at": "2026-06-29 10:22:59+00:00", "updated_at": "2026-06-29 10:27:17.537134+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "ai-agents"], "entities": ["MCP", "Model Context Protocol", "Claude", "Cursor", "Windsurf", "TypeScript", "Express", "Zod"], "alternates": {"html": "https://wpnews.pro/news/build-your-first-mcp-server-in-30-minutes", "markdown": "https://wpnews.pro/news/build-your-first-mcp-server-in-30-minutes.md", "text": "https://wpnews.pro/news/build-your-first-mcp-server-in-30-minutes.txt", "jsonld": "https://wpnews.pro/news/build-your-first-mcp-server-in-30-minutes.jsonld"}}