ui

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: Apache-2.0 Imports: 17 Imported by: 1

README

ext/ui — MCP Apps server support for mcpkit

Go server-side support for the MCP Apps extension (io.modelcontextprotocol/ui). Lets you write MCP servers whose tools expose interactive HTML UIs that hosts (basic-host, Claude.ai, ChatGPT, etc.) render in sandboxed iframes.

Separate Go module (github.com/panyam/mcpkit/ext/ui) so the core mcpkit module stays zero-deps. Import this package only when you want to advertise MCP Apps support.

Looking for the architecture overview? Read examples/apps/FLOW.md — explains how basic-host, the sandbox iframe, the App, the bridge JS, and the MCP server fit together.

Why the bridge exists

The App is a regular HTML/JS bundle running inside a sandboxed iframe in the host's browser. The MCP server is a separate process speaking JSON-RPC. Three problems immediately:

  1. The App can't speak MCP/JSON-RPC directly. It's in a sandboxed iframe with no network access to the server (sandbox attribute + CSP enforce this).
  2. The App needs to talk to the host, not just the server. Things like "push a message into the chat", "trigger a file download", "request fullscreen" — those aren't MCP tool calls; they're host capabilities the App needs to invoke.
  3. The host and the App live in different iframe origins (basic-host on :8080, sandbox iframe on :8081, App iframe nested inside the sandbox). Cross-origin communication has to go through postMessage.

The bridge is what lets the App act on all three needs without seeing the MCP protocol directly:

flowchart LR
  subgraph Server["Server process (your Go binary)"]
    MCP["mcpkit<br/>core / server / ext/ui"]
  end
  subgraph Browser["Browser process"]
    direction TB
    Host["Host page<br/>basic-host, Claude.ai, ..."]
    Sandbox["Sandbox iframe<br/>(different origin)"]
    App["App iframe<br/>your HTML"]
    Bridge["mcp-app-bridge.js<br/>inside the App"]
    App -.->|loads| Bridge
  end
  Host -- "MCP/JSON-RPC<br/>over Streamable HTTP" --> MCP
  MCP -- "tools, resources" --> Host
  Bridge -- "postMessage" --> Sandbox
  Sandbox -- "postMessage" --> Host

