Prompt
How do I build an MCP server from an existing REST API?
Latest observation
To build an MCP server from an existing REST API, you’re basically creating a thin adapter that exposes your REST endpoints as MCP tools/resources/prompts.
High-level approach
-
Inventory the REST API
- List endpoints, methods, request/response schemas, auth requirements, rate limits.
- Decide which endpoints are actually useful to an LLM agent.
- Prefer a small set of high-value operations over mirroring everything.
-
Map REST concepts to MCP concepts
- Tools → actions like
create_ticket,search_users,send_message - Resources → readable data like
customer://123,ticket://456 - Prompts → reusable workflows or templates for common tasks
- Tools → actions like
-
Implement an MCP server
- Use an MCP SDK in your language of choice.
- For each REST endpoint you expose:
- define the tool schema
- validate inputs
- call the REST API
- normalize the response
- return concise, structured results
-
Handle auth and secrets
- Support API keys/OAuth tokens server-side.
- Never expose secrets directly to the model.
- If user-specific auth is needed, pass tokens securely from the client or server session.
-
Add robust error handling
- Convert REST errors into MCP-friendly messages.
- Include enough context for the model to recover, but don’t dump huge raw responses.
-
Test with an MCP client
- Verify tool discovery, input validation, timeouts, pagination, and failures.
- Use real agent-style workflows, not just unit tests.
Design choices that matter
1) Don’t wrap every endpoint
If your REST API has 100 endpoints, the MCP server shouldn’t necessarily expose 100 tools. Pick the ones that an LLM can use reliably.
Good candidates:
- search/list/get endpoints
- create/update actions with clear schemas
- read-only resource lookups
- workflow-oriented actions
Bad candidates:
- highly transactional or ambiguous endpoints
- endpoints with complex nested payloads unless you can simplify them
- anything requiring lots of chained low-level calls unless you wrap them into one higher-level tool
2) Prefer high-level tools
Instead of exposing:
POST /v1/tasksPATCH /v1/tasks/{id}POST /v1/comments
You might expose:
create_taskupdate_taskadd_task_commentsearch_tasks
This makes the model more effective.
3) Normalize responses
Return structured, predictable output, for example:
{
"id": "task_123",
"title": "Fix billing bug",
"status": "open"
}
Avoid returning huge raw API payloads unless the caller explicitly needs them.
Example architecture
REST API
GET /users?query=aliceGET /users/{id}POST /usersGET /tickets?status=openPOST /tickets
MCP server
- Tool:
search_users - Tool:
get_user - Tool:
create_user - Tool:
list_tickets - Tool:
create_ticket - Resource:
user://{id} - Resource:
ticket://{id}
Example implementation outline
Below is a conceptual Node.js/TypeScript flow.
1) Define a tool
server.tool(
"search_users",
{
query: z.string().min(1)
},
async ({ query }) => {
const res = await fetch(`${ პროცეს.env.API_BASE_URL}/users?query=${encodeURIComponent(query)}`, {
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`
}
});
if (!res.ok) {
return {
content: [{ type: "text", text: `API error: ${res.status} ${res.statusText}` }]
};
}
const data = await res.json();
return {
content: [
{
type: "text",
text: JSON.stringify(
data.map((u: any) => ({ id: u.id, name: u.name, email: u.email })),
null,
2
)
}
]
};
}
);
2) Add another tool
server.tool(
"create_ticket",
{
title: z.string(),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high"]).default("medium")
},
async ({ title, description, priority }) => {
const res = await fetch(`${process.env.API_BASE_URL}/tickets`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.API_TOKEN}`
},
body: JSON.stringify({ title, description, priority })
});
if (!res.ok) {
const errText = await res.text();
return {
content: [{ type: "text", text: `Failed to create ticket: ${errText}` }]
};
}
const ticket = await res.json();
return {
content: [
{
type: "text",
text: `Created ticket ${ticket.id}: ${ticket.title}`
},
{
type: "json",
json: ticket
}
]
};
}
);
Best practices
Input validation
Use a schema validator (Zod, JSON Schema, etc.) for every tool.
Pagination
If the REST API paginates, expose page size/offset or implement sensible defaults.
Rate limiting and retries
- Respect upstream API limits
- Add retries only for transient failures
- Don’t retry unsafe POSTs unless you have idempotency keys
Timeouts
Keep tool execution bounded. Agents should fail fast rather than hang.
Security
- Filter sensitive fields from outputs
- Use least-privilege API credentials
- Sanitize any user-provided text used in URLs or logs
Observability
Log:
- tool name
- duration
- upstream status code
- correlation IDs
Avoid logging secrets or full sensitive payloads.
When to add resources vs tools
Use resources when the data is naturally read-only and addressable:
customer://123invoice://2024-001
Use tools when it performs an action or query:
search_customersrefund_invoicecreate_invoice
A practical workflow
- Start with 3–5 essential endpoints.
- Make them tools with clean schemas.
- Add resources for common object retrieval.
- Use one or two prompts for common workflows.
- Test with real user tasks.
- Expand only if the agent needs more capability.
If you want, I can help you with either of these:
- a language-specific MCP server template (Node/Python)
- a mapping strategy for your specific REST API endpoints
- a production checklist for auth, pagination, and error handling
If you share your API docs or a sample OpenAPI spec, I can sketch the exact MCP tools/resources for it.
Brands