Unified MCP Server

Tool Results & Code Execution

Some MCP clients don't put every tool result into the model's context. Instead, the model writes code that calls the tools as functions, filters or joins what comes back, and returns only the answer. Anthropic's programmatic tool calling and Cloudflare's Code Mode work this way. It suits the Unified MCP server well: list tools return whole pages of contacts, employees or invoices, and code can page through them and keep only what matters.

Code usually receives only a tool result's text, so this page describes exactly what that text contains, then shows how to use our tools from Claude's code execution.

What a tool returns

A successful call to a data tool (list_*, get_*, create_*, update_*, ...) returns the same JSON object in two places:

  • content[0].text: the result as compact JSON text
  • structuredContent: the result as an object, sent to clients on MCP revision 2025-06-18 or later

A list is wrapped as { "results": [...] } in both, because structuredContent must be a JSON object. This is the shape each tool's outputSchema describes:

{ "results": [{ "id": "123", "name": "Jane Doe", "emails": [{ "email": "jane@example.com" }] }] }

A get, create or update returns the object itself, eg. { "id": "123", "name": "Jane Doe" }. A call that returns no body has empty text.

A failed call has isError: true, and its text is JSON too, so code can branch on the status:

{ "error": true, "status": 404, "message": "Contact not found" }

status is the HTTP status (400 for a missing required parameter, 429 for your workspace's rate limit, otherwise the status the integration's API returned), and is left out when it is unknown. Authentication, preview and OAuth-scope refusals are plain-text guidance instead.

Text is capped at 512 KB. Above that, a client that also receives structuredContent gets a short note in the text pointing to it, and an older client gets an error. Read structuredContent when you have it, and narrow large results with limit and fields.

Anthropic's code execution

With programmatic tool calling, Claude writes Python in Anthropic's code execution container, and each tool it calls pauses the code until your application returns the result. The intermediate results never enter Claude's context.

Tools that come from Anthropic's MCP connector (mcp_servers + mcp_toolset) cannot be called from code execution. To use our tools from code, connect to the Unified MCP server yourself and pass its tools to Claude as your own tools, with allowed_callers set to the code execution tool. Your application then forwards each call to the MCP server.

The example below does this in TypeScript with the MCP TypeScript SDK and the Anthropic SDK:

import Anthropic from '@anthropic-ai/sdk';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import type { CallToolResult, TextContent } from '@modelcontextprotocol/sdk/types.js';

const anthropic = new Anthropic();

// 1. Connect to the Unified MCP server for one connection, limited to the tools this task needs.
const mcp = new Client({ name: 'my-app', version: '1.0.0' });
await mcp.connect(
    new StreamableHTTPClientTransport(new URL('https://mcp-api.unified.to/mcp?connection=YYYYYYY&tools=list_*,get_*'), {
        requestInit: { headers: { Authorization: `Bearer ${process.env.UNIFIED_API_KEY}` } },
    })
);

// 2. Offer each MCP tool to Claude as a tool that its code can call.
const { tools } = await mcp.listTools();
const claudeTools = [
    { type: 'code_execution_20260120' as const, name: 'code_execution' as const },
    ...tools.map((tool) => ({
        name: tool.name,
        description: `${tool.description ?? ''}\nReturns JSON. A list returns {"results": [...]}; a failure returns {"error": true, "status": ..., "message": ...}.`,
        input_schema: tool.inputSchema,
        allowed_callers: ['code_execution_20260120' as const],
    })),
];

// 3. Run the conversation, forwarding every tool call to the MCP server.
const messages: Anthropic.MessageParam[] = [
    { role: 'user', content: 'Which open deals over $50,000 have had no activity in the last 30 days?' },
];
let container: string | undefined;

while (true) {
    const response = await anthropic.messages.create({
        model: 'claude-opus-5-5',
        max_tokens: 16000,
        tools: claudeTools,
        messages,
        ...(container ? { container } : {}),
    });
    // A paused code execution run must continue in the same container.
    container = response.container?.id ?? container;
    messages.push({ role: 'assistant', content: response.content });

    if (response.stop_reason === 'pause_turn') {
        continue;
    }
    if (response.stop_reason !== 'tool_use') {
        for (const block of response.content) {
            if (block.type === 'text') {
                console.log(block.text);
            }
        }
        break;
    }

    // Answer every pending call in one user message that holds only tool_result blocks.
    const results = await Promise.all(
        response.content
            .filter((block) => block.type === 'tool_use')
            .map(async (block) => {
                const result = (await mcp.callTool({ name: block.name, arguments: block.input as Record<string, unknown> })) as CallToolResult;
                const text = result.content
                    .filter((part): part is TextContent => part.type === 'text')
                    .map((part) => part.text)
                    .join('');
                return {
                    type: 'tool_result' as const,
                    tool_use_id: block.id,
                    // structuredContent has the same shape as the text, without the text's size cap.
                    content: !result.isError && result.structuredContent ? JSON.stringify(result.structuredContent) : text,
                    ...(result.isError ? { is_error: true } : {}),
                };
            })
    );
    messages.push({ role: 'user', content: results });
}

await mcp.close();

Claude's code receives each result as a string and parses it, for example:

deals = json.loads(await list_crm_deals({"limit": 100}))["results"]

A few things to know:

  • Keep the tool list short. Every tool you pass is part of each request. Use the tools and permissions options on the MCP URL, which accept wildcards such as list_*.
  • Each tool call is an API request. A script that pages through a large list makes one request per page, and each counts against your plan and your rate limit like any other tool call. A 429 comes back as an error result the code can see.
  • Answer quickly. A paused tool call times out after about 4 minutes, and the code then gets a TimeoutError.
  • The model may still call a tool directly. allowed_callers guides Claude but doesn't forbid a direct call, so the loop above handles both the same way.

See Anthropic's programmatic tool calling guide for its limits and the full request flow.

Are we missing anything? Let us know
Was this page helpful?