@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.
- package/CHANGELOG.md +20 -1
- package/README.md +178 -0
- package/dist/src/ai.d.ts +96 -0
- package/dist/src/ai.d.ts.map +1 -0
- package/dist/src/ai.js +151 -0
- package/dist/src/ai.js.map +1 -0
- package/dist/src/gate.d.ts +7 -0
- package/dist/src/gate.d.ts.map +1 -1
- package/dist/src/gate.js +36 -2
- package/dist/src/gate.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lock/check.d.ts +143 -0
- package/dist/src/lock/check.d.ts.map +1 -0
- package/dist/src/lock/check.js +274 -0
- package/dist/src/lock/check.js.map +1 -0
- package/dist/src/lock/digest.d.ts +48 -0
- package/dist/src/lock/digest.d.ts.map +1 -0
- package/dist/src/lock/digest.js +234 -0
- package/dist/src/lock/digest.js.map +1 -0
- package/dist/src/lock/events.d.ts +82 -0
- package/dist/src/lock/events.d.ts.map +1 -0
- package/dist/src/lock/events.js +80 -0
- package/dist/src/lock/events.js.map +1 -0
- package/dist/src/lock/index.d.ts +20 -0
- package/dist/src/lock/index.d.ts.map +1 -0
- package/dist/src/lock/index.js +14 -0
- package/dist/src/lock/index.js.map +1 -0
- package/dist/src/lock/lock.d.ts +195 -0
- package/dist/src/lock/lock.d.ts.map +1 -0
- package/dist/src/lock/lock.js +1075 -0
- package/dist/src/lock/lock.js.map +1 -0
- package/dist/src/lock/pg.d.ts +150 -0
- package/dist/src/lock/pg.d.ts.map +1 -0
- package/dist/src/lock/pg.js +539 -0
- package/dist/src/lock/pg.js.map +1 -0
- package/dist/src/lock/rawList.d.ts +127 -0
- package/dist/src/lock/rawList.d.ts.map +1 -0
- package/dist/src/lock/rawList.js +561 -0
- package/dist/src/lock/rawList.js.map +1 -0
- package/dist/src/lock/signing.d.ts +35 -0
- package/dist/src/lock/signing.d.ts.map +1 -0
- package/dist/src/lock/signing.js +85 -0
- package/dist/src/lock/signing.js.map +1 -0
- package/dist/src/lock/store.d.ts +149 -0
- package/dist/src/lock/store.d.ts.map +1 -0
- package/dist/src/lock/store.js +459 -0
- package/dist/src/lock/store.js.map +1 -0
- package/dist/src/schemaDiff.d.ts +21 -2
- package/dist/src/schemaDiff.d.ts.map +1 -1
- package/dist/src/schemaDiff.js +79 -1
- package/dist/src/schemaDiff.js.map +1 -1
- 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
|
-
## [
|
|
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:
|
package/dist/src/ai.d.ts
ADDED
|
@@ -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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"}
|
package/dist/src/gate.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAGL,YAAY,EAIb,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EACL,UAAU,
|
|
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
|
-
|
|
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({
|