@orangecheck/agent-mcp 0.1.0 → 0.3.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,5 +1,9 @@
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.
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
+
6
+
3
7
  Wrap [Model Context Protocol](https://modelcontextprotocol.io/) tool invocations in [OC Agent](https://github.com/orangecheck/oc-agent-protocol) envelopes. Every call produces a signed, scope-constrained, verifiable `agent-action` stamp.
4
8
 
5
9
  ## Why
package/dist/index.js CHANGED
@@ -30,13 +30,7 @@ function invocationId(agentAddress, signedAt, delegationId, scopeExercised, inv)
30
30
  }
31
31
  async function stampInvocation(input) {
32
32
  const scopeExercised = input.scopeExercised ?? `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;
33
- const granted = input.delegation.scopes.map(agentCore.parseScope);
34
- const exercisedParsed = agentCore.parseScope(scopeExercised);
35
- if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
36
- throw new Error(
37
- `stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
38
- );
39
- }
33
+ agentCore.assertScopeGranted(input.delegation, scopeExercised, "stampInvocation");
40
34
  const bytes = canonicalizeInvocation(input.invocation);
41
35
  return agentSigner.signAsAgent({
42
36
  agent: input.agent,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent"],"mappings":";;;;;;;AAqDO,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;AAKrF,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAIC,oBAAU,CAAA;AACtD,EAAA,MAAM,eAAA,GAAkBA,qBAAW,cAAc,CAAA;AACjD,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,CAAC,MAAMC,oBAAA,CAAW,eAAA,EAAiB,CAAC,CAAC,CAAA,EAAG;AACtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,qCAAqC,cAAc,CAAA,yCAAA;AAAA,KACvD;AAAA,EACJ;AAEA,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 canonicalize,\n computeActionId,\n hexEncode,\n isSubScope,\n parseScope,\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 // Tiny pre-flight: confirm the exercised scope is a sub-scope of *some*\n // granted scope. This keeps a common integration mistake from producing\n // an envelope that later fails verifier-side.\n const granted = input.delegation.scopes.map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\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":";;;;;;;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"]}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
- import { canonicalize, hexEncode, computeActionId, parseScope, isSubScope } from '@orangecheck/agent-core';
2
+ import { canonicalize, hexEncode, computeActionId, assertScopeGranted } from '@orangecheck/agent-core';
3
3
  import { signAsAgent } from '@orangecheck/agent-signer';
4
4
 
5
5
  // src/index.ts
@@ -28,13 +28,7 @@ function invocationId(agentAddress, signedAt, delegationId, scopeExercised, inv)
28
28
  }
29
29
  async function stampInvocation(input) {
30
30
  const scopeExercised = input.scopeExercised ?? `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;
31
- const granted = input.delegation.scopes.map(parseScope);
32
- const exercisedParsed = parseScope(scopeExercised);
33
- if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
34
- throw new Error(
35
- `stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
36
- );
37
- }
31
+ assertScopeGranted(input.delegation, scopeExercised, "stampInvocation");
38
32
  const bytes = canonicalizeInvocation(input.invocation);
