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
66 * under whichever request triggered it; see the Cloud DO).66 * under whichever request triggered it; see the Cloud DO).
67 */67 */
68 readonly parentSpan?: Tracer.AnySpan | (() => Tracer.AnySpan | undefined);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
71export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> =75export type ExecutorMcpServerConfig<E extends Cause.YieldableError = Cause.YieldableError> =
@@ -85,9 +89,21 @@ const getElicitationSupport = (server: McpServer): { form: boolean; url: boolean · 13 unmodified lines
85 return { form: Boolean(elicitation.form), url: Boolean(elicitation.url) };89 return { form: Boolean(elicitation.form), url: Boolean(elicitation.url) };
86};90};
87 91
92const 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
88const supportsManagedElicitation = (server: McpServer): boolean =>98const supportsManagedElicitation = (server: McpServer): boolean =>
89 getElicitationSupport(server).form;99 getElicitationSupport(server).form;
90 100
101const capabilitySnapshot = (server: McpServer) => ({
102 clientCapabilities: server.server.getClientCapabilities() ?? null,
103 elicitationSupport: getElicitationSupport(server),
104 managedElicitation: supportsManagedElicitation(server),
105});
106
91type ElicitInputParams =107type ElicitInputParams =
92 | {108 | {
93 mode?: "form";109 mode?: "form";
@@ -118,7 +134,10 @@ const elicitationRequestToParams: (request: ElicitationRequest) => ElicitInputPa · 24 unmodified lines
118 );134 );
119 135
120const makeMcpElicitationHandler =136const makeMcpElicitationHandler =
121 (server: McpServer): ElicitationHandler =>137 (
138 server: McpServer,
139 debugLog?: (event: string, data: Record<string, unknown>) => void,
140 ): ElicitationHandler =>
122 (ctx: ElicitationContext): Effect.Effect<typeof ElicitationResponse.Type> => {141 (ctx: ElicitationContext): Effect.Effect<typeof ElicitationResponse.Type> => {
123 const { url: supportsUrl } = getElicitationSupport(server);142 const { url: supportsUrl } = getElicitationSupport(server);
124 143
@@ -133,19 +152,51 @@ const makeMcpElicitationHandler = · 8 unmodified lines
133 : elicitationRequestToParams(ctx.request);152 : elicitationRequestToParams(ctx.request);
134 153
135 return Effect.promise(async (): Promise<typeof ElicitationResponse.Type> => {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 try {166 try {
137 const response = await server.server.elicitInput(167 const response = await server.server.elicitInput(
138 params as Parameters<typeof server.server.elicitInput>[0],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 return {180 return {
142 action: response.action,181 action: response.action,
143 content: response.content,182 content: response.content,
144 };183 };
145 } catch (err) {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 console.error(193 console.error(
147 "[executor] elicitInput failed — falling back to cancel.",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 return { action: "cancel" };201 return { action: "cancel" };
151 }202 }
@@ -205,6 +256,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 53 unmodified lines
205 // deferred past the outer Effect's await), so we use the runtime to256 // deferred past the outer Effect's await), so we use the runtime to
206 // re-enter Effect-land at each callback edge.257 // re-enter Effect-land at each callback edge.
207 const runtime = yield* Effect.runtime<never>();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 const resolveParentSpan = (): Tracer.AnySpan | undefined => {269 const resolveParentSpan = (): Tracer.AnySpan | undefined => {
210 const ps = config.parentSpan;270 const ps = config.parentSpan;
@@ -228,13 +288,27 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 17 unmodified lines
228 288
229 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>289 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>
230 Effect.gen(function* () {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 if (supportsManagedElicitation(server)) {297 if (supportsManagedElicitation(server)) {
232 const result = yield* engine.execute(code, {298 const result = yield* engine.execute(code, {
233 onElicitation: makeMcpElicitationHandler(server),299 onElicitation: makeMcpElicitationHandler(server, debugLog),
234 });300 });
235 return toMcpResult(formatExecuteResult(result));301 return toMcpResult(formatExecuteResult(result));
236 }302 }
237 const outcome = yield* engine.executeWithPause(code);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 return outcome.status === "completed"312 return outcome.status === "completed"
239 ? toMcpResult(formatExecuteResult(outcome.result))313 ? toMcpResult(formatExecuteResult(outcome.result))
240 : toMcpPausedResult(formatPausedExecution(outcome.execution));314 : toMcpPausedResult(formatPausedExecution(outcome.execution));
@@ -253,8 +327,15 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 12 unmodified lines
253 content: Record<string, unknown> | undefined,327 content: Record<string, unknown> | undefined,
254 ): Effect.Effect<McpToolResult, E> =>328 ): Effect.Effect<McpToolResult, E> =>
255 Effect.gen(function* () {329 Effect.gen(function* () {
330 debugLog("resume.call", {
331 executionId,
332 action,
333 hasContent: content !== undefined,
334 clientCapabilities: server.server.getClientCapabilities() ?? null,
335 });
256 const outcome = yield* engine.resume(executionId, { action, content });336 const outcome = yield* engine.resume(executionId, { action, content });
257 if (!outcome) {337 if (!outcome) {
338 debugLog("resume.missing_execution", { executionId });
258 return {339 return {
259 content: [340 content: [
260 { type: "text" as const, text: `No paused execution: ${executionId}` },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 isError: true,343 isError: true,
263 } satisfies McpToolResult;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 return outcome.status === "completed"355 return outcome.status === "completed"
266 ? toMcpResult(formatExecuteResult(outcome.result))356 ? toMcpResult(formatExecuteResult(outcome.result))
267 : toMcpPausedResult(formatPausedExecution(outcome.execution));357 : toMcpPausedResult(formatPausedExecution(outcome.execution));
@@ -331,6 +421,19 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 63 unmodified lines
331 } else {421 } else {
332 resumeTool.enable();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 yield* Effect.sync(() => {439 yield* Effect.sync(() => {