OpenAI Agent
read as.md OpenAI’s chat API doesn’t speak MCP, so you bridge ggui’s MCP tools into OpenAI’s function-calling shape yourself. Two packages do the work:
@modelcontextprotocol/sdk— connects to your ggui MCP endpoint (a localggui servebelow), enumerates ggui’s tools, executes tool calls.openai— drives the GPT loop and decides when to call those tools.
The bridge is mechanical: list MCP tools once, map each to an OpenAI function definition, then forward every tool_call GPT emits straight through to mcpClient.callTool.
npm install openai @modelcontextprotocol/sdkexport OPENAI_API_KEY="sk-..."import OpenAI from "openai";import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// 1. Connect to ggui's MCP server (`ggui serve --dev-allow-all` accepts any// bearer — local dev only).const mcpClient = new Client({ name: "openai-ggui-agent", version: "0.1.0" }, {});await mcpClient.connect( new StreamableHTTPClientTransport(new URL("http://127.0.0.1:6781/mcp"), { requestInit: { headers: { Authorization: "Bearer dev" }, }, }));
// 2. Enumerate ggui's MCP tools and bridge them into OpenAI's// function-calling tool shape. MCP `inputSchema` is already JSON Schema,// which is exactly what OpenAI's `parameters` expects.const { tools: mcpTools } = await mcpClient.listTools();const openaiTools: OpenAI.ChatCompletionTool[] = mcpTools.map((t) => ({ type: "function", function: { name: t.name, description: t.description ?? "", parameters: t.inputSchema as Record<string, unknown>, },}));
async function chat(userPrompt: string) { console.log(`\nUser: ${userPrompt}`);
const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: "system", content: "You are a helpful assistant. Use the available ggui tools to render interactive UIs whenever you need structured input from the user.", }, { role: "user", content: userPrompt }, ];
// 3. Drive the tool-call loop until GPT stops asking for tools. while (true) { const completion = await openai.chat.completions.create({ model: "gpt-5.5", messages, tools: openaiTools, });
const message = completion.choices[0].message; messages.push(message);
if (!message.tool_calls || message.tool_calls.length === 0) { console.log(`\nAssistant: ${message.content}`); return; }
// 4. Forward every tool call through MCP, feed results back to GPT. for (const call of message.tool_calls) { const result = await mcpClient.callTool({ name: call.function.name, arguments: JSON.parse(call.function.arguments), });
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result.content), }); } }}
await chat("Help me plan a team dinner for 8 people");await mcpClient.close();npx tsx openai-agent.tsGPT calls ggui_handshake then ggui_render; the tool result carries {sessionId, resourceUri} — an MCP-Apps resource your host mounts. GPT then calls ggui_consume({sessionId}), and the loop delivers the submitted payload on the next turn.
How the bridge works
Section titled “How the bridge works”- MCP enumerates the toolset.
listTools()returns whatever ggui exposes (ggui_handshake,ggui_render,ggui_consume, …); you don’t hard-code a tool schema in your agent. - MCP schemas are already OpenAI-compatible.
tool.inputSchemais JSON Schema; OpenAI’sfunction.parametersis JSON Schema. The map is one-to-one. - Tool dispatch is dumb forwarding. Every
tool_callGPT emits is passed verbatim tomcpClient.callTool— the MCP server (not your agent) owns render lifecycle, handshake, render, and consume. - Identity vs grouping. Identity comes from the
Authorizationheader; conversation grouping (resume viaggui_list_sessions) comes from the optional_meta["ai.ggui/host-session"]request slice. Reusing the sameClientkeeps the transport alive but does not itself group renders.
OpenAI-specific notes
Section titled “OpenAI-specific notes”- Function arguments arrive as a JSON string — always
JSON.parse(toolCall.function.arguments)before forwarding. - Tool results are returned as
{ role: "tool", tool_call_id, content }messages (not Anthropic’stool_resultblock shape). result.contentfrom MCP is an array of content blocks;JSON.stringify-ing it gives GPT a faithful view of structured payloads. For text-only tools you can pullresult.content[0].textinstead.
- Claude Agent example — same outcome via the Claude Agent SDK’s native MCP support (no manual bridging).
- Gemini Agent example — the same MCP-bridge pattern adapted to Google’s SDK.
- How it works — the three channels (bootstrap, MCP, WebSocket) and envelope shapes.