MCP Server

MCP Server Tools

The MCP (Model Context Protocol) Server handler supports tools, enabling you to expose your API routes as executable functions that AI clients can call to perform actions or retrieve data.

Tools are the core building block of MCP servers, allowing AI systems to interact with your services and discover capabilities through your Zuplo gateway.

Overview

Zuplo's MCP tools work by automatically transforming your API routes into MCP tool definitions. When an AI client calls a tool, the MCP server invokes the corresponding route handler in your gateway.

This means any existing API route can be instantly turned into an MCP tool with minimal configuration.

Configuration

Route Configuration

Configure a route in your OpenAPI doc:

JSONCode
{ "/weather/current": { "get": { "operationId": "getCurrentWeather", "summary": "Get current weather", "description": "Retrieve current weather conditions for a specified location", "parameters": [ { "name": "location", "in": "query", "required": true, "schema": { "type": "string" } } ], "x-zuplo-route": { "corsPolicy": "none", "handler": { "export": "default", "module": "$import(./modules/weather)" }, "mcp": { "type": "tool", "name": "get_current_weather", "description": "Retrieve current weather conditions for a specified location" } } } } }

To provide MCP specific metadata for the tool, use the mcp property within x-zuplo-route:

The x-zuplo-route.mcp configuration for tools supports:

  • type (string: optional, defaults to tool) - Set to "tool" to denote this operation is an MCP tool.
  • name (string: optional) - The identifier for the MCP tool. Defaults to the operation's operationId. If the operationId is not set, falls back to an auto-generated name.
  • description (string: optional) - Description of what the tool does. Falls back to the operation's description or summary. If the route's description or summary fields are not set, falls back to an auto-generated description. The fallback uses at most the first 350 characters of the operation's description, followed by .... Set description here when the tool needs a longer description: this field has no length limit.
  • enabled (boolean: optional) - Whether this tool is enabled. Defaults to true.
  • includeOutputSchema (boolean: optional) - Whether to advertise the route's successful (2xx) response schema as the tool's outputSchema. Overrides the handler's includeOutputSchema option for this tool.
  • includeStructuredContent (boolean: optional) - Whether to return the response JSON as structuredContent. Overrides the handler's includeStructuredContent option for this tool. Turns on automatically when includeOutputSchema is true.
  • toolInputStyle (string: optional) - "flat" or "nested". How this tool advertises the operation's inputs. Overrides the handler's toolInputStyle option for this tool. See Set the input style per tool.
  • annotations (object: optional) - An object containing tool annotations:
    • title (string: optional) - A human-readable title for the tool, often used by clients.
    • readOnlyHint (boolean: optional) - Hint that the tool is read-only.
    • destructiveHint (boolean: optional) - Hint that the tool has mutating side effects.
    • idempotentHint (boolean: optional) - Hint that the tool is idempotent.
    • openWorldHint (boolean: optional) - Hint that the tool operates in an open-world context of external entities (like web-search).
  • _meta (object: optional) - An object containing any arbitrary metadata.

The route handler for your tool can be any standard Zuplo request handler like the URL Forwarder or the Redirect handler or a custom function module. The route receives the request triggered by the MCP tool call within the gateway and returns a response that will be passed back through the MCP server to the AI client.

POST routes with a requestBody and a defined schema are translated into an MCP tool's parameters. When invoked, these are validated by the MCP server to ensure the tool is being correctly used by the LLM.

Other methods like GET, DELETE, etc. work in a similar fashion in order to support complex tools in the shape of your APIs.

MCP Server Handler Configuration

Add tool configuration to your MCP Server handler options using the operations array:

JSONCode
{ "paths": { "/mcp": { "post": { "x-zuplo-route": { "handler": { "export": "mcpServerHandler", "module": "$import(@zuplo/runtime)", "options": { "name": "example-mcp-server", "version": "1.0.0", "operations": [ { "file": "./config/routes.oas.json", "id": "getCurrentWeather" } ] } } } } } } }

See further details in the MCP Server Handler documentation.

Tool input schemas

Zuplo derives each tool's inputSchema from the operation's OpenAPI definition. The toolInputStyle setting decides its shape:

  • nested (default) - Each input channel the operation declares is its own object argument: body, queryParams, pathParams, or headers.
  • flat - The request body's properties and the operation's path, query, and header parameters all become top-level tool arguments. This is the shape MCP clients send on a first call.

Set toolInputStyle in the handler options to choose the style for every tool on the server. To give one tool a different style, see Set the input style per tool.

When a client calls a flat tool, the gateway routes each argument back to the channel it came from, so a body field lands in the request body and a query parameter lands in the URL.

