History

cloud/local/examples: thread per-user ScopeStack through HTTP + MCP

- 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].

Rhys Sullivan committed 6 months ago

Commit d4e311e1 parent 427699c25 files · +320 −174Browse files

Showing the changes to packages (1 of 25 files). Show all

Unified · Split

packages/hosts/mcp/src/server.ts+106−3
@@ -66,6 +66,10 @@ type SharedMcpServerConfig = { · 65 unmodified lines
6666 * under whichever request triggered it; see the Cloud DO).
6767 */
6868 readonly parentSpan?: Tracer.AnySpan | (() => Tracer.AnySpan | undefined);
69+ /**
70+ * Enable verbose MCP capability / elicitation debug logging.
71+ */
72+ readonly debug?: boolean;
6973 };
7074
7175 export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> =
@@ -85,9 +89,21 @@ const getElicitationSupport = (server: McpServer): { form: boolean; url: boolean · 13 unmodified lines
8589 return { form: Boolean(elicitation.form), url: Boolean(elicitation.url) };
8690 };
8791
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+
8898 const supportsManagedElicitation = (server: McpServer): boolean =>
8999 getElicitationSupport(server).form;
90100
101+const capabilitySnapshot = (server: McpServer) => ({
102+ clientCapabilities: server.server.getClientCapabilities() ?? null,
103+ elicitationSupport: getElicitationSupport(server),
104+ managedElicitation: supportsManagedElicitation(server),
105+});
106+
91107 type ElicitInputParams =
92108 | {
93109 mode?: "form";
@@ -118,7 +134,10 @@ const elicitationRequestToParams: (request: ElicitationRequest) => ElicitInputPa · 24 unmodified lines
118134 );
119135
120136 const makeMcpElicitationHandler =
121− (server: McpServer): ElicitationHandler =>
137+ (
138+ server: McpServer,
139+ debugLog?: (event: string, data: Record<string, unknown>) => void,
140+ ): ElicitationHandler =>
122141 (ctx: ElicitationContext): Effect.Effect<typeof ElicitationResponse.Type> => {
123142 const { url: supportsUrl } = getElicitationSupport(server);
124143
@@ -133,19 +152,51 @@ const makeMcpElicitationHandler = · 8 unmodified lines
133152 : elicitationRequestToParams(ctx.request);
134153
135154 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+ });
136166 try {
137167 const response = await server.server.elicitInput(
138168 params as Parameters<typeof server.server.elicitInput>[0],
139169 );
140170
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+
141180 return {
142181 action: response.action,
143182 content: response.content,
144183 };
145184 } 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+ });
146193 console.error(
147194 "[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+ }),
149200 );
150201 return { action: "cancel" };
151202 }
@@ -205,6 +256,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 53 unmodified lines
205256 // deferred past the outer Effect's await), so we use the runtime to
206257 // re-enter Effect-land at each callback edge.
207258 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+ };
208268
209269 const resolveParentSpan = (): Tracer.AnySpan | undefined => {
210270 const ps = config.parentSpan;
@@ -228,13 +288,27 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 17 unmodified lines
228288
229289 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>
230290 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+ });
231297 if (supportsManagedElicitation(server)) {
232298 const result = yield* engine.execute(code, {
233− onElicitation: makeMcpElicitationHandler(server),
299+ onElicitation: makeMcpElicitationHandler(server, debugLog),
234300 });
235301 return toMcpResult(formatExecuteResult(result));
236302 }
237303 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+ });
238312 return outcome.status === "completed"
239313 ? toMcpResult(formatExecuteResult(outcome.result))
240314 : toMcpPausedResult(formatPausedExecution(outcome.execution));
@@ -253,8 +327,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 12 unmodified lines
253327 content: Record<string, unknown> | undefined,
254328 ): Effect.Effect<McpToolResult, E> =>
255329 Effect.gen(function* () {
330+ debugLog("resume.call", {
331+ executionId,
332+ action,
333+ hasContent: content !== undefined,
334+ clientCapabilities: server.server.getClientCapabilities() ?? null,
335+ });
256336 const outcome = yield* engine.resume(executionId, { action, content });
257337 if (!outcome) {
338+ debugLog("resume.missing_execution", { executionId });
258339 return {
259340 content: [
260341 { type: "text" as const, text: `No paused execution: ${executionId}` },
@@ -262,6 +343,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 1 unmodified line
262343 isError: true,
263344 } satisfies McpToolResult;
264345 }
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+ });
265355 return outcome.status === "completed"
266356 ? toMcpResult(formatExecuteResult(outcome.result))
267357 : toMcpPausedResult(formatPausedExecution(outcome.execution));
@@ -331,6 +421,19 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 63 unmodified lines
331421 } else {
332422 resumeTool.enable();
333423 }
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+ });
334437 };
335438
336439 yield* Effect.sync(() => {