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
@@ -128,7 +128,7 @@ Detailed fa-mcp-sdk docs are in `FA-MCP-SDK-DOC/`:
128
128
  | `05-ad-authorization.md` | AD group authorization, AD config |
129
129
  | `06-utilities.md` | Error handling, logging, Consul |
130
130
  | `07-testing-and-operations.md` | Test clients (STDIO, HTTP, SSE, Streamable HTTP) |
131
- | `08-agent-tester-and-headless-api.md` | Agent Tester, Headless API, structured logging, automated testing |
131
+ | `08-agent-tester-and-headless-api.md` | Agent Tester, Headless API, structured logging, automated testing, MCP Apps mode (capability negotiation, `appCalls[]`, widget rendering, App Inspector) |
132
132
  | `09-database.md` | PostgreSQL sugar layer (`queryMAIN`, `execMAIN`, upserts, `mergeByBatch`), `pgvector`, secondary DBs |
133
133
  | `10-mcp-apps.md` | Building / extending MCP Apps (UI-augmented tools) — protocol contract, SDK surface, patterns, pitfalls |
134
134
 
@@ -18,11 +18,11 @@ npm install fa-mcp-sdk
18
18
  | [03-configuration](03-configuration.md) | `appConfig`, YAML config, access points for external services, cache | Server configuration, external services |
19
19
  | [04-authentication](04-authentication.md) | JWT, Basic auth, server tokens, `createAuthMW()`, Token Generator, CLI Token Generator, JWT Generation API | Authentication setup |
20
20
  | [05-ad-authorization](05-ad-authorization.md) | AD group authorization at HTTP/tool levels | AD group restrictions |
21
- | [06-utilities](06-utilities.md) | `ServerError`, `normalizeHeaders`, logging, MCP debug switches (`DEBUG=mcp:*`), Consul, graceful shutdown | Error handling, utilities, request tracing |
22
- | [07-testing-and-operations](07-testing-and-operations.md) | Test clients (STDIO, HTTP, SSE, Streamable HTTP) | Testing, deployment |
23
- | [08-agent-tester-and-headless-api](08-agent-tester-and-headless-api.md) | Agent Tester, Headless API, structured logging, automated testing, UI `data-testid` reference | Agent-driven tool development, CLI automation, UI E2E tests |
21
+ | [06-utilities](06-utilities.md) | `ServerError`, `normalizeHeaders`, logging, MCP debug switches (`DEBUG=mcp:*`), JSON-lines sink (`mcp.debug.logFile` → `emitTrace`), built-in debug tools (`mcp.debug.builtinTools`), Consul, graceful shutdown | Error handling, utilities, request tracing, post-mortem analysis |
22
+ | [07-testing-and-operations](07-testing-and-operations.md) | Test clients (STDIO, HTTP, SSE, Streamable HTTP); universal `debug-tool` fixture covering every `CallToolResult` shape | Testing, deployment, exercising client code against image/audio/resource/error/delay variants |
23
+ | [08-agent-tester-and-headless-api](08-agent-tester-and-headless-api.md) | Agent Tester, Headless API, structured logging, automated testing, UI `data-testid` reference. **MCP Apps mode**: capability negotiation, `appCalls[]` / `app_calls[]`, widget iframe bridge, App Inspector tab | Agent-driven tool development, CLI automation, UI E2E tests, MCP Apps host for development |
24
24
  | [09-database](09-database.md) | PostgreSQL sugar layer (`queryMAIN`, `execMAIN`, `getInsertSqlMAIN`, `getMergeSqlMAIN`, `mergeByBatch`), `pgvector`, secondary DBs | Database access, upserts, batching |
25
- | [10-mcp-apps](10-mcp-apps.md) | Self-contained digest of the MCP Apps protocol + SDK pinned to `@modelcontextprotocol/ext-apps v1.7.2` (spec 2026-01-26): `ui://` resources, `_meta.ui`, JSON-RPC messages, `App` class, host context, patterns, pitfalls | Building / extending MCP Apps (UI-augmented tools) |
25
+ | [10-mcp-apps](10-mcp-apps.md) | Self-contained digest of the MCP Apps protocol + SDK pinned to `@modelcontextprotocol/ext-apps v1.7.2` (spec 2026-01-26): `ui://` resources, `_meta.ui`, JSON-RPC messages, `App` class, host context, patterns, pitfalls. **Canonical example** (`examples/mcp-apps-canonical/`, `npm run example:mcp-apps`) and widget-side debug helpers (`mcp-debug-log`, `mcp-debug-refresh`). Cross-links to Agent Tester as a dev-host (doc 08) | Building / extending MCP Apps (UI-augmented tools) |
26
26
 
