The MCP Custom Tools plugin allows you to create sophisticated MCP
(Model Context Protocol) tools
using code rather than just simple OpenAPI route mappings. 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.
This is more powerful than the MCP Server Handler
which automatically transforms your OpenAPI routes into MCP tools. Custom MCP
Tools give you full programmatic control over tool behavior.
Key Features
Programmatic Control: Define tools using TypeScript with full access to
Zuplo's runtime
Complex Workflows: Chain multiple API calls, implement business logic, and
handle complex data transformations
Type Safety: Built-in Zod schema validation for inputs and outputs
Runtime Integration: Access to context.invokeRoute(), logging, and other
Zuplo runtime features
Quick Start
1. Create Your Custom Tools
Create a module that defines your custom MCP tools:
Code(typescript)
// modules/mcp-tools.tsimport { McpCustomToolsSDK, McpToolDefinition } from "@zuplo/runtime";import { z } from "zod/v4";const sdk = new McpCustomToolsSDK();export const addNumbersTool: McpToolDefinition = sdk.defineTool({ name: "add_numbers", description: "Adds two numbers together and returns the result", schema: z.object({ a: z.number().describe("First number to add"), b: z.number().describe("Second number to add"), }), handler: async (args, context) => { context.log.info(`Adding ${args.a} + ${args.b}`); const result = args.a + args.b; return sdk.textResponse(`${args.a} + ${args.b} = ${result}`); },});export const myTools: McpToolDefinition[] = [addNumbersTool];
2. Register the Plugin
Configure the plugin in your runtime initialization. This must occur in the
file named zuplo.runtime.ts:
Code(typescript)
// modules/zuplo.runtime.tsimport { RuntimeExtensions, McpCustomToolsPlugin } from "@zuplo/runtime";import { myTools } from "./mcp-tools";export function runtimeInit(runtime: RuntimeExtensions) { const mcpPlugin = new McpCustomToolsPlugin({ name: "My Custom MCP Server", version: "1.0.0", endpoint: "/mcp-custom", // Optional, defaults to "/mcp" tools: myTools, }); runtime.addPlugin(mcpPlugin);}
3. Deploy and Test
Deploy your project and test your MCP server:
Code(bash)
# Test with MCP Inspectornpx @modelcontextprotocol/inspector# Or test with curlcurl https://your-gateway.zuplo.dev/mcp-custom \ -X POST \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": "0", "method": "tools/list" }'
SDK Reference
McpCustomToolsSDK
The main SDK class providing helper methods for creating tools and responses.
Methods
createTool() Returns a new McpToolBuilder for fluent tool definition.
defineTool(config) Define a tool with a configuration object.
Response Helpers:
textResponse(text: string) - Create a text response
jsonResponse(data: any) - Create a JSON response with structured content
errorResponse(message: string) - Create an error response
imageResponse(data: string, mimeType: string) - Create an image response
resourceResponse(uri: string, mimeType?: string) - Create a resource
response
McpToolBuilder
Fluent builder class for creating type-safe tools.
Using the ZuploContext invokeRoute, you can create powerful aggregate
workflows that call multiple routes on your gateway. This works by re-invoking
routes on your gateway without having to go back out to HTTP.
context.invokeRoutewill utilize the full inbound and outbound policy
pipeline. This means that policies you set on your MCP server route will be
invoked alongside policies that are associated with any calls made through
invokeRoute.
The MCP Inspector is ideal
for testing custom tools:
Code(bash)
npx @modelcontextprotocol/inspector
Set Transport Type to "Streamable HTTP"
Set URL to your custom tools endpoint (e.g.,
https://your-gateway.zuplo.dev/mcp-custom)
Connect and test your tools interactively
Using cURL
Test individual tools directly:
Code(bash)
# List available toolscurl https://your-gateway.zuplo.dev/mcp-custom \ -X POST \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": "0", "method": "tools/list" }'# Call a specific toolcurl https://your-gateway.zuplo.dev/mcp-custom \ -X POST \ -H 'Content-Type: application/json' \ -d '{ "jsonrpc": "2.0", "id": "1", "method": "tools/call", "params": { "name": "add_numbers", "arguments": { "a": 5, "b": 3 } } }'
Best Practices
Input Validation
Always use a Zod schema with the schema param to validate inputs. This
validation is done automatically and the args passed to your handler are type
safe. Providing descriptive schemas also ensures an MCP Client's LLM always has
the appropriate context on exactly what arguments to provide to tools.
Code(typescript)
schema: z.object({ userId: z.string().uuid().describe("Valid UUID for user ID"), amount: z.number().positive().max(10000).describe("Amount in cents"),});
Tool Design
Clear Names: Use descriptive, action-oriented names (get_user_profile,
create_order)
Detailed Descriptions: Help AI systems understand what your tool does
Error Handling: Provide meaningful error messages
Schema Design
Code(typescript)
// Good: Descriptive and well-structuredschema: z.object({ customerId: z.uuid().describe("UUID of the customer"), orderType: z.enum(["standard", "express", "overnight"]).describe("Delivery speed"), items: z.array(z.object({ productId: z.string().describe("Product SKU or ID"), quantity: z.number().int().positive().describe("Number of items"), })).min(1).describe("List of items to order"),}),// Good: Output schema for structured responsesoutputSchema: z.object({ orderId: z.string().describe("Generated order ID"), total: z.number().describe("Total amount in cents"), status: z.enum(["pending", "confirmed", "failed"]).describe("Order status"),})
Troubleshooting
Common Issues
Tool not appearing in tools/list:
Check tool name for duplicates
Verify tool is included in the tools array when registering the plugin
Check for validation errors in plugin configuration or relevant logs
Schema validation errors:
Ensure Zod schemas are properly defined and aligned with expected tool handler
usage
Check that handler arguments match schema types
Verify output matches outputSchema if defined
Handler execution failures:
Apply logs using context.log.error()
Verify API routes being invoked through invokeRoute exist and are accessible
Test individual API calls outside the MCP context
Debugging Tips
Enable Debug Logging: Use context.log.debug() liberally
Test Components Separately: Test API routes and business logic
independently
Use MCP Inspector: Interactive testing is invaluable for development