# ZuploMcpSdk

The `ZuploMcpSdk` class provides a helper API for custom MCP tool handlers to
interact with the MCP runtime. Use it to read metadata from the incoming
`tools/call` request and to override fields of the tool result the gateway sends
back to the AI client.

```ts
import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  const sdk = new ZuploMcpSdk(context);

  // Read the incoming tool call request
  const mcpRequest = sdk.getRawCallToolRequest();
  context.log.info(`Tool called: ${mcpRequest?.params.name}`);

  // Invoke a route on your gateway
  const response = await context.invokeRoute("/todos");

  // Override the content the model sees, keeping auto-derived structuredContent
  sdk.setRawCallToolResult({
    content: [{ type: "text", text: "Fetched the todo list" }],
  });

  return response;
}
```

`ZuploMcpSdk` is available in custom tool handlers for both the
[MCP Server handler](../handlers/mcp-server.mdx) and the
[MCP Gateway](../mcp-gateway/introduction.mdx). It is exported from
`@zuplo/runtime`.

## Methods

### `setRawCallToolResult(result)`

Overrides fields of the MCP tool result the gateway is about to send to the AI
client. Every field is independently optional — an absent field keeps the value
the gateway derives from the downstream response.

```ts
setRawCallToolResult(result: ZuploMcpToolResultOverride): void
```

#### Parameters

The `result` argument is a `ZuploMcpToolResultOverride` object with the
following optional fields:

| Field               | Type                        | Description                                                                                                                  |
| ------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `content`           | `CallToolResult["content"]` | Content blocks the model reads. Replaces the gateway's default (the serialized downstream body in a text block).             |
| `structuredContent` | `Record<string, unknown>`   | The structured form of the tool's output. Must be a JSON object — the MCP 2025-11-25 wire format rejects arrays and scalars. |
| `_meta`             | `Record<string, unknown>`   | Per-call metadata. Replaces the gateway's default of `{}`.                                                                   |

#### Compact summary example

The most useful combination is `content` alone: a human-readable summary
replaces the raw serialized body in the model's context, while
`structuredContent` is still auto-derived from the downstream response. This
reduces token consumption without losing data.

```ts
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;
}
```

The model sees `"Fetched the todo list"` instead of the full JSON body, but the
`structuredContent` field still carries the complete payload for spec-compliant
clients that read it.

#### Full override example

Override all available fields at once:

```ts
import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  const response = await context.invokeRoute("/orders");
  const orders = await response.json();

  new ZuploMcpSdk(context).setRawCallToolResult({
    content: [
      {
        type: "text",
        text: `Found ${orders.length} orders totaling $${orders.total}`,
      },
    ],
    structuredContent: { orders: orders.items, count: orders.length },
    _meta: { source: "order-service", version: "2.0" },
  });

  return response;
}
```

:::note

**`isError` is not overridable.** It follows the HTTP status of the response
your handler returns, so upstream failures cannot be masked. If the downstream
route returns a non-2xx status, the tool result carries `isError: true`
regardless of any override.

:::

:::caution{title="Single-use — consumed once"}

The override is **consumed** when the gateway assembles the tool result. It is
read and deleted from the context at that point, so calling
`setRawCallToolResult` twice in the same request replaces the first value before
the gateway reads it.

:::

#### How context lookup works

`setRawCallToolResult` writes the override to the context that carried the
matching `tools/call` request. The lookup travels exactly one
`context.invokeRoute` generation up from the context passed to the constructor.

This means:

- In a typical custom tool handler that calls `context.invokeRoute`, the
  override is written to the parent context (the one carrying the `tools/call`
  request), which is correct.
- If your handler calls `invokeRoute` to a route that **also** calls
  `invokeRoute`, and you construct `ZuploMcpSdk` in that grandchild invocation,
  the override is stored on the immediate parent — not the grandparent that
  carries the tool call. The gateway never reads it. Construct `ZuploMcpSdk` in
  the handler that is one level below the MCP route.

### `getRawCallToolRequest()`

Retrieves the original MCP `tools/call` request object from the context. Use
this to access metadata like the `_meta` field from the incoming tool call.

```ts
getRawCallToolRequest(): CallToolRequest | null
```

Returns the `CallToolRequest` object, or `null` if no MCP tool call is in flight
on the current or parent context.

```ts
import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
  const sdk = new ZuploMcpSdk(context);
  const mcpRequest = sdk.getRawCallToolRequest();

  if (mcpRequest) {
    const meta = mcpRequest.params._meta;
    context.log.info(`Incoming _meta: ${JSON.stringify(meta)}`);
  }

  // ... handle the tool call
}
```

Unlike `setRawCallToolResult`, this method can be called any number of times —
the request object is not consumed.

## Output schema enforcement

When a tool advertises an `outputSchema` (via `includeOutputSchema` on the
handler or route config), the gateway validates the tool result's
`structuredContent` against that schema before sending it to the client. A
result whose `structuredContent` does not conform to the advertised schema fails
the call with a diagnostic `isError` result instead of a raw protocol error.

This enforcement is skipped on the error path (`isError: true`), matching the
MCP SDK's own behavior.

### `includeOutputSchema` implies `includeStructuredContent`

Advertising an `outputSchema` while returning no `structuredContent` makes the
advertised schema inaccurate — spec-compliant clients reject the call. When
`includeOutputSchema` resolves to `true`, the gateway automatically forces
`includeStructuredContent` to `true` as well, and logs a warning naming the
route where the override kicked in.

You can set both explicitly to avoid the warning:

```json
{
  "x-zuplo-route": {
    "handler": {
      "export": "mcpServerHandler",
      "module": "$import(@zuplo/runtime)",
      "options": {
        "includeOutputSchema": true,
        "includeStructuredContent": true
      }
    }
  }
}
```

## Type reference

### `ZuploMcpToolResultOverride`

```ts
interface ZuploMcpToolResultOverride {
  content?: CallToolResult["content"];
  structuredContent?: Record<string, unknown>;
  _meta?: Record<string, unknown>;
}
```

The subset of a `tools/call` result that a module author may override via
`setRawCallToolResult`. Every field is independently optional.

## See also

- [MCP Server custom tools](../mcp-server/custom-tools.mdx) — how to build
  custom MCP tool handlers with TypeScript
- [MCP Server handler](../handlers/mcp-server.mdx) — handler configuration
  reference including `includeOutputSchema` and `includeStructuredContent`
- [MCP Gateway introduction](../mcp-gateway/introduction.mdx) — overview of the
  MCP Gateway product
- [MCP specification](https://modelcontextprotocol.io/specification/) — the
  canonical protocol reference
