{
"$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"
}