27
27
  ## Key Exports
28
28
 
@@ -35,7 +35,9 @@ import { createAuthMW, generateToken, getAuthHeadersForTests, TTokenType, genera
35
35
 
36
36
  // Tools & Errors
37
37
  import {
38
- formatToolResult, asTextContent, asJson, getJsonFromResult,
38
+ formatToolResult, formatToolError, // formatToolError → sets isError: true
39
+ asTextContent, asTextError, asJson, asJsonError, // direct-shape helpers (text + structured, ok + error)
40
+ getJsonFromResult,
39
41
  TToolHandlerResponse, IToolHandlerTextResponse, IToolHandlerStructuredResponse,
40
42
  ToolExecutionError, ServerError, BaseMcpError, ValidationError, getTools,
41
43
  } from 'fa-mcp-sdk';
@@ -55,6 +57,18 @@ import { logger, fileLogger, Logger, applyLoggerSettings, trim, ppj, toError, to
55
57
  // MCP debug switches (DEBUG=mcp:tool|mcp:resource|mcp:prompt|mcp:notification or DEBUG=mcp:*)
56
58
  import { debugMcpTool, debugMcpResource, debugMcpPrompt, debugMcpNotification, debugTokenAuth } from 'fa-mcp-sdk';
57
59
 
60
+ // JSON-lines debug sink (mcp.debug.logFile) — see 06-utilities → "JSON-lines Sink"
61
+ import { emitTrace, configureDebugSink, initDebugTraceFromConfig } from 'fa-mcp-sdk';
62
+
63
+ // Built-in debug tools (enabled by mcp.debug.builtinTools=true)
64
+ // — see 06-utilities → "Built-in Debug Tools", 07-testing → "Universal debug-tool", 10-mcp-apps § 8.14
65
+ import {
66
+ BUILTIN_MCP_DEBUG_TOOLS, BUILTIN_MCP_DEBUG_TOOL_NAMES,
67
+ MCP_DEBUG_LOG_TOOL_NAME, MCP_DEBUG_REFRESH_TOOL_NAME,
68
+ isBuiltinDebugTool, handleBuiltinDebugTool,
69
+ DEBUG_TOOL, DEBUG_TOOL_NAME, handleDebugTool, registerDebugTool,
70
+ } from 'fa-mcp-sdk';
71
+
58
72
  // Test Clients
59
73
  import { McpHttpClient, McpStdioClient, McpSseClient, McpStreamableHttpClient } from 'fa-mcp-sdk';
60
74
 
@@ -56,6 +56,69 @@ The handler must return `TToolHandlerResponse` — a discriminated union of
56
56
  the value as-is to the MCP client over STDIO, SSE, and HTTP. Use `formatToolResult()`
57
57
  to pick the right shape based on `appConfig.mcp.tools.answerAs`.
58
58
 
59
+ ### Returning errors — `isError: true` vs `throw`
60
+
61
+ The MCP spec distinguishes two error classes, and the LLM behaves very differently for each:
62
+
63
+ | Error class | How to return | What the LLM sees |
64
+ |------------------------|----------------------------------------------------------|----------------------------------------------------|
65
+ | **Tool-level** | `return formatToolError(msg)` (`isError: true` in result) | Error text inside the conversation — can self-correct, retry, ask the user |
66
+ | **Protocol-level** | `throw new ToolExecutionError(name, msg)` | JSON-RPC `error` envelope — most clients surface this as a hard sandbox failure the model cannot react to |
67
+
68
+ **Use `formatToolError()` for:**
69
+
70
+ - resource not found (`Issue AITECH-1 not found`)
71
+ - business validation (`Date must be in the past`)
72
+ - upstream API returned a recoverable error (404, 422, "rate limited, retry later")
73
+ - partial success the LLM should explain to the user
74
+
75
+ **Throw for:**
76
+
77
+ - unknown tool name (`switch` default branch)
78
+ - missing required transport feature (e.g. no `Mcp-Session-Id` for stateful clients)
79
+ - genuine infrastructure failure (DB connection dead, secret missing) that the LLM cannot work around
80
+
81
+ ```typescript
82
+ import {
83
+ formatToolResult, formatToolError, ToolExecutionError,
84
+ IToolHandlerParams, TToolHandlerResponse,
85
+ } from 'fa-mcp-sdk';
86
+
87
+ export const handleToolCall = async (
88
+ params: IToolHandlerParams,
89
+ ): Promise<TToolHandlerResponse> => {
90
+ const { name, arguments: args } = params;
91
+
92
+ switch (name) {
93
+ case 'get_issue': {
94
+ const issue = await jira.findIssue(args.key);
95
+ if (!issue) {
96
+ // Tool-level: LLM sees "Issue X not found" and can ask the user to clarify.
97
+ return formatToolError(`Issue ${args.key} not found`);
98
+ }
99
+ return formatToolResult(issue);
100
+ }
101
+
102
+ default:
103
+ // Protocol-level: client routing problem, not something the LLM should retry.
104
+ throw new ToolExecutionError(name, `Unknown tool: ${name}`);
105
+ }
106
+ };
107
+ ```
108
+
109
+ Direct-shape helpers (ignore `tools.answerAs`):
110
+
111
+ ```typescript
112
+ import { asTextError, asJsonError } from 'fa-mcp-sdk';
113
+
114
+ asTextError('Not found'); // { content: [{type:'text', text:'Not found'}], isError: true }
115
+ asJsonError({ code: 'NOT_FOUND', key: 'X' }); // { structuredContent: {...}, isError: true }
116
+ ```
117
+
118
+ > **Migration tip.** If your current handler does `throw new ToolExecutionError(name, 'Not found: ...')`
119
+ > for missing resources, convert those branches to `return formatToolError('Not found: ...')`. The
120
+ > LLM will start surfacing "Such an issue does not exist" to the user instead of failing the call.
121
+
59
122
  ### Headers Access
60
123
 
61
124
  Headers are normalized to lowercase. Available in HTTP/SSE transports:
@@ -77,7 +77,8 @@ const normalized = normalizeHeaders({
77
77
 
78
78
  ```typescript
79
79
  import {
80
- getTools, formatToolResult, getJsonFromResult, asTextContent, asJson,
80
+ getTools, formatToolResult, formatToolError, getJsonFromResult,
81
+ asTextContent, asTextError, asJson, asJsonError,
81
82
  TToolHandlerResponse, IToolHandlerTextResponse, IToolHandlerStructuredResponse,
82
83
  } from 'fa-mcp-sdk';
83
84
 
@@ -87,27 +88,40 @@ const tools = await getTools(); // Get registered tools
87
88
  // Return type: TToolHandlerResponse<T> = IToolHandlerTextResponse | IToolHandlerStructuredResponse<T>
88
89
  const result = formatToolResult<{ message: string; data: object }>({ message: 'Done', data: {} });
89
90
 
91
+ // Tool-level error — `isError: true` so the LLM sees it in conversation
92
+ // and can self-correct instead of treating it as a protocol failure.
93
+ const fail = formatToolError(`Issue ${key} not found`);
94
+
90
95
  // Returns structuredContent or JSON from text depending on appConfig.mcp.tools.answerAs
91
96
  const original = getJsonFromResult<T>(result);
92
97
 
93
98
  // Direct formatting helpers (ignore tools.answerAs config):
94
- asTextContent('Hello'); // IToolHandlerTextResponse: { content: [{ type: 'text', text: 'Hello' }] }
95
- asJson({ status: 'ok' }); // IToolHandlerStructuredResponse: { structuredContent: { status: 'ok' } }
99
+ asTextContent('Hello'); // { content: [{ type: 'text', text: 'Hello' }] }
100
+ asJson({ status: 'ok' }); // { structuredContent: { status: 'ok' } }
101
+ asTextError('Not found'); // { content: [{ type: 'text', text: 'Not found' }], isError: true }
102
+ asJsonError({ code: 'NOT_FOUND' }); // { structuredContent: { code: 'NOT_FOUND' }, isError: true }
96
103
  ```
97
104
 
98
105
  ### Return Type Signatures
99
106
 
100
107
  ```typescript
101
108
  function formatToolResult<T = any>(json: T): TToolHandlerResponse<T>;
109
+ function formatToolError<T = any>(json: T): TToolHandlerResponse<T>; // sets isError: true
102
110
  function asTextContent(text: string): IToolHandlerTextResponse;
111
+ function asTextError(text: string): IToolHandlerTextResponse; // sets isError: true
103
112
  function asJson<T = any>(json: T): IToolHandlerStructuredResponse<T>;
113
+ function asJsonError<T = any>(json: T): IToolHandlerStructuredResponse<T>; // sets isError: true
104
114
  function getJsonFromResult<T = any>(result: TToolHandlerResponse | any): T;
105
115
  ```
106
116
 
107
117
  ### When to Use Which
108
118
 
109
- - **`formatToolResult()`** — Primary choice in tool handlers. Respects `appConfig.mcp.tools.answerAs` config.
110
- - **`asTextContent()` / `asJson()`** — Direct formatting, ignores `tools.answerAs`. Use when specific format needed.
119
+ - **`formatToolResult()` / `formatToolError()`** — Primary choices in tool handlers. Respect
120
+ `appConfig.mcp.tools.answerAs`. Use `formatToolError()` for *tool-level* failures (not found,
121
+ validation, upstream 4xx) so the LLM sees them in conversation. See
122
+ [02-1-tools-and-api.md → "Returning errors"](./02-1-tools-and-api.md) for the full guide.
123
+ - **`asTextContent()` / `asTextError()` / `asJson()` / `asJsonError()`** — Direct formatting,
124
+ ignore `tools.answerAs`. Use when a specific shape is required.
111
125
  - **`getJsonFromResult()`** — Inverse of `formatToolResult()`. Extracts JSON from either format. Use in tests.
112
126
 
113
127
  ## Network Utilities
@@ -291,6 +305,120 @@ the category is off. The four built-in `debugMcpTool`/`debugMcpResource`/`debugM
291
305
  them from your own code (e.g. emit a custom line inside `handle-tool-call.ts` whenever
292
306
  `debugMcpTool.enabled` is true).
293
307
 
308
+ ## JSON-lines Sink (`mcp.debug.logFile`)
309
+
310
+ `DEBUG=mcp:*` writes ANSI-coloured human-readable text to stderr — perfect for live development,
311
+ useless for post-mortem (colours, interleaved process output, no structured fields). Set
312
+ `mcp.debug.logFile` to an absolute path and the SDK additionally mirrors every `mcp:tool`,
313
+ `mcp:resource`, `mcp:prompt` event as one JSON object per line. The stderr stream is unchanged —
314
+ the sink is purely additive.
315
+
316
+ ```yaml
317
+ # config/default.yaml — or any environment override
318
+ mcp:
319
+ debug:
320
+ logFile: /var/log/mcp/server-debug.jsonl # absolute path; parent dir is created on first event
321
+ builtinTools: false # see next section
322
+ ```
323
+
324
+ Or via env (mapped through `config/custom-environment-variables.yaml`):
325
+
326
+ ```bash
327
+ MCP_DEBUG_LOG_FILE=/var/log/mcp/server.jsonl yarn start
328
+ ```
329
+
330
+ ### Event Shape
331
+
332
+ Each line is a self-contained JSON object. `ts` (ISO timestamp) and `ch` (channel) are always
333
+ present; remaining fields depend on the channel and `kind`.
334
+
335
+ ```jsonl
336
+ {"ts":"2026-05-19T12:34:56.124Z","ch":"mcp:tool","kind":"req","name":"get_rate","args":{"from":"EUR"},"corr":"a3f1c0d2"}
337
+ {"ts":"2026-05-19T12:34:56.171Z","ch":"mcp:tool","kind":"res","name":"get_rate","ms":47,"corr":"a3f1c0d2","ok":true}
338
+ {"ts":"2026-05-19T12:34:57.012Z","ch":"mcp:tool","kind":"err","name":"get_rate","ms":2998,"corr":"b9c20f3a","error":"Connection timeout"}
339
+ {"ts":"2026-05-19T12:34:57.045Z","ch":"mcp:resource","kind":"read-res","uri":"ui://weather/view.html","ms":3}
340
+ {"ts":"2026-05-19T12:34:57.090Z","ch":"mcp:prompt","kind":"get-res","name":"agent_prompt","ms":1}
341
+ ```
342
+
343
+ | Channel | `kind` values | Useful fields |
344
+ |-----------------|----------------------------------------------------------------|------------------------------|
345
+ | `mcp:tool` | `req` / `res` / `err` | `name`, `args`, `ms`, `corr` |
346
+ | `mcp:resource` | `list-req` / `list-res` / `read-req` / `read-res` / `read-err` | `uri`, `count`, `ms` |
347
+ | `mcp:prompt` | `list-req` / `list-res` / `get-req` / `get-res` / `get-err` | `name`, `count`, `ms` |
348
+ | `app:view-log` | `log` (emitted by built-in `mcp-debug-log` tool) | `type`, `payload` |
349
+
350
+ `corr` is an 8-char hex correlation ID — pair `req` ↔ `res`/`err` for one tool call.
351
+
352
+ ### Working With The File
353
+
354
+ Standard JSON toolchain works as-is:
355
+
356
+ ```bash
357
+ # p95 latency by tool
358
+ jq -r 'select(.ch=="mcp:tool" and .kind=="res") | "\(.name)\t\(.ms)"' /var/log/mcp/*.jsonl \
359
+ | sort | datamash -g 1 perc:95 2
360
+
361
+ # all errors of the last hour
362
+ jq 'select((.kind|test("err$")) and (.ts > "2026-05-19T11:00:00"))' /var/log/mcp/*.jsonl
363
+
364
+ # events pushed by widgets via mcp-debug-log
365
+ jq 'select(.ch=="app:view-log")' /var/log/mcp/*.jsonl
366
+ ```
367
+
368
+ ### Programmatic Access
369
+
370
+ If you need to write into the same channel from your own code (e.g. tag a domain event so it shows
371
+ up alongside MCP traffic), use the helpers directly:
372
+
373
+ ```typescript
374
+ import { emitTrace, configureDebugSink } from 'fa-mcp-sdk';
375
+
376
+ // At startup the SDK already calls configureDebugSink(appConfig.mcp.debug.logFile);
377
+ // re-configure on the fly only in tests.
378
+ configureDebugSink('/tmp/mcp-test.jsonl');
379
+
380
+ emitTrace('app:billing', { kind: 'charge', userId, amountCents });
381
+ // → {"ts":"…","ch":"app:billing","kind":"charge","userId":"…","amountCents":1299}
382
+ ```
383
+
384
+ `emitTrace` is a no-op when no sink is configured — the guard is cheap, leave the calls in.
385
+
386
+ ## Built-in Debug Tools (`mcp.debug.builtinTools`)
387
+
388
+ A single flag registers three SDK-provided tools that exist to be called from widget code or
389
+ integration tests, never by the LLM. All three are marked `_meta.ui.visibility: ['app']`, so MCP App
390
+ hosts (Agent Tester, Claude Desktop with apps support, etc.) hide them from the agent's tool list.
391
+
392
+ ```yaml
393
+ mcp:
394
+ debug:
395
+ builtinTools: true # or MCP_DEBUG_BUILTIN_TOOLS=true
396
+ ```
397
+
398
+ | Tool name | Caller | Purpose |
399
+ |---------------------|----------------|-----------------------------------------------------------------------------|
400
+ | `mcp-debug-log` | Widget | Push a structured event into the same channel as `DEBUG=mcp:*` / JSON-lines |
401
+ | `mcp-debug-refresh` | Widget | Read back lightweight server state (timestamp + counter) without the LLM |
402
+ | `debug-tool` | Test client | Universal CallToolResult fixture — see [07-testing-and-operations](07-testing-and-operations.md) → "Universal `debug-tool` for Integration Tests" |
403
+
404
+ The widget-facing tools are covered in [10-mcp-apps](10-mcp-apps.md) → "Widget-side debug helpers"
405
+ (the canonical example calls them through `app.callServerTool(...)`). Names and constants are
406
+ exported when you need to reference them in test code:
407
+
408
+ ```typescript
409
+ import {
410
+ MCP_DEBUG_LOG_TOOL_NAME, // 'mcp-debug-log'
411
+ MCP_DEBUG_REFRESH_TOOL_NAME, // 'mcp-debug-refresh'
412
+ DEBUG_TOOL_NAME, // 'debug-tool'
413
+ BUILTIN_MCP_DEBUG_TOOLS, // Tool[] descriptors for the two widget tools
414
+ DEBUG_TOOL, // Tool descriptor for the test fixture
415
+ } from 'fa-mcp-sdk';
416
+ ```
417
+
418
+ > Leave `builtinTools: false` in production unless a widget genuinely needs `mcp-debug-log` /
419
+ > `mcp-debug-refresh` at runtime. The tools are inert to the LLM, but they still occupy space in the
420
+ > `tools/list` payload and add a small amount of routing overhead per call.
421
+
294
422
  ## Event System
295
423
 
296
424
  ```typescript
@@ -102,6 +102,91 @@ const result = await client.callTool('my_tool', { query: 'test' }, headers);
102
102
  - **Transport parity** — same behavior across STDIO, HTTP, SSE
103
103
  - **Edge cases** — empty strings, large payloads, special characters
104
104
 
105
+ ## Universal `debug-tool` for Integration Tests
106
+
107
+ When the system-under-test is a **client** (Agent Tester, custom MCP host, CI smoke test) rather
108
+ than the server, you usually need a server that produces every kind of `CallToolResult` on demand —
109
+ text, image, audio, embedded resources, mixed blocks, `isError: true`, slow responses, large
110
+ payloads. The SDK ships a single parameterised fixture so test code never has to roll its own
111
+ fake server.
112
+
113
+ Enable it together with the other built-ins ([06-utilities](06-utilities.md) → "Built-in Debug
114
+ Tools"):
115
+
116
+ ```yaml
117
+ mcp:
118
+ debug:
119
+ builtinTools: true
120
+ ```
121
+
122
+ This appends a tool named `debug-tool` to the server's `tools/list`, hidden from the LLM via
123
+ `_meta.ui.visibility: ['app']`.
124
+
125
+ ### Input Schema
126
+
127
+ | Argument | Type / values | Default | Purpose |
128
+ |----------------------------|---------------------------------------------------------------|---------|--------------------------------------|
129
+ | `contentType` | `text` \| `image` \| `audio` \| `resource` \| `resourceLink` \| `mixed` | `text` | Which content-block type to emit. `mixed` returns one of each (ignores `multipleBlocks`) |
130
+ | `multipleBlocks` | `boolean` | `true` | Emit 3 blocks of the chosen type vs. 1 |
131
+ | `includeStructuredContent` | `boolean` | `true` | Include `result.structuredContent` with `{ config, timestamp, counter, largeInputLength? }` |
132
+ | `includeMeta` | `boolean` | `true` | Include `result._meta.debugInfo` |
133
+ | `simulateError` | `boolean` | `false` | Set `result.isError = true` (call still resolves) |
134
+ | `delayMs` | `number` ≥ 0 | none | Artificial latency for timeout / loading-state tests |
135
+ | `largeInput` | `string` | none | Large payload — echoed back as `structuredContent.largeInputLength` |
136
+
137
+ ### Example: Single Server, Every Variation
138
+
139
+ ```typescript
140
+ // tests/agent-tester/content-types.test.ts
141
+ import { McpHttpClient } from 'fa-mcp-sdk';
142
+
143
+ const client = new McpHttpClient('http://localhost:9876');
144
+
145
+ test('renders mixed text + image + audio', async () => {
146
+ const result = await client.callTool('debug-tool', { contentType: 'mixed' });
147
+ expect(result.content).toHaveLength(3);
148
+ expect(result.content.map((b: any) => b.type)).toEqual(['text', 'image', 'audio']);
149
+ });
150
+
151
+ test('isError: true is surfaced', async () => {
152
+ const result = await client.callTool('debug-tool', {
153
+ contentType: 'text',
154
+ simulateError: true,
155
+ });
156
+ expect((result as any).isError).toBe(true);
157
+ });
158
+
159
+ test('respects delayMs for loading-state tests', async () => {
160
+ const t0 = Date.now();
161
+ await client.callTool('debug-tool', { contentType: 'text', delayMs: 800 });
162
+ expect(Date.now() - t0).toBeGreaterThanOrEqual(800);
163
+ });
164
+
165
+ test('large payload survives the round trip', async () => {
166
+ const big = 'x'.repeat(200_000);
167
+ const result = await client.callTool('debug-tool', { contentType: 'text', largeInput: big });
168
+ expect((result as any).structuredContent.largeInputLength).toBe(200_000);
169
+ });
170
+ ```
171
+
172
+ ### Standalone Test Server
173
+
174
+ If you need a throw-away server outside `initMcpServer` (e.g. spinning up a bare
175
+ `@modelcontextprotocol/sdk` `McpServer` for an in-process test), use `registerDebugTool` directly:
176
+
177
+ ```typescript
178
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
179
+ import { registerDebugTool, DEBUG_TOOL_NAME } from 'fa-mcp-sdk';
180
+
181
+ const server = new McpServer({ name: 'test-fixture', version: '0.0.0' });
182
+ registerDebugTool(server);
183
+ // → callTool(DEBUG_TOOL_NAME, { contentType: 'mixed' }) works against this server.
184
+ ```
185
+
186
+ The helper accepts any object with `registerTool(name, def, handler)` — structurally compatible
187
+ with the high-level SDK API — so the SDK does not pull in a hard dependency on
188
+ `@modelcontextprotocol/sdk/server/mcp.js`.
189
+
105
190
  ## Best Practices
106
191
 
107
192
  ### Project Organization