AI News Hub Logo

AI News Hub

How to Build Your First Custom MCP Server in Under an Hour

DEV Community
Nex Tools

The Model Context Protocol explained practically — with a real working example you can deploy today Most Claude Code tutorials tell you to install MCP servers. This one shows you how to build one. Because once you understand that MCP is just a JSON-RPC server with a specific protocol, the entire ecosystem opens up. You can connect Claude Code to any internal tool, any database, any API — not just the pre-built connectors. Let me walk you through building a real MCP server from scratch. MCP (Model Context Protocol) is Anthropic's open standard for connecting AI assistants to external tools and data sources. In practice: it's a server that Claude Code can call. The server exposes "tools" that Claude can invoke. Claude decides when and how to use them based on the conversation. The protocol itself is simple: Server starts and announces its tools Claude sends tool calls as JSON-RPC requests Server executes and returns results Claude uses the results to respond That's genuinely all there is to it. Want to use pre-built MCP servers and AI tools without coding? Check out NEXTOOLS — ready-to-use AI tools for builders. A simple MCP server that gives Claude Code access to your project's TODO list. You'll be able to: Ask Claude "what's left on my todo list?" Have Claude add new tasks automatically Mark tasks as complete through conversation Not fancy. But genuinely useful, and the pattern scales to anything. Node.js 18+ Claude Code installed 30-60 minutes mkdir my-todo-mcp cd my-todo-mcp npm init -y npm install @modelcontextprotocol/sdk The MCP SDK handles the protocol layer. You write the business logic. Create index.js: import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { readFileSync, writeFileSync, existsSync } from "fs"; const TODO_FILE = "./todos.json"; function loadTodos() { if (!existsSync(TODO_FILE)) return []; return JSON.parse(readFileSync(TODO_FILE, "utf-8")); } function saveTodos(todos) { writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2)); } const server = new Server( { name: "todo-mcp", version: "1.0.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_todos", description: "List all TODO items", inputSchema: { type: "object", properties: {} }, }, { name: "add_todo", description: "Add a new TODO item", inputSchema: { type: "object", properties: { text: { type: "string", description: "The TODO item text" }, }, required: ["text"], }, }, { name: "complete_todo", description: "Mark a TODO item as complete by index", inputSchema: { type: "object", properties: { index: { type: "number", description: "0-based index of the item" }, }, required: ["index"], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const todos = loadTodos(); if (name === "list_todos") { if (todos.length === 0) { return { content: [{ type: "text", text: "No todos yet." }] }; } const text = todos .map((t, i) => `${i + 1}. [${t.done ? "x" : " "}] ${t.text}`) .join("\n"); return { content: [{ type: "text", text }] }; } if (name === "add_todo") { todos.push({ text: args.text, done: false }); saveTodos(todos); return { content: [{ type: "text", text: `Added: ${args.text}` }] }; } if (name === "complete_todo") { if (args.index = todos.length) { return { content: [{ type: "text", text: "Invalid index." }] }; } todos[args.index].done = true; saveTodos(todos); return { content: [{ type: "text", text: `Completed: ${todos[args.index].text}` }], }; } throw new Error(`Unknown tool: ${name}`); }); const transport = new StdioServerTransport(); await server.connect(transport); Add to your ~/.claude/claude_desktop_config.json (or the equivalent for your setup): { "mcpServers": { "todo": { "command": "node", "args": ["/absolute/path/to/my-todo-mcp/index.js"], "type": "stdio" } } } Restart Claude Code. The server loads automatically. Ready to see how MCP can transform your workflow? Explore NEXTOOLS — built with MCP integrations. Now in Claude Code: You: "Add a todo: fix the authentication bug" Claude: [calls add_todo] Added: fix the authentication bug You: "What's on my list?" Claude: [calls list_todos] 1. [ ] fix the authentication bug 2. [ ] update the README 3. [x] deploy to staging You: "Mark item 1 as done" Claude: [calls complete_todo with index 0] Completed: fix the authentication bug It just works. Claude decides when to use the tools based on context. Once you understand this pattern, the sky is the limit: Internal database queries: // Tool: query_customers // Returns: customer data from your PostgreSQL Slack notifications: // Tool: send_slack_message // Triggers: webhook to your team channel Custom analytics: // Tool: get_conversion_rate // Returns: live data from your analytics database File operations: // Tool: read_env_var // Returns: specific env values without exposing the whole .env Any tool you build becomes a natural language interface. "Get me the conversion rate for this week" just works. A few things to add before real use: Error handling: try { // your tool logic } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true, }; } Input validation: if (!args.text || typeof args.text !== "string") { return { content: [{ type: "text", text: "text is required" }] }; } Logging: process.stderr.write(`Tool called: ${name}\n`); Note: always log to stderr, not stdout. The MCP protocol uses stdout for JSON-RPC messages. 1. Logging to stdout stderr for logs. 2. Async without await await causes silent failures. 3. Relative paths in config 4. Not restarting after changes Now that you have the pattern, here's a useful progression: File reader - Give Claude access to specific directories without it being able to touch others Git wrapper - Let Claude query your git log in structured ways API proxy - Wrap internal APIs with simplified interfaces Database reader - Read-only access to production data with guardrails Each one takes about an hour to build. Each one becomes a permanent part of your Claude Code setup. MCP is why Claude Code is different from every other AI coding tool. Other tools: you prompt the AI, hope it knows the right answer, fix its mistakes. Claude Code with MCP: Claude has real tools, real data, real capabilities. It's not guessing — it's executing. The developers who figure this out early are going to have a serious edge. Want to see more AI dev tools and workflows? Follow along at NEXTOOLS. I write about building real things with Claude Code. Follow for weekly posts on AI-powered development workflows. Code from this article is available at NEXTOOLS.