The weather route above declares one required query parameter, so its flat tool takes one required argument:

JSONCode
{ "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"], "additionalProperties": false }

A request body works the same way — its fields are the tool's arguments. Given this operation:

JSONCode
{ "/orders": { "post": { "operationId": "createOrder", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "productId": { "type": "string" }, "quantity": { "type": "number" } }, "required": ["productId"], "additionalProperties": false } } } } } } }

A flat tool advertises the body's fields directly:

JSONCode
{ "type": "object", "properties": { "productId": { "type": "string" }, "quantity": { "type": "number" } }, "required": ["productId"], "additionalProperties": false }

And a call passes them at the top level:

JSONCode
{ "name": "createOrder", "arguments": { "productId": "sku-123", "quantity": 2 } }

Parameters declared with in: cookie aren't exposed as tool arguments.

Undeclared arguments

Your own OpenAPI schema decides whether an argument the tool doesn't declare is rejected:

  • The schema sets additionalProperties: false. The tool's flat schema is closed too, and an undeclared argument fails with JSON-RPC error -32602.
  • The schema omits additionalProperties. JSON Schema permits extras, so the tool accepts the argument and forwards it in the request body.

Set additionalProperties: false on a request body schema when you want the gateway to reject fields a model invented rather than pass them upstream.

An argument routes to a request header only when a header parameter declares that exact name. An undeclared argument never becomes an upstream header.

When arguments stay nested

Some operations can't be flattened without changing what the tool accepts. Those tools keep the nested shape even when toolInputStyle is flat, and the MCP server logs the reason when it registers the tool:

  • Two channels declare the same name. A limit query parameter alongside a limit body field, for example. The whole schema stays nested and the server logs a warning. Rename one of them to get flat arguments.
  • The request body isn't a plain object. An array, a string, or a body built from oneOf, anyOf, or allOf has no property names to hoist. Neither does one that constrains the object as a whole, through minProperties, patternProperties, or dependentRequired. The parameters still flatten and the body keeps its own body argument. Enable debugMode to see which tools this applies to.

A nested schema wraps each channel the operation declares in its own object:

JSONCode
{ "type": "object", "properties": { "body": { "type": "object", "properties": { "productId": { "type": "string" } }, "required": ["productId"], "additionalProperties": false } }, "required": ["body"], "additionalProperties": false }

The channel names are body, queryParams, pathParams, and headers. Only the channels the operation declares appear.

Calling a tool with nested arguments

Flat tools also accept the nested form, so a client written against the nested schema keeps working:

JSONCode
{ "name": "createOrder", "arguments": { "body": { "productId": "sku-123", "quantity": 2 } } }

Send flat arguments in new clients — that's what a flat tool advertises.

Prompts don't use input channels. A prompt's arguments come from its request body schema directly, so prompts/get always takes them at the top level.

Set the input style per tool

Set toolInputStyle in a route's x-zuplo-route.mcp block to give that tool a different input style from the rest of the server:

JSONCode
"x-zuplo-route": { "mcp": { "type": "tool", "toolInputStyle": "flat" } }

Each tool uses the first value it finds:

  1. The tool's own toolInputStyle.
  2. The handler's toolInputStyle option.
  3. nested.

The override works in either direction: a flat tool on a nested server, or a nested tool on a flat server. Use it to add tools with flat inputs to an existing MCP server without changing the schema its current tools advertise, and without creating a second MCP route. Flat tools still accept nested arguments, so switching one tool to flat doesn't break existing callers. Only that tool's advertised inputSchema changes.

toolInputStyle accepts only "flat" or "nested". The build rejects any other value in x-zuplo-route.mcp. The MCP server also checks the value when it registers the tool and throws an error that names the operation:

Code
MCP tool configuration error: invalid toolInputStyle "Flat" on operation "createOrder". Use "flat" or "nested".

If your MCP server still uses the deprecated files option, set toolInputStyle in the operation's x-zuplo-mcp-tool extension instead.

Track tool calls with runtime hooks

When the MCP Server handler invokes an API route for a tool, the global runtime hooks run for both the client request and the internal route invocation. Each invocation has its own request and context:

ValueClient requestInternal tool invocation
Method and pathPOST /mcp, or your configured MCP pathThe API route's method and path, such as GET /weather/current?location=Boston
BodyThe MCP JSON-RPC envelopeThe tool arguments mapped to the API request body, if the route uses one
context.routeThe MCP routeThe tool's API route
context.parentContextundefinedThe context of the MCP request that invoked the route

Path and query arguments become URL parameters, and declared header arguments become request headers. A GET tool invocation has no request body.

