Skip to content

Conversation

@Rodriguespn
Copy link
Contributor

Overview

This PR introduces UI widget support to mcp-lite, enabling developers to create interactive widgets similar to mcp-use. The implementation adds a new uiResource() API that automatically registers widgets as both tools (accepting parameters) and resources (serving the UI).

What's New

Core Changes

  • uiResource() method in McpServer class for registering interactive widgets
  • Support for three widget types:
    • externalUrl: Wraps external URLs in an iframe with props injection
    • rawHtml: Serves inline HTML directly (useful for simple widgets)
    • remoteDom: Executes scripts with a dedicated root element
  • Auto-registration as both tool and resource with proper URI scheme (ui://widget/...)
  • Props flow via URL query parameters and window.openai.toolInput injection

New Package: @mcp-lite/react

  • useWidget<TProps, TState>(): Main hook for widget development
  • useWidgetProps<TProps>(): Direct access to tool input props
  • useWidgetTheme(): Theme support (light/dark)
  • useWidgetState<TState>(): Persistent widget state management
  • Full TypeScript support with OpenAI Apps SDK compatibility
  • Reactive subscriptions using useSyncExternalStore

Example Implementation

  • Complete example in examples/react-widget/
  • Three widget demos: inline counter, weather widget, and task list
  • React widget app with Vite dev server
  • Comprehensive testing documentation

How to Test Manually

1. Start the MCP Server

cd examples/react-widget
bun install
bun run dev

The server will start on http://localhost:3002/mcp

2. Start the Widget App (for externalUrl testing)

cd examples/react-widget/widget-app
bun install
bun run dev

The widget app will start on http://localhost:5173/

3. Run Automated Tests

cd examples/react-widget
bun run test:manual

This will test:

  • Tool listing and discovery
  • Calling widgets with parameters
  • Resource resolution
  • Props injection verification
  • HTML rendering for all three widget types

4. Manual Testing Steps

Test Tool Call

curl -X POST http://localhost:3002/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "weather-widget",
      "arguments": {
        "location": "London",
        "temperature": 15
      }
    }
  }'

Expected: Returns a resource link like ui://widget/weather-widget-{uuid}.html?props=...

Test Resource Retrieval

# Replace {uuid} with actual UUID from tool call response
curl http://localhost:3002/mcp/resource/ui%3A%2F%2Fwidget%2Fweather-widget-{uuid}.html%3Fprops%3D...

Expected: Returns HTML with embedded iframe and props injection

Test in Browser

  1. Open the resource URL in a browser
  2. Verify the widget loads with correct props
  3. Check theme support (light/dark)
  4. Test interactive features (buttons, API calls)

5. Verify React Integration

Open examples/react-widget/widget-app/src/WeatherWidget.tsx to see:

  • useWidget() hook usage
  • Props destructuring
  • Theme integration
  • callTool() and sendFollowUpMessage() examples
  • State management

Testing Documentation

See examples/react-widget/TESTING.md for:

  • Detailed test scenarios
  • Debugging tips
  • Browser-based testing
  • End-to-end integration tests

Breaking Changes

None. This is a new experimental feature that doesn't affect existing APIs.

Notes

  • The uiResource() API is marked as experimental
  • OpenAI Apps SDK compatibility layer is included for future ChatGPT integration
  • All linting and type-checking passes
  • Examples use Bun runtime but work with any modern runtime

Checklist

  • Implementation complete
  • Examples provided
  • Testing guide written
  • Linting passes
  • Type-checking passes
  • Manual testing performed
  • Documentation updated

Co-authored-by: @Rodriguespn

- Add uiResource() method to McpServer for registering interactive UI widgets
- Support three widget types: externalUrl, rawHtml, and remoteDom
- Create @mcp-lite/react package with useWidget hook and OpenAI Apps SDK compatibility
- Add comprehensive example with weather widget demonstrating React integration
- Include manual testing guide and automated test script
- Auto-register widgets as both tools (with params) and resources (static + dynamic)
- Props flow via URL query params and window.openai.toolInput injection
@changeset-bot
Copy link

changeset-bot bot commented Nov 3, 2025

⚠️ No Changeset found

Latest commit: afe6a6a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Rodriguespn
Copy link
Contributor Author

@laulauland Could you please review this PR? 🙏

This adds UI widget support with a new uiResource() API and @mcp-lite/react package for building interactive widgets similar to mcp-use.

Key features:

  • Three widget types: externalUrl, rawHtml, remoteDom
  • React hooks with OpenAI Apps SDK compatibility
  • Complete example with weather widget
  • Comprehensive testing guide

Let me know if you have any questions!

- Fix tsconfig.json to use proper bundler module resolution
- Add skipLibCheck to avoid bun-types conflicts
- Fix optional chaining in test-manual.ts script
@laulauland
Copy link
Member

laulauland commented Nov 5, 2025

Thanks for the detailed PR and examples! I appreciate the thought that went into this, and I understand the pain of having to wrangle all the boilerplate to wire up UI resources with MCP tools. Making this easier is a good idea.

My main concern is adding a significant API surface area to packages/core that isn't part of the official MCP spec. The uiResource() method and all the widget rendering logic (~280 lines) introduce concepts like:

  • The ui:// URI scheme
  • OpenAI-specific conventions (window.openai.toolInput)
  • HTML templating that are host-specific extensions

These are host-specific extensions rather than core MCP protocol features.

One of mcp-lite's core features is staying lightweight and focused exclusively on the official MCP specification in the core package, then using auxiliary packages to augment functionality. This keeps the core lean and makes it easier to maintain as the spec evolves.

I think we can achieve the same developer experience while keeping this separation.

Option 1: Separate Widget Package

Create a new @mcp-lite/widgets-openai package that provides the uiResource() functionality:

import { McpServer } from 'mcp-lite';
import { registerOpenAIWidget } from '@mcp-lite/widgets-openai';

const server = new McpServer({ name: "app", version: "1.0.0" });

// All the widget registration logic lives in the separate package
registerOpenAIWidget(server, {
  type: 'externalUrl',
  name: 'weather-widget',
  url: 'http://localhost:5173',
  inputSchema: { ... }
});
  • Core stays pure MCP spec
  • Widget helpers use server.tool() and server.resource() under the hood
  • @mcp-lite/react can depend on @mcp-lite/widgets-openai for React-specific hooks
  • Future platforms can have their own widget packages if conventions differ

Option 2: Just show examples

Alternatively, keep @mcp-lite/react minimal (just useWidget() hooks), and move the full implementation pattern to examples/openai-widgets/ with a helper file.

Ideally btw the example should demonstrate bundling and serving the React app assets directly from the MCP server, rather than requiring a separate deployment. This gives developers a much better experience—everything lives in one application.

For example:

// examples/openai-widgets/src/index.ts
import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import { McpServer } from 'mcp-lite';
import { registerOpenAIWidget } from './widget-helpers.js';

const app = new Hono();
const server = new McpServer({ name: "app", version: "1.0.0" });

// Serve built React assets from ./dist/widgets
app.use('/widgets/*', serveStatic({ root: './dist' }));

// Register widget pointing to locally-served assets
registerOpenAIWidget(server, {
  type: 'externalUrl',
  name: 'weather-widget',
  url: 'http://localhost:3000/widgets/weather/index.html', // Served by same server
  inputSchema: { ... }
});

// MCP endpoint
app.all('/mcp', /* ... */);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants