Skip to content

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 local ggui serve below), 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.

Terminal window
npm install openai @modelcontextprotocol/sdk
Terminal window
export OPENAI_API_KEY="sk-..."
openai-agent.ts
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();
Terminal window
npx tsx openai-agent.ts

GPT 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.

  • 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.inputSchema is JSON Schema; OpenAI’s function.parameters is JSON Schema. The map is one-to-one.
  • Tool dispatch is dumb forwarding. Every tool_call GPT emits is passed verbatim to mcpClient.callTool — the MCP server (not your agent) owns render lifecycle, handshake, render, and consume.
  • Identity vs grouping. Identity comes from the Authorization header; conversation grouping (resume via ggui_list_sessions) comes from the optional _meta["ai.ggui/host-session"] request slice. Reusing the same Client keeps the transport alive but does not itself group renders.
  • 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’s tool_result block shape).
  • result.content from 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 pull result.content[0].text instead.
  • 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.