39
33
  return signAsAgent({
40
34
  agent: input.agent,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAqDO,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;AAKrF,EAAA,MAAM,OAAA,GAAU,KAAA,CAAM,UAAA,CAAW,MAAA,CAAO,IAAI,UAAU,CAAA;AACtD,EAAA,MAAM,eAAA,GAAkB,WAAW,cAAc,CAAA;AACjD,EAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,CAAC,MAAM,UAAA,CAAW,eAAA,EAAiB,CAAC,CAAC,CAAA,EAAG;AACtD,IAAA,MAAM,IAAI,KAAA;AAAA,MACN,qCAAqC,cAAc,CAAA,yCAAA;AAAA,KACvD;AAAA,EACJ;AAEA,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 canonicalize,\n computeActionId,\n hexEncode,\n isSubScope,\n parseScope,\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 // Tiny pre-flight: confirm the exercised scope is a sub-scope of *some*\n // granted scope. This keeps a common integration mistake from producing\n // an envelope that later fails verifier-side.\n const granted = input.delegation.scopes.map(parseScope);\n const exercisedParsed = parseScope(scopeExercised);\n if (!granted.some((g) => isSubScope(exercisedParsed, g))) {\n throw new Error(\n `stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`\n );\n }\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":";;;;;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"]}
package/package.json CHANGED
@@ -1,62 +1,65 @@
1
1
  {
2
- "name": "@orangecheck/agent-mcp",
3
- "version": "0.1.0",
4
- "description": "Wrap Model Context Protocol tool invocations in OC Agent envelopes — every call produces a signed, scope-constrained, verifiable agent-action stamp.",
5
- "keywords": [
6
- "bitcoin",
7
- "agent",
8
- "authority",
9
- "mcp",
10
- "model-context-protocol",
11
- "bip322",
12
- "oc-agent",
13
- "orangecheck"
14
- ],
15
- "author": "OrangeCheck",
16
- "license": "MIT",
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/orangecheck/oc-packages.git",
20
- "directory": "agent-mcp"
21
- },
22
- "homepage": "https://github.com/orangecheck/oc-agent-protocol",
23
- "bugs": {
24
- "url": "https://github.com/orangecheck/oc-agent-protocol/issues"
25
- },
26
- "main": "./dist/index.js",
27
- "module": "./dist/index.mjs",
28
- "types": "./dist/index.d.ts",
29
- "exports": {
30
- ".": {
31
- "types": "./dist/index.d.ts",
32
- "import": "./dist/index.mjs",
33
- "require": "./dist/index.js"
34
- }
35
- },
36
- "files": [
37
- "dist",
38
- "src",
39
- "README.md",
40
- "LICENSE"
41
- ],
42
- "scripts": {
43
- "build": "tsup",
44
- "dev": "tsup --watch",
45
- "type-check": "tsc --noEmit",
46
- "test": "vitest run",
47
- "test:watch": "vitest",
48
- "clean": "rm -rf dist",
49
- "prepublishOnly": "npm run clean && npm run build"
50
- },
51
- "dependencies": {
52
- "@noble/hashes": "^1.5.0",
53
- "@orangecheck/agent-core": "^0.1.0",
54
- "@orangecheck/agent-signer": "^0.1.0"
55
- },
56
- "devDependencies": {
57
- "@types/node": "^22.10.2",
58
- "tsup": "^8.3.5",
59
- "typescript": "^5.7.2",
60
- "vitest": "^3.2.4"
2
+ "name": "@orangecheck/agent-mcp",
3
+ "version": "0.3.0",
4
+ "description": "Wrap Model Context Protocol tool invocations in OC Agent envelopes — every call produces a signed, scope-constrained, verifiable agent-action stamp.",
5
+ "keywords": [
6
+ "bitcoin",
7
+ "agent",
8
+ "authority",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "bip322",
12
+ "oc-agent",
13
+ "orangecheck"
14
+ ],
15
+ "author": "OrangeCheck",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/orangecheck/oc-packages.git",
20
+ "directory": "agent-mcp"
21
+ },
22
+ "homepage": "https://github.com/orangecheck/oc-agent-protocol",
23
+ "bugs": {
24
+ "url": "https://github.com/orangecheck/oc-agent-protocol/issues"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "module": "./dist/index.mjs",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.mjs",
33
+ "require": "./dist/index.js"
61
34
  }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "dev": "tsup --watch",
45
+ "type-check": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "clean": "rm -rf dist",
49
+ "prepublishOnly": "npm run clean && npm run build"
50
+ },
51
+ "dependencies": {
52
+ "@noble/hashes": "^1.5.0",
53
+ "@orangecheck/agent-core": "^1.2.0",
54
+ "@orangecheck/agent-signer": "^0.3.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^22.10.2",
58
+ "tsup": "^8.3.5",
59
+ "typescript": "^5.7.2",
60
+ "vitest": "^3.2.4"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
62
65
  }
package/src/index.ts CHANGED
@@ -11,11 +11,10 @@
11
11
 
12
12
  import { sha256 } from '@noble/hashes/sha256';
13
13
  import {
14
+ assertScopeGranted,
14
15
  canonicalize,
15
16
  computeActionId,
16
17
  hexEncode,
17
- isSubScope,
18
- parseScope,
19
18
  type DelegationEnvelope,
20
19
  } from '@orangecheck/agent-core';
21
20
  import {
@@ -106,16 +105,7 @@ export async function stampInvocation(input: StampInvocationInput): Promise<Acti
106
105
  input.scopeExercised ??
107
106
  `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;
108
107
 
109
- // Tiny pre-flight: confirm the exercised scope is a sub-scope of *some*
110
- // granted scope. This keeps a common integration mistake from producing
111
- // an envelope that later fails verifier-side.
112
- const granted = input.delegation.scopes.map(parseScope);
113
- const exercisedParsed = parseScope(scopeExercised);
114
- if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
115
- throw new Error(
116
- `stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
117
- );
118
- }
108
+ assertScopeGranted(input.delegation, scopeExercised, 'stampInvocation');
119
109
 
