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
211 return this.sessionMeta;211 return this.sessionMeta;
212 }212 }
213 213
214 private loadSessionMetaEffect(): Effect.Effect<SessionMeta | null> {
215 return Effect.promise(() => this.loadSessionMeta()).pipe(
216 Effect.withSpan("mcp.session.load_meta"),
217 );
218 }
219
214 private async saveSessionMeta(sessionMeta: SessionMeta): Promise<void> {220 private async saveSessionMeta(sessionMeta: SessionMeta): Promise<void> {
215 this.sessionMeta = sessionMeta;221 this.sessionMeta = sessionMeta;
216 await this.ctx.storage.put(SESSION_META_KEY, sessionMeta);222 await this.ctx.storage.put(SESSION_META_KEY, sessionMeta);
@@ -227,6 +233,12 @@ export class McpSessionDO extends DurableObject { · 10 unmodified lines
227 ]);233 ]);
228 }234 }
229 235
236 private clearSessionStateEffect(): Effect.Effect<void> {
237 return Effect.promise(() => this.clearSessionState()).pipe(
238 Effect.withSpan("mcp.session.clear_state"),
239 );
240 }
241
230 private createConnectedRuntimeEffect(242 private createConnectedRuntimeEffect(
231 sessionMeta: SessionMeta,243 sessionMeta: SessionMeta,
232 options: { readonly dbHandle: DbHandle; readonly enableJsonResponse?: boolean },244 options: { readonly dbHandle: DbHandle; readonly enableJsonResponse?: boolean },
@@ -271,12 +283,14 @@ export class McpSessionDO extends DurableObject { · 38 unmodified lines
271 const sessionMeta = yield* resolveSessionMeta(token.organizationId).pipe(283 const sessionMeta = yield* resolveSessionMeta(token.organizationId).pipe(
272 Effect.provide(makeResolveOrganizationServices(dbHandle)),284 Effect.provide(makeResolveOrganizationServices(dbHandle)),
273 );285 );
274 yield* Effect.promise(() => self.saveSessionMeta(sessionMeta));286 yield* Effect.promise(() => self.saveSessionMeta(sessionMeta)).pipe(
287 Effect.withSpan("mcp.session.save_meta"),
288 );
275 return sessionMeta;289 return sessionMeta;
276 } finally {290 } finally {
277 yield* Effect.promise(() => dbHandle.end());291 yield* Effect.promise(() => dbHandle.end());
278 }292 }
279 });293 }).pipe(Effect.withSpan("mcp.session.resolve_and_store_meta"));
280 }294 }
281 295
282 async init(token: McpSessionInit, incoming?: IncomingTraceHeaders): Promise<void> {296 async init(token: McpSessionInit, incoming?: IncomingTraceHeaders): Promise<void> {
@@ -346,7 +360,7 @@ export class McpSessionDO extends DurableObject { · 63 unmodified lines
346 private handleRequestWithRequestScopedRuntimeEffect(request: Request) {360 private handleRequestWithRequestScopedRuntimeEffect(request: Request) {
347 const self = this;361 const self = this;
348 return Effect.gen(function* () {362 return Effect.gen(function* () {
349 const sessionMeta = yield* Effect.promise(() => self.loadSessionMeta());363 const sessionMeta = yield* self.loadSessionMetaEffect();
350 if (!sessionMeta) {364 if (!sessionMeta) {
351 return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect");365 return jsonRpcError(404, -32001, "Session timed out due to inactivity — please reconnect");
352 }366 }
@@ -355,7 +369,9 @@ export class McpSessionDO extends DurableObject { · 2 unmodified lines
355 self.lastActivityMs = Date.now();369 self.lastActivityMs = Date.now();
356 370
357 const dbHandle = makeRequestScopedDb();371 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 );
359 return yield* Effect.acquireUseRelease(375 return yield* Effect.acquireUseRelease(
360 self.createConnectedRuntimeEffect(sessionMeta, {376 self.createConnectedRuntimeEffect(sessionMeta, {
361 dbHandle,377 dbHandle,
@@ -364,21 +380,37 @@ export class McpSessionDO extends DurableObject { · 2 unmodified lines
364 ({ transport }) =>380 ({ transport }) =>
365 Effect.gen(function* () {381 Effect.gen(function* () {
366 const response = yield* Effect.promise(() => transport.handleRequest(request)).pipe(382 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 }),
368 );392 );
393 yield* Effect.annotateCurrentSpan({
394 "mcp.response.status_code": response.status,
395 });
369 if (request.method === "DELETE") {396 if (request.method === "DELETE") {
370 yield* Effect.promise(() => self.clearSessionState());397 yield* self.clearSessionStateEffect();
371 }398 }
372 return response;399 return response;
373 }),400 }),
374 ({ mcpServer, transport }) =>401 ({ mcpServer, transport }) =>
375 Effect.gen(function* () {402 Effect.gen(function* () {
376 yield* Effect.promise(() => transport.close().catch(() => undefined));403 yield* Effect.promise(() => transport.close().catch(() => undefined)).pipe(
377 yield* Effect.promise(() => mcpServer.close().catch(() => undefined));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 );
378 yield* cleanupDb;409 yield* cleanupDb;
379 }),410 }).pipe(Effect.withSpan("mcp.session.runtime.release")),
380 );411 );
381 }).pipe(412 }).pipe(
413 Effect.withSpan("mcp.session.request_scoped_runtime"),
382 Effect.catchAllCause((cause) =>414 Effect.catchAllCause((cause) =>
383 Effect.sync(() => {415 Effect.sync(() => {
384 console.error("[mcp-session] request-scoped handleRequest error:", cause);416 console.error("[mcp-session] request-scoped handleRequest error:", cause);
@@ -451,10 +483,23 @@ export class McpSessionDO extends DurableObject { · 66 unmodified lines
451 const self = this;483 const self = this;
452 return Effect.gen(function* () {484 return Effect.gen(function* () {
453 const response = yield* Effect.promise(() => transport.handleRequest(request)).pipe(485 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 }),
455 );495 );
496 yield* Effect.annotateCurrentSpan({
497 "mcp.response.status_code": response.status,
498 });
456 if (request.method === "DELETE") {499 if (request.method === "DELETE") {
457 yield* Effect.promise(() => self.cleanup());500 yield* Effect.promise(() => self.cleanup()).pipe(
501 Effect.withSpan("mcp.session.cleanup"),
502 );
458 }503 }
459 return response;504 return response;
460 }).pipe(505 }).pipe(
packages/core/execution/src/description.ts+13−2
@@ -14,8 +14,19 @@ export const buildExecuteDescription = (executor: Executor): Effect.Effect<strin · 13 unmodified lines
14 .list()14 .list()
15 .pipe(Effect.orDie, Effect.withSpan("executor.sources.list"));15 .pipe(Effect.orDie, Effect.withSpan("executor.sources.list"));
16 16
17 return formatDescription(sources);17 const description = yield* Effect.sync(() => formatDescription(sources)).pipe(
18 }).pipe(Effect.withSpan("buildExecuteDescription"));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"));
19 30
20const formatDescription = (sources: readonly Source[]): string => {31const formatDescription = (sources: readonly Source[]): string => {
21 const lines: string[] = [32 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
194 194
195 return searchTools(executor, args.query ?? "", limit, {195 return searchTools(executor, args.query ?? "", limit, {
196 namespace: args.namespace,196 namespace: args.namespace,
197 });197 }).pipe(
198 Effect.withSpan("mcp.tool.dispatch", {
199 attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
200 }),
201 );
198 }202 }
199 if (path === "executor.sources.list") {203 if (path === "executor.sources.list") {
200 if (args !== undefined && !isRecord(args)) {204 if (args !== undefined && !isRecord(args)) {
@@ -225,7 +229,11 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 24 unmodified lines
225 return listExecutorSources(executor, {229 return listExecutorSources(executor, {
226 query: isRecord(args) && typeof args.query === "string" ? args.query : undefined,230 query: isRecord(args) && typeof args.query === "string" ? args.query : undefined,
227 limit,231 limit,
228 });232 }).pipe(
233 Effect.withSpan("mcp.tool.dispatch", {
234 attributes: { "mcp.tool.name": path, "executor.tool.builtin": true },
235 }),
236 );
229 }237 }
230 if (path === "describe.tool") {238 if (path === "describe.tool") {
231 if (!isRecord(args)) {239 if (!isRecord(args)) {
@@ -248,7 +256,15 @@ const makeFullInvoker = (executor: Executor, invokeOptions: InvokeOptions): Sand · 16 unmodified lines
248 );256 );
249 }257 }
250 258
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 );
252 }268 }
253 return base.invoke({ path, args });269 return base.invoke({ path, args });
254 },270 },
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
288 }288 }
289 289
290 const all = yield* executor.tools.list().pipe(Effect.orDie);290 const all = yield* executor.tools.list().pipe(Effect.orDie);
291 return all291 const results = all
292 .filter((tool: Tool) => matchesNamespace(tool, options?.namespace))292 .filter((tool: Tool) => matchesNamespace(tool, options?.namespace))
293 .map((tool: Tool) => scoreToolMatch(tool, query))293 .map((tool: Tool) => scoreToolMatch(tool, query))
294 .filter((tool): tool is ToolDiscoveryResult => tool !== null)294 .filter((tool): tool is ToolDiscoveryResult => tool !== null)
295 .sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))295 .sort((left, right) => right.score - left.score || left.path.localeCompare(right.path))
296 .slice(0, limit);296 .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;
297});303});
298 304
299/** What `tools.executor.sources.list()` calls inside the sandbox. */305/** 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
333 }) satisfies ExecutorSourceListItem,339 }) satisfies ExecutorSourceListItem,
334 );340 );
335 341
336 return withCounts342 const results = withCounts
337 .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id))343 .sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id))
338 .slice(0, limit);344 .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;
339});351});
340 352
341/** What `tools.describe.tool()` calls inside the sandbox. */353/** 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
892 for (const { source, pluginId } of staticSources.values()) {892 for (const { source, pluginId } of staticSources.values()) {
893 staticList.push(staticDeclToSource(source, pluginId));893 staticList.push(staticDeclToSource(source, pluginId));
894 }894 }
895 return [...staticList, ...dynamic.map(rowToSource)];895 const merged = [...staticList, ...dynamic.map(rowToSource)];
896 });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"));
897 902
898 // Bulk-resolve annotations across a set of dynamic tool rows by903 // Bulk-resolve annotations across a set of dynamic tool rows by
899 // grouping them under their owning plugin's resolveAnnotations904 // grouping them under their owning plugin's resolveAnnotations
@@ -936,7 +941,9 @@ export const createExecutor = < · 36 unmodified lines
936 const listTools = (filter?: ToolListFilter) =>941 const listTools = (filter?: ToolListFilter) =>
937 Effect.gen(function* () {942 Effect.gen(function* () {
938 const dynamic = yield* core.findMany({ model: "tool" });943 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 );
940 947
941 const out: Tool[] = [];948 const out: Tool[] = [];
942 // Static tools — annotations from the declaration, not a resolver.949 // Static tools — annotations from the declaration, not a resolver.
@@ -946,9 +953,14 @@ export const createExecutor = < · 3 unmodified lines
946 for (const row of dynamic) {953 for (const row of dynamic) {
947 out.push(rowToTool(row, annotations.get(row.id)));954 out.push(rowToTool(row, annotations.get(row.id)));
948 }955 }
949 if (!filter) return out;956 const result = filter ? out.filter((t) => toolMatchesFilter(t, filter)) : out;
950 return out.filter((t) => toolMatchesFilter(t, filter));957 yield* Effect.annotateCurrentSpan({
951 });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"));
952 964
953 // Load all definitions for a single source as a plain map.965 // Load all definitions for a single source as a plain map.
954 const loadDefinitionsForSource = (sourceId: string) =>966 const loadDefinitionsForSource = (sourceId: string) =>
@@ -975,7 +987,9 @@ export const createExecutor = < · 20 unmodified lines
975 }) =>987 }) =>
976 Effect.gen(function* () {988 Effect.gen(function* () {
977 const defs: Record<string, unknown> = opts.sourceId989 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 )
979 : {};993 : {};
980 994
981 const attachDefs = (schema: unknown): unknown => {995 const attachDefs = (schema: unknown): unknown => {
@@ -988,11 +1002,18 @@ export const createExecutor = < · 6 unmodified lines
988 const outputSchema = attachDefs(opts.rawOutput);1002 const outputSchema = attachDefs(opts.rawOutput);
989 1003
990 const defsMap = new Map<string, unknown>(Object.entries(defs));1004 const defsMap = new Map<string, unknown>(Object.entries(defs));
991 const preview = buildToolTypeScriptPreview({1005 const preview = yield* Effect.sync(() =>
992 inputSchema,1006 buildToolTypeScriptPreview({ inputSchema, outputSchema, defs: defsMap }),
993 outputSchema,1007 ).pipe(
994 defs: defsMap,1008 Effect.withSpan("schema.compile.preview", {
995 });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 );
996 1017
997 return new ToolSchema({1018 return new ToolSchema({
998 id: ToolId.make(opts.toolId),1019 id: ToolId.make(opts.toolId),
@@ -1012,6 +1033,11 @@ export const createExecutor = < · 13 unmodified lines
1012 // no `$defs` attach; just wrap the declared schemas.1033 // no `$defs` attach; just wrap the declared schemas.
1013 const staticEntry = staticTools.get(toolId);1034 const staticEntry = staticTools.get(toolId);
1014 if (staticEntry) {1035 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 });
1015 return yield* buildToolSchemaView({1041 return yield* buildToolSchemaView({
1016 toolId,1042 toolId,
1017 name: staticEntry.tool.name,1043 name: staticEntry.tool.name,
@@ -1021,11 +1047,18 @@ export const createExecutor = < · 3 unmodified lines
1021 rawOutput: staticEntry.tool.outputSchema,1047 rawOutput: staticEntry.tool.outputSchema,
1022 });1048 });
1023 }1049 }
1024 const row = yield* core.findOne({1050 const row = yield* core
1025 model: "tool",1051 .findOne({
1026 where: [{ field: "id", value: toolId }],1052 model: "tool",
1027 });1053 where: [{ field: "id", value: toolId }],
1054 })
1055 .pipe(Effect.withSpan("executor.tool.resolve"));
1028 if (!row) return null;1056 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 });
1029 return yield* buildToolSchemaView({1062 return yield* buildToolSchemaView({
1030 toolId,1063 toolId,
1031 name: row.name,1064 name: row.name,
@@ -1034,7 +1067,11 @@ export const createExecutor = < · 2 unmodified lines
1034 rawInput: decodeJsonColumn(row.input_schema),1067 rawInput: decodeJsonColumn(row.input_schema),
1035 rawOutput: decodeJsonColumn(row.output_schema),1068 rawOutput: decodeJsonColumn(row.output_schema),
1036 });1069 });
1037 });1070 }).pipe(
1071 Effect.withSpan("executor.tool.schema", {
1072 attributes: { "mcp.tool.name": toolId },
1073 }),
1074 );
1038 1075
1039 // Bulk definitions accessor — every source's $defs, grouped by1076 // Bulk definitions accessor — every source's $defs, grouped by
1040 // source id. One query against the definition table, plus an1077 // source id. One query against the definition table, plus an
@@ -1121,31 +1158,44 @@ export const createExecutor = < · 80 unmodified lines
1121 // Static path — O(1) map lookup, no DB hit.1158 // Static path — O(1) map lookup, no DB hit.
1122 const staticEntry = staticTools.get(toolId);1159 const staticEntry = staticTools.get(toolId);
1123 if (staticEntry) {1160 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 });
1124 yield* enforceApproval(1167 yield* enforceApproval(
1125 staticEntry.tool.annotations,1168 staticEntry.tool.annotations,
1126 toolId,1169 toolId,
1127 args,1170 args,
1128 options,1171 options,
1129 );1172 ).pipe(Effect.withSpan("executor.tool.enforce_approval"));
1130 return yield* wrapInvocationError(1173 return yield* wrapInvocationError(
1131 staticEntry.tool.handler({1174 staticEntry.tool.handler({
1132 ctx: staticEntry.ctx,1175 ctx: staticEntry.ctx,
1133 args,1176 args,
1134 elicit: buildElicit(toolId, args, options),1177 elicit: buildElicit(toolId, args, options),
1135 }),1178 }),
1136 );1179 ).pipe(Effect.withSpan("executor.tool.handler"));
1137 }1180 }
1138 1181
1139 // Dynamic path — DB lookup + delegate to owning plugin.1182 // Dynamic path — DB lookup + delegate to owning plugin.
1140 const row = yield* core.findOne({1183 const row = yield* core
1141 model: "tool",1184 .findOne({
1142 where: [{ field: "id", value: toolId }],1185 model: "tool",
1143 });1186 where: [{ field: "id", value: toolId }],
1187 })
1188 .pipe(Effect.withSpan("executor.tool.resolve"));
1144 if (!row) {1189 if (!row) {
1145 return yield* new ToolNotFoundError({1190 return yield* new ToolNotFoundError({
1146 toolId: ToolId.make(toolId),1191 toolId: ToolId.make(toolId),
1147 });1192 });
1148 }1193 }
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 });
1149 const runtime = runtimes.get(row.plugin_id);1199 const runtime = runtimes.get(row.plugin_id);
1150 if (!runtime) {1200 if (!runtime) {
1151 return yield* new PluginNotLoadedError({1201 return yield* new PluginNotLoadedError({
@@ -1167,14 +1217,18 @@ export const createExecutor = < · 15 unmodified lines
1167 // around a single storage read.1217 // around a single storage read.
1168 let annotations: ToolAnnotations | undefined;1218 let annotations: ToolAnnotations | undefined;
1169 if (runtime.plugin.resolveAnnotations) {1219 if (runtime.plugin.resolveAnnotations) {
1170 const map = yield* runtime.plugin.resolveAnnotations({1220 const map = yield* runtime.plugin
1171 ctx: runtime.ctx,1221 .resolveAnnotations({
1172 sourceId: row.source_id,1222 ctx: runtime.ctx,
1173 toolRows: [row],1223 sourceId: row.source_id,
1174 });1224 toolRows: [row],
1225 })
1226 .pipe(Effect.withSpan("executor.tool.resolve_annotations"));
1175 annotations = map[toolId];1227 annotations = map[toolId];
1176 }1228 }
1177 yield* enforceApproval(annotations, toolId, args, options);1229 yield* enforceApproval(annotations, toolId, args, options).pipe(
1230 Effect.withSpan("executor.tool.enforce_approval"),
1231 );
1178 1232
1179 return yield* wrapInvocationError(1233 return yield* wrapInvocationError(
1180 runtime.plugin.invokeTool({1234 runtime.plugin.invokeTool({
@@ -1183,8 +1237,14 @@ export const createExecutor = < · 2 unmodified lines
1183 args,1237 args,
1184 elicit: buildElicit(toolId, args, options),1238 elicit: buildElicit(toolId, args, options),
1185 }),1239 }),
1186 );1240 ).pipe(Effect.withSpan("executor.tool.handler"));
1187 });1241 }).pipe(
1242 Effect.withSpan("executor.tool.invoke", {
1243 attributes: {
1244 "mcp.tool.name": toolId,
1245 },
1246 }),
1247 );
1188 1248
1189 const removeSource = (sourceId: string) =>1249 const removeSource = (sourceId: string) =>
1190 Effect.gen(function* () {1250 Effect.gen(function* () {
packages/core/sdk/src/schema-types.ts+8−5
@@ -364,16 +364,18 @@ export const schemaToTypeScriptPreviewWithDefs = ( · 363 unmodified lines
364 };364 };
365};365};
366 366
367export type ToolTypeScriptPreview = {
368 inputTypeScript?: string;
369 outputTypeScript?: string;
370 typeScriptDefinitions?: Record<string, string>;
371};
372
367export const buildToolTypeScriptPreview = (input: {373export const buildToolTypeScriptPreview = (input: {
368 inputSchema?: unknown;374 inputSchema?: unknown;
369 outputSchema?: unknown;375 outputSchema?: unknown;
370 defs: ReadonlyMap<string, unknown>;376 defs: ReadonlyMap<string, unknown>;
371 options?: TypeScriptRenderOptions;377 options?: TypeScriptRenderOptions;
372}): {378}): ToolTypeScriptPreview => {
373 inputTypeScript?: string;
374 outputTypeScript?: string;
375 typeScriptDefinitions?: Record<string, string>;
376} => {
377 const inputPreview =379 const inputPreview =
378 input.inputSchema !== undefined380 input.inputSchema !== undefined
379 ? schemaToTypeScriptPreviewWithDefs(input.inputSchema, input.defs, input.options)381 ? schemaToTypeScriptPreviewWithDefs(input.inputSchema, input.defs, input.options)
@@ -396,3 +398,4 @@ export const buildToolTypeScriptPreview = (input: { · 16 unmodified lines
396 : {}),398 : {}),
397 };399 };
398};400};
401
packages/core/storage-core/src/factory.ts+82−14
@@ -638,7 +638,14 @@ export const createAdapter = ( · 637 unmodified lines
638 data.select,638 data.select,
639 );639 );
640 return out as unknown as R;640 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 ),
642 649
643 createMany: <T extends Record<string, unknown>, R = T>(data: {650 createMany: <T extends Record<string, unknown>, R = T>(data: {
644 model: string;651 model: string;
@@ -678,7 +685,15 @@ export const createAdapter = ( · 33 unmodified lines
678 );685 );
679 }686 }
680 return out as unknown as readonly R[];687 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 ),
682 697
683 findOne: <T>(data: {698 findOne: <T>(data: {
684 model: string;699 model: string;
@@ -698,7 +713,14 @@ export const createAdapter = ( · 13 unmodified lines
698 const out = yield* maybeTransformOutput(data.model, res, data.select);713 const out = yield* maybeTransformOutput(data.model, res, data.select);
699 const merged = yield* attachJoinedRows(out, res, data.join);714 const merged = yield* attachJoinedRows(out, res, data.join);
700 return merged as unknown as T | null;715 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 ),
702 724
703 findMany: <T>(data: {725 findMany: <T>(data: {
704 model: string;726 model: string;
@@ -734,7 +756,14 @@ export const createAdapter = ( · 29 unmodified lines
734 out.push(merged);756 out.push(merged);
735 }757 }
736 return out as readonly T[];758 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 ),
738 767
739 count: (data: { model: string; where?: Where[] | undefined }) =>768 count: (data: { model: string; where?: Where[] | undefined }) =>
740 Effect.gen(function* () {769 Effect.gen(function* () {
@@ -743,7 +772,14 @@ export const createAdapter = ( · 2 unmodified lines
743 model: getModelName(data.model),772 model: getModelName(data.model),
744 where,773 where,
745 });774 });
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 ),
747 783
748 update: <T>(data: {784 update: <T>(data: {
749 model: string;785 model: string;
@@ -765,7 +801,14 @@ export const createAdapter = ( · 15 unmodified lines
765 });801 });
766 const out = yield* maybeTransformOutput(data.model, res);802 const out = yield* maybeTransformOutput(data.model, res);
767 return out as unknown as T | null;803 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 ),
769 812
770 updateMany: (data: {813 updateMany: (data: {
771 model: string;814 model: string;
@@ -785,7 +828,14 @@ export const createAdapter = ( · 13 unmodified lines
785 where,828 where,
786 update,829 update,
787 });830 });
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 ),
789 839
790 delete: (data: { model: string; where: Where[] }) =>840 delete: (data: { model: string; where: Where[] }) =>
791 Effect.gen(function* () {841 Effect.gen(function* () {
@@ -794,7 +844,14 @@ export const createAdapter = ( · 2 unmodified lines
794 model: getModelName(data.model),844 model: getModelName(data.model),
795 where,845 where,
796 });846 });
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 ),
798 855
799 deleteMany: (data: { model: string; where: Where[] }) =>856 deleteMany: (data: { model: string; where: Where[] }) =>
800 Effect.gen(function* () {857 Effect.gen(function* () {
@@ -803,17 +860,28 @@ export const createAdapter = ( · 2 unmodified lines
803 model: getModelName(data.model),860 model: getModelName(data.model),
804 where,861 where,
805 });862 });
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 ),
807 871
808 transaction: <R, E>(872 transaction: <R, E>(
809 callback: (trx: DBTransactionAdapter) => Effect.Effect<R, E>,873 callback: (trx: DBTransactionAdapter) => Effect.Effect<R, E>,
810 ) => {874 ) => {
811 const txFn = config.transaction;875 const txFn = config.transaction;
812 if (!txFn) {876 const ran = !txFn ? callback(self) : txFn(callback);
813 // No real transaction support — just run the callback against self.877 return ran.pipe(
814 return callback(self);878 Effect.withSpan("executor.storage.transaction", {
815 }879 attributes: {
816 return txFn(callback);880 "executor.storage.adapter_id": config.adapterId,
881 "executor.storage.transaction.native": txFn !== undefined,
882 },
883 }),
884 );
817 },885 },
818 886
819 // Forward the backend's createSchema verbatim. Upstream better-auth887 // 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
361 return t;361 return t;
362 };362 };
363 363
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
364 const custom: CustomAdapter = {370 const custom: CustomAdapter = {
365 create: ({ model, data }) =>371 create: ({ model, data }) =>
366 Effect.gen(function* () {372 Effect.gen(function* () {
@@ -375,7 +381,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 8 unmodified lines
375 model,381 model,
376 );382 );
377 return row as never;383 return row as never;
378 }),384 }).pipe(
385 Effect.withSpan("executor.storage.backend.create", {
386 attributes: backendAttrs(model),
387 }),
388 ),
379 389
380 // Real multi-row INSERT in fixed-size chunks. One statement per390 // Real multi-row INSERT in fixed-size chunks. One statement per
381 // chunk, not one per row — per-row loops blow the Hyperdrive391 // chunk, not one per row — per-row loops blow the Hyperdrive
@@ -401,7 +411,14 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 19 unmodified lines
401 for (const row of rows) all.push(row);411 for (const row of rows) all.push(row);
402 }412 }
403 return all as never;413 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 ),
405 422
406 findOne: ({ model, where, join }) =>423 findOne: ({ model, where, join }) =>
407 Effect.gen(function* () {424 Effect.gen(function* () {
@@ -430,7 +447,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 22 unmodified lines
430 model,447 model,
431 )) as Record<string, unknown>[];448 )) as Record<string, unknown>[];
432 return (rows[0] ?? null) as never;449 return (rows[0] ?? null) as never;
433 }),450 }).pipe(
451 Effect.withSpan("executor.storage.backend.find_one", {
452 attributes: backendAttrs(model),
453 }),
454 ),
434 455
435 findMany: ({ model, where, limit, sortBy, offset, join }) =>456 findMany: ({ model, where, limit, sortBy, offset, join }) =>
436 Effect.gen(function* () {457 Effect.gen(function* () {
@@ -477,7 +498,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 40 unmodified lines
477 model,498 model,
478 )) as Record<string, unknown>[];499 )) as Record<string, unknown>[];
479 return rows as never[];500 return rows as never[];
480 }),501 }).pipe(
502 Effect.withSpan("executor.storage.backend.find_many", {
503 attributes: backendAttrs(model),
504 }),
505 ),
481 506
482 count: ({ model, where }) =>507 count: ({ model, where }) =>
483 Effect.gen(function* () {508 Effect.gen(function* () {
@@ -492,7 +517,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 8 unmodified lines
492 )) as { c: number | string | bigint }[];517 )) as { c: number | string | bigint }[];
493 const raw = rows[0]?.c ?? 0;518 const raw = rows[0]?.c ?? 0;
494 return typeof raw === "number" ? raw : Number(raw);519 return typeof raw === "number" ? raw : Number(raw);
495 }),520 }).pipe(
521 Effect.withSpan("executor.storage.backend.count", {
522 attributes: backendAttrs(model),
523 }),
524 ),
496 525
497 update: ({ model, where, update }) =>526 update: ({ model, where, update }) =>
498 Effect.gen(function* () {527 Effect.gen(function* () {
@@ -529,7 +558,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 30 unmodified lines
529 model,558 model,
530 )) as Record<string, unknown>[];559 )) as Record<string, unknown>[];
531 return (reread[0] ?? null) as never;560 return (reread[0] ?? null) as never;
532 }),561 }).pipe(
562 Effect.withSpan("executor.storage.backend.update", {
563 attributes: backendAttrs(model),
564 }),
565 ),
533 566
534 updateMany: ({ model, where, update }) =>567 updateMany: ({ model, where, update }) =>
535 Effect.gen(function* () {568 Effect.gen(function* () {
@@ -554,7 +587,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 18 unmodified lines
554 model,587 model,
555 );588 );
556 return n;589 return n;
557 }),590 }).pipe(
591 Effect.withSpan("executor.storage.backend.update_many", {
592 attributes: backendAttrs(model),
593 }),
594 ),
558 595
559 delete: ({ model, where }) =>596 delete: ({ model, where }) =>
560 Effect.gen(function* () {597 Effect.gen(function* () {
@@ -575,7 +612,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 14 unmodified lines
575 () => Promise.resolve(db.delete(table).where(eq(table.id, first.id))),612 () => Promise.resolve(db.delete(table).where(eq(table.id, first.id))),
576 model,613 model,
577 );614 );
578 }),615 }).pipe(
616 Effect.withSpan("executor.storage.backend.delete", {
617 attributes: backendAttrs(model),
618 }),
619 ),
579 620
580 deleteMany: ({ model, where }) =>621 deleteMany: ({ model, where }) =>
581 Effect.gen(function* () {622 Effect.gen(function* () {
@@ -598,7 +639,11 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 16 unmodified lines
598 model,639 model,
599 );640 );
600 return n;641 return n;
601 }),642 }).pipe(
643 Effect.withSpan("executor.storage.backend.delete_many", {
644 attributes: backendAttrs(model),
645 }),
646 ),
602 };647 };
603 648
604 // Transaction strategy differs by dialect:649 // Transaction strategy differs by dialect:
@@ -659,6 +704,13 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 54 unmodified lines
659 if (e instanceof TxFailure) return e.inner;704 if (e instanceof TxFailure) return e.inner;
660 return classifyError("pg transaction", undefined, e);705 return classifyError("pg transaction", undefined, e);
661 }),706 }),
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 }),
662 ) as Effect.Effect<R, E | StorageFailure>;714 ) as Effect.Effect<R, E | StorageFailure>;
663 }715 }
664 716
@@ -711,7 +763,15 @@ export const drizzleAdapter = (options: DrizzleAdapterOptions): DBAdapter => { · 46 unmodified lines
711 });763 });
712 }764 }
713 return result;765 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 );
715 }775 }
716 : undefined;776 : undefined;
717 777
packages/hosts/mcp/src/server.ts+81−40
@@ -195,7 +195,11 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 194 unmodified lines
195): Effect.Effect<McpServer> =>195): Effect.Effect<McpServer> =>
196 Effect.gen(function* () {196 Effect.gen(function* () {
197 const engine = "engine" in config ? config.engine : createExecutionEngine(config);197 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 ));
199 203
200 // Captured at construction time. SDK callbacks fire later (often204 // Captured at construction time. SDK callbacks fire later (often
201 // deferred past the outer Effect's await), so we use the runtime to205 // 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
211 return parent ? Effect.withParentSpan(effect, parent) : effect;215 return parent ? Effect.withParentSpan(effect, parent) : effect;
212 };216 };
213 217
214 const server = new McpServer(218 const server = yield* Effect.sync(
215 { name: "executor", version: "1.0.0" },219 () =>
216 { capabilities: { tools: {} }, jsonSchemaValidator: new CfWorkerJsonSchemaValidator() },220 new McpServer(
217 );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"));
218 228
219 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>229 const executeCode = (code: string): Effect.Effect<McpToolResult, E> =>
220 Effect.gen(function* () {230 Effect.gen(function* () {
@@ -228,7 +238,14 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines
228 return outcome.status === "completed"238 return outcome.status === "completed"
229 ? toMcpResult(formatExecuteResult(outcome.result))239 ? toMcpResult(formatExecuteResult(outcome.result))
230 : toMcpPausedResult(formatPausedExecution(outcome.execution));240 : 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 );
232 249
233 const resumeExecution = (250 const resumeExecution = (
234 executionId: string,251 executionId: string,
@@ -239,48 +256,70 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 4 unmodified lines
239 const outcome = yield* engine.resume(executionId, { action, content });256 const outcome = yield* engine.resume(executionId, { action, content });
240 if (!outcome) {257 if (!outcome) {
241 return {258 return {
242 content: [{ type: "text", text: `No paused execution: ${executionId}` }],259 content: [
260 { type: "text" as const, text: `No paused execution: ${executionId}` },
261 ],
243 isError: true,262 isError: true,
244 };263 } satisfies McpToolResult;
245 }264 }
246 return outcome.status === "completed"265 return outcome.status === "completed"
247 ? toMcpResult(formatExecuteResult(outcome.result))266 ? toMcpResult(formatExecuteResult(outcome.result))
248 : toMcpPausedResult(formatPausedExecution(outcome.execution));267 : 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 );
250 277
251 // --- tools ---278 // --- tools ---
252 279
253 const executeTool = server.registerTool(280 const executeTool = yield* Effect.sync(() =>
254 "execute",281 server.registerTool(
255 {282 "execute",
256 description,283 {
257 inputSchema: { code: z.string().trim().min(1) },284 description,
258 },285 inputSchema: { code: z.string().trim().min(1) },
259 ({ code }) => Runtime.runPromise(runtime)(anchor(executeCode(code))),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 }),
260 );293 );
261 294
262 const resumeTool = server.registerTool(295 const resumeTool = yield* Effect.sync(() =>
263 "resume",296 server.registerTool(
264 {297 "resume",
265 description: [298 {
266 "Resume a paused execution using the executionId returned by execute.",299 description: [
267 "Never call this without user approval unless they explicitly state otherwise.",300 "Resume a paused execution using the executionId returned by execute.",
268 ].join("\n"),301 "Never call this without user approval unless they explicitly state otherwise.",
269 inputSchema: {302 ].join("\n"),
270 executionId: z.string().describe("The execution ID from the paused result"),303 inputSchema: {
271 action: z304 executionId: z.string().describe("The execution ID from the paused result"),
272 .enum(["accept", "decline", "cancel"])305 action: z
273 .describe("How to respond to the interaction"),306 .enum(["accept", "decline", "cancel"])
274 content: z307 .describe("How to respond to the interaction"),
275 .string()308 content: z
276 .describe("Optional JSON-encoded response content for form elicitations")309 .string()
277 .default("{}"),310 .describe("Optional JSON-encoded response content for form elicitations")
311 .default("{}"),
312 },
278 },313 },
279 },314 ({ executionId, action, content: rawContent }) =>
280 ({ executionId, action, content: rawContent }) =>315 Runtime.runPromise(runtime)(
281 Runtime.runPromise(runtime)(316 anchor(resumeExecution(executionId, action, parseJsonContent(rawContent))),
282 anchor(resumeExecution(executionId, action, parseJsonContent(rawContent))),317 ),
283 ),318 ),
319 ).pipe(
320 Effect.withSpan("mcp.host.register_tool", {
321 attributes: { "mcp.tool.name": "resume" },
322 }),
284 );323 );
285 324
286 // --- capability-based tool visibility ---325 // --- capability-based tool visibility ---
@@ -294,8 +333,10 @@ export const createExecutorMcpServer = <E extends Cause.YieldableError>( · 7 unmodified lines
294 }333 }
295 };334 };
296 335
297 syncToolAvailability();336 yield* Effect.sync(() => {
298 server.server.oninitialized = syncToolAvailability;337 syncToolAvailability();
338 server.server.oninitialized = syncToolAvailability;
339 }).pipe(Effect.withSpan("mcp.host.sync_tool_availability"));
299 340
300 return server;341 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
15export const resolveHeaders = (15export const resolveHeaders = (
16 headers: Record<string, HeaderValue>,16 headers: Record<string, HeaderValue>,
17 secrets: { readonly get: (id: string) => Effect.Effect<string | null, unknown> },17 secrets: { readonly get: (id: string) => Effect.Effect<string | null, unknown> },
18): Effect.Effect<Record<string, string>> =>18): Effect.Effect<Record<string, string>> => {
19 Effect.gen(function* () {19 const entries = Object.entries(headers);
20 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* () {
21 // Resolve secret-backed headers in parallel. Missing / failing25 // Resolve secret-backed headers in parallel. Missing / failing
22 // lookups drop the header rather than fail the invocation, same26 // lookups drop the header rather than fail the invocation, same
23 // as the serial version.27 // as the serial version.
@@ -48,7 +52,15 @@ export const resolveHeaders = ( · 24 unmodified lines
48 if (value !== null) resolved[name] = value;52 if (value !== null) resolved[name] = value;
49 }53 }
50 return resolved;54 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};
52 64
53// ---------------------------------------------------------------------------65// ---------------------------------------------------------------------------
54// Response helpers66// Response helpers
@@ -74,6 +86,15 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 19 unmodified lines
74) {86) {
75 const client = yield* HttpClient.HttpClient;87 const client = yield* HttpClient.HttpClient;
76 88
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
77 // Build the GraphQL request body98 // Build the GraphQL request body
78 const variables: Record<string, unknown> = {};99 const variables: Record<string, unknown> = {};
79 for (const varName of operation.variableNames) {100 for (const varName of operation.variableNames) {
@@ -121,6 +142,12 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 41 unmodified lines
121 const gqlBody = body as { data?: unknown; errors?: unknown[] } | null;142 const gqlBody = body as { data?: unknown; errors?: unknown[] } | null;
122 const hasErrors = Array.isArray(gqlBody?.errors) && gqlBody.errors.length > 0;143 const hasErrors = Array.isArray(gqlBody?.errors) && gqlBody.errors.length > 0;
123 144
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
124 return new InvocationResult({151 return new InvocationResult({
125 status,152 status,
126 data: gqlBody?.data ?? null,153 data: gqlBody?.data ?? null,
@@ -150,4 +177,11 @@ export const invokeWithLayer = ( · 23 unmodified lines
150 cause: err,177 cause: err,
151 }),178 }),
152 ),179 ),
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 }),
153 );187 );
packages/plugins/mcp/src/sdk/connection.ts+5−1
@@ -79,7 +79,11 @@ const connectClient = (input: { · 78 unmodified lines
79 cause instanceof Error ? cause.message : String(cause)79 cause instanceof Error ? cause.message : String(cause)
80 }`,80 }`,
81 }),81 }),
82 });82 }).pipe(
83 Effect.withSpan("plugin.mcp.connection.handshake", {
84 attributes: { "plugin.mcp.transport": input.transport },
85 }),
86 );
83 87
84 return connectionFromClient(client);88 return connectionFromClient(client);
85 });89 });
packages/plugins/mcp/src/sdk/invoke.ts+40−6
@@ -136,7 +136,11 @@ const useConnection = ( · 135 unmodified lines
136 cause instanceof Error ? cause.message : String(cause)136 cause instanceof Error ? cause.message : String(cause)
137 }`,137 }`,
138 }),138 }),
139 });139 }).pipe(
140 Effect.withSpan("plugin.mcp.client.call_tool", {
141 attributes: { "mcp.tool.name": toolName },
142 }),
143 );
140 });144 });
141 145
142// ---------------------------------------------------------------------------146// ---------------------------------------------------------------------------
@@ -163,8 +167,12 @@ export interface InvokeMcpToolInput { · 20 unmodified lines
163 167
164export const invokeMcpTool = (168export const invokeMcpTool = (
165 input: InvokeMcpToolInput,169 input: InvokeMcpToolInput,
166): Effect.Effect<unknown, McpConnectionError | McpInvocationError> =>170): Effect.Effect<unknown, McpConnectionError | McpInvocationError> => {
167 Effect.gen(function* () {171 const transport: string =
172 input.sourceData.transport === "stdio"
173 ? "stdio"
174 : (input.sourceData.remoteTransport ?? "auto");
175 return Effect.gen(function* () {
168 const cacheKey = connectionCacheKey(input.sourceData);176 const cacheKey = connectionCacheKey(input.sourceData);
169 const args = asRecord(input.args);177 const args = asRecord(input.args);
170 178
@@ -173,7 +181,15 @@ export const invokeMcpTool = ( · 2 unmodified lines
173 const connector = input.resolveConnector();181 const connector = input.resolveConnector();
174 input.pendingConnectors.set(cacheKey, connector);182 input.pendingConnectors.set(cacheKey, connector);
175 183
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 );
177 193
178 return yield* useConnection(194 return yield* useConnection(
179 firstConnection,195 firstConnection,
@@ -194,7 +210,25 @@ export const invokeMcpTool = ( · 14 unmodified lines
194 args,210 args,
195 input.elicit,211 input.elicit,
196 );212 );
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 ),
198 ),222 ),
199 );223 );
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
445 Effect.catchAll(() =>445 Effect.catchAll(() =>
446 Effect.succeed({ ok: false as const, manifest: null }),446 Effect.succeed({ ok: false as const, manifest: null }),
447 ),447 ),
448 Effect.withSpan("mcp.plugin.discover_tools"),
448 );449 );
449 450
450 if (result.ok && result.manifest) {451 if (result.ok && result.manifest) {
@@ -465,6 +466,7 @@ export const mcpPlugin = definePlugin( · 14 unmodified lines
465 }).pipe(466 }).pipe(
466 Effect.map(() => true),467 Effect.map(() => true),
467 Effect.catchAll(() => Effect.succeed(false)),468 Effect.catchAll(() => Effect.succeed(false)),
469 Effect.withSpan("mcp.plugin.probe_oauth"),
468 );470 );
469 471
470 if (hasOAuth) {472 if (hasOAuth) {
@@ -483,7 +485,11 @@ export const mcpPlugin = definePlugin( · 12 unmodified lines
483 "Could not connect to MCP endpoint and no OAuth was detected",485 "Could not connect to MCP endpoint and no OAuth was detected",
484 ),486 ),
485 );487 );
486 });488 }).pipe(
489 Effect.withSpan("mcp.plugin.probe_endpoint", {
490 attributes: { "mcp.endpoint": endpoint },
491 }),
492 );
487 493
488 const configFile = options?.configFile;494 const configFile = options?.configFile;
489 495
@@ -493,7 +499,14 @@ export const mcpPlugin = definePlugin( · 3 unmodified lines
493 const sd = toStoredSourceData(config);499 const sd = toStoredSourceData(config);
494 500
495 // Resolve auth (may fail if stdio gate is off)501 // 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 );
497 510
498 const connector = createMcpConnector(ci);511 const connector = createMcpConnector(ci);
499 // Try discovery. If it fails (auth, network, bad spec), we still512 // Try discovery. If it fails (auth, network, bad spec), we still
@@ -505,6 +518,9 @@ export const mcpPlugin = definePlugin( · 5 unmodified lines
505 mcpDiscoveryError(`MCP discovery failed: ${err.message}`),518 mcpDiscoveryError(`MCP discovery failed: ${err.message}`),
506 ),519 ),
507 Effect.either,520 Effect.either,
521 Effect.withSpan("mcp.plugin.discover_tools", {
522 attributes: { "mcp.source.namespace": namespace },
523 }),
508 );524 );
509 const manifest =525 const manifest =
510 discovery._tag === "Right"526 discovery._tag === "Right"
@@ -513,73 +529,101 @@ export const mcpPlugin = definePlugin( · 2 unmodified lines
513 529
514 const sourceName = manifest.server?.name ?? config.name ?? namespace;530 const sourceName = manifest.server?.name ?? config.name ?? namespace;
515 531
516 yield* ctx.transaction(532 yield* ctx
517 Effect.gen(function* () {533 .transaction(
518 // Remove stale rows for this namespace (plugin-owned)534 Effect.gen(function* () {
519 yield* ctx.storage.removeBindingsByNamespace(namespace);535 // Remove stale rows for this namespace (plugin-owned)
520 yield* ctx.storage.removeSource(namespace);536 yield* ctx.storage.removeBindingsByNamespace(namespace);
521 537 yield* ctx.storage.removeSource(namespace);
522 yield* ctx.storage.putSource({538
523 namespace,539 yield* ctx.storage.putSource({
524 name: sourceName,540 namespace,
525 config: sd,541 name: sourceName,
526 });542 config: sd,
527 543 });
528 yield* ctx.storage.putBindings(544
529 namespace,545 yield* ctx.storage.putBindings(
530 manifest.tools.map((e) => ({546 namespace,
531 toolId: `${namespace}.${e.toolId}`,547 manifest.tools.map((e) => ({
532 binding: toBinding(e),548 toolId: `${namespace}.${e.toolId}`,
533 })),549 binding: toBinding(e),
534 );550 })),
535 551 );
536 yield* ctx.core.sources.register({552
537 id: namespace,553 yield* ctx.core.sources.register({
538 kind: "mcp",554 id: namespace,
539 name: sourceName,555 kind: "mcp",
540 url: sd.transport === "remote" ? sd.endpoint : undefined,556 name: sourceName,
541 canRemove: true,557 url: sd.transport === "remote" ? sd.endpoint : undefined,
542 canRefresh: true,558 canRemove: true,
543 canEdit: sd.transport === "remote",559 canRefresh: true,
544 tools: manifest.tools.map((e) => ({560 canEdit: sd.transport === "remote",
545 name: e.toolId,561 tools: manifest.tools.map((e) => ({
546 description: e.description ?? `MCP tool: ${e.toolName}`,562 name: e.toolId,
547 inputSchema: e.inputSchema,563 description: e.description ?? `MCP tool: ${e.toolName}`,
548 outputSchema: e.outputSchema,564 inputSchema: e.inputSchema,
549 })),565 outputSchema: e.outputSchema,
550 });566 })),
551 }),567 });
552 );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 );
553 578
554 if (configFile) {579 if (configFile) {
555 yield* configFile.upsertSource(580 yield* configFile
556 toMcpConfigEntry(namespace, sourceName, config),581 .upsertSource(toMcpConfigEntry(namespace, sourceName, config))
557 );582 .pipe(Effect.withSpan("mcp.plugin.config_file.upsert"));
558 }583 }
559 584
560 if (discovery._tag === "Left") {585 if (discovery._tag === "Left") {
561 return yield* Effect.fail(discovery.left);586 return yield* Effect.fail(discovery.left);
562 }587 }
563 return { toolCount: manifest.tools.length, namespace };588 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 );
565 597
566 const removeSource = (namespace: string) =>598 const removeSource = (namespace: string) =>
567 Effect.gen(function* () {599 Effect.gen(function* () {
568 yield* ctx.transaction(600 yield* ctx
569 Effect.gen(function* () {601 .transaction(
570 yield* ctx.storage.removeBindingsByNamespace(namespace);602 Effect.gen(function* () {
571 yield* ctx.storage.removeSource(namespace);603 yield* ctx.storage.removeBindingsByNamespace(namespace);
572 yield* ctx.core.sources.unregister(namespace);604 yield* ctx.storage.removeSource(namespace);
573 }),605 yield* ctx.core.sources.unregister(namespace);
574 );606 }),
607 )
608 .pipe(Effect.withSpan("mcp.plugin.persist_remove"));
575 if (configFile) {609 if (configFile) {
576 yield* configFile.removeSource(namespace);610 yield* configFile
611 .removeSource(namespace)
612 .pipe(Effect.withSpan("mcp.plugin.config_file.remove"));
577 }613 }
578 });614 }).pipe(
615 Effect.withSpan("mcp.plugin.remove_source", {
616 attributes: { "mcp.source.namespace": namespace },
617 }),
618 );
579 619
580 const refreshSource = (namespace: string) =>620 const refreshSource = (namespace: string) =>
581 Effect.gen(function* () {621 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 );
583 if (!sd) {627 if (!sd) {
584 return yield* Effect.fail(628 return yield* Effect.fail(
585 remoteConnectionError(629 remoteConnectionError(
@@ -588,49 +632,72 @@ export const mcpPlugin = definePlugin( · 2 unmodified lines
588 );632 );
589 }633 }
590 634
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 );
592 const manifest = yield* discoverTools(createMcpConnector(ci)).pipe(643 const manifest = yield* discoverTools(createMcpConnector(ci)).pipe(
593 Effect.mapError((err) =>644 Effect.mapError((err) =>
594 mcpDiscoveryError(`MCP refresh failed: ${err.message}`),645 mcpDiscoveryError(`MCP refresh failed: ${err.message}`),
595 ),646 ),
647 Effect.withSpan("mcp.plugin.discover_tools", {
648 attributes: { "mcp.source.namespace": namespace },
649 }),
596 );650 );
597 651
598 const existing = yield* ctx.storage.getSource(namespace);652 const existing = yield* ctx.storage.getSource(namespace);
599 const sourceName =653 const sourceName =
600 manifest.server?.name ?? existing?.name ?? namespace;654 manifest.server?.name ?? existing?.name ?? namespace;
601 655
602 yield* ctx.transaction(656 yield* ctx
603 Effect.gen(function* () {657 .transaction(
604 yield* ctx.storage.removeBindingsByNamespace(namespace);658 Effect.gen(function* () {
605 yield* ctx.core.sources.unregister(namespace);659 yield* ctx.storage.removeBindingsByNamespace(namespace);
606 660 yield* ctx.core.sources.unregister(namespace);
607 yield* ctx.storage.putBindings(661
608 namespace,662 yield* ctx.storage.putBindings(
609 manifest.tools.map((e) => ({663 namespace,
610 toolId: `${namespace}.${e.toolId}`,664 manifest.tools.map((e) => ({
611 binding: toBinding(e),665 toolId: `${namespace}.${e.toolId}`,
612 })),666 binding: toBinding(e),
613 );667 })),
614 yield* ctx.core.sources.register({668 );
615 id: namespace,669 yield* ctx.core.sources.register({
616 kind: "mcp",670 id: namespace,
617 name: sourceName,671 kind: "mcp",
618 url: sd.transport === "remote" ? sd.endpoint : undefined,672 name: sourceName,
619 canRemove: true,673 url: sd.transport === "remote" ? sd.endpoint : undefined,
620 canRefresh: true,674 canRemove: true,
621 canEdit: sd.transport === "remote",675 canRefresh: true,
622 tools: manifest.tools.map((e) => ({676 canEdit: sd.transport === "remote",
623 name: e.toolId,677 tools: manifest.tools.map((e) => ({
624 description: e.description ?? `MCP tool: ${e.toolName}`,678 name: e.toolId,
625 inputSchema: e.inputSchema,679 description: e.description ?? `MCP tool: ${e.toolName}`,
626 outputSchema: e.outputSchema,680 inputSchema: e.inputSchema,
627 })),681 outputSchema: e.outputSchema,
628 });682 })),
629 }),683 });
630 );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 );
631 694
632 return { toolCount: manifest.tools.length };695 return { toolCount: manifest.tools.length };
633 });696 }).pipe(
697 Effect.withSpan("mcp.plugin.refresh_source", {
698 attributes: { "mcp.source.namespace": namespace },
699 }),
700 );
634 701
635 const startOAuth = (input: McpOAuthStartInput) =>702 const startOAuth = (input: McpOAuthStartInput) =>
636 Effect.gen(function* () {703 Effect.gen(function* () {
@@ -659,24 +726,27 @@ export const mcpPlugin = definePlugin( · 22 unmodified lines
659 Effect.mapError((e) =>726 Effect.mapError((e) =>
660 mcpOAuthError(`OAuth start failed: ${e.message}`),727 mcpOAuthError(`OAuth start failed: ${e.message}`),
661 ),728 ),
729 Effect.withSpan("mcp.plugin.oauth.start_authorization"),
662 );730 );
663 731
664 yield* ctx.storage.putOAuthSession(sessionId, {732 yield* ctx.storage
665 endpoint: fullEndpoint,733 .putOAuthSession(sessionId, {
666 redirectUrl: input.redirectUrl,734 endpoint: fullEndpoint,
667 codeVerifier: started.codeVerifier,735 redirectUrl: input.redirectUrl,
668 resourceMetadataUrl: started.resourceMetadataUrl,736 codeVerifier: started.codeVerifier,
669 authorizationServerUrl: started.authorizationServerUrl,737 resourceMetadataUrl: started.resourceMetadataUrl,
670 resourceMetadata: started.resourceMetadata,738 authorizationServerUrl: started.authorizationServerUrl,
671 authorizationServerMetadata: started.authorizationServerMetadata,739 resourceMetadata: started.resourceMetadata,
672 clientInformation: started.clientInformation,740 authorizationServerMetadata: started.authorizationServerMetadata,
673 });741 clientInformation: started.clientInformation,
742 })
743 .pipe(Effect.withSpan("mcp.plugin.oauth.persist_session"));
674 744
675 return {745 return {
676 sessionId,746 sessionId,
677 authorizationUrl: started.authorizationUrl,747 authorizationUrl: started.authorizationUrl,
678 };748 };
679 });749 }).pipe(Effect.withSpan("mcp.plugin.start_oauth"));
680 750
681 const completeOAuth = (input: McpOAuthCompleteInput) =>751 const completeOAuth = (input: McpOAuthCompleteInput) =>
682 Effect.gen(function* () {752 Effect.gen(function* () {
@@ -705,6 +775,7 @@ export const mcpPlugin = definePlugin( · 22 unmodified lines
705 Effect.mapError((e) =>775 Effect.mapError((e) =>
706 mcpOAuthError(`OAuth exchange failed: ${e.message}`),776 mcpOAuthError(`OAuth exchange failed: ${e.message}`),
707 ),777 ),
778 Effect.withSpan("mcp.plugin.oauth.exchange_code"),
708 );779 );
709 780
710 const accessSecretId = `mcp-oauth-access-${input.state}`;781 const accessSecretId = `mcp-oauth-access-${input.state}`;
@@ -757,7 +828,7 @@ export const mcpPlugin = definePlugin( · 46 unmodified lines
757 expiresAt,828 expiresAt,
758 scope: exchanged.tokens.scope ?? null,829 scope: exchanged.tokens.scope ?? null,
759 };830 };
760 });831 }).pipe(Effect.withSpan("mcp.plugin.complete_oauth"));
761 832
762 const updateSource = (833 const updateSource = (
763 namespace: string,834 namespace: string,
@@ -783,10 +854,18 @@ export const mcpPlugin = definePlugin( · 19 unmodified lines
783 name: input.name?.trim() || existing.name,854 name: input.name?.trim() || existing.name,
784 config: updatedConfig,855 config: updatedConfig,
785 });856 });
786 });857 }).pipe(
858 Effect.withSpan("mcp.plugin.update_source", {
859 attributes: { "mcp.source.namespace": namespace },
860 }),
861 );
787 862
788 const getSource = (namespace: string) =>863 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 );
790 869
791 return {870 return {
792 probeEndpoint,871 probeEndpoint,
@@ -804,14 +883,22 @@ export const mcpPlugin = definePlugin( · 11 unmodified lines
804 Effect.gen(function* () {883 Effect.gen(function* () {
805 const runtime = yield* ensureRuntime();884 const runtime = yield* ensureRuntime();
806 885
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 );
808 if (!entry) {891 if (!entry) {
809 return yield* Effect.fail(892 return yield* Effect.fail(
810 new Error(`No MCP binding found for tool "${toolRow.id}"`),893 new Error(`No MCP binding found for tool "${toolRow.id}"`),
811 );894 );
812 }895 }
813 896
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 );
815 if (!sd) {902 if (!sd) {
816 return yield* Effect.fail(903 return yield* Effect.fail(
817 new Error(904 new Error(
@@ -837,12 +924,25 @@ export const mcpPlugin = definePlugin( · 19 unmodified lines
837 err instanceof Error ? err.message : String(err),924 err instanceof Error ? err.message : String(err),
838 }),925 }),
839 ),926 ),
927 Effect.withSpan("mcp.plugin.resolve_connector", {
928 attributes: {
929 "mcp.source.namespace": entry.namespace,
930 "mcp.source.transport": sd.transport,
931 },
932 }),
840 ),933 ),
841 connectionCache: runtime.connectionCache,934 connectionCache: runtime.connectionCache,
842 pendingConnectors: runtime.pendingConnectors,935 pendingConnectors: runtime.pendingConnectors,
843 elicit,936 elicit,
844 });937 });
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 ),
846 946
847 detect: ({ url }) =>947 detect: ({ url }) =>
848 Effect.gen(function* () {948 Effect.gen(function* () {
@@ -865,6 +965,7 @@ export const mcpPlugin = definePlugin( · 16 unmodified lines
865 const connected = yield* discoverTools(connector).pipe(965 const connected = yield* discoverTools(connector).pipe(
866 Effect.map(() => true),966 Effect.map(() => true),
867 Effect.catchAll(() => Effect.succeed(false)),967 Effect.catchAll(() => Effect.succeed(false)),
968 Effect.withSpan("mcp.plugin.discover_tools"),
868 );969 );
869 970
870 if (connected) {971 if (connected) {
@@ -885,6 +986,7 @@ export const mcpPlugin = definePlugin( · 14 unmodified lines
885 }).pipe(986 }).pipe(
886 Effect.map(() => true),987 Effect.map(() => true),
887 Effect.catchAll(() => Effect.succeed(false)),988 Effect.catchAll(() => Effect.succeed(false)),
989 Effect.withSpan("mcp.plugin.probe_oauth"),
888 );990 );
889 991
890 if (hasOAuth) {992 if (hasOAuth) {
@@ -898,7 +1000,12 @@ export const mcpPlugin = definePlugin( · 7 unmodified lines
898 }1000 }
899 1001
900 return null;1002 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 ),
902 1009
903 // MCP tools never require approval at the tool level — elicitation is1010 // MCP tools never require approval at the tool level — elicitation is
904 // handled mid-invocation by the server via the elicit capability.1011 // handled mid-invocation by the server via the elicit capability.
@@ -928,7 +1035,7 @@ export const mcpPlugin = definePlugin( · 23 unmodified lines
928 yield* Scope.close(runtime.cacheScope, Exit.void);1035 yield* Scope.close(runtime.cacheScope, Exit.void);
929 runtimeRef.current = null;1036 runtimeRef.current = null;
930 }1037 }
931 }),1038 }).pipe(Effect.withSpan("mcp.plugin.close")),
932 };1039 };
933 },1040 },
934);1041);
packages/plugins/openapi/src/sdk/invoke.ts+37−5
@@ -102,9 +102,13 @@ export const resolveHeaders = ( · 101 unmodified lines
102): Effect.Effect<102): Effect.Effect<
103 Record<string, string>,103 Record<string, string>,
104 OpenApiInvocationError | StorageFailure104 OpenApiInvocationError | StorageFailure
105> =>105> => {
106 Effect.gen(function* () {106 const entries = Object.entries(headers);
107 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* () {
108 // Fan out secret lookups: on every invocation, one or two headers112 // Fan out secret lookups: on every invocation, one or two headers
109 // typically each hit the secret store. Resolving them in parallel113 // typically each hit the secret store. Resolving them in parallel
110 // is a free wall-clock win — preserved order is only needed for114 // is a free wall-clock win — preserved order is only needed for
@@ -134,7 +138,15 @@ export const resolveHeaders = ( · 23 unmodified lines
134 const resolved: Record<string, string> = {};138 const resolved: Record<string, string> = {};
135 for (const { name, value } of values) resolved[name] = value;139 for (const { name, value } of values) resolved[name] = value;
136 return resolved;140 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};
138 150
139const applyHeaders = (151const applyHeaders = (
140 request: HttpClientRequest.HttpClientRequest,152 request: HttpClientRequest.HttpClientRequest,
@@ -170,6 +182,14 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 29 unmodified lines
170) {182) {
171 const client = yield* HttpClient.HttpClient;183 const client = yield* HttpClient.HttpClient;
172 184
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
173 const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);193 const resolvedPath = yield* resolvePath(operation.pathTemplate, args, operation.parameters);
174 194
175 const path = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;195 const path = resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;
@@ -216,6 +236,9 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( · 40 unmodified lines
216 );236 );
217 237
218 const status = response.status;238 const status = response.status;
239 yield* Effect.annotateCurrentSpan({
240 "http.status_code": status,
241 });
219 const responseHeaders: Record<string, string> = { ...response.headers };242 const responseHeaders: Record<string, string> = { ...response.headers };
220 243
221 const contentType = response.headers["content-type"] ?? null;244 const contentType = response.headers["content-type"] ?? null;
@@ -268,7 +291,16 @@ export const invokeWithLayer = ( · 46 unmodified lines
268 ).pipe(Layer.provide(httpClientLayer))291 ).pipe(Layer.provide(httpClientLayer))
269 : httpClientLayer;292 : httpClientLayer;
270 293
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 );
272};304};
273 305
274// ---------------------------------------------------------------------------306// ---------------------------------------------------------------------------