@frontmcp/sdk 0.5.0 → 0.6.0

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 (226) hide show
  1. package/README.md +3 -3
  2. package/package.json +8 -19
  3. package/src/adapter/adapter.instance.js +5 -0
  4. package/src/adapter/adapter.instance.js.map +1 -1
  5. package/src/auth/authorization/authorization.class.d.ts +1 -4
  6. package/src/auth/authorization/authorization.class.js +6 -13
  7. package/src/auth/authorization/authorization.class.js.map +1 -1
  8. package/src/auth/flows/session.verify.flow.d.ts +1 -0
  9. package/src/auth/flows/session.verify.flow.js +11 -1
  10. package/src/auth/flows/session.verify.flow.js.map +1 -1
  11. package/src/auth/flows/well-known.jwks.flow.js +2 -2
  12. package/src/auth/flows/well-known.jwks.flow.js.map +1 -1
  13. package/src/auth/jwks/dev-key-persistence.d.ts +63 -0
  14. package/src/auth/jwks/dev-key-persistence.js +219 -0
  15. package/src/auth/jwks/dev-key-persistence.js.map +1 -0
  16. package/src/auth/jwks/index.d.ts +1 -0
  17. package/src/auth/jwks/index.js +1 -0
  18. package/src/auth/jwks/index.js.map +1 -1
  19. package/src/auth/jwks/jwks.service.d.ts +7 -4
  20. package/src/auth/jwks/jwks.service.js +81 -12
  21. package/src/auth/jwks/jwks.service.js.map +1 -1
  22. package/src/auth/jwks/jwks.types.d.ts +7 -0
  23. package/src/auth/jwks/jwks.types.js.map +1 -1
  24. package/src/auth/machine-id.d.ts +5 -0
  25. package/src/auth/machine-id.js +32 -0
  26. package/src/auth/machine-id.js.map +1 -0
  27. package/src/auth/session/index.d.ts +1 -0
  28. package/src/auth/session/index.js +3 -1
  29. package/src/auth/session/index.js.map +1 -1
  30. package/src/auth/session/record/session.base.js +5 -3
  31. package/src/auth/session/record/session.base.js.map +1 -1
  32. package/src/auth/session/record/session.stateless.d.ts +2 -2
  33. package/src/auth/session/record/session.stateless.js +5 -3
  34. package/src/auth/session/record/session.stateless.js.map +1 -1
  35. package/src/auth/session/redis-session.store.d.ts +64 -0
  36. package/src/auth/session/redis-session.store.js +204 -0
  37. package/src/auth/session/redis-session.store.js.map +1 -0
  38. package/src/auth/session/session.service.d.ts +0 -2
  39. package/src/auth/session/session.service.js +1 -7
  40. package/src/auth/session/session.service.js.map +1 -1
  41. package/src/auth/session/transport-session.manager.js +3 -5
  42. package/src/auth/session/transport-session.manager.js.map +1 -1
  43. package/src/auth/session/transport-session.types.d.ts +4 -0
  44. package/src/auth/session/transport-session.types.js +4 -3
  45. package/src/auth/session/transport-session.types.js.map +1 -1
  46. package/src/auth/session/utils/session-id.utils.d.ts +12 -1
  47. package/src/auth/session/utils/session-id.utils.js +48 -9
  48. package/src/auth/session/utils/session-id.utils.js.map +1 -1
  49. package/src/auth/ui/base-layout.d.ts +0 -8
  50. package/src/auth/ui/base-layout.js +1 -14
  51. package/src/auth/ui/base-layout.js.map +1 -1
  52. package/src/auth/ui/index.d.ts +3 -4
  53. package/src/auth/ui/index.js +10 -11
  54. package/src/auth/ui/index.js.map +1 -1
  55. package/src/auth/ui/{htmx-templates.d.ts → templates.d.ts} +5 -6
  56. package/src/auth/ui/{htmx-templates.js → templates.js} +8 -15
  57. package/src/auth/ui/templates.js.map +1 -0
  58. package/src/common/decorators/decorator-utils.js.map +1 -1
  59. package/src/common/decorators/front-mcp.decorator.js +28 -2
  60. package/src/common/decorators/front-mcp.decorator.js.map +1 -1
  61. package/src/common/index.d.ts +0 -1
  62. package/src/common/index.js +0 -1
  63. package/src/common/index.js.map +1 -1
  64. package/src/common/interfaces/adapter.interface.d.ts +6 -0
  65. package/src/common/interfaces/adapter.interface.js.map +1 -1
  66. package/src/common/interfaces/execution-context.interface.d.ts +52 -3
  67. package/src/common/interfaces/execution-context.interface.js +88 -3
  68. package/src/common/interfaces/execution-context.interface.js.map +1 -1
  69. package/src/common/interfaces/flow.interface.d.ts +13 -0
  70. package/src/common/interfaces/flow.interface.js +24 -0
  71. package/src/common/interfaces/flow.interface.js.map +1 -1
  72. package/src/common/interfaces/server.interface.d.ts +9 -0
  73. package/src/common/interfaces/server.interface.js.map +1 -1
  74. package/src/common/metadata/app.metadata.d.ts +108 -0
  75. package/src/common/metadata/front-mcp.metadata.d.ts +659 -2
  76. package/src/common/metadata/front-mcp.metadata.js +3 -1
  77. package/src/common/metadata/front-mcp.metadata.js.map +1 -1
  78. package/src/common/metadata/provider.metadata.d.ts +14 -0
  79. package/src/common/metadata/provider.metadata.js +18 -2
  80. package/src/common/metadata/provider.metadata.js.map +1 -1
  81. package/src/common/metadata/tool.metadata.d.ts +33 -1
  82. package/src/common/metadata/tool.metadata.js.map +1 -1
  83. package/src/common/migrate/auth-transport.migrate.d.ts +62 -0
  84. package/src/common/migrate/auth-transport.migrate.js +140 -0
  85. package/src/common/migrate/auth-transport.migrate.js.map +1 -0
  86. package/src/common/migrate/index.d.ts +1 -0
  87. package/src/common/migrate/index.js +6 -0
  88. package/src/common/migrate/index.js.map +1 -0
  89. package/src/common/schemas/http-output.schema.d.ts +10 -2
  90. package/src/common/schemas/index.d.ts +1 -0
  91. package/src/common/schemas/index.js +1 -0
  92. package/src/common/schemas/index.js.map +1 -1
  93. package/src/common/schemas/session-header.schema.d.ts +16 -0
  94. package/src/common/schemas/session-header.schema.js +42 -0
  95. package/src/common/schemas/session-header.schema.js.map +1 -0
  96. package/src/common/tokens/front-mcp.tokens.js +3 -1
  97. package/src/common/tokens/front-mcp.tokens.js.map +1 -1
  98. package/src/common/types/options/auth.options.d.ts +233 -3
  99. package/src/common/types/options/auth.options.js +29 -40
  100. package/src/common/types/options/auth.options.js.map +1 -1
  101. package/src/common/types/options/index.d.ts +2 -0
  102. package/src/common/types/options/index.js +2 -0
  103. package/src/common/types/options/index.js.map +1 -1
  104. package/src/common/types/options/redis.options.d.ts +22 -0
  105. package/src/common/types/options/redis.options.js +45 -0
  106. package/src/common/types/options/redis.options.js.map +1 -0
  107. package/src/common/types/options/transport.options.d.ts +84 -0
  108. package/src/common/types/options/transport.options.js +121 -0
  109. package/src/common/types/options/transport.options.js.map +1 -0
  110. package/src/completion/flows/complete.flow.d.ts +17 -2
  111. package/src/context/frontmcp-context-storage.d.ts +94 -0
  112. package/src/context/frontmcp-context-storage.js +183 -0
  113. package/src/context/frontmcp-context-storage.js.map +1 -0
  114. package/src/context/frontmcp-context.d.ts +269 -0
  115. package/src/context/frontmcp-context.js +360 -0
  116. package/src/context/frontmcp-context.js.map +1 -0
  117. package/src/context/frontmcp-context.provider.d.ts +43 -0
  118. package/src/context/frontmcp-context.provider.js +61 -0
  119. package/src/context/frontmcp-context.provider.js.map +1 -0
  120. package/src/context/index.d.ts +34 -0
  121. package/src/context/index.js +64 -0
  122. package/src/context/index.js.map +1 -0
  123. package/src/context/request-context-storage.d.ts +89 -0
  124. package/src/context/request-context-storage.js +183 -0
  125. package/src/context/request-context-storage.js.map +1 -0
  126. package/src/context/request-context.d.ts +184 -0
  127. package/src/context/request-context.js +209 -0
  128. package/src/context/request-context.js.map +1 -0
  129. package/src/context/request-context.provider.d.ts +37 -0
  130. package/src/context/request-context.provider.js +51 -0
  131. package/src/context/request-context.provider.js.map +1 -0
  132. package/src/context/session-key.provider.d.ts +45 -0
  133. package/src/context/session-key.provider.js +65 -0
  134. package/src/context/session-key.provider.js.map +1 -0
  135. package/src/context/trace-context.d.ts +43 -0
  136. package/src/context/trace-context.js +142 -0
  137. package/src/context/trace-context.js.map +1 -0
  138. package/src/errors/index.d.ts +1 -1
  139. package/src/errors/index.js +3 -1
  140. package/src/errors/index.js.map +1 -1
  141. package/src/errors/mcp.error.d.ts +7 -0
  142. package/src/errors/mcp.error.js +11 -1
  143. package/src/errors/mcp.error.js.map +1 -1
  144. package/src/flows/flow.instance.d.ts +16 -0
  145. package/src/flows/flow.instance.js +166 -80
  146. package/src/flows/flow.instance.js.map +1 -1
  147. package/src/flows/flow.registry.d.ts +5 -0
  148. package/src/flows/flow.registry.js +45 -3
  149. package/src/flows/flow.registry.js.map +1 -1
  150. package/src/front-mcp/front-mcp.d.ts +12 -0
  151. package/src/front-mcp/front-mcp.js +22 -3
  152. package/src/front-mcp/front-mcp.js.map +1 -1
  153. package/src/front-mcp/front-mcp.providers.d.ts +266 -1
  154. package/src/front-mcp/front-mcp.providers.js +2 -1
  155. package/src/front-mcp/front-mcp.providers.js.map +1 -1
  156. package/src/front-mcp/serverless-handler.d.ts +28 -0
  157. package/src/front-mcp/serverless-handler.js +61 -0
  158. package/src/front-mcp/serverless-handler.js.map +1 -0
  159. package/src/hooks/hooks.utils.d.ts +1 -1
  160. package/src/hooks/hooks.utils.js +10 -3
  161. package/src/hooks/hooks.utils.js.map +1 -1
  162. package/src/index.d.ts +8 -4
  163. package/src/index.js +20 -1
  164. package/src/index.js.map +1 -1
  165. package/src/logger/instances/instance.logger.js +0 -1
  166. package/src/logger/instances/instance.logger.js.map +1 -1
  167. package/src/logging/flows/set-level.flow.d.ts +17 -2
  168. package/src/notification/notification.service.js +5 -1
  169. package/src/notification/notification.service.js.map +1 -1
  170. package/src/prompt/flows/get-prompt.flow.d.ts +97 -2
  171. package/src/prompt/flows/prompts-list.flow.d.ts +12 -1
  172. package/src/provider/provider.registry.d.ts +97 -5
  173. package/src/provider/provider.registry.js +306 -9
  174. package/src/provider/provider.registry.js.map +1 -1
  175. package/src/provider/provider.types.d.ts +21 -3
  176. package/src/provider/provider.types.js.map +1 -1
  177. package/src/resource/flows/read-resource.flow.d.ts +22 -3
  178. package/src/resource/flows/resource-templates-list.flow.d.ts +20 -1
  179. package/src/resource/flows/resources-list.flow.d.ts +20 -1
  180. package/src/resource/flows/subscribe-resource.flow.d.ts +17 -2
  181. package/src/resource/flows/unsubscribe-resource.flow.d.ts +17 -2
  182. package/src/scope/flows/http.request.flow.js +43 -7
  183. package/src/scope/flows/http.request.flow.js.map +1 -1
  184. package/src/scope/scope.instance.js +12 -5
  185. package/src/scope/scope.instance.js.map +1 -1
  186. package/src/server/adapters/base.host.adapter.d.ts +9 -0
  187. package/src/server/adapters/base.host.adapter.js.map +1 -1
  188. package/src/server/adapters/express.host.adapter.d.ts +12 -0
  189. package/src/server/adapters/express.host.adapter.js +21 -1
  190. package/src/server/adapters/express.host.adapter.js.map +1 -1
  191. package/src/server/server.instance.d.ts +3 -0
  192. package/src/server/server.instance.js +14 -7
  193. package/src/server/server.instance.js.map +1 -1
  194. package/src/tool/flows/call-tool.flow.d.ts +118 -13
  195. package/src/tool/flows/call-tool.flow.js +240 -194
  196. package/src/tool/flows/call-tool.flow.js.map +1 -1
  197. package/src/tool/flows/tools-list.flow.d.ts +25 -11
  198. package/src/tool/flows/tools-list.flow.js +82 -31
  199. package/src/tool/flows/tools-list.flow.js.map +1 -1
  200. package/src/tool/tool.instance.d.ts +1 -4
  201. package/src/transport/adapters/transport.streamable-http.adapter.js +1 -0
  202. package/src/transport/adapters/transport.streamable-http.adapter.js.map +1 -1
  203. package/src/transport/flows/handle.sse.flow.js +9 -2
  204. package/src/transport/flows/handle.sse.flow.js.map +1 -1
  205. package/src/transport/flows/handle.streamable-http.flow.js +63 -6
  206. package/src/transport/flows/handle.streamable-http.flow.js.map +1 -1
  207. package/src/transport/mcp-handlers/complete-request.handler.d.ts +27 -1
  208. package/src/transport/mcp-handlers/get-prompt-request.handler.d.ts +52 -1
  209. package/src/transport/mcp-handlers/index.d.ts +413 -7
  210. package/src/transport/mcp-handlers/initialize-request.handler.js +12 -2
  211. package/src/transport/mcp-handlers/initialize-request.handler.js.map +1 -1
  212. package/src/transport/mcp-handlers/list-prompts-request.handler.d.ts +27 -1
  213. package/src/transport/mcp-handlers/list-resource-templates-request.handler.d.ts +32 -1
  214. package/src/transport/mcp-handlers/list-resources-request.handler.d.ts +32 -1
  215. package/src/transport/mcp-handlers/list-tools-request.handler.d.ts +30 -1
  216. package/src/transport/mcp-handlers/logging-set-level-request.handler.d.ts +20 -0
  217. package/src/transport/mcp-handlers/read-resource-request.handler.d.ts +27 -1
  218. package/src/transport/mcp-handlers/subscribe-request.handler.d.ts +20 -0
  219. package/src/transport/mcp-handlers/unsubscribe-request.handler.d.ts +20 -0
  220. package/src/transport/transport.registry.d.ts +68 -4
  221. package/src/transport/transport.registry.js +313 -11
  222. package/src/transport/transport.registry.js.map +1 -1
  223. package/src/auth/ui/htmx-templates.js.map +0 -1
  224. package/src/common/providers/session.provider.d.ts +0 -13
  225. package/src/common/providers/session.provider.js +0 -27
  226. package/src/common/providers/session.provider.js.map +0 -1
