# MCP Server Custom Tools

The MCP Server Handler supports custom tools that allow you to create
sophisticated MCP
([Model Context Protocol](https://modelcontextprotocol.io/introduction)) 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.

:::tip

Custom tools give you full programmatic control over tool behavior within an
[MCP Server Handler](../handlers/mcp-server.mdx). 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:

```json
{
  "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:

```typescript
// 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;
}
```

```typescript
// 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:

```json
{
  "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:

```bash
# 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:

```typescript
// 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.

:::note

`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.

```json
{
  "/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:

```json
{
  "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:

```typescript
// 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:

```typescript
// 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](../handlers/mcp-server.mdx)
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](../handlers/mcp-server.mdx) - For simple route-to-tool
  mapping
- [Model Context Protocol Overview](../mcp-server/introduction.mdx) -
  Understanding MCP concepts
- [MCP Specification](https://modelcontextprotocol.io/specification/) - Official
  protocol documentation
