Skip to content

Testing OpenAI Apps SDK Widget Integration

Testing OpenAI Apps SDK Widget Integration

Section titled “Testing OpenAI Apps SDK Widget Integration”

This guide explains how to test the widget integration for rover-mcp.

The rover-mcp server now supports the OpenAI Apps SDK, allowing it to serve rich UI widgets alongside MCP tool responses. This integration includes:

  • Phase 1: Tool responses include _meta fields with widget URIs
  • Phase 2: Widget HTML bundles served via MCP resource handlers
  • Phase 3 (Optional): Production React widget bundles

Both search_offers and search_nearby_offers tools now return responses with:

{
"content": [
{
"type": "text",
"text": "{...offer data...}"
}
],
"_meta": {
"openai/outputTemplate": "ui://widget/offer-list.html",
"openai/toolInvocation/invoking": "Searching for offers...",
"openai/toolInvocation/invoked": "Found 42 offers!",
"openai/widgetAccessible": true,
"openai/resultCanProduceWidget": true
}
}

The server registers two widget resources:

  1. ui://widget/offer-list.html - Card-based list view for offers
  2. ui://widget/offer-map.html - Location-based retailer view

When ChatGPT receives a tool response with _meta.openai/outputTemplate, it calls the MCP ReadResource endpoint to fetch the widget HTML bundle.

All tests pass successfully:

Terminal window
# Run widget package tests
go test -v ./pkg/widgets
# Run all unit tests
go test ./cmd/... ./pkg/...
  • ✅ Widget registry validation (2 widgets registered)
  • ✅ Widget HTML embedding via go:embed
  • ✅ MCP resource registration
  • _meta field generation for both widgets
  • ✅ Tool response JSON marshaling with _meta
  • ✅ HTML structure validation (DOCTYPE, window.openai, etc.)

The MCP Inspector is an interactive tool for testing MCP servers.

Terminal window
# Install MCP Inspector (requires Node.js)
npm install -g @modelcontextprotocol/inspector
# Start rover-mcp server (in one terminal)
cd /Users/s.hollinger/experiments/pilot-agent/rover-mcp
go run ./cmd/mcp-server
# Run MCP Inspector (in another terminal)
npx @modelcontextprotocol/inspector http://localhost:8080
  1. List Resources - Verify widgets are registered:

    Method: resources/list
    Expected: 2 resources (offer-list.html, offer-map.html)
  2. Read Widget Resource - Fetch widget HTML:

    Method: resources/read
    Params: { "uri": "ui://widget/offer-list.html" }
    Expected: HTML content with window.openai references
  3. Call search_offers Tool - Verify _meta in response:

    Method: tools/call
    Params: {
    "name": "search_offers",
    "arguments": {
    "query": "coffee",
    "user_id": "test-user-123"
    }
    }
    Expected: Response contains _meta with openai/outputTemplate
  4. Verify Widget URI - Check that _meta.openai/outputTemplate matches registered resource URI

  • ChatGPT Plus or Team subscription
  • Developer mode enabled in ChatGPT settings
  • ngrok or similar tunneling tool for local testing
Terminal window
# Install ngrok (if needed)
brew install ngrok
# Start rover-mcp server
go run ./cmd/mcp-server
# Create tunnel to localhost:8080
ngrok http 8080
# Note the ngrok URL (e.g., https://abc123.ngrok-free.app)
  1. Open ChatGPT Settings → Developer Mode

  2. Add MCP Connector:

    • Name: rover-mcp
    • URL: https://your-ngrok-id.ngrok-free.app
    • Authentication: Bearer token from AWS Secrets Manager
  3. Test the integration:

    User: "Find coffee offers for user 12345"
    Expected: ChatGPT calls search_offers and renders offer-list widget

When ChatGPT calls search_offers:

  1. Tool Execution: Server returns JSON data + _meta
  2. Widget Discovery: ChatGPT reads _meta.openai/outputTemplate
  3. Resource Fetch: ChatGPT calls ReadResource("ui://widget/offer-list.html")
  4. Widget Render: ChatGPT displays widget in iframe with window.openai populated

The widget will:

  • Read offer data from window.openai.toolOutput
  • Adapt to theme (window.openai.theme)
  • Display offers in card layout
Terminal window
# Verify widgets are embedded correctly
go run ./cmd/mcp-server 2>&1 | grep -i widget
# View widget HTML directly
cat pkg/widgets/html/offer-list.html
cat pkg/widgets/html/offer-map.html

Add debug logging in main.go after widget registration:

if err := widgets.RegisterAll(srv.server); err != nil {
logger.Error("Failed to register widget resources", propError, err)
os.Exit(1)
}
logger.Info("Registered widget resources successfully", "count", len(widgets.Registry))

Test tool responses include _meta:

Terminal window
# Start server with debug logging
LOG_LEVEL=debug go run ./cmd/mcp-server
# Make tool call via HTTP
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_offers",
"arguments": {
"query": "coffee",
"user_id": "test-123"
}
}
}'
# Response should contain _meta.openai/outputTemplate

The current widgets are vanilla JavaScript placeholders. To create production React widgets:

Terminal window
cd rover-mcp
mkdir widgets && cd widgets
# Initialize npm project
pnpm init
# Install dependencies
pnpm add react react-dom
pnpm add -D vite @vitejs/plugin-react typescript
widgets/src/offer-list/index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { useOpenAiGlobal } from '../hooks/use-openai-global';
import { useWidgetProps } from '../hooks/use-widget-props';
interface OfferData {
offers: Array<{
id: string;
points: number;
offer_description: string;
category: string;
}>;
}
function OfferListWidget() {
const theme = useOpenAiGlobal('theme');
const data = useWidgetProps<OfferData>();
return (
<div className={`offer-list ${theme}`}>
{data?.offers?.map(offer => (
<div key={offer.id} className="offer-card">
<div className="points">{offer.points} pts</div>
<div className="description">{offer.offer_description}</div>
<div className="category">{offer.category}</div>
</div>
))}
</div>
);
}
createRoot(document.getElementById('offer-root')!).render(<OfferListWidget />);
Terminal window
# Build widgets
cd widgets
pnpm run build
# Copy to Go package
cp dist/offer-list.html ../pkg/widgets/html/
cp dist/offer-map.html ../pkg/widgets/html/
# Rebuild Go binary (widgets are embedded via go:embed)
cd ..
go build ./cmd/mcp-server