120
110
  const bytes = canonicalizeInvocation(input.invocation);
121
111
  return signAsAgent({
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ScopeNotGrantedError } from '@orangecheck/agent-core';
4
+
5
+ import { stampInvocation } from './index.js';
6
+
7
+ const AGENT = 'bc1qagent0000000000000000000000000000000000';
8
+ const sign = () => async (msg: string) => Buffer.from(msg, 'utf8').toString('base64');
9
+
10
+ const SEALED = {
11
+ v: 1,
12
+ alg: 'x25519-xchacha20poly1305',
13
+ recipients: [{ device_id: 'dev1', ct: 'AA' }],
14
+ ct: 'BB',
15
+ nonce: 'CC',
16
+ } as never;
17
+
18
+ /**
19
+ * This adapter used to open-code the pre-flight scope check. It now routes to
20
+ * agent-core's `assertScopeGranted`, and these assert the routing rather than
21
+ * the check — agent-core owns testing the check itself.
22
+ *
23
+ * The case that matters is the encrypted one: a v1.2 private delegation has no
24
+ * readable `scopes`, and the old `?? []` reported that as "not a sub-scope of
25
+ * any granted scope" — pointing the integrator at a scope string that was
26
+ * probably fine, when the actual need was to decrypt the grant.
27
+ */
28
+ describe('stampInvocation scope pre-flight', () => {
29
+ const base = {
30
+ agent: { address: AGENT, sign: sign() },
31
+ invocation: { server: 'https://m.example.com', tool: 'search', arguments: { q: 'x' } },
32
+ } as never;
33
+
34
+ it('names encrypted scopes rather than blaming the scope string', async () => {
35
+ const err = await stampInvocation({
36
+ ...(base as object),
37
+ delegation: { scopes_encrypted: SEALED },
38
+ scopeExercised: 'mcp:invoke(server=https://m.example.com,tool=search)',
39
+ } as never).catch((e: unknown) => e);
40
+
41
+ expect(err).toBeInstanceOf(ScopeNotGrantedError);
42
+ expect((err as ScopeNotGrantedError).reason).toBe('scopes_encrypted');
43
+ expect((err as Error).message).toMatch(/decryptPrivateScopes/);
44
+ });
45
+
46
+ it('treats an absent grant as granting nothing', async () => {
47
+ const err = await stampInvocation({
48
+ ...(base as object),
49
+ delegation: {},
50
+ scopeExercised: 'mcp:invoke(server=https://m.example.com,tool=search)',
51
+ } as never).catch((e: unknown) => e);
52
+
53
+ expect((err as ScopeNotGrantedError).reason).toBe('no_scopes');
54
+ });
55
+ });