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.
- package/cli-template/AGENTS.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +19 -5
- package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +63 -0
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +133 -5
- package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +85 -0
- package/cli-template/FA-MCP-SDK-DOC/08-agent-tester-and-headless-api.md +284 -0
- package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +90 -0
- package/cli-template/examples/mcp-apps-canonical/README.md +62 -0
- package/cli-template/examples/mcp-apps-canonical/server.ts +95 -0
- package/cli-template/examples/mcp-apps-canonical/widget/index.html +147 -0
- package/cli-template/package.json +2 -1
- package/config/_local.yaml +6 -0
- package/config/custom-environment-variables.yaml +5 -0
- package/config/default.yaml +15 -0
- package/dist/core/_types_/config.d.ts +20 -0
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/_types_/types.d.ts +13 -0
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.js +79 -2
- package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
- package/dist/core/agent-tester/services/TesterAgentService.d.ts +14 -0
- package/dist/core/agent-tester/services/TesterAgentService.d.ts.map +1 -1
- package/dist/core/agent-tester/services/TesterAgentService.js +101 -1
- package/dist/core/agent-tester/services/TesterAgentService.js.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.d.ts +1 -0
- package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.js +46 -19
- package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
- package/dist/core/agent-tester/services/mcp-apps-utils.d.ts +22 -0
- package/dist/core/agent-tester/services/mcp-apps-utils.d.ts.map +1 -0
- package/dist/core/agent-tester/services/mcp-apps-utils.js +77 -0
- package/dist/core/agent-tester/services/mcp-apps-utils.js.map +1 -0
- package/dist/core/agent-tester/types.d.ts +65 -0
- package/dist/core/agent-tester/types.d.ts.map +1 -1
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/init-mcp-server.d.ts.map +1 -1
- package/dist/core/init-mcp-server.js +46 -5
- package/dist/core/init-mcp-server.js.map +1 -1
- package/dist/core/mcp/builtin-debug-tools.d.ts +41 -0
- package/dist/core/mcp/builtin-debug-tools.d.ts.map +1 -0
- package/dist/core/mcp/builtin-debug-tools.js +75 -0
- package/dist/core/mcp/builtin-debug-tools.js.map +1 -0
- package/dist/core/mcp/debug-trace.d.ts +26 -0
- package/dist/core/mcp/debug-trace.d.ts.map +1 -0
- package/dist/core/mcp/debug-trace.js +79 -0
- package/dist/core/mcp/debug-trace.js.map +1 -0
- package/dist/core/mcp/prompts.d.ts.map +1 -1
- package/dist/core/mcp/prompts.js +11 -0
- package/dist/core/mcp/prompts.js.map +1 -1
- package/dist/core/mcp/resources.d.ts.map +1 -1
- package/dist/core/mcp/resources.js +11 -0
- package/dist/core/mcp/resources.js.map +1 -1
- package/dist/core/utils/formatToolResult.d.ts +39 -0
- package/dist/core/utils/formatToolResult.d.ts.map +1 -1
- package/dist/core/utils/formatToolResult.js +58 -0
- package/dist/core/utils/formatToolResult.js.map +1 -1
- package/dist/core/utils/testing/debug-tool.d.ts +35 -0
- package/dist/core/utils/testing/debug-tool.d.ts.map +1 -0
- package/dist/core/utils/testing/debug-tool.js +146 -0
- package/dist/core/utils/testing/debug-tool.js.map +1 -0
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +26 -1
- package/dist/core/web/server-http.js.map +1 -1
- package/dist/core/web/static/agent-tester/index.html +55 -0
- package/dist/core/web/static/agent-tester/script.js +986 -9
- package/dist/core/web/static/agent-tester/styles.css +416 -0
- 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
|
+
});
|