@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.
- package/README.md +3 -3
- package/package.json +8 -19
- package/src/adapter/adapter.instance.js +5 -0
- package/src/adapter/adapter.instance.js.map +1 -1
- package/src/auth/authorization/authorization.class.d.ts +1 -4
- package/src/auth/authorization/authorization.class.js +6 -13
- package/src/auth/authorization/authorization.class.js.map +1 -1
- package/src/auth/flows/session.verify.flow.d.ts +1 -0
- package/src/auth/flows/session.verify.flow.js +11 -1
- package/src/auth/flows/session.verify.flow.js.map +1 -1
- package/src/auth/flows/well-known.jwks.flow.js +2 -2
- package/src/auth/flows/well-known.jwks.flow.js.map +1 -1
- package/src/auth/jwks/dev-key-persistence.d.ts +63 -0
- package/src/auth/jwks/dev-key-persistence.js +219 -0
- package/src/auth/jwks/dev-key-persistence.js.map +1 -0
- package/src/auth/jwks/index.d.ts +1 -0
- package/src/auth/jwks/index.js +1 -0
- package/src/auth/jwks/index.js.map +1 -1
- package/src/auth/jwks/jwks.service.d.ts +7 -4
- package/src/auth/jwks/jwks.service.js +81 -12
- package/src/auth/jwks/jwks.service.js.map +1 -1
- package/src/auth/jwks/jwks.types.d.ts +7 -0
- package/src/auth/jwks/jwks.types.js.map +1 -1
- package/src/auth/machine-id.d.ts +5 -0
- package/src/auth/machine-id.js +32 -0
- package/src/auth/machine-id.js.map +1 -0
- package/src/auth/session/index.d.ts +1 -0
- package/src/auth/session/index.js +3 -1
- package/src/auth/session/index.js.map +1 -1
- package/src/auth/session/record/session.base.js +5 -3
- package/src/auth/session/record/session.base.js.map +1 -1
- package/src/auth/session/record/session.stateless.d.ts +2 -2
- package/src/auth/session/record/session.stateless.js +5 -3
- package/src/auth/session/record/session.stateless.js.map +1 -1
- package/src/auth/session/redis-session.store.d.ts +64 -0
- package/src/auth/session/redis-session.store.js +204 -0
- package/src/auth/session/redis-session.store.js.map +1 -0
- package/src/auth/session/session.service.d.ts +0 -2
- package/src/auth/session/session.service.js +1 -7
- package/src/auth/session/session.service.js.map +1 -1
- package/src/auth/session/transport-session.manager.js +3 -5
- package/src/auth/session/transport-session.manager.js.map +1 -1
- package/src/auth/session/transport-session.types.d.ts +4 -0
- package/src/auth/session/transport-session.types.js +4 -3
- package/src/auth/session/transport-session.types.js.map +1 -1
- package/src/auth/session/utils/session-id.utils.d.ts +12 -1
- package/src/auth/session/utils/session-id.utils.js +48 -9
- package/src/auth/session/utils/session-id.utils.js.map +1 -1
- package/src/auth/ui/base-layout.d.ts +0 -8
- package/src/auth/ui/base-layout.js +1 -14
- package/src/auth/ui/base-layout.js.map +1 -1
- package/src/auth/ui/index.d.ts +3 -4
- package/src/auth/ui/index.js +10 -11
- package/src/auth/ui/index.js.map +1 -1
- package/src/auth/ui/{htmx-templates.d.ts → templates.d.ts} +5 -6
- package/src/auth/ui/{htmx-templates.js → templates.js} +8 -15
- package/src/auth/ui/templates.js.map +1 -0
- package/src/common/decorators/decorator-utils.js.map +1 -1
- package/src/common/decorators/front-mcp.decorator.js +28 -2
- package/src/common/decorators/front-mcp.decorator.js.map +1 -1
- package/src/common/index.d.ts +0 -1
- package/src/common/index.js +0 -1
- package/src/common/index.js.map +1 -1
- package/src/common/interfaces/adapter.interface.d.ts +6 -0
- package/src/common/interfaces/adapter.interface.js.map +1 -1
- package/src/common/interfaces/execution-context.interface.d.ts +52 -3
- package/src/common/interfaces/execution-context.interface.js +88 -3
- package/src/common/interfaces/execution-context.interface.js.map +1 -1
- package/src/common/interfaces/flow.interface.d.ts +13 -0
- package/src/common/interfaces/flow.interface.js +24 -0
- package/src/common/interfaces/flow.interface.js.map +1 -1
- package/src/common/interfaces/server.interface.d.ts +9 -0
- package/src/common/interfaces/server.interface.js.map +1 -1
- package/src/common/metadata/app.metadata.d.ts +108 -0
- package/src/common/metadata/front-mcp.metadata.d.ts +659 -2
- package/src/common/metadata/front-mcp.metadata.js +3 -1
- package/src/common/metadata/front-mcp.metadata.js.map +1 -1
- package/src/common/metadata/provider.metadata.d.ts +14 -0
- package/src/common/metadata/provider.metadata.js +18 -2
- package/src/common/metadata/provider.metadata.js.map +1 -1
- package/src/common/metadata/tool.metadata.d.ts +33 -1
- package/src/common/metadata/tool.metadata.js.map +1 -1
- package/src/common/migrate/auth-transport.migrate.d.ts +62 -0
- package/src/common/migrate/auth-transport.migrate.js +140 -0
- package/src/common/migrate/auth-transport.migrate.js.map +1 -0
- package/src/common/migrate/index.d.ts +1 -0
- package/src/common/migrate/index.js +6 -0
- package/src/common/migrate/index.js.map +1 -0
- package/src/common/schemas/http-output.schema.d.ts +10 -2
- package/src/common/schemas/index.d.ts +1 -0
- package/src/common/schemas/index.js +1 -0
- package/src/common/schemas/index.js.map +1 -1
- package/src/common/schemas/session-header.schema.d.ts +16 -0
- package/src/common/schemas/session-header.schema.js +42 -0
- package/src/common/schemas/session-header.schema.js.map +1 -0
- package/src/common/tokens/front-mcp.tokens.js +3 -1
- package/src/common/tokens/front-mcp.tokens.js.map +1 -1
- package/src/common/types/options/auth.options.d.ts +233 -3
- package/src/common/types/options/auth.options.js +29 -40
- package/src/common/types/options/auth.options.js.map +1 -1
- package/src/common/types/options/index.d.ts +2 -0
- package/src/common/types/options/index.js +2 -0
- package/src/common/types/options/index.js.map +1 -1
- package/src/common/types/options/redis.options.d.ts +22 -0
- package/src/common/types/options/redis.options.js +45 -0
- package/src/common/types/options/redis.options.js.map +1 -0
- package/src/common/types/options/transport.options.d.ts +84 -0
- package/src/common/types/options/transport.options.js +121 -0
- package/src/common/types/options/transport.options.js.map +1 -0
- package/src/completion/flows/complete.flow.d.ts +17 -2
- package/src/context/frontmcp-context-storage.d.ts +94 -0
- package/src/context/frontmcp-context-storage.js +183 -0
- package/src/context/frontmcp-context-storage.js.map +1 -0
- package/src/context/frontmcp-context.d.ts +269 -0
- package/src/context/frontmcp-context.js +360 -0
- package/src/context/frontmcp-context.js.map +1 -0
- package/src/context/frontmcp-context.provider.d.ts +43 -0
- package/src/context/frontmcp-context.provider.js +61 -0
- package/src/context/frontmcp-context.provider.js.map +1 -0
- package/src/context/index.d.ts +34 -0
- package/src/context/index.js +64 -0
- package/src/context/index.js.map +1 -0
- package/src/context/request-context-storage.d.ts +89 -0
- package/src/context/request-context-storage.js +183 -0
- package/src/context/request-context-storage.js.map +1 -0
- package/src/context/request-context.d.ts +184 -0
- package/src/context/request-context.js +209 -0
- package/src/context/request-context.js.map +1 -0
- package/src/context/request-context.provider.d.ts +37 -0
- package/src/context/request-context.provider.js +51 -0
- package/src/context/request-context.provider.js.map +1 -0
- package/src/context/session-key.provider.d.ts +45 -0
- package/src/context/session-key.provider.js +65 -0
- package/src/context/session-key.provider.js.map +1 -0
- package/src/context/trace-context.d.ts +43 -0
- package/src/context/trace-context.js +142 -0
- package/src/context/trace-context.js.map +1 -0
- package/src/errors/index.d.ts +1 -1
- package/src/errors/index.js +3 -1
- package/src/errors/index.js.map +1 -1
- package/src/errors/mcp.error.d.ts +7 -0
- package/src/errors/mcp.error.js +11 -1
- package/src/errors/mcp.error.js.map +1 -1
- package/src/flows/flow.instance.d.ts +16 -0
- package/src/flows/flow.instance.js +166 -80
- package/src/flows/flow.instance.js.map +1 -1
- package/src/flows/flow.registry.d.ts +5 -0
- package/src/flows/flow.registry.js +45 -3
- package/src/flows/flow.registry.js.map +1 -1
- package/src/front-mcp/front-mcp.d.ts +12 -0
- package/src/front-mcp/front-mcp.js +22 -3
- package/src/front-mcp/front-mcp.js.map +1 -1
- package/src/front-mcp/front-mcp.providers.d.ts +266 -1
- package/src/front-mcp/front-mcp.providers.js +2 -1
- package/src/front-mcp/front-mcp.providers.js.map +1 -1
- package/src/front-mcp/serverless-handler.d.ts +28 -0
- package/src/front-mcp/serverless-handler.js +61 -0
- package/src/front-mcp/serverless-handler.js.map +1 -0
- package/src/hooks/hooks.utils.d.ts +1 -1
- package/src/hooks/hooks.utils.js +10 -3
- package/src/hooks/hooks.utils.js.map +1 -1
- package/src/index.d.ts +8 -4
- package/src/index.js +20 -1
- package/src/index.js.map +1 -1
- package/src/logger/instances/instance.logger.js +0 -1
- package/src/logger/instances/instance.logger.js.map +1 -1
- package/src/logging/flows/set-level.flow.d.ts +17 -2
- package/src/notification/notification.service.js +5 -1
- package/src/notification/notification.service.js.map +1 -1
- package/src/prompt/flows/get-prompt.flow.d.ts +97 -2
- package/src/prompt/flows/prompts-list.flow.d.ts +12 -1
- package/src/provider/provider.registry.d.ts +97 -5
- package/src/provider/provider.registry.js +306 -9
- package/src/provider/provider.registry.js.map +1 -1
- package/src/provider/provider.types.d.ts +21 -3
- package/src/provider/provider.types.js.map +1 -1
- package/src/resource/flows/read-resource.flow.d.ts +22 -3
- package/src/resource/flows/resource-templates-list.flow.d.ts +20 -1
- package/src/resource/flows/resources-list.flow.d.ts +20 -1
- package/src/resource/flows/subscribe-resource.flow.d.ts +17 -2
- package/src/resource/flows/unsubscribe-resource.flow.d.ts +17 -2
- package/src/scope/flows/http.request.flow.js +43 -7
- package/src/scope/flows/http.request.flow.js.map +1 -1
- package/src/scope/scope.instance.js +12 -5
- package/src/scope/scope.instance.js.map +1 -1
- package/src/server/adapters/base.host.adapter.d.ts +9 -0
- package/src/server/adapters/base.host.adapter.js.map +1 -1
- package/src/server/adapters/express.host.adapter.d.ts +12 -0
- package/src/server/adapters/express.host.adapter.js +21 -1
- package/src/server/adapters/express.host.adapter.js.map +1 -1
- package/src/server/server.instance.d.ts +3 -0
- package/src/server/server.instance.js +14 -7
- package/src/server/server.instance.js.map +1 -1
- package/src/tool/flows/call-tool.flow.d.ts +118 -13
- package/src/tool/flows/call-tool.flow.js +240 -194
- package/src/tool/flows/call-tool.flow.js.map +1 -1
- package/src/tool/flows/tools-list.flow.d.ts +25 -11
- package/src/tool/flows/tools-list.flow.js +82 -31
- package/src/tool/flows/tools-list.flow.js.map +1 -1
- package/src/tool/tool.instance.d.ts +1 -4
- package/src/transport/adapters/transport.streamable-http.adapter.js +1 -0
- package/src/transport/adapters/transport.streamable-http.adapter.js.map +1 -1
- package/src/transport/flows/handle.sse.flow.js +9 -2
- package/src/transport/flows/handle.sse.flow.js.map +1 -1
- package/src/transport/flows/handle.streamable-http.flow.js +63 -6
- package/src/transport/flows/handle.streamable-http.flow.js.map +1 -1
- package/src/transport/mcp-handlers/complete-request.handler.d.ts +27 -1
- package/src/transport/mcp-handlers/get-prompt-request.handler.d.ts +52 -1
- package/src/transport/mcp-handlers/index.d.ts +413 -7
- package/src/transport/mcp-handlers/initialize-request.handler.js +12 -2
- package/src/transport/mcp-handlers/initialize-request.handler.js.map +1 -1
- package/src/transport/mcp-handlers/list-prompts-request.handler.d.ts +27 -1
- package/src/transport/mcp-handlers/list-resource-templates-request.handler.d.ts +32 -1
- package/src/transport/mcp-handlers/list-resources-request.handler.d.ts +32 -1
- package/src/transport/mcp-handlers/list-tools-request.handler.d.ts +30 -1
- package/src/transport/mcp-handlers/logging-set-level-request.handler.d.ts +20 -0
- package/src/transport/mcp-handlers/read-resource-request.handler.d.ts +27 -1
- package/src/transport/mcp-handlers/subscribe-request.handler.d.ts +20 -0
- package/src/transport/mcp-handlers/unsubscribe-request.handler.d.ts +20 -0
- package/src/transport/transport.registry.d.ts +68 -4
- package/src/transport/transport.registry.js +313 -11
- package/src/transport/transport.registry.js.map +1 -1
- package/src/auth/ui/htmx-templates.js.map +0 -1
- package/src/common/providers/session.provider.d.ts +0 -13
- package/src/common/providers/session.provider.js +0 -27
- 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,
|
|
440
|
+
const { tool, rawOutput, uiResult, uiMeta } = this.state;
|
|
284
441
|
if (!tool) {
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
//
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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),
|