@mcp-index/sdk 0.18.1 → 0.19.1

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 (55) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +178 -0
  3. package/dist/src/ai.d.ts +96 -0
  4. package/dist/src/ai.d.ts.map +1 -0
  5. package/dist/src/ai.js +151 -0
  6. package/dist/src/ai.js.map +1 -0
  7. package/dist/src/gate.d.ts +7 -0
  8. package/dist/src/gate.d.ts.map +1 -1
  9. package/dist/src/gate.js +36 -2
  10. package/dist/src/gate.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -1
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/lock/check.d.ts +143 -0
  16. package/dist/src/lock/check.d.ts.map +1 -0
  17. package/dist/src/lock/check.js +274 -0
  18. package/dist/src/lock/check.js.map +1 -0
  19. package/dist/src/lock/digest.d.ts +48 -0
  20. package/dist/src/lock/digest.d.ts.map +1 -0
  21. package/dist/src/lock/digest.js +234 -0
  22. package/dist/src/lock/digest.js.map +1 -0
  23. package/dist/src/lock/events.d.ts +82 -0
  24. package/dist/src/lock/events.d.ts.map +1 -0
  25. package/dist/src/lock/events.js +80 -0
  26. package/dist/src/lock/events.js.map +1 -0
  27. package/dist/src/lock/index.d.ts +20 -0
  28. package/dist/src/lock/index.d.ts.map +1 -0
  29. package/dist/src/lock/index.js +14 -0
  30. package/dist/src/lock/index.js.map +1 -0
  31. package/dist/src/lock/lock.d.ts +195 -0
  32. package/dist/src/lock/lock.d.ts.map +1 -0
  33. package/dist/src/lock/lock.js +1075 -0
  34. package/dist/src/lock/lock.js.map +1 -0
  35. package/dist/src/lock/pg.d.ts +150 -0
  36. package/dist/src/lock/pg.d.ts.map +1 -0
  37. package/dist/src/lock/pg.js +539 -0
  38. package/dist/src/lock/pg.js.map +1 -0
  39. package/dist/src/lock/rawList.d.ts +127 -0
  40. package/dist/src/lock/rawList.d.ts.map +1 -0
  41. package/dist/src/lock/rawList.js +561 -0
  42. package/dist/src/lock/rawList.js.map +1 -0
  43. package/dist/src/lock/signing.d.ts +35 -0
  44. package/dist/src/lock/signing.d.ts.map +1 -0
  45. package/dist/src/lock/signing.js +85 -0
  46. package/dist/src/lock/signing.js.map +1 -0
  47. package/dist/src/lock/store.d.ts +149 -0
  48. package/dist/src/lock/store.d.ts.map +1 -0
  49. package/dist/src/lock/store.js +459 -0
  50. package/dist/src/lock/store.js.map +1 -0
  51. package/dist/src/schemaDiff.d.ts +21 -2
  52. package/dist/src/schemaDiff.d.ts.map +1 -1
  53. package/dist/src/schemaDiff.js +79 -1
  54. package/dist/src/schemaDiff.js.map +1 -1
  55. package/package.json +31 -2
package/CHANGELOG.md CHANGED
@@ -3,7 +3,26 @@
3
3
  All notable changes to the TypeScript SDK. Versioning is semver (0.x: minor =
4
4
  additive feature, patch = fix). Backward-compatible additions only in 0.2.0.
5
5
 
6
- ## [Unreleased]
6
+ ## [0.19.1] - 2026-10-21
7
+
8
+ ### Security
9
+
10
+ - A title or annotation hint change that comes with a new optional parameter is now held for review instead of accepted. Before, the gate auto-accepted the parameter and re-pinned the tool, taking the new `title`, `annotations.title`, readOnlyHint, destructiveHint, idempotentHint or openWorldHint with it unreviewed. Guard and strict now hold it the way they hold a description change; monitor still lets the call run and reports it, and no longer re-pins. Lock mode is unchanged. Same as mcpindex-gate.
11
+ - A title or annotation hint change is now held for review wherever it used to go through. A readOnlyHint, destructiveHint, idempotentHint or openWorldHint change on a tool whose definition hash still matches is held in either direction: a tool newly claiming to be read-only or non-destructive is the case a host would run unattended. So is a hint change that comes with a first outputSchema, and a behavioural validation that clears no longer re-pins over a title or hint change. Guard and strict hold these the way they hold a description change; monitor lets the call run and reports it. A title change on a hash-matched tool was already held. Lock mode is unchanged.
12
+
13
+ ### Added
14
+
15
+ - `classifyLabelChanges` and `LABEL_ONLY_KINDS`, with two new `ChangeKind` members, `title-changed` (tool `title` or `annotations.title` added, removed or changed) and `annotation-hint-changed` (any move of readOnlyHint, destructiveHint, idempotentHint or openWorldHint that `annotation-flip-to-destructive` does not already cover). Labels for a reviewer only: `classifyChange` is unchanged and the kinds never enter a gate verdict. Same output as the Python `classify_label_changes`.
16
+
17
+ ## [0.19.0] - 2026-10-20
18
+
19
+ ### Added
20
+
21
+ - Lock mode, in three new subpaths. It follows mcp.lock draft 1, revision 1d, which is not stable: files carry `lockVersion: "1-draft"` and a later reader will refuse them. It is opt-in, and the root `@mcp-index/sdk` import does not load any of it.
22
+ - `@mcp-index/sdk/lock`: reads and writes a lock in `.mcp/lock/` and `.mcp/contracts/`, checks each `tools/list` against it, and refuses calls to tools that are not approved with error -32011. `lockClient` and `LockedTransport` put it in front of the official MCP client. `FileLockStore` approves and revokes, verifies a signed manifest when a signer is configured, and refuses a `sequencePath` or `trustedRootPath` inside the lock's repository. Listing and refusal events go to a sink you pass.
23
+ - `@mcp-index/sdk/lock/pg`: `PgLockStore`, a Postgres store with append-only hash-chained records, row-level security, `app-scoped` and `role-bound` tenancy, two roles, and an optional signed head that needs a sequence store.
24
+ - `@mcp-index/sdk/ai`: `lockTransport` for `createMCPClient` from the Vercel AI SDK.
25
+ - Optional peer dependencies: `@ai-sdk/mcp` for `/ai`, `pg` for `/lock/pg`, `sigstore` for signed locks. The package still has no runtime dependencies.
7
26
 
8
27
  ## 0.18.1 - 2026-10-13
9
28
 
package/README.md CHANGED
@@ -174,6 +174,184 @@ When enabled, each tool you pin and each contract drift sends **one one-way sign
174
174
  opt-in richer tier (it behaves identically today). The full notice is exported as
175
175
  `DRIFT_TELEMETRY_NOTICE`. Unset the variable any time to stop.
176
176
 
177
+ ## Lock mode (`@mcp-index/sdk/lock`)
178
+
179
+ Lock mode checks every `tools/list` against an mcp.lock kept in your repository
180
+ (`.mcp/lock/` and `.mcp/contracts/`, format: mcp.lock draft 1, revision 1e). The model sees
181
+ only tools whose definition digest matches an approval, and a call to anything else is
182
+ refused with JSON-RPC error -32011 before it reaches the server. Lock mode is opt-in and
183
+ draft 1 is not stable. Nothing here runs unless you construct it, and the root
184
+ `@mcp-index/sdk` import does not load it.
185
+
186
+ Connect the official MCP client through `lockClient`:
187
+
188
+ ```ts
189
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
190
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
191
+ import { FileLockStore, lockClient } from "@mcp-index/sdk/lock";
192
+ import type { Identity } from "@mcp-index/sdk/lock";
193
+
194
+ const identity: Identity = { kind: "stdio", package: "npm:@example/docs-server" };
195
+ const store = new FileLockStore({ root: process.cwd() }); // the directory that holds .mcp/
196
+ const client = new Client({ name: "my-host", version: "1.0.0" });
197
+ const transport = new StdioClientTransport({ command: "npx", args: ["-y", "@example/docs-server"] });
198
+ const locked = await lockClient(client, transport, { store, identity, posture: "guard" });
199
+
200
+ const { tools } = await client.listTools(); // approved tools only
201
+ console.log(tools.map((t) => t.name), locked.decision?.withheld);
202
+ ```
203
+
204
+ `posture` is `monitor` (show everything, report what does not match), `guard` (withhold it)
205
+ or `strict` (same as guard in draft 1). Lock mode never approves a tool on first sight and
206
+ never re-pins. The lock changes only through `store.approve` and `store.revoke`, or a
207
+ reviewed edit to the lock files:
208
+
209
+ ```ts
210
+ import { readFileSync } from "node:fs";
211
+
212
+ // The Tool object exactly as the server sent it, saved for review. Not an SDK-parsed copy:
213
+ // the SDK drops members it does not know, and the digest covers every member.
214
+ const tool: unknown = JSON.parse(readFileSync("review/search.json", "utf8"));
215
+ await store.approve({ identity, tool, subject: "github:you", via: "api", ref: "pr-42" });
216
+ ```
217
+
218
+ The identity names the server, in one of three forms:
219
+
220
+ ```ts
221
+ const remote: Identity = { kind: "remote", url: "https://mcp.example.com/v1/mcp" };
222
+ const pkg: Identity = { kind: "stdio", package: "npm:@example/docs-server" }; // npm, pypi or oci
223
+ const cmd: Identity = { kind: "stdio", command: "uvx", args: ["git+https://github.com/example/server"] };
224
+ ```
225
+
226
+ `remoteIdentity`, `packageIdentity` and `commandIdentity` build them in normalized form.
227
+ `commandIdentity` throws when an argument looks like a credential.
228
+
229
+ A signed lock verifies `.mcp/lock/MANIFEST.json.sigstore` against a signer from your host
230
+ configuration. It needs the optional `sigstore` peer, a trust root file, and a sequence file
231
+ where the host keeps the highest manifest sequence it accepted, so an older signed manifest
232
+ is refused after a newer one:
233
+
234
+ ```ts
235
+ const signed = new FileLockStore({
236
+ root: "/srv/app",
237
+ signer: {
238
+ issuer: "https://token.actions.githubusercontent.com",
239
+ san: "https://github.com/acme/app/.github/workflows/lock.yml@refs/heads/main",
240
+ },
241
+ trustedRootPath: "/etc/mcplock/trusted_root.json",
242
+ sequencePath: "/var/lib/mcplock/sequence",
243
+ });
244
+ ```
245
+
246
+ Both paths must lie outside the repository that holds the lock, or the constructor throws
247
+ `HostConfigError`. The repository is the nearest directory at or above `root` with a `.git`
248
+ directory or a linked worktree's `.git` file; a submodule's `.git` file sends the search on to
249
+ the superproject. The check compares directories by device and inode, so a case-folded
250
+ spelling on macOS and a symlink followed by `..` are judged where they land, and a `..` in the
251
+ part of a path that does not exist yet is refused. When no `.git` entry exists at or above
252
+ `root`, only `root` itself is protected: a path beside it in the same untracked directory is
253
+ accepted, so keep host files somewhere the lock's writers cannot reach. A signer without a sequence store makes every load
254
+ `lock-signature-invalid`.
255
+
256
+ ## Postgres lock store (`@mcp-index/sdk/lock/pg`)
257
+
258
+ `PgLockStore` keeps approvals in Postgres instead of files: append-only, hash-chained
259
+ records per tenant, with row-level security on both tables. `pg` is an optional peer.
260
+
261
+ The schema owner runs the migrations once. They are exported as SQL text:
262
+
263
+ ```ts
264
+ import { writeFileSync } from "node:fs";
265
+ import { MIGRATION_SQL, ROLE_BOUND_SQL, ROLE_SQL } from "@mcp-index/sdk/lock/pg";
266
+
267
+ writeFileSync("mcplock-schema.sql", MIGRATION_SQL + ROLE_SQL);
268
+ writeFileSync("mcplock-role-bound.sql", ROLE_BOUND_SQL); // only for role-bound tenancy
269
+ // psql -v ON_ERROR_STOP=1 -f mcplock-schema.sql
270
+ ```
271
+
272
+ `ROLE_SQL` creates two roles without login. `mcplock_runtime` may only read, and is what hosts
273
+ in lock mode connect as. `mcplock_approver` may also append records and move the head, and
274
+ belongs to the approval path only. Neither may update or delete a record. Grant each to its
275
+ own login, and never connect as a superuser, since a superuser bypasses row-level security:
276
+
277
+ ```sql
278
+ GRANT mcplock_runtime TO host_login;
279
+ GRANT mcplock_approver TO approver_login;
280
+ ```
281
+
282
+ Two tenancy modes:
283
+
284
+ - `app-scoped` (the default, `MIGRATION_SQL` alone). The store sets the tenant from the
285
+ principal at the start of each transaction. This stops a query bug from reaching another
286
+ tenant. It does not stop someone holding a shared login, who can set any tenant.
287
+ - `role-bound` (`ROLE_BOUND_SQL` after the other two). Each tenant gets its own logins, and
288
+ the administrator maps each login to its tenant in `mcplock.tenant_logins`. The policies
289
+ read the tenant from the login, so a login reaches only its own tenant. Use this before
290
+ sharing one database between tenants that do not trust each other.
291
+
292
+ ```ts
293
+ import { PgLockStore, createPgPool } from "@mcp-index/sdk/lock/pg";
294
+
295
+ const pool = await createPgPool(process.env.MCPLOCK_PG_URL ?? "postgres://host_login@127.0.0.1/app");
296
+ const store = new PgLockStore({
297
+ pool,
298
+ principal: { tenantId: "acme", subject: "svc:docs-host" }, // from your own auth, never a header
299
+ tenancy: "app-scoped",
300
+ });
301
+ const snapshot = await store.loadSnapshot();
302
+ console.log(snapshot.snapshotId);
303
+ ```
304
+
305
+ With `headVerifier` set, each load checks a signature over the head row, and the store must
306
+ also get a `sequenceStore`, for example `fileSequenceStore("/var/lib/mcplock/pg-sequence")`
307
+ from `@mcp-index/sdk/lock`, or every load is `lock-signature-invalid`. Without a head
308
+ signature, whoever can write the tables can rewrite the chain. Errors never carry the
309
+ connection string unredacted.
310
+
311
+ ## Lock mode for the Vercel AI SDK (`@mcp-index/sdk/ai`)
312
+
313
+ Wrap the transport you hand to `createMCPClient` from `@ai-sdk/mcp` (an optional peer). The
314
+ wrapper reads the raw `tools/list` exchange, checks every tool against your mcp.lock, and the
315
+ client only ever sees approved tools, through `tools()` or `listTools()` alike:
316
+
317
+ ```ts
318
+ import { createMCPClient } from "@ai-sdk/mcp";
319
+ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
320
+ import { lockTransport } from "@mcp-index/sdk/ai";
321
+ import { FileLockStore } from "@mcp-index/sdk/lock";
322
+ import type { Identity } from "@mcp-index/sdk/lock";
323
+
324
+ const identity: Identity = { kind: "stdio", package: "npm:@example/docs-server" };
325
+ const store = new FileLockStore({ root: process.cwd() });
326
+ const inner = new Experimental_StdioMCPTransport({ command: "npx", args: ["-y", "@example/docs-server"] });
327
+ const transport = lockTransport(inner, { store, identity, posture: "guard" });
328
+ const client = await createMCPClient({ transport });
329
+ const tools = await client.tools(); // approved tools only
330
+ console.log(Object.keys(tools));
331
+ ```
332
+
333
+ A call to a withheld or unlisted tool never reaches the server. `callTool()` and the tool's
334
+ `execute()` reject with an `MCPClientError` whose `code` is -32011 and `data.reason` says why;
335
+ inside `generateText` that shows up as a `tool-error` part.
336
+
337
+ Limits:
338
+
339
+ - A digest says whether the contract changed. It says nothing about what the server does
340
+ when a tool runs.
341
+ - The AI SDK client does not refetch on `notifications/tools/list_changed`. After an approval
342
+ or a server change, call `tools()` again; until then the previous allow-list holds. While a
343
+ listing is being collected, every call is refused with `no-listing`.
344
+ - A tool's top-level `_meta` is not part of its digest. It reaches the client only when it
345
+ matches the `metaDigest` recorded at approval; otherwise, under guard, the tool is shown
346
+ without it and the decision reports `meta-changed` or `meta-unapproved`.
347
+ - `@mcp-index/sdk/ai` imports only types from `@ai-sdk/mcp`, so loading it needs nothing at
348
+ runtime. The `@ai-sdk/mcp` peer itself needs Node >= 22; that is the only part with that
349
+ requirement, and the rest of the package runs on Node 20.
350
+ - The AI SDK's own transports reject any message carrying a `__proto__` key before the wrapper
351
+ sees it, so such a listing never arrives and nothing is callable. The official SDK transports
352
+ pass it through and the lock withholds the tool. Instructions given to the client as
353
+ `initialInitializeResult` never cross the transport and are not checked.
354
+
177
355
  ## Related packages
