---
title: OpenAI Agent
description: Bridge OpenAI function calling to ggui's MCP server so GPT can show interactive UIs and read back structured user input.
---

:::note[Simpler path with Claude]
Using Anthropic? The [Claude Agent example](/examples/claude-agent/) uses the Claude Agent SDK's built-in MCP support — no manual tool bridging required. The pattern below is for hosts (OpenAI, Gemini, custom) that don't speak MCP natively.
:::

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

## Setup

```bash
npm install openai @modelcontextprotocol/sdk
```

```bash
export OPENAI_API_KEY="sk-..."
```

:::note[Hosted ggui — coming soon]
The snippet below targets a local `ggui serve --dev-allow-all` (`http://127.0.0.1:6781/mcp`, `Authorization: Bearer dev` — local dev only; default `ggui serve` requires a paired bearer). A hosted endpoint is coming soon: swap in `https://mcp.ggui.ai/apps/<appId>` and a `ggui_user_…` connector key from `ggui keys create`.
:::

## Code

```typescript
// 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();
```

## Run

```bash
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.

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

## 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'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.

## Next

- [Claude Agent example](/examples/claude-agent/) — same outcome via the Claude Agent SDK's native MCP support (no manual bridging).
- [Gemini Agent example](/examples/gemini-agent/) — the same MCP-bridge pattern adapted to Google's SDK.
- [How it works](/how-it-works/) — the three channels (bootstrap, MCP, WebSocket) and envelope shapes.