npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mcp-ui-ext-apps-openai

v1.0.3

Published

Unified utility for building apps that work on both OpenAI ChatGPT and MCP Apps platforms

Readme

mcp-ui-ext-apps-openai

npm version GitHub

Unified utility for building apps that work on both OpenAI ChatGPT and MCP Apps platforms.

Author: lkm1developer

Installation

npm install mcp-ui-ext-apps-openai

Peer Dependencies

This package requires the following peer dependencies:

npm install @modelcontextprotocol/ext-apps react

Usage

React Hook (Recommended)

import { useUnifiedApp } from "mcp-ui-ext-apps-openai/react";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

// Define your structured data type
interface CounterData {
  status: boolean;
  value?: number;
  error?: string;
}

function CounterApp() {
  const {
    app,
    isConnected,
    platform,
    hostContext,
    initialProps,
    widgetState,
    setWidgetState,
  } = useUnifiedApp({
    appInfo: { name: "Counter App", version: "1.0.0" },
    capabilities: {},
    onError: (e) => console.error("[CounterApp]", e),
  });

  // Get counter value from widget state
  const counter = (widgetState as { value?: number } | null)?.value ?? 0;

  // Fetch counter from server
  const fetchCounter = async () => {
    if (!app) return;

    const result = await app.callServerTool({ name: "get-counter", arguments: {} });
    const data = (result as CallToolResult).structuredContent as unknown as CounterData;

    if (data.status && data.value !== undefined) {
      setWidgetState({ value: data.value });
    }
  };

  // Initialize from initialProps (OpenAI) or fetch from server (MCP)
  useEffect(() => {
    if (!app || !isConnected) return;

    if (initialProps !== undefined) {
      const data = (initialProps as CallToolResult).structuredContent as unknown as CounterData;
      if (data?.status && data.value !== undefined) {
        setWidgetState({ value: data.value });
        return;
      }
    }

    fetchCounter();
  }, [app, isConnected, initialProps]);

  // Increment counter
  const handleIncrement = async () => {
    if (!app) return;

    const newValue = counter + 1;
    setWidgetState({ value: newValue }); // Optimistic update

    const result = await app.callServerTool({
      name: "set-counter",
      arguments: { value: newValue }
    });
    const data = (result as CallToolResult).structuredContent as unknown as CounterData;

    if (!data.status) {
      setWidgetState({ value: counter }); // Revert on error
    }
  };

  return (
    <div>
      <p>Platform: {platform}</p>
      <p>Theme: {hostContext?.theme}</p>
      <p>Counter: {counter}</p>
      <button onClick={handleIncrement}>+</button>
      <button onClick={fetchCounter}>Refresh</button>
    </div>
  );
}

Core Utilities (No React)

import { detectPlatform, isOpenAI, isMCP, createUnifiedApp } from "mcp-ui-ext-apps-openai";

// Check platform
const platform = detectPlatform(); // "openai" | "mcp" | "unknown"

if (isOpenAI()) {
  console.log("Running on OpenAI ChatGPT");
} else if (isMCP()) {
  console.log("Running on MCP Apps");
}

// Create unified app (for OpenAI only - use React hook for MCP)
const { app, isConnected, error } = createUnifiedApp({
  appInfo: { name: "My App", version: "1.0.0" },
  onError: console.error,
});

if (app && isConnected) {
  const result = await app.callServerTool({ name: "my-tool", arguments: {} });
}

API Reference

useUnifiedApp(options)

React hook that provides a unified app interface for both platforms.

Options

| Option | Type | Description | |--------|------|-------------| | appInfo | { name: string; version: string } | App information | | capabilities | McpUiAppCapabilities | Optional capabilities | | onToolInput | (input: unknown) => void | Called when tool input is received | | onToolResult | (result: UnifiedToolResult) => void | Called when tool result is received | | onHostContextChanged | (context: UnifiedHostContext) => void | Called when host context changes | | onTeardown | () => void | Called when app is being torn down | | onError | (error: Error) => void | Called on errors (also logs to console) |

