Why can the model see the structured content returned by the MCP tool?

@mcp.tool(description=“Add two numbers”)
def add(a: int, b: int) → CallToolResult:
“”“Add two numbers”“”
‘’’
return (
[{“type”: “text”, “text”: f"The sum of {a} and {b} is {a+b}"}],
{“result”: a + b}
)

hi @hopegood

structured content (structuredContent) from MCP tools is not sent to the model. It is stored in the artifact field of the ToolMessage, which is kept separate from what the LLM sees. This is intentional - structured content is meant for your application code, not for the model.

When your MCP tool returns a CallToolResult, it contains two parts:

  1. content - A list of content blocks (text, images, etc.) that the model sees
  2. structuredContent - Machine-parseable data (JSON, etc.) that is not visible to the model

The langchain-mcp-adapters library handles this separation:

MCP Tool returns CallToolResult
    ├── content (text blocks)        ->  ToolMessage.content     ->  sent to the model
    └── structuredContent (JSON)     ->  ToolMessage.artifact     ->  NOT sent to the model

In the Python adapter source code (langchain-mcp-adapters/tools.py), the _convert_call_tool_result() function handles this separation. The structuredContent is wrapped in an MCPToolArtifact:

# From langchain_mcp_adapters/tools.py

class MCPToolArtifact(TypedDict):
    """Wraps structured content from MCP tool calls."""
    structured_content: dict[str, Any]

def _convert_call_tool_result(call_tool_result):
    # CallToolResult.content -> converted to LangChain content blocks (model sees these)
    tool_content: list[ToolMessageContentBlock] = [
        _convert_mcp_content_to_lc_block(content)
        for content in call_tool_result.content
    ]

    # CallToolResult.structuredContent -> stored as MCPToolArtifact (model does NOT see)
    artifact: MCPToolArtifact | None = None
    if call_tool_result.structuredContent is not None:
        artifact = MCPToolArtifact(
            structured_content=call_tool_result.structuredContent
        )

    return tool_content, artifact

The tool is configured with response_format="content_and_artifact", so LangChain unpacks this tuple into ToolMessage.content (sent to the model) and ToolMessage.artifact (kept for application code only).

IMHO the separation exists for good reasons:

  • Token efficiency - Structured data (large JSON blobs, database results) can be very large and would waste tokens if sent to the model
  • separation of concerns - The model works with human-readable text; your application code works with structured data
  • MCP protocol design - The MCP specification itself defines structuredContent as an optional machine-readable supplement, separate from the content that is shown to the model

If you want the model to see the structured content, try this:

option 1: interceptor

Use an mcp interceptor to append the structured content as a text block in the content array before it’s returned:

import json
from mcp.types import TextContent

async def append_structured_content(request, handler):
    result = await handler(request)
    if result.structuredContent:
        result.content += [
            TextContent(
                type="text",
                text=json.dumps(result.structuredContent)
            ),
        ]
    return result

option 2: structured data directly in content

@mcp.tool(description="Add two numbers")
def add(a: int, b: int) -> list[dict]:
    """Add two numbers"""
    result = a + b
    return [
        {"type": "text", "text": f"The sum of {a} and {b} is {result}"},
        {"type": "text", "text": json.dumps({"result": result})}
    ]

This way, both the human-readable text and the structured data are in the content array and will be visible to the model.

option 3: access structured content in application code

If you don’t need the model to see it, but your application needs the data, extract it from the artifact after invocation:

from langchain_core.messages import ToolMessage

for message in result["messages"]:
    if isinstance(message, ToolMessage) and message.artifact:
        structured_data = message.artifact["structured_content"]

summary

Field Visible to Model? Purpose
content (text/image blocks) Yes Human-readable output for the LLM
structuredContent No (stored in artifact) Machine-parseable data for application code

sources

Thank you very much for your answer, but now the model is also showing {“result”: a + b}, which, according to the official documentation, should not be visible.

@hopegood

could you share some of your code? how do you define the tool and your workflow/agent?

hmmm you return a plain python tuple but annotates the return type as → CallToolResult. The mcp sdk does isinstance(result, CallToolResult) which fails for a plain tuple.

Since the tuple may not be recognized as a CallToolResult, the SDK’s _convert_to_content() function treats the entire tuple as content - no structuredContent is ever set.

Try this:

 @mcp.tool(description="Add two numbers")
  def add(a: int, b: int) -> CallToolResult:
      return CallToolResult(
          content=[TextContent(type="text", text=f"The sum of {a} and {b} is {a+b}")],
          structuredContent={"result": a + b}
      )

Hello, according to your code, the tool message sent to the model is as follows:

{
“content”: “[{"type": "text", "text": "{\"_meta\":null,\"content\":[{\"type\":\"text\",\"text\":\"The sum of 3 and 5 is 8\",\"annotations\":null,\"_meta\":null}],\"structuredContent\":{\"result\":8},\"isError\":false}", "id": "lc_ba00f2b6-7ff3-4e69-8a61-b0a0af1cac6d"}]”,

“role”: “tool”,

“tool_call_id”: “call_00_QRAB2NHQmkvbTde1NMRoMg2u”

} This still sends all the content returned by the tool message to the large model.

could you?

1:The tool code is as follows:

from fastmcp import FastMCP
from mcp.types import CallToolResult, TextContent
from langchain_mcp_adapters.tools import MCPToolArtifact
from typing import Dict, Any
from mcp import types
mcp = FastMCP("Math")


@mcp.tool(description="Add two numbers")
def add(a: int, b: int) -> CallToolResult:
      return CallToolResult(
          content=[TextContent(type="text", text=f"The sum of {a} and {b} is {a+b}")],
          structuredContent={"result": a + b}
      )
      
if __name__ == "__main__":
    mcp.run(transport="stdio")  

2:The agent code is as follows:

import asyncio
import json
from langchain_mcp_adapters.client import MultiServerMCPClient  
from langchain.agents import create_agent
from localLLMConfiger import office_chat_model as model
import fiddlerConfiger
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_core.messages import ToolMessage
from langchain_mcp_adapters.interceptors import MCPToolCallRequest
from mcp.types import TextContent

async def append_structured_content(request: MCPToolCallRequest, handler):
    """Append structured content from artifact to tool message."""
    result = await handler(request)
    if result.structuredContent:
        result.content += [
            TextContent(type="text", text=json.dumps(result.structuredContent)),
        ]
    return result


async def main():
    client = MultiServerMCPClient(
        {
            "file": {
                "transport": "stdio",
                "command": r"C:\Users\13901\AppData\Local\Programs\Python\Python313\python.exe",
                "args": [r"E:\study\python\app\llm\langchain\agent\langgraph\mcp\structContentMCPServer.py"],
            }
        }
    )
    tools = await client.get_tools()
    agent = create_agent(model,tools)
    result = await agent.ainvoke({
            "messages": [{"role": "user", "content": "3加5等于多少?"}]
        })

    # print(result)
    # Extract structured content from tool messages
    for message in result["messages"]:
        if isinstance(message, ToolMessage) and message.artifact:
           structured_content = message.artifact["structured_content"]
           print("Structured Content:", structured_content)
    
    
if __name__ == "__main__":
    asyncio.run(main())    

Is this interceptor append_structured_content being used/registered anywhere? It actually does what you don’t want to happen

Oh, it’s never been used anywhere.

could you tell me the versions of all your python pakcages?

and what provider and model do you use? Some models.

I’m still debuggig this…

ok, I found the root of the issue:

instead of

from fastmcp import FastMCP                                                                                                                                         

use

from mcp.server.fastmcp import FastMCP 

and of course install mcp

thank you

1 Like