MCP App
One service-readiness workflow, expressed once and emitted as every surface a host can reach: a real local MCP server, a typed tool, an interactive MCP App resource, a Skill, a session-start hook, a fixture-check script, and a deterministic eval. It is the example to read when you want to see how the surfaces fit together instead of studying one of them alone.
- Run from the repository root:
pnpm example:mcp-app - Package:
@agent-bundle-example/mcp-app - Public dependencies:
agent-bundle(workspace:*),@agent-bundle/runtime,react,zod;@rstest/browser,@rstest/core, andplaywrightfor the browser-app test - Targets:
portable,codex,claude: every target ships thestatusserver and its App - Credentials: none: the eval and the fixture check read only checked-in data
- Source:
examples/mcp-app
What it proves
-
Routes are the registration. The config names no server, tool, resource, or App. The
statusserver exists becausesrc/mcp/status/**holds three route modules:tools/show-status.tsx(one service's health),resources/readiness-policy.tsx(the release rule atpolicy://mcp-app-example/readiness), andapps/status.ts+status.html(the App the tool opens through_meta.ui.resourceUri, spelled once withappResourceUri('status')).show-statususesdefineTool, so that App binding, its schemas, and its handler stay in one definition. There is no handwrittenMcpServer, noregisterTool, and no@modelcontextprotocol/serverdependency. See MCP servers and MCP Apps. -
The generated stdio lifecycle is not yours to write. The build emits the server entry and its lifecycle shell: the console-to-stderr guard, signal handling, stdin-EOF exit, bounded shutdown, and the heartbeat.
-
An MCP App is a compiled resource, not a served page. The App compiles once, to
mcp-apps/status.htmlin the artifact root with a stableresourceUri, and is inlined into the generatedstatusserver entry, which every selected host launches from that same root. -
Degraded evidence is first-class.
src/service-status.tsholds the schemas and thecompilerandpayments-apirecords, andpayments-apideliberately returns degraded latency. The interesting output is a failing check that is reported honestly, not a green screenshot. -
A browser surface is its own proof level.
tests/browser-app/status-panel.browser.test.tsmounts the production-compiled App HTML over the product bridge throughagent-bundle/test/browser, thebrowser-applevel in Testing. -
The App is a client of the bridge, not a transport of its own.
src/mcp/status/apps/status.tscreates its client withcreateAppClientfromagent-bundle/app, receives the openingshow-statuscall's input and structured result throughonToolInput/onToolResult, and surfaces failed or unstructured opening results throughonToolErrorregistered ontool:status/show-status. Those route listeners fire when the host's initialize result namesshow-statusas the opening tool;onToolCancelledseparately surfaces a host-cancelled opening call. Refresh status calls the sameshow-statustool again for the service on screen, and Read policy reads thereadiness-policyresource throughrequest('resources/read', …). The route id, input, and result are typed by the generated.agent-bundle/routes.d.ts, so the App declares no contract map of its own. No JSON-RPC ids, pending maps,postMessage('*'), orstructuredContentunwrapping remain in the view. See The typed App client. -
The
websurface ships inside the artifact. The example configuresweb: { apps: [{ app: 'status/status', tool: 'show-status', allow: ['call-tool'] }] }, so the composite root gains awebsection inagent-bundle.manifest.jsonandbin/mcp-app-example.mjseven withoutsrc/cli/**. From the built artifact:That prints the ready line and serves the status App on loopback.
agent-bundle devserves the same page at/web/status/status. See Exposing an App in the browser.
What is authored
Working in the Workbench
- Under Application → Skills, select
service-readiness. Its document renders by default; use the inspector for source/generated output, resources, and eval coverage. - Under Application → Events / Hooks, select the configured
sessionStarthook and choose the Claude fixture. Run it to inspect canonical input, returned context, and the Claude projection. - Under Application → Scripts, select
check-service-fixtureand run it. The emitted checker resolves the packaged status fixture beside its emitted module, so it succeeds independently of the shell's working directory. - Select the
statusApp under Application → MCP → status → Apps. The workspace opens a session against the published build withshow-statusbound to the App; choose a service and Call tool and preview to render the panel beside the tool result. Advanced → Artifact shows the composite plugin root, wheremcp-apps/status.htmlappears whichever target is selected. - Before two eval runs exist, Advanced → Evals → Compare deliberately shows
At least two recorded runs are needed before a comparison can be aligned., the precise empty state, not an error. - Open Advanced → Protocol, choose portable and the
statusserver, open the session, list tools, selectshow-status, choosepayments-api, and invoke it. Invocation history shows the degraded summary with labelled Availability and P95 latency checks, the latter failing. Open the App preview: the rendered panel shows the same record through the MCP Apps bridge, with a text-labelled amberdegradedindicator; Refresh status and Read policy call the real tool and resource. Inspect the protocol trace, use Restart MCP session, then close, reset, and reopen the session to exercise the lifecycle. Then open Trace to see the MCP request, response, notifications, and session activity joined by their session and JSON-RPC request ids. - In Advanced → Evals → Runs, select
mcp-app-status, runstatus-is-healthy, and inspect the completed passing trial attributed toservice-readiness.
If you edit a source file, rebuild and wait for a Failed or Idle state before judging the result; a Building state is still in progress.
Noninteractive checks
After the repository-level pnpm build:
pnpm check runs validation, build, and typecheck without the Workbench.
Running the server on stdio
mcp run resolves the generated entry from the portable target's MCP manifest, building a
temporary artifact first; pass --artifact artifact to reuse the pnpm build output instead.
Closing stdin exits 0 and Ctrl-C exits 130, and per-server state persists under
.agent-bundle/mcp-run/portable/status.
The command loads the project-root .env set, including the selected --mode variants, by
default. Launch environment precedence is manifest env, then .env files, then exported operator
variables. Repeatable --env-file <path> replaces the conventional files, --no-env skips them,
and --plugin-root <path> is only for a copied-artifact rehearsal. The full contract is in
Runtime environment.