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.
Overview
Section titled “Overview”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
_metafields with widget URIs - Phase 2: Widget HTML bundles served via MCP resource handlers
- Phase 3 (Optional): Production React widget bundles
What Was Implemented
Section titled “What Was Implemented”Tool Response Structure
Section titled “Tool Response Structure”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 }}Widget Resources
Section titled “Widget Resources”The server registers two widget resources:
- ui://widget/offer-list.html - Card-based list view for offers
- 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.
Unit & Integration Tests
Section titled “Unit & Integration Tests”All tests pass successfully:
# Run widget package testsgo test -v ./pkg/widgets
# Run all unit testsgo test ./cmd/... ./pkg/...Test Coverage
Section titled “Test Coverage”- ✅ Widget registry validation (2 widgets registered)
- ✅ Widget HTML embedding via
go:embed - ✅ MCP resource registration
- ✅
_metafield generation for both widgets - ✅ Tool response JSON marshaling with
_meta - ✅ HTML structure validation (DOCTYPE, window.openai, etc.)
Manual Testing with MCP Inspector
Section titled “Manual Testing with MCP Inspector”The MCP Inspector is an interactive tool for testing MCP servers.
# 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-mcpgo run ./cmd/mcp-server
# Run MCP Inspector (in another terminal)npx @modelcontextprotocol/inspector http://localhost:8080Test Steps
Section titled “Test Steps”-
List Resources - Verify widgets are registered:
Method: resources/listExpected: 2 resources (offer-list.html, offer-map.html) -
Read Widget Resource - Fetch widget HTML:
Method: resources/readParams: { "uri": "ui://widget/offer-list.html" }Expected: HTML content with window.openai references -
Call search_offers Tool - Verify _meta in response:
Method: tools/callParams: {"name": "search_offers","arguments": {"query": "coffee","user_id": "test-user-123"}}Expected: Response contains _meta with openai/outputTemplate -
Verify Widget URI - Check that _meta.openai/outputTemplate matches registered resource URI
Testing with ChatGPT Developer Mode
Section titled “Testing with ChatGPT Developer Mode”Prerequisites
Section titled “Prerequisites”- ChatGPT Plus or Team subscription
- Developer mode enabled in ChatGPT settings
- ngrok or similar tunneling tool for local testing
Setup with ngrok
Section titled “Setup with ngrok”# Install ngrok (if needed)brew install ngrok
# Start rover-mcp servergo run ./cmd/mcp-server
# Create tunnel to localhost:8080ngrok http 8080
# Note the ngrok URL (e.g., https://abc123.ngrok-free.app)Configure ChatGPT
Section titled “Configure ChatGPT”-
Open ChatGPT Settings → Developer Mode
-
Add MCP Connector:
- Name: rover-mcp
- URL:
https://your-ngrok-id.ngrok-free.app - Authentication: Bearer token from AWS Secrets Manager
-
Test the integration:
User: "Find coffee offers for user 12345"Expected: ChatGPT calls search_offers and renders offer-list widget
Expected Behavior
Section titled “Expected Behavior”When ChatGPT calls search_offers:
- Tool Execution: Server returns JSON data + _meta
- Widget Discovery: ChatGPT reads
_meta.openai/outputTemplate - Resource Fetch: ChatGPT calls
ReadResource("ui://widget/offer-list.html") - Widget Render: ChatGPT displays widget in iframe with
window.openaipopulated
The widget will:
- Read offer data from
window.openai.toolOutput - Adapt to theme (
window.openai.theme) - Display offers in card layout
Debugging Widget Issues
Section titled “Debugging Widget Issues”Check Widget HTML
Section titled “Check Widget HTML”# Verify widgets are embedded correctlygo run ./cmd/mcp-server 2>&1 | grep -i widget
# View widget HTML directlycat pkg/widgets/html/offer-list.htmlcat pkg/widgets/html/offer-map.htmlVerify Resource Registration
Section titled “Verify Resource Registration”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))Check Tool Response
Section titled “Check Tool Response”Test tool responses include _meta:
# Start server with debug loggingLOG_LEVEL=debug go run ./cmd/mcp-server
# Make tool call via HTTPcurl -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/outputTemplateWidget Development (Phase 3 - Optional)
Section titled “Widget Development (Phase 3 - Optional)”The current widgets are vanilla JavaScript placeholders. To create production React widgets:
Setup Widget Build Environment
Section titled “Setup Widget Build Environment”cd rover-mcpmkdir widgets && cd widgets
# Initialize npm projectpnpm init
# Install dependenciespnpm add react react-dompnpm add -D vite @vitejs/plugin-react typescriptCreate Widget Component
Section titled “Create Widget Component”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 />);Build and Deploy
Section titled “Build and Deploy”# Build widgetscd widgetspnpm run build
# Copy to Go packagecp 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