Widget receives toolOutput: null despite tool returning valid structuredContent (model sees data, widget doesn't)

Widget receives toolOutput: null despite tool returning valid structuredContent

Summary

When my MCP tool returns structuredContent, the ChatGPT model correctly receives and responds to the data, but the widget’s window.openai.toolOutput is null. The data reaches the model but does not get injected into the widget runtime.

Environment

  • Platform: ChatGPT web (desktop)
  • MCP Server: Cloudflare Workers with @modelcontextprotocol/sdk
  • Widget: React 19 + Vite, served as text/html+skybridge

Steps to Reproduce

  1. Register an MCP resource with mimeType: "text/html+skybridge"
  2. Register a tool that returns structuredContent
  3. Connect the app to ChatGPT
  4. Invoke the tool via conversation
  5. Observe that the model responds correctly using the data
  6. Check window.openai.toolOutput in the widget — it’s null

Expected Behavior

window.openai.toolOutput should contain the structuredContent returned by the tool.

Actual Behavior

window.openai.toolOutput is null, even though:

  • The tool call succeeds (visible in ChatGPT’s tool explorer)
  • The model narrates the response using the returned data

Evidence

Tool Response (from ChatGPT tool explorer):

{
  "type": "list",
  "items": [
    { "id": 1, "title": "Item 1", "description": "..." },
    { "id": 2, "title": "Item 2", "description": "..." }
  ],
  "total": 20
}

Model Response:

The model correctly lists the items with titles and descriptions — proving it received structuredContent.

Widget’s window.openai (logged via console):

{
  "displayMode": "inline",
  "maxWidth": 768,
  "theme": "dark",
  "locale": "en-US",
  "userAgent": { "device": { "type": "desktop", "platform": "web" }, "capabilities": { "hover": true, "touch": false } },
  "view": { "mode": "inline" },
  "widget": { "state": null, "props": null },
  "toolInput": {},
  "toolOutput": null,
  "toolResponseMetadata": null,
  "widgetState": null,
  "subjectId": null,
  "safeArea": { "insets": { "top": 0, "bottom": 0, "left": 0, "right": 0 } }
}

Server Code (simplified)

// Resource registration
server.registerResource(
  "my-widget",
  "ui://widgets/my-widget.html",
  {},
  async () => ({
    contents: [{
      uri: "ui://widgets/my-widget.html",
      mimeType: "text/html+skybridge",
      text: htmlContent,
      _meta: {
        "openai/widgetPrefersBorder": true,
        "openai/widgetCSP": {
          connect_domains: ["https://my-domain.com"],
          resource_domains: ["https://my-domain.com"],
        },
      },
    }],
  })
);

// Tool registration
server.registerTool(
  "get-items",
  {
    title: "Get Items",
    description: "Returns a list of items",
    inputSchema: {},
    _meta: {
      "openai/outputTemplate": "ui://widgets/my-widget.html",
    },
  },
  async () => {
    const data = await fetchItems();
    return {
      content: [{ text: `Found ${data.length} items`, type: "text" }],
      structuredContent: { type: "list", items: data },
    };
  }
);

Widget Code (simplified)

function App() {
  console.log("window.openai:", JSON.stringify(window.openai, null, 2));
  console.log("toolOutput:", window.openai?.toolOutput);

  const toolOutput = window.openai?.toolOutput;

  if (toolOutput?.type === "list") {
    return <ItemList items={toolOutput.items} />;
  }

  return <div>toolOutput is null</div>;
}

What I’ve Tried

  • Verified mimeType is "text/html+skybridge"
  • Confirmed outputTemplate URI matches registered resource URI exactly
  • Used static version strings to avoid cache mismatches
  • Disconnected and reconnected the app
  • Started fresh conversations
  • Tested on multiple browsers

Question

Is there a known issue or additional configuration required for structuredContent to be injected into window.openai.toolOutput? The docs indicate this should happen automatically when the widget loads.

Here’s what Claude Code recommends:

Wait for toolOutput to be populated (not just check if window.openai exists)

That seems to be working on my end.