178
356
 
179
357
  Three ways to bring mcpindex into an agent, for different surfaces:
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `@mcp-index/sdk/ai`: lock mode for the Vercel AI SDK MCP client
3
+ * (`createMCPClient` from `@ai-sdk/mcp`).
4
+ *
5
+ * The AI SDK client parses `tools/list` with its own zod schema and builds
6
+ * tools from what is left, so a digest over `client.listTools()` would not be
7
+ * a digest over what the server sent. lockTransport wraps the transport the
8
+ * client is given and works on JSON-RPC messages before the client sees them.
9
+ * The message work is LockedTransport from `../lock/rawList.ts`: every page of
10
+ * a listing is collected under private ids and digested raw, withheld tools
11
+ * are dropped, and the client gets one result with no cursor. `client.tools()`
12
+ * and `client.listTools()` + `toolsFromDefinitions()` both only ever see
13
+ * approved tools (under guard).
14
+ *
15
+ * Every `tools/call` is checked against the allow-list from the latest
16
+ * complete listing. A refused call is never sent; the client gets a JSON-RPC
17
+ * -32011 error with data.reason. In @ai-sdk/mcp 2.0.74 that rejects
18
+ * `callTool()` and the tool's `execute()` with an MCPClientError carrying
19
+ * code -32011 and data { reason, tool }; it is not retried (the client never
20
+ * retries an error with a JSON-RPC code). Inside generateText the throw
21
+ * becomes a `tool-error` part, so the model sees the refusal as a tool error.
22
+ *
23
+ * Limits, all from the AI SDK client as of 2.0.74:
24
+ * - The client treats any server notification as "Unsupported message
25
+ * type" and reports it to onUncaughtError, so it never refetches on
26
+ * `notifications/tools/list_changed`. The wrapper still marks a listing
27
+ * as due (`listingDue`) and keeps the old allow-list until a new complete
28
+ * listing. The app has to call `tools()` again to see newly approved
29
+ * tools, and to drop ones the server removed.
30
+ * - The wrapper reports `supportsProtocolVersionDiscovery = false`, so the
31
+ * client uses the `initialize` handshake. Server instructions then arrive
32
+ * in the initialize result, where they are digested and, under guard,
33
+ * stripped when they differ from the lock. Instructions passed to the
34
+ * client as `initialInitializeResult` never cross the transport and are
35
+ * not checked.
36
+ * - The AI SDK's own stdio, SSE and HTTP transports parse with a secure
37
+ * JSON parser that throws on a `__proto__` key. Such a message never
38
+ * reaches the wrapper, so there is no listing and no tool is callable;
39
+ * the official SDK transports keep the key and the lock withholds the tool.
40
+ * - `@ai-sdk/mcp` needs Node >= 22. This module itself only imports types
41
+ * from it.
42
+ *
43
+ * The check covers the tool contract the model reads, and nothing about what
44
+ * the server does when a tool runs.
45
+ */
46
+ import type { JSONRPCMessage, MCPTransport, MCPTransportCloseOptions, MCPTransportSendOptions } from "@ai-sdk/mcp";
47
+ import type { CallGate, Decision } from "./lock/check.js";
48
+ import type { LockEventSink } from "./lock/events.js";
49
+ import type { Identity, Posture } from "./lock/lock.js";
50
+ import type { LockStore } from "./lock/store.js";
51
+ /** Host settings that are not about which lock or which server. */
52
+ export interface AiLockHostConfig {
53
+ /** The host name written into events. Default "mcpindex-sdk". */
54
+ readonly host?: string;
55
+ /** Age after which a call reloads the snapshot. Default 60 s. */
56
+ readonly maxStalenessMs?: number;
57
+ readonly now?: () => number;
58
+ }
59
+ export interface AiLockOptions {
60
+ readonly store: LockStore;
61
+ /** This server's identity (section 3), from host configuration. */
62
+ readonly identity: Identity;
63
+ /** monitor, guard, or strict (behaves as guard in draft 1). */
64
+ readonly posture: Posture;
65
+ readonly sink?: LockEventSink;
66
+ readonly hostConfig?: AiLockHostConfig;
67
+ }
68
+ export declare class AiLockTransport implements MCPTransport {
69
+ #private;
70
+ /** Forces the initialize handshake, where instructions are checked. */
71
+ readonly supportsProtocolVersionDiscovery = false;
72
+ onclose?: () => void;
73
+ onerror?: (error: Error) => void;
74
+ onmessage?: (message: JSONRPCMessage) => void;
75
+ constructor(inner: MCPTransport, options: AiLockOptions);
76
+ get supportsMcpToolParameterHeaders(): boolean | undefined;
77
+ get protocolVersion(): string | undefined;
78
+ set protocolVersion(version: string | undefined);
79
+ setProtocolVersion(version: string): void;
80
+ /** The per-connection allow-list. */
81
+ get gate(): CallGate;
82
+ /** The decision for the latest complete listing, or null before one. */
83
+ get decision(): Decision | null;
84
+ /** True after list_changed until the next complete listing (call tools() again). */
85
+ get listingDue(): boolean;
86
+ start(): Promise<void>;
87
+ send(message: JSONRPCMessage, options?: MCPTransportSendOptions): Promise<void>;
88
+ close(options?: MCPTransportCloseOptions): Promise<void>;
89
+ }
90
+ /**
91
+ * Wrap `inner` in lock mode for `createMCPClient({ transport })`.
92
+ * Lock mode has no auto-accept, first-use pin or re-pin; the lock changes
93
+ * only through LockStore.approve and revoke.
94
+ */
95
+ export declare function lockTransport(inner: MCPTransport, options: AiLockOptions): AiLockTransport;
96
+ //# sourceMappingURL=ai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/ai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,aAAa,CAAC;AAIrB,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACxC;AAuCD,qBAAa,eAAgB,YAAW,YAAY;;IAClD,uEAAuE;IACvE,QAAQ,CAAC,gCAAgC,SAAS;IAClD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;gBAKlC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa;IAUvD,IAAI,+BAA+B,IAAI,OAAO,GAAG,SAAS,CAEzD;IAED,IAAI,eAAe,IAAI,MAAM,GAAG,SAAS,CAExC;IAED,IAAI,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAE9C;IAED,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIzC,qCAAqC;IACrC,IAAI,IAAI,IAAI,QAAQ,CAEnB;IAED,wEAAwE;IACxE,IAAI,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAE9B;IAED,oFAAoF;IACpF,IAAI,UAAU,IAAI,OAAO,CAExB;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/E,KAAK,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;CAI/D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,GAAG,eAAe,CAE1F"}
package/dist/src/ai.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `@mcp-index/sdk/ai`: lock mode for the Vercel AI SDK MCP client
3
+ * (`createMCPClient` from `@ai-sdk/mcp`).
4
+ *
5
+ * The AI SDK client parses `tools/list` with its own zod schema and builds
6
+ * tools from what is left, so a digest over `client.listTools()` would not be
7
+ * a digest over what the server sent. lockTransport wraps the transport the
8
+ * client is given and works on JSON-RPC messages before the client sees them.
9
+ * The message work is LockedTransport from `../lock/rawList.ts`: every page of
10
+ * a listing is collected under private ids and digested raw, withheld tools
11
+ * are dropped, and the client gets one result with no cursor. `client.tools()`
12
+ * and `client.listTools()` + `toolsFromDefinitions()` both only ever see
13
+ * approved tools (under guard).
14
+ *
15
+ * Every `tools/call` is checked against the allow-list from the latest
16
+ * complete listing. A refused call is never sent; the client gets a JSON-RPC
17
+ * -32011 error with data.reason. In @ai-sdk/mcp 2.0.74 that rejects
18
+ * `callTool()` and the tool's `execute()` with an MCPClientError carrying
19
+ * code -32011 and data { reason, tool }; it is not retried (the client never
20
+ * retries an error with a JSON-RPC code). Inside generateText the throw
21
+ * becomes a `tool-error` part, so the model sees the refusal as a tool error.
22
+ *
23
+ * Limits, all from the AI SDK client as of 2.0.74:
24
+ * - The client treats any server notification as "Unsupported message
25
+ * type" and reports it to onUncaughtError, so it never refetches on
26
+ * `notifications/tools/list_changed`. The wrapper still marks a listing
27
+ * as due (`listingDue`) and keeps the old allow-list until a new complete
28
+ * listing. The app has to call `tools()` again to see newly approved
29
+ * tools, and to drop ones the server removed.
30
+ * - The wrapper reports `supportsProtocolVersionDiscovery = false`, so the
31
+ * client uses the `initialize` handshake. Server instructions then arrive
32
+ * in the initialize result, where they are digested and, under guard,
33
+ * stripped when they differ from the lock. Instructions passed to the
34
+ * client as `initialInitializeResult` never cross the transport and are
35
+ * not checked.
36
+ * - The AI SDK's own stdio, SSE and HTTP transports parse with a secure
37
+ * JSON parser that throws on a `__proto__` key. Such a message never
38
+ * reaches the wrapper, so there is no listing and no tool is callable;
39
+ * the official SDK transports keep the key and the lock withholds the tool.
40
+ * - `@ai-sdk/mcp` needs Node >= 22. This module itself only imports types
41
+ * from it.
42
+ *
43
+ * The check covers the tool contract the model reads, and nothing about what
44
+ * the server does when a tool runs.
45
+ */
46
+ import { LockedTransport } from "./lock/rawList.js";
47
+ /**
48
+ * Adapts an AI SDK MCPTransport (or an official SDK transport, which has the
49
+ * same shape) to the Transport that LockedTransport drives.
50
+ */
51
+ class InnerAdapter {
52
+ inner;
53
+ onclose;
54
+ onerror;
55
+ onmessage;
56
+ closeOptions;
57
+ constructor(inner) {
58
+ this.inner = inner;
59
+ }
60
+ async start() {
61
+ this.inner.onmessage = (message) => this.onmessage?.(message);
62
+ this.inner.onerror = (error) => this.onerror?.(error instanceof Error ? error : new Error(String(error)));
63
+ this.inner.onclose = () => this.onclose?.();
64
+ await this.inner.start();
65
+ }
66
+ async send(message, options) {
67
+ const m = message;
68
+ if (options === undefined)
69
+ await this.inner.send(m);
70
+ else
71
+ await this.inner.send(m, options);
72
+ }
73
+ async close() {
74
+ const options = this.closeOptions;
75
+ if (options === undefined)
76
+ await this.inner.close();
77
+ else
78
+ await this.inner.close(options);
79
+ }
80
+ setProtocolVersion(version) {
81
+ if (typeof this.inner.setProtocolVersion === "function")
82
+ this.inner.setProtocolVersion(version);
83
+ else
84
+ this.inner.protocolVersion = version;
85
+ }
86
+ }
87
+ export class AiLockTransport {
88
+ /** Forces the initialize handshake, where instructions are checked. */
89
+ supportsProtocolVersionDiscovery = false;
90
+ onclose;
91
+ onerror;
92
+ onmessage;
93
+ #adapter;
94
+ #locked;
95
+ constructor(inner, options) {
96
+ const { hostConfig, ...rest } = options;
97
+ // hostConfig goes first so it cannot replace store, identity or posture.
98
+ // Any key the core refuses (autoAccept, tofu, repin, ...) at either level
99
+ // reaches the LockedTransport constructor and throws there.
100
+ const lockOptions = { ...(hostConfig ?? {}), ...rest };
101
+ this.#adapter = new InnerAdapter(inner);
102
+ this.#locked = new LockedTransport(this.#adapter, lockOptions);
103
+ }
104
+ get supportsMcpToolParameterHeaders() {
105
+ return this.#adapter.inner.supportsMcpToolParameterHeaders;
106
+ }
107
+ get protocolVersion() {
108
+ return this.#adapter.inner.protocolVersion;
109
+ }
110
+ set protocolVersion(version) {
111
+ if (version !== undefined)
112
+ this.#adapter.setProtocolVersion(version);
113
+ }
114
+ setProtocolVersion(version) {
115
+ this.#adapter.setProtocolVersion(version);
116
+ }
117
+ /** The per-connection allow-list. */
118
+ get gate() {
119
+ return this.#locked.gate;
120
+ }
121
+ /** The decision for the latest complete listing, or null before one. */
122
+ get decision() {
123
+ return this.#locked.decision;
124
+ }
125
+ /** True after list_changed until the next complete listing (call tools() again). */
126
+ get listingDue() {
127
+ return this.#locked.listingDue;
128
+ }
129
+ async start() {
130
+ this.#locked.onmessage = (message) => this.onmessage?.(message);
131
+ this.#locked.onerror = (error) => this.onerror?.(error);
132
+ this.#locked.onclose = () => this.onclose?.();
133
+ await this.#locked.start();
134
+ }
135
+ async send(message, options) {
136
+ await this.#locked.send(message, options);
137
+ }
138
+ async close(options) {
139
+ this.#adapter.closeOptions = options;
140
+ await this.#locked.close();
141
+ }
142
+ }
143
+ /**
144
+ * Wrap `inner` in lock mode for `createMCPClient({ transport })`.
145
+ * Lock mode has no auto-accept, first-use pin or re-pin; the lock changes
146
+ * only through LockStore.approve and revoke.
147
+ */
148
+ export function lockTransport(inner, options) {
149
+ return new AiLockTransport(inner, options);
150
+ }
151
+ //# sourceMappingURL=ai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai.js","sourceRoot":"","sources":["../../src/ai.ts"],"names":[],"mappings":"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"}
@@ -188,6 +188,13 @@ export declare class Gate {
188
188
  private static riskOf;
189
189
  private static markerIn;
190
190
  private markerIn;
191
+ /** A HOLD when `classifyLabelChanges` reports a title or annotation hint change against
192
+ * the pin, else null. Called on every path that would otherwise proceed with a moved title
193
+ * or hint or re-pin over one: the benign auto-accept, a hash match, and a behavioural
194
+ * clear. A title is text the model reads; a hint decides what a host lets run unattended,
195
+ * in either direction. The hash covers neither, and the label kinds stay out of `drift` so
196
+ * no kind set, hold class or telemetry row moves. Mirrors gate.py `_label_hold`. */
197
+ private labelHold;
191
198
  private descriptionHold;
192
199
  private driftHold;
193
200
  private behavioralInconclusive;
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"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"}
package/dist/src/gate.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * only the PUBLIC contract dict the caller observed.
8
8
  */
9
9
  import { Decision, HoldClass, hashTool, makeVerdict, utcNowIso, } from "./preflight.js";
10
- import { ChangeKind, UndiffableError, classifyChange, isNumericOnlyDescriptionChange, isSafetyRelevant, jsonEqual, } from "./schemaDiff.js";
10
+ import { ChangeKind, UndiffableError, classifyChange, classifyLabelChanges, isNumericOnlyDescriptionChange, isSafetyRelevant, jsonEqual, } from "./schemaDiff.js";
11
11
  import { CONTRACT_FIELDS_VERSION, comparedFields, comparedKeys, fieldLabel, fieldsToBackfill, isComplete, } from "./contractFields.js";
12
12
  import { assess, blastRank } from "./risk.js";
13
13
  import { classifyToolDef } from "./actionClass.js";
@@ -73,6 +73,9 @@ const DANGEROUS_REASON_MARKERS = [
73
73
  "could not complete",
74
74
  "DESCRIPTION changed",
75
75
  "schema text changed",
76
+ // A title or annotation hint that moved beside an otherwise benign change. A title is
77
+ // text the model reads; a hint decides what a host lets run unattended.
78
+ "title or annotation hint changed",
76
79
  // title, icons, _meta, execution or a later top-level field on a hash-matched tool.
77
80
  "a tool field outside the definition hash changed",
78
81
  "outputSchema text was added",
@@ -984,7 +987,10 @@ export class Gate {
984
987
  if (added.length > 0 && schemaCarriesText(observedRec["outputSchema"])) {
985
988
  return this.driftHold(pinned.toolName, added, "contract DRIFTED: outputSchema text was added to a pinned tool (strings the host passes on and the hash does not cover); review and re-pin if expected", HoldClass.DRIFT);
986
989
  }
987
- return null;
990
+ // A hint or title that moved on a hash-matched tool, on its own or beside an additive
991
+ // outputSchema. comparedFields leaves the four hints out, so this is the only place a
992
+ // hint-only change is seen.
993
+ return this.labelHold(pinned.toolName, drift, pinned.schema, observed);
988
994
  }
989
995
  validateDrift(name, pinned, observed, effects = null) {
990
996
  let drift;
@@ -1053,6 +1059,10 @@ export class Gate {
1053
1059
  if (schemaTextBlocks(pinned.schema, observed)) {
1054
1060
  return this.driftHold(name, drift, "contract DRIFTED: schema text changed (an added parameter carries text, or the input schema is not the pinned schema once added parameters are removed)", HoldClass.DRIFT);
1055
1061
  }
1062
+ // A title or annotation hint that rides along with an otherwise benign change.
1063
+ const labelHold = this.labelHold(name, drift, pinned.schema, observed);
1064
+ if (labelHold !== null)
1065
+ return labelHold;
1056
1066
  if (effects !== null) {
1057
1067
  effects.push({ kind: "repin", toolName: name, observed });
1058
1068
  }
@@ -1112,6 +1122,26 @@ export class Gate {
1112
1122
  return Gate.markerIn(defn);
1113
1123
  }
1114
1124
  // -------------------------------------------------------------- verdicts
1125
+ /** A HOLD when `classifyLabelChanges` reports a title or annotation hint change against
1126
+ * the pin, else null. Called on every path that would otherwise proceed with a moved title
1127
+ * or hint or re-pin over one: the benign auto-accept, a hash match, and a behavioural
1128
+ * clear. A title is text the model reads; a hint decides what a host lets run unattended,
1129
+ * in either direction. The hash covers neither, and the label kinds stay out of `drift` so
1130
+ * no kind set, hold class or telemetry row moves. Mirrors gate.py `_label_hold`. */
1131
+ labelHold(name, drift, pinnedSchema, observed) {
1132
+ let labels;
1133
+ try {
1134
+ labels = [...new Set(classifyLabelChanges(pinnedSchema ?? {}, observed).map((c) => c.path))].sort();
1135
+ }
1136
+ catch {
1137
+ return this.driftHold(name, drift, "contract DRIFTED and the title and annotation hint comparison could not complete; fail-closed HOLD", HoldClass.DRIFT);
1138
+ }
1139
+ if (labels.length === 0)
1140
+ return null;
1141
+ return this.driftHold(name, drift, "contract DRIFTED: a title or annotation hint changed (" +
1142
+ labels.join(", ") +
1143
+ "); review the before/after and re-pin if expected", HoldClass.DRIFT);
1144
+ }
1115
1145
  descriptionHold(name, drift, oldDesc, newDesc) {
1116
1146
  return makeVerdict({
1117
1147
  serverId: this._serverId,
@@ -1307,6 +1337,10 @@ export class Gate {
1307
1337
  BehavioralOutcome.CLEARED,
1308
1338
  ];
1309
1339
  }
1340
+ // The sandbox is as blind to a title or hint as to a description.
1341
+ const labelHold = this.labelHold(toolName, [...staticVerdict.drift], pinned.schema, observed);
1342
+ if (labelHold !== null)
1343
+ return [labelHold, BehavioralOutcome.CLEARED];
1310
1344
  this.repinOne(toolName, observed);
1311
1345
  return [
1312
1346
  makeVerdict({