ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Getting Started
Concepts
API Management
AI Gateway
MCP Gateway
MCP Server
    Overview
    Getting Started
      ToolsPromptsResourcesTestingTroubleshootingGraphQLCustom ToolsOpenAI Apps SDK
      Guides
      Configuration Migration Guide
    Developer Portal
    Development
    Deploying & Source Control
    Analytics
    Observability
    Networking & Infrastructure
    Account Management
    Programming API
    Build with AI
    Zuplo CLI
    Migration Guides
    Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
    powered by Zuplo
    MCP Server

    MCP Server Custom Tools

    The MCP Server Handler supports custom tools that allow you to create sophisticated MCP (Model Context Protocol) tools using OpenAPI specifications and custom handler functions. This provides the flexibility to build complex workflows that can invoke multiple API routes, implement custom business logic, and provide rich responses to AI systems without having to sacrifice the governance and power of an OpenAPI configuration.

    Custom tools give you full programmatic control over tool behavior within an MCP Server Handler. Define tools using standard OpenAPI patterns with custom TypeScript handlers for complex multi-step workflows and custom logic.

    Key Features

    • OpenAPI Standard: Define tools using standard OpenAPI specifications
    • Custom Handlers: Implement complex logic using TypeScript functions
    • Complex Workflows: Chain multiple API calls, implement business logic, and handle complex data transformations
    • Type Safety: Built-in JSON Schema validation for LLM tool arguments and inputs
    • Runtime Integration: Access to context.invokeRoute(), logging, and other Zuplo runtime features

    Quick Start

    1. Define Your API Specification

    Create an OpenAPI specification that defines your tools:

    Code
    { "openapi": "3.1.0", "info": { "version": "0.0.1", "title": "My Calculator API", "description": "A simple calculator API with basic arithmetic operations" }, "paths": { "/add": { "post": { "summary": "Add two numbers", "description": "Adds two numbers together and returns the result", "operationId": "addNumbers", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TwoNumberOperation" } } } }, "x-zuplo-route": { "corsPolicy": "none", "handler": { "export": "default", "module": "$import(./modules/add)" }, "mcp": { "type": "tool" } } } }, "/multiply": { "post": { "summary": "Multiply two numbers", "description": "Multiplies two numbers together and returns the result", "operationId": "multiplyNumbers", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TwoNumberOperation" } } } }, "x-zuplo-route": { "corsPolicy": "none", "handler": { "export": "default", "module": "$import(./modules/multiply)" }, "mcp": { "type": "tool" } } } } }, "components": { "schemas": { "TwoNumberOperation": { "type": "object", "required": ["a", "b"], "properties": { "a": { "type": "number", "description": "First number" }, "b": { "type": "number", "description": "Second number" } }, "example": { "a": 10, "b": 5 } } } } }

    2. Create Custom Handler Functions

    Create handler modules for your tools that map to your routes:

    Code
    // modules/add.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const args = await request.json(); context.log.info(`Adding ${args.a} + ${args.b}`); return args.a + args.b; }
    Code
    // modules/multiply.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const args = await request.json(); context.log.info(`Multiplying ${args.a} * ${args.b}`); return args.a * args.b; }

    3. Configure the MCP Server Handler

    Configure the MCP Server Handler to use your OpenAPI specification:

    Code
    { "paths": { "/mcp": { "post": { "operationId": "mcp-server-handler", "x-zuplo-route": { "handler": { "export": "mcpServerHandler", "module": "$import(@zuplo/runtime)", "options": { "name": "Calculator MCP Server", "version": "0.0.0", "operations": [ { "file": "./config/calculator-api.oas.json", "id": "addNumbers" }, { "file": "./config/calculator-api.oas.json", "id": "multiplyNumbers" } ] } } } } } } }

    4. Deploy and Test

    Deploy your project and test your MCP server:

    TerminalCode
    # Test with MCP Inspector npx @modelcontextprotocol/inspector # Or test with curl curl https://your-gateway.zuplo.dev/mcp \ -X POST \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": "0", "method": "tools/list" }'

    Advanced Usage

    Complex Multi-Step Workflows

    Create sophisticated workflows that chain multiple operations:

    Code
    // modules/process-order.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const args = await request.json(); // Step 1: Validate customer const customerResp = await context.invokeRoute( `/customers/${args.customerId}`, ); if (!customerResp.ok) { throw new Error("Customer not found"); } // Step 2: Check inventory const inventoryChecks = await Promise.all( args.items.map((item: any) => context.invokeRoute( `/inventory/${item.productId}/check?quantity=${item.quantity}`, ), ), ); const unavailableItems = inventoryChecks .map((resp, i) => ({ resp, item: args.items[i] })) .filter(({ resp }) => !resp.ok) .map(({ item }) => item.productId); if (unavailableItems.length > 0) { throw new Error(`Items not available: ${unavailableItems.join(", ")}`); } // Step 3: Create order const orderResp = await context.invokeRoute("/orders", { method: "POST", body: JSON.stringify({ customerId: args.customerId, items: args.items, }), headers: { "Content-Type": "application/json" }, }); const order = await orderResp.json(); return { orderId: order.id, status: "created", total: order.total, estimatedDelivery: order.estimatedDelivery, }; }

    This utilizes context.invokeRoute to invoke various routes on a gateway. This powerful workflow lets you create composite routes and tools that call many different routes on your gateway.

    context.invokeRoute will utilize the full inbound and outbound policy pipeline but does not go back out to HTTP: requests stay within the gateway. This means that policies you set on your MCP server route will be invoked alongside policies that are associated with any calls made through context.invokeRoute.

    A corresponding OpenAPI definition makes this custom route available on your gateway.

    Code
    { "/process-order": { "post": { "summary": "Process a customer order", "description": "Process a customer order through multiple validation steps", "operationId": "processOrder", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["customerId", "items"], "properties": { "customerId": { "type": "string", "description": "Unique customer identifier" }, "items": { "type": "array", "description": "List of items to order", "items": { "type": "object", "required": ["productId", "quantity"], "properties": { "productId": { "type": "string", "description": "Product identifier" }, "quantity": { "type": "number", "description": "Number of items to order" } } } } } } } } }, "responses": { "200": { "description": "Order processed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "orderId": { "type": "string", "description": "Generated order ID" }, "status": { "type": "string", "enum": ["created", "pending", "confirmed"], "description": "Order status" }, "total": { "type": "number", "description": "Total amount" } } } } } } }, "x-zuplo-route": { "handler": { "export": "default", "module": "$import(./modules/process-order)" } } } } }

    Then, simply add this operation to an MCP server to make it available as a tool:

    Code
    { "paths": { "/mcp": { "post": { "x-zuplo-route": { "handler": { "export": "mcpServerHandler", "module": "$import(@zuplo/runtime)", "options": { "operations": [ { "file": "./config/routes.oas.json", "id": "processOrder" } ] } } } } } } }

    Error Handling

    Handle errors gracefully by throwing standard JavaScript errors. These then get caught by the MCP Server and are served back to the LLM client so it can take action:

    Code
    // modules/validate-user.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const args = await request.json(); if (args.shouldFail) { throw new Error(args.errorMessage || "Validation failed"); } return { valid: true, userId: args.userId }; }

    Accessing Request Headers

    Access the original request headers through the standard ZuploRequest object. These headers are piped through the MCP server on your gateway into your route. This means you can handle headers from MCP clients like you would on any other route:

    Code
    // modules/check-headers.ts import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const args = await request.json(); const headerValue = request.headers.get(args.headerName); if (headerValue) { return `Header '${args.headerName}': ${headerValue}`; } else { return `Header '${args.headerName}' not found`; } }

    Controlling tool results with ZuploMcpSdk

    By default, the MCP Server handler serializes the downstream response body into a single text content block and, when includeStructuredContent is enabled, derives structuredContent from it. The ZuploMcpSdk class gives you programmatic control over what the AI client receives — without changing the response your handler returns.

    The most common use case is a compact summary: replace the full serialized body in content with a short human-readable summary, while the gateway still auto-derives structuredContent from the downstream response. This reduces the token count in the model's context without losing data.

    Code
    import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime"; export default async function (request: ZuploRequest, context: ZuploContext) { const response = await context.invokeRoute("/todos"); new ZuploMcpSdk(context).setRawCallToolResult({ content: [{ type: "text", text: "Fetched the todo list" }], }); return response; }

    You can also read metadata from the incoming tools/call request using getRawCallToolRequest(), and override structuredContent and _meta independently. For the full API reference, see ZuploMcpSdk.

    Best Practices

    Tool Design

    1. Clear Operation IDs: Use descriptive, action-oriented operation IDs (addNumbers, processOrder)
    2. Detailed Descriptions: Help AI systems understand what your tool does
    3. Error Handling: Throw meaningful JavaScript errors
    4. Unique Names: Ensure operation IDs are unique across your API

    Troubleshooting

    Common Issues

    Tool not appearing in tools/list:

    • Check that the endpoint is a POST method in your OpenAPI spec
    • Verify the operation has an operationId
    • Check for validation errors in your OpenAPI specification
    • Ensure the handler module exports a default function

    Handler execution failures:

    • Use context.log.error(), context.log.warn(), context.log.info() for logging
    • Verify API routes being invoked through invokeRoute exist and are accessible
    • Test individual API calls outside the MCP context
    • Check that your handler function is properly exported as default

    Schema validation errors:

    • Ensure JSON schemas are properly defined in the OpenAPI spec
    • Check that request body schemas match the data your handler expects
    • Verify response schemas match the data your handler returns

    Debugging Tips

    1. Enable Debug Logging: Use context.log.debug() liberally and turn on debug mode in your MCP server
    2. Test Components Separately: Test API routes and business logic independently
    3. Use MCP Inspector: Interactive testing is invaluable for development
    4. Validate OpenAPI: Use tools like Swagger Editor to validate your OpenAPI specification

    Learn More

    • ZuploMcpSdk - Programmatic control over MCP tool results
    • MCP Server Handler - For simple route-to-tool mapping
    • Model Context Protocol Overview - Understanding MCP concepts
    • MCP Specification - Official protocol documentation
    Edit this page
    Last modified on August 28, 2026
    GraphQLOpenAI Apps SDK
    On this page
    • Key Features
    • Quick Start
      • 1. Define Your API Specification
      • 2. Create Custom Handler Functions
      • 3. Configure the MCP Server Handler
      • 4. Deploy and Test
    • Advanced Usage
      • Complex Multi-Step Workflows
      • Error Handling
      • Accessing Request Headers
    • Controlling tool results with ZuploMcpSdk
    • Best Practices
      • Tool Design
    • Troubleshooting
      • Common Issues
      • Debugging Tips
    • Learn More
    JSON
    TypeScript
    TypeScript
    JSON
    TypeScript
    JSON
    JSON
    TypeScript
    TypeScript
    TypeScript