Skip to content

Conversation

@frontegg-david
Copy link
Contributor

@frontegg-david frontegg-david commented Jan 27, 2026

Summary by CodeRabbit

  • New Features

    • MCP Apps (ext-apps) support: bidirectional host↔widget messaging plus public methods for model-context updates, logging, and dynamic tool registration/unregistration; display modes supported.
  • Documentation

    • Extensive ext-apps docs, examples, platform compatibility, MIME-type guidance, and integration notes.
  • Improvements

    • Hardened handshake, origin-trust, platform detection, host capability validation, MIME utilities, and new UI config fields (resourceUri, widgetCapabilities).
  • Tests

    • Wide-ranging unit/integration tests for handlers, types, adapters, flows, and platform metadata.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Adds MCP Apps (ext-apps, SEP-1865): new protocol types, JSON‑RPC handler, adapter and bridge methods (model-context, logging, tool register/unregister), MIME/display-mode utilities, UI config/schema fields, transport flow integration, extensive tests, and documentation updates.

Changes

Cohort / File(s) Summary
Ext‑apps types & handler
libs/sdk/src/ext-apps/ext-apps.types.ts, libs/sdk/src/ext-apps/ext-apps.handler.ts, libs/sdk/src/ext-apps/index.ts
New protocol type definitions, JSON‑RPC request/response/notification types, centralized error codes, and a typed ExtAppsMessageHandler with routing, capability checks, error classes, and factory export.
SDK exports & metadata
libs/sdk/src/index.ts, libs/sdk/src/ext-apps/README.md, libs/sdk/src/common/metadata/front-mcp.metadata.ts, libs/sdk/src/common/tokens/front-mcp.tokens.ts
Re-exports for ext-apps, new README, metadata/schema and token additions to surface extApps config in front-mcp metadata.
SDK options/schema & types
libs/sdk/src/common/types/options/ext-apps/*, libs/sdk/src/common/types/options/index.ts
New ext-apps options schema and interfaces, barrel exports exposing ext-apps options and host capabilities.
SDK transport flow & tool integration
libs/sdk/src/transport/flows/handle.streamable-http.flow.ts, libs/sdk/src/tool/flows/tools-list.flow.ts
Adds extApps request routing and onExtApps plan stage, session/transport handling, callTool integration for tool execution, and tools-list flow honoring resourceUri and emitting ui/capabilities.
UI bridge types & adapters
libs/ui/src/bridge/types.ts, libs/ui/src/bridge/adapters/ext-apps.adapter.ts, libs/ui/src/bridge/adapters/claude.adapter.ts
Host capability flags added; ext-apps and Claude detection hardened; handshake validation; adapter methods added: updateModelContext, log, registerTool, unregisterTool; new ExtAppsNotSupportedError gating.
Front runtime / IIFE bridge
libs/uipack/src/bridge-runtime/iife-generator.ts, libs/ui/src/bridge/__tests__/iife-generator.test.ts
IIFE/runtime: origin-trust guard, handshake validation, new notification handlers (tool-input-partial, cancelled), FrontMcpBridge methods exposed for ext-apps; related tests updated.
UIpack platform metadata & MIME/display utilities
libs/uipack/src/adapters/platform-meta.ts, libs/uipack/src/adapters/platform-meta.constants.ts, libs/uipack/src/adapters/index.ts, libs/uipack/src/adapters/__tests__/platform-meta.test.ts
Adds getExtAppsMimeType/isExtAppsMimeType, ExtAppsDisplayMode and DISPLAY_MODE_MAP/mapDisplayMode, extends discovery metadata (ui/resourceUri, ui/csp, mimeType) and tests.
UIpack UI config types
libs/uipack/src/types/ui-config.ts
Adds resourceUri?: string and widgetCapabilities?: { toolListChanged?: boolean; supportsPartialInput?: boolean } to UITemplateConfig.
UI adapter tests
libs/ui/src/bridge/adapters/__tests__/ext-apps.adapter.test.ts
New adapter tests covering detection rules, Claude/OpenAI exclusions, handshake, host capability gating, and extended method behavior.
Ext‑apps tests (SDK)
libs/sdk/src/ext-apps/__tests__/*
New comprehensive tests for handler behavior, types, error codes, transport flow integration, and schema/plan validation.
UIpack platform constants tests
libs/uipack/src/adapters/__tests__/platform-meta.constants.test.ts
Tests for DISPLAY_MODE_MAP and mapDisplayMode behavior and aliases.
Docs & API reference
docs/draft/docs/ui/advanced/mcp-bridge.mdx, docs/draft/docs/ui/advanced/platforms.mdx, docs/draft/docs/ui/api-reference/runtime.mdx, docs/draft/docs/ui/integration/tools.mdx
Documentation and API reference additions for ext-apps methods on MCPBridge, platform compatibility, MIME utilities, widget capabilities, examples, and platform notes.

Sequence Diagram(s)

sequenceDiagram
    participant W as Widget (ext-app)
    participant I as IIFE / ExtAppsAdapter
    participant H as ExtAppsMessageHandler
    participant B as Bridge / HostContext
    participant S as Server / Tool runtime

    W->>I: postMessage(JSON-RPC request e.g. ui/callServerTool)
    I->>I: validate origin / handshake
    I->>H: handleRequest(request)
    H->>H: route method, validate hostCapabilities
    H->>B: delegate operation (callTool / updateModelContext / registerTool / ...)
    B->>S: execute operation (tool run, update state, open link)
    S-->>B: result or error
    B-->>H: return result
    H->>I: JSON-RPC response or error
    I->>W: postMessage(response)

    S->>I: notification (ui/notifications/tool-input-partial or cancelled)
    I->>W: emit notification event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 In tiny hops across JSON streams,
I stitch new bridges, build new dreams.
Tools register, contexts flow,
Logs like carrots, softly glow —
Ext‑apps spring where MCP beams.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: Add MCP Apps (ext-apps) support' accurately and concisely summarizes the main change: introducing comprehensive support for the MCP Apps (ext-apps) protocol across documentation, SDK, and UI components.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 65.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/sdk/src/tool/flows/tools-list.flow.ts (1)

478-516: Scope resourceUri override to non-OpenAI platforms only.

resourceUri is designed for ext-apps/MCP Apps per SEP-1865 and should not override OpenAI's openai/outputTemplate. The buildToolDiscoveryMeta function (the reference implementation) does not apply resourceUri to OpenAI, and E2E tests validate that openai/outputTemplate uses only the auto-generated ui://widget/... format. Apply the suggested guard to ensure OpenAI always receives the auto-generated URI:

Guard to fix resourceUri scope
-          const widgetUri = uiConfig.resourceUri || `ui://widget/${encodeURIComponent(finalName)}.html`;
+          const defaultWidgetUri = `ui://widget/${encodeURIComponent(finalName)}.html`;
+          const widgetUri = isOpenAIPlatform ? defaultWidgetUri : (uiConfig.resourceUri ?? defaultWidgetUri);
🤖 Fix all issues with AI agents
In `@docs/draft/docs/ui/advanced/platforms.mdx`:
- Around line 149-158: The example discovery response uses a flattened key
"toolListChanged" but the SDK emits a nested shape under
"ui/capabilities.tools.listChanged"; update the JSON example so
"ui/capabilities" contains a "tools" object with a "listChanged" boolean (keep
"supportsPartialInput" as-is under "ui/capabilities") to match the actual
emitted shape (i.e., replace "toolListChanged" with "tools": { "listChanged":
... } under "ui/capabilities").

In `@docs/draft/docs/ui/integration/tools.mdx`:
- Around line 219-225: The example has a mismatch between
widgetCapabilities.toolListChanged and its comment: currently toolListChanged is
false but the comment says the widget can emit tool list changes; update the
example by either setting toolListChanged: true if the intent is to indicate the
widget can emit tool list changes, or change the comment to "Widget cannot emit
tool list changes" to match toolListChanged: false; locate this in the ui: {
template: MyWidget, widgetCapabilities: { ... } } block and make the value and
comment consistent.

In `@libs/sdk/src/ext-apps/README.md`:
- Around line 148-172: The README example imports ExtAppsMessageHandler and
createExtAppsMessageHandler from a non-exported subpath; update the import to
pull these symbols from the package main entry instead (import
ExtAppsMessageHandler and createExtAppsMessageHandler from '@frontmcp/sdk'),
since ext-apps is re-exported via the main index and the subpath
'@frontmcp/sdk/ext-apps' is not configured in package.json.

In `@libs/ui/src/bridge/adapters/__tests__/ext-apps.adapter.test.ts`:
- Around line 1-185: Add unit tests in this file for the four public
ExtAppsAdapter methods updateModelContext, log, registerTool, and
unregisterTool: for each method include success-path tests that simulate the
host exposing the corresponding capability on the window (e.g., mock the host
API the adapter calls so updateModelContext resolves,
registerTool/unregisterTool call succeed, and log calls the host logger),
error-path tests where the host capability is absent or throws (assert the
adapter throws or returns the expected error), and for log include a test where
host logging is unavailable so the method falls back to console logging; locate
the adapter via ExtAppsAdapter and mock window properties/host functions used
inside updateModelContext, log, registerTool, and unregisterTool to trigger each
branch.

In `@libs/uipack/src/adapters/platform-meta.constants.ts`:
- Around line 30-37: Add unit tests for the exported function mapDisplayMode to
cover standard modes, aliases and unknown inputs: assert that "inline",
"fullscreen", and "pip" map to the expected ExtAppsDisplayMode values (use the
same expected enum/constant values used by DISPLAY_MODE_MAP), assert that
aliases "widget" maps to the same value as "inline" and "panel" maps to the same
value as "fullscreen", and assert that unknown/invalid inputs (e.g.,
"unknownMode", empty string, and a non-matching value) return undefined; locate
mapDisplayMode and DISPLAY_MODE_MAP in platform-meta.constants.ts to reference
the exact expected outputs in your assertions.

In `@libs/uipack/src/bridge-runtime/iife-generator.ts`:
- Around line 367-376: The originTrustPending flag is set to true when
attempting to establish trustedOrigin but is never reset, which can block future
valid messages; update the logic around trustedOrigin and originTrustPending so
that originTrustPending is reset to false after the trust attempt completes —
set originTrustPending = false when trust is successfully established (after
assigning trustedOrigin) and also reset it on failure paths (e.g., when origin
is falsy or window.parent === window) or implement a short timeout fallback to
clear originTrustPending; modify the block handling
trustedOrigin/originTrustPending (the variables trustedOrigin and
originTrustPending in this module) accordingly.
🧹 Nitpick comments (8)
libs/sdk/src/ext-apps/README.md (1)

165-168: Consider adding type annotations to the handler context example.

The example shows callTool returning Promise<unknown> via the flows call, but the actual type information is lost. Consider showing the typed return for clarity.

libs/ui/src/bridge/adapters/__tests__/ext-apps.adapter.test.ts (1)

177-184: Incomplete SSR test - consider removing or implementing properly.

This test doesn't actually verify SSR behavior since jsdom always provides a window object. The assertion expect(typeof window).toBe('object') just confirms jsdom is working, not that the adapter handles SSR correctly.

Consider either:

  1. Removing this test (the behavior is implicitly tested by the code structure)
  2. Using a separate test environment without jsdom for true SSR testing
♻️ Option 1: Remove the incomplete test
-  describe('canHandle in SSR context', () => {
-    it('should return false when window is undefined', () => {
-      // We can't easily mock window being undefined in jsdom,
-      // but we can verify the first check in the method
-      expect(typeof window).toBe('object');
-      // The method should handle window being undefined gracefully
-    });
-  });
libs/ui/src/bridge/types.ts (1)

364-365: Import ExtAppsDisplayMode from SDK for consistency with ext-apps type definitions.

The displayModes field in ExtAppsInitializeResult uses the inline union ('inline' | 'fullscreen' | 'pip')[]. Since ExtAppsDisplayMode is exported from libs/sdk/src/ext-apps/ext-apps.types.ts with the identical definition, import and reuse it to maintain consistency across the codebase and with the SEP-1865 ext-apps protocol specification.

libs/sdk/src/ext-apps/__tests__/ext-apps.handler.test.ts (1)

310-345: Consider testing all log levels individually with assertions.

The loop at lines 320-330 iterates through log levels but doesn't verify that the correct logger method was called for each level. Consider adding explicit assertions.

♻️ Optional: Add explicit logger call verification
 for (const level of levels) {
   const response = await handler.handleRequest(
     createRequest('ui/log', {
       level,
       message: `Test ${level} message`,
       data: { extra: 'data' },
     }),
   );

   expect(response.error).toBeUndefined();
+  // Verify logger was called with correct level
+  expect(mockLogger[level]).toHaveBeenCalledWith(
+    expect.stringContaining(`Test ${level} message`),
+    expect.objectContaining({ extra: 'data' })
+  );
 }
libs/ui/src/bridge/adapters/ext-apps.adapter.ts (1)

240-246: Use ExtAppsNotSupportedError instead of generic Error for capability checks.

Per coding guidelines, adapters should use specific error classes with MCP error codes. Replace generic Error with ExtAppsNotSupportedError for better error categorization and consistency with the SDK pattern.

The error class is exported from @frontmcp/sdk and already used in the same pattern throughout libs/sdk/src/ext-apps/ext-apps.handler.ts. Apply this to all four capability check failures:

  • Line 196 in callTool(): Server tool proxy check
  • Line 242 in updateModelContext(): Model context update check
  • Line 281 in registerTool(): Widget tool registration check
  • Line 296 in unregisterTool(): Widget tool unregistration check

Add the import: import { ExtAppsNotSupportedError } from '@frontmcp/sdk'

libs/sdk/src/ext-apps/ext-apps.handler.ts (3)

136-151: Consider omitting stack traces in production error responses.

Line 148 exposes error.stack in JSON-RPC error responses. While useful for debugging, stack traces can leak internal implementation details to untrusted widgets.

♻️ Suggested approach
       return {
         jsonrpc: '2.0',
         id,
         error: {
           code: errorCode,
           message: errorMessage,
-          data: error instanceof Error ? { stack: error.stack } : undefined,
+          // Omit stack in production to avoid leaking internal details
+          data: process.env.NODE_ENV === 'development' && error instanceof Error 
+            ? { stack: error.stack } 
+            : undefined,
         },
       };

331-360: Consider validating log level against known values.

The handleLog method accepts any level value and falls through to info for unknown levels. While this is resilient, explicitly validating against ExtAppsLogLevel values would catch misuse early.

♻️ Optional: Add level validation
   private async handleLog(params: ExtAppsLogParams): Promise<void> {
     if (!this.hostCapabilities.logging) {
       throw new ExtAppsNotSupportedError('Logging not supported by host');
     }

     const { level, message, data } = params;

     if (!message || typeof message !== 'string') {
       throw new ExtAppsInvalidParamsError('Log message is required');
     }

+    const validLevels = ['debug', 'info', 'warn', 'error'];
+    if (level && !validLevels.includes(level)) {
+      throw new ExtAppsInvalidParamsError(`Invalid log level '${level}'. Must be one of: ${validLevels.join(', ')}`);
+    }

     const widgetLogger = this.logger.child(`Widget:${this.context.sessionId}`);

383-385: Consider stricter validation for inputSchema.

The check typeof inputSchema !== 'object' would pass for arrays and null. For a JSON Schema, you likely want a plain object.

♻️ Stricter schema validation
-    if (!inputSchema || typeof inputSchema !== 'object') {
+    if (!inputSchema || typeof inputSchema !== 'object' || Array.isArray(inputSchema)) {
       throw new ExtAppsInvalidParamsError('Tool input schema is required');
     }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@libs/sdk/src/ext-apps/__tests__/ext-apps.handler.test.ts`:
- Around line 19-33: The mockLogger in this test uses an unsafe `as any` cast
and is missing the `debug` method and the correct shape expected by
FrontMcpLogger; update the mock to include debug (jest.fn()) and make its shape
conform to FrontMcpLogger (or create a MockFrontMcpLogger type) and remove the
`as any` cast in createMockContext; specifically modify the mockLogger
declaration and then return it from createMockContext typed as FrontMcpLogger
(or the dedicated mock type) so ExtAppsHandlerContext receives a properly typed
logger without using `as any`, keeping identifiers mockLogger,
createMockContext, ExtAppsHandlerContext and FrontMcpLogger in mind.

In `@libs/sdk/src/ext-apps/ext-apps.handler.ts`:
- Around line 33-52: Make ExtAppsHandlerContext generic over a tool result type
(e.g. <TToolResult = unknown>) and change callTool from returning
Promise<unknown> to Promise<TToolResult>; then propagate that generic parameter
through ExtAppsMessageHandlerOptions, ExtAppsMessageHandler, and the
createExtAppsMessageHandler factory so handler implementations and callers can
type tool responses; update all referenced type signatures
(ExtAppsHandlerContext, ExtAppsMessageHandlerOptions, ExtAppsMessageHandler,
createExtAppsMessageHandler) to accept the TToolResult generic and use it for
callTool return types and any handler return types.
- Around line 159-195: routeMethod currently forwards possibly null/undefined
params to handlers which destructure them and cause TypeError that gets mapped
to INTERNAL_ERROR; add a small guard (e.g., isParamsObject(params) or
validateParamsObject) at the top of routeMethod to check that params is a
non-null object (and if needed an array check depending on handler expectations)
and if the check fails throw or return an MCP-compliant InvalidParams error
(INVALID_PARAMS) before switching to handlers; update routeMethod to call this
guard prior to the switch and mention the affected handlers (handleInitialize,
handleUpdateModelContext, handleOpenLink, handleSetDisplayMode, handleClose,
handleLog, handleRegisterTool, handleUnregisterTool) so they never receive
null/undefined params.
🧹 Nitpick comments (1)
libs/ui/src/bridge/adapters/ext-apps.adapter.ts (1)

530-552: Tighten initialize response validation to reject arrays.

hostCapabilities and hostContext should be objects, not arrays; adding Array.isArray checks prevents malformed responses from passing validation.

♻️ Suggested tweak
-    if (
-      r['hostCapabilities'] !== undefined &&
-      (typeof r['hostCapabilities'] !== 'object' || r['hostCapabilities'] === null)
-    ) {
+    if (
+      r['hostCapabilities'] !== undefined &&
+      (typeof r['hostCapabilities'] !== 'object' || r['hostCapabilities'] === null || Array.isArray(r['hostCapabilities']))
+    ) {
       return false;
     }
 
-    if (r['hostContext'] !== undefined && (typeof r['hostContext'] !== 'object' || r['hostContext'] === null)) {
+    if (
+      r['hostContext'] !== undefined &&
+      (typeof r['hostContext'] !== 'object' || r['hostContext'] === null || Array.isArray(r['hostContext']))
+    ) {
       return false;
     }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/sdk/src/transport/flows/handle.streamable-http.flow.ts (1)

147-154: Add runtime type guard before calling startsWith on method.

Optional chaining (?.) only protects against null and undefined, not non-string values. A malformed request with method: 123 or method: [] will crash with method.startsWith is not a function, causing a 500 error instead of a proper RPC error. Since httpInputSchema doesn't validate request body structure, runtime type safety is necessary.

Suggested fix
-    } else if (method?.startsWith('ui/')) {
+    } else if (typeof method === 'string' && method.startsWith('ui/')) {
       // Ext-apps methods: ui/initialize, ui/callServerTool, etc.
       this.state.set('requestType', 'extApps');
🤖 Fix all issues with AI agents
In `@libs/sdk/src/transport/flows/handle.streamable-http.flow.ts`:
- Around line 491-509: The callTool implementation currently returns the raw
result when CallToolResultSchema.safeParse fails; instead, follow the pattern in
call-tool-request.handler.ts and throw an InternalMcpError so ExtApps will
convert it to a JSON-RPC error. Update the callTool async function to run
CallToolResultSchema.safeParse(result) and if parsed.success is false, throw a
new InternalMcpError with a short message and include parse error details (and
optionally the original result) in the error metadata; keep the existing return
of parsed.data when successful.
🧹 Nitpick comments (2)
libs/sdk/src/common/types/options/ext-apps/schema.ts (1)

60-60: Consider extracting the displayModes enum values to a shared constant.

The literal values 'inline', 'fullscreen', 'pip' are duplicated across the codebase. While an ExtAppsDisplayMode type already exists in libs/sdk/src/ext-apps/ext-apps.types.ts, consider extracting the literal values themselves as a constant (e.g., const DISPLAY_MODES = ['inline', 'fullscreen', 'pip'] as const) that can be reused in this schema and other locations like ext-apps.handler.ts.

libs/sdk/src/transport/flows/__tests__/handle.streamable-http.ext-apps.test.ts (1)

68-96: Consider exercising router behavior instead of string primitives.

These checks only validate startsWith('ui/') rather than the actual router stage; a small helper or light router invocation would make this test more meaningful and resilient to routing logic changes.

@frontegg-david frontegg-david merged commit 6232c99 into main Jan 28, 2026
36 checks passed
@frontegg-david frontegg-david deleted the mcp-apps branch January 28, 2026 04:02
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