MCP Server Tools
- Applies to
- Gatewayruntime for API, AI, and MCP gateways
- Hosting
- Managed EdgeManaged DedicatedSelf-Hosted
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:
Code
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 totool) - Set to"tool"to denote this operation is an MCP tool.name(string: optional) - The identifier for the MCP tool. Defaults to the operation'soperationId. If theoperationIdis not set, falls back to an auto-generated name.description(string: optional) - Description of what the tool does. Falls back to the operation'sdescriptionorsummary. If the route'sdescriptionorsummaryfields are not set, falls back to an auto-generated description. The fallback uses at most the first 350 characters of the operation'sdescription, followed by.... Setdescriptionhere when the tool needs a longer description: this field has no length limit.enabled(boolean: optional) - Whether this tool is enabled. Defaults totrue.includeOutputSchema(boolean: optional) - Whether to advertise the route's successful (2xx) response schema as the tool'soutputSchema. Overrides the handler'sincludeOutputSchemaoption for this tool.includeStructuredContent(boolean: optional) - Whether to return the response JSON asstructuredContent. Overrides the handler'sincludeStructuredContentoption for this tool. Turns on automatically whenincludeOutputSchemaistrue.toolInputStyle(string: optional) -"flat"or"nested". How this tool advertises the operation's inputs. Overrides the handler'stoolInputStyleoption 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:
Code
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:pathParams,queryParams,body, orheaders.flat- The operation's path, query, and header parameters and the request body's properties all become top-level tool arguments.
Some models, particularly less capable ones, send a body's fields at the top
level even when the tool advertises them under body. Against a nested schema,
that first call fails validation and the model retries with the wrapper, which
costs a round trip on every call. Flat input removes the wrapper. MCP doesn't
require either shape, and capable models generally follow whatever schema a tool
advertises, so flat is opt-in.
Set toolInputStyle in the
handler options to choose the style
for every tool on the server:
Code
To give one tool a different style, see
Set the input style per tool. The default is
nested on every compatibility date.
For example, POST /stores/{storeId}/search declares a path parameter
storeId, an optional query parameter limit, a required header x-tenant,
and a JSON body with a required query and an optional sort. The two styles
advertise these schemas (each property's schema shortened):
nested
flat
A flat call passes every argument at the top level:
Code
The gateway routes each argument back to the channel that declared it, so the
route receives POST /stores/s1/search?limit=10 with the header
x-tenant: acme and the body {"query":"hoodie"}. That's the same request the
equivalent nested call produces.
For the full mapping rules, every case that keeps a tool nested, and the log and build messages, see Flat tool input.
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 as an invalid-arguments error. - 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:
- Two channels declare the same name. A
limitquery parameter alongside alimitbody field, for example. 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
oneOforanyOfhas no property names to hoist. Neither does one that constrains the object as a whole, such as withminPropertiesorpatternProperties.
From
compatibility date 2026-11-06,
a tool whose toolInputStyle is flat and that can't be fully flattened fails
the build. The error names the tool, the operation, and the cause. Change the
schema so it flattens, or set "toolInputStyle": "nested" on that tool. A root
allOf of plain object schemas and a dependentRequired that names only body
properties also flatten from this date.
On earlier compatibility dates, those tools keep part or all of the nested shape
even when toolInputStyle is flat, and the MCP server logs the reason when it
registers the tool. With a name collision, the whole schema stays nested and the
server logs a warning. With a body that isn't a plain object, the parameters
still flatten and the body keeps its own body argument. Enable
debugMode to see which tools this
applies to.
See Flat tool input for the full list of body shapes and the exact build errors.
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:
Code
Send flat arguments in new clients — that's what a flat tool advertises. A call that mixes the two forms is read as flat.
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:
Code
Each tool uses the first value it finds:
- The tool's own
toolInputStyle. - The handler's
toolInputStyleoption. 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. On the handler, any other value is read as
nested without an error.
If your MCP server still uses the deprecated files option, set
toolInputStyle in the operation's x-zuplo-mcp-tool extension instead. The
MCP server checks that value when it registers the tool and throws an error that
names the operation:
Code
From compatibility date
2026-11-06,
toolInputStyle must be a literal value on the handler and on every tool. A
$env(...) value fails the build.
Parameter serialization
From compatibility date
2026-11-06, the MCP
Server handler writes path, query, and header arguments into the request using
each parameter's OpenAPI style, explode, allowReserved, and content.
Before that date it uses one fixed encoding per location, so a route that
declares a style such as deepObject, pipeDelimited, label, or matrix may
receive a value it can't parse.
Code
Tool names follow the same rule set from that date: names that don't match the MCP tool naming rule are rewritten. See Build warning: tool name doesn't follow the MCP tool naming rule.
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:
| Value | Client request | Internal tool invocation |
|---|---|---|
| Method and path | POST /mcp, or your configured MCP path | The API route's method and path, such as GET /weather/current?location=Boston |
| Body | The MCP JSON-RPC envelope | The tool arguments mapped to the API request body, if the route uses one |
context.route | The MCP route | The tool's API route |
context.parentContext | undefined | The 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:
Code
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:
Code
Response:
Code
Call a Tool
Use the MCP tools/call method to execute a tool:
Code
Response:
Code
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_userorsearch_productsare much better thanop1orendpoint.
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.
Code
Code
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.