Use context.parentContext to distinguish client requests from internal invocations. It identifies any internal route invocation, including calls made with context.invokeRoute in your own code.

To log only the API route invocations for MCP tools, add this global request hook in modules/zuplo.runtime.ts:

TypeScriptCode
import { RuntimeExtensions, ZuploMcpSdk } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addRequestHook((request, context) => { const toolCall = new ZuploMcpSdk(context).getRawCallToolRequest(); if (context.parentContext && toolCall) { context.log.info({ toolName: toolCall.params.name, method: request.method, path: new URL(request.url).pathname, requestId: context.requestId, contextId: context.contextId, }); } return request; }); }

This logs a tool's API route invocation without also counting the outer /mcp request. To count only client requests instead, filter for context.parentContext === undefined. The SDK helper reads the current context and its immediate parent; see MCP metadata in runtime hooks for timing and nested-call details.

By default, the internal invocation shares the parent's requestId and has its own contextId. Use requestId to group related events and contextId to identify each execution. See Request/Response Hooks for hook behavior and response logging.

This extra hook execution applies when a tool invokes a Zuplo API route. Proxying to an upstream MCP server doesn't create an internal API route invocation for each tool call.

Testing MCP Tools

List Available Tools

Use the MCP tools/list method to see available tools:

TerminalCode
curl https://my-gateway.zuplo.dev/mcp \ -X POST \ -H 'accept: application/json, text/event-stream' \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/list" }'

Response:

JSONCode
{ "jsonrpc": "2.0", "id": "1", "result": { "tools": [ { "name": "get_current_weather", "description": "Retrieve current weather conditions for a specified location", "inputSchema": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"], "additionalProperties": false } } ] } }

Call a Tool

Use the MCP tools/call method to execute a tool:

TerminalCode
curl https://my-gateway.zuplo.dev/mcp \ -X POST \ -H 'accept: application/json, text/event-stream' \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "get_current_weather", "arguments": { "location": "San Francisco" } } }'

Response:

JSONCode
{ "jsonrpc": "2.0", "id": "1", "result": { "content": [ { "type": "text", "text": "{\"location\":\"San Francisco\",\"temperature\":72,\"condition\":\"Sunny\"}" } ] } }

Best Practices

Meaningful Names and Descriptions

Always set meaningful operationIds (like get_users, create_new_deployment, or update_shopping_cart) and descriptions as these help LLMs understand exactly what each tool does.

When you need to provide more meaningful descriptions or names that don't align well with the operationId, set the metadata in x-zuplo-route.mcp.

A tool that takes its description from the operation's description gets at most the first 350 characters of it, followed by .... To give a tool a longer description, set it in x-zuplo-route.mcp.description, which has no length limit.

Read more about authoring usable tools and good prompt engineering practices with Anthropic's Prompt engineering overview.

AI models rely heavily on tool descriptions to understand when and how to use a tool.

  • Be Descriptive: Explain exactly what the tool does and what inputs it expects.
  • Use Meaningful Names: Operation IDs like create_user or search_products are much better than op1 or endpoint.

Schema Design

Use descriptive and well-structured JSON schemas for your tools (in your OpenAPI requestBody and response). This is used by the server to validate MCP client inputs (that is, JSON generated by an LLM). Providing descriptive schemas ensures an MCP Client's LLM always has the appropriate context on exactly what arguments to provide to tools and can dramatically reduce invalid tool usage. This validation is done automatically.

JSONCode
// Good! Uses descriptive names and specific types with limiters and formats. { "type": "object", "required": ["userId"], "properties": { "userId": { "type": "string", "format": "uuid", "description": "Valid UUID for user ID" }, "amount": { "type": "number", "minimum": 0, "maximum": 10000, "description": "Amount in cents" } } }
JSONCode
// Bad! Confusing. What's "a"? What's "b"? An LLM won't understand this. { "type": "object", "required": ["userId"], "properties": { "a": { "type": "string" }, "b": { "type": "number" } } }

Defining clear schemas in your OpenAPI document ensures your handler always receives valid data. The MCP server uses these schemas to validate arguments provided by the AI client before your handler is ever called. Input validation is an important part of MCP, so ensure you have strong validation in your OpenAPI JSON schemas!

Keep Tool Routes Private

A tool's route is also a REST endpoint that clients can reach directly. If AI clients should reach the operation only through the MCP server, add the Internal Only policy to the route with "status": 404. See Keep REST routes private.

Custom Tools

For complex workflows that don't map 1:1 to a single API route, or require advanced logic, consider using Custom MCP Tools.

Last modified on