@orangecheck/agent-mcp 0.2.0 → 0.4.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 +4 -0
- package/dist/index.d.mts +1 -11
- package/dist/index.d.ts +1 -11
- package/dist/index.js +1 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
- package/src/index.test.ts +1 -1
- package/src/index.ts +2 -60
- package/src/scope-preflight.test.ts +55 -0
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.d.mts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
2
|
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
-
import { ConsoleClient, PostActionResult } from '@orangecheck/agent-console-client';
|
|
4
|
-
export { ConsoleClient, PostActionResult, postActionToConsole } from '@orangecheck/agent-console-client';
|
|
5
3
|
|
|
6
4
|
interface McpInvocation {
|
|
7
5
|
server: string;
|
|
@@ -28,12 +26,4 @@ interface InvokeWithStampResult<T> {
|
|
|
28
26
|
}
|
|
29
27
|
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
console?: ConsoleClient;
|
|
33
|
-
}
|
|
34
|
-
interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
35
|
-
posted: PostActionResult | null;
|
|
36
|
-
}
|
|
37
|
-
declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
|
|
38
|
-
|
|
39
|
-
export { type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, invokeWithStampAndPost, stampInvocation };
|
|
29
|
+
export { type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { DelegationEnvelope } from '@orangecheck/agent-core';
|
|
2
2
|
import { SignerRef, ActionEnvelope } from '@orangecheck/agent-signer';
|
|
3
|
-
import { ConsoleClient, PostActionResult } from '@orangecheck/agent-console-client';
|
|
4
|
-
export { ConsoleClient, PostActionResult, postActionToConsole } from '@orangecheck/agent-console-client';
|
|
5
3
|
|
|
6
4
|
interface McpInvocation {
|
|
7
5
|
server: string;
|
|
@@ -28,12 +26,4 @@ interface InvokeWithStampResult<T> {
|
|
|
28
26
|
}
|
|
29
27
|
declare function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promise<InvokeWithStampResult<T>>;
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
console?: ConsoleClient;
|
|
33
|
-
}
|
|
34
|
-
interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
35
|
-
posted: PostActionResult | null;
|
|
36
|
-
}
|
|
37
|
-
declare function invokeWithStampAndPost<T>(input: InvokeWithStampAndPostInput<T>): Promise<InvokeWithStampAndPostResult<T>>;
|
|
38
|
-
|
|
39
|
-
export { type InvokeWithStampAndPostInput, type InvokeWithStampAndPostResult, type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, invokeWithStampAndPost, stampInvocation };
|
|
29
|
+
export { type InvokeWithStampInput, type InvokeWithStampResult, type McpInvocation, type StampInvocationInput, canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
var sha256 = require('@noble/hashes/sha256');
|
|
4
4
|
var agentCore = require('@orangecheck/agent-core');
|
|
5
5
|
var agentSigner = require('@orangecheck/agent-signer');
|
|
6
|
-
var agentConsoleClient = require('@orangecheck/agent-console-client');
|
|
7
6
|
|
|
8
7
|
// src/index.ts
|
|
9
8
|
function canonicalizeInvocation(inv) {
|
|
@@ -31,13 +30,7 @@ function invocationId(agentAddress, signedAt, delegationId, scopeExercised, inv)
|
|
|
31
30
|
}
|
|
32
31
|
async function stampInvocation(input) {
|
|
33
32
|
const scopeExercised = input.scopeExercised ?? `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;
|
|
34
|
-
|
|
35
|
-
const exercisedParsed = agentCore.parseScope(scopeExercised);
|
|
36
|
-
if (!granted.some((g) => agentCore.isSubScope(exercisedParsed, g))) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
33
|
+
agentCore.assertScopeGranted(input.delegation, scopeExercised, "stampInvocation");
|
|
41
34
|
const bytes = canonicalizeInvocation(input.invocation);
|
|
42
35
|
return agentSigner.signAsAgent({
|
|
43
36
|
agent: input.agent,
|
|
@@ -53,28 +46,11 @@ async function invokeWithStamp(input) {
|
|
|
53
46
|
const result = await input.call(input.invocation);
|
|
54
47
|
return { result, action };
|
|
55
48
|
}
|
|
56
|
-
async function invokeWithStampAndPost(input) {
|
|
57
|
-
const { result, action } = await invokeWithStamp(input);
|
|
58
|
-
let posted = null;
|
|
59
|
-
if (input.console) {
|
|
60
|
-
try {
|
|
61
|
-
posted = await agentConsoleClient.postActionToConsole(action, input.console);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.error("[oc-agent-mcp] postActionToConsole failed:", err);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return { result, action, posted };
|
|
67
|
-
}
|
|
68
49
|
|
|
69
|
-
Object.defineProperty(exports, "postActionToConsole", {
|
|
70
|
-
enumerable: true,
|
|
71
|
-
get: function () { return agentConsoleClient.postActionToConsole; }
|
|
72
|
-
});
|
|
73
50
|
exports.canonicalizeInvocation = canonicalizeInvocation;
|
|
74
51
|
exports.invocationHash = invocationHash;
|
|
75
52
|
exports.invocationId = invocationId;
|
|
76
53
|
exports.invokeWithStamp = invokeWithStamp;
|
|
77
|
-
exports.invokeWithStampAndPost = invokeWithStampAndPost;
|
|
78
54
|
exports.stampInvocation = stampInvocation;
|
|
79
55
|
//# sourceMappingURL=index.js.map
|
|
80
56
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["canonicalize","hexEncode","sha256","computeActionId","parseScope","isSubScope","signAsAgent","_post"],"mappings":"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","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\n// ─────────────────────────────────────────────────────────────────────────────\n// Console integration — re-export from the shared client\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport {\n postActionToConsole,\n type ConsoleClient,\n type PostActionResult,\n} from '@orangecheck/agent-console-client';\n\nimport {\n postActionToConsole as _post,\n type ConsoleClient as _ConsoleClient,\n type PostActionResult as _PostActionResult,\n} from '@orangecheck/agent-console-client';\n\nexport interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {\n /** Console credentials. If absent, behaves like invokeWithStamp + posted: null. */\n console?: _ConsoleClient;\n}\n\nexport interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {\n /** Set when console is configured AND the POST succeeded. */\n posted: _PostActionResult | null;\n}\n\n/**\n * invokeWithStamp + post the stamped envelope to console.ochk.io. The\n * post happens AFTER the call so a failing post never prevents the\n * underlying MCP invocation from running. If `console` is omitted,\n * this is identical to invokeWithStamp + posted: null.\n */\nexport async function invokeWithStampAndPost<T>(\n input: InvokeWithStampAndPostInput<T>\n): Promise<InvokeWithStampAndPostResult<T>> {\n const { result, action } = await invokeWithStamp(input);\n let posted: _PostActionResult | null = null;\n if (input.console) {\n try {\n posted = await _post(action, input.console);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[oc-agent-mcp] postActionToConsole failed:', err);\n }\n }\n return { result, action, posted };\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,8 +1,6 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
-
import { canonicalize, hexEncode, computeActionId,
|
|
2
|
+
import { canonicalize, hexEncode, computeActionId, assertScopeGranted } from '@orangecheck/agent-core';
|
|
3
3
|
import { signAsAgent } from '@orangecheck/agent-signer';
|
|
4
|
-
import { postActionToConsole } from '@orangecheck/agent-console-client';
|
|
5
|
-
export { postActionToConsole } from '@orangecheck/agent-console-client';
|
|
6
4
|
|
|
7
5
|
// src/index.ts
|
|
8
6
|
function canonicalizeInvocation(inv) {
|
|
@@ -30,13 +28,7 @@ function invocationId(agentAddress, signedAt, delegationId, scopeExercised, inv)
|
|
|
30
28
|
}
|
|
31
29
|
async function stampInvocation(input) {
|
|
32
30
|
const scopeExercised = input.scopeExercised ?? `mcp:invoke(server=${input.invocation.server.trim()},tool=${input.invocation.tool})`;
|
|
33
|
-
|
|
34
|
-
const exercisedParsed = parseScope(scopeExercised);
|
|
35
|
-
if (!granted.some((g) => isSubScope(exercisedParsed, g))) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`stampInvocation: scope_exercised (${scopeExercised}) is not a sub-scope of any granted scope`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
31
|
+
assertScopeGranted(input.delegation, scopeExercised, "stampInvocation");
|
|
40
32
|
const bytes = canonicalizeInvocation(input.invocation);
|
|
41
33
|
return signAsAgent({
|
|
42
34
|
agent: input.agent,
|
|
@@ -52,19 +44,7 @@ async function invokeWithStamp(input) {
|
|
|
52
44
|
const result = await input.call(input.invocation);
|
|
53
45
|
return { result, action };
|
|
54
46
|
}
|
|
55
|
-
async function invokeWithStampAndPost(input) {
|
|
56
|
-
const { result, action } = await invokeWithStamp(input);
|
|
57
|
-
let posted = null;
|
|
58
|
-
if (input.console) {
|
|
59
|
-
try {
|
|
60
|
-
posted = await postActionToConsole(action, input.console);
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error("[oc-agent-mcp] postActionToConsole failed:", err);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return { result, action, posted };
|
|
66
|
-
}
|
|
67
47
|
|
|
68
|
-
export { canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp,
|
|
48
|
+
export { canonicalizeInvocation, invocationHash, invocationId, invokeWithStamp, stampInvocation };
|
|
69
49
|
//# sourceMappingURL=index.mjs.map
|
|
70
50
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangecheck/agent-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -50,15 +50,14 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@noble/hashes": "^1.5.0",
|
|
53
|
-
"@orangecheck/agent-
|
|
54
|
-
"@orangecheck/agent-
|
|
55
|
-
"@orangecheck/agent-signer": "^0.1.0"
|
|
53
|
+
"@orangecheck/agent-core": "^2.1.0",
|
|
54
|
+
"@orangecheck/agent-signer": "^0.3.0"
|
|
56
55
|
},
|
|
57
56
|
"devDependencies": {
|
|
58
57
|
"@types/node": "^22.10.2",
|
|
59
58
|
"tsup": "^8.3.5",
|
|
60
59
|
"typescript": "^5.7.2",
|
|
61
|
-
"vitest": "^
|
|
60
|
+
"vitest": "^4.1.11"
|
|
62
61
|
},
|
|
63
62
|
"publishConfig": {
|
|
64
63
|
"access": "public"
|
package/src/index.test.ts
CHANGED
|
@@ -62,7 +62,7 @@ describe('stampInvocation', () => {
|
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
64
|
expect(action.content.mime).toBe('application/vnd.oc-agent.mcp-invocation+json');
|
|
65
|
-
const r = await verifyAction({ action, delegation, verifyBip322: fakeVerify });
|
|
65
|
+
const r = await verifyAction({ action, delegation, verifyBip322: fakeVerify, skipRevocationCheck: true });
|
|
66
66
|
expect(r.ok).toBe(true);
|
|
67
67
|
});
|
|
68
68
|
|
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
|
-
|
|
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({
|
|
@@ -156,51 +146,3 @@ export async function invokeWithStamp<T>(input: InvokeWithStampInput<T>): Promis
|
|
|
156
146
|
const result = await input.call(input.invocation);
|
|
157
147
|
return { result, action };
|
|
158
148
|
}
|
|
159
|
-
|
|
160
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
161
|
-
// Console integration — re-export from the shared client
|
|
162
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
export {
|
|
165
|
-
postActionToConsole,
|
|
166
|
-
type ConsoleClient,
|
|
167
|
-
type PostActionResult,
|
|
168
|
-
} from '@orangecheck/agent-console-client';
|
|
169
|
-
|
|
170
|
-
import {
|
|
171
|
-
postActionToConsole as _post,
|
|
172
|
-
type ConsoleClient as _ConsoleClient,
|
|
173
|
-
type PostActionResult as _PostActionResult,
|
|
174
|
-
} from '@orangecheck/agent-console-client';
|
|
175
|
-
|
|
176
|
-
export interface InvokeWithStampAndPostInput<T> extends InvokeWithStampInput<T> {
|
|
177
|
-
/** Console credentials. If absent, behaves like invokeWithStamp + posted: null. */
|
|
178
|
-
console?: _ConsoleClient;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export interface InvokeWithStampAndPostResult<T> extends InvokeWithStampResult<T> {
|
|
182
|
-
/** Set when console is configured AND the POST succeeded. */
|
|
183
|
-
posted: _PostActionResult | null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* invokeWithStamp + post the stamped envelope to console.ochk.io. The
|
|
188
|
-
* post happens AFTER the call so a failing post never prevents the
|
|
189
|
-
* underlying MCP invocation from running. If `console` is omitted,
|
|
190
|
-
* this is identical to invokeWithStamp + posted: null.
|
|
191
|
-
*/
|
|
192
|
-
export async function invokeWithStampAndPost<T>(
|
|
193
|
-
input: InvokeWithStampAndPostInput<T>
|
|
194
|
-
): Promise<InvokeWithStampAndPostResult<T>> {
|
|
195
|
-
const { result, action } = await invokeWithStamp(input);
|
|
196
|
-
let posted: _PostActionResult | null = null;
|
|
197
|
-
if (input.console) {
|
|
198
|
-
try {
|
|
199
|
-
posted = await _post(action, input.console);
|
|
200
|
-
} catch (err) {
|
|
201
|
-
// eslint-disable-next-line no-console
|
|
202
|
-
console.error('[oc-agent-mcp] postActionToConsole failed:', err);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return { result, action, posted };
|
|
206
|
-
}
|
|
@@ -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
|
+
});
|