The App calls mcp.callTool(...) / mcp.sendMessage(...) / etc. Those calls become postMessage to the parent (sandbox), which relays to the host. The host then either:

  • handles the call locally (it's a host-capability like sendMessage, openLink, requestDisplayMode), or
  • forwards it to the MCP server as a JSON-RPC call (it's a callTool / readResource).

Two example flows make the distinction visible:

sequenceDiagram
    participant App
    participant Bridge as bridge.js
    participant Sandbox
    participant Host
    participant Server as MCP server<br/>(mcpkit)

    Note over App: A — App calls a server tool
    App->>Bridge: mcp.callTool({name: "refresh-data"})
    Bridge->>Sandbox: postMessage(tools/call)
    Sandbox->>Host: relay
    Host->>Server: tools/call (MCP/JSON-RPC)
    Server-->>Host: result
    Host-->>Sandbox: result
    Sandbox-->>Bridge: postMessage
    Bridge-->>App: resolve callTool() promise

    Note over App: B — App pushes a host event<br/>(no server round-trip)
    App->>Bridge: mcp.sendMessage("done!")
    Bridge->>Sandbox: postMessage(ui/message)
    Sandbox->>Host: relay
    Host->>Host: handle locally —<br/>e.g., chat insert, log line

This is why mcpkit ships both server-side Go code (core/, server/, ext/ui/) and a bridge JS library (ext/ui/assets/mcp-app-bridge.ts → compiled JS). You need both to write an App from scratch — server-side for the MCP surface, browser-side for the App's interactive behavior.

In the apps-compat flow we test against (examples/apps/compat/), the App HTML comes from upstream verbatim and embeds upstream's bridge, so our Go fixtures only need the server side. But when you write a new App, mcpkit's bridge JS is what goes in your App HTML.

"Tool" vs "App tool" — when to use each

A plain MCP tool is any callable function a client/host can invoke via tools/call. It returns text or structured content. No UI.

An App tool is a tool that also exposes an interactive UI — its definition carries _meta.ui.resourceUri pointing at HTML the host fetches and renders as an iframe. Mechanically it's just a tool with extra metadata + a paired resource.

Pattern API When to use
Plain tool srv.RegisterTool(def, handler) or core.TypedTool[In, Out](...) Tool that produces text/structured output. No UI. Most "regular" MCP tools.
App tool with its own UI ui.RegisterTypedAppTool(reg, TypedAppToolConfig[In, Out]{...}) Tool whose result is best rendered as an interactive App (chart, map, form, code editor). The helper auto-pairs the tool with its UI resource and sets _meta.ui.resourceUri.
App-only tool sharing another App's iframe core.TypedTool + manual ToolDef.Meta.UI mutation + srv.RegisterTool App-side helper called via the bridge from inside an existing App's iframe (e.g., polling for stats, logging events). Doesn't render anything itself; _meta.ui.visibility = ["app"] hides it from the model. See examples/apps/compat/system-monitor and debug-server.

So every App tool is also an MCP tool; "App tool" is shorthand for "tool with bonus UI metadata + a paired HTML resource". RegisterTypedAppTool is the ergonomic helper that sets all the metadata correctly and validates the pairing. When you don't fit its shape (no UI resource of your own), drop down to the lower-level API.

Improvement tracked in issue 548: making the RegisterTypedAppTool helper accept an optional empty ResourceURI for the app-only-tool case so you don't have to drop down to core.TypedTool.

How the server and the App actually talk

The server and the App page never communicate directly — they talk through the host (basic-host, Claude.ai, ChatGPT, etc.) using two completely different channels.

flowchart LR
  subgraph Server["Server process (Go binary)"]
    MCP["mcpkit<br/>(core / server / ext/ui)"]
  end
  subgraph Browser["Browser process"]
    direction TB
    Host["Host page<br/>(basic-host, Claude.ai, ...)"]
    Sandbox["Sandbox iframe<br/>(different origin)"]
    App["App iframe<br/>(your HTML)"]
    Bridge["mcp-app-bridge.js<br/>(inside the App)"]
    App -.->|loads| Bridge
  end
  Host <-- "Channel 1: MCP/JSON-RPC<br/>over Streamable HTTP / stdio" --> MCP
  Bridge -- "Channel 2: postMessage<br/>(across iframe boundaries)" --> Sandbox
  Sandbox -- "postMessage relay" --> Host

Channel 1 is HTTP/JSON-RPC (or stdio) between the host and the server. That's what core/ and server/ implement on the Go side.

Channel 2 is postMessage between iframes inside the browser. The App can't reach the host's window directly (they're in different origins by design), so each mcp.callTool(...) / mcp.sendMessage(...) becomes a postMessage to the sandbox iframe, which relays to the host.

The host is the bridge between the two channels: it speaks JSON-RPC to your Go server AND postMessage to the App. The App never makes an HTTP call to your Go server. The Go server never speaks postMessage.

Two example flows make the distinction visible:

sequenceDiagram
    participant App
    participant Bridge as bridge.js
    participant Sandbox
    participant Host
    participant Server as MCP server<br/>(mcpkit)

    Note over App: A — App calls a server tool
    App->>Bridge: mcp.callTool({name: "refresh-data"})
    Bridge->>Sandbox: postMessage(tools/call)
    Sandbox->>Host: relay
    Host->>Server: tools/call (MCP/JSON-RPC)
    Server-->>Host: result
    Host-->>Sandbox: result
    Sandbox-->>Bridge: postMessage
    Bridge-->>App: resolve callTool() promise

    Note over App: B — App pushes a host event (no server round-trip)
    App->>Bridge: mcp.sendMessage("done!")
    Bridge->>Sandbox: postMessage(ui/message)
    Sandbox->>Host: relay
    Host->>Host: handle locally —<br/>e.g., chat insert, log line

Three concrete consequences for Go developers:

  1. You don't need a separate HTTP server for your App's HTML. The App is delivered as a resources/read JSON-RPC response payload — the same /mcp endpoint mcpkit's server already exposes handles it. (Even stdio MCP servers can expose Apps; the host just gets the HTML over stdio.)
  2. The App can't fetch() your Go server. If it tries, browser same-origin policy blocks it (the iframe runs at the host's sandbox origin, not your server's origin). All interaction goes through the bridge.
  3. You need both halves of mcpkit to write a real App from scratch. The Go side (core/ + server/ + ext/ui/) handles Channel 1. The JS bridge (ext/ui/assets/mcp-app-bridge.ts → compiled JS) handles Channel 2's App end. In compat fixtures the App HTML comes from upstream verbatim, so the JS bridge is upstream's; for non-compat Apps the JS bridge is mcpkit's.

For the full runtime architecture — iframe nesting, postMessage relay details, where the App HTML comes from — see examples/apps/FLOW.md.

What this package provides

Server-side Go API
Symbol Purpose
UIExtension{} Extension marker — pass to server.WithExtension(...) to advertise MCP Apps support in initialize
RegisterAppTool(reg, AppToolConfig) Register a tool + its UI resource in one call. Most tools use this.
RegisterTypedAppTool(reg, TypedAppToolConfig[In, Out]) Typed wrapper — auto-derives InputSchema from In and OutputSchema from Out via reflection, wraps the handler. The common path for new code.
AppToolConfig / TypedAppToolConfig[In, Out] Config structs — fields like Title, Description, ResourceURI, Execution, Visibility, CSP, Permissions, PrefersBorder, Domain, SupportedDisplayModes, TemplateHandler, InputSchemaOverride
ElicitWithApp / SampleWithApp Helpers that attach _meta.ui to server→client elicitation / sampling requests so the host can render rich App UI for the prompt
RefValidator Validates at startup that every tool referencing ui:// resources has a matching resource registered — catches typos early
NotifyResourcesChanged(ctx) From inside a tool handler, signal the client that resource lists changed (e.g., after generating a new dynamic resource)
Bridge JS (for App authors)
Asset Purpose
assets/mcp-app-bridge.ts → compiled JS + .d.ts TypeScript source for mcpkit's bridge — the JS library that runs inside your App iframe and exposes mcp.callTool, mcp.readResource, mcp.sendMessage, mcp.sendLog, mcp.openLink, mcp.updateModelContext, mcp.requestDisplayMode, mcp.downloadFile, mcp.selectFile/selectFiles. Spec-compatible with upstream's bridge.
BridgeTemplateDef() + BridgeData Go html/template integration — drop {{ template "mcp-app-bridge" . }} into your App HTML template to inject the bridge inline
ServeBridge() (HTTP handler) Serves the bridge JS at /_mcpkit/mcp-app-bridge.js for external <script src> loading
InjectAppBridge(html) / AppShellHTML(title, body) Convenience helpers for ad-hoc inline injection
Host-side helpers (for harness / agent-runner builders)
Symbol Purpose
AppHost A Go-side embeddable "host" you can drive programmatically — connects to MCP servers, hands their tools back, runs the bridge protocol. For headless agent harnesses or testing.
ServerRegistry Manage multiple MCP server connections in one host process
InProcessBridge Drives the bridge protocol without a real iframe — for unit testing App logic against a mcpkit server

Quick start (server side)

package main

import (
    "github.com/panyam/mcpkit/core"
    "github.com/panyam/mcpkit/ext/ui"
    "github.com/panyam/mcpkit/server"
)

type weatherInput struct {
    City string `json:"city" jsonschema:"required"`
}

type weatherOutput struct {
    TempC float64 `json:"tempC"`
    Conditions string `json:"conditions"`
}

func main() {
    srv := server.NewServer(
        core.ServerInfo{Name: "weather-app", Version: "1.0"},
        server.WithExtension(&ui.UIExtension{}),
    )

    ui.RegisterTypedAppTool(srv, ui.TypedAppToolConfig[weatherInput, weatherOutput]{
        Name:        "get-weather",
        Title:       "Get Weather",
        Description: "Returns current weather for a city.",
        Execution:   &core.ToolExecution{TaskSupport: core.TaskSupportForbidden},
        Handler: func(ctx core.ToolContext, in weatherInput) (weatherOutput, error) {
            return weatherOutput{TempC: 22.5, Conditions: "sunny"}, nil
        },
        ResourceURI: "ui://weather/mcp-app.html",
        ResourceHandler: func(ctx core.ResourceContext, req core.ResourceRequest) (core.ResourceResult, error) {
            return core.ResourceResult{Contents: []core.ResourceReadContent{{
                URI: req.URI, MimeType: core.AppMIMEType, Text: weatherHTML,
            }}}, nil
        },
    })

    srv.Run(":3101")
}

weatherHTML would be your App's HTML bundle. For a real one, embed mcpkit's bridge via the BridgeTemplateDef() helper and write your UI in whatever framework you like.

What lives in _meta.ui (the MCP Apps spec)

RegisterAppTool builds a core.ToolMeta that ships under _meta on the tool definition:

{
  "name": "get-weather",
  "title": "Get Weather",
  "description": "...",
  "inputSchema": { ... },
  "outputSchema": { ... },
  "execution": { "taskSupport": "forbidden" },
  "_meta": {
    "ui": {
      "resourceUri": "ui://weather/mcp-app.html",
      "visibility": ["model", "app"],
      "csp": { ... },
      "permissions": [ ... ],
      "supportedDisplayModes": ["inline", "fullscreen"]
    },
    "ui/resourceUri": "ui://weather/mcp-app.html"
  }
}

The flat ui/resourceUri key alongside the nested ui.resourceUri is a backward-compat fallback for older clients — mcpkit's core.ToolMeta emits both via custom MarshalJSON (added in PR 538, mirrors upstream's ext-apps SDK behavior). Unmarshaling accepts either form.

Escape hatches

RegisterTypedAppTool derives schemas via reflection (invopop/jsonschema). When reflection isn't expressive enough:

  • InputSchemaOverride field on TypedAppToolConfig — pass a raw map[string]any or json.RawMessage to bypass reflection entirely. Use when defaults / descriptions contain commas (invopop's struct-tag parser truncates at commas), when you need JSON Schema 2020-12 features (if/then/else, $anchor), or when you need byte-for-byte parity with an external reference schema. See PR 545 / issue 542.
  • Drop down to core.TypedTool + srv.RegisterTool for tools that don't have their own UI resource (app-only tools that share an iframe). RegisterTypedAppTool always pairs a tool with a resource; for the no-resource case use the lower-level API. See examples in examples/apps/compat/system-monitor/main.go and debug-server/main.go. (Tracked as a gap in issue 548.)

Sub-module status

  • Separate go.mod (see Sub-Modules in the root CLAUDE.md) — make test at the repo root does NOT cover this package. Use make test-ui or cd ext/ui && go test ./....
  • Run make tidy-all after touching core/ imports.
  • The bridge JS source (assets/mcp-app-bridge.ts) builds via make build-bridge (delegates to assets/'s pnpm setup).

Gotchas

  • Tool without a UI resource: RegisterTypedAppTool requires a ResourceURI + ResourceHandler pair. For tools that don't have their own UI (app-only helpers sharing an iframe), use core.TypedTool + srv.RegisterTool directly with manual ToolDef.Meta.UI construction. Improvement tracked in issue 548.
  • Comma-bearing defaults/descriptions in struct tags: invopop's tag parser splits on commas; values containing commas get silently truncated. Use InputSchemaOverride to bypass. See issue 542 (closed by PR 545).
  • interface{} / any field in input struct: produces a schema the MCP SDK client-side zod validator rejects. Use InputSchemaOverride with an explicit empty-shape map ({} for the any field). Tracked in issue 548.
  • Background goroutines that outlive the tool handler: use core.DetachForBackground(ctx) (not context.WithoutCancel) — preserves the session-level push channel.

Tracing across the Apps Bridge

SEP-414 P6 (issue 660) relays W3C trace context across the iframe↔host postMessage boundary so browser-side traces stitch with the backend tool-call span. Two opt-in surfaces:

  • TS bridgeMCPApp.setTraceContextProvider(fn) registers a provider the bridge consults before each outbound request; merges traceparent / tracestate into params._meta. Caller-set _meta wins (provider is a fallback). Wire against your browser OTel SDK via propagation.inject(context.active(), carrier).
  • Go AppHostui.WithTracerProvider(tp) opts the forward path into emitting an apps.host.forward span whose parent is the inbound _meta.traceparent from the bridge envelope; the outbound MCP call preserves the iframe's traceparent on the wire so the server's dispatch span stitches in.

See docs/APPS_DESIGN.md § Tracing across the Apps Bridge for the design, demo wiring, and the open spec question.

Documentation

Overview

Package ui provides the MCP Apps extension (io.modelcontextprotocol/ui) for mcpkit servers. It declares the extension in the initialize response so clients know the server supports interactive HTML UIs.

This is a separate Go module (github.com/panyam/mcpkit/ext/ui) so that the core mcpkit module stays zero-deps. Import this package to advertise MCP Apps support on your server.

Usage:

srv := server.NewServer(info,
    server.WithExtension(ui.UIExtension{}),
)

Index

Constants

View Source
const BridgePath = "/_mcpkit/mcp-app-bridge.js"

BridgePath is the default URL path used by ServeBridge.

Variables

View Source
var AppBridgeScript string

AppBridgeScript is the compiled MCP App Bridge JS. Source: assets/mcp-app-bridge.ts.

Functions

func AppShellHTML added in v0.2.18

func AppShellHTML(title string, bodyHTML string, cfg ...*BridgeConfig) string

AppShellHTML generates a minimal HTML5 document with the bridge pre-injected. Use this in resource handlers that build HTML programmatically rather than from template files.

An optional BridgeConfig sets the app identity for the ui/initialize handshake. Pass nil to use defaults.

func BridgeTemplateDef added in v0.2.18

func BridgeTemplateDef() string

BridgeTemplateDef returns the raw text of the "mcpkit-bridge" named template definition. Parse it into your template set:

tmpl := template.Must(template.New("page").Parse(pageHTML))
template.Must(tmpl.Parse(ui.BridgeTemplateDef()))

Then use it in your HTML:

{{ template "mcpkit-bridge" .Bridge }}

func ElicitWithApp added in v0.1.31

ElicitWithApp sends an elicitation/create request with MCP Apps metadata. This is a convenience wrapper around core.Elicit that populates _meta.ui so the host can render a UI resource during input collection.

func InjectAppBridge added in v0.2.18

func InjectAppBridge(html string, cfg ...*BridgeConfig) string

InjectAppBridge inserts the MCP App Bridge <script> block before </body> in the provided HTML. If no </body> is found, the script is appended to the end.

An optional BridgeConfig sets the app identity for the ui/initialize handshake. Pass nil to use the bridge's built-in defaults.

The call is idempotent — if the sentinel comment is already present in html, the original string is returned unchanged.

func RegisterAppTool

func RegisterAppTool(reg ToolResourceRegistrar, cfg AppToolConfig)

RegisterAppTool registers both a tool (with _meta.ui metadata) and its matching ui:// resource in one call. Ensures the tool's resourceUri and the resource URI are consistent, and sets the correct MIME type automatically.

Example:

ui.RegisterAppTool(srv, ui.AppToolConfig{
    Name:        "build_deck",
    Description: "Build a slide deck",
    InputSchema: map[string]any{"type": "object"},
    ResourceURI: "ui://decks/view",
    ToolHandler: buildDeckHandler,
    ResourceHandler: serveDeckHTML,
})

func RegisterTypedAppTool added in v0.2.26

func RegisterTypedAppTool[In, Out any](reg ToolResourceRegistrar, cfg TypedAppToolConfig[In, Out])

RegisterTypedAppTool registers a typed tool + resource pair. It auto-derives InputSchema from the In type parameter and wraps the typed handler, then delegates to RegisterAppTool for all the app-specific wiring (UI metadata, template detection, resource registration, concrete fallback generation).

Example:

type addTaskInput struct {
    Title    string `json:"title" jsonschema:"required,description=Task title"`
    Priority string `json:"priority,omitempty" jsonschema:"enum=low,enum=medium,enum=high"`
}

ui.RegisterTypedAppTool(srv, ui.TypedAppToolConfig[addTaskInput, string]{
    Name:        "add_task",
    Description: "Add a task to the board",
    Handler: func(ctx core.ToolContext, input addTaskInput) (string, error) {
        return fmt.Sprintf("Added task: %s", input.Title), nil
    },
    ResourceURI:     "ui://tasks/board",
    ResourceHandler: serveBoardHTML,
})

func RequestDisplayMode added in v0.1.31

func RequestDisplayMode(ctx context.Context, mode core.DisplayMode)

RequestDisplayMode sends a display mode change notification to the client. Call this from a tool handler to request the host to change how the app is displayed (e.g., switch from inline to fullscreen).

The notification is fire-and-forget; the host may ignore it if the requested mode is not supported.

func SampleWithApp added in v0.1.31

SampleWithApp sends a sampling/createMessage request with MCP Apps metadata. This is a convenience wrapper around core.Sample that populates _meta.ui so the host can associate the sampling request with a UI resource.

func ServeBridge added in v0.2.18

func ServeBridge() http.Handler

ServeBridge returns an http.Handler that serves the compiled bridge JS. Mount it on your mux alongside MCP and REST handlers:

mux.Handle(ui.BridgePath, ui.ServeBridge())

HTML can then reference it via <script src="/_mcpkit/mcp-app-bridge.js">. Note: sandboxed iframes need the serving origin in their CSP connect-src or resource-src for this to work. For simplest setup, use InjectAppBridge to inline the script instead.

func ToBytes added in v0.2.41

func ToBytes(v any) ([]byte, error)

ToBytes converts a Response.Result (any) to []byte for unmarshalling. Response.Result may be json.RawMessage, []byte, or an arbitrary struct.

Types

type AppBridge added in v0.2.41

type AppBridge interface {
	// Send sends a JSON-RPC request to the app and waits for the response.
	// Used by AppHost to call tools/list and tools/call on the app.
	Send(ctx context.Context, req *core.Request) (*core.Response, error)

	// SetRequestHandler registers a handler for app→host requests
	// (e.g., tools/call, resources/read forwarded to the MCP server).
	// Must be called before Start.
	SetRequestHandler(fn func(ctx context.Context, req *core.Request) *core.Response)

	// SetNotificationHandler registers a handler for app→host notifications
	// (e.g., notifications/tools/list_changed, ui/log).
	// Must be called before Start.
	SetNotificationHandler(fn func(method string, params json.RawMessage))

	// Start begins the bridge's communication loop.
	Start() error

	// Close shuts down the bridge.
	Close() error
}

AppBridge abstracts the bidirectional communication channel between the host (AppHost) and the app (iframe JS or in-process Go handler). The protocol is JSON-RPC 2.0, mirroring the postMessage protocol defined in mcp-app-bridge.ts.

type AppHost added in v0.2.41

type AppHost struct {
	// contains filtered or unexported fields
}

AppHost wraps an MCP Client and an AppBridge, mediating between an MCP App (running in a browser iframe or in-process) and an MCP server.

It provides:

  • Host→App: ListAppTools and CallAppTool forward requests to the app
  • App→Host: app requests (tools/call, resources/read) are forwarded to the server
  • Cache: app tool list is cached and refreshed on notifications/tools/list_changed
  • Aggregation: ListAllTools merges server and app tools for LLM presentation

func NewAppHost added in v0.2.41

func NewAppHost(c *client.Client, bridge AppBridge, opts ...AppHostOption) *AppHost

NewAppHost creates an AppHost that mediates between the given MCP Client (connected to a server) and AppBridge (connected to an app).

Call Start() after creating to wire up request/notification routing.

func (*AppHost) CallAppTool added in v0.2.41

func (h *AppHost) CallAppTool(ctx context.Context, name string, args map[string]any) (*core.ToolResult, error)

CallAppTool invokes a tool registered by the app via the bridge.

func (*AppHost) Close added in v0.2.41

func (h *AppHost) Close() error

Close shuts down the bridge. The caller is responsible for closing the underlying Client separately — AppHost does not own the Client lifetime.

func (*AppHost) ListAllTools added in v0.2.41

func (h *AppHost) ListAllTools(ctx context.Context) ([]core.ToolDef, error)

ListAllTools returns tools from both the MCP server and the app, suitable for presenting to an LLM. Server tools come first, then app tools.

func (*AppHost) ListAppTools added in v0.2.41

func (h *AppHost) ListAppTools(ctx context.Context) ([]core.ToolDef, error)

ListAppTools returns the tools registered by the app. Returns the cached list; the cache is refreshed automatically on notifications/tools/list_changed.

func (*AppHost) RefreshAppTools added in v0.2.41

func (h *AppHost) RefreshAppTools(ctx context.Context) error

RefreshAppTools sends tools/list to the app and updates the cached tool list.

func (*AppHost) Start added in v0.2.41

func (h *AppHost) Start(ctx context.Context) error

Start wires up the bridge handlers and begins the communication loop. Must be called after client.Connect() — the bridge may need to forward requests that require an active MCP session.

type AppHostOption added in v0.2.41

type AppHostOption func(*AppHost)

AppHostOption configures an AppHost.

func WithTracerProvider added in v0.2.47

func WithTracerProvider(tp core.TracerProvider) AppHostOption

WithTracerProvider opts the AppHost into SEP-414 instrumentation of the app→host forward path: when an app sends a request via the bridge (MCPApp.callTool / readResource / sendMessage), AppHost extracts any inbound `params._meta.traceparent` (relayed across the postMessage boundary by the JS bridge's setTraceContextProvider hook) and wraps the forward to the MCP server in an `apps.host.forward` span with that trace context as parent.

Nil and core.NoopTracerProvider{} both skip the install entirely — zero overhead on the unconfigured path. The bridge envelope itself preserves caller-set `_meta.traceparent` regardless of this option; it only governs whether AppHost emits a span around the forward.

type AppToolConfig

type AppToolConfig struct {
	// Name is the tool identifier used in tools/call.
	Name string

	// Title is an optional human-readable display name. Per MCP spec hosts
	// SHOULD prefer Title for user-facing surfaces; Name is the machine
	// identifier passed to tools/call.
	Title string

	// Description is a human-readable summary of what the tool does.
	Description string

	// InputSchema is the JSON Schema for the tool's arguments.
	InputSchema any

	// OutputSchema is the optional JSON Schema for the tool's
	// structuredContent output. When set, the host knows the tool's
	// response shape and can validate / render structured results.
	OutputSchema any

	// Execution declares task-execution metadata for this tool. Most
	// non-tasks-v2 tools set this to &core.ToolExecution{TaskSupport:
	// "forbidden"} to explicitly signal they don't participate in async
	// task flow.
	Execution *core.ToolExecution

	// ResourceURI is the ui:// URI for the app's HTML resource.
	ResourceURI string

	// ToolHandler handles tool invocations.
	ToolHandler core.ToolHandler

	// ResourceHandler serves the HTML content for the ui:// resource.
	ResourceHandler core.ResourceHandler

	// Visibility controls who can see/call this tool.
	// Nil means default (both model and app).
	Visibility []core.UIVisibility

	// CSP declares external domains the app needs.
	CSP *core.UICSPConfig

	// Permissions declares Permission-Policy capabilities the App needs.
	// See core.UIPermissions for the wire shape. Note that per the MCP Apps
	// spec, permissions belong on the UI resource's _meta.ui — set this
	// field on your ResourceHandler's per-content Meta to take effect.
	// Setting it here flows into the tool's _meta.ui for symmetry, but
	// basic-host does not read permissions from tool meta.
	Permissions *core.UIPermissions

	// PrefersBorder hints whether the host should draw a visible border.
	PrefersBorder *bool

	// Domain requests a dedicated sandbox origin for the app.
	Domain string

	// SupportedDisplayModes declares which display modes this app supports.
	// Nil means the host decides.
	SupportedDisplayModes []core.DisplayMode

	// TemplateHandler serves HTML content for a ui:// resource template.
	// Required when ResourceURI contains "{" (template variable).
	// When set, RegisterAppTool registers a resource template instead of
	// a concrete resource.
	TemplateHandler core.TemplateHandler

	// ResourceDescription is the optional human-readable description for
	// the registered ui:// resource (the `description` field on
	// resources/list responses). Distinct from `Description` which
	// describes the tool. When empty, the resource is registered with
	// no description — matches the pre-field behavior. Per
	// conformance/RESOURCES_META_AUDIT.md, upstream's per-fixture
	// resources/list responses include a description string ("Dashboard
	// UI", "PDF Viewer UI", etc.); set this field to surface those for
	// parity with upstream.
	ResourceDescription string
}

AppToolConfig configures a tool + resource pair for RegisterAppTool.

type BridgeConfig added in v0.2.18

type BridgeConfig struct {
	// App name sent as appInfo.name in ui/initialize. Default: "mcp-app".
	Name string
	// App version sent as appInfo.version. Default: "0.0.0".
	Version string
	// Protocol version. Default: "2026-01-26" (current MCP Apps spec).
	ProtocolVersion string
}

BridgeConfig customizes the bridge's app identity sent during the ui/initialize handshake with the host.

type BridgeData added in v0.2.18

type BridgeData struct {
	AppName         string      // App name for ui/initialize handshake
	AppVersion      string      // App version
	ProtocolVersion string      // MCP Apps spec version (default: "2026-01-26")
	BridgeJS        template.JS // The bridge runtime JS (trusted, unescaped)
}

BridgeData is the template data for the "mcpkit-bridge" template. Use with html/template — BridgeJS is typed as template.JS so it passes through without escaping; string fields are auto-escaped.

func NewBridgeData added in v0.2.18

func NewBridgeData(appName, appVersion string) BridgeData

NewBridgeData creates BridgeData with the bridge JS pre-loaded. ProtocolVersion defaults to "2026-01-26" (current MCP Apps spec).

type CollisionHandler added in v0.2.41

type CollisionHandler func(toolName string, serverIDs []string)

CollisionHandler is called when Add or a tools/list_changed notification creates a new tool name collision. Informational — lets the host log, alert, or adjust its resolver strategy proactively.

type InProcessAppBridge added in v0.2.41

type InProcessAppBridge struct {
	// contains filtered or unexported fields
}

InProcessAppBridge implements AppBridge for testing by dispatching to registered Go handler functions. No iframe, no postMessage — tool registration and dispatch happen entirely in-process.

func NewInProcessAppBridge added in v0.2.41

func NewInProcessAppBridge() *InProcessAppBridge

NewInProcessAppBridge creates a bridge for testing app-provided tools without an iframe or postMessage transport.

func (*InProcessAppBridge) Close added in v0.2.41

func (b *InProcessAppBridge) Close() error

Close implements AppBridge.

func (*InProcessAppBridge) RegisterTool added in v0.2.41

func (b *InProcessAppBridge) RegisterTool(name string, def core.ToolDef, handler func(args map[string]any) (any, error))

RegisterTool simulates app-side tool registration (the Go equivalent of MCPApp.registerTool in the bridge JS). It adds a tool and fires a notifications/tools/list_changed notification to the host.

func (*InProcessAppBridge) RemoveTool added in v0.2.41

func (b *InProcessAppBridge) RemoveTool(name string)

RemoveTool unregisters a tool and fires notifications/tools/list_changed.

func (*InProcessAppBridge) Send added in v0.2.41

Send implements AppBridge. It dispatches tools/list and tools/call to the internal tool registry, mirroring handleHostRequest in mcp-app-bridge.ts.

func (*InProcessAppBridge) SendToHost added in v0.2.41

func (b *InProcessAppBridge) SendToHost(ctx context.Context, method string, params any) (*core.Response, error)

SendToHost simulates an app→host request (e.g., MCPApp.callTool() calling a server-side tool). The request is forwarded to the handler set by AppHost.

func (*InProcessAppBridge) SetNotificationHandler added in v0.2.41

func (b *InProcessAppBridge) SetNotificationHandler(fn func(method string, params json.RawMessage))

SetNotificationHandler implements AppBridge.

func (*InProcessAppBridge) SetRequestHandler added in v0.2.41

func (b *InProcessAppBridge) SetRequestHandler(fn func(ctx context.Context, req *core.Request) *core.Response)

SetRequestHandler implements AppBridge.

func (*InProcessAppBridge) Start added in v0.2.41

func (b *InProcessAppBridge) Start() error

Start implements AppBridge. For in-process bridges this is a no-op.

type RegisteredTool added in v0.2.41

type RegisteredTool struct {
	core.ToolDef
	ServerID string `json:"serverId"` // which server owns this tool
	Source   string `json:"source"`   // "server" or "app"
}

RegisteredTool is a tool with routing metadata attached. The tool name stays clean (no server ID prefix) — routing is via ServerID sidecar.

type RegistryOption added in v0.2.41

type RegistryOption func(*ServerRegistry)

RegistryOption configures a ServerRegistry.

func WithCollisionHandler added in v0.2.41

func WithCollisionHandler(fn CollisionHandler) RegistryOption

WithCollisionHandler sets a callback for tool name collision notifications.

func WithRegistryNotificationHandler added in v0.2.41

func WithRegistryNotificationHandler(fn func(serverID, method string, params any)) RegistryOption

WithRegistryNotificationHandler sets a unified notification handler that receives notifications from all servers, tagged with the server ID.

func WithToolResolver added in v0.2.41

func WithToolResolver(fn ToolResolver) RegistryOption

WithToolResolver sets the resolver for ambiguous tool names.

type ServerRegistry added in v0.2.41

type ServerRegistry struct {
	// contains filtered or unexported fields
}

ServerRegistry manages connections to multiple MCP servers and provides unified tool aggregation and routing. Each server can have its own auth, app bridge, and reconnection policy.

func NewServerRegistry added in v0.2.41

func NewServerRegistry(opts ...RegistryOption) *ServerRegistry

NewServerRegistry creates a registry for managing multiple MCP server connections with unified tool routing.

func (*ServerRegistry) Add added in v0.2.41

func (r *ServerRegistry) Add(ctx context.Context, id string, c *client.Client) error

Add registers a connected MCP client under the given server ID. The client must already be connected (via client.Connect). The caller owns client construction and auth configuration — the registry only manages routing.

func (*ServerRegistry) AddWithBridge added in v0.2.41

func (r *ServerRegistry) AddWithBridge(ctx context.Context, id string, c *client.Client, bridge AppBridge) error

AddWithBridge registers a connected MCP client with an app bridge. The bridge enables app-provided tool aggregation and host↔app request routing.

func (*ServerRegistry) AllTools added in v0.2.41

func (r *ServerRegistry) AllTools(ctx context.Context) ([]RegisteredTool, error)

AllTools returns all tools from all servers with routing metadata. Server tools come first (grouped by server ID), then app tools.

func (*ServerRegistry) CallTool added in v0.2.41

func (r *ServerRegistry) CallTool(ctx context.Context, name string, args map[string]any) (*core.ToolResult, error)

CallTool routes a tool call by name. If the name is unambiguous (only one server has it), routes directly. If ambiguous, invokes the ToolResolver. If no resolver is set, returns a descriptive error listing candidates.

func (*ServerRegistry) CallToolOn added in v0.2.41

func (r *ServerRegistry) CallToolOn(ctx context.Context, serverID, name string, args map[string]any) (*core.ToolResult, error)

CallToolOn routes a tool call to a specific server, bypassing resolution.

func (*ServerRegistry) Close added in v0.2.41

func (r *ServerRegistry) Close() error

Close shuts down all servers in the registry. Closes app bridges but not the underlying clients (caller owns client lifetime).

func (*ServerRegistry) Remove added in v0.2.41

func (r *ServerRegistry) Remove(id string) error

Remove disconnects and removes a server. Closes the app bridge if present. Does NOT close the underlying client — the caller owns client lifetime.

func (*ServerRegistry) Servers added in v0.2.41

func (r *ServerRegistry) Servers() []string

Servers returns the IDs of all registered servers, sorted alphabetically.

type ToolResolver added in v0.2.41

type ToolResolver func(ctx context.Context, name string,
	candidates []RegisteredTool, args map[string]any) (serverID string, err error)

ToolResolver is called when CallTool hits an ambiguous tool name (same name registered by multiple servers). It receives the candidates and the call arguments, and returns the server ID to route to.

Implementations can use any strategy: static priority, arg-based routing, LLM sampling, user elicitation, round-robin, etc.

type ToolResourceRegistrar

type ToolResourceRegistrar interface {
	RegisterTool(def core.ToolDef, handler core.ToolHandler)
	RegisterResource(def core.ResourceDef, handler core.ResourceHandler)
	RegisterResourceTemplate(def core.ResourceTemplate, handler core.TemplateHandler)
}

ToolResourceRegistrar is the interface needed by RegisterAppTool to register tools, resources, and resource templates. Satisfied by *server.Server without importing it.

type TypedAppToolConfig added in v0.2.26

type TypedAppToolConfig[In, Out any] struct {
	// Name is the tool identifier used in tools/call.
	Name string

	// Title is an optional human-readable display name. Per MCP spec hosts
	// SHOULD prefer Title for user-facing surfaces; Name remains the
	// machine identifier passed to tools/call.
	Title string

	// Description is a human-readable summary of what the tool does.
	Description string

	// Execution declares task-execution metadata for this tool. Most
	// non-tasks-v2 tools set this to &core.ToolExecution{TaskSupport:
	// "forbidden"} to explicitly signal they don't participate in async
	// task flow.
	Execution *core.ToolExecution

	// InputSchemaOverride replaces the InputSchema that would otherwise be
	// auto-derived from the In type parameter. Use when struct-tag-based
	// reflection can't produce the exact schema you need — e.g., defaults or
	// descriptions containing commas (invopop's tag parser truncates at the
	// first comma; see issue 542), schemas using JSON Schema 2020-12
	// features (if/then/else, $anchor, complex allOf composition), or
	// fixtures that need byte-for-byte parity with an external reference
	// schema.
	//
	// When set, the handler still unmarshals into In, so the override must
	// stay compatible with In's wire shape (same field names + types). Pass
	// a json.RawMessage or any value that marshals to the desired JSON
	// Schema.
	InputSchemaOverride any

	// OutputSchemaOverride is the symmetric mirror of InputSchemaOverride for
	// the OutputSchema field. Use when Out's auto-derived schema can't be
	// made byte-identical to an external reference — common cases are
	// nullable types (upstream's `z.string().nullable()` wants
	// `{"type": ["string", "null"]}` which Go reflection won't produce),
	// `interface{}` / `any` fields that invopop reflects to schemas strict
	// MCP-SDK clients reject, or apps/compat fixtures that need byte-for-byte
	// parity with an upstream reference.
	//
	// The override is preserved as-is on the wire. The handler still returns
	// Out, so the override must stay compatible with Out's wire shape.
	OutputSchemaOverride any

	// InputSchemaPatch runs against the reflected input schema after
	// generation. The patch fn sees a SchemaBuilder over the live map
	// and edits in place (e.g., `s.Prop("url").Desc(...).Default(...)`).
	// Lighter-weight than InputSchemaOverride for the common case of
	// "tweak a few fields"; falls back to Override-style replacement
	// via `PropertyBuilder.Replace(...)` for the irreducible cases
	// (nullable, anyOf, record-of-union). Precedence: if both
	// InputSchemaOverride and InputSchemaPatch are set, Override wins
	// and Patch is silently skipped. Issue 556.
	InputSchemaPatch func(*core.SchemaBuilder)

	// OutputSchemaPatch is the symmetric mirror for the output schema.
	// Skipped when Out is `string` / `core.ToolResult` / `core.ToolResponse`
	// (those don't generate an output schema). Same Override/Patch
	// precedence rule as InputSchemaPatch. Issue 556.
	OutputSchemaPatch func(*core.SchemaBuilder)

	// Handler handles tool invocations with typed input.
	Handler func(ctx core.ToolContext, input In) (Out, error)

	// ResourceURI is the ui:// URI for the app's HTML resource.
	ResourceURI string

	// ResourceHandler serves the HTML content for the ui:// resource.
	ResourceHandler core.ResourceHandler

	// Visibility controls who can see/call this tool.
	Visibility []core.UIVisibility

	// CSP declares external domains the app needs.
	CSP *core.UICSPConfig

	// Permissions declares Permission-Policy capabilities the App needs.
	// See core.UIPermissions for the wire shape. Note that per the MCP Apps
	// spec, permissions belong on the UI resource's _meta.ui — set this
	// field on your ResourceHandler's per-content Meta to take effect.
	// Setting it here flows into the tool's _meta.ui for symmetry, but
	// basic-host does not read permissions from tool meta.
	Permissions *core.UIPermissions

	// PrefersBorder hints whether the host should draw a visible border.
	PrefersBorder *bool

	// Domain requests a dedicated sandbox origin for the app.
	Domain string

	// SupportedDisplayModes declares which display modes this app supports.
	SupportedDisplayModes []core.DisplayMode

	// TemplateHandler serves HTML content for a ui:// resource template.
	TemplateHandler core.TemplateHandler

	// ResourceDescription is the human-readable description for the ui://
	// resource registered alongside the tool (the `description` field on
	// resources/list responses). Distinct from `Description` which describes
	// the tool. Set this to surface per-fixture descriptions on
	// resources/list (matches upstream's per-fixture convention; documented
	// in conformance/RESOURCES_META_AUDIT.md).
	ResourceDescription string
}

TypedAppToolConfig configures a typed tool + resource pair for RegisterTypedAppTool. It replaces AppToolConfig's InputSchema and ToolHandler with a typed handler — the schema is auto-derived from the In type parameter, and the handler receives typed input.

type UIExtension

type UIExtension struct{}

UIExtension declares support for the MCP Apps extension. Register it on the server to advertise UI rendering capability in the initialize response. Also validates that tools referencing ui:// resources have matching resource registrations (via RefValidator).

func (UIExtension) Extension

func (UIExtension) Extension() core.Extension

Extension returns the MCP Apps extension metadata.

func (UIExtension) ValidateRefs

func (UIExtension) ValidateRefs(tools []core.ToolDef, resourceURIs []string, templateURIs []string) []string

ValidateRefs checks that all tools with _meta.ui.resourceUri reference a registered resource or matching template. Returns warnings for unresolvable references. Implements core.RefValidator.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL