History

feat(otel): instrument tool dispatch, plugins, storage, schema, transport

Closes the holistic OTEL coverage gap so every executor request can be attributed to a named subsystem in Axiom. Verified end-to-end against a deployed Vercel API call: the previously-unspanned 817ms host-side gap now resolves into named child spans (tool.resolve, resolve_annotations, storage.find_one × N by table, plugin.openapi.secret.resolve, etc). Spans added across five subsystems: - Tool dispatch (sdk/executor.ts, execution/engine.ts, tool-invoker.ts): executor.tool.{invoke,resolve,resolve_annotations,enforce_approval, handler,schema,schema.load_defs}, executor.tools.{list,list.annotations}, plus dispatch-path / source / plugin attributes. Built-in search / describe / list-sources branches now also produce mcp.tool.dispatch envelopes for trace-shape parity with plugin tool calls. - Plugin invocation (plugins/{openapi,graphql,mcp}/sdk): plugin.openapi.{invoke,secret.resolve}, plugin.graphql.{invoke,secret.resolve}, plugin.mcp.{invoke,connection.acquire,connection.handshake,client.call_tool, invoke.retry}. Existing OpenApi.* / GraphQL.* spans retained; new plugin.* spans wrap them so secret-hydration vs HTTP time is visible. - Storage layer (storage-core/factory.ts, storage-drizzle/adapter.ts): Two-layer instrumentation. executor.storage.* on the factory abstraction and executor.storage.backend.* on the drizzle implementation. Gap between them surfaces transform overhead vs SQL/driver cost. Per-op attributes carry table, row count, and transaction strategy. - Schema generation (execution/description.ts, sdk/schema-types.ts): Renamed buildExecuteDescription span to schema.describe.execute. Added schema.compile.{description,preview} so cold-startup and describe-tool latency split into source-listing vs DB-fetch vs TS-rendering. - MCP transport / host (cloud/mcp-session.ts, hosts/mcp/server.ts, plugins/mcp/sdk/plugin.ts): mcp.session.{load_meta,save_meta, cleanup,resolve_and_store_meta,request_scoped_runtime,...}, mcp.host.{create_executor_server,create_server,register_tool, sync_tool_availability,tool.execute,tool.resume}, mcp.plugin.{invoke_tool,load_binding,load_source_config, resolve_connector,discover_tools,probe_endpoint,probe_oauth, add_source,remove_source,refresh_source,update_source,...}. Also rich attributes (method, content-type, content-length, response status) added to the existing transport.handleRequest span. Naming convention: subsystem-prefixed dot-namespaced spans — mcp.* / executor.* / executor.tool.* / executor.sources.* / executor.storage.* / plugin.{openapi,graphql,mcp}.* / vault.* / schema.*. No behavior changes; instrumentation only.

Rhys Sullivan committed 6 months ago

Commit e6a65371 parent db5367714 files · +764 −237Browse files

Unified · Split

14 files
  1. apps/cloud/src/mcp-session.ts +56 −11
  2. packages/core/execution/src/description.ts +13 −2
  3. packages/core/execution/src/engine.ts +19 −3
  4. packages/core/execution/src/tool-invoker.ts +14 −2
  5. packages/core/sdk/src/executor.ts +91 −31
  6. packages/core/sdk/src/schema-types.ts +8 −5
  7. packages/core/storage-core/src/factory.ts +82 −14
  8. packages/core/storage-drizzle/src/adapter.ts +70 −10
  9. packages/hosts/mcp/src/server.ts +81 −40
  10. packages/plugins/graphql/src/sdk/invoke.ts +38 −4
  11. packages/plugins/mcp/src/sdk/connection.ts +5 −1
  12. packages/plugins/mcp/src/sdk/invoke.ts +40 −6
  13. packages/plugins/mcp/src/sdk/plugin.ts +210 −103
  14. packages/plugins/openapi/src/sdk/invoke.ts +37 −5
apps/cloud/src/mcp-session.ts+56−11
@@ -211,6 +211,12 @@ export class McpSessionDO extends DurableObject { · 210 unmodified lines
211211 return this.sessionMeta;
212212 }
213213
214+ private loadSessionMetaEffect(): Effect.Effect<SessionMeta | null> {
215+ return Effect.promise(() => this.loadSessionMeta()).pipe(
216+ Effect.withSpan("mcp.session.load_meta"),
217+ );
218+ }
219+
214220 private async saveSessionMeta(sessionMeta: SessionMeta): Promise<void> {
215221 this.sessionMeta = sessionMeta;
216222 await this.ctx.storage.put(SESSION_META_KEY, sessionMeta);
@@ -227,6 +233,12 @@ export class McpSessionDO extends DurableObject { · 10 unmodified lines
227233 ]);
228234 }
229235
236+ private clearSessionStateEffect(): Effect.Effect<void> {
237+ return Effect.promise(() => this.clearSessionState()).pipe(
238+ Effect.withSpan("mcp.session.clear_state"),
239+ );
240+ }
241+
230242 private createConnectedRuntimeEffect(
231243 sessionMeta: SessionMeta,
232244 options: { readonly dbHandle: DbHandle; readonly enableJsonResponse?: boolean },
@@ -271,12 +283,14 @@ export class McpSessionDO extends DurableObject { · 38 unmodified lines
271283 const sessionMeta = yield* resolveSessionMeta(token.organizationId).pipe(
272284 Effect.provide(makeResolveOrganizationServices(dbHandle)),
273285 );
274− yield* Effect.promise(() => self.saveSessionMeta(sessionMeta));
286+ yield* Effect.promise(() => self.saveSessionMeta(sessionMeta)).pipe(
287+ Effect.withSpan("mcp.session.save_meta"),
288+ );
275289 return sessionMeta;
276290 } finally {
277291 yield* Effect.promise(() => dbHandle.end());
278292 }
279− });
293+ }).pipe(Effect.withSpan("mcp.session.resolve_and_store_meta"));
280294 }
281295
282296 async init(token: McpSessionInit, incoming?: IncomingTraceHeaders): Promise<void> {
@@ -346,7 +360,7 @@ export class McpSessionDO extends DurableObject { · 63 unmodified lines
346360 private handleRequestWithRequestScopedRuntimeEffect(request: Request) {
347361 const self = this;
348362 return Effect.gen(function* () {
349− const sessionMeta = yield* Effect.promise(() => self.loadSessionMeta());
363+ const sessionMeta = yield* self.loadSessionMetaEffect();
350364 if (!sessionMeta) {
351365 return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect");
352366 }
@@ -355,7 +369,9 @@ export class McpSessionDO extends DurableObject { · 2 unmodified lines
355369 self.lastActivityMs = Date.now();
356370
357371 const dbHandle = makeRequestScopedDb();
358− const cleanupDb = Effect.promise(() => dbHandle.end());
372+ const cleanupDb = Effect.promise(() => dbHandle.end()).pipe(
373+ Effect.withSpan("mcp.session.db.close"),
374+ );
359375 return yield* Effect.acquireUseRelease(
360376 self.createConnectedRuntimeEffect(sessionMeta, {
361377 dbHandle,
@@ -364,21 +380,37 @@ export class McpSessionDO extends DurableObject { · 2 unmodified lines
364380 ({ transport }) =>
365381 Effect.gen(function* () {
366382 const response = yield* Effect.promise(() => transport.handleRequest(request)).pipe(
367− Effect.withSpan("McpSessionDO.transport.handleRequest"),
383+ Effect.withSpan("McpSessionDO.transport.handleRequest", {
384+ attributes: {
385+ "mcp.request.method": request.method,
386+ "mcp.request.content_type":
387+ request.headers.get("content-type") ?? "",
388+ "mcp.request.content_length":
389+ request.headers.get("content-length") ?? "",
390+ },
391+ }),
368392 );
393+ yield* Effect.annotateCurrentSpan({
394+ "mcp.response.status_code": response.status,
395+ });
369396 if (request.method === "DELETE") {
370− yield* Effect.promise(() => self.clearSessionState());
397+ yield* self.clearSessionStateEffect();
371398 }
372399 return response;
373400 }),
374401 ({ mcpServer, transport }) =>
375402 Effect.gen(function* () {
376− yield* Effect.promise(() => transport.close().catch(() => undefined));
377− yield* Effect.promise(() => mcpServer.close().catch(() => undefined));
403+ yield* Effect.promise(() => transport.close().catch(() => undefined)).pipe(
404+ Effect.withSpan("mcp.session.transport.close"),
405+ );
406+ yield* Effect.promise(() => mcpServer.close().catch(() => undefined)).pipe(
407+ Effect.withSpan("mcp.session.server.close"),
408+ );
378409 yield* cleanupDb;
379− }),
410+ }).pipe(Effect.withSpan("mcp.session.runtime.release")),
380411 );
381412 }).pipe(
413+ Effect.withSpan("mcp.session.request_scoped_runtime"),
382414 Effect.catchAllCause((cause) =>
383415 Effect.sync(() => {
384416 console.error("[mcp-session] request-scoped handleRequest error:", cause);
@@ -451,10 +483,23 @@ export class McpSessionDO extends DurableObject { · 66 unmodified lines
451483 const self = this;
452484 return Effect.gen(function* () {
453485 const response = yield* Effect.promise(() => transport.handleRequest(request)).pipe(
454− Effect.withSpan("McpSessionDO.transport.handleRequest"),
486+ Effect.withSpan("McpSessionDO.transport.handleRequest", {
487+ attributes: {
488+ "mcp.request.method": request.method,
489+ "mcp.request.content_type":
490+ request.headers.get("content-type") ?? "",
491+ "mcp.request.content_length":
492+ request.headers.get("content-length") ?? "",
493+ },
494+ }),
455495 );
496+ yield* Effect.annotateCurrentSpan({
497+ "mcp.response.status_code": response.status,
498+ });
456499 if (request.method === "DELETE") {
457− yield* Effect.promise(() => self.cleanup());
500+ yield* Effect.promise(() => self.cleanup()).pipe(
501+ Effect.withSpan("mcp.session.cleanup"),
502+ );
458503 }
459504 return response;
460505 }).pipe(
packages/core/execution/src/description.ts+13−2
@@ -14,8 +14,19 @@ export const buildExecuteDescription = (executor: Executor): Effect.Effect<strin · 13 unmodified lines
1414 .list()
1515 .pipe(Effect.orDie, Effect.withSpan("executor.sources.list"));
1616
17− return formatDescription(sources);
18− }).pipe(Effect.withSpan("buildExecuteDescription"));
17+ const description = yield* Effect.sync(() => formatDescription(sources)).pipe(
18+ Effect.withSpan("schema.compile.description", {
19+ attributes: { "executor.source_count": sources.length },
20+ }),
21+ );
22+
23+ yield* Effect.annotateCurrentSpan({
24+ "executor.source_count": sources.length,
25+ "schema.kind": "execute",
26+ });
27+
28+ return description;
29+ }).pipe(Effect.withSpan("schema.describe.execute"));
1930
2031 const formatDescription = (sources: readonly Source[]): string => {
2132 const lines: string[] = [
packages/core/execution/src/engine.ts+19−3
@@ -194,7 +194,11 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 193 unmodified lines
194194
195195 return searchTools(executor, args.query ?? "", limit, {
196196 namespace: args.namespace,
197− });
197+ }).pipe(
198+ Effect.withSpan("mcp.tool.dispatch", {
199+ attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
200+ }),
201+ );
198202 }
199203 if (path === "executor.sources.list") {
200204 if (args !== undefined && !isRecord(args)) {
@@ -225,7 +229,11 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 24 unmodified lines
225229 return listExecutorSources(executor, {
226230 query: isRecord(args) && typeof args.query === "string" ? args.query : undefined,
227231 limit,
228− });
232+ }).pipe(
233+ Effect.withSpan("mcp.tool.dispatch", {
234+ attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
235+ }),
236+ );
229237 }
230238 if (path === "describe.tool") {
231239 if (!isRecord(args)) {
@@ -248,7 +256,15 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 16 unmodified lines
248256 );
249257 }
250258
251− return describeTool(executor, args.path);
259+ return describeTool(executor, args.path).pipe(
260+ Effect.withSpan("mcp.tool.dispatch", {
261+ attributes: {
262+ "mcp.tool.name": path,
263+ "executor.tool.builtin": true,
264+ "executor.tool.target_path": args.path,
265+ },
266+ }),
267+ );
252268 }
253269 return base.invoke({ path, args });
254270 },
packages/core/execution/src/tool-invoker.ts+14−2
@@ -288,12 +288,18 @@ export const searchTools = Effect.fn("executor.tools.search")(function* ( · 287 unmodified lines
288288 }
289289
290290 const all = yield* executor.tools.list().pipe(Effect.orDie);
291− return all
291+ const results = all
292292 .filter((tool: Tool) => matchesNamespace(tool, options?.namespace))
293293 .map((tool: Tool) => scoreToolMatch(tool, query))
294294 .filter((tool): tool is ToolDiscoveryResult => tool !== null)
295295 .sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
296296 .slice(0, limit);
297+
298+ yield* Effect.annotateCurrentSpan({
299+ "executor.search.candidate_count": all.length,
300+ "executor.search.result_count": results.length,
301+ });
302+ return results;
297303 });
298304
299305 /** What `tools.executor.sources.list()` calls inside the sandbox. */
@@ -333,9 +339,15 @@ export const listExecutorSources = Effect.fn("executor.sources.list")(function* · 33 unmodified lines
333339 }) satisfies ExecutorSourceListItem,
334340 );
335341
336− return withCounts
342+ const results = withCounts
337343 .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id))
338344 .slice(0, limit);
345+
346+ yield* Effect.annotateCurrentSpan({
347+ "executor.sources.candidate_count": sources.length,
348+ "executor.sources.result_count": results.length,
349+ });
350+ return results;
339351 });
340352
341353 /** What `tools.describe.tool()` calls inside the sandbox. */
packages/core/sdk/src/executor.ts+91−31
@@ -892,8 +892,13 @@ export const createExecutor = < · 891 unmodified lines
892892 for (const { source, pluginId } of staticSources.values()) {
893893 staticList.push(staticDeclToSource(source, pluginId));
894894 }
895− return [...staticList, ...dynamic.map(rowToSource)];
896− });
895+ const merged = [...staticList, ...dynamic.map(rowToSource)];
896+ yield* Effect.annotateCurrentSpan({
897+ "executor.sources.static_count": staticList.length,
898+ "executor.sources.dynamic_count": dynamic.length,
899+ });
900+ return merged;
901+ }).pipe(Effect.withSpan("executor.sources.list"));
897902
898903 // Bulk-resolve annotations across a set of dynamic tool rows by
899904 // grouping them under their owning plugin's resolveAnnotations
@@ -936,7 +941,9 @@ export const createExecutor = < · 36 unmodified lines
936941 const listTools = (filter?: ToolListFilter) =>
937942 Effect.gen(function* () {
938943 const dynamic = yield* core.findMany({ model: "tool" });
939− const annotations = yield* resolveAnnotationsFor(dynamic);
944+ const annotations = yield* resolveAnnotationsFor(dynamic).pipe(
945+ Effect.withSpan("executor.tools.list.annotations"),
946+ );
940947
941948 const out: Tool[] = [];
942949 // Static tools — annotations from the declaration, not a resolver.
@@ -946,9 +953,14 @@ export const createExecutor = < · 3 unmodified lines
946953 for (const row of dynamic) {
947954 out.push(rowToTool(row, annotations.get(row.id)));
948955 }
949− if (!filter) return out;
950− return out.filter((t) => toolMatchesFilter(t, filter));
951− });
956+ const result = filter ? out.filter((t) => toolMatchesFilter(t, filter)) : out;
957+ yield* Effect.annotateCurrentSpan({
958+ "executor.tools.static_count": staticTools.size,
959+ "executor.tools.dynamic_count": dynamic.length,
960+ "executor.tools.result_count": result.length,
961+ });
962+ return result;
963+ }).pipe(Effect.withSpan("executor.tools.list"));
952964
953965 // Load all definitions for a single source as a plain map.
954966 const loadDefinitionsForSource = (sourceId: string) =>
@@ -975,7 +987,9 @@ export const createExecutor = < · 20 unmodified lines
975987 }) =>
976988 Effect.gen(function* () {
977989 const defs: Record<string, unknown> = opts.sourceId
978− ? yield* loadDefinitionsForSource(opts.sourceId)
990+ ? yield* loadDefinitionsForSource(opts.sourceId).pipe(
991+ Effect.withSpan("executor.tool.schema.load_defs"),
992+ )
979993 : {};
980994
981995 const attachDefs = (schema: unknown): unknown => {
@@ -988,11 +1002,18 @@ export const createExecutor = < · 6 unmodified lines
9881002 const outputSchema = attachDefs(opts.rawOutput);
9891003
9901004 const defsMap = new Map<string, unknown>(Object.entries(defs));
991− const preview = buildToolTypeScriptPreview({
992− inputSchema,
993− outputSchema,
994− defs: defsMap,
995− });
1005+ const preview = yield* Effect.sync(() =>
1006+ buildToolTypeScriptPreview({ inputSchema, outputSchema, defs: defsMap }),
1007+ ).pipe(
1008+ Effect.withSpan("schema.compile.preview", {
1009+ attributes: {
1010+ "schema.kind": "tool.preview",
1011+ "schema.has_input": inputSchema !== undefined,
1012+ "schema.has_output": outputSchema !== undefined,
1013+ "schema.def_count": defsMap.size,
1014+ },
1015+ }),
1016+ );
9961017
9971018 return new ToolSchema({
9981019 id: ToolId.make(opts.toolId),
@@ -1012,6 +1033,11 @@ export const createExecutor = < · 13 unmodified lines
10121033 // no `$defs` attach; just wrap the declared schemas.
10131034 const staticEntry = staticTools.get(toolId);
10141035 if (staticEntry) {
1036+ yield* Effect.annotateCurrentSpan({
1037+ "executor.tool.dispatch_path": "static",
1038+ "executor.source_id": staticEntry.source.id,
1039+ "executor.source_kind": staticEntry.source.kind,
1040+ });
10151041 return yield* buildToolSchemaView({
10161042 toolId,
10171043 name: staticEntry.tool.name,
@@ -1021,11 +1047,18 @@ export const createExecutor = < · 3 unmodified lines
10211047 rawOutput: staticEntry.tool.outputSchema,
10221048 });
10231049 }
1024− const row = yield* core.findOne({
1025− model: "tool",
1026− where: [{ field: "id", value: toolId }],
1027− });
1050+ const row = yield* core
1051+ .findOne({
1052+ model: "tool",
1053+ where: [{ field: "id", value: toolId }],
1054+ })
1055+ .pipe(Effect.withSpan("executor.tool.resolve"));
10281056 if (!row) return null;
1057+ yield* Effect.annotateCurrentSpan({
1058+ "executor.tool.dispatch_path": "dynamic",
1059+ "executor.source_id": row.source_id,
1060+ "executor.plugin_id": row.plugin_id,
1061+ });
10291062 return yield* buildToolSchemaView({
10301063 toolId,
10311064 name: row.name,
@@ -1034,7 +1067,11 @@ export const createExecutor = < · 2 unmodified lines
10341067 rawInput: decodeJsonColumn(row.input_schema),
10351068 rawOutput: decodeJsonColumn(row.output_schema),
10361069 });
1037− });
1070+ }).pipe(
1071+ Effect.withSpan("executor.tool.schema", {
1072+ attributes: { "mcp.tool.name": toolId },
1073+ }),
1074+ );
10381075
10391076 // Bulk definitions accessor — every source's $defs, grouped by
10401077 // source id. One query against the definition table, plus an
@@ -1121,31 +1158,44 @@ export const createExecutor = < · 80 unmodified lines
11211158 // Static path — O(1) map lookup, no DB hit.
11221159 const staticEntry = staticTools.get(toolId);
11231160 if (staticEntry) {
1161+ yield* Effect.annotateCurrentSpan({
1162+ "executor.tool.dispatch_path": "static",
1163+ "executor.source_id": staticEntry.source.id,
1164+ "executor.source_kind": staticEntry.source.kind,
1165+ "executor.plugin_id": staticEntry.pluginId,
1166+ });
11241167 yield* enforceApproval(
11251168 staticEntry.tool.annotations,
11261169 toolId,
11271170 args,
11281171 options,
1129− );
1172+ ).pipe(Effect.withSpan("executor.tool.enforce_approval"));
11301173 return yield* wrapInvocationError(
11311174 staticEntry.tool.handler({
11321175 ctx: staticEntry.ctx,
11331176 args,
11341177 elicit: buildElicit(toolId, args, options),
11351178 }),
1136− );
1179+ ).pipe(Effect.withSpan("executor.tool.handler"));
11371180 }
11381181
11391182 // Dynamic path — DB lookup + delegate to owning plugin.
1140− const row = yield* core.findOne({
1141− model: "tool",
1142− where: [{ field: "id", value: toolId }],
1143− });
1183+ const row = yield* core
1184+ .findOne({
1185+ model: "tool",
1186+ where: [{ field: "id", value: toolId }],
1187+ })
1188+ .pipe(Effect.withSpan("executor.tool.resolve"));
11441189 if (!row) {
11451190 return yield* new ToolNotFoundError({
11461191 toolId: ToolId.make(toolId),
11471192 });
11481193 }
1194+ yield* Effect.annotateCurrentSpan({
1195+ "executor.tool.dispatch_path": "dynamic",
1196+ "executor.source_id": row.source_id,
1197+ "executor.plugin_id": row.plugin_id,
1198+ });
11491199 const runtime = runtimes.get(row.plugin_id);
11501200 if (!runtime) {
11511201 return yield* new PluginNotLoadedError({
@@ -1167,14 +1217,18 @@ export const createExecutor = < · 15 unmodified lines
11671217 // around a single storage read.
11681218 let annotations: ToolAnnotations | undefined;
11691219 if (runtime.plugin.resolveAnnotations) {
1170− const map = yield* runtime.plugin.resolveAnnotations({
1171− ctx: runtime.ctx,
1172− sourceId: row.source_id,
1173− toolRows: [row],
1174− });
1220+ const map = yield* runtime.plugin
1221+ .resolveAnnotations({
1222+ ctx: runtime.ctx,
1223+ sourceId: row.source_id,
1224+ toolRows: [row],
1225+ })
1226+ .pipe(Effect.withSpan("executor.tool.resolve_annotations"));
11751227 annotations = map[toolId];
11761228 }
1177− yield* enforceApproval(annotations, toolId, args, options);
1229+ yield* enforceApproval(annotations, toolId, args, options).pipe(
1230+ Effect.withSpan("executor.tool.enforce_approval"),
1231+ );
11781232
11791233 return yield* wrapInvocationError(
11801234 runtime.plugin.invokeTool({
@@ -1183,8 +1237,14 @@ export const createExecutor = < · 2 unmodified lines
11831237 args,
11841238 elicit: buildElicit(toolId, args, options),
11851239 }),
1186− );
1187− });
1240+ ).pipe(Effect.withSpan("executor.tool.handler"));
1241+ }).pipe(
1242+ Effect.withSpan("executor.tool.invoke", {
1243+ attributes: {
1244+ "mcp.tool.name": toolId,
1245+ },
1246+ }),
1247+ );
11881248
11891249 const removeSource = (sourceId: string) =>
11901250 Effect.gen(function* () {
packages/core/sdk/src/schema-types.ts+8−5
@@ -364,16 +364,18 @@ export const schemaToTypeScriptPreviewWithDefs = ( · 363 unmodified lines
364364 };
365365 };
366366
367+export type ToolTypeScriptPreview = {
368+ inputTypeScript?: string;
369+ outputTypeScript?: string;
370+ typeScriptDefinitions?: Record<string, string>;
371+};
372+
367373 export const buildToolTypeScriptPreview = (input: {
368374 inputSchema?: unknown;
369375 outputSchema?: unknown;
370376 defs: ReadonlyMap<string, unknown>;
371377 options?: TypeScriptRenderOptions;
372−}): {
373− inputTypeScript?: string;
374− outputTypeScript?: string;
375− typeScriptDefinitions?: Record<string, string>;
376−} => {
378+}): ToolTypeScriptPreview => {
377379 const inputPreview =
378380 input.inputSchema !== undefined
379381 ? schemaToTypeScriptPreviewWithDefs(input.inputSchema, input.defs, input.options)
@@ -396,3 +398,4 @@ export const buildToolTypeScriptPreview = (input: { · 16 unmodified lines
396398 : {}),
397399 };
398400 };
401+
packages/core/storage-core/src/factory.ts+82−14
@@ -638,7 +638,14 @@ export const createAdapter = ( · 637 unmodified lines
638638 data.select,
639639 );
640640 return out as unknown as R;
641− }),
641+ }).pipe(
642+ Effect.withSpan("executor.storage.create", {
643+ attributes: {
644+ "executor.storage.adapter_id": config.adapterId,
645+ "executor.storage.table": data.model,
646+ },
647+ }),
648+ ),
642649
643650 createMany: <T extends Record<string, unknown>, R = T>(data: {
644651 model: string;
@@ -678,7 +685,15 @@ export const createAdapter = ( · 33 unmodified lines
678685 );
679686 }
680687 return out as unknown as readonly R[];
681− }),
688+ }).pipe(
689+ Effect.withSpan("executor.storage.create_many", {
690+ attributes: {
691+ "executor.storage.adapter_id": config.adapterId,
692+ "executor.storage.table": data.model,
693+ "executor.storage.row_count": data.data.length,
694+ },
695+ }),
696+ ),
682697
683698 findOne: <T>(data: {
684699 model: string;
@@ -698,7 +713,14 @@ export const createAdapter = ( · 13 unmodified lines
698713 const out = yield* maybeTransformOutput(data.model, res, data.select);
699714 const merged = yield* attachJoinedRows(out, res, data.join);
700715 return merged as unknown as T | null;
701− }),
716+ }).pipe(
717+ Effect.withSpan("executor.storage.find_one", {
718+ attributes: {
719+ "executor.storage.adapter_id": config.adapterId,
720+ "executor.storage.table": data.model,
721+ },
722+ }),
723+ ),
702724
703725 findMany: <T>(data: {
704726 model: string;
@@ -734,7 +756,14 @@ export const createAdapter = ( · 29 unmodified lines
734756 out.push(merged);
735757 }
736758 return out as readonly T[];
737− }),
759+ }).pipe(
760+ Effect.withSpan("executor.storage.find_many", {
761+ attributes: {
762+ "executor.storage.adapter_id": config.adapterId,
763+ "executor.storage.table": data.model,
764+ },
765+ }),
766+ ),
738767
739768 count: (data: { model: string; where?: Where[] | undefined }) =>
740769 Effect.gen(function* () {
@@ -743,7 +772,14 @@ export const createAdapter = ( · 2 unmodified lines
743772 model: getModelName(data.model),
744773 where,
745774 });
746− }),
775+ }).pipe(
776+ Effect.withSpan("executor.storage.count", {
777+ attributes: {
778+ "executor.storage.adapter_id": config.adapterId,
779+ "executor.storage.table": data.model,
780+ },
781+ }),
782+ ),
747783
748784 update: <T>(data: {
749785 model: string;
@@ -765,7 +801,14 @@ export const createAdapter = ( · 15 unmodified lines
765801 });
766802 const out = yield* maybeTransformOutput(data.model, res);
767803 return out as unknown as T | null;
768− }),
804+ }).pipe(
805+ Effect.withSpan("executor.storage.update", {
806+ attributes: {
807+ "executor.storage.adapter_id": config.adapterId,
808+ "executor.storage.table": data.model,
809+ },
810+ }),
811+ ),
769812
770813 updateMany: (data: {
771814 model: string;
@@ -785,7 +828,14 @@ export const createAdapter = ( · 13 unmodified lines
785828 where,
786829 update,
787830 });
788− }),
831+ }).pipe(
832+ Effect.withSpan("executor.storage.update_many", {
833+ attributes: {
834+ "executor.storage.adapter_id": config.adapterId,
835+ "executor.storage.table": data.model,
836+ },
837+ }),
838+ ),
789839
790840 delete: (data: { model: string; where: Where[] }) =>
791841 Effect.gen(function* () {
@@ -794,7 +844,14 @@ export const createAdapter = ( · 2 unmodified lines
794844 model: getModelName(data.model),
795845 where,
796846 });
797− }),
847+ }).pipe(
848+ Effect.withSpan("executor.storage.delete", {
849+ attributes: {
850+ "executor.storage.adapter_id": config.adapterId,
851+ "executor.storage.table": data.model,
852+ },
853+ }),
854+ ),
798855
799856 deleteMany: (data: { model: string; where: Where[] }) =>
800857 Effect.gen(function* () {
@@ -803,17 +860,28 @@ export const createAdapter = ( · 2 unmodified lines
803860 model: getModelName(data.model),
804861 where,
805862 });
806− }),
863+ }).pipe(
864+ Effect.withSpan("executor.storage.delete_many", {
865+ attributes: {
866+ "executor.storage.adapter_id": config.adapterId,
867+ "executor.storage.table": data.model,
868+ },
869+ }),
870+ ),
807871
808872 transaction: <R, E>(
809873 callback: (trx: DBTransactionAdapter) => Effect.Effect<R, E>,
810874 ) => {
811875 const txFn = config.transaction;
812− if (!txFn) {
813− // No real transaction support — just run the callback against self.
814− return callback(self);
815− }
816− return txFn(callback);
876+ const ran = !txFn ? callback(self) : txFn(callback);
877+ return ran.pipe(
878+ Effect.withSpan("executor.storage.transaction", {
879+ attributes: {
880+ "executor.storage.adapter_id": config.adapterId,
881+ "executor.storage.transaction.native": txFn !== undefined,
882+ },
883+ }),
884+ );
817885 },
818886
819887 // Forward the backend's createSchema verbatim. Upstream better-auth
packages/core/storage-drizzle/src/adapter.ts+70−10
@@ -361,6 +361,12 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 360 unmodified lines
361361 return t;
362362 };
363363
364+ const backendAttrs = (model: string) => ({
365+ "executor.storage.backend": "drizzle" as const,
366+ "executor.storage.drizzle.provider": provider,
367+ "executor.storage.table": model,
368+ });
369+
364370 const custom: CustomAdapter = {
365371 create: ({ model, data }) =>
366372 Effect.gen(function* () {
@@ -375,7 +381,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 8 unmodified lines
375381 model,
376382 );
377383 return row as never;
378− }),
384+ }).pipe(
385+ Effect.withSpan("executor.storage.backend.create", {
386+ attributes: backendAttrs(model),
387+ }),
388+ ),
379389
380390 // Real multi-row INSERT in fixed-size chunks. One statement per
381391 // chunk, not one per row — per-row loops blow the Hyperdrive
@@ -401,7 +411,14 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 19 unmodified lines
401411 for (const row of rows) all.push(row);
402412 }
403413 return all as never;
404− }),
414+ }).pipe(
415+ Effect.withSpan("executor.storage.backend.create_many", {
416+ attributes: {
417+ ...backendAttrs(model),
418+ "executor.storage.row_count": data.length,
419+ },
420+ }),
421+ ),
405422
406423 findOne: ({ model, where, join }) =>
407424 Effect.gen(function* () {
@@ -430,7 +447,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 22 unmodified lines
430447 model,
431448 )) as Record<string, unknown>[];
432449 return (rows[0] ?? null) as never;
433− }),
450+ }).pipe(
451+ Effect.withSpan("executor.storage.backend.find_one", {
452+ attributes: backendAttrs(model),
453+ }),
454+ ),
434455
435456 findMany: ({ model, where, limit, sortBy, offset, join }) =>
436457 Effect.gen(function* () {
@@ -477,7 +498,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 40 unmodified lines
477498 model,
478499 )) as Record<string, unknown>[];
479500 return rows as never[];
480− }),
501+ }).pipe(
502+ Effect.withSpan("executor.storage.backend.find_many", {
503+ attributes: backendAttrs(model),
504+ }),
505+ ),
481506
482507 count: ({ model, where }) =>
483508 Effect.gen(function* () {
@@ -492,7 +517,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 8 unmodified lines
492517 )) as { c: number | string | bigint }[];
493518 const raw = rows[0]?.c ?? 0;
494519 return typeof raw === "number" ? raw : Number(raw);
495− }),
520+ }).pipe(
521+ Effect.withSpan("executor.storage.backend.count", {
522+ attributes: backendAttrs(model),
523+ }),
524+ ),
496525
497526 update: ({ model, where, update }) =>
498527 Effect.gen(function* () {
@@ -529,7 +558,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 30 unmodified lines
529558 model,
530559 )) as Record<string, unknown>[];
531560 return (reread[0] ?? null) as never;
532− }),
561+ }).pipe(
562+ Effect.withSpan("executor.storage.backend.update", {
563+ attributes: backendAttrs(model),
564+ }),
565+ ),
533566
534567 updateMany: ({ model, where, update }) =>
535568 Effect.gen(function* () {
@@ -554,7 +587,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 18 unmodified lines
554587 model,
555588 );
556589 return n;
557− }),
590+ }).pipe(
591+ Effect.withSpan("executor.storage.backend.update_many", {
592+ attributes: backendAttrs(model),
593+ }),
594+ ),
558595
559596 delete: ({ model, where }) =>
560597 Effect.gen(function* () {
@@ -575,7 +612,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 14 unmodified lines
575612 () => Promise.resolve(db.delete(table).where(eq(table.id, first.id))),
576613 model,
577614 );
578− }),
615+ }).pipe(
616+ Effect.withSpan("executor.storage.backend.delete", {
617+ attributes: backendAttrs(model),
618+ }),
619+ ),
579620
580621 deleteMany: ({ model, where }) =>
581622 Effect.gen(function* () {
@@ -598,7 +639,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 16 unmodified lines
598639 model,
599640 );
600641 return n;
601− }),
642+ }).pipe(
643+ Effect.withSpan("executor.storage.backend.delete_many", {
644+ attributes: backendAttrs(model),
645+ }),
646+ ),
602647 };
603648
604649 // Transaction strategy differs by dialect:
@@ -659,6 +704,13 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 54 unmodified lines
659704 if (e instanceof TxFailure) return e.inner;
660705 return classifyError("pg transaction", undefined, e);
661706 }),
707+ Effect.withSpan("executor.storage.backend.transaction", {
708+ attributes: {
709+ "executor.storage.backend": "drizzle",
710+ "executor.storage.drizzle.provider": provider,
711+ "executor.storage.transaction.strategy": "drizzle_native",
712+ },
713+ }),
662714 ) as Effect.Effect<R, E | StorageFailure>;
663715 }
664716
@@ -711,7 +763,15 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 46 unmodified lines
711763 });
712764 }
713765 return result;
714− });
766+ }).pipe(
767+ Effect.withSpan("executor.storage.backend.transaction", {
768+ attributes: {
769+ "executor.storage.backend": "drizzle",
770+ "executor.storage.drizzle.provider": provider,
771+ "executor.storage.transaction.strategy": "raw_begin_commit",
772+ },
773+ }),
774+ );
715775 }
716776 : undefined;
717777
packages/hosts/mcp/src/server.ts+81−40
@@ -195,7 +195,11 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 194 unmodified lines
195195 ): Effect.Effect<McpServer> =>
196196 Effect.gen(function* () {
197197 const engine = "engine" in config ? config.engine : createExecutionEngine(config);
198− const description = config.description ?? (yield* engine.getDescription);
198+ const description =
199+ config.description ??
200+ (yield* engine.getDescription.pipe(
201+ Effect.withSpan("mcp.host.get_description"),
202+ ));
199203
200204 // Captured at construction time. SDK callbacks fire later (often
201205 // deferred past the outer Effect's await), so we use the runtime to
@@ -211,10 +215,16 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 9 unmodified lines
211215 return parent ? Effect.withParentSpan(effect, parent) : effect;
212216 };
213217
214− const server = new McpServer(
215− { name: "executor", version: "1.0.0" },
216− { capabilities: { tools: {} }, jsonSchemaValidator: new CfWorkerJsonSchemaValidator() },
217− );
218+ const server = yield* Effect.sync(
219+ () =>
220+ new McpServer(
221+ { name: "executor", version: "1.0.0" },
222+ {
223+ capabilities: { tools: {} },
224+ jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
225+ },
226+ ),
227+ ).pipe(Effect.withSpan("mcp.host.create_server"));
218228
219229 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>
220230 Effect.gen(function* () {
@@ -228,7 +238,14 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines
228238 return outcome.status === "completed"
229239 ? toMcpResult(formatExecuteResult(outcome.result))
230240 : toMcpPausedResult(formatPausedExecution(outcome.execution));
231− });
241+ }).pipe(
242+ Effect.withSpan("mcp.host.tool.execute", {
243+ attributes: {
244+ "mcp.tool.name": "execute",
245+ "mcp.execute.code_length": code.length,
246+ },
247+ }),
248+ );
232249
233250 const resumeExecution = (
234251 executionId: string,
@@ -239,48 +256,70 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 4 unmodified lines
239256 const outcome = yield* engine.resume(executionId, { action, content });
240257 if (!outcome) {
241258 return {
242− content: [{ type: "text", text: `No paused execution: ${executionId}` }],
259+ content: [
260+ { type: "text" as const, text: `No paused execution: ${executionId}` },
261+ ],
243262 isError: true,
244− };
263+ } satisfies McpToolResult;
245264 }
246265 return outcome.status === "completed"
247266 ? toMcpResult(formatExecuteResult(outcome.result))
248267 : toMcpPausedResult(formatPausedExecution(outcome.execution));
249− });
268+ }).pipe(
269+ Effect.withSpan("mcp.host.tool.resume", {
270+ attributes: {
271+ "mcp.tool.name": "resume",
272+ "mcp.execute.resume.action": action,
273+ "mcp.execute.execution_id": executionId,
274+ },
275+ }),
276+ );
250277
251278 // --- tools ---
252279
253− const executeTool = server.registerTool(
254− "execute",
255− {
256− description,
257− inputSchema: { code: z.string().trim().min(1) },
258− },
259− ({ code }) => Runtime.runPromise(runtime)(anchor(executeCode(code))),
280+ const executeTool = yield* Effect.sync(() =>
281+ server.registerTool(
282+ "execute",
283+ {
284+ description,
285+ inputSchema: { code: z.string().trim().min(1) },
286+ },
287+ ({ code }) => Runtime.runPromise(runtime)(anchor(executeCode(code))),
288+ ),
289+ ).pipe(
290+ Effect.withSpan("mcp.host.register_tool", {
291+ attributes: { "mcp.tool.name": "execute" },
292+ }),
260293 );
261294
262− const resumeTool = server.registerTool(
263− "resume",
264− {
265− description: [
266− "Resume a paused execution using the executionId returned by execute.",
267− "Never call this without user approval unless they explicitly state otherwise.",
268− ].join("\n"),
269− inputSchema: {
270− executionId: z.string().describe("The execution ID from the paused result"),
271− action: z
272− .enum(["accept", "decline", "cancel"])
273− .describe("How to respond to the interaction"),
274− content: z
275− .string()
276− .describe("Optional JSON-encoded response content for form elicitations")
277− .default("{}"),
295+ const resumeTool = yield* Effect.sync(() =>
296+ server.registerTool(
297+ "resume",
298+ {
299+ description: [
300+ "Resume a paused execution using the executionId returned by execute.",
301+ "Never call this without user approval unless they explicitly state otherwise.",
302+ ].join("\n"),
303+ inputSchema: {
304+ executionId: z.string().describe("The execution ID from the paused result"),
305+ action: z
306+ .enum(["accept", "decline", "cancel"])
307+ .describe("How to respond to the interaction"),
308+ content: z
309+ .string()
310+ .describe("Optional JSON-encoded response content for form elicitations")
311+ .default("{}"),
312+ },
278313 },
279− },
280− ({ executionId, action, content: rawContent }) =>
281− Runtime.runPromise(runtime)(
282− anchor(resumeExecution(executionId, action, parseJsonContent(rawContent))),
283− ),
314+ ({ executionId, action, content: rawContent }) =>
315+ Runtime.runPromise(runtime)(
316+ anchor(resumeExecution(executionId, action, parseJsonContent(rawContent))),
317+ ),
318+ ),
319+ ).pipe(
320+ Effect.withSpan("mcp.host.register_tool", {
321+ attributes: { "mcp.tool.name": "resume" },
322+ }),
284323 );
285324
286325 // --- capability-based tool visibility ---
@@ -294,8 +333,10 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines
294333 }
295334 };
296335
297− syncToolAvailability();
298− server.server.oninitialized = syncToolAvailability;
336+ yield* Effect.sync(() => {
337+ syncToolAvailability();
338+ server.server.oninitialized = syncToolAvailability;
339+ }).pipe(Effect.withSpan("mcp.host.sync_tool_availability"));
299340
300341 return server;
301− });
342+ }).pipe(Effect.withSpan("mcp.host.create_executor_server"));
packages/plugins/graphql/src/sdk/invoke.ts+38−4
@@ -15,9 +15,13 @@ import { · 14 unmodified lines
1515 export const resolveHeaders = (
1616 headers: Record<string, HeaderValue>,
1717 secrets: { readonly get: (id: string) => Effect.Effect<string | null, unknown> },
18−): Effect.Effect<Record<string, string>> =>
19− Effect.gen(function* () {
20− const entries = Object.entries(headers);
18+): Effect.Effect<Record<string, string>> => {
19+ const entries = Object.entries(headers);
20+ const secretCount = entries.reduce(
21+ (acc, [, value]) => (typeof value === "string" ? acc : acc + 1),
22+ 0,
23+ );
24+ return Effect.gen(function* () {
2125 // Resolve secret-backed headers in parallel. Missing / failing
2226 // lookups drop the header rather than fail the invocation, same
2327 // as the serial version.
@@ -48,7 +52,15 @@ export const resolveHeaders = ( · 24 unmodified lines
4852 if (value !== null) resolved[name] = value;
4953 }
5054 return resolved;
51− });
55+ }).pipe(
56+ Effect.withSpan("plugin.graphql.secret.resolve", {
57+ attributes: {
58+ "plugin.graphql.headers.total": entries.length,
59+ "plugin.graphql.headers.secret_count": secretCount,
60+ },
61+ }),
62+ );
63+};
5264
5365 // ---------------------------------------------------------------------------
5466 // Response helpers
@@ -74,6 +86,15 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 19 unmodified lines
7486 ) {
7587 const client = yield* HttpClient.HttpClient;
7688
89+ yield* Effect.annotateCurrentSpan({
90+ "http.method": "POST",
91+ "http.url": endpoint,
92+ "plugin.graphql.endpoint": endpoint,
93+ "plugin.graphql.operation_kind": operation.kind,
94+ "plugin.graphql.field_name": operation.fieldName,
95+ "plugin.graphql.headers.resolved_count": Object.keys(resolvedHeaders).length,
96+ });
97+
7798 // Build the GraphQL request body
7899 const variables: Record<string, unknown> = {};
79100 for (const varName of operation.variableNames) {
@@ -121,6 +142,12 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 41 unmodified lines
121142 const gqlBody = body as { data?: unknown; errors?: unknown[] } | null;
122143 const hasErrors = Array.isArray(gqlBody?.errors) && gqlBody.errors.length > 0;
123144
145+ yield* Effect.annotateCurrentSpan({
146+ "http.status_code": status,
147+ "plugin.graphql.has_errors": hasErrors,
148+ "plugin.graphql.error_count": hasErrors ? gqlBody!.errors!.length : 0,
149+ });
150+
124151 return new InvocationResult({
125152 status,
126153 data: gqlBody?.data ?? null,
@@ -150,4 +177,11 @@ export const invokeWithLayer = ( · 23 unmodified lines
150177 cause: err,
151178 }),
152179 ),
180+ Effect.withSpan("plugin.graphql.invoke", {
181+ attributes: {
182+ "plugin.graphql.endpoint": endpoint,
183+ "plugin.graphql.operation_kind": operation.kind,
184+ "plugin.graphql.field_name": operation.fieldName,
185+ },
186+ }),
153187 );
packages/plugins/mcp/src/sdk/connection.ts+5−1
@@ -79,7 +79,11 @@ const connectClient = (input: { · 78 unmodified lines
7979 cause instanceof Error ? cause.message : String(cause)
8080 }`,
8181 }),
82− });
82+ }).pipe(
83+ Effect.withSpan("plugin.mcp.connection.handshake", {
84+ attributes: { "plugin.mcp.transport": input.transport },
85+ }),
86+ );
8387
8488 return connectionFromClient(client);
8589 });
packages/plugins/mcp/src/sdk/invoke.ts+40−6
@@ -136,7 +136,11 @@ const useConnection = ( · 135 unmodified lines
136136 cause instanceof Error ? cause.message : String(cause)
137137 }`,
138138 }),
139− });
139+ }).pipe(
140+ Effect.withSpan("plugin.mcp.client.call_tool", {
141+ attributes: { "mcp.tool.name": toolName },
142+ }),
143+ );
140144 });
141145
142146 // ---------------------------------------------------------------------------
@@ -163,8 +167,12 @@ export interface InvokeMcpToolInput { · 20 unmodified lines
163167
164168 export const invokeMcpTool = (
165169 input: InvokeMcpToolInput,
166−): Effect.Effect<unknown, McpConnectionError | McpInvocationError> =>
167− Effect.gen(function* () {
170+): Effect.Effect<unknown, McpConnectionError | McpInvocationError> => {
171+ const transport: string =
172+ input.sourceData.transport === "stdio"
173+ ? "stdio"
174+ : (input.sourceData.remoteTransport ?? "auto");
175+ return Effect.gen(function* () {
168176 const cacheKey = connectionCacheKey(input.sourceData);
169177 const args = asRecord(input.args);
170178
@@ -173,7 +181,15 @@ export const invokeMcpTool = ( · 2 unmodified lines
173181 const connector = input.resolveConnector();
174182 input.pendingConnectors.set(cacheKey, connector);
175183
176− const firstConnection = yield* input.connectionCache.get(cacheKey);
184+ const firstConnection = yield* input.connectionCache.get(cacheKey).pipe(
185+ Effect.withSpan("plugin.mcp.connection.acquire", {
186+ attributes: {
187+ "plugin.mcp.transport": transport,
188+ "plugin.mcp.cache_key": cacheKey,
189+ "plugin.mcp.attempt": 1,
190+ },
191+ }),
192+ );
177193
178194 return yield* useConnection(
179195 firstConnection,
@@ -194,7 +210,25 @@ export const invokeMcpTool = ( · 14 unmodified lines
194210 args,
195211 input.elicit,
196212 );
197− }),
213+ }).pipe(
214+ Effect.withSpan("plugin.mcp.invoke.retry", {
215+ attributes: {
216+ "plugin.mcp.transport": transport,
217+ "plugin.mcp.cache_key": cacheKey,
218+ "mcp.tool.name": input.toolName,
219+ },
220+ }),
221+ ),
198222 ),
199223 );
200− }).pipe(Effect.scoped);
224+ }).pipe(
225+ Effect.scoped,
226+ Effect.withSpan("plugin.mcp.invoke", {
227+ attributes: {
228+ "mcp.tool.name": input.toolName,
229+ "plugin.mcp.tool_id": input.toolId,
230+ "plugin.mcp.transport": transport,
231+ },
232+ }),
233+ );
234+};
packages/plugins/mcp/src/sdk/plugin.ts+210−103
@@ -445,6 +445,7 @@ export const mcpPlugin = definePlugin( · 444 unmodified lines
445445 Effect.catchAll(() =>
446446 Effect.succeed({ ok: false as const, manifest: null }),
447447 ),
448+ Effect.withSpan("mcp.plugin.discover_tools"),
448449 );
449450
450451 if (result.ok && result.manifest) {
@@ -465,6 +466,7 @@ export const mcpPlugin = definePlugin( · 14 unmodified lines
465466 }).pipe(
466467 Effect.map(() => true),
467468 Effect.catchAll(() => Effect.succeed(false)),
469+ Effect.withSpan("mcp.plugin.probe_oauth"),
468470 );
469471
470472 if (hasOAuth) {
@@ -483,7 +485,11 @@ export const mcpPlugin = definePlugin( · 12 unmodified lines
483485 "Could not connect to MCP endpoint and no OAuth was detected",
484486 ),
485487 );
486− });
488+ }).pipe(
489+ Effect.withSpan("mcp.plugin.probe_endpoint", {
490+ attributes: { "mcp.endpoint": endpoint },
491+ }),
492+ );
487493
488494 const configFile = options?.configFile;
489495
@@ -493,7 +499,14 @@ export const mcpPlugin = definePlugin( · 3 unmodified lines
493499 const sd = toStoredSourceData(config);
494500
495501 // Resolve auth (may fail if stdio gate is off)
496− const ci = yield* resolveConnectorInput(sd, ctx, allowStdio);
502+ const ci = yield* resolveConnectorInput(sd, ctx, allowStdio).pipe(
503+ Effect.withSpan("mcp.plugin.resolve_connector", {
504+ attributes: {
505+ "mcp.source.namespace": namespace,
506+ "mcp.source.transport": sd.transport,
507+ },
508+ }),
509+ );
497510
498511 const connector = createMcpConnector(ci);
499512 // Try discovery. If it fails (auth, network, bad spec), we still
@@ -505,6 +518,9 @@ export const mcpPlugin = definePlugin( · 5 unmodified lines
505518 mcpDiscoveryError(`MCP discovery failed: ${err.message}`),
506519 ),
507520 Effect.either,
521+ Effect.withSpan("mcp.plugin.discover_tools", {
522+ attributes: { "mcp.source.namespace": namespace },
523+ }),
508524 );
509525 const manifest =
510526 discovery._tag === "Right"
@@ -513,73 +529,101 @@ export const mcpPlugin = definePlugin( · 2 unmodified lines
513529
514530 const sourceName = manifest.server?.name ?? config.name ?? namespace;
515531
516− yield* ctx.transaction(
517− Effect.gen(function* () {
518− // Remove stale rows for this namespace (plugin-owned)
519− yield* ctx.storage.removeBindingsByNamespace(namespace);
520− yield* ctx.storage.removeSource(namespace);
521−
522− yield* ctx.storage.putSource({
523− namespace,
524− name: sourceName,
525− config: sd,
526− });
527−
528− yield* ctx.storage.putBindings(
529− namespace,
530− manifest.tools.map((e) => ({
531− toolId: `${namespace}.${e.toolId}`,
532− binding: toBinding(e),
533− })),
534− );
535−
536− yield* ctx.core.sources.register({
537− id: namespace,
538− kind: "mcp",
539− name: sourceName,
540− url: sd.transport === "remote" ? sd.endpoint : undefined,
541− canRemove: true,
542− canRefresh: true,
543− canEdit: sd.transport === "remote",
544− tools: manifest.tools.map((e) => ({
545− name: e.toolId,
546− description: e.description ?? `MCP tool: ${e.toolName}`,
547− inputSchema: e.inputSchema,
548− outputSchema: e.outputSchema,
549− })),
550− });
551− }),
552− );
532+ yield* ctx
533+ .transaction(
534+ Effect.gen(function* () {
535+ // Remove stale rows for this namespace (plugin-owned)
536+ yield* ctx.storage.removeBindingsByNamespace(namespace);
537+ yield* ctx.storage.removeSource(namespace);
538+
539+ yield* ctx.storage.putSource({
540+ namespace,
541+ name: sourceName,
542+ config: sd,
543+ });
544+
545+ yield* ctx.storage.putBindings(
546+ namespace,
547+ manifest.tools.map((e) => ({
548+ toolId: `${namespace}.${e.toolId}`,
549+ binding: toBinding(e),
550+ })),
551+ );
552+
553+ yield* ctx.core.sources.register({
554+ id: namespace,
555+ kind: "mcp",
556+ name: sourceName,
557+ url: sd.transport === "remote" ? sd.endpoint : undefined,
558+ canRemove: true,
559+ canRefresh: true,
560+ canEdit: sd.transport === "remote",
561+ tools: manifest.tools.map((e) => ({
562+ name: e.toolId,
563+ description: e.description ?? `MCP tool: ${e.toolName}`,
564+ inputSchema: e.inputSchema,
565+ outputSchema: e.outputSchema,
566+ })),
567+ });
568+ }),
569+ )
570+ .pipe(
571+ Effect.withSpan("mcp.plugin.persist_source", {
572+ attributes: {
573+ "mcp.source.namespace": namespace,
574+ "mcp.source.tool_count": manifest.tools.length,
575+ },
576+ }),
577+ );
553578
554579 if (configFile) {
555− yield* configFile.upsertSource(
556− toMcpConfigEntry(namespace, sourceName, config),
557− );
580+ yield* configFile
581+ .upsertSource(toMcpConfigEntry(namespace, sourceName, config))
582+ .pipe(Effect.withSpan("mcp.plugin.config_file.upsert"));
558583 }
559584
560585 if (discovery._tag === "Left") {
561586 return yield* Effect.fail(discovery.left);
562587 }
563588 return { toolCount: manifest.tools.length, namespace };
564− });
589+ }).pipe(
590+ Effect.withSpan("mcp.plugin.add_source", {
591+ attributes: {
592+ "mcp.source.transport": config.transport,
593+ "mcp.source.name": config.name,
594+ },
595+ }),
596+ );
565597
566598 const removeSource = (namespace: string) =>
567599 Effect.gen(function* () {
568− yield* ctx.transaction(
569− Effect.gen(function* () {
570− yield* ctx.storage.removeBindingsByNamespace(namespace);
571− yield* ctx.storage.removeSource(namespace);
572− yield* ctx.core.sources.unregister(namespace);
573− }),
574− );
600+ yield* ctx
601+ .transaction(
602+ Effect.gen(function* () {
603+ yield* ctx.storage.removeBindingsByNamespace(namespace);
604+ yield* ctx.storage.removeSource(namespace);
605+ yield* ctx.core.sources.unregister(namespace);
606+ }),
607+ )
608+ .pipe(Effect.withSpan("mcp.plugin.persist_remove"));
575609 if (configFile) {
576− yield* configFile.removeSource(namespace);
610+ yield* configFile
611+ .removeSource(namespace)
612+ .pipe(Effect.withSpan("mcp.plugin.config_file.remove"));
577613 }
578− });
614+ }).pipe(
615+ Effect.withSpan("mcp.plugin.remove_source", {
616+ attributes: { "mcp.source.namespace": namespace },
617+ }),
618+ );
579619
580620 const refreshSource = (namespace: string) =>
581621 Effect.gen(function* () {
582− const sd = yield* ctx.storage.getSourceConfig(namespace);
622+ const sd = yield* ctx.storage.getSourceConfig(namespace).pipe(
623+ Effect.withSpan("mcp.plugin.load_source_config", {
624+ attributes: { "mcp.source.namespace": namespace },
625+ }),
626+ );
583627 if (!sd) {
584628 return yield* Effect.fail(
585629 remoteConnectionError(
@@ -588,49 +632,72 @@ export const mcpPlugin = definePlugin( · 2 unmodified lines
588632 );
589633 }
590634
591− const ci = yield* resolveConnectorInput(sd, ctx, allowStdio);
635+ const ci = yield* resolveConnectorInput(sd, ctx, allowStdio).pipe(
636+ Effect.withSpan("mcp.plugin.resolve_connector", {
637+ attributes: {
638+ "mcp.source.namespace": namespace,
639+ "mcp.source.transport": sd.transport,
640+ },
641+ }),
642+ );
592643 const manifest = yield* discoverTools(createMcpConnector(ci)).pipe(
593644 Effect.mapError((err) =>
594645 mcpDiscoveryError(`MCP refresh failed: ${err.message}`),
595646 ),
647+ Effect.withSpan("mcp.plugin.discover_tools", {
648+ attributes: { "mcp.source.namespace": namespace },
649+ }),
596650 );
597651
598652 const existing = yield* ctx.storage.getSource(namespace);
599653 const sourceName =
600654 manifest.server?.name ?? existing?.name ?? namespace;
601655
602− yield* ctx.transaction(
603− Effect.gen(function* () {
604− yield* ctx.storage.removeBindingsByNamespace(namespace);
605− yield* ctx.core.sources.unregister(namespace);
606−
607− yield* ctx.storage.putBindings(
608− namespace,
609− manifest.tools.map((e) => ({
610− toolId: `${namespace}.${e.toolId}`,
611− binding: toBinding(e),
612− })),
613− );
614− yield* ctx.core.sources.register({
615− id: namespace,
616− kind: "mcp",
617− name: sourceName,
618− url: sd.transport === "remote" ? sd.endpoint : undefined,
619− canRemove: true,
620− canRefresh: true,
621− canEdit: sd.transport === "remote",
622− tools: manifest.tools.map((e) => ({
623− name: e.toolId,
624− description: e.description ?? `MCP tool: ${e.toolName}`,
625− inputSchema: e.inputSchema,
626− outputSchema: e.outputSchema,
627− })),
628− });
629− }),
630− );
656+ yield* ctx
657+ .transaction(
658+ Effect.gen(function* () {
659+ yield* ctx.storage.removeBindingsByNamespace(namespace);
660+ yield* ctx.core.sources.unregister(namespace);
661+
662+ yield* ctx.storage.putBindings(
663+ namespace,
664+ manifest.tools.map((e) => ({
665+ toolId: `${namespace}.${e.toolId}`,
666+ binding: toBinding(e),
667+ })),
668+ );
669+ yield* ctx.core.sources.register({
670+ id: namespace,
671+ kind: "mcp",
672+ name: sourceName,
673+ url: sd.transport === "remote" ? sd.endpoint : undefined,
674+ canRemove: true,
675+ canRefresh: true,
676+ canEdit: sd.transport === "remote",
677+ tools: manifest.tools.map((e) => ({
678+ name: e.toolId,
679+ description: e.description ?? `MCP tool: ${e.toolName}`,
680+ inputSchema: e.inputSchema,
681+ outputSchema: e.outputSchema,
682+ })),
683+ });
684+ }),
685+ )
686+ .pipe(
687+ Effect.withSpan("mcp.plugin.persist_source", {
688+ attributes: {
689+ "mcp.source.namespace": namespace,
690+ "mcp.source.tool_count": manifest.tools.length,
691+ },
692+ }),
693+ );
631694
632695 return { toolCount: manifest.tools.length };
633− });
696+ }).pipe(
697+ Effect.withSpan("mcp.plugin.refresh_source", {
698+ attributes: { "mcp.source.namespace": namespace },
699+ }),
700+ );
634701
635702 const startOAuth = (input: McpOAuthStartInput) =>
636703 Effect.gen(function* () {
@@ -659,24 +726,27 @@ export const mcpPlugin = definePlugin( · 22 unmodified lines
659726 Effect.mapError((e) =>
660727 mcpOAuthError(`OAuth start failed: ${e.message}`),
661728 ),
729+ Effect.withSpan("mcp.plugin.oauth.start_authorization"),
662730 );
663731
664− yield* ctx.storage.putOAuthSession(sessionId, {
665− endpoint: fullEndpoint,
666− redirectUrl: input.redirectUrl,
667− codeVerifier: started.codeVerifier,
668− resourceMetadataUrl: started.resourceMetadataUrl,
669− authorizationServerUrl: started.authorizationServerUrl,
670− resourceMetadata: started.resourceMetadata,
671− authorizationServerMetadata: started.authorizationServerMetadata,
672− clientInformation: started.clientInformation,
673− });
732+ yield* ctx.storage
733+ .putOAuthSession(sessionId, {
734+ endpoint: fullEndpoint,
735+ redirectUrl: input.redirectUrl,
736+ codeVerifier: started.codeVerifier,
737+ resourceMetadataUrl: started.resourceMetadataUrl,
738+ authorizationServerUrl: started.authorizationServerUrl,
739+ resourceMetadata: started.resourceMetadata,
740+ authorizationServerMetadata: started.authorizationServerMetadata,
741+ clientInformation: started.clientInformation,
742+ })
743+ .pipe(Effect.withSpan("mcp.plugin.oauth.persist_session"));
674744
675745 return {
676746 sessionId,
677747 authorizationUrl: started.authorizationUrl,
678748 };
679− });
749+ }).pipe(Effect.withSpan("mcp.plugin.start_oauth"));
680750
681751 const completeOAuth = (input: McpOAuthCompleteInput) =>
682752 Effect.gen(function* () {
@@ -705,6 +775,7 @@ export const mcpPlugin = definePlugin( · 22 unmodified lines
705775 Effect.mapError((e) =>
706776 mcpOAuthError(`OAuth exchange failed: ${e.message}`),
707777 ),
778+ Effect.withSpan("mcp.plugin.oauth.exchange_code"),
708779 );
709780
710781 const accessSecretId = `mcp-oauth-access-${input.state}`;
@@ -757,7 +828,7 @@ export const mcpPlugin = definePlugin( · 46 unmodified lines
757828 expiresAt,
758829 scope: exchanged.tokens.scope ?? null,
759830 };
760− });
831+ }).pipe(Effect.withSpan("mcp.plugin.complete_oauth"));
761832
762833 const updateSource = (
763834 namespace: string,
@@ -783,10 +854,18 @@ export const mcpPlugin = definePlugin( · 19 unmodified lines
783854 name: input.name?.trim() || existing.name,
784855 config: updatedConfig,
785856 });
786− });
857+ }).pipe(
858+ Effect.withSpan("mcp.plugin.update_source", {
859+ attributes: { "mcp.source.namespace": namespace },
860+ }),
861+ );
787862
788863 const getSource = (namespace: string) =>
789− ctx.storage.getSource(namespace);
864+ ctx.storage.getSource(namespace).pipe(
865+ Effect.withSpan("mcp.plugin.get_source", {
866+ attributes: { "mcp.source.namespace": namespace },
867+ }),
868+ );
790869
791870 return {
792871 probeEndpoint,
@@ -804,14 +883,22 @@ export const mcpPlugin = definePlugin( · 11 unmodified lines
804883 Effect.gen(function* () {
805884 const runtime = yield* ensureRuntime();
806885
807− const entry = yield* ctx.storage.getBinding(toolRow.id);
886+ const entry = yield* ctx.storage.getBinding(toolRow.id).pipe(
887+ Effect.withSpan("mcp.plugin.load_binding", {
888+ attributes: { "mcp.tool.name": toolRow.id },
889+ }),
890+ );
808891 if (!entry) {
809892 return yield* Effect.fail(
810893 new Error(`No MCP binding found for tool "${toolRow.id}"`),
811894 );
812895 }
813896
814− const sd = yield* ctx.storage.getSourceConfig(entry.namespace);
897+ const sd = yield* ctx.storage.getSourceConfig(entry.namespace).pipe(
898+ Effect.withSpan("mcp.plugin.load_source_config", {
899+ attributes: { "mcp.source.namespace": entry.namespace },
900+ }),
901+ );
815902 if (!sd) {
816903 return yield* Effect.fail(
817904 new Error(
@@ -837,12 +924,25 @@ export const mcpPlugin = definePlugin( · 19 unmodified lines
837924 err instanceof Error ? err.message : String(err),
838925 }),
839926 ),
927+ Effect.withSpan("mcp.plugin.resolve_connector", {
928+ attributes: {
929+ "mcp.source.namespace": entry.namespace,
930+ "mcp.source.transport": sd.transport,
931+ },
932+ }),
840933 ),
841934 connectionCache: runtime.connectionCache,
842935 pendingConnectors: runtime.pendingConnectors,
843936 elicit,
844937 });
845− }),
938+ }).pipe(
939+ Effect.withSpan("mcp.plugin.invoke_tool", {
940+ attributes: {
941+ "mcp.tool.name": toolRow.id,
942+ "mcp.tool.source_id": toolRow.source_id,
943+ },
944+ }),
945+ ),
846946
847947 detect: ({ url }) =>
848948 Effect.gen(function* () {
@@ -865,6 +965,7 @@ export const mcpPlugin = definePlugin( · 16 unmodified lines
865965 const connected = yield* discoverTools(connector).pipe(
866966 Effect.map(() => true),
867967 Effect.catchAll(() => Effect.succeed(false)),
968+ Effect.withSpan("mcp.plugin.discover_tools"),
868969 );
869970
870971 if (connected) {
@@ -885,6 +986,7 @@ export const mcpPlugin = definePlugin( · 14 unmodified lines
885986 }).pipe(
886987 Effect.map(() => true),
887988 Effect.catchAll(() => Effect.succeed(false)),
989+ Effect.withSpan("mcp.plugin.probe_oauth"),
888990 );
889991
890992 if (hasOAuth) {
@@ -898,7 +1000,12 @@ export const mcpPlugin = definePlugin( · 7 unmodified lines
8981000 }
8991001
9001002 return null;
901− }).pipe(Effect.catchAll(() => Effect.succeed(null))),
1003+ }).pipe(
1004+ Effect.catchAll(() => Effect.succeed(null)),
1005+ Effect.withSpan("mcp.plugin.detect", {
1006+ attributes: { "mcp.endpoint": url },
1007+ }),
1008+ ),
9021009
9031010 // MCP tools never require approval at the tool level — elicitation is
9041011 // handled mid-invocation by the server via the elicit capability.
@@ -928,7 +1035,7 @@ export const mcpPlugin = definePlugin( · 23 unmodified lines
9281035 yield* Scope.close(runtime.cacheScope, Exit.void);
9291036 runtimeRef.current = null;
9301037 }
931− }),
1038+ }).pipe(Effect.withSpan("mcp.plugin.close")),
9321039 };
9331040 },
9341041 );
packages/plugins/openapi/src/sdk/invoke.ts+37−5
@@ -102,9 +102,13 @@ export const resolveHeaders = ( · 101 unmodified lines
102102 ): Effect.Effect<
103103 Record<string, string>,
104104 OpenApiInvocationError | StorageFailure
105−> =>
106− Effect.gen(function* () {
107− const entries = Object.entries(headers);
105+> => {
106+ const entries = Object.entries(headers);
107+ const secretCount = entries.reduce(
108+ (acc, [, value]) => (typeof value === "string" ? acc : acc + 1),
109+ 0,
110+ );
111+ return Effect.gen(function* () {
108112 // Fan out secret lookups: on every invocation, one or two headers
109113 // typically each hit the secret store. Resolving them in parallel
110114 // is a free wall-clock win — preserved order is only needed for
@@ -134,7 +138,15 @@ export const resolveHeaders = ( · 23 unmodified lines
134138 const resolved: Record<string, string> = {};
135139 for (const { name, value } of values) resolved[name] = value;
136140 return resolved;
137− });
141+ }).pipe(
142+ Effect.withSpan("plugin.openapi.secret.resolve", {
143+ attributes: {
144+ "plugin.openapi.headers.total": entries.length,
145+ "plugin.openapi.headers.secret_count": secretCount,
146+ },
147+ }),
148+ );
149+};
138150
139151 const applyHeaders = (
140152 request: HttpClientRequest.HttpClientRequest,
@@ -170,6 +182,14 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 29 unmodified lines
170182 ) {
171183 const client = yield* HttpClient.HttpClient;
172184
185+ yield* Effect.annotateCurrentSpan({
186+ "http.method": operation.method.toUpperCase(),
187+ "http.route": operation.pathTemplate,
188+ "plugin.openapi.method": operation.method.toUpperCase(),
189+ "plugin.openapi.path_template": operation.pathTemplate,
190+ "plugin.openapi.headers.resolved_count": Object.keys(resolvedHeaders).length,
191+ });
192+
173193 const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);
174194
175195 const path = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;
@@ -216,6 +236,9 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 40 unmodified lines
216236 );
217237
218238 const status = response.status;
239+ yield* Effect.annotateCurrentSpan({
240+ "http.status_code": status,
241+ });
219242 const responseHeaders: Record<string, string> = { ...response.headers };
220243
221244 const contentType = response.headers["content-type"] ?? null;
@@ -268,7 +291,16 @@ export const invokeWithLayer = ( · 46 unmodified lines
268291 ).pipe(Layer.provide(httpClientLayer))
269292 : httpClientLayer;
270293
271− return invoke(operation, args, resolvedHeaders).pipe(Effect.provide(clientWithBaseUrl));
294+ return invoke(operation, args, resolvedHeaders).pipe(
295+ Effect.provide(clientWithBaseUrl),
296+ Effect.withSpan("plugin.openapi.invoke", {
297+ attributes: {
298+ "plugin.openapi.method": operation.method.toUpperCase(),
299+ "plugin.openapi.path_template": operation.pathTemplate,
300+ "plugin.openapi.base_url": baseUrl,
301+ },
302+ }),
303+ );
272304 };
273305
274306 // ---------------------------------------------------------------------------