ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
    IntroductionToolsPromptsResourcesTestingGraphQLCustom ToolsOpenAI Apps SDK
    Guides
MCP Gateway
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
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`; } }

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

  • 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 March 23, 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
  • Best Practices
    • Tool Design
  • Troubleshooting
    • Common Issues
    • Debugging Tips
  • Learn More
JSON
TypeScript
TypeScript
JSON
TypeScript
JSON
JSON
TypeScript
TypeScript