Returns

| Property | Type | Description | |----------|------|-------------| | app | UnifiedApp \| null | The unified app instance | | isConnected | boolean | Whether connected to host | | error | Error \| null | Any connection error | | platform | "openai" \| "mcp" \| "unknown" | Detected platform | | hostContext | UnifiedHostContext | Current host context (theme, locale, etc.) | | initialProps | unknown | Initial props from toolOutput (OpenAI only) | | widgetProps | Record<string, unknown> | Widget props (OpenAI only) | | widgetState | unknown | Widget state (works on both platforms) | | setWidgetState | (state: T) => void | Set widget state | | updateWidgetState | (state: Partial<T>) => void | Partially update widget state |

UnifiedApp Interface

| Method | Description | |--------|-------------| | callServerTool({ name, arguments }) | Call a server tool | | sendMessage(message, options?) | Send a message to the host | | sendLog({ level, data }) | Send a log message | | openLink({ url }) | Open an external link | | getHostContext() | Get current host context | | setWidgetState(state) | Set widget state (OpenAI only) | | updateWidgetState(state) | Update widget state partially (OpenAI only) | | requestDisplayMode(mode) | Request display mode (OpenAI only) | | requestClose() | Request to close widget (OpenAI only) | | uploadFile(file) | Upload a file (OpenAI only) | | getFileDownloadUrl({ fileId }) | Get file download URL (OpenAI only) |

Platform-Specific Behavior

| Feature | OpenAI | MCP | |---------|--------|-----| | widgetState | Syncs to OpenAI + React state | React state only | | initialProps | From toolOutput | undefined | | widgetProps | From widget.props | {} | | uploadFile | Works | Throws error | | requestDisplayMode | Works | No-op | | callCompletion | Works | Throws error |

Structured Tool Data

Tools should return structured data in structuredContent:

// Server tool response format
{
  status: true,
  value: 42,
  // or on error:
  // status: false,
  // error: "Something went wrong"
}

// Access in client
const result = await app.callServerTool({ name: "get-counter", arguments: {} });
const data = (result as CallToolResult).structuredContent as unknown as CounterData;

if (data.status) {
  console.log("Value:", data.value);
} else {
  console.error("Error:", data.error);
}

License

MIT

Examples

The examples/ directory contains a complete counter app example:

Structure

examples/
├── server/          # MCP server with counter tools
│   ├── server.ts    # Express + MCP server
│   └── package.json
└── ui/              # React counter app
    ├── src/main.tsx # Counter UI using useUnifiedApp
    └── package.json

Server Example (examples/server/)

A fully-featured MCP server that:

  • Auto-detects OpenAI vs MCP Apps clients via user-agent
  • Uses proper MIME types (text/html+skybridge for OpenAI, standard for MCP)
  • Registers resources with registerAppResource
  • Registers tools with registerAppTool
  • Manages sessions (stateful, 5 min timeout)

Tools:

  • counter - Widget tool with resource binding
  • get-counter - Returns { status: true, value: number }
  • set-counter - Accepts { value: number }, returns { status: true, value: number }

Run:

cd examples/server
npm install
npm run dev  # Starts on http://localhost:3001

UI Example (examples/ui/)

A React counter app that:

  • Uses useUnifiedApp hook for cross-platform support
  • Initializes from initialProps (OpenAI) or fetches via get-counter (MCP)
  • Shows counter with +/- buttons
  • Optimistically updates UI, reverts on error
  • Persists changes via set-counter tool

Run:

cd examples/ui
npm install
npm run build  # Builds to dist/index.html

The built dist/index.html is served by the server as the widget HTML resource.

Running the Full Example

  1. Build the UI:

    cd examples/ui && npm install && npm run build
  2. Start the server:

    cd examples/server && npm install && npm run dev
  3. Connect from OpenAI ChatGPT or MCP Apps client to http://localhost:3001/mcp