@orangecheck/agent-mcp 0.4.2 → 0.5.0

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @orangecheck/agent-mcp
2
2
 
3
- > **Full reference:** [docs.ochk.io/sdk/agent-mcp](https://docs.ochk.io/sdk/agent-mcp) — auto-generated from the TypeScript source on every release.
3
+ > **Full reference:** [ochk.io/docs/sdk/agent-mcp](https://ochk.io/docs/sdk/agent-mcp) — auto-generated from the TypeScript source on every release.
4
4
  > Hand-written prose below is the high-level overview; the docs site is the source of truth for every export, type, and signature.
5
5
 
6
6
 
@@ -28,31 +28,38 @@ npm i @orangecheck/agent-core @orangecheck/agent-signer
28
28
  ## Quickstart
29
29
 
30
30
  ```ts
31
- import { invokeWithStamp } from '@orangecheck/agent-mcp';
31
+ import { ACTION_META_KEY, invokeWithStamp } from '@orangecheck/agent-mcp';
32
32
 
33
33
  // You already have an agent SignerRef and a DelegationEnvelope from
34
34
  // @orangecheck/agent-signer. The delegation grants something like
35
- // mcp:invoke(server=https://mcp.example.com,tool=search)
35
+ // mcp:invoke(server=https://mcp.example.com/mcp,tool=search)
36
36
 
37
37
  const { result, action } = await invokeWithStamp({
38
38
  agent,
39
39
  delegation,
40
40
  invocation: {
41
- server: 'https://mcp.example.com',
41
+ server: 'https://mcp.example.com/mcp',
42
42
  tool: 'search',
43
43
  arguments: { query: 'bitcoin identity', limit: 10 },
44
44
  },
45
- call: async (inv) => myMcpClient.invoke(inv.server, inv.tool, inv.arguments),
45
+ // The action rides in the request, so the server verifies it first.
46
+ call: async (inv, action) =>
47
+ myMcpClient.request('tools/call', {
48
+ name: inv.tool,
49
+ arguments: inv.arguments,
50
+ _meta: { [ACTION_META_KEY]: action },
51
+ }),
46
52
  });
47
-
48
- // `result` is whatever the MCP server returned.
49
- // `action` is the signed envelope; ship it alongside the result, publish to
50
- // Nostr kind-30084, anchor to OpenTimestamps, etc.
51
53
  ```
52
54
 
55
+ `ACTION_META_KEY` is `io.ochk/agent-action`. MCP reserves `_meta` for metadata
56
+ and allows prefixed keys, so a server that does not check actions ignores it.
57
+ `server` is the endpoint URL exactly as the delegation's scope names it: case,
58
+ path and a trailing slash all count.
59
+
53
60
  ## What gets stamped
54
61
 
55
- The stamp's `content.hash` is a SHA-256 of the RFC-8785-canonicalized JSON:
62
+ The stamp's `content.hash` is a SHA-256 of the RFC-8785-canonicalized JSON, followed by one LF (0x0A):
56
63
 
57
64
  ```json
58
65
  {
@@ -82,62 +89,64 @@ Produces a signed `agent-action` without calling the tool. Useful for pre-author
82
89
 
83
90
  ### `invokeWithStamp(input) -> Promise<{ result, action }>`
84
91
 
85
- Stamps the invocation first, then calls the tool. The stamp is returned alongside the tool's result — ship them together.
86
-
87
- Rationale for stamping-before-calling: if the downstream call hangs or fails, there is still an on-record commitment that this agent attempted this specific invocation. The server-side verifier sees a stamped intent plus the server's own transcript.
92
+ Stamps the invocation first, then calls `call(invocation, action)`, so the request can carry the action. The action is also returned with the result for your own record.
88
93
 
89
94
  ## Server-side verification
90
95
 
91
- A server receiving a stamped invocation verifies it with `@orangecheck/agent-core`:
96
+ A server checks the action before it runs the tool, with `@orangecheck/agent-core` 2.7 or later:
92
97
 
93
98
  ```ts
94
- import { verifyAction } from '@orangecheck/agent-core';
99
+ import { createReplayGuard, isSubScope, parseScope, verifyAction } from '@orangecheck/agent-core';
100
+ import { ACTION_META_KEY, canonicalizeInvocation } from '@orangecheck/agent-mcp';
95
101
  import { verifySignature } from '@orangecheck/bip322';
96
102
 
97
- // `revocations` is required: the kind-30085 envelopes you fetched with
98
- // "#t": [delegation.id] (see @orangecheck/agent-core's README), or
99
- // `skipRevocationCheck: true` to state that you are not checking.
103
+ const seen = createReplayGuard(); // one process; across instances, the same record in a shared store
104
+
105
+ const action = params._meta?.[ACTION_META_KEY];
106
+ // `revocations`: the kind-30085 envelopes you fetched with "#t": [delegation.id].
107
+ // If no relay answered, you have not checked: refuse the call.
100
108
  const r = await verifyAction({
101
109
  action,
102
110
  delegation,
103
111
  revocations,
104
112
  verifyBip322: async (m, s, a) => verifySignature(a, m, s),
113
+ // Binds the action to this exact request.
114
+ content: canonicalizeInvocation({ server: MY_ENDPOINT, tool: params.name, arguments: params.arguments }),
105
115
  });
106
- if (!r.ok) throw new Error(`auth failed: ${r.code}`);
107
-
108
- // Also re-hash the incoming (server, tool, arguments) to confirm the stamp
109
- // covers exactly this request:
110
- import { invocationHash } from '@orangecheck/agent-mcp';
111
- const expectedHash = invocationHash({
112
- server: myUrl,
113
- tool: req.tool,
114
- arguments: req.arguments,
115
- });
116
- if (expectedHash !== action.content.hash) {
117
- throw new Error('stamp does not cover this request');
118
- }
116
+ if (!r.ok) throw new Error(`refused: ${r.code}`);
117
+
118
+ // The exercised scope must name this server and this tool, not just some grant.
119
+ const here = parseScope(`mcp:invoke(server=${MY_ENDPOINT},tool=${params.name})`);
120
+ if (!isSubScope(parseScope(action.scope_exercised), here)) throw new Error('refused: E_SCOPE_DENIED');
121
+
122
+ // One signed action runs once.
123
+ if (!seen.firstUse(action)) throw new Error('refused: already used');
119
124
  ```
120
125
 
126
+ `verifyAction` checks as of now: the delegation in force now, any revocation already in effect, and `signed_at` within 120 s of your clock (`E_STALE` otherwise).
127
+
121
128
  ## Composing with OpenTimestamps
122
129
 
123
130
  For invocations where priority-against-revocation matters:
124
131
 
125
132
  ```ts
126
- import { stampCommit } from '@orangecheck/stamp-ots';
133
+ import { submitToCalendars, toStampOts } from '@orangecheck/stamp-ots';
127
134
 
128
135
  const { action } = await invokeWithStamp({ /* … */ });
129
- action.ots = await stampCommit(action.id);
136
+ // Pending until a Bitcoin block confirms it.
137
+ action.ots = toStampOts(await submitToCalendars(action.id));
130
138
  ```
131
139
 
132
140
  ## Composing with Nostr
133
141
 
134
- Publish the action for public auditability (OC Agent reuses OC Stamp's kind-30084 transport, disambiguated by envelope `kind`):
142
+ Publish the action for public auditability on kind 30084, OC Agent's action kind. The `t` tag carries the delegation id, so anyone can list a delegation's actions with `{ kinds: [30084], '#t': [delegation.id] }` (oc-agent-protocol SPEC §10.3). Tags sit on the Nostr event, outside the signed envelope, so they change no id:
135
143
 
136
144
  ```ts
137
145
  await nostr.publish({
138
146
  kind: 30084,
139
147
  tags: [
140
148
  ['d', 'oc-agent-act:' + action.id],
149
+ ['t', action.delegation_id],
141
150
  ['kind', 'agent-action'],
142
151
  ['delegation', action.delegation_id],
143
152
  ['agent', action.signer.address],
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DelegationEnvelope } from '@orangecheck/agent-core';
2
2
  import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
3
3
 
4
+ declare const ACTION_META_KEY = "io.ochk/agent-action";
4
5
  interface McpInvocation {
5
6
  server: string;
6
7
  tool: string;
@@ -18,7 +19,7 @@ interface StampInvocationInput {
18
19
  }
19
20
  declare function stampInvocation(input: StampInvocationInput): Promise<ActionEnvelope>;
20
21
  interface InvokeWithStampInput<T> extends StampInvocationInput {
21
- call: (inv: McpInvocation) => Promise<T>;
22
+ call: (inv: McpInvocation, action: ActionEnvelope) => Promise<T>;
22
23
  }
23
24
  interface InvokeWithStampResult<T> {
24
25
  result: T;
@@ -26,4 +27,4 @@ interface InvokeWithStampResult<T> {
26
27
  }
27
28
  declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
28
29
 
29
- export { type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
30
+ export { ACTION_META_KEY, type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DelegationEnvelope } from '@orangecheck/agent-core';
2
2
  import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
3
3
 
4
+ declare const ACTION_META_KEY = "io.ochk/agent-action";
4
5
  interface McpInvocation {
5
6
  server: string;
6
7
  tool: string;
@@ -18,7 +19,7 @@ interface StampInvocationInput {
18
19
  }
19
20
  declare function stampInvocation(input: StampInvocationInput): Promise<ActionEnvelope>;
20
21
  interface InvokeWithStampInput<T> extends StampInvocationInput {
21
- call: (inv: McpInvocation) => Promise<T>;
22
+ call: (inv: McpInvocation, action: ActionEnvelope) => Promise<T>;
22
23
  }
23
24
  interface InvokeWithStampResult<T> {
24
25
  result: T;
@@ -26,4 +27,4 @@ interface InvokeWithStampResult<T> {
26
27
  }
27
28
  declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
28
29
 
29
- export { type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
30
+ export { ACTION_META_KEY, type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var agentCore = require('@orangecheck/agent-core');
5
5
  var agentSigner = require('@orangecheck/agent-signer');
6
6
 
7
7
  // src/index.ts
8
+ var ACTION_META_KEY = "io.ochk/agent-action";
8
9
  function canonicalizeInvocation(inv) {
9
10
  const canon = {
10
11
  arguments: inv.arguments,
@@ -43,10 +44,11 @@ async function stampInvocation(input) {
43
44
  }
44
45
  async function invokeWithStamp(input) {
45
46
  const action = await stampInvocation(input);
46
- const result = await input.call(input.invocation);
47
+ const result = await input.call(input.invocation, action);
47
48
  return { result, action };
48
49
  }
49
50
 
51
+ exports.ACTION_META_KEY = ACTION_META_KEY;
50
52
  exports.canonicalizeInvocation = canonicalizeInvocation;
51
53
  exports.invocationHash = invocationHash;
52
54
  exports.invocationId = invocationId;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","assertScopeGranted","signAsAgent"],"mappings":";;;;;;;AAoDO,SAAS,uBAAuB,GAAA,EAAgC;AACnE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,WAAW,GAAA,CAAI,SAAA;AAAA,IACf,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,IAAA,EAAK;AAAA,IACxB,MAAM,GAAA,CAAI;AAAA,GACd;AACA,EAAA,MAAM,GAAA,GAAMA,uBAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,eAAe,GAAA,EAA4B;AACvD,EAAA,OAAO,YAAYC,mBAAA,CAAUC,aAAA,CAAO,sBAAA,CAAuB,GAAG,CAAC,CAAC,CAAA;AACpE;AAEO,SAAS,YAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,GAAA,EACM;AACN,EAAA,OAAOC,yBAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,eAAe,GAAG,CAAA;AAAA,IAChC,cAAA,EAAgB,sBAAA,CAAuB,GAAG,CAAA,CAAE,UAAA;AAAA,IAC5C,YAAA,EAAc,8CAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAoBA,eAAsB,gBAAgB,KAAA,EAAsD;AACxF,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IACN,CAAA,kBAAA,EAAqB,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAA,EAAM,CAAA,MAAA,EAAS,KAAA,CAAM,UAAA,CAAW,IAAI,CAAA,CAAA,CAAA;AAErF,EAAAC,4BAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,iBAAiB,CAAA;AAEtE,EAAA,MAAM,KAAA,GAAQ,sBAAA,CAAuB,KAAA,CAAM,UAAU,CAAA;AACrD,EAAA,OAAOC,uBAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,cAAA,CAAe,MAAM,UAAU,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IAC5E,IAAA,EAAM,8CAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAyBA,eAAsB,gBAAmB,KAAA,EAAmE;AACxG,EAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,KAAK,CAAA;AAC1C,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,IAAA,CAAK,MAAM,UAAU,CAAA;AAChD,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B","file":"index.js","sourcesContent":["// @orangecheck/agent-mcp — wrap MCP tool invocations in OC Agent envelopes.\n//\n// An MCP client calls tools on an MCP server: { server, name, arguments }.\n// This module stamps each invocation as an `agent-action` whose `content.hash`\n// is a SHA-256 of the canonicalized (server, name, arguments) tuple, scoped\n// as `mcp:invoke(server=<url>,tool=<name>)`. The server — or any counterparty\n// — verifies the action before trusting the call.\n//\n// The wrapper is transport-agnostic. Wire it into whatever MCP client you use\n// by calling `invokeWithStamp()` around the raw call.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n computeActionId,\n hexEncode,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization of an MCP invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface McpInvocation {\n /** Origin URL of the MCP server. Trimmed and lowercased for canonicalization. */\n server: string;\n /** Tool name (method) being invoked. */\n tool: string;\n /** JSON-serializable arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for an MCP invocation. The bytes are what the\n * action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape:\n * {\n * \"arguments\": <canonical JSON of args>,\n * \"server\": <server url>,\n * \"tool\": <tool name>\n * }\n *\n * The shape is intentionally minimal so verifiers can reconstruct it without\n * knowing anything server- or tool-specific.\n */\nexport function canonicalizeInvocation(inv: McpInvocation): Uint8Array {\n const canon = {\n arguments: inv.arguments,\n server: inv.server.trim(),\n tool: inv.tool,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function invocationHash(inv: McpInvocation): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeInvocation(inv)));\n}\n\nexport function invocationId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n inv: McpInvocation\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: invocationHash(inv),\n content_length: canonicalizeInvocation(inv).byteLength,\n content_mime: 'application/vnd.oc-agent.mcp-invocation+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamped invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampInvocationInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n invocation: McpInvocation;\n /** Defaults to `mcp:invoke(server=<server>,tool=<tool>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\n/**\n * Stamp an MCP invocation without actually calling it. Useful when the caller\n * wants the signed envelope for out-of-band auditing before (or independent of)\n * running the tool.\n */\nexport async function stampInvocation(input: StampInvocationInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ??\n `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampInvocation');\n\n const bytes = canonicalizeInvocation(input.invocation);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation,\n content: { hash: invocationHash(input.invocation), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.mcp-invocation+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// invokeWithStamp — call the tool AND produce the stamp around it\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface InvokeWithStampInput<T> extends StampInvocationInput {\n /** The underlying transport call. Returns whatever the MCP server returns. */\n call: (inv: McpInvocation) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n}\n\n/**\n * Stamp the invocation *first*, then execute the call. The stamped envelope\n * is returned alongside the result so the caller can ship them together.\n *\n * Rationale for stamping first: if the downstream call hangs or fails, we\n * still have an on-record commitment that this agent attempted this specific\n * invocation. The server-side verifier sees a stamped intent plus the server's\n * own transcript.\n */\nexport async function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>> {\n const action = await stampInvocation(input);\n const result = await input.call(input.invocation);\n return { result, action };\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","assertScopeGranted","signAsAgent"],"mappings":";;;;;;;AA+BO,IAAM,eAAA,GAAkB;AAkCxB,SAAS,uBAAuB,GAAA,EAAgC;AACnE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,WAAW,GAAA,CAAI,SAAA;AAAA,IACf,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,IAAA,EAAK;AAAA,IACxB,MAAM,GAAA,CAAI;AAAA,GACd;AACA,EAAA,MAAM,GAAA,GAAMA,uBAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,eAAe,GAAA,EAA4B;AACvD,EAAA,OAAO,YAAYC,mBAAA,CAAUC,aAAA,CAAO,sBAAA,CAAuB,GAAG,CAAC,CAAC,CAAA;AACpE;AAEO,SAAS,YAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,GAAA,EACM;AACN,EAAA,OAAOC,yBAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,eAAe,GAAG,CAAA;AAAA,IAChC,cAAA,EAAgB,sBAAA,CAAuB,GAAG,CAAA,CAAE,UAAA;AAAA,IAC5C,YAAA,EAAc,8CAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAoBA,eAAsB,gBAAgB,KAAA,EAAsD;AACxF,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IACN,CAAA,kBAAA,EAAqB,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAA,EAAM,CAAA,MAAA,EAAS,KAAA,CAAM,UAAA,CAAW,IAAI,CAAA,CAAA,CAAA;AAErF,EAAAC,4BAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,iBAAiB,CAAA;AAEtE,EAAA,MAAM,KAAA,GAAQ,sBAAA,CAAuB,KAAA,CAAM,UAAU,CAAA;AACrD,EAAA,OAAOC,uBAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,cAAA,CAAe,MAAM,UAAU,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IAC5E,IAAA,EAAM,8CAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AA4BA,eAAsB,gBAAmB,KAAA,EAAmE;AACxG,EAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,KAAK,CAAA;AAC1C,EAAA,MAAM,SAAS,MAAM,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,YAAY,MAAM,CAAA;AACxD,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B","file":"index.js","sourcesContent":["// @orangecheck/agent-mcp — wrap MCP tool invocations in OC Agent envelopes.\n//\n// An MCP client calls tools on an MCP server: { server, name, arguments }.\n// This module stamps each invocation as an `agent-action` whose `content.hash`\n// is a SHA-256 of the canonicalized (server, name, arguments) tuple, scoped\n// as `mcp:invoke(server=<url>,tool=<name>)`. The server — or any counterparty\n// — verifies the action before trusting the call.\n//\n// The wrapper is transport-agnostic. Wire it into whatever MCP client you use\n// by calling `invokeWithStamp()` around the raw call, and send the action with\n// the request under `params._meta[ACTION_META_KEY]`.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n computeActionId,\n hexEncode,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n/**\n * Where an MCP request carries its action: `params._meta[ACTION_META_KEY]` on\n * the `tools/call` request. MCP reserves `_meta` for metadata and allows\n * prefixed keys there, so a server that does not check actions ignores it.\n */\nexport const ACTION_META_KEY = 'io.ochk/agent-action';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization of an MCP invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface McpInvocation {\n /**\n * The MCP server's endpoint URL, exactly as the delegation's\n * `mcp:invoke(server=…)` scope names it. Only surrounding whitespace is\n * trimmed: case, path and a trailing slash all count.\n */\n server: string;\n /** Tool name (method) being invoked. */\n tool: string;\n /** JSON-serializable arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for an MCP invocation. The bytes are what the\n * action envelope's `content.hash` is a SHA-256 of: RFC 8785 JSON of the\n * object below, followed by one LF (0x0A).\n *\n * Shape:\n * {\n * \"arguments\": <canonical JSON of args>,\n * \"server\": <server url>,\n * \"tool\": <tool name>\n * }\n *\n * The shape is intentionally minimal so verifiers can reconstruct it without\n * knowing anything server- or tool-specific.\n */\nexport function canonicalizeInvocation(inv: McpInvocation): Uint8Array {\n const canon = {\n arguments: inv.arguments,\n server: inv.server.trim(),\n tool: inv.tool,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function invocationHash(inv: McpInvocation): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeInvocation(inv)));\n}\n\nexport function invocationId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n inv: McpInvocation\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: invocationHash(inv),\n content_length: canonicalizeInvocation(inv).byteLength,\n content_mime: 'application/vnd.oc-agent.mcp-invocation+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamped invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampInvocationInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n invocation: McpInvocation;\n /** Defaults to `mcp:invoke(server=<server>,tool=<tool>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\n/**\n * Stamp an MCP invocation without actually calling it. Useful when the caller\n * wants the signed envelope for out-of-band auditing before (or independent of)\n * running the tool.\n */\nexport async function stampInvocation(input: StampInvocationInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ??\n `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampInvocation');\n\n const bytes = canonicalizeInvocation(input.invocation);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation,\n content: { hash: invocationHash(input.invocation), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.mcp-invocation+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// invokeWithStamp — call the tool AND produce the stamp around it\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface InvokeWithStampInput<T> extends StampInvocationInput {\n /**\n * The underlying transport call. It gets the signed action: send it with\n * the request, under `params._meta[ACTION_META_KEY]`, so the server can\n * verify it before it runs the tool.\n */\n call: (inv: McpInvocation, action: ActionEnvelope) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n}\n\n/**\n * Stamp the invocation *first*, then make the call with the action in hand,\n * so the request carries it and the server verifies before it acts. The\n * action is also returned with the result for the caller's own record.\n *\n * A server checks it live (agent-core `verifyAction`): the delegation in force\n * now, the action signed just now, and an id it has not seen before.\n */\nexport async function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>> {\n const action = await stampInvocation(input);\n const result = await input.call(input.invocation, action);\n return { result, action };\n}\n"]}
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { canonicalize, hexEncode, computeActionId, assertScopeGranted } from '@o
3
3
  import { signAsAgent } from '@orangecheck/agent-signer';
4
4
 
5
5
  // src/index.ts
6
+ var ACTION_META_KEY = "io.ochk/agent-action";
6
7
  function canonicalizeInvocation(inv) {
7
8
  const canon = {
8
9
  arguments: inv.arguments,
@@ -41,10 +42,10 @@ async function stampInvocation(input) {
41
42
  }
42
43
  async function invokeWithStamp(input) {
43
44
  const action = await stampInvocation(input);
44
- const result = await input.call(input.invocation);
45
+ const result = await input.call(input.invocation, action);
45
46
  return { result, action };
46
47
  }
47
48
 
48
- export { canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
49
+ export { ACTION_META_KEY, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
49
50
  //# sourceMappingURL=index.mjs.map
50
51
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAoDO,SAAS,uBAAuB,GAAA,EAAgC;AACnE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,WAAW,GAAA,CAAI,SAAA;AAAA,IACf,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,IAAA,EAAK;AAAA,IACxB,MAAM,GAAA,CAAI;AAAA,GACd;AACA,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,eAAe,GAAA,EAA4B;AACvD,EAAA,OAAO,YAAY,SAAA,CAAU,MAAA,CAAO,sBAAA,CAAuB,GAAG,CAAC,CAAC,CAAA;AACpE;AAEO,SAAS,YAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,GAAA,EACM;AACN,EAAA,OAAO,eAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,eAAe,GAAG,CAAA;AAAA,IAChC,cAAA,EAAgB,sBAAA,CAAuB,GAAG,CAAA,CAAE,UAAA;AAAA,IAC5C,YAAA,EAAc,8CAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAoBA,eAAsB,gBAAgB,KAAA,EAAsD;AACxF,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IACN,CAAA,kBAAA,EAAqB,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAA,EAAM,CAAA,MAAA,EAAS,KAAA,CAAM,UAAA,CAAW,IAAI,CAAA,CAAA,CAAA;AAErF,EAAA,kBAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,iBAAiB,CAAA;AAEtE,EAAA,MAAM,KAAA,GAAQ,sBAAA,CAAuB,KAAA,CAAM,UAAU,CAAA;AACrD,EAAA,OAAO,WAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,cAAA,CAAe,MAAM,UAAU,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IAC5E,IAAA,EAAM,8CAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AAyBA,eAAsB,gBAAmB,KAAA,EAAmE;AACxG,EAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,KAAK,CAAA;AAC1C,EAAA,MAAM,MAAA,GAAS,MAAM,KAAA,CAAM,IAAA,CAAK,MAAM,UAAU,CAAA;AAChD,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B","file":"index.mjs","sourcesContent":["// @orangecheck/agent-mcp — wrap MCP tool invocations in OC Agent envelopes.\n//\n// An MCP client calls tools on an MCP server: { server, name, arguments }.\n// This module stamps each invocation as an `agent-action` whose `content.hash`\n// is a SHA-256 of the canonicalized (server, name, arguments) tuple, scoped\n// as `mcp:invoke(server=<url>,tool=<name>)`. The server — or any counterparty\n// — verifies the action before trusting the call.\n//\n// The wrapper is transport-agnostic. Wire it into whatever MCP client you use\n// by calling `invokeWithStamp()` around the raw call.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n computeActionId,\n hexEncode,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization of an MCP invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface McpInvocation {\n /** Origin URL of the MCP server. Trimmed and lowercased for canonicalization. */\n server: string;\n /** Tool name (method) being invoked. */\n tool: string;\n /** JSON-serializable arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for an MCP invocation. The bytes are what the\n * action envelope's `content.hash` is a SHA-256 of.\n *\n * Shape:\n * {\n * \"arguments\": <canonical JSON of args>,\n * \"server\": <server url>,\n * \"tool\": <tool name>\n * }\n *\n * The shape is intentionally minimal so verifiers can reconstruct it without\n * knowing anything server- or tool-specific.\n */\nexport function canonicalizeInvocation(inv: McpInvocation): Uint8Array {\n const canon = {\n arguments: inv.arguments,\n server: inv.server.trim(),\n tool: inv.tool,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function invocationHash(inv: McpInvocation): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeInvocation(inv)));\n}\n\nexport function invocationId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n inv: McpInvocation\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: invocationHash(inv),\n content_length: canonicalizeInvocation(inv).byteLength,\n content_mime: 'application/vnd.oc-agent.mcp-invocation+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamped invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampInvocationInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n invocation: McpInvocation;\n /** Defaults to `mcp:invoke(server=<server>,tool=<tool>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\n/**\n * Stamp an MCP invocation without actually calling it. Useful when the caller\n * wants the signed envelope for out-of-band auditing before (or independent of)\n * running the tool.\n */\nexport async function stampInvocation(input: StampInvocationInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ??\n `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampInvocation');\n\n const bytes = canonicalizeInvocation(input.invocation);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation,\n content: { hash: invocationHash(input.invocation), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.mcp-invocation+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// invokeWithStamp — call the tool AND produce the stamp around it\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface InvokeWithStampInput<T> extends StampInvocationInput {\n /** The underlying transport call. Returns whatever the MCP server returns. */\n call: (inv: McpInvocation) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n}\n\n/**\n * Stamp the invocation *first*, then execute the call. The stamped envelope\n * is returned alongside the result so the caller can ship them together.\n *\n * Rationale for stamping first: if the downstream call hangs or fails, we\n * still have an on-record commitment that this agent attempted this specific\n * invocation. The server-side verifier sees a stamped intent plus the server's\n * own transcript.\n */\nexport async function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>> {\n const action = await stampInvocation(input);\n const result = await input.call(input.invocation);\n return { result, action };\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AA+BO,IAAM,eAAA,GAAkB;AAkCxB,SAAS,uBAAuB,GAAA,EAAgC;AACnE,EAAA,MAAM,KAAA,GAAQ;AAAA,IACV,WAAW,GAAA,CAAI,SAAA;AAAA,IACf,MAAA,EAAQ,GAAA,CAAI,MAAA,CAAO,IAAA,EAAK;AAAA,IACxB,MAAM,GAAA,CAAI;AAAA,GACd;AACA,EAAA,MAAM,GAAA,GAAM,aAAa,KAAsD,CAAA;AAC/E,EAAA,OAAO,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,MAAM,IAAI,CAAA;AAC9C;AAEO,SAAS,eAAe,GAAA,EAA4B;AACvD,EAAA,OAAO,YAAY,SAAA,CAAU,MAAA,CAAO,sBAAA,CAAuB,GAAG,CAAC,CAAC,CAAA;AACpE;AAEO,SAAS,YAAA,CACZ,YAAA,EACA,QAAA,EACA,YAAA,EACA,gBACA,GAAA,EACM;AACN,EAAA,OAAO,eAAA,CAAgB;AAAA,IACnB,OAAA,EAAS,YAAA;AAAA,IACT,YAAA,EAAc,eAAe,GAAG,CAAA;AAAA,IAChC,cAAA,EAAgB,sBAAA,CAAuB,GAAG,CAAA,CAAE,UAAA;AAAA,IAC5C,YAAA,EAAc,8CAAA;AAAA,IACd,SAAA,EAAW,QAAA;AAAA,IACX,aAAA,EAAe,YAAA;AAAA,IACf,eAAA,EAAiB;AAAA,GACpB,CAAA;AACL;AAoBA,eAAsB,gBAAgB,KAAA,EAAsD;AACxF,EAAA,MAAM,cAAA,GACF,KAAA,CAAM,cAAA,IACN,CAAA,kBAAA,EAAqB,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAA,EAAM,CAAA,MAAA,EAAS,KAAA,CAAM,UAAA,CAAW,IAAI,CAAA,CAAA,CAAA;AAErF,EAAA,kBAAA,CAAmB,KAAA,CAAM,UAAA,EAAY,cAAA,EAAgB,iBAAiB,CAAA;AAEtE,EAAA,MAAM,KAAA,GAAQ,sBAAA,CAAuB,KAAA,CAAM,UAAU,CAAA;AACrD,EAAA,OAAO,WAAA,CAAY;AAAA,IACf,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,YAAY,KAAA,CAAM,UAAA;AAAA,IAClB,OAAA,EAAS,EAAE,IAAA,EAAM,cAAA,CAAe,MAAM,UAAU,CAAA,EAAG,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAW;AAAA,IAC5E,IAAA,EAAM,8CAAA;AAAA,IACN,cAAA;AAAA,IACA,UAAU,KAAA,CAAM;AAAA,GACnB,CAAA;AACL;AA4BA,eAAsB,gBAAmB,KAAA,EAAmE;AACxG,EAAA,MAAM,MAAA,GAAS,MAAM,eAAA,CAAgB,KAAK,CAAA;AAC1C,EAAA,MAAM,SAAS,MAAM,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,YAAY,MAAM,CAAA;AACxD,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B","file":"index.mjs","sourcesContent":["// @orangecheck/agent-mcp — wrap MCP tool invocations in OC Agent envelopes.\n//\n// An MCP client calls tools on an MCP server: { server, name, arguments }.\n// This module stamps each invocation as an `agent-action` whose `content.hash`\n// is a SHA-256 of the canonicalized (server, name, arguments) tuple, scoped\n// as `mcp:invoke(server=<url>,tool=<name>)`. The server — or any counterparty\n// — verifies the action before trusting the call.\n//\n// The wrapper is transport-agnostic. Wire it into whatever MCP client you use\n// by calling `invokeWithStamp()` around the raw call, and send the action with\n// the request under `params._meta[ACTION_META_KEY]`.\n\nimport { sha256 } from '@noble/hashes/sha256';\nimport {\n assertScopeGranted,\n canonicalize,\n computeActionId,\n hexEncode,\n type DelegationEnvelope,\n} from '@orangecheck/agent-core';\nimport {\n signAsAgent,\n type ActionEnvelope,\n type SignerRef,\n} from '@orangecheck/agent-signer';\n\n/**\n * Where an MCP request carries its action: `params._meta[ACTION_META_KEY]` on\n * the `tools/call` request. MCP reserves `_meta` for metadata and allows\n * prefixed keys there, so a server that does not check actions ignores it.\n */\nexport const ACTION_META_KEY = 'io.ochk/agent-action';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Canonicalization of an MCP invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface McpInvocation {\n /**\n * The MCP server's endpoint URL, exactly as the delegation's\n * `mcp:invoke(server=…)` scope names it. Only surrounding whitespace is\n * trimmed: case, path and a trailing slash all count.\n */\n server: string;\n /** Tool name (method) being invoked. */\n tool: string;\n /** JSON-serializable arguments. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * Produce the canonical bytes for an MCP invocation. The bytes are what the\n * action envelope's `content.hash` is a SHA-256 of: RFC 8785 JSON of the\n * object below, followed by one LF (0x0A).\n *\n * Shape:\n * {\n * \"arguments\": <canonical JSON of args>,\n * \"server\": <server url>,\n * \"tool\": <tool name>\n * }\n *\n * The shape is intentionally minimal so verifiers can reconstruct it without\n * knowing anything server- or tool-specific.\n */\nexport function canonicalizeInvocation(inv: McpInvocation): Uint8Array {\n const canon = {\n arguments: inv.arguments,\n server: inv.server.trim(),\n tool: inv.tool,\n };\n const str = canonicalize(canon as unknown as Parameters<typeof canonicalize>[0]);\n return new TextEncoder().encode(str + '\\n');\n}\n\nexport function invocationHash(inv: McpInvocation): string {\n return 'sha256:' + hexEncode(sha256(canonicalizeInvocation(inv)));\n}\n\nexport function invocationId(\n agentAddress: string,\n signedAt: string,\n delegationId: string,\n scopeExercised: string,\n inv: McpInvocation\n): string {\n return computeActionId({\n address: agentAddress,\n content_hash: invocationHash(inv),\n content_length: canonicalizeInvocation(inv).byteLength,\n content_mime: 'application/vnd.oc-agent.mcp-invocation+json',\n signed_at: signedAt,\n delegation_id: delegationId,\n scope_exercised: scopeExercised,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Stamped invocation\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface StampInvocationInput {\n agent: SignerRef;\n delegation: DelegationEnvelope;\n invocation: McpInvocation;\n /** Defaults to `mcp:invoke(server=<server>,tool=<tool>)` — the tightest admissible sub-scope. */\n scopeExercised?: string;\n signedAt?: Date;\n}\n\n/**\n * Stamp an MCP invocation without actually calling it. Useful when the caller\n * wants the signed envelope for out-of-band auditing before (or independent of)\n * running the tool.\n */\nexport async function stampInvocation(input: StampInvocationInput): Promise<ActionEnvelope> {\n const scopeExercised =\n input.scopeExercised ??\n `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;\n\n assertScopeGranted(input.delegation, scopeExercised, 'stampInvocation');\n\n const bytes = canonicalizeInvocation(input.invocation);\n return signAsAgent({\n agent: input.agent,\n delegation: input.delegation,\n content: { hash: invocationHash(input.invocation), length: bytes.byteLength },\n mime: 'application/vnd.oc-agent.mcp-invocation+json',\n scopeExercised,\n signedAt: input.signedAt,\n });\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// invokeWithStamp — call the tool AND produce the stamp around it\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface InvokeWithStampInput<T> extends StampInvocationInput {\n /**\n * The underlying transport call. It gets the signed action: send it with\n * the request, under `params._meta[ACTION_META_KEY]`, so the server can\n * verify it before it runs the tool.\n */\n call: (inv: McpInvocation, action: ActionEnvelope) => Promise<T>;\n}\n\nexport interface InvokeWithStampResult<T> {\n result: T;\n action: ActionEnvelope;\n}\n\n/**\n * Stamp the invocation *first*, then make the call with the action in hand,\n * so the request carries it and the server verifies before it acts. The\n * action is also returned with the result for the caller's own record.\n *\n * A server checks it live (agent-core `verifyAction`): the delegation in force\n * now, the action signed just now, and an id it has not seen before.\n */\nexport async function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>> {\n const action = await stampInvocation(input);\n const result = await input.call(input.invocation, action);\n return { result, action };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecheck/agent-mcp",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Wrap Model Context Protocol tool invocations in OC Agent envelopes — every call produces a signed, scope-constrained, verifiable agent-action stamp.",
5
5
  "keywords": [
6
6
  "bitcoin",
package/src/index.test.ts CHANGED
@@ -3,7 +3,13 @@ import { describe, expect, it } from 'vitest';
3
3
  import { verifyAction } from '@orangecheck/agent-core';
4
4
  import { createDelegation } from '@orangecheck/agent-signer';
5
5
 
6
- import { canonicalizeInvocation, invocationHash, invokeWithStamp, stampInvocation } from './index.js';
6
+ import {
7
+ ACTION_META_KEY,
8
+ canonicalizeInvocation,
9
+ invocationHash,
10
+ invokeWithStamp,
11
+ stampInvocation,
12
+ } from './index.js';
7
13
 
8
14
  const PRINCIPAL = 'bc1qprincipal000000000000000000000000000000';
9
15
  const AGENT = 'bc1qagent0000000000000000000000000000000000';
@@ -109,4 +115,26 @@ describe('invokeWithStamp', () => {
109
115
  expect(action.kind).toBe('agent-action');
110
116
  expect(action.delegation_id).toBe(delegation.id);
111
117
  });
118
+
119
+ it('hands the action to the call, so the request can carry it', async () => {
120
+ const delegation = await createDelegation({
121
+ principal: { address: PRINCIPAL, signMessage: fakeSign(PRINCIPAL) },
122
+ agentAddress: AGENT,
123
+ scopes: ['mcp:invoke(server=https://mcp.example.com,tool=search)'],
124
+ ttlMs: 60 * 60 * 1000,
125
+ });
126
+ const sent: unknown[] = [];
127
+ const { action } = await invokeWithStamp({
128
+ agent: { address: AGENT, signMessage: fakeSign(AGENT) },
129
+ delegation,
130
+ invocation: { server: 'https://mcp.example.com', tool: 'search', arguments: { q: 'bitcoin' } },
131
+ call: async (inv, signed) => {
132
+ sent.push({ name: inv.tool, arguments: inv.arguments, _meta: { [ACTION_META_KEY]: signed } });
133
+ return null;
134
+ },
135
+ });
136
+ expect(sent).toEqual([
137
+ { name: 'search', arguments: { q: 'bitcoin' }, _meta: { 'io.ochk/agent-action': action } },
138
+ ]);
139
+ });
112
140
  });
package/src/index.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  // — verifies the action before trusting the call.
8
8
  //
9
9
  // The wrapper is transport-agnostic. Wire it into whatever MCP client you use
10
- // by calling `invokeWithStamp()` around the raw call.
10
+ // by calling `invokeWithStamp()` around the raw call, and send the action with
11
+ // the request under `params._meta[ACTION_META_KEY]`.
11
12
 
12
13
  import { sha256 } from '@noble/hashes/sha256';
13
14
  import {
@@ -23,12 +24,23 @@ import {
23
24
  type SignerRef,
24
25
  } from '@orangecheck/agent-signer';
25
26
 
27
+ /**
28
+ * Where an MCP request carries its action: `params._meta[ACTION_META_KEY]` on
29
+ * the `tools/call` request. MCP reserves `_meta` for metadata and allows
30
+ * prefixed keys there, so a server that does not check actions ignores it.
31
+ */
32
+ export const ACTION_META_KEY = 'io.ochk/agent-action';
33
+
26
34
  // ─────────────────────────────────────────────────────────────────────────────
27
35
  // Canonicalization of an MCP invocation
28
36
  // ─────────────────────────────────────────────────────────────────────────────
29
37
 
30
38
  export interface McpInvocation {
31
- /** Origin URL of the MCP server. Trimmed and lowercased for canonicalization. */
39
+ /**
40
+ * The MCP server's endpoint URL, exactly as the delegation's
41
+ * `mcp:invoke(server=…)` scope names it. Only surrounding whitespace is
42
+ * trimmed: case, path and a trailing slash all count.
43
+ */
32
44
  server: string;
33
45
  /** Tool name (method) being invoked. */
34
46
  tool: string;
@@ -38,7 +50,8 @@ export interface McpInvocation {
38
50
 
39
51
  /**
40
52
  * Produce the canonical bytes for an MCP invocation. The bytes are what the
41
- * action envelope's `content.hash` is a SHA-256 of.
53
+ * action envelope's `content.hash` is a SHA-256 of: RFC 8785 JSON of the
54
+ * object below, followed by one LF (0x0A).
42
55
  *
43
56
  * Shape:
44
57
  * {
@@ -123,8 +136,12 @@ export async function stampInvocation(input: StampInvocationInput): Promise<Acti
123
136
  // ─────────────────────────────────────────────────────────────────────────────
124
137
 
125
138
  export interface InvokeWithStampInput<T> extends StampInvocationInput {
126
- /** The underlying transport call. Returns whatever the MCP server returns. */
127
- call: (inv: McpInvocation) => Promise<T>;
139
+ /**
140
+ * The underlying transport call. It gets the signed action: send it with
141
+ * the request, under `params._meta[ACTION_META_KEY]`, so the server can
142
+ * verify it before it runs the tool.
143
+ */
144
+ call: (inv: McpInvocation, action: ActionEnvelope) => Promise<T>;
128
145
  }
129
146
 
130
147
  export interface InvokeWithStampResult<T> {
@@ -133,16 +150,15 @@ export interface InvokeWithStampResult<T> {
133
150
  }
134
151
 
135
152
  /**
136
- * Stamp the invocation *first*, then execute the call. The stamped envelope
137
- * is returned alongside the result so the caller can ship them together.
153
+ * Stamp the invocation *first*, then make the call with the action in hand,
154
+ * so the request carries it and the server verifies before it acts. The
155
+ * action is also returned with the result for the caller's own record.
138
156
  *
139
- * Rationale for stamping first: if the downstream call hangs or fails, we
140
- * still have an on-record commitment that this agent attempted this specific
141
- * invocation. The server-side verifier sees a stamped intent plus the server's
142
- * own transcript.
157
+ * A server checks it live (agent-core `verifyAction`): the delegation in force
158
+ * now, the action signed just now, and an id it has not seen before.
143
159
  */
144
160
  export async function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>> {
145
161
  const action = await stampInvocation(input);
146
- const result = await input.call(input.invocation);
162
+ const result = await input.call(input.invocation, action);
147
163
  return { result, action };
148
164
  }