fa-mcp-sdk 0.4.124 → 0.4.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/cli-template/AGENTS.md +1 -1
  2. package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +19 -5
  3. package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +63 -0
  4. package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +133 -5
  5. package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +85 -0
  6. package/cli-template/FA-MCP-SDK-DOC/08-agent-tester-and-headless-api.md +284 -0
  7. package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +90 -0
  8. package/cli-template/examples/mcp-apps-canonical/README.md +62 -0
  9. package/cli-template/examples/mcp-apps-canonical/server.ts +95 -0
  10. package/cli-template/examples/mcp-apps-canonical/widget/index.html +147 -0
  11. package/cli-template/package.json +2 -1
  12. package/config/_local.yaml +6 -0
  13. package/config/custom-environment-variables.yaml +5 -0
  14. package/config/default.yaml +15 -0
  15. package/dist/core/_types_/config.d.ts +20 -0
  16. package/dist/core/_types_/config.d.ts.map +1 -1
  17. package/dist/core/_types_/types.d.ts +13 -0
  18. package/dist/core/_types_/types.d.ts.map +1 -1
  19. package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
  20. package/dist/core/agent-tester/agent-tester-router.js +79 -2
  21. package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
  22. package/dist/core/agent-tester/services/TesterAgentService.d.ts +14 -0
  23. package/dist/core/agent-tester/services/TesterAgentService.d.ts.map +1 -1
  24. package/dist/core/agent-tester/services/TesterAgentService.js +101 -1
  25. package/dist/core/agent-tester/services/TesterAgentService.js.map +1 -1
  26. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts +1 -0
  27. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
  28. package/dist/core/agent-tester/services/TesterMcpClientService.js +46 -19
  29. package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
  30. package/dist/core/agent-tester/services/mcp-apps-utils.d.ts +22 -0
  31. package/dist/core/agent-tester/services/mcp-apps-utils.d.ts.map +1 -0
  32. package/dist/core/agent-tester/services/mcp-apps-utils.js +77 -0
  33. package/dist/core/agent-tester/services/mcp-apps-utils.js.map +1 -0
  34. package/dist/core/agent-tester/types.d.ts +65 -0
  35. package/dist/core/agent-tester/types.d.ts.map +1 -1
  36. package/dist/core/index.d.ts +4 -1
  37. package/dist/core/index.d.ts.map +1 -1
  38. package/dist/core/index.js +4 -1
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/init-mcp-server.d.ts.map +1 -1
  41. package/dist/core/init-mcp-server.js +46 -5
  42. package/dist/core/init-mcp-server.js.map +1 -1
  43. package/dist/core/mcp/builtin-debug-tools.d.ts +41 -0
  44. package/dist/core/mcp/builtin-debug-tools.d.ts.map +1 -0
  45. package/dist/core/mcp/builtin-debug-tools.js +75 -0
  46. package/dist/core/mcp/builtin-debug-tools.js.map +1 -0
  47. package/dist/core/mcp/debug-trace.d.ts +26 -0
  48. package/dist/core/mcp/debug-trace.d.ts.map +1 -0
  49. package/dist/core/mcp/debug-trace.js +79 -0
  50. package/dist/core/mcp/debug-trace.js.map +1 -0
  51. package/dist/core/mcp/prompts.d.ts.map +1 -1
  52. package/dist/core/mcp/prompts.js +11 -0
  53. package/dist/core/mcp/prompts.js.map +1 -1
  54. package/dist/core/mcp/resources.d.ts.map +1 -1
  55. package/dist/core/mcp/resources.js +11 -0
  56. package/dist/core/mcp/resources.js.map +1 -1
  57. package/dist/core/utils/formatToolResult.d.ts +39 -0
  58. package/dist/core/utils/formatToolResult.d.ts.map +1 -1
  59. package/dist/core/utils/formatToolResult.js +58 -0
  60. package/dist/core/utils/formatToolResult.js.map +1 -1
  61. package/dist/core/utils/testing/debug-tool.d.ts +35 -0
  62. package/dist/core/utils/testing/debug-tool.d.ts.map +1 -0
  63. package/dist/core/utils/testing/debug-tool.js +146 -0
  64. package/dist/core/utils/testing/debug-tool.js.map +1 -0
  65. package/dist/core/web/server-http.d.ts.map +1 -1
  66. package/dist/core/web/server-http.js +26 -1
  67. package/dist/core/web/server-http.js.map +1 -1
  68. package/dist/core/web/static/agent-tester/index.html +55 -0
  69. package/dist/core/web/static/agent-tester/script.js +986 -9
  70. package/dist/core/web/static/agent-tester/styles.css +416 -0
  71. package/package.json +1 -1
