Lesson 5 - Step 5 of 6

Tool Result Format

Learn about content types (text, image) and the isError flag.

The CallToolResult Structure

When a tool executes, it returns a CallToolResult object. This object contains the tool's output in a structured format that supports multiple content types.

json
{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "The result of the tool execution"
            }
        ],
        "isError": false
    }
}

Result Fields

Field Type Description
content array Array of content items (text, image, etc.)
isError boolean Optional. True if the tool execution failed

Content Types

MCP supports multiple content types in tool results. The most common are text and image.

Text Content

The most common content type. Use for any text-based output:

json
{
    "content": [
        {
            "type": "text",
            "text": "The sum of 5 and 3 is 8"
        }
    ]
}

Image Content

For tools that generate or return images. Images are base64-encoded:

json
{
    "content": [
        {
            "type": "image",
            "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ...",
            "mimeType": "image/png"
        }
    ]
}

Multiple Content Items

Tools can return multiple content items of different types:

json
{
    "content": [
        {
            "type": "text",
            "text": "Here is the chart you requested:"
        },
        {
            "type": "image",
            "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ...",
            "mimeType": "image/png"
        },
        {
            "type": "text",
            "text": "The chart shows sales data for Q1 2024."
        }
    ]
}

Python Helper Functions

Create helper functions to format tool results consistently:

python
"""
result_helpers.py - Helper functions for formatting tool results.
"""

import base64


def text_result(text):
    """
    Create a text content result.

    Args:
        text: The text to return

    Returns:
        dict: MCP content object
    """
    return {
        "content": [
            {
                "type": "text",
                "text": str(text)
            }
        ]
    }


def error_result(message):
    """
    Create an error result.

    Args:
        message: Error message

    Returns:
        dict: MCP content object with isError flag
    """
    return {
        "content": [
            {
                "type": "text",
                "text": f"Error: {message}"
            }
        ],
        "isError": True
    }


def image_result(image_bytes, mime_type="image/png", caption=None):
    """
    Create an image content result.

    Args:
        image_bytes: Raw image bytes
        mime_type: MIME type of the image
        caption: Optional text caption

    Returns:
        dict: MCP content object
    """
    content = []

    if caption:
        content.append({
            "type": "text",
            "text": caption
        })

    content.append({
        "type": "image",
        "data": base64.b64encode(image_bytes).decode("utf-8"),
        "mimeType": mime_type
    })

    return {"content": content}


def multi_result(*items):
    """
    Create a result with multiple content items.

    Args:
        *items: Content items (dicts with type, text/data, etc.)

    Returns:
        dict: MCP content object
    """
    return {"content": list(items)}

Using the Helpers

Here's how to use these helpers in your tool handlers:

python
def execute_add(arguments):
    """Add two numbers."""
    a = arguments.get("a")
    b = arguments.get("b")

    if a is None or b is None:
        return error_result("Both 'a' and 'b' are required")

    result = a + b
    return text_result(f"The sum of {a} and {b} is {result}")


def execute_divide(arguments):
    """Divide two numbers."""
    a = arguments.get("a")
    b = arguments.get("b")

    if b == 0:
        return error_result("Cannot divide by zero")

    result = a / b
    return text_result(f"{a} / {b} = {result}")


def execute_generate_chart(arguments):
    """Generate a chart image."""
    # ... chart generation logic ...
    chart_bytes = generate_chart(arguments)

    return image_result(
        chart_bytes,
        mime_type="image/png",
        caption="Sales chart for the requested period"
    )

The isError Flag

The isError flag indicates that the tool executed but encountered an error. This is different from a JSON-RPC error:

isError vs JSON-RPC Error

Use isError: true when the tool ran but failed (e.g., API returned an error, file not found). Use JSON-RPC errors for request-level problems (e.g., unknown tool, invalid arguments).

python
def execute_fetch_url(arguments):
    """Fetch content from a URL."""
    url = arguments.get("url")

    try:
        import urllib.request
        with urllib.request.urlopen(url, timeout=10) as response:
            content = response.read().decode("utf-8")
            return text_result(content[:1000])  # First 1000 chars

    except urllib.error.HTTPError as e:
        return error_result(f"HTTP {e.code}: {e.reason}")

    except urllib.error.URLError as e:
        return error_result(f"Failed to connect: {e.reason}")

    except Exception as e:
        return error_result(f"Unexpected error: {str(e)}")

Complete Response Examples

Successful Text Result

json
{
    "jsonrpc": "2.0",
    "id": 5,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "The sum of 10 and 25 is 35"
            }
        ]
    }
}

Error Result

json
{
    "jsonrpc": "2.0",
    "id": 6,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "Error: Cannot divide by zero"
            }
        ],
        "isError": true
    }
}
Best Practices

Always return meaningful error messages. The AI model uses these to understand what went wrong and potentially retry with different arguments or inform the user.