MCP AppsΒΆ
MCP Apps support lets ContextForge advertise MCP UI capabilities and enforce UI metadata while keeping the feature disabled by default for a controlled rollout. It introduces a narrow AppBridge path that MCP Hosts can use on behalf of browser-hosted MCP Apps to call app-visible tools without exposing normal model-facing tool surfaces or gateway-internal routing headers.
Status and ScopeΒΆ
MCP Apps support is feature flagged:
When disabled, the gateway does not advertise MCP Apps capabilities and rejects MCP Apps UI resource registration or AppBridge requests.
When enabled, the gateway can:
- advertise the
io.modelcontextprotocol/uicapability during MCP initialization for authenticated callers; - store UI metadata on tools and resources through the shared
extensionMetadatafield; - project known MCP Apps metadata into MCP protocol
_meta.uifields; - filter app-only tools out of model-facing tool listings;
- create short-lived AppBridge sessions bound to a user, MCP session, virtual server, and UI resource;
- allow AppBridge RPC calls only to tools marked for app use.
The current AppBridge RPC surface supports tools/call. Other MCP methods are rejected with JSON-RPC Method not found.
MCP CapabilityΒΆ
For authenticated MCP initialization requests, the gateway advertises:
{
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"version": "2026-01-26",
"resources": {
"schemes": ["ui://"]
},
"bridge": {
"methods": ["tools/call"]
}
}
}
}
}
The capability is omitted when MCP Apps are disabled or when the caller is not authorized.
Metadata ModelΒΆ
ContextForge stores MCP Apps data in the extensionMetadata object on tools and resources. MCP Apps metadata is keyed by the MCP UI capability identifier:
{
"extensionMetadata": {
"io.modelcontextprotocol/ui": {
"audience": ["model"],
"resourceUri": "ui://widgets/customer-search"
}
}
}
The gateway also accepts the internal snake_case form, extension_metadata, in Python service paths. Public API payloads should use extensionMetadata.
Deprecated Flat ui/resourceUri KeyΒΆ
Upstream servers advertise Apps metadata on the wire as _meta, which the gateway folds into extensionMetadata during ingest. Alongside the nested _meta.ui object, the gateway still honours the deprecated flat _meta["ui/resourceUri"] key so that servers emitting only the older shape keep their tool/UI association:
Precedence and handling:
- The nested object wins. If
_meta.uicontainsresourceUri(orresource_uri), the flat key is ignored. Precedence is decided by key presence, not truthiness, so a nested value that is empty or malformed stays authoritative and is rejected by validation rather than being silently replaced by the deprecated value. - Only well-formed
ui://values are folded in. An unusable flat value is ignored and logged. Folding it in would fail extension metadata validation and drop the whole tool from gateway sync, which is worse than ingesting the tool without a UI association. - Ingest only. ContextForge always projects the nested shape outbound; it does not re-emit the deprecated key.
The flat key is scheduled for removal from the spec before GA, so treat it as a compatibility path rather than a supported input format.
Tool MetadataΒΆ
Tool metadata controls who sees or can invoke the tool:
{
"name": "open_customer_widget",
"description": "Open the customer search widget.",
"inputSchema": {
"type": "object",
"properties": {
"customerId": {
"type": "string"
}
}
},
"extensionMetadata": {
"io.modelcontextprotocol/ui": {
"audience": ["model"],
"resourceUri": "ui://widgets/customer-search"
}
}
}
Audience behavior:
| Audience | Model-facing tools/list | AppBridge tools/call |
|---|---|---|
| omitted | visible | denied |
model | visible | denied |
app | hidden | allowed |
model, app | visible | allowed |
Use audience: ["app"] for helper tools that should only be callable by UI code running through a validated AppBridge session.
UI Resource MetadataΒΆ
MCP Apps UI resources use the ui:// URI scheme and must be HTML:
{
"uri": "ui://widgets/customer-search",
"name": "Customer search widget",
"mimeType": "text/html;profile=mcp-app",
"content": "<div id=\"customer-search-root\"></div>",
"extensionMetadata": {
"io.modelcontextprotocol/ui": {
"csp": {
"connectDomains": ["https://api.example.com"],
"resourceDomains": ["https://cdn.example.com"],
"frameDomains": [],
"baseUriDomains": []
},
"sandbox": ["allow-scripts", "allow-forms"],
"permissions": []
}
}
}
For ui:// resources, ContextForge requires all of the following:
- MCP Apps must be enabled.
- Public MCP App resources must use
text/html;profile=mcp-app. Additional parameters, such ascharset=utf-8, are accepted when the MCP App profile parameter is present. extensionMetadata["io.modelcontextprotocol/ui"]must exist.cspmust be a non-empty object.sandboxmust be a non-empty list.
CSP and Sandbox ValidationΒΆ
The gateway validates CSP metadata before accepting a UI resource. Public resource metadata should use the MCP Apps domain-list keys:
baseUriDomainsconnectDomainsframeDomainsresourceDomains
Rejected CSP sources:
*javascript:file:data:'unsafe-inline''unsafe-eval'
Allowed sandbox tokens:
allow-downloadsallow-formsallow-modalsallow-popupsallow-scripts
The gateway deliberately rejects high-risk sandbox tokens such as allow-same-origin and allow-popups-to-escape-sandbox.
Permission tokens are optional. When present, each token must be lowercase, start with a letter, and contain only letters, digits, or hyphens.
AppBridge FlowΒΆ
AppBridge is Host-mediated. The sandboxed View exchanges JSON-RPC messages with the Host through postMessage; the Host validates those messages and uses the gateway AppBridge endpoints on the View's behalf. The View must not receive or store the user's gateway bearer token.
AppBridge requests use short-lived sessions. A session binds four independent facts:
- the authenticated gateway user;
- the MCP session ID;
- the virtual server ID;
- the
ui://resource URI.
sequenceDiagram
participant Host as MCP Host
participant Gateway as ContextForge
participant DB as Database
participant App as MCP App iframe
Host->>Gateway: initialize
Gateway-->>Host: capabilities.extensions.io.modelcontextprotocol/ui
Host->>Gateway: resources/read ui://widget
Gateway-->>Host: HTML + _meta.ui policy
Host->>App: render sandboxed iframe
Host->>Gateway: POST /appbridge/sessions
Gateway->>DB: verify resource visibility and store AppBridge session
Gateway-->>Host: appSessionId, serverId, expiresAt
App->>Host: postMessage JSON-RPC tools/call
Host->>Gateway: POST /appbridge/sessions/{id}/rpc tools/call
Gateway->>DB: validate session owner, MCP session, server binding
Gateway->>Gateway: invoke app-visible tool
Gateway-->>Host: JSON-RPC result
Host-->>App: postMessage JSON-RPC result Create an AppBridge SessionΒΆ
This is a Host-to-gateway request. The Authorization header belongs to the Host's authenticated gateway connection and must not be injected into the sandboxed View.
POST /appbridge/sessions
Authorization: Bearer <token>
Mcp-Session-Id: <mcp-session-id>
Content-Type: application/json
{
"resourceUri": "ui://widgets/customer-search",
"serverId": "server-123"
}
Successful response:
{
"appSessionId": "<A very random-looking session id string>"
"resourceUri": "ui://widgets/customer-search",
"serverId": "server-123",
"expiresAt": "2026-06-05T15:30:00+00:00"
}
The session endpoint requires resources.read. Before creating the session, the gateway reads the requested UI resource with the caller's normal token scope and RBAC context.
Call an App-Visible ToolΒΆ
This is also a Host-to-gateway request. The View sends the JSON-RPC payload to the Host through AppBridge/postMessage; the Host forwards approved calls to the gateway endpoint below.
POST /appbridge/sessions/5a51a7f84aa548d99aa13f4b5f07ed76/rpc
Authorization: Bearer <token>
Mcp-Session-Id: <mcp-session-id>
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {
"name": "customer_widget_lookup",
"arguments": {
"customerId": "C123"
}
}
}
The RPC endpoint requires tools.execute. The tool must resolve within the stored server binding and must be app-visible through audience: ["app"] or audience: ["model", "app"].
Security InvariantsΒΆ
MCP Apps cross a browser UI boundary, so ContextForge treats AppBridge as a separate, constrained execution path.
The gateway enforces:
- Feature flag deny-by-default: all MCP Apps routes and
ui://resources are unavailable unlessMCPGATEWAY_MCP_APPS_ENABLED=true. - AppBridge rate limiting:
/appbridge/sessionsand session RPC calls use the high-risk rate limit tier when gateway rate limiting is enabled. - Session ownership before bridge creation: AppBridge sessions require an existing MCP session owned by the same user, unless the requester has admin bypass.
- Explicit server binding: AppBridge sessions require
serverId; RPC calls cannot switch to a different server through headers or request parameters. - Resource visibility first: creating an AppBridge session reads the
ui://resource through normal token scoping and RBAC before persisting the session. - Tool visibility split: app-only helper tools are hidden from model-facing
tools/list; model-only tools are rejected by AppBridge. - Host-mediated View calls: the sandboxed View talks to the Host through AppBridge messages; gateway bearer credentials remain in the Host or gateway integration layer.
- No direct-proxy header trust: AppBridge strips
X-Context-Forge-Gateway-Idbefore invoking tools and uses the stored server binding instead. - Short-lived sessions: sessions expire after
MCPGATEWAY_MCP_APPS_SESSION_TTLseconds. Expired rows are ignored during lookup and cleaned up by the MCP Apps session cleanup service when enabled. - Strict UI policy:
ui://resources require explicit CSP and sandbox metadata before registration.
These checks complement, but do not replace, the normal two-layer ContextForge security model: token scoping controls what a caller can see, and RBAC controls what a caller can do.
Operational GuidanceΒΆ
Enable MCP Apps only for deployments that intentionally serve trusted UI resources:
MCPGATEWAY_MCP_APPS_ENABLED=true
MCPGATEWAY_MCP_APPS_SESSION_TTL=900
MCPGATEWAY_MCP_APPS_SESSION_CLEANUP_ENABLED=true
MCPGATEWAY_MCP_APPS_SESSION_CLEANUP_INTERVAL_SECONDS=300
MCPGATEWAY_MCP_APPS_SESSION_CLEANUP_BATCH_SIZE=1000
Recommended rollout steps:
- Keep the feature disabled in production until at least one UI resource and its AppBridge helper tools have been reviewed together.
- Register
ui://resources with the narrowest CSP and sandbox policies that still allow the UI to function. - Mark helper tools with
audience: ["app"]unless the model also needs to call them directly. - Exercise deny paths during validation: wrong MCP session, wrong server ID, missing CSP, missing sandbox, app calling model-only tool, and model listing app-only tool.
- Monitor normal audit, token usage, and structured logs for AppBridge session creation and tool invocation failures.
Related FilesΒΆ
mcpgateway/services/mcp_apps.py- metadata validation, capability payload, model/app filtering, and AppBridge session helper.mcpgateway/main.py- AppBridge session and RPC endpoints.mcpgateway/services/tool_service.py- app-visible tool invocation guard.mcpgateway/services/resource_service.py-ui://resource validation.mcpgateway/services/mcp_method_registry.py- MCP method routing helper.mcpgateway/alembic/versions/b6c7d8e9f0a1_add_mcp_app_metadata.py- database migration for MCP Apps metadata and AppBridge sessions.