Build Your Own MCP Server: Give Claude Access to Any API
MCP (Model Context Protocol) is like a USB interface for AI: a standard connector through which Claude plugs into any tool. We break down how to design and run your own MCP server — from choosing what tools to expose to making the first call from Claude Desktop.
IntermediateMCP30 minTypeScript, MCP SDK, Claude Desktop
1
When MCP, and when just regular code?
You want to give Claude access to your CRM. Without MCP, you have three paths: data in the prompt (limited by context), middleware service (maintain forever), or hardcoded API calls (tight coupling). MCP solves this differently — a universal protocol, like USB-C: one connector for all devices.
Simple rule: Claude decides which tool to call autonomously — use MCP. Fixed chain of calls — regular code or n8n. The core value is reuse: write a server for Jira once, and Claude Desktop, Cursor, any MCP client can connect to it.
When MCP, when not?
Claude chooses the tool and call order autonomously
Need to reuse API access across different clients
Linear pipeline with fixed sequence
One-off script for a specific task
MCP isn't a replacement for Function Calling. Function Calling — you control the logic in code. MCP — Claude decides what to call. Different levels of autonomy.
2
Narrow tools beat broad ones — every time
A tool called `manage_database(action, params)` is a button labeled 'make it good'. Claude doesn't know what will happen and calls it unpredictably. But `query_users`, `update_order`, `get_product` — three clear buttons that Claude combines on its own.
Answer three questions before writing code: what should Claude be able to do, what doesn't it need, and where's the safety boundary. Start with read-only tools — Claude will experiment, and it's better if experiments don't cost you database rows.
❌ Broad tool
- manage_database(action, params)
- Claude doesn't know what will happen
- Errors are hard to debug
✅ Narrow tools
- query_users, update_order, get_product
- Claude combines them itself
- Each tested independently
Write the tool description before the code. If you can't explain in two sentences when Claude should call it — the tool boundary is still fuzzy.
3
Three server layers — and only one is yours
An MCP server isn't a web server in the traditional sense. It's a translator process: on one side Claude with its requests, on the other your API or database. The translator receives a request, makes the appropriate call, returns the result in a format Claude understands.
Transport (stdio or HTTP+SSE) — always start with stdio: no network issues, no auth, easier to debug. Protocol (JSON-RPC 2.0) — the SDK completely hides this detail. Your business logic — the only layer where you make decisions. Everything else is boilerplate the SDK generates for you.
Your business logic
What the tool actually does
Protocol (JSON-RPC 2.0)
Handled by the SDK
Transport (stdio / HTTP+SSE)
Start with stdio — simpler
MCP-сервер = три слоя:
Бизнес-логика: ваш код (единственное, что вы пишете)
Протокол: JSON-RPC 2.0 (SDK обрабатывает)
Транспорт: stdio (локально) или HTTP+SSE (удалённо)
Инструмент = имя + схема входа + обработчик
get_order({ orderId }) → данные заказа из БД
Начинайте с stdio — проще отлаживать4
A tool description is a prompt. Write it like one
When Claude chooses a tool, it reads three things: the name, description, and parameter schema. That's the prompt — just in JSON Schema format. The quality of these three elements determines whether Claude calls the tool correctly, too often, too rarely, or with wrong parameters.
Name = verb + object: `get_customer_orders`, `search_knowledge_base`. Not `tool1`, not `handleRequest`. Claude uses the name as its first hint when picking a tool, so it should be self-evident.
Description = a mini-instruction for Claude. Include: what the tool does, when to call it, what it returns. Separately: when NOT to call it. Feels redundant, but negative instructions are exactly what prevent false calls.
Parameter schema — add descriptions to every field. `customerId` with no description is a mystery. `customerId` with 'UUID from CRM, not email or name' is a concrete instruction. Claude will pass exactly what you expect. Principle: if an intern could understand the tool by reading only the schema — it's good.
Инструмент: search_orders
Описание: "Поиск заказов по статусу или клиенту.
Вызывать: когда спрашивают о заказах, доставке.
НЕ вызывать: для создания/изменения заказов."
Параметры:
customerId — "UUID из CRM, не email и не имя"
status — pending | shipped | delivered
limit — макс 20, по умолчанию 10Include in the tool description when NOT to call it. Sounds odd, but negative instructions reduce false calls dramatically.
5
Errors are prompts for Claude, not logs for you
Imagine Claude is a delivery driver you gave an address to. GPS says 'street not found' — one situation (clarify). 'Road closed' — another (go around). 'GPS unavailable' — a third (wait). Your job is to give Claude exactly these signals, not just 'something went wrong'.
In a regular API, an error goes to the developer as HTTP 500. In MCP, the error goes to Claude, and it needs to do something with it: retry, explain to the user, or choose a different tool. Two types: process exceptions (server crashed — Claude is helpless) and content errors (`isError: true` with a description — Claude reads and reacts). For business errors, always use the second. Never throw exceptions from a handler — it kills the entire MCP server, not just one call.
What to write in the error? Three elements: what happened, why, what to try next. Not 'Internal error' but 'Order not found. The orderId may be from another system. Try search_orders by customer name.' Claude will literally read this and adjust its next step.
Test errors first. Call the tool with a nonexistent ID, an empty string, invalid parameters — and verify Claude gets helpful hints, not stack traces.
6
Connected — now iterate on descriptions, not code
Claude Desktop is the ideal environment for testing your MCP server because it's an X-ray: you see not just results, but how Claude reasoned, which tool it picked, with what parameters. Adding the server means a config file with the path to your compiled JS, restart the app, tools appear.
But more important than setup is testing methodology. Ask Claude to perform a task requiring a chain of calls: 'Find orders from customer Ivanov in the last week and tell me which one is in delivery.' This tests both individual tools and their combination — exactly what you built the MCP server for.
Iteration is the most valuable part of the process. After the first test you'll discover: Claude calls with wrong parameters (description problem), calls when it shouldn't (description too broad), doesn't call when it should (name not obvious). Each signal is a fix to the tool description, not the logic. Iterating on descriptions gives more than refactoring code.
Use MCP Inspector (`npx @modelcontextprotocol/inspector`) before Claude Desktop — it shows your tools and lets you call them manually, like Postman for MCP. Saves 80% of debugging time.
Result
A working MCP server connected to Claude Desktop. Claude calls your tools autonomously — because descriptions are written as prompts, not documentation.