@cursor/july 0.1.88 → 0.1.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/bin/agent-serve.js +1 -1
  2. package/dist/internal/advertise-tools.d.ts.map +1 -1
  3. package/dist/internal/advertise-tools.js +1 -1
  4. package/dist/internal/cli-ax.js +1 -1
  5. package/dist/internal/cli-deploy.d.ts +3 -2
  6. package/dist/internal/cli-deploy.d.ts.map +1 -1
  7. package/dist/internal/cli-deploy.js +84 -9
  8. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  9. package/dist/internal/cursor/account-mcp.js +8 -4
  10. package/dist/internal/cursor/backend-client.d.ts +8 -0
  11. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  12. package/dist/internal/cursor/backend-client.js +23 -1
  13. package/dist/internal/deploy-client.d.ts +42 -1
  14. package/dist/internal/deploy-client.d.ts.map +1 -1
  15. package/dist/internal/deploy-client.js +70 -2
  16. package/dist/internal/mcp-endpoint.js +2 -1
  17. package/dist/internal/session-engine.d.ts +9 -2
  18. package/dist/internal/session-engine.d.ts.map +1 -1
  19. package/dist/internal/session-engine.js +85 -28
  20. package/dist/internal/tool-policy.d.ts +33 -1
  21. package/dist/internal/tool-policy.d.ts.map +1 -1
  22. package/dist/internal/tool-policy.js +53 -0
  23. package/dist/playground/assets/{index-Bqn91tW4.js → index-BmiIjFlM.js} +45 -45
  24. package/dist/playground/assets/index-DQGZnAI0.css +1 -0
  25. package/dist/playground/index.html +2 -2
  26. package/dist/types.d.ts +22 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/skills/debug/SKILL.md +1 -2
  30. package/skills/framework-map/SKILL.md +1 -2
  31. package/src/bin/agent-serve.ts +1 -1
  32. package/src/internal/advertise-tools.ts +1 -0
  33. package/src/internal/cli-ax.ts +1 -1
  34. package/src/internal/cli-deploy.ts +123 -15
  35. package/src/internal/cursor/account-mcp.ts +15 -1
  36. package/src/internal/cursor/backend-client.ts +33 -0
  37. package/src/internal/deploy-client.ts +122 -2
  38. package/src/internal/mcp-endpoint.ts +2 -1
  39. package/src/internal/session-engine.ts +86 -6
  40. package/src/internal/tool-policy.ts +80 -1
  41. package/src/types.ts +19 -0
  42. package/dist/playground/assets/index-C0_5hOsf.css +0 -1
@@ -15,6 +15,8 @@
15
15
 
16
16
  import { randomUUID } from "node:crypto";
17
17
  import { z } from "zod";
18
+ import type { McpToolAnnotations } from "../../types.js";
19
+ import { boundedToolAnnotations } from "../mcp-host.js";
18
20
 
19
21
  const CLIENT_VERSION = "agent-serve-0.1.0";
20
22
  const EXCHANGE_PATH = "/auth/exchange_user_api_key";
@@ -85,6 +87,8 @@ const mcpToolSchema = z.object({
85
87
  inputSchema: z.unknown().optional(),
86
88
  /** Flat JSON-string output schema (`output_schema_json` on the proto). */
87
89
  outputSchemaJson: z.string().optional(),
90
+ /** Flat JSON-string MCP annotations (`annotations_json` on the proto). */
91
+ annotationsJson: z.string().optional(),
88
92
  });
89
93
 
90
94
  const getAvailableMcpServersResponseSchema = z.object({
@@ -154,6 +158,13 @@ export interface AccountMcpTool {
154
158
  * matching the optional MCP `Tool.outputSchema` field.
155
159
  */
156
160
  outputSchema?: Record<string, unknown>;
161
+ /**
162
+ * MCP spec tool annotations as the connector's server reported them,
163
+ * bounded to the spec fields at parse time. Absent stays absent —
164
+ * untrusted hints, and the fail-closed signal for consumers that classify
165
+ * on them (dry-run effect resolution).
166
+ */
167
+ annotations?: McpToolAnnotations;
157
168
  }
158
169
 
159
170
  export interface AccountMcpServer {
@@ -331,11 +342,13 @@ export class CursorBackendClient {
331
342
  status: server.status,
332
343
  tools: (server.tools ?? []).map((tool) => {
333
344
  const outputSchema = parseOutputSchemaJson(tool.outputSchemaJson);
345
+ const annotations = parseAnnotationsJson(tool.annotationsJson);
334
346
  return {
335
347
  name: tool.name,
336
348
  description: tool.description ?? "",
337
349
  inputSchema: normalizeInputSchema(tool.inputSchema),
338
350
  ...(outputSchema === undefined ? {} : { outputSchema }),
351
+ ...(annotations === undefined ? {} : { annotations }),
339
352
  };
340
353
  }),
341
354
  }));
@@ -707,6 +720,26 @@ function parseOutputSchemaJson(
707
720
  : undefined;
708
721
  }
709
722
 
723
+ /**
724
+ * Parse the flat JSON-string annotations off the wire and bound them to the
725
+ * MCP-spec fields. Malformed or non-object payloads behave like absent ones,
726
+ * so readers fail closed.
727
+ */
728
+ function parseAnnotationsJson(
729
+ annotationsJson: string | undefined
730
+ ): McpToolAnnotations | undefined {
731
+ if (annotationsJson === undefined) {
732
+ return undefined;
733
+ }
734
+ let parsed: unknown;
735
+ try {
736
+ parsed = JSON.parse(annotationsJson);
737
+ } catch {
738
+ return undefined;
739
+ }
740
+ return boundedToolAnnotations(parsed);
741
+ }
742
+
710
743
  type McpResultJson = z.infer<typeof mcpResultSchema>;
711
744
 
712
745
  function toCallResult(result: McpResultJson): AccountMcpCallResult {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Thin client for the Agent Serve deployment management API
3
- * (`/internal/agent-serve/deployments` on the Cursor backend). Pure
3
+ * (`/internal/agent-serve/deployments` on the Cursor backend) and the v2
4
+ * AgentSDK accept path (`/internal/agentkit/deployments`). Pure
4
5
  * request/response + error mapping — no console I/O, no polling loops;
5
6
  * rendering lives in `cli-deploy.ts`.
6
7
  *
@@ -212,6 +213,48 @@ export interface StopAccepted {
212
213
 
213
214
  export type DeleteAccepted = StopAccepted;
214
215
 
216
+ /**
217
+ * `POST /internal/agentkit/deployments` 202 body. Distinct from v1
218
+ * {@link DeployAccepted}: the v2 control plane returns catalog ids, not
219
+ * an engine alias or one-shot alias token.
220
+ */
221
+ export interface AgentkitDeployAccepted {
222
+ applicationId: string;
223
+ releaseId: string;
224
+ status: string;
225
+ }
226
+
227
+ /** Customer-visible v2 deploy status (`GET /internal/agentkit/deployments/:slug`). */
228
+ export type AgentkitCustomerStatus =
229
+ | "pending"
230
+ | "deploying"
231
+ | "running"
232
+ | "failed"
233
+ | "stopped";
234
+
235
+ export type AgentkitDeployProgress =
236
+ | "queued"
237
+ | "creating"
238
+ | "building"
239
+ | "starting"
240
+ | "finalizing"
241
+ | "running"
242
+ | "failed"
243
+ | "stopped";
244
+
245
+ export interface AgentkitDeployStatus {
246
+ applicationId: string;
247
+ releaseId: string;
248
+ slug: string;
249
+ teamId: number;
250
+ status: AgentkitCustomerStatus;
251
+ progress: AgentkitDeployProgress;
252
+ statusMessage: string | null;
253
+ }
254
+
255
+ export const TERMINAL_AGENTKIT_DEPLOY_STATUSES: ReadonlySet<AgentkitCustomerStatus> =
256
+ new Set(["running", "failed", "stopped"]);
257
+
215
258
  /** One secret's metadata from the list endpoint — names only, never values. */
216
259
  export interface SecretInfo {
217
260
  name: string;
@@ -260,6 +303,35 @@ const deployAcceptedSchema = z
260
303
  })
261
304
  .passthrough();
262
305
 
306
+ const agentkitDeployAcceptedSchema = z
307
+ .object({
308
+ applicationId: z.string().min(1),
309
+ releaseId: z.string().min(1),
310
+ status: z.string(),
311
+ })
312
+ .passthrough();
313
+
314
+ const agentkitDeployStatusSchema = z
315
+ .object({
316
+ applicationId: z.string().min(1),
317
+ releaseId: z.string().min(1),
318
+ slug: z.string().min(1),
319
+ teamId: z.number(),
320
+ status: z.enum(["pending", "deploying", "running", "failed", "stopped"]),
321
+ progress: z.enum([
322
+ "queued",
323
+ "creating",
324
+ "building",
325
+ "starting",
326
+ "finalizing",
327
+ "running",
328
+ "failed",
329
+ "stopped",
330
+ ]),
331
+ statusMessage: z.string().nullable(),
332
+ })
333
+ .passthrough();
334
+
263
335
  const deploymentSchema = z.object({
264
336
  id: z.number(),
265
337
  slug: z.string(),
@@ -482,6 +554,54 @@ export class AgentServeDeployClient {
482
554
  return deployAcceptedSchema.parse(raw);
483
555
  }
484
556
 
557
+ /**
558
+ * Create or redeploy onto the v2 control plane
559
+ * (`POST /internal/agentkit/deployments`). 202 means accepted; poll
560
+ * {@link getAgentkitDeploy} until a terminal status.
561
+ */
562
+ async deployAgentkit(args: {
563
+ teamId: number;
564
+ slug: string;
565
+ gitRepoUrl: string;
566
+ gitRef?: string;
567
+ agentPath?: string;
568
+ repositories?: string[];
569
+ }): Promise<AgentkitDeployAccepted> {
570
+ const slugError = validateDeploymentSlug(args.slug);
571
+ if (slugError !== undefined) {
572
+ throw new DeployApiError(slugError, 400);
573
+ }
574
+ const raw = await this.request({
575
+ method: "POST",
576
+ path: "/internal/agentkit/deployments",
577
+ body: {
578
+ teamId: args.teamId,
579
+ slug: args.slug,
580
+ gitRepoUrl: args.gitRepoUrl,
581
+ gitRef: args.gitRef,
582
+ agentPath: args.agentPath,
583
+ repositories:
584
+ args.repositories !== undefined && args.repositories.length > 0
585
+ ? args.repositories
586
+ : undefined,
587
+ },
588
+ context: { verb: "deploy", slug: args.slug },
589
+ });
590
+ return agentkitDeployAcceptedSchema.parse(raw);
591
+ }
592
+
593
+ async getAgentkitDeploy(args: {
594
+ teamId: number;
595
+ slug: string;
596
+ }): Promise<AgentkitDeployStatus> {
597
+ const raw = await this.request({
598
+ method: "GET",
599
+ path: `/internal/agentkit/deployments/${encodeURIComponent(args.slug)}?teamId=${args.teamId}`,
600
+ context: { verb: "get deployment", slug: args.slug },
601
+ });
602
+ return agentkitDeployStatusSchema.parse(raw);
603
+ }
604
+
485
605
  async listDeployments(args: { teamId: number }): Promise<Deployment[]> {
486
606
  const raw = await this.request({
487
607
  method: "GET",
@@ -756,7 +876,7 @@ function mapErrorResponse(
756
876
  // (api2 vs api.cursor.com), not a missing deployment / closed gate.
757
877
  if (/Route \S+ not found/i.test(bodyText)) {
758
878
  return new DeployApiError(
759
- `Agent Serve deploy API not found on this host — use the default api.cursor.com (unset CURSOR_API_BASE_URL), or point CURSOR_API_BASE_URL at a backend that mounts /internal/agent-serve/*.`,
879
+ `Agent Serve deploy API not found on this host — use the default api.cursor.com (unset CURSOR_API_BASE_URL), or point CURSOR_API_BASE_URL at a backend that mounts /internal/agent-serve/* and /internal/agentkit/*.`,
760
880
  404
761
881
  );
762
882
  }
@@ -318,8 +318,9 @@ function buildConnectionBridgeMcpServer(
318
318
  }
319
319
  // The one tool path that never reaches `executeServerTool`, so it asks
320
320
  // the policy itself.
321
- const answer = engine.bridgedCallAnswer({
321
+ const answer = await engine.bridgedCallAnswer({
322
322
  sessionId: sessionId ?? "",
323
+ connectionName,
323
324
  toolName: request.params.name,
324
325
  });
325
326
  if (answer.answered) {
@@ -55,6 +55,7 @@ import {
55
55
  isHostOauthConnectionTransport,
56
56
  isSymbolicConnectionTransport,
57
57
  type JsonValue,
58
+ type McpToolAnnotations,
58
59
  type PendingApproval,
59
60
  type ReceiveInput,
60
61
  type ResolvedAgent,
@@ -2480,6 +2481,14 @@ export class SessionEngine {
2480
2481
  const decision = decideToolCall({
2481
2482
  dryRun: args.record.dryRun === true,
2482
2483
  effect: args.tool.definition.effect,
2484
+ ...(args.tool.advertised === true
2485
+ ? {
2486
+ mcpTool: {
2487
+ name: args.tool.name,
2488
+ annotations: args.tool.annotations,
2489
+ },
2490
+ }
2491
+ : {}),
2483
2492
  dryRunResult: args.tool.definition.dryRunResult,
2484
2493
  input: validation.value,
2485
2494
  });
@@ -2490,6 +2499,17 @@ export class SessionEngine {
2490
2499
  );
2491
2500
  return normalizeToolResult(decision.result);
2492
2501
  }
2502
+ if (decision.kind === "refuse") {
2503
+ this.logger(
2504
+ `[session] tool ${args.tool.name} refused, effect unclassified (${toolCallId})`
2505
+ );
2506
+ // The coverage signal: a run reports how much of itself was runnable.
2507
+ args.emit({
2508
+ type: "action.effect_unclassified",
2509
+ data: { callId: toolCallId, toolName: args.tool.name },
2510
+ });
2511
+ return toolErrorResult(decision.message);
2512
+ }
2493
2513
  const needsGate = await evaluateNeedsApproval(
2494
2514
  args.tool.definition.needsApproval,
2495
2515
  validation.value
@@ -2778,6 +2798,9 @@ export class SessionEngine {
2778
2798
  const decision = decideToolCall({
2779
2799
  dryRun: record?.dryRun === true,
2780
2800
  effect: tool.definition.effect,
2801
+ ...(tool.advertised === true
2802
+ ? { mcpTool: { name: tool.name, annotations: tool.annotations } }
2803
+ : {}),
2781
2804
  dryRunResult: tool.definition.dryRunResult,
2782
2805
  input: validation.value,
2783
2806
  });
@@ -2798,7 +2821,17 @@ export class SessionEngine {
2798
2821
  }).catch(() => {});
2799
2822
  };
2800
2823
  try {
2801
- if (decision.kind === "answer") {
2824
+ if (decision.kind === "refuse") {
2825
+ this.logger(
2826
+ `[session] tool ${toolName} refused, effect unclassified (direct ${callId})`
2827
+ );
2828
+ emit?.({
2829
+ type: "action.effect_unclassified",
2830
+ data: { callId, toolName },
2831
+ });
2832
+ result = normalizeToolResult(toolErrorResult(decision.message));
2833
+ isError = true;
2834
+ } else if (decision.kind === "answer") {
2802
2835
  this.logger(
2803
2836
  `[session] tool ${toolName} answered by host (direct ${callId})`
2804
2837
  );
@@ -2896,28 +2929,75 @@ export class SessionEngine {
2896
2929
  * bridged reads too; resolving effects from a server's own listing is the
2897
2930
  * next unit's work.
2898
2931
  */
2899
- bridgedCallAnswer(args: {
2932
+ async bridgedCallAnswer(args: {
2900
2933
  sessionId: string;
2934
+ connectionName: string;
2901
2935
  toolName: string;
2902
- }): { answered: true; result: SDKCustomToolResult } | { answered: false } {
2936
+ }): Promise<
2937
+ { answered: true; result: SDKCustomToolResult } | { answered: false }
2938
+ > {
2903
2939
  // Read off the in-flight turn's context rather than the session store:
2904
2940
  // the bridge already refuses a call with no active turn, so the record is
2905
2941
  // in memory. That keeps this free for the sessions that are not dry runs,
2906
- // which is every session today.
2907
- const record = this.activeToolContexts.get(args.sessionId)?.record;
2942
+ // which is every session today — the listing below is only paid inside a
2943
+ // dry run.
2944
+ const context = this.activeToolContexts.get(args.sessionId);
2945
+ if (context?.record.dryRun !== true) {
2946
+ return { answered: false };
2947
+ }
2948
+ // A bridged tool carries no `defineTool`, so its effect comes from the
2949
+ // server's own listing annotations; a tool the listing cannot classify
2950
+ // refuses rather than being stubbed. A failed listing classifies nothing,
2951
+ // which lands on the same refusal — never on a run.
2952
+ const annotations = await this.bridgedToolAnnotations(
2953
+ args.connectionName,
2954
+ args.toolName
2955
+ );
2908
2956
  const decision = decideToolCall({
2909
- dryRun: record?.dryRun === true,
2957
+ dryRun: true,
2910
2958
  effect: undefined,
2959
+ mcpTool: { name: args.toolName, annotations },
2911
2960
  });
2912
2961
  if (decision.kind === "run") {
2913
2962
  return { answered: false };
2914
2963
  }
2964
+ if (decision.kind === "refuse") {
2965
+ this.logger(
2966
+ `[session] bridged tool ${args.toolName} refused, effect unclassified (${args.sessionId})`
2967
+ );
2968
+ context.emit({
2969
+ type: "action.effect_unclassified",
2970
+ data: {
2971
+ callId: newToolCallId(args.toolName),
2972
+ toolName: args.toolName,
2973
+ connection: args.connectionName,
2974
+ },
2975
+ });
2976
+ return { answered: true, result: toolErrorResult(decision.message) };
2977
+ }
2915
2978
  this.logger(
2916
2979
  `[session] bridged tool ${args.toolName} answered by host (${args.sessionId})`
2917
2980
  );
2918
2981
  return { answered: true, result: normalizeToolResult(decision.result) };
2919
2982
  }
2920
2983
 
2984
+ /**
2985
+ * The bounded annotations a connection's listing declares for one tool, or
2986
+ * undefined when the tool is unlisted, unannotated, or the listing fails —
2987
+ * all of which the policy reads as unclassified, the refusing state.
2988
+ */
2989
+ private async bridgedToolAnnotations(
2990
+ connectionName: string,
2991
+ toolName: string
2992
+ ): Promise<McpToolAnnotations | undefined> {
2993
+ try {
2994
+ const listed = await this.host.mcp.listTools(connectionName);
2995
+ return listed.find((tool) => tool.name === toolName)?.annotations;
2996
+ } catch {
2997
+ return undefined;
2998
+ }
2999
+ }
3000
+
2921
3001
  // ==========================================================================
2922
3002
  // Events: append, dispatch to channel handlers + hooks
2923
3003
  // ==========================================================================
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type {
13
+ McpToolAnnotations,
13
14
  ToolEffect,
14
15
  ToolEffectDeclaration,
15
16
  ToolExecuteResult,
@@ -20,7 +21,14 @@ export type ToolDecision =
20
21
  /** Run the tool body. */
21
22
  | { kind: "run" }
22
23
  /** Do not run it; return this to the model instead. */
23
- | { kind: "answer"; result: ToolExecuteResult };
24
+ | { kind: "answer"; result: ToolExecuteResult }
25
+ /**
26
+ * Do not run it; fail the call with this message. Only ever produced in a
27
+ * dry run, for an MCP tool nothing classifies: stubbing it would let the
28
+ * run reach a confident conclusion from no data, and running it could leak
29
+ * a real write — refusing is the one failure that is loud.
30
+ */
31
+ | { kind: "refuse"; message: string };
24
32
 
25
33
  /** What a policy decision needs to know about the call. */
26
34
  export interface ToolCallFacts {
@@ -28,6 +36,14 @@ export interface ToolCallFacts {
28
36
  dryRun: boolean;
29
37
  /** The tool's declaration, if it made one. */
30
38
  effect: ToolEffectDeclaration<never> | undefined;
39
+ /**
40
+ * Present when the call targets a tool nobody authored — an advertised MCP
41
+ * passthrough or a bridged MCP call. Consulted only when {@link effect} is
42
+ * undeclared: the server's own annotations classify the call, and a tool
43
+ * they cannot classify refuses in a dry run instead of being stubbed as a
44
+ * write the way an undeclared authored tool is.
45
+ */
46
+ mcpTool?: { name: string; annotations?: McpToolAnnotations };
31
47
  /** What a stubbed write should answer with, if the tool supplied one. */
32
48
  dryRunResult?: ToolExecuteResult | ((input: never) => ToolExecuteResult);
33
49
  /**
@@ -64,11 +80,74 @@ export function resolveEffect(
64
80
  return declared === "read" ? "read" : "write";
65
81
  }
66
82
 
83
+ /**
84
+ * The effect an MCP server's own annotations declare, fail-closed.
85
+ *
86
+ * `readOnlyHint: true` without `destructiveHint: true` is a read; either
87
+ * negative signal is a write; anything else — absent annotations, or
88
+ * annotations that carry no effect signal (a bare `title`) — is `undefined`,
89
+ * unclassified. Mirrors `@anysphere/mcp-core/mcp-tool-annotations`
90
+ * (`classifyMcpToolAnnotationRisk`), which this package cannot import (npm
91
+ * publish, workspace-only dependency); keep the two in sync.
92
+ */
93
+ export function classifyAnnotationsEffect(
94
+ annotations: McpToolAnnotations | undefined
95
+ ): ToolEffect | undefined {
96
+ if (annotations === undefined) {
97
+ return undefined;
98
+ }
99
+ if (
100
+ annotations.destructiveHint !== true &&
101
+ annotations.readOnlyHint === true
102
+ ) {
103
+ return "read";
104
+ }
105
+ if (
106
+ annotations.readOnlyHint === false ||
107
+ annotations.destructiveHint === true
108
+ ) {
109
+ return "write";
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ /**
115
+ * What a dry run answers when nothing classifies an MCP tool. Deliberately
116
+ * reads as a strict API rather than disclosing the session's posture (see
117
+ * {@link ACKNOWLEDGED}), and names what is missing so the fix — the server
118
+ * annotating, or a server-side classification entry — is one step away.
119
+ */
120
+ function unclassifiedRefusal(toolName: string): string {
121
+ return (
122
+ `Tool "${toolName}" is not available in this session: its MCP server ` +
123
+ `does not declare whether it reads or writes (readOnlyHint/` +
124
+ `destructiveHint), and no server-side classification exists for it, so ` +
125
+ `this session cannot tell whether calling it would change anything ` +
126
+ `outside the session. Use a tool that declares its effect.`
127
+ );
128
+ }
129
+
67
130
  /** Decide what to do with one tool call. Pure; no I/O, no session state. */
68
131
  export function decideToolCall(facts: ToolCallFacts): ToolDecision {
69
132
  if (!facts.dryRun) {
70
133
  return { kind: "run" };
71
134
  }
135
+ if (facts.effect === undefined && facts.mcpTool !== undefined) {
136
+ const effect = classifyAnnotationsEffect(facts.mcpTool.annotations);
137
+ if (effect === undefined) {
138
+ return {
139
+ kind: "refuse",
140
+ message: unclassifiedRefusal(facts.mcpTool.name),
141
+ };
142
+ }
143
+ if (effect === "read") {
144
+ return { kind: "run" };
145
+ }
146
+ return {
147
+ kind: "answer",
148
+ result: dryRunAnswer(facts.dryRunResult, facts.input),
149
+ };
150
+ }
72
151
  if (resolveEffect(facts.effect, facts.input) === "read") {
73
152
  return { kind: "run" };
74
153
  }
package/src/types.ts CHANGED
@@ -1439,6 +1439,17 @@ export type SessionEventPayload =
1439
1439
  parentCallId?: string;
1440
1440
  };
1441
1441
  }
1442
+ | {
1443
+ /**
1444
+ * A dry-run call was refused because nothing classifies the tool's
1445
+ * effect: the MCP server sent no readOnlyHint/destructiveHint and no
1446
+ * server-side classification exists for it. The per-run count of these
1447
+ * is the coverage signal — how much of a backtest was actually
1448
+ * runnable, reported rather than assumed.
1449
+ */
1450
+ type: "action.effect_unclassified";
1451
+ data: { callId: string; toolName: string; connection?: string };
1452
+ }
1442
1453
  | {
1443
1454
  type: "subagent.called";
1444
1455
  data: { callId: string; name?: string; description?: string };
@@ -2620,6 +2631,14 @@ export interface DiscoveredTool {
2620
2631
  * classification can read them; absent is the fail-closed state.
2621
2632
  */
2622
2633
  annotations?: McpToolAnnotations;
2634
+ /**
2635
+ * True for a 1:1 MCP passthrough synthesized from an advertised
2636
+ * connection's listing. The policy treats these differently from authored
2637
+ * tools when nothing declares an effect: an undeclared authored tool is
2638
+ * stubbed as a write in a dry run, an unclassifiable advertised tool is
2639
+ * refused — see `decideToolCall`.
2640
+ */
2641
+ advertised?: true;
2623
2642
  /**
2624
2643
  * Agent tools: script body materialized under
2625
2644
  * `.agent-serve/tools/<name>.sh`.