DEV Community

Cover image for Mapping an existing API to MCP tools
Saif
Saif

Posted on

Mapping an existing API to MCP tools

Imagine your team has an internal productivity API, built over the years, that powers projects, tasks, comments, roles, notifications, and access control. It’s stable and well‑used, but it’s not yet ready for AI agents.

The challenge is to expose this system as a clean, composable set of MCP tools, not just wrappers, but schema‑driven, predictable, and reusable.

Here’s how you can do it.

1️⃣ Understand your API surface

Group endpoints into logical domains:

  • Users
  • Tasks
  • Comments
  • Roles
  • Projects

For each domain, list the operations: fetch, list, create, update, delete. This mapping leads to tool names like get_user, list_projects, create_task.

2️⃣ MCP tools represent capabilities, not routes

Each tool is a single, self‑contained capability:

name: "get_user"
description: "Retrieve a user by ID"
inputSchema: { userId: string }
outputSchema: { name: string; email: string }
handler: async ({ userId }) => { /* ... */ }
Enter fullscreen mode Exit fullscreen mode

Flatten all request inputs into inputSchema and define predictable JSON outputs. Remove HTTP status codes from the interface.

3️⃣ Transform OpenAPI into Agent‑Friendly Schemas

From OpenAPI:

GET /users/{id}
- path param: id: string
- 200 response: { name, email }
Enter fullscreen mode Exit fullscreen mode

To MCP tool:

name: "get_user",
inputSchema: { userId: z.string() },
outputSchema: { name: z.string(), email: z.string() }
Enter fullscreen mode Exit fullscreen mode

This makes tools self‑documenting and composable.

4️⃣ Handle real‑world patterns

We applied consistent schemas for:

  • CRUD (get_user, create_task)
  • Filtering/Search (search_tasks)
  • Batch Operations (create_tasks_batch)
  • File Uploads/Downloads (signed URLs)

Example filter:

inputSchema: {
  status: z.enum(["open", "in_progress", "closed"]),
  assignedTo: z.string().optional()
}
Enter fullscreen mode Exit fullscreen mode

5️⃣ Keep tools predictable and reusable

Stick to conventions (get_, list_, create_, update_, delete_), share object schemas, and avoid mixing unrelated actions in a single tool.

6️⃣ Clean, testable implementation

Modular structure:

src/
  tools/
    user-tools.ts
    task-tools.ts
  schemas/
    shared.ts
  server.ts
Enter fullscreen mode Exit fullscreen mode

Example:

export const getUserTool: MCPTool = {
  name: "get_user",
  description: "Retrieve a user by ID",
  inputSchema: z.object({ userId: z.string() }),
  outputSchema: UserSchema,
  handler: async ({ userId }) => {
    const user = getUserById(userId);
    if (!user) throw { code: "NOT_FOUND", message: "User not found" };
    return user;
  }
};
Enter fullscreen mode Exit fullscreen mode

7️⃣ Test with a mock backend

With mockDB:

it("get_user: throws on missing user", async () => {
  await expect(getUserTool.handler({ userId: "nope" }))
    .rejects.toMatchObject({ code: "NOT_FOUND" });
});
Enter fullscreen mode Exit fullscreen mode

Result: A predictable, composable MCP tool layer with structured errors and maintainable code, ready for AI agents to use reliably.

📖 Read the deep dive

Your turn. Let me know your experiences about mapping API to MCP in the comments section below.

Top comments (0)