- cloud: resolves userId from the authenticated session and builds a [userOrgScope, orgScope] stack via createScopedExecutor(userId, orgId, orgName). userId threaded through execution-stack, mcp-session, and MCP dispatch init. - cloud env refactor: deletes the lazy Proxy apps/cloud/src/env.ts in favor of cloudflare:workers env imported at call sites; adds env-augment.d.ts to type secrets not present in generated wrangler types. - cloud/server.ts: env-guarded OTEL wiring (skips instrument() when AXIOM_TOKEN absent); captures rawFetch before instrument() mutates it so dev requests still land on the Hono app. - cloud/services/db.ts: prefers DATABASE_URL over the hyperdrive proxy so local dev works without the tunnel. - vite.config.ts: devCrashGuard plugin that prints uncaught Worker errors with a stable exit. - hosts/mcp: optional debug flag surfaces verbose session logging, wired up from cloud mcp-session. - local/examples/marketing: migrated to scopes: [scope].
Showing the changes to packages (1 of 25 files). Show all
Unified · Split
| @@ -66,6 +66,10 @@ type SharedMcpServerConfig = { · 65 unmodified lines | ||
| 66 | 66 | * under whichever request triggered it; see the Cloud DO). |
| 67 | 67 | */ |
| 68 | 68 | readonly parentSpan?: Tracer.AnySpan | (() => Tracer.AnySpan | undefined); |
| 69 | + /** | |
| 70 | + * Enable verbose MCP capability / elicitation debug logging. | |
| 71 | + */ | |
| 72 | + readonly debug?: boolean; | |
| 69 | 73 | }; |
| 70 | 74 | |
| 71 | 75 | export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> = |
| @@ -85,9 +89,21 @@ const getElicitationSupport = (server: McpServer): { form: boolean; url: boolean · 13 unmodified lines | ||
| 85 | 89 | return { form: Boolean(elicitation.form), url: Boolean(elicitation.url) }; |
| 86 | 90 | }; |
| 87 | 91 | |
| 92 | +const readDebugDefault = (): boolean => { | |
| 93 | + if (typeof process === "undefined" || !process.env) return false; | |
| 94 | + const value = process.env.EXECUTOR_MCP_DEBUG; | |
| 95 | + return value === "1" || value === "true"; | |
| 96 | +}; | |
| 97 | + | |
| 88 | 98 | const supportsManagedElicitation = (server: McpServer): boolean => |
| 89 | 99 | getElicitationSupport(server).form; |
| 90 | 100 | |
| 101 | +const capabilitySnapshot = (server: McpServer) => ({ | |
| 102 | + clientCapabilities: server.server.getClientCapabilities() ?? null, | |
| 103 | + elicitationSupport: getElicitationSupport(server), | |
| 104 | + managedElicitation: supportsManagedElicitation(server), | |
| 105 | +}); | |
| 106 | + | |
| 91 | 107 | type ElicitInputParams = |
| 92 | 108 | | { |
| 93 | 109 | mode?: "form"; |
| @@ -118,7 +134,10 @@ const elicitationRequestToParams: (request: ElicitationRequest) => ElicitInputPa · 24 unmodified lines | ||
| 118 | 134 | ); |
| 119 | 135 | |
| 120 | 136 | const makeMcpElicitationHandler = |
| 121 | − (server: McpServer): ElicitationHandler => | |
| 137 | + ( | |
| 138 | + server: McpServer, | |
| 139 | + debugLog?: (event: string, data: Record<string, unknown>) => void, | |
| 140 | + ): ElicitationHandler => | |
| 122 | 141 | (ctx: ElicitationContext): Effect.Effect<typeof ElicitationResponse.Type> => { |
| 123 | 142 | const { url: supportsUrl } = getElicitationSupport(server); |
| 124 | 143 | |
| @@ -133,19 +152,51 @@ const makeMcpElicitationHandler = · 8 unmodified lines | ||
| 133 | 152 | : elicitationRequestToParams(ctx.request); |
| 134 | 153 | |
| 135 | 154 | return Effect.promise(async (): Promise<typeof ElicitationResponse.Type> => { |
| 155 | + debugLog?.("elicitation.request", { | |
| 156 | + requestTag: ctx.request._tag, | |
| 157 | + supportsUrl, | |
| 158 | + message: ctx.request.message, | |
| 159 | + hasRequestedSchema: | |
| 160 | + ctx.request._tag === "FormElicitation" | |
| 161 | + ? Object.keys(ctx.request.requestedSchema).length > 0 | |
| 162 | + : false, | |
| 163 | + url: ctx.request._tag === "UrlElicitation" ? ctx.request.url : undefined, | |
| 164 | + clientCapabilities: server.server.getClientCapabilities() ?? null, | |
| 165 | + }); | |
| 136 | 166 | try { |
| 137 | 167 | const response = await server.server.elicitInput( |
| 138 | 168 | params as Parameters<typeof server.server.elicitInput>[0], |
| 139 | 169 | ); |
| 140 | 170 | |
| 171 | + debugLog?.("elicitation.response", { | |
| 172 | + requestTag: ctx.request._tag, | |
| 173 | + action: response.action, | |
| 174 | + hasContent: | |
| 175 | + typeof response.content === "object" && | |
| 176 | + response.content !== null && | |
| 177 | + Object.keys(response.content).length > 0, | |
| 178 | + }); | |
| 179 | + | |
| 141 | 180 | return { |
| 142 | 181 | action: response.action, |
| 143 | 182 | content: response.content, |
| 144 | 183 | }; |
| 145 | 184 | } catch (err) { |
| 185 | + debugLog?.("elicitation.error", { | |
| 186 | + requestTag: ctx.request._tag, | |
| 187 | + error: | |
| 188 | + err instanceof Error | |
| 189 | + ? { name: err.name, message: err.message, stack: err.stack } | |
| 190 | + : { message: String(err) }, | |
| 191 | + clientCapabilities: server.server.getClientCapabilities() ?? null, | |
| 192 | + }); | |
| 146 | 193 | console.error( |
| 147 | 194 | "[executor] elicitInput failed — falling back to cancel.", |
| 148 | − err instanceof Error ? err.message : err, | |
| 195 | + JSON.stringify({ | |
| 196 | + error: err instanceof Error ? err.message : String(err), | |
| 197 | + requestTag: ctx.request._tag, | |
| 198 | + ...capabilitySnapshot(server), | |
| 199 | + }), | |
| 149 | 200 | ); |
| 150 | 201 | return { action: "cancel" }; |
| 151 | 202 | } |
| @@ -205,6 +256,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 53 unmodified lines | ||
| 205 | 256 | // deferred past the outer Effect's await), so we use the runtime to |
| 206 | 257 | // re-enter Effect-land at each callback edge. |
| 207 | 258 | const runtime = yield* Effect.runtime<never>(); |
| 259 | + const debugEnabled = config.debug ?? readDebugDefault(); | |
| 260 | + const debugLog = (event: string, data: Record<string, unknown>) => { | |
| 261 | + if (!debugEnabled) return; | |
| 262 | + try { | |
| 263 | + console.error(`[executor:mcp] ${event} ${JSON.stringify(data)}`); | |
| 264 | + } catch { | |
| 265 | + console.error(`[executor:mcp] ${event}`, data); | |
| 266 | + } | |
| 267 | + }; | |
| 208 | 268 | |
| 209 | 269 | const resolveParentSpan = (): Tracer.AnySpan | undefined => { |
| 210 | 270 | const ps = config.parentSpan; |
| @@ -228,13 +288,27 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 17 unmodified lines | ||
| 228 | 288 | |
| 229 | 289 | const executeCode = (code: string): Effect.Effect<McpToolResult, E> => |
| 230 | 290 | Effect.gen(function* () { |
| 291 | + debugLog("execute.call", { | |
| 292 | + managedElicitation: supportsManagedElicitation(server), | |
| 293 | + elicitationSupport: getElicitationSupport(server), | |
| 294 | + clientCapabilities: server.server.getClientCapabilities() ?? null, | |
| 295 | + codeLength: code.length, | |
| 296 | + }); | |
| 231 | 297 | if (supportsManagedElicitation(server)) { |
| 232 | 298 | const result = yield* engine.execute(code, { |
| 233 | − onElicitation: makeMcpElicitationHandler(server), | |
| 299 | + onElicitation: makeMcpElicitationHandler(server, debugLog), | |
| 234 | 300 | }); |
| 235 | 301 | return toMcpResult(formatExecuteResult(result)); |
| 236 | 302 | } |
| 237 | 303 | const outcome = yield* engine.executeWithPause(code); |
| 304 | + debugLog("execute.paused_flow_result", { | |
| 305 | + status: outcome.status, | |
| 306 | + executionId: outcome.status === "paused" ? outcome.execution.id : undefined, | |
| 307 | + interactionKind: | |
| 308 | + outcome.status === "paused" | |
| 309 | + ? outcome.execution.elicitationContext.request._tag | |
| 310 | + : undefined, | |
| 311 | + }); | |
| 238 | 312 | return outcome.status === "completed" |
| 239 | 313 | ? toMcpResult(formatExecuteResult(outcome.result)) |
| 240 | 314 | : toMcpPausedResult(formatPausedExecution(outcome.execution)); |
| @@ -253,8 +327,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 12 unmodified lines | ||
| 253 | 327 | content: Record<string, unknown> | undefined, |
| 254 | 328 | ): Effect.Effect<McpToolResult, E> => |
| 255 | 329 | Effect.gen(function* () { |
| 330 | + debugLog("resume.call", { | |
| 331 | + executionId, | |
| 332 | + action, | |
| 333 | + hasContent: content !== undefined, | |
| 334 | + clientCapabilities: server.server.getClientCapabilities() ?? null, | |
| 335 | + }); | |
| 256 | 336 | const outcome = yield* engine.resume(executionId, { action, content }); |
| 257 | 337 | if (!outcome) { |
| 338 | + debugLog("resume.missing_execution", { executionId }); | |
| 258 | 339 | return { |
| 259 | 340 | content: [ |
| 260 | 341 | { type: "text" as const, text: `No paused execution: ${executionId}` }, |
| @@ -262,6 +343,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 1 unmodified line | ||
| 262 | 343 | isError: true, |
| 263 | 344 | } satisfies McpToolResult; |
| 264 | 345 | } |
| 346 | + debugLog("resume.result", { | |
| 347 | + executionId, | |
| 348 | + status: outcome.status, | |
| 349 | + nextExecutionId: outcome.status === "paused" ? outcome.execution.id : undefined, | |
| 350 | + interactionKind: | |
| 351 | + outcome.status === "paused" | |
| 352 | + ? outcome.execution.elicitationContext.request._tag | |
| 353 | + : undefined, | |
| 354 | + }); | |
| 265 | 355 | return outcome.status === "completed" |
| 266 | 356 | ? toMcpResult(formatExecuteResult(outcome.result)) |
| 267 | 357 | : toMcpPausedResult(formatPausedExecution(outcome.execution)); |
| @@ -331,6 +421,19 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 63 unmodified lines | ||
| 331 | 421 | } else { |
| 332 | 422 | resumeTool.enable(); |
| 333 | 423 | } |
| 424 | + console.error( | |
| 425 | + "[executor] MCP capability snapshot", | |
| 426 | + JSON.stringify({ | |
| 427 | + ...capabilitySnapshot(server), | |
| 428 | + resumeEnabled: !supportsManagedElicitation(server), | |
| 429 | + }), | |
| 430 | + ); | |
| 431 | + debugLog("tool.visibility", { | |
| 432 | + clientCapabilities: server.server.getClientCapabilities() ?? null, | |
| 433 | + elicitationSupport: getElicitationSupport(server), | |
| 434 | + managedElicitation: supportsManagedElicitation(server), | |
| 435 | + resumeEnabled: !supportsManagedElicitation(server), | |
| 436 | + }); | |
| 334 | 437 | }; |
| 335 | 438 | |
| 336 | 439 | yield* Effect.sync(() => { |