MCP Apps SDK Integration Analysis
MCP Apps SDK Integration Analysis
Section titled “MCP Apps SDK Integration Analysis”Executive Summary
Section titled “Executive Summary”The rover-mcp service already uses the mark3labs/mcp-go SDK (v0.31.0), which fully supports the OpenAI Apps SDK requirements. The Go SDK provides:
CallToolResultwith embeddedResult.Metafield for_metaresponsesAddResource()andAddResourceTemplate()methods for widget serving- Resource handlers via
ResourceHandlerFunc
Key Finding: We can keep the Go implementation - we just need to add resource handlers and enhance tool responses with _meta fields.
Current Implementation vs Pizzaz Server
Section titled “Current Implementation vs Pizzaz Server”✅ What We Already Have
Section titled “✅ What We Already Have”| Feature | Pizzaz (TypeScript) | rover-mcp (Go) | Status |
|---|---|---|---|
| MCP Protocol | @modelcontextprotocol/sdk v0.5.0 | mark3labs/mcp-go v0.31.0 | ✅ Compatible |
| Tool Registration | server.setRequestHandler(ListToolsRequestSchema) | srv.server.AddTool() | ✅ Implemented |
| Tool Execution | server.setRequestHandler(CallToolRequestSchema) | Tool handler funcs | ✅ Implemented |
| HTTP Transport | SSE over HTTP | HTTP with auth | ✅ Implemented |
| Health Endpoint | Not shown | /health | ✅ Implemented |
❌ What We Need to Add
Section titled “❌ What We Need to Add”| Feature | Pizzaz (TypeScript) | rover-mcp (Go) | Status |
|---|---|---|---|
| Resource Listing | server.setRequestHandler(ListResourcesRequestSchema) | Need to add | ❌ Missing |
| Resource Reading | server.setRequestHandler(ReadResourceRequestSchema) | Need to add | ❌ Missing |
| Resource Templates | server.setRequestHandler(ListResourceTemplatesRequestSchema) | Need to add | ❌ Missing |
| _meta in Responses | Included in tool results | Need to add | ❌ Missing |
| structuredContent | Returned from tools | Need to add | ⚠️ Partial |
API Schema Comparison
Section titled “API Schema Comparison”Tool Response Format
Section titled “Tool Response Format”Pizzaz Server (TypeScript)
Section titled “Pizzaz Server (TypeScript)”// pizzaz_server_node/src/server.ts:213-224return { content: [{ type: "text", text: widget.responseText }], structuredContent: { pizzaTopping: args.pizzaTopping }, _meta: { "openai/outputTemplate": widget.templateUri, "openai/toolInvocation/invoking": widget.invoking, "openai/toolInvocation/invoked": widget.invoked, "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true }};rover-mcp Current (Go)
Section titled “rover-mcp Current (Go)”// cmd/mcp-server/main.go:336jsonBytes, err := json.Marshal(results)return mcp.NewToolResultText(string(jsonBytes)), nilrover-mcp Proposed (Go)
Section titled “rover-mcp Proposed (Go)”// Proposed enhancementreturn &mcp.CallToolResult{ Result: mcp.Result{ Meta: map[string]any{ "openai/outputTemplate": "ui://widget/offer-list.html", "openai/toolInvocation/invoking": "Searching for offers...", "openai/toolInvocation/invoked": "Found offers!", "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, }, }, Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Found %d offers matching '%s'", len(results), query), }, },}, nilResource Definition
Section titled “Resource Definition”Pizzaz Server (TypeScript)
Section titled “Pizzaz Server (TypeScript)”// pizzaz_server_node/src/server.ts:143-149const resources: Resource[] = widgets.map((widget) => ({ uri: widget.templateUri, // "ui://widget/pizza-map.html" name: widget.title, // "Show Pizza Map" description: `${widget.title} widget markup`, mimeType: "text/html+skybridge", _meta: widgetMeta(widget)}));rover-mcp Proposed (Go)
Section titled “rover-mcp Proposed (Go)”// Proposed implementationwidgets := []Widget{ { URI: "ui://widget/offer-list.html", Name: "Offer List", Description: "Display offers in a card list", MIMEType: "text/html+skybridge", HTML: loadWidgetHTML("offer-list.html"), },}
for _, widget := range widgets { srv.server.AddResource( mcp.Resource{ URI: widget.URI, Name: widget.Name, Description: widget.Description, MIMEType: widget.MIMEType, }, func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: widget.URI, MIMEType: widget.MIMEType, Text: widget.HTML, }, }, nil }, )}Resource Reading
Section titled “Resource Reading”Pizzaz Server (TypeScript)
Section titled “Pizzaz Server (TypeScript)”// pizzaz_server_node/src/server.ts:177-194server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const widget = widgetsByUri.get(request.params.uri);
return { contents: [{ uri: widget.templateUri, mimeType: "text/html+skybridge", text: widget.html, _meta: widgetMeta(widget) }] };});rover-mcp Proposed (Go)
Section titled “rover-mcp Proposed (Go)”// ResourceHandlerFunc automatically called by SDKfunc makeResourceHandler(widget Widget) server.ResourceHandlerFunc { return func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: widget.URI, MIMEType: widget.MIMEType, Text: widget.HTML, }, }, nil }}Current rover-mcp Tools
Section titled “Current rover-mcp Tools”1. search_offers
Section titled “1. search_offers”Current Response:
// Returns JSON array of offersreturn mcp.NewToolResultText(string(jsonBytes)), nilProposed Enhanced Response:
return &mcp.CallToolResult{ Result: mcp.Result{ Meta: map[string]any{ "openai/outputTemplate": "ui://widget/offer-list.html", "openai/toolInvocation/invoking": "Searching for offers...", "openai/toolInvocation/invoked": fmt.Sprintf("Found %d offers!", len(results)), "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, }, }, Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Found %d offers matching '%s' for user %s", len(results), query, userID), }, },}, nilWidget Data Structure:
{ "offers": [...], "query": "coffee", "userID": "123", "totalResults": 15}2. search_nearby_offers
Section titled “2. search_nearby_offers”Current Response:
return mcp.NewToolResultText(string(jsonBytes)), nilProposed Enhanced Response:
return &mcp.CallToolResult{ Result: mcp.Result{ Meta: map[string]any{ "openai/outputTemplate": "ui://widget/offer-map.html", "openai/toolInvocation/invoking": "Mapping nearby offers...", "openai/toolInvocation/invoked": "Mapped offers near you!", "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, }, }, Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: fmt.Sprintf("Found %d retailers with matching offers nearby", len(results)), }, },}, nilWidget Data Structure:
{ "retailers": [...], "offerDetails": [...], "userLocation": {"latitude": 37.7749, "longitude": -122.4194}, "query": "coffee"}3. Other Tools (llm_feedback, fetch_webpage, web_search, get_user_purchase_history, search_products)
Section titled “3. Other Tools (llm_feedback, fetch_webpage, web_search, get_user_purchase_history, search_products)”These tools can remain text-only or optionally add widgets later:
- llm_feedback: Keep as-is (no widget needed)
- fetch_webpage: Could add a reader widget for formatted display
- web_search: Could add search results widget
- get_user_purchase_history: Could add purchase timeline widget
- search_products: Could add product grid widget
Implementation Roadmap
Section titled “Implementation Roadmap”Phase 1: Enhance Tool Responses (No Breaking Changes)
Section titled “Phase 1: Enhance Tool Responses (No Breaking Changes)”Goal: Add _meta fields to tool responses while maintaining backward compatibility.
Changes:
- Update
search_offersto returnCallToolResultwith_meta - Update
search_nearby_offersto returnCallToolResultwith_meta - Define widget URIs as constants
Code Location: cmd/mcp-server/main.go
Effort: 2-3 hours
Example:
// Constants for widget URIsconst ( widgetOfferList = "ui://widget/offer-list.html" widgetOfferMap = "ui://widget/offer-map.html")
// In search_offers tool handlerreturn &mcp.CallToolResult{ Result: mcp.Result{ Meta: map[string]any{ "openai/outputTemplate": widgetOfferList, "openai/toolInvocation/invoking": "Searching for offers...", "openai/toolInvocation/invoked": "Found offers!", "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, }, }, Content: []mcp.Content{ mcp.TextContent{ Type: "text", Text: string(jsonBytes), // Keep existing JSON for backward compat }, },}, nilPhase 2: Add Resource Handlers
Section titled “Phase 2: Add Resource Handlers”Goal: Implement MCP resource protocol to serve widget HTML/JS/CSS.
Changes:
- Create
internal/widgets/package - Define
Widgetstruct and registry - Implement resource handlers
- Add
go:embedfor widget files (or CDN URLs)
Code Location: New internal/widgets/widgets.go
Effort: 4-6 hours
Example:
package widgets
import ( _ "embed" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server")
//go:embed html/offer-list.htmlvar offerListHTML string
//go:embed html/offer-map.htmlvar offerMapHTML string
type Widget struct { URI string Name string Description string MIMEType string HTML string}
var Widgets = []Widget{ { URI: "ui://widget/offer-list.html", Name: "Offer List", Description: "Display offers in a card list", MIMEType: "text/html+skybridge", HTML: offerListHTML, }, { URI: "ui://widget/offer-map.html", Name: "Offer Map", Description: "Display offers on an interactive map", MIMEType: "text/html+skybridge", HTML: offerMapHTML, },}
func RegisterAll(srv *server.MCPServer) { for _, widget := range Widgets { w := widget // Capture for closure srv.AddResource( mcp.Resource{ URI: w.URI, Name: w.Name, Description: w.Description, MIMEType: w.MIMEType, }, func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: w.URI, MIMEType: w.MIMEType, Text: w.HTML, }, }, nil }, ) }}Usage in main.go:
import "rover-mcp/internal/widgets"
// After creating MCP serversrv := NewMCPServer()
// Register all widgets as resourceswidgets.RegisterAll(srv.server)
// Continue with tool registration...Phase 3: Create Widget Bundles
Section titled “Phase 3: Create Widget Bundles”Goal: Build React widgets that render offer data from window.openai.toolOutput.
Changes:
- Create
widgets/directory in project root - Setup Vite + React build system
- Create offer list widget
- Create offer map widget
- Build and embed HTML/JS/CSS
Code Location: New widgets/ directory
Effort: 8-12 hours (depending on complexity)
Directory Structure:
rover-mcp/├── widgets/│ ├── package.json│ ├── vite.config.ts│ ├── tsconfig.json│ ├── src/│ │ ├── offer-list/│ │ │ └── index.tsx│ │ ├── offer-map/│ │ │ └── index.tsx│ │ ├── use-openai-global.ts│ │ ├── use-widget-props.ts│ │ └── types.ts│ └── dist/ # Generated bundles│ ├── offer-list.html│ └── offer-map.html└── internal/widgets/ └── html/ # Embedded widget files ├── offer-list.html (copied from dist) └── offer-map.html (copied from dist)Build Process:
cd widgetspnpm installpnpm run build # Outputs to dist/
# Copy to internal/widgets/html/cp dist/*.html ../internal/widgets/html/Example Widget (Simplified):
import React from "react";import { createRoot } from "react-dom/client";
interface OfferData { offers: Array<{ id: string; description: string; points: number; category: string; }>; query: string;}
function OfferListWidget() { // Access data from MCP server via window.openai const data = window.openai?.toolOutput as OfferData; const theme = window.openai?.theme || "light";
if (!data?.offers) { return <div>No offers found</div>; }
return ( <div className={`offer-list theme-${theme}`}> <h2>Found {data.offers.length} offers for "{data.query}"</h2> {data.offers.map((offer) => ( <div key={offer.id} className="offer-card"> <div className="offer-points">{offer.points} pts</div> <div className="offer-description">{offer.description}</div> </div> ))} </div> );}
createRoot(document.getElementById("offer-root")).render(<OfferListWidget />);Phase 4: Testing & Deployment
Section titled “Phase 4: Testing & Deployment”Goal: Validate Apps SDK integration in ChatGPT developer mode.
Changes:
- Test with MCP Inspector locally
- Test with ChatGPT developer mode via ngrok
- Deploy to dev/stage environments
- Update documentation
Effort: 4-6 hours
Breaking Changes Assessment
Section titled “Breaking Changes Assessment”None - Fully Backward Compatible!
Section titled “None - Fully Backward Compatible!”The proposed changes are 100% backward compatible because:
-
Existing clients that don’t support Apps SDK will:
- Ignore the
_metafield (per MCP spec) - Continue to receive
Content[]with text/JSON - Work exactly as they do today
- Ignore the
-
Apps SDK clients will:
- Read
_meta.openai/outputTemplate - Request widget via
ReadResource - Render widget with data from
Content
- Read
-
No schema changes to existing tools:
- All input parameters stay the same
- All output data stays the same
- Only adding optional
_metafield
Key Differences: TypeScript vs Go
Section titled “Key Differences: TypeScript vs Go”| Aspect | TypeScript (Pizzaz) | Go (rover-mcp) | Notes |
|---|---|---|---|
| SDK | @modelcontextprotocol/sdk | mark3labs/mcp-go | Both official MCP implementations |
| Transport | SSE via SSEServerTransport | HTTP via StreamableHTTPServer | Both support MCP over HTTP |
| Typing | Runtime (TypeScript) | Compile-time (Go) | Go provides stronger type safety |
| Resource Handlers | setRequestHandler() | AddResource() + handler func | Same capability, different API |
| Tool Results | Plain object with _meta | CallToolResult struct with Meta field | Same wire format |
| Widget Storage | In-memory constants | go:embed or CDN | Go can embed at compile time |
| Content Type | "text/html+skybridge" | "text/html+skybridge" | Same MIME type |
Recommended Approach
Section titled “Recommended Approach”Option A: Minimal Changes (Recommended)
Section titled “Option A: Minimal Changes (Recommended)”Pros:
- Keep all existing Go code
- Add
_metato 2 tools (search_offers, search_nearby_offers) - Add resource handlers for 2 widgets
- No new dependencies
- No TypeScript/Node.js required in deployment
Cons:
- Need to build widgets separately (one-time setup)
- Slightly more verbose syntax than TypeScript
Timeline: 2-3 days total
Option B: Full TypeScript Rewrite
Section titled “Option B: Full TypeScript Rewrite”Pros:
- Closer to Pizzaz example
- TypeScript tooling for widgets
Cons:
- Rewrite entire server (weeks of work)
- Lose existing integrations (auth, metrics, logging)
- Add Node.js to deployment
- Higher operational complexity
Timeline: 3-4 weeks total
Verdict: ❌ Not recommended
Next Steps
Section titled “Next Steps”- ✅ This Document: Review and approve approach
- 📝 Phase 1: Enhance tool responses (search_offers, search_nearby_offers)
- 🔧 Phase 2: Add resource handlers and widget registry
- 🎨 Phase 3: Build React widgets
- 🧪 Phase 4: Test in ChatGPT developer mode
Conclusion
Section titled “Conclusion”The rover-mcp Go server is fully compatible with OpenAI Apps SDK. The mark3labs/mcp-go SDK provides all necessary primitives:
- ✅
CallToolResult.Metafor_metaresponses - ✅
AddResource()for widget serving - ✅
ResourceHandlerFuncfor dynamic content - ✅ SSE/HTTP transport support
No rewrite needed - just enhance existing tools and add resource handlers for widgets!