{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreig2bdcs7yv4mthzo3ijfiyex2anc7txhhaqbfbazdwiephha7jbhy",
    "uri": "at://did:plc:lk3jfj3zq4k4wxnk474axylu/app.bsky.feed.post/3mpgpw37txcm2"
  },
  "path": "/t/the-new-chatgpt-5-5-instant-broke-multi-step-app-mcp-tool-calls/1385257#post_5",
  "publishedAt": "2026-06-29T14:33:48.000Z",
  "site": "https://community.openai.com",
  "tags": [
    "@tangweigangsir",
    "@mcp.tool"
  ],
  "textContent": "Hi Eric,\n\nThis problem seems like a major breakage in ChatGPT Apps behavior. I think it is worth escalating ASAP.\n\nPer @tangweigangsir’s suggestion, I created a minimal reproducible example of a Python MCP server that demonstrates a description-driven dependent tool chain. It exposes three tools: first_call, second_call, and third_call. Each tool’s description tells the model which tool to call next after success, and each response returns the exact next\ntool arguments. The final step requires finish_success=“finish_success”.\n\nIt fails with the same “toolchain” error that our actual app fails with:\n\nYou can easily reproduce the issue, simply run this `server.py` and ask ChatGPT to use it in Instant mode (without Auto thinking):\n\n\n    from __future__ import annotations\n\n    import argparse\n    import os\n    import secrets\n    import time\n    from typing import Literal\n\n    from mcp.server.fastmcp import FastMCP\n    from mcp.server.transport_security import TransportSecuritySettings\n    from pydantic import Field\n\n\n    LOCAL_ALLOWED_HOSTS = [\"127.0.0.1:*\", \"localhost:*\", \"[::1]:*\"]\n    LOCAL_ALLOWED_ORIGINS = [\"http://127.0.0.1:*\", \"http://localhost:*\", \"http://[::1]:*\"]\n\n    mcp = FastMCP(\n        \"dependent-tool-sequence-mrp\",\n        instructions=(\n            \"This server demonstrates a description-driven three-tool sequence. \"\n            \"Call first_call first, then second_call, then third_call.\"\n        ),\n        stateless_http=True,\n        json_response=True,\n    )\n\n    _runs: dict[str, dict[str, object]] = {}\n\n\n    def _csv_values(values: list[str]) -> list[str]:\n        items: list[str] = []\n        for value in values:\n            items.extend(part.strip() for part in value.split(\",\") if part.strip())\n        return items\n\n\n    def _dedupe(values: list[str]) -> list[str]:\n        seen: set[str] = set()\n        result: list[str] = []\n        for value in values:\n            if value not in seen:\n                seen.add(value)\n                result.append(value)\n        return result\n\n\n    def _expanded_hosts(hosts: list[str]) -> list[str]:\n        expanded: list[str] = []\n        for host in hosts:\n            expanded.append(host)\n            if \":\" not in host and not host.endswith(\":*\"):\n                expanded.append(f\"{host}:*\")\n        return _dedupe(expanded)\n\n\n    def _origins_for_hosts(hosts: list[str]) -> list[str]:\n        origins: list[str] = []\n        for host in hosts:\n            if host in {\"0.0.0.0\", \"::\"}:\n                continue\n            if host.startswith((\"http://\", \"https://\")):\n                origins.append(host)\n                continue\n            origins.extend([f\"http://{host}\", f\"https://{host}\"])\n        return _dedupe(origins)\n\n\n    def _new_token(prefix: str) -> str:\n        return f\"{prefix}_{secrets.token_urlsafe(8)}\"\n\n\n    @mcp.tool(\n        name=\"first_call\",\n        description=(\n            \"Step 1 of 3. Call this tool first. If this tool returns status='success', \"\n            \"the next action is to call the MCP tool named second_call with the run_id \"\n            \"and first_call_token returned by this tool.\"\n        ),\n    )\n    def first_call() -> dict[str, object]:\n        \"\"\"Start the dependent tool-call sequence.\"\"\"\n        run_id = _new_token(\"run\")\n        first_call_token = _new_token(\"first\")\n        _runs[run_id] = {\n            \"created_at\": time.time(),\n            \"first_call_token\": first_call_token,\n            \"second_call_token\": None,\n            \"complete\": False,\n        }\n\n        return {\n            \"status\": \"success\",\n            \"run_id\": run_id,\n            \"first_call_token\": first_call_token,\n            \"next_tool\": \"second_call\",\n            \"next_arguments\": {\n                \"run_id\": run_id,\n                \"first_call_token\": first_call_token,\n            },\n        }\n\n\n    @mcp.tool(\n        name=\"second_call\",\n        description=(\n            \"Step 2 of 3. Call this tool only after first_call returns status='success'. \"\n            \"Use the exact run_id and first_call_token returned by first_call. If this \"\n            \"tool returns status='success', the next action is to call the MCP tool named \"\n            \"third_call with the run_id, second_call_token, and finish_success='finish_success'.\"\n        ),\n    )\n    def second_call(\n        run_id: str = Field(description=\"The run_id returned by first_call.\"),\n        first_call_token: str = Field(description=\"The first_call_token returned by first_call.\"),\n    ) -> dict[str, object]:\n        \"\"\"Continue the sequence after first_call.\"\"\"\n        run = _runs.get(run_id)\n        if run is None:\n            return {\n                \"status\": \"error\",\n                \"message\": \"Unknown run_id. Call first_call before second_call.\",\n            }\n\n        if run[\"first_call_token\"] != first_call_token:\n            return {\n                \"status\": \"error\",\n                \"message\": \"Invalid first_call_token. Use the exact token returned by first_call.\",\n            }\n\n        second_call_token = _new_token(\"second\")\n        run[\"second_call_token\"] = second_call_token\n\n        return {\n            \"status\": \"success\",\n            \"run_id\": run_id,\n            \"second_call_token\": second_call_token,\n            \"next_tool\": \"third_call\",\n            \"next_arguments\": {\n                \"run_id\": run_id,\n                \"second_call_token\": second_call_token,\n                \"finish_success\": \"finish_success\",\n            },\n        }\n\n\n    @mcp.tool(\n        name=\"third_call\",\n        description=(\n            \"Step 3 of 3. Call this tool only after second_call returns status='success'. \"\n            \"Use the exact run_id and second_call_token returned by second_call, and set \"\n            \"finish_success exactly to 'finish_success'. If this tool returns status='success', \"\n            \"finish the user interaction with a successful final response.\"\n        ),\n    )\n    def third_call(\n        run_id: str = Field(description=\"The run_id originally returned by first_call.\"),\n        second_call_token: str = Field(description=\"The second_call_token returned by second_call.\"),\n        finish_success: Literal[\"finish_success\"] = Field(\n            description=\"Must be exactly the string 'finish_success'.\"\n        ),\n    ) -> dict[str, object]:\n        \"\"\"Finish the sequence after second_call.\"\"\"\n        run = _runs.get(run_id)\n        if run is None:\n            return {\n                \"status\": \"error\",\n                \"message\": \"Unknown run_id. Call first_call before third_call.\",\n            }\n\n        if run[\"second_call_token\"] != second_call_token:\n            return {\n                \"status\": \"error\",\n                \"message\": \"Invalid second_call_token. Use the exact token returned by second_call.\",\n            }\n\n        run[\"complete\"] = True\n        return {\n            \"status\": \"success\",\n            \"run_id\": run_id,\n            \"finish_success\": finish_success,\n            \"message\": \"Dependent MCP tool sequence completed successfully.\",\n        }\n\n\n    def main() -> None:\n        parser = argparse.ArgumentParser(description=\"Minimal dependent-tool MCP server.\")\n        parser.add_argument(\n            \"--transport\",\n            choices=[\"http\", \"stdio\"],\n            default=\"http\",\n            help=\"Run as Streamable HTTP for EC2 or stdio for local MCP clients.\",\n        )\n        parser.add_argument(\"--host\", default=\"0.0.0.0\", help=\"HTTP host bind address.\")\n        parser.add_argument(\"--port\", type=int, default=8000, help=\"HTTP port.\")\n        parser.add_argument(\n            \"--allowed-host\",\n            action=\"append\",\n            default=[],\n            help=(\n                \"Allowed HTTP Host header for Streamable HTTP. Repeat or comma-separate. \"\n                \"Example: --allowed-host aleena-unilobed-karma.ngrok-free.dev\"\n            ),\n        )\n        parser.add_argument(\n            \"--allowed-origin\",\n            action=\"append\",\n            default=[],\n            help=(\n                \"Allowed Origin header. Repeat or comma-separate. If omitted, origins are \"\n                \"derived from allowed hosts.\"\n            ),\n        )\n        parser.add_argument(\n            \"--disable-dns-rebinding-protection\",\n            action=\"store_true\",\n            help=\"Disable Host/Origin validation. Useful only for local experiments.\",\n        )\n        args = parser.parse_args()\n\n        if args.transport == \"stdio\":\n            mcp.run(transport=\"stdio\")\n            return\n\n        env_allowed_hosts = os.getenv(\"MCP_ALLOWED_HOSTS\", \"\")\n        env_allowed_origins = os.getenv(\"MCP_ALLOWED_ORIGINS\", \"\")\n        configured_hosts = _csv_values(args.allowed_host + [env_allowed_hosts])\n        configured_origins = _csv_values(args.allowed_origin + [env_allowed_origins])\n\n        allowed_hosts = _dedupe(LOCAL_ALLOWED_HOSTS + _expanded_hosts(configured_hosts))\n        allowed_origins = _dedupe(\n            LOCAL_ALLOWED_ORIGINS + configured_origins + _origins_for_hosts(configured_hosts)\n        )\n        mcp.settings.host = args.host\n        mcp.settings.port = args.port\n        mcp.settings.transport_security = TransportSecuritySettings(\n            enable_dns_rebinding_protection=not args.disable_dns_rebinding_protection,\n            allowed_hosts=allowed_hosts,\n            allowed_origins=allowed_origins,\n        )\n\n        import uvicorn\n\n        uvicorn.run(mcp.streamable_http_app(), host=args.host, port=args.port)\n\n\n    if __name__ == \"__main__\":\n        main()\n\n",
  "title": "The new ChatGPT 5.5 Instant broke multi-step App/MCP tool calls"
}