@@ -8,6 +8,8 @@ const zod_1 = require("zod");
8
8
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
9
9
  const errors_1 = require("../../errors");
10
10
  const ui_1 = require("../ui");
11
+ const adapters_1 = require("@frontmcp/ui/adapters");
12
+ const registry_1 = require("@frontmcp/ui/registry");
11
13
  const inputSchema = zod_1.z.object({
12
14
  request: types_js_1.CallToolRequestSchema,
13
15
  // z.any() used because ctx is the MCP SDK's ToolCallExtra type which varies by SDK version
@@ -27,6 +29,10 @@ const stateSchema = zod_1.z.object({
27
29
  output: outputSchema,
28
30
  // Tool owner ID for hook filtering (set during parseInput)
29
31
  _toolOwnerId: zod_1.z.string().optional(),
32
+ // UI result from applyUI stage (if UI config exists)
33
+ uiResult: zod_1.z.any().optional(),
34
+ // UI metadata from rendering (merged into _meta)
35
+ uiMeta: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
30
36
  });
31
37
  const plan = {
32
38
  pre: [
@@ -38,30 +44,10 @@ const plan = {
38
44
  'acquireSemaphore',
39
45
  ],
40
46
  execute: ['validateInput', 'execute', 'validateOutput'],
41
- finalize: ['releaseSemaphore', 'releaseQuota', 'finalize'],
47
+ finalize: ['releaseSemaphore', 'releaseQuota', 'applyUI', 'finalize'],
42
48
  };
43
49
  const name = 'tools:call-tool';
44
50
  const { Stage } = (0, common_1.FlowHooksOf)(name);
45
- /**
46
- * Safely stringify a value, handling circular references and other edge cases.
47
- * This prevents tool calls from failing due to serialization errors.
48
- */
49
- const safeStringify = (value, space) => {
50
- const seen = new WeakSet();
51
- try {
52
- return JSON.stringify(value, (_key, val) => {
53
- if (typeof val === 'object' && val !== null) {
54
- if (seen.has(val))
55
- return '[Circular]';
56
- seen.add(val);
57
- }
58
- return val;
59
- }, space);
60
- }
61
- catch {
62
- return JSON.stringify({ error: 'Output could not be serialized' });
63
- }
64
- };
65
51
  let CallToolFlow = class CallToolFlow extends common_1.FlowBase {
66
52
  logger = this.scopeLogger.child('CallToolFlow');
67
53
  async parseInput() {
@@ -278,196 +264,211 @@ let CallToolFlow = class CallToolFlow extends common_1.FlowBase {
278
264
  this.state.toolContext?.mark('releaseQuota');
279
265
  this.logger.verbose('releaseQuota:done');
280
266
  }
267
+ /**
268
+ * Apply UI rendering to the tool response.
269
+ * This stage handles all UI-related logic including platform detection,
270
+ * serving mode resolution, and content formatting.
271
+ */
272
+ async applyUI() {
273
+ this.logger.verbose('applyUI:start');
274
+ const { tool, rawOutput, input } = this.state;
275
+ // Skip if no tool or no UI config
276
+ if (!tool || !(0, ui_1.hasUIConfig)(tool.metadata)) {
277
+ this.logger.verbose('applyUI:skip (no UI config)');
278
+ return;
279
+ }
280
+ try {
281
+ // Cast scope to Scope to access toolUI and notifications
282
+ const scope = this.scope;
283
+ // Get session info for platform detection from authInfo (already in state from parseInput)
284
+ const { authInfo } = this.state;
285
+ const sessionId = authInfo?.sessionId;
286
+ const requestId = (0, crypto_1.randomUUID)();
287
+ // Get platform type: first check sessionIdPayload (detected from user-agent),
288
+ // then fall back to notification service (detected from MCP clientInfo),
289
+ // finally default to 'unknown' (conservative: skip UI for unknown clients)
290
+ const platformType = authInfo?.sessionIdPayload?.platformType ??
291
+ (sessionId ? scope.notifications.getPlatformType(sessionId) : undefined) ??
292
+ 'unknown';
293
+ // Resolve the effective serving mode based on configuration and client capabilities
294
+ // Default is 'auto' which selects the best mode for the platform
295
+ const configuredMode = tool.metadata.ui.servingMode ?? 'auto';
296
+ const resolvedMode = (0, adapters_1.resolveServingMode)({
297
+ configuredMode,
298
+ platformType,
299
+ });
300
+ // If client doesn't support UI (e.g., Gemini, unknown, or forced mode not available)
301
+ // skip UI rendering entirely
302
+ if (!resolvedMode.supportsUI || resolvedMode.effectiveMode === null) {
303
+ this.logger.verbose('applyUI: Skipping UI (client does not support it)', {
304
+ tool: tool.metadata.name,
305
+ platform: platformType,
306
+ configuredMode,
307
+ reason: resolvedMode.reason,
308
+ });
309
+ return;
310
+ }
311
+ const servingMode = resolvedMode.effectiveMode;
312
+ const useStructuredContent = resolvedMode.useStructuredContent;
313
+ let htmlContent;
314
+ let uiMeta = {};
315
+ if (servingMode === 'static') {
316
+ // For static mode: no additional rendering needed
317
+ // Widget was already registered at server startup
318
+ this.logger.verbose('applyUI: UI using static mode (structured data only)', {
319
+ tool: tool.metadata.name,
320
+ platform: platformType,
321
+ });
322
+ }
323
+ else if (servingMode === 'hybrid') {
324
+ // For hybrid mode: build the component payload
325
+ const componentPayload = scope.toolUI.buildHybridComponentPayload({
326
+ toolName: tool.metadata.name,
327
+ template: tool.metadata.ui.template,
328
+ uiConfig: tool.metadata.ui,
329
+ });
330
+ if (componentPayload) {
331
+ uiMeta = {
332
+ 'ui/component': componentPayload,
333
+ 'ui/type': componentPayload.type,
334
+ };
335
+ }
336
+ this.logger.verbose('applyUI: UI using hybrid mode (structured data + component)', {
337
+ tool: tool.metadata.name,
338
+ platform: platformType,
339
+ hasComponent: !!componentPayload,
340
+ componentType: componentPayload?.type,
341
+ componentHash: componentPayload?.hash,
342
+ });
343
+ }
344
+ else {
345
+ // For inline mode (default): render HTML with data embedded
346
+ const uiRenderResult = await scope.toolUI.renderAndRegisterAsync({
347
+ toolName: tool.metadata.name,
348
+ requestId,
349
+ input: input?.arguments && typeof input.arguments === 'object' && !Array.isArray(input.arguments)
350
+ ? input.arguments
351
+ : {},
352
+ output: rawOutput,
353
+ structuredContent: undefined,
354
+ uiConfig: tool.metadata.ui,
355
+ platformType,
356
+ });
357
+ // Handle graceful degradation: rendering failed in production
358
+ if ((0, registry_1.isUIRenderFailure)(uiRenderResult)) {
359
+ this.logger.warn('applyUI: UI rendering failed (graceful degradation)', {
360
+ tool: tool.metadata.name,
361
+ reason: uiRenderResult.reason,
362
+ platform: platformType,
363
+ });
364
+ // Proceed without UI - tool result will not have ui/html metadata
365
+ htmlContent = undefined;
366
+ uiMeta = {};
367
+ }
368
+ else {
369
+ // Extract HTML from platform-specific meta key
370
+ const htmlKey = platformType === 'openai' ? 'openai/html' : platformType === 'ext-apps' ? 'ui/html' : 'frontmcp/html';
371
+ htmlContent = uiRenderResult?.meta?.[htmlKey];
372
+ // Fallback to ui/html for compatibility
373
+ if (!htmlContent) {
374
+ htmlContent = uiRenderResult?.meta?.['ui/html'];
375
+ }
376
+ uiMeta = uiRenderResult.meta || {};
377
+ }
378
+ }
379
+ // Build the response content using the extracted utility
380
+ const uiResult = (0, adapters_1.buildToolResponseContent)({
381
+ rawOutput,
382
+ htmlContent,
383
+ servingMode,
384
+ useStructuredContent,
385
+ platformType,
386
+ });
387
+ // Store UI result and metadata in state for finalize stage
388
+ this.state.set('uiResult', uiResult);
389
+ this.state.set('uiMeta', uiMeta);
390
+ this.logger.verbose('applyUI: UI processed', {
391
+ tool: tool.metadata.name,
392
+ platform: platformType,
393
+ servingMode,
394
+ useStructuredContent,
395
+ format: uiResult.format,
396
+ contentCleared: uiResult.contentCleared,
397
+ });
398
+ }
399
+ catch (error) {
400
+ // UI rendering failure should not fail the tool call
401
+ const errorMessage = error instanceof Error ? error.message : String(error);
402
+ const errorStack = error instanceof Error ? error.stack : undefined;
403
+ const uiConfig = tool.metadata.ui;
404
+ this.logger.error('applyUI: UI rendering failed', {
405
+ tool: tool.metadata.name,
406
+ error: errorMessage,
407
+ stack: errorStack,
408
+ templateType: uiConfig?.template
409
+ ? typeof uiConfig.template === 'function'
410
+ ? 'react-component'
411
+ : typeof uiConfig.template === 'string'
412
+ ? uiConfig.template.endsWith('.tsx') || uiConfig.template.endsWith('.jsx')
413
+ ? 'react-file'
414
+ : 'html-file'
415
+ : 'unknown'
416
+ : 'none',
417
+ });
418
+ // In debug mode, also log to console for immediate visibility
419
+ if (process.env['DEBUG'] || process.env['NODE_ENV'] === 'development') {
420
+ console.error('[FrontMCP] UI Rendering Error:', {
421
+ tool: tool.metadata.name,
422
+ error: errorMessage,
423
+ stack: errorStack,
424
+ });
425
+ }
426
+ }
427
+ this.logger.verbose('applyUI:done');
428
+ }
429
+ /**
430
+ * Finalize the tool response.
431
+ * Validates output, applies UI result from applyUI stage, and sends the response.
432
+ *
433
+ * Note: This stage runs even when execute fails (as part of cleanup).
434
+ * If rawOutput is undefined, it means an error occurred during execution
435
+ * and the error will be propagated by the flow framework - we should not
436
+ * throw a new error here.
437
+ */
281
438
  async finalize() {
282
439
  this.logger.verbose('finalize:start');
283
- const { tool, rawOutput, input } = this.state;
440
+ const { tool, rawOutput, uiResult, uiMeta } = this.state;
284
441
  if (!tool) {
285
- this.logger.error('finalize: tool not found in state');
286
- throw new errors_1.ToolExecutionError('unknown', new Error('Tool not found in state'));
442
+ // No tool found - this is an early failure, just skip finalization
443
+ this.logger.verbose('finalize: skipping (no tool in state)');
444
+ return;
287
445
  }
288
446
  if (rawOutput === undefined) {
289
- this.logger.error('finalize: tool output not found in state');
290
- throw new errors_1.ToolExecutionError(tool.metadata.name, new Error('Tool output not found'));
447
+ // No output means execute stage failed - skip finalization
448
+ // The original error will be propagated by the flow framework
449
+ this.logger.verbose('finalize: skipping (no output - execute stage likely failed)');
450
+ return;
291
451
  }
292
452
  // Parse and construct the MCP-compliant output using safeParseOutput
293
453
  const parseResult = tool.safeParseOutput(rawOutput);
294
454
  if (!parseResult.success) {
295
- // add support for request id in error messages
296
455
  this.logger.error('finalize: output validation failed', {
297
456
  tool: tool.metadata.name,
298
457
  errors: parseResult.error,
299
458
  });
300
- // Use InvalidOutputError, which hides internal details in production
301
459
  throw new errors_1.InvalidOutputError();
302
460
  }
303
461
  const result = parseResult.data;
304
- // If tool has UI config, render and add to _meta
305
- if ((0, ui_1.hasUIConfig)(tool.metadata)) {
306
- try {
307
- // Cast scope to Scope to access toolUI and notifications
308
- const scope = this.scope;
309
- // Get session info for platform detection from authInfo (already in state from parseInput)
310
- const { authInfo } = this.state;
311
- const sessionId = authInfo?.sessionId;
312
- const requestId = (0, crypto_1.randomUUID)();
313
- // Get platform type: first check sessionIdPayload (detected from user-agent),
314
- // then fall back to notification service (detected from MCP clientInfo),
315
- // finally default to 'openai'
316
- const platformType = authInfo?.sessionIdPayload?.platformType ??
317
- (sessionId ? scope.notifications.getPlatformType(sessionId) : undefined) ??
318
- 'openai';
319
- // Get the serving mode (default to 'inline' for backward compatibility)
320
- const servingMode = tool.metadata.ui.servingMode ?? 'inline';
321
- if (servingMode === 'static') {
322
- // For static mode: return ONLY structured data
323
- // The static widget was already registered at server startup and advertised in tools/list
324
- // Widget reads tool output from platform context (e.g., window.openai.toolOutput)
325
- // NO UI _meta fields needed - the client uses the outputTemplate URI from tools/list
326
- // Return structured data as JSON text content
327
- result.content = [
328
- {
329
- type: 'text',
330
- text: safeStringify(rawOutput),
331
- },
332
- ];
333
- // Do NOT add any UI _meta fields - widget reads from platform context
334
- // The outputTemplate URI (ui://widget/{toolName}.html) was already provided in tools/list
335
- this.logger.verbose('finalize: UI using static mode (structured data only)', {
336
- tool: tool.metadata.name,
337
- platform: platformType,
338
- outputKeys: rawOutput && typeof rawOutput === 'object' && !Array.isArray(rawOutput) ? Object.keys(rawOutput) : [],
339
- });
340
- }
341
- else if (servingMode === 'hybrid') {
342
- // For hybrid mode: return structured data + transpiled component code
343
- // The hybrid shell (React runtime + renderer) was pre-compiled at server startup
344
- // and advertised via outputTemplate URI in tools/list.
345
- // The shell dynamically imports the component code from _meta['ui/component']
346
- // and renders it with toolOutput data from the platform context.
347
- // Build the component payload with transpiled code
348
- const componentPayload = scope.toolUI.buildHybridComponentPayload({
349
- toolName: tool.metadata.name,
350
- template: tool.metadata.ui.template,
351
- uiConfig: tool.metadata.ui,
352
- });
353
- // Return structured data as JSON text content
354
- result.content = [
355
- {
356
- type: 'text',
357
- text: safeStringify(rawOutput),
358
- },
359
- ];
360
- // Add component payload to _meta for the hybrid shell's dynamic renderer
361
- if (componentPayload) {
362
- result._meta = {
363
- ...result._meta,
364
- 'ui/component': componentPayload,
365
- 'ui/type': componentPayload.type,
366
- };
367
- }
368
- this.logger.verbose('finalize: UI using hybrid mode (structured data + component)', {
369
- tool: tool.metadata.name,
370
- platform: platformType,
371
- hasComponent: !!componentPayload,
372
- componentType: componentPayload?.type,
373
- componentHash: componentPayload?.hash,
374
- outputKeys: rawOutput && typeof rawOutput === 'object' && !Array.isArray(rawOutput) ? Object.keys(rawOutput) : [],
375
- });
376
- }
377
- else {
378
- // For inline mode (default): render HTML with data embedded in each response
379
- // Use async version to support React component templates via SSR
380
- const uiResult = await scope.toolUI.renderAndRegisterAsync({
381
- toolName: tool.metadata.name,
382
- requestId,
383
- input: input?.arguments && typeof input.arguments === 'object' && !Array.isArray(input.arguments)
384
- ? input.arguments
385
- : {},
386
- output: rawOutput,
387
- structuredContent: result.structuredContent,
388
- uiConfig: tool.metadata.ui,
389
- platformType,
390
- });
391
- // Merge UI metadata into result._meta
392
- result._meta = {
393
- ...result._meta,
394
- ...uiResult.meta,
395
- };
396
- // For platforms that support widgets (OpenAI, ext-apps), clear content since widget displays data
397
- // For Claude and other platforms, keep the text content as they don't support _meta UI fields
398
- const supportsWidgets = platformType === 'openai' || platformType === 'ext-apps';
399
- if (supportsWidgets) {
400
- // Clear content - widget will display from _meta['ui/html']
401
- result.content = [];
402
- }
403
- else {
404
- // For Claude and other platforms without widget support:
405
- // Return JSON data + HTML template as artifact hint
406
- const htmlContent = uiResult?.meta?.['ui/html'];
407
- if (htmlContent) {
408
- // Include HTML template as artifact hint for Claude
409
- // Claude can use this to create an HTML artifact for visual display
410
- result.content = [
411
- {
412
- type: 'text',
413
- text: `## Data\n\`\`\`json\n${safeStringify(rawOutput, 2)}\n\`\`\`\n\n## Visual Template (for artifact rendering)\n\`\`\`html\n${htmlContent}\n\`\`\``,
414
- },
415
- ];
416
- }
417
- else {
418
- // Fallback: JSON only
419
- result.content = [
420
- {
421
- type: 'text',
422
- text: safeStringify(rawOutput, 2),
423
- },
424
- ];
425
- }
426
- }
427
- this.logger.verbose('finalize: UI metadata added (inline mode)', {
428
- tool: tool.metadata.name,
429
- platform: platformType,
430
- supportsWidgets,
431
- contentCleared: supportsWidgets,
432
- htmlHintIncluded: !supportsWidgets && !!uiResult?.meta?.['ui/html'],
433
- });
434
- }
462
+ // Apply UI result if available (from applyUI stage)
463
+ if (uiResult) {
464
+ result.content = uiResult.content;
465
+ // Set structuredContent from UI result (contains raw tool output)
466
+ // Cast to Record<string, unknown> since MCP protocol expects object type
467
+ if (uiResult.structuredContent !== undefined && uiResult.structuredContent !== null) {
468
+ result.structuredContent = uiResult.structuredContent;
435
469
  }
436
- catch (error) {
437
- // UI rendering failure should not fail the tool call
438
- // Log with full context to help debugging
439
- const errorMessage = error instanceof Error ? error.message : String(error);
440
- const errorStack = error instanceof Error ? error.stack : undefined;
441
- const uiConfig = tool.metadata.ui;
442
- this.logger.error('finalize: UI rendering failed', {
443
- tool: tool.metadata.name,
444
- error: errorMessage,
445
- stack: errorStack,
446
- templateType: uiConfig?.template
447
- ? typeof uiConfig.template === 'function'
448
- ? 'react-component'
449
- : typeof uiConfig.template === 'string'
450
- ? uiConfig.template.endsWith('.tsx') || uiConfig.template.endsWith('.jsx')
451
- ? 'react-file'
452
- : 'html-file'
453
- : 'unknown'
454
- : 'none',
455
- hasStructuredContent: result.structuredContent !== undefined,
456
- structuredContentKeys: result.structuredContent &&
457
- typeof result.structuredContent === 'object' &&
458
- result.structuredContent !== null &&
459
- !Array.isArray(result.structuredContent)
460
- ? Object.keys(result.structuredContent)
461
- : [],
462
- });
463
- // In debug mode, also log to console for immediate visibility
464
- if (process.env['DEBUG'] || process.env['NODE_ENV'] === 'development') {
465
- console.error('[FrontMCP] UI Rendering Error:', {
466
- tool: tool.metadata.name,
467
- error: errorMessage,
468
- stack: errorStack,
469
- });
470
- }
470
+ if (uiMeta) {
471
+ result._meta = { ...result._meta, ...uiMeta };
471
472
  }
472
473
  }
473
474
  // Log the final result being sent
@@ -480,6 +481,45 @@ let CallToolFlow = class CallToolFlow extends common_1.FlowBase {
480
481
  metaKeys: result._meta ? Object.keys(result._meta) : [],
481
482
  isError: result.isError,
482
483
  });
484
+ // Debug: Write full response to file if DEBUG_TOOL_RESPONSE is set
485
+ if (process.env['DEBUG_TOOL_RESPONSE']) {
486
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
487
+ const fs = require('fs').promises;
488
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
489
+ const os = require('os');
490
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
491
+ const path = require('path');
492
+ const debugOutput = {
493
+ timestamp: new Date().toISOString(),
494
+ tool: tool.metadata.name,
495
+ rawOutput,
496
+ uiResult,
497
+ uiMeta,
498
+ finalResult: result,
499
+ };
500
+ // Use os.tmpdir() for cross-platform compatibility (works on Windows, macOS, Linux)
501
+ const tempDir = os.tmpdir();
502
+ const defaultPath = path.join(tempDir, 'tool-response-debug.json');
503
+ let outputPath = process.env['DEBUG_TOOL_RESPONSE_PATH'] || defaultPath;
504
+ // Path traversal protection: ensure custom path is within allowed directories
505
+ if (process.env['DEBUG_TOOL_RESPONSE_PATH']) {
506
+ const resolvedPath = path.resolve(outputPath);
507
+ const resolvedTempDir = path.resolve(tempDir);
508
+ const cwd = path.resolve(process.cwd());
509
+ // Only allow paths within temp directory or current working directory
510
+ const isInTempDir = resolvedPath.startsWith(resolvedTempDir + path.sep) || resolvedPath === resolvedTempDir;
511
+ const isInCwd = resolvedPath.startsWith(cwd + path.sep) || resolvedPath === cwd;
512
+ if (!isInTempDir && !isInCwd) {
513
+ console.error(`[DEBUG] DEBUG_TOOL_RESPONSE_PATH must be within temp directory (${tempDir}) or current working directory (${cwd}). ` +
514
+ `Falling back to default path.`);
515
+ outputPath = defaultPath;
516
+ }
517
+ }
518
+ // Use async write to avoid blocking the event loop (fire-and-forget)
519
+ fs.writeFile(outputPath, JSON.stringify(debugOutput, null, 2))
520
+ .then(() => console.log(`[DEBUG] Tool response written to: ${outputPath}`))
521
+ .catch((err) => console.error(`[DEBUG] Failed to write tool response: ${err.message}`));
522
+ }
483
523
  // Respond with the properly formatted MCP result
484
524
  this.respond(result);
485
525
  this.logger.verbose('finalize:done');
@@ -551,6 +591,12 @@ tslib_1.__decorate([
551
591
  tslib_1.__metadata("design:paramtypes", []),
552
592
  tslib_1.__metadata("design:returntype", Promise)
553
593
  ], CallToolFlow.prototype, "releaseQuota", null);
594
+ tslib_1.__decorate([
595
+ Stage('applyUI'),
596
+ tslib_1.__metadata("design:type", Function),
597
+ tslib_1.__metadata("design:paramtypes", []),
598
+ tslib_1.__metadata("design:returntype", Promise)
599
+ ], CallToolFlow.prototype, "applyUI", null);
554
600
  tslib_1.__decorate([
555
601
  Stage('finalize'),
556
602
  tslib_1.__metadata("design:type", Function),