@@ -309,6 +309,7 @@ Only `message` is required. `mcpConfig` is required for tool calls.
309
309
  | `agentPrompt` | no | Agent prompt to send to the LLM as the system prompt. When provided, **replaces** the MCP server's `agent_prompt`. When omitted, the MCP server's `agent_prompt` is used (if available), otherwise a built-in default |
310
310
  | `customPrompt` | no | Additional instructions appended after `agentPrompt`. Use for per-request modifiers without replacing the main prompt |
311
311
  | `modelConfig` | no | LLM model settings (model name, temperature, maxTokens, maxTurns) |
312
+ | `appMode` | no | Boolean. When `true`, advertises MCP Apps UI capability on the MCP `initialize` handshake (so the server returns UI-augmented tool variants), appends an MCP-Apps-aware system prompt, and records the would-be-rendered UI resource for each tool call in `trace.turns[].app_calls[]`. Default `false` — text-only behavior. See [MCP Apps Mode](#mcp-apps-mode) |
312
313
 
313
314
  #### Brief Response (default)
314
315
 
@@ -421,6 +422,266 @@ Compare `system_prompt_sent` and agent responses between variations to find the
421
422
 
422
423
  The headless API shares sessions with the chat UI. To start a fresh conversation, omit `sessionId`. To continue an existing conversation, pass `sessionId` from a previous response.
423
424
 
425
+ ## MCP Apps Mode
426
+
427
+ Agent Tester doubles as a developer-grade MCP Apps host. When activated, it advertises UI capability
428
+ on the MCP `initialize` handshake so the connected server can branch between text-only and
429
+ UI-augmented tool variants, renders returned UI resources inside sandboxed iframes alongside chat
430
+ messages, and exposes the same wire-level events to headless tests. The mode is **fully optional**
431
+ and orthogonal to existing features — when off, Agent Tester behaves exactly as before.
432
+
433
+ ### Toggling MCP Apps mode
434
+
435
+ The header carries a global `Apps` checkbox (test-id `at-app-mode-toggle`) visible on every tab.
436
+ Toggling it:
437
+
438
+ 1. Persists the choice in `localStorage['agentTesterAppMode']`.
439
+ 2. Reconnects the MCP client with the new capability set. The capability sent on `initialize` is:
440
+ ```json
441
+ { "capabilities": { "extensions": { "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] } } } }
442
+ ```
443
+ 3. Clears all currently mounted widget iframes (their capability context just changed).
444
+ 4. Updates the Tool Tester dropdown — tools with `_meta.ui.resourceUri` get a 🖼 marker.
445
+
446
+ The same `appMode: true` flag travels through the request body of `POST /api/chat/test`, so
447
+ headless tests can exercise both transports of the same tool from a single suite.
448
+
449
+ ### Capability negotiation
450
+
451
+ Servers MUST gate UI-only behavior on `getUiCapability(clientCapabilities)` per the MCP Apps spec
452
+ (`fa-mcp-sdk` re-exports `getUiCapability`, `hostSupportsMcpApps`, `MCP_APPS_EXTENSION_ID`,
453
+ `MCP_APPS_RESOURCE_MIME_TYPE` — see [10-mcp-apps.md](10-mcp-apps.md) §6.1.1). With Agent Tester:
454
+
455
+ | Toggle state | Sent on `initialize` | Server expected to |
456
+ |---|---|---|
457
+ | OFF (default) | no `extensions["io.modelcontextprotocol/ui"]` | return text-only `content[]`, ignore `_meta.ui.*` |
458
+ | ON | `extensions["io.modelcontextprotocol/ui"]: { mimeTypes: ["text/html;profile=mcp-app"] }` | return text fallback **and** UI resource (embedded `content[]` entry or via tool's `_meta.ui.resourceUri`) |
459
+
460
+ Connection caching in the agent-tester service keys on `appMode`, so flipping the toggle always
461
+ forces a fresh `Client` — old text-only handles are never reused for an app-mode session.
462
+
463
+ ### `appCalls[]` in `/api/chat/message` responses
464
+
465
+ When `appMode` is on, every tool invocation made during the turn produces an `appCalls[]` entry on
466
+ the chat response. Each entry pairs the OpenAI `tool_call_id` with the **untruncated**
467
+ `CallToolResult` and, when available, the UI resource the host would render:
468
+
469
+ ```json
470
+ {
471
+ "id": "msg-…",
472
+ "message": "Here are the results — see the chart below.",
473
+ "sessionId": "abc",
474
+ "metadata": { "response_time": 1842, "tools_used": ["get_weather"] },
475
+ "appCalls": [
476
+ {
477
+ "callId": "call_abc123",
478
+ "toolName": "get_weather",
479
+ "arguments": { "location": "London" },
480
+ "result": { "content": [{ "type": "text", "text": "{...}" }], "structuredContent": { /* ... */ } },
481
+ "uiResource": {
482
+ "uri": "ui://weather/view.html",
483
+ "mimeType": "text/html;profile=mcp-app",
484
+ "text": "<!DOCTYPE html>...",
485
+ "meta": { "csp": { "connectDomains": ["https://api.openweathermap.org"] }, "prefersBorder": true }
486
+ }
487
+ }
488
+ ]
489
+ }
490
+ ```
491
+
492
+ The LLM context still receives the truncated tool result (the standard `agentTester.modelConfig
493
+ .toolResultLimitChars` truncation continues to apply); `appCalls[]` carries the full payload for
494
+ the UI bridge so the widget sees what the server actually returned.
495
+
496
+ Two extraction paths are supported in priority order:
497
+
498
+ 1. **Embedded resource** — a `content[]` block of type `resource` whose `mimeType` is
499
+ `text/html;profile=mcp-app`. Used as-is.
500
+ 2. **`_meta.ui.resourceUri`** — when the tool definition (preserved via `tools/list`) carries this
501
+ field, Agent Tester issues `resources/read` against the URI and uses the returned
502
+ `mcp-app`-typed content.
503
+
504
+ When neither path yields a UI resource, `uiResource` is omitted but the `appCall` entry is still
505
+ present — useful for tests that assert "this app-mode call did **not** return a widget".
506
+
507
+ ### `app_calls[]` in `/api/chat/test` traces
508
+
509
+ The headless API exposes the same information on `trace.turns[].app_calls[]`:
510
+
511
+ ```json
512
+ {
513
+ "trace": {
514
+ "turns": [
515
+ {
516
+ "turn": 1,
517
+ "tool_calls": [{ "name": "get_weather", "arguments": { "location": "London" } }],
518
+ "tool_results": [{ "name": "get_weather", "result": { "...": "..." }, "duration_ms": 230 }],
519
+ "app_calls": [
520
+ {
521
+ "callId": "call_abc123",
522
+ "toolName": "get_weather",
523
+ "arguments": { "location": "London" },
524
+ "result": { "...": "full untruncated result..." },
525
+ "uiResource": { "uri": "ui://weather/view.html", "mimeType": "text/html;profile=mcp-app", "text": "<!DOCTYPE html>..." }
526
+ }
527
+ ]
528
+ }
529
+ ]
530
+ }
531
+ }
532
+ ```
533
+
534
+ Headless never mounts an iframe — `app_calls[]` is the **trace** of what would have been
535
+ delivered to a real host. This lets test authors assert that a tool correctly ships both
536
+ representations (text and UI) without needing a browser.
537
+
538
+ ### Writing automated tests for both modes
539
+
540
+ Pattern: assert that a single tool behaves correctly under both capability sets in one suite. The
541
+ text-only branch verifies fallback contract; the app-mode branch verifies the UI delivery path.
542
+
543
+ ```typescript
544
+ import { describe, expect, test } from '@jest/globals';
545
+
546
+ const baseUrl = process.env.MCP_BASE_URL ?? 'http://localhost:9876';
547
+ const mcpConfig = { url: `${baseUrl}/mcp`, transport: 'http' as const };
548
+
549
+ async function run(message: string, appMode: boolean) {
550
+ const r = await fetch(`${baseUrl}/agent-tester/api/chat/test`, {
551
+ method: 'POST',
552
+ headers: { 'Content-Type': 'application/json' },
553
+ body: JSON.stringify({ message, mcpConfig, appMode }),
554
+ });
555
+ return r.json();
556
+ }
557
+
558
+ describe('get_weather honors host capabilities', () => {
559
+ test('text-only host gets text response', async () => {
560
+ const r = await run('What is the weather in London?', false);
561
+ const firstTool = r.trace.turns[0].tool_results[0];
562
+ expect(firstTool.result.content[0].type).toBe('text');
563
+ expect(r.trace.turns[0].app_calls).toBeUndefined();
564
+ });
565
+
566
+ test('app-mode host gets UI resource', async () => {
567
+ const r = await run('What is the weather in London?', true);
568
+ const apps = r.trace.turns[0].app_calls;
569
+ expect(apps).toHaveLength(1);
570
+ expect(apps[0].uiResource.mimeType).toBe('text/html;profile=mcp-app');
571
+ expect(apps[0].uiResource.text).toMatch(/<html|<body|<div/);
572
+ });
573
+
574
+ test('text fallback still present in app-mode (spec compliance)', async () => {
575
+ // Per MCP Apps spec, content[] MUST contain a meaningful text representation
576
+ // even when UI is supported. This catches servers that drop text in app-mode.
577
+ const r = await run('What is the weather in London?', true);
578
+ const fullResult = r.trace.turns[0].app_calls[0].result;
579
+ expect(fullResult.content?.some((c: any) => c.type === 'text' && c.text)).toBe(true);
580
+ });
581
+ });
582
+ ```
583
+
584
+ Pair this with `data-testid`-based Playwright tests that mount the actual iframe (see
585
+ [UI Test Selectors](#ui-test-selectors-data-testid) below) for full end-to-end coverage.
586
+
587
+ ### `uiResource` in `/api/mcp/call-tool` (Tool Tester support)
588
+
589
+ When the active MCP session is in app-mode, the direct invocation endpoint also extracts and
590
+ returns the UI resource:
591
+
592
+ ```
593
+ POST /agent-tester/api/mcp/call-tool
594
+ ```
595
+
596
+ Request:
597
+ ```json
598
+ { "serverName": "my-mcp", "toolName": "get_weather", "parameters": { "location": "London" } }
599
+ ```
600
+
601
+ Response (with appMode active on the connected server):
602
+ ```json
603
+ {
604
+ "success": true,
605
+ "durationMs": 230,
606
+ "result": { "content": [...], "structuredContent": {...} },
607
+ "uiResource": { "uri": "ui://...", "mimeType": "text/html;profile=mcp-app", "text": "<!DOCTYPE...", "meta": {...} }
608
+ }
609
+ ```
610
+
611
+ The Tool Tester tab uses this to render a split-view: raw JSON on the left, mounted widget on the
612
+ right. The widget runs the full handshake (`ui/initialize` → `tool-input` → `tool-result`) the
613
+ same way it would inside a chat message, so you can iterate on widget HTML without a chat agent
614
+ in the loop.
615
+
616
+ ### `GET /api/mcp/ui-resources`
617
+
618
+ Lists all UI resources advertised by a connected server. Used by the App Inspector tab; available
619
+ for headless inventory checks.
620
+
621
+ ```
622
+ GET /agent-tester/api/mcp/ui-resources?serverName=<name>
623
+ ```
624
+
625
+ Response:
626
+ ```json
627
+ {
628
+ "resources": [
629
+ {
630
+ "uri": "ui://weather/view.html",
631
+ "name": "Weather View",
632
+ "mimeType": "text/html;profile=mcp-app",
633
+ "description": "Interactive weather display"
634
+ }
635
+ ]
636
+ }
637
+ ```
638
+
639
+ Filter logic: keeps resources whose `mimeType` is `text/html;profile=mcp-app` **or** whose `uri`
640
+ starts with `ui://`. Returns `404` when the named server is not connected.
641
+
642
+ ### App Inspector tab
643
+
644
+ A third tab (test-id `at-tab-inspector`) appears next to Chat / Tool Tester. It surfaces:
645
+
646
+ - **App Tools** — every tool from the connected server with a `🖼 UI` flag for those that carry
647
+ `_meta.ui.resourceUri`. Each app-tool gets a "Launch widget" button that runs the tool with an
648
+ arguments JSON (prompted) and mounts the returned widget in a modal — useful for iterating on a
649
+ widget without going through chat or Tool Tester.
650
+ - **UI Resources** — output of `GET /api/mcp/ui-resources` for the connected server.
651
+ - **UI Message Log** — live capture of every JSON-RPC frame passing through the iframe bridges
652
+ (host→view, view→host, View-initiated tool calls, log notifications). Filterable by direction.
653
+ Last 500 entries kept in memory.
654
+
655
+ The Inspector is the recommended surface for debugging widget protocol issues — handshake
656
+ failures, missing `tool-input` notifications, unhandled View requests all surface here.
657
+
658
+ ### Sandbox & security model (developer-mode trade-offs)
659
+
660
+ Agent Tester implements the **desktop-style** host pattern from the spec (§6.1 of the original
661
+ proposal): a single iframe on the same origin as the host page, with CSP applied via
662
+ `<meta http-equiv="Content-Security-Policy">` inside `srcdoc`. The directive list is built from
663
+ `_meta.ui.csp` (`connectDomains`, `resourceDomains`, `frameDomains`, `baseUriDomains`) — same
664
+ mapping as the canonical web hosts. Permission Policy comes from `_meta.ui.permissions` and is
665
+ attached to the iframe's `allow=` attribute.
666
+
667
+ Notable developer-mode trade-offs (accepted because this is a dev tool, not a production host):
668
+
669
+ - **Meta-CSP is theoretically bypassable** (a malicious View could try to inject another `<meta>`
670
+ before the host's, though Agent Tester injects the CSP meta as the first child of `<head>` so
671
+ this is unlikely in practice). Production hosts SHOULD use HTTP headers from a separate
672
+ sandbox origin — see the spec digest for guidance.
673
+ - **View → Host `tools/call`** is proxied to the agent-tester's own `/api/mcp/call-tool` endpoint
674
+ using whatever auth the user already configured. The first such call within a session shows a
675
+ confirm modal ("Widget for tool X wants to call tool Y — Allow? / Deny?") with an opt-in
676
+ "don't ask again in this session" checkbox (stored in `sessionStorage`).
677
+ - **Live-widget cap**: up to 5 mounted iframes at once. Older widgets demote to a "poster" with a
678
+ reload hint to bound memory in long chat sessions.
679
+
680
+ ### Configuration
681
+
682
+ No new top-level config keys. App-mode behavior is purely runtime — controlled by the user toggle
683
+ or the `appMode` request flag. The existing `agentTester.*` options still apply.
684
+
424
685
  ## Structured JSON Logging (`agentTester.logJson`)
425
686
 
426
687
  When `agentTester.logJson` is `true`, each agent event is emitted as a single-line JSON object on stdout — useful for real-time monitoring, debugging, and log aggregation.
@@ -605,11 +866,34 @@ The sidebar shows only the current model name (read-only) and a gear button. All
605
866
  | testid | Element |
606
867
  |---|---|
607
868
  | `at-sidebar-toggle-mobile` | Mobile sidebar toggle |
869
+ | `at-tab-chat` / `at-tab-tool-tester` / `at-tab-inspector` | Tab switcher buttons (Chat / Tool Tester / App Inspector) |
870
+ | `at-app-mode-toggle` | MCP Apps mode checkbox — toggles app-mode capability and widget rendering |
871
+ | `at-app-mode-toggle-label` | Wrapping `<label>` of the checkbox (carries `is-disabled` class when transport is unsupported) |
608
872
  | `at-default-format` | Default display format `<select>` (HTML / MD) |
609
873
  | `at-theme-toggle` | Light/dark theme toggle |
610
874
  | `at-clear-chat` | Clear chat button |
611
875
  | `at-logout-btn` | Logout button (visible only when `useAuth` is true) |
612
876
 
877
+ **Tool Tester — MCP Apps split-view (only when app-mode is on AND the response carries a `uiResource`)**
878
+
879
+ | testid | Element |
880
+ |---|---|
881
+ | `at-tt-ui-panel` | Third panel mounted next to Request/Response when a UI widget is rendered |
882
+
883
+ **App Inspector tab**
884
+
885
+ | testid | Element |
886
+ |---|---|
887
+ | `at-tab-pane-inspector` | Inspector pane container |
888
+ | `at-inspector-tools-panel` | Left column: tools + resources |
889
+ | `at-inspector-tools-list` | App Tools list container; each tool item carries `has-ui` class when it ships a UI resource |
890
+ | `at-inspector-resources-list` | UI Resources list container |
891
+ | `at-inspector-refresh` | Refresh button (re-queries `/api/mcp/ui-resources` and re-renders tools) |
892
+ | `at-inspector-log-panel` | Right column: live `ui/*` JSON-RPC log |
893
+ | `at-inspector-log` | Log `<pre>` (newest entry at bottom; capped at 500) |
894
+ | `at-inspector-log-filter` | Direction filter `<select>` (All / view→host / host→view / view→host tool-call) |
895
+ | `at-inspector-log-clear` | Clear log button |
896
+
613
897
  **Chat area**
614
898
 
615
899
  | testid | Element |
@@ -29,6 +29,38 @@ hosts (MCP-UI vs. OpenAI Apps SDK fragmentation), (b) inconsistent security mode
29
29
  content, and (c) inability to ship rich interactive surfaces (dashboards, players, forms, maps)
30
30
  through the protocol.
31
31
 
32
+ ### Hosts that ship with this SDK
33
+
34
+ `fa-mcp-sdk` includes **Agent Tester** (`/agent-tester`) — a developer-grade MCP Apps host. Toggle
35
+ the `Apps` checkbox in the header to advertise UI capability on the MCP `initialize` handshake;
36
+ returned widgets render inline under each chat message, Tool Tester gains a split-view raw/UI
37
+ panel, and a dedicated **App Inspector** tab surfaces every `ui/*` JSON-RPC frame for debugging.
38
+ The same `appMode: true` flag is accepted on the headless `POST /api/chat/test` endpoint so
39
+ automated tests can assert both text-only and UI-augmented behaviors of the same tool in one
40
+ suite. See [08-agent-tester-and-headless-api.md](08-agent-tester-and-headless-api.md) → "MCP Apps
41
+ Mode" for the full surface (capability negotiation, `appCalls[]` / `app_calls[]`, the
42
+ `/api/mcp/ui-resources` endpoint, security trade-offs of desktop-style hosting).
43
+
44
+ This is intentionally a **dev-tool host** — for production MCP App rendering use Claude Desktop,
45
+ Claude.ai, or another spec-compliant host listed below.
46
+
47
+ ### Canonical example
48
+
49
+ A minimal, runnable reference lives at `examples/mcp-apps-canonical/`:
50
+
51
+ ```bash
52
+ npm run example:mcp-apps # starts on :7080
53
+ # then open http://localhost:7080/agent-tester, toggle Apps, ask "what time is it?"
54
+ ```
55
+
56
+ The example is the smallest possible MCP Apps server (one tool, one widget, no
57
+ build step) and is the canonical reference for the `mcp-app-create` and
58
+ `mcp-app-add-to-server` skills. Copy the three patterns documented in its
59
+ README when wiring MCP Apps into your own project. The structure intentionally
60
+ uses `fa-mcp-sdk`'s `initMcpServer` + `customResources` instead of
61
+ `registerAppTool`/`registerAppResource` so it shares the same auth, transport,
62
+ and logging plumbing as every other tool you ship.
63
+
32
64
  ## 3. Architecture
33
65
 
34
66
  Three entities cooperate over two transports:
@@ -765,6 +797,64 @@ app.sendLog({ level: "info", logger: "WeatherApp", data: "Refreshed forecast" })
765
797
 
766
798
  `basic-host` surfaces these in the console panel; production hosts MAY route them to telemetry.
767
799
 
800
+ ### 8.14 Widget-side debug helpers (`mcp-debug-log` / `mcp-debug-refresh`)
801
+
802
+ `app.sendLog` (above) is a host-side concern — what reaches your server logs depends on the host's
803
+ telemetry settings. When you need the widget to push events **into the same channel as your server
804
+ debug stream** (so they show up in `DEBUG=mcp:*` and the JSON-lines file from
805
+ [06-utilities](06-utilities.md) → "JSON-lines Sink"), enable the SDK's built-in helper tools:
806
+
807
+ ```yaml
808
+ # config/default.yaml
809
+ mcp:
810
+ debug:
811
+ builtinTools: true
812
+ ```
813
+
814
+ This registers two app-only tools (hidden from the LLM via `_meta.ui.visibility: ['app']`):
815
+
816
+ ```ts
817
+ // from inside widget JS — replace `host.postMessage` with whatever JSON-RPC bridge you use
818
+ await app.callServerTool({
819
+ name: 'mcp-debug-log',
820
+ arguments: {
821
+ type: 'render-error',
822
+ payload: { stack: err.stack, viewState: snapshot },
823
+ },
824
+ });
825
+ // → server-side: emits {"ch":"app:view-log","kind":"log","type":"render-error","payload":{...}}
826
+
827
+ const fresh = await app.callServerTool({ name: 'mcp-debug-refresh', arguments: {} });
828
+ // fresh.structuredContent === { timestamp: '2026-05-19T08:34:12.115Z', counter: 47 }
829
+ ```
830
+
831
+ Use `mcp-debug-log` to capture client-side errors, user-action breadcrumbs, or view-state snapshots
832
+ without owning a logger, fetch client, or JWT in the widget. Use `mcp-debug-refresh` for polling /
833
+ heartbeat scenarios where the widget needs lightweight server state but you don't want the LLM to
834
+ see the call.
835
+
836
+ ### 8.15 Canonical example
837
+
838
+ The smallest working server lives at `examples/mcp-apps-canonical/` (added to your project by the
839
+ CLI template). Run it with:
840
+
841
+ ```bash
842
+ npm run example:mcp-apps # starts on :7080
843
+ # then open http://localhost:7080/agent-tester, toggle Apps, ask "what time is it?"
844
+ ```
845
+
846
+ The example demonstrates exactly the three patterns above:
847
+
848
+ - `tools[i]._meta.ui.resourceUri` linking a tool to its widget (server.ts).
849
+ - `customResources[i]` serving the `ui://` HTML with the right MIME type (server.ts).
850
+ - The `ui/initialize` → `ui/notifications/initialized` → `ui/notifications/tool-result` handshake
851
+ inside an inlined-CSP widget (widget/index.html).
852
+
853
+ The example uses `fa-mcp-sdk`'s `initMcpServer` + `customResources` rather than
854
+ `registerAppTool`/`registerAppResource` so it inherits the same auth, transport, debug, and logging
855
+ plumbing as the rest of your server. Use it as the copy-paste starting point — the `mcp-app-create`
856
+ and `mcp-app-add-to-server` skills both point here.
857
+
768
858
  ## 9. Authorization
769
859
 
770
860
  Apps inherit MCP's OAuth model (see the upstream MCP spec § Basic / Authorization). Two patterns
@@ -0,0 +1,62 @@
1
+ # MCP Apps — Canonical Example
2
+
3
+ The smallest working `fa-mcp-sdk` server that ships an MCP App: one tool, one UI
4
+ resource, one self-contained widget. Use it as the reference when adding MCP
5
+ Apps to your own server.
6
+
7
+ ## Layout
8
+
9
+ ```
10
+ examples/mcp-apps-canonical/
11
+ ├── server.ts # registers one tool + one ui:// resource
12
+ ├── widget/
13
+ │ └── index.html # the View (vanilla HTML+JS, no build step)
14
+ └── README.md
15
+ ```
16
+
17
+ - **`server.ts` (`tools[]`, `customResources[]`)** — read this first. It shows
18
+ the exact `_meta.ui.resourceUri` link from a tool to its widget and how to
19
+ serve the widget through `customResources` with mime type
20
+ `text/html;profile=mcp-app`.
21
+ - **`widget/index.html`** — implements the MCP Apps View handshake
22
+ (`ui/initialize` → `ui/notifications/initialized` → `ui/notifications/tool-result`)
23
+ and a button that triggers `tools/call` from inside the iframe. Everything is
24
+ inlined so the widget runs under the spec's restrictive default CSP.
25
+
26
+ ## Run
27
+
28
+ ```bash
29
+ npm install
30
+ npm run example:mcp-apps
31
+ ```
32
+
33
+ The script starts the server on port **7080** (overriding the parent project's
34
+ default via `WS_PORT=7080`).
35
+
36
+ 1. Open <http://localhost:7080/agent-tester>.
37
+ 2. Toggle the **Apps** checkbox in the header to advertise MCP Apps capability.
38
+ 3. Ask: `What time is it?`
39
+ 4. The agent calls `get-time`; the widget appears under the assistant message
40
+ and shows the timestamp. Press **Refresh** to call the tool again from the
41
+ widget without going through the LLM.
42
+
43
+ ## What to copy into your own server
44
+
45
+ 1. **`tools[i]._meta.ui.resourceUri`** — `server.ts`, the `tools` array. Points
46
+ the tool at the widget. Without this, the host renders nothing.
47
+ 2. **`customResources[i]`** — `server.ts`, the `customResources` array. Serves
48
+ the `ui://` HTML; the `mimeType` MUST be the constant
49
+ `MCP_APPS_RESOURCE_MIME_TYPE`. Use `content: async () => fs.readFile(...)`
50
+ for file-backed widgets or `content: '<html>...</html>'` for tiny inline ones.
51
+ 3. **Widget handshake** — `widget/index.html`, ~50 lines below the styles. Every
52
+ MCP Apps widget MUST send `ui/initialize` then `ui/notifications/initialized`
53
+ before reading `ui/notifications/tool-result`. Use `tools/call` from the View
54
+ for user-driven server calls.
55
+
56
+ ## Capability fallback
57
+
58
+ The spec requires a meaningful text response even for hosts that don't render
59
+ widgets. `get-time` always returns the timestamp in `structuredContent` /
60
+ `content[]` — the widget is a progressive enhancement. Use `getUiCapability`
61
+ from `fa-mcp-sdk` to branch handlers when the UI-vs-text divergence is larger
62
+ than a couple of formatting tweaks.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Canonical MCP Apps example for fa-mcp-sdk.
3
+ *
4
+ * A minimal HTTP MCP server that registers one tool (`get-time`) with a UI
5
+ * widget. The widget renders the timestamp inside the host's iframe and can
6
+ * call back into the server via `app.callServerTool('get-time')`.
7
+ *
8
+ * Run:
9
+ * npm run example:mcp-apps
10
+ * # then open http://localhost:7080/agent-tester, toggle Apps, ask
11
+ * # "what time is it?"
12
+ *
13
+ * What to copy when adding MCP Apps to your own server:
14
+ * - `tools[]` — `_meta.ui.resourceUri` links the tool to the widget.
15
+ * - `customResources[]` — serves the `ui://` HTML; `mimeType` MUST be
16
+ * `MCP_APPS_RESOURCE_MIME_TYPE`.
17
+ * - `toolHandler` — branch on `getUiCapability(...)` to provide a
18
+ * text-only fallback for non-MCP-Apps hosts.
19
+ */
20
+ import fs from 'node:fs/promises';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
25
+ import {
26
+ IResourceData,
27
+ IToolHandlerParams,
28
+ MCP_APPS_RESOURCE_MIME_TYPE,
29
+ McpServerData,
30
+ TToolHandlerResponse,
31
+ formatToolResult,
32
+ initMcpServer,
33
+ } from 'fa-mcp-sdk';
34
+
35
+ const THIS_DIR = path.dirname(fileURLToPath(import.meta.url));
36
+ const WIDGET_HTML_PATH = path.join(THIS_DIR, 'widget', 'index.html');
37
+ const RESOURCE_URI = 'ui://get-time/view.html';
38
+
39
+ const tools: Tool[] = [
40
+ {
41
+ name: 'get-time',
42
+ title: 'Get current server time',
43
+ description:
44
+ 'Returns the current server timestamp. When the host supports MCP Apps, the response ' +
45
+ 'is rendered by an interactive widget; otherwise the agent sees the timestamp as plain text.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {},
49
+ },
50
+ _meta: {
51
+ ui: { resourceUri: RESOURCE_URI },
52
+ },
53
+ },
54
+ ];
55
+
56
+ const customResources: IResourceData[] = [
57
+ {
58
+ uri: RESOURCE_URI,
59
+ name: 'get-time-widget',
60
+ description: 'MCP Apps widget that renders the get-time tool result.',
61
+ mimeType: MCP_APPS_RESOURCE_MIME_TYPE,
62
+ content: async () => fs.readFile(WIDGET_HTML_PATH, 'utf-8'),
63
+ _meta: {
64
+ ui: {
65
+ preferredFrameSize: ['100%', '180px'],
66
+ },
67
+ },
68
+ },
69
+ ];
70
+
71
+ const toolHandler = async (params: IToolHandlerParams): Promise<TToolHandlerResponse> => {
72
+ if (params.name !== 'get-time') {
73
+ throw new Error(`Unknown tool: ${params.name}`);
74
+ }
75
+ return formatToolResult({
76
+ timestamp: new Date().toISOString(),
77
+ iso: new Date().toISOString(),
78
+ });
79
+ };
80
+
81
+ const serverData: McpServerData = {
82
+ tools,
83
+ toolHandler,
84
+ agentBrief: 'Canonical MCP Apps example — returns server time with an interactive widget.',
85
+ agentPrompt:
86
+ 'You are the demo agent for the canonical MCP Apps example. ' +
87
+ 'When the user asks about time, call the get-time tool. The host renders the response in a widget; ' +
88
+ 'briefly acknowledge the result without restating the timestamp verbatim.',
89
+ customResources,
90
+ };
91
+
92
+ initMcpServer(serverData).catch((error) => {
93
+ console.error('Failed to start canonical MCP Apps example:', error);
94
+ process.exit(1);
95
+ });