@crediolabs/policy-builder-mcp 0.1.17 → 0.2.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 +42 -52
- package/dist/bin/server.js +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/schemas.d.ts +122 -5
- package/dist/src/schemas.js +22 -9
- package/dist/src/server.js +16 -35
- package/dist/src/tools/result.js +8 -9
- package/dist/src/transports/http.d.ts +6 -0
- package/dist/src/transports/http.js +57 -14
- package/dist/src/transports/stdio.js +1 -1
- package/package.json +8 -12
- package/src/index.ts +1 -1
- package/src/schemas.ts +24 -9
- package/src/server.ts +27 -34
- package/src/tools/result.ts +6 -8
- package/src/transports/http.ts +67 -13
- package/src/transports/stdio.ts +1 -1
- package/dist-cjs/package.json +0 -3
- package/dist-cjs/src/index.d.ts +0 -7
- package/dist-cjs/src/index.js +0 -20
- package/dist-cjs/src/schemas.d.ts +0 -1055
- package/dist-cjs/src/schemas.js +0 -130
- package/dist-cjs/src/server.d.ts +0 -6
- package/dist-cjs/src/server.js +0 -62
- package/dist-cjs/src/tools/result.d.ts +0 -24
- package/dist-cjs/src/tools/result.js +0 -39
- package/dist-cjs/src/transports/http.d.ts +0 -15
- package/dist-cjs/src/transports/http.js +0 -140
- package/dist-cjs/src/transports/stdio.d.ts +0 -1
- package/dist-cjs/src/transports/stdio.js +0 -18
package/README.md
CHANGED
|
@@ -1,39 +1,30 @@
|
|
|
1
1
|
# @crediolabs/policy-builder-mcp
|
|
2
2
|
|
|
3
|
-
MCP server exposing the
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Install
|
|
9
|
-
|
|
10
|
-
```sh
|
|
11
|
-
npm install @crediolabs/policy-builder-mcp
|
|
12
|
-
# or run without installing:
|
|
13
|
-
npx @crediolabs/policy-builder-mcp
|
|
14
|
-
```
|
|
3
|
+
MCP server exposing the OctoGate policy toolchain to agents. It wraps
|
|
4
|
+
[`@crediolabs/policy-synth`](https://www.npmjs.com/package/@crediolabs/policy-synth):
|
|
5
|
+
an agent records a Soroban transaction, synthesises the minimal policy that
|
|
6
|
+
permits exactly that flow, verifies and simulates it, and receives an
|
|
7
|
+
unsigned install transaction for the user's wallet to sign.
|
|
15
8
|
|
|
16
9
|
## Run
|
|
17
10
|
|
|
18
11
|
```sh
|
|
19
|
-
|
|
20
|
-
policy-builder-mcp
|
|
21
|
-
policy-builder-mcp --http --http-port N # Streamable HTTP transport on port N
|
|
22
|
-
```
|
|
12
|
+
# stdio (default; Claude Desktop and local agents)
|
|
13
|
+
bunx @crediolabs/policy-builder-mcp
|
|
23
14
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
# Streamable HTTP on localhost:3001
|
|
16
|
+
bunx @crediolabs/policy-builder-mcp --http
|
|
17
|
+
bunx @crediolabs/policy-builder-mcp --http --http-port 8080
|
|
18
|
+
```
|
|
28
19
|
|
|
29
|
-
|
|
20
|
+
Claude Desktop / Claude Code configuration:
|
|
30
21
|
|
|
31
22
|
```json
|
|
32
23
|
{
|
|
33
24
|
"mcpServers": {
|
|
34
25
|
"policy-builder": {
|
|
35
|
-
"command": "
|
|
36
|
-
"args": ["
|
|
26
|
+
"command": "bunx",
|
|
27
|
+
"args": ["@crediolabs/policy-builder-mcp"]
|
|
37
28
|
}
|
|
38
29
|
}
|
|
39
30
|
}
|
|
@@ -41,35 +32,34 @@ used. Source of truth: [`bin/server.ts`](./bin/server.ts).
|
|
|
41
32
|
|
|
42
33
|
## Tools
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
test-covered. Install, on-chain verify, and simulate are later phases.
|
|
35
|
+
| Tool | Purpose |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `record_transaction` | Decode a transaction (hash or envelope XDR) into a `RecordedTransaction`. |
|
|
38
|
+
| `synthesize_policy` | Synthesise a `ProposedPolicy` from a recording or a `MandateSpec`. |
|
|
39
|
+
| `simulate_policy` | Replay a recording against a proposed predicate; run the deny-case battery. |
|
|
40
|
+
| `verify_policy` | Static minimality check on a proposed predicate. |
|
|
41
|
+
| `install_policy` | Build the unsigned `add_context_rule` transaction XDR. |
|
|
42
|
+
| `revoke_policy` | Build the unsigned `remove_context_rule` transaction XDR. |
|
|
43
|
+
| `get_interpreter_info` | Pinned interpreter address, grammar version and wasm sha256, optionally verified live. |
|
|
44
|
+
|
|
45
|
+
Failures come back as machine-readable tool errors (stable `code`,
|
|
46
|
+
`severity`, `retryable`), never as transport-level throws, so an agent can
|
|
47
|
+
branch on them.
|
|
48
|
+
|
|
49
|
+
## Security model
|
|
50
|
+
|
|
51
|
+
- **Stateless, no key material.** Install and revoke return *unsigned* XDR;
|
|
52
|
+
the wallet signature is the user-confirmation step.
|
|
53
|
+
- **Loopback only by default.** The HTTP transport refuses to bind a
|
|
54
|
+
non-loopback host; it serves `127.0.0.1`, `::1` or `localhost` unless the
|
|
55
|
+
embedding caller explicitly opts out (`allowExternalHost: true` via the
|
|
56
|
+
programmatic API), because the surface is unauthenticated by design.
|
|
57
|
+
- See the
|
|
58
|
+
[architecture document](https://github.com/untangledfinance/octogate/blob/main/docs/architecture.md)
|
|
59
|
+
for what the on-chain interpreter does and does not enforce, and the
|
|
60
|
+
[repository README](https://github.com/untangledfinance/octogate#readme)
|
|
61
|
+
for the contracts' audit status.
|
|
72
62
|
|
|
73
63
|
## License
|
|
74
64
|
|
|
75
|
-
MIT
|
|
65
|
+
MIT
|
package/dist/bin/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// packages/policy-builder-mcp/bin/server.ts - CLI entry point selecting the transport.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
5
|
// policy-builder-mcp (stdio - default; Claude Desktop / local agents)
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-mcp/src/index.ts - public re-exports for the MCP server package.
|
|
2
2
|
export { runRecordTransaction, runSynthesizePolicy, } from '@crediolabs/policy-synth/run';
|
|
3
3
|
export { RecordTransactionToolShape, SynthesizePolicyToolShape, } from "./schemas.js";
|
|
4
4
|
export { createMcpServer, registerTools } from "./server.js";
|
package/dist/src/schemas.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare const RecordTransactionToolShape: {
|
|
|
15
15
|
* front-end; the body re-validates against the discriminated union. */
|
|
16
16
|
export declare const SynthesizePolicyToolShape: {
|
|
17
17
|
readonly source: z.ZodOptional<z.ZodEnum<["mandate", "recording"]>>;
|
|
18
|
-
readonly mandate: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
readonly mandate: z.ZodOptional<z.ZodEffects<z.ZodObject<{
|
|
19
19
|
chain: z.ZodLiteral<"stellar">;
|
|
20
20
|
contract: z.ZodString;
|
|
21
21
|
method: z.ZodOptional<z.ZodString>;
|
|
@@ -102,6 +102,64 @@ export declare const SynthesizePolicyToolShape: {
|
|
|
102
102
|
validUntilLedger?: number | undefined;
|
|
103
103
|
validUntilUnixSeconds?: number | undefined;
|
|
104
104
|
}>>;
|
|
105
|
+
}, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
|
|
106
|
+
chain: z.ZodLiteral<"stellar">;
|
|
107
|
+
contract: z.ZodString;
|
|
108
|
+
method: z.ZodOptional<z.ZodString>;
|
|
109
|
+
spendingLimit: z.ZodOptional<z.ZodObject<{
|
|
110
|
+
token: z.ZodString;
|
|
111
|
+
limit: z.ZodString;
|
|
112
|
+
windowSeconds: z.ZodNumber;
|
|
113
|
+
}, "strip", z.ZodTypeAny, {
|
|
114
|
+
token: string;
|
|
115
|
+
limit: string;
|
|
116
|
+
windowSeconds: number;
|
|
117
|
+
}, {
|
|
118
|
+
token: string;
|
|
119
|
+
limit: string;
|
|
120
|
+
windowSeconds: number;
|
|
121
|
+
}>>;
|
|
122
|
+
approvalThreshold: z.ZodOptional<z.ZodNumber>;
|
|
123
|
+
recipients: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
124
|
+
expiry: z.ZodOptional<z.ZodObject<{
|
|
125
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
126
|
+
validUntilUnixSeconds: z.ZodOptional<z.ZodNumber>;
|
|
127
|
+
}, "strip", z.ZodTypeAny, {
|
|
128
|
+
validUntilLedger?: number | undefined;
|
|
129
|
+
validUntilUnixSeconds?: number | undefined;
|
|
130
|
+
}, {
|
|
131
|
+
validUntilLedger?: number | undefined;
|
|
132
|
+
validUntilUnixSeconds?: number | undefined;
|
|
133
|
+
}>>;
|
|
134
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
135
|
+
chain: z.ZodLiteral<"stellar">;
|
|
136
|
+
contract: z.ZodString;
|
|
137
|
+
method: z.ZodOptional<z.ZodString>;
|
|
138
|
+
spendingLimit: z.ZodOptional<z.ZodObject<{
|
|
139
|
+
token: z.ZodString;
|
|
140
|
+
limit: z.ZodString;
|
|
141
|
+
windowSeconds: z.ZodNumber;
|
|
142
|
+
}, "strip", z.ZodTypeAny, {
|
|
143
|
+
token: string;
|
|
144
|
+
limit: string;
|
|
145
|
+
windowSeconds: number;
|
|
146
|
+
}, {
|
|
147
|
+
token: string;
|
|
148
|
+
limit: string;
|
|
149
|
+
windowSeconds: number;
|
|
150
|
+
}>>;
|
|
151
|
+
approvalThreshold: z.ZodOptional<z.ZodNumber>;
|
|
152
|
+
recipients: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
153
|
+
expiry: z.ZodOptional<z.ZodObject<{
|
|
154
|
+
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
155
|
+
validUntilUnixSeconds: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
}, "strip", z.ZodTypeAny, {
|
|
157
|
+
validUntilLedger?: number | undefined;
|
|
158
|
+
validUntilUnixSeconds?: number | undefined;
|
|
159
|
+
}, {
|
|
160
|
+
validUntilLedger?: number | undefined;
|
|
161
|
+
validUntilUnixSeconds?: number | undefined;
|
|
162
|
+
}>>;
|
|
105
163
|
}, z.ZodTypeAny, "passthrough">>>;
|
|
106
164
|
readonly recordedTx: z.ZodOptional<z.ZodObject<{
|
|
107
165
|
network: z.ZodEnum<["mainnet", "testnet"]>;
|
|
@@ -369,18 +427,66 @@ export declare const SynthesizePolicyToolShape: {
|
|
|
369
427
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
370
428
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
371
429
|
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
430
|
+
oraclePriceBound: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
431
|
+
asset: z.ZodEffects<z.ZodString, string, string>;
|
|
432
|
+
operator: z.ZodEnum<["eq", "lt", "lte", "gt", "gte"]>;
|
|
433
|
+
value: z.ZodString;
|
|
434
|
+
decimals: z.ZodNumber;
|
|
435
|
+
}, "strip", z.ZodTypeAny, {
|
|
436
|
+
value: string;
|
|
437
|
+
operator: "eq" | "lt" | "lte" | "gt" | "gte";
|
|
438
|
+
asset: string;
|
|
439
|
+
decimals: number;
|
|
440
|
+
}, {
|
|
441
|
+
value: string;
|
|
442
|
+
operator: "eq" | "lt" | "lte" | "gt" | "gte";
|
|
443
|
+
asset: string;
|
|
444
|
+
decimals: number;
|
|
445
|
+
}>, "many">>;
|
|
372
446
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
373
447
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
374
448
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
375
449
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
376
450
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
377
451
|
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
452
|
+
oraclePriceBound: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
453
|
+
asset: z.ZodEffects<z.ZodString, string, string>;
|
|
454
|
+
operator: z.ZodEnum<["eq", "lt", "lte", "gt", "gte"]>;
|
|
455
|
+
value: z.ZodString;
|
|
456
|
+
decimals: z.ZodNumber;
|
|
457
|
+
}, "strip", z.ZodTypeAny, {
|
|
458
|
+
value: string;
|
|
459
|
+
operator: "eq" | "lt" | "lte" | "gt" | "gte";
|
|
460
|
+
asset: string;
|
|
461
|
+
decimals: number;
|
|
462
|
+
}, {
|
|
463
|
+
value: string;
|
|
464
|
+
operator: "eq" | "lt" | "lte" | "gt" | "gte";
|
|
465
|
+
asset: string;
|
|
466
|
+
decimals: number;
|
|
467
|
+
}>, "many">>;
|
|
378
468
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
379
469
|
windowSeconds: z.ZodOptional<z.ZodNumber>;
|
|
380
470
|
validUntilLedger: z.ZodOptional<z.ZodNumber>;
|
|
381
471
|
limitAmount: z.ZodOptional<z.ZodString>;
|
|
382
472
|
invocationLimit: z.ZodOptional<z.ZodNumber>;
|
|
383
473
|
swapRecipientAllowlist: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
474
|
+
oraclePriceBound: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
475
|
+
asset: z.ZodEffects<z.ZodString, string, string>;
|
|
476
|
+
operator: z.ZodEnum<["eq", "lt", "lte", "gt", "gte"]>;
|
|
477
|
+
value: z.ZodString;
|
|
478
|
+
decimals: z.ZodNumber;
|
|
479
|
+
}, "strip", z.ZodTypeAny, {
|
|
480
|
+
value: string;
|
|
481
|
+
operator: "eq" | "lt" | "lte" | "gt" | "gte";
|
|
482
|
+
asset: string;
|
|
483
|
+
decimals: number;
|
|
484
|
+
}, {
|
|
485
|
+
value: string;
|
|
486
|
+
operator: "eq" | "lt" | "lte" | "gt" | "gte";
|
|
487
|
+
asset: string;
|
|
488
|
+
decimals: number;
|
|
489
|
+
}>, "many">>;
|
|
384
490
|
}, z.ZodTypeAny, "passthrough">>>;
|
|
385
491
|
readonly confidenceOverride: z.ZodOptional<z.ZodObject<{
|
|
386
492
|
threshold: z.ZodNumber;
|
|
@@ -1028,20 +1134,31 @@ export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, Si
|
|
|
1028
1134
|
* (the policy already carries the encoded predicate); the run-layer
|
|
1029
1135
|
* extracts them from there. */
|
|
1030
1136
|
export declare const InstallPolicyToolShape: {
|
|
1031
|
-
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
1032
|
-
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
|
1033
1137
|
readonly rule: z.ZodOptional<z.ZodUnknown>;
|
|
1034
1138
|
readonly installNonce: z.ZodOptional<z.ZodNumber>;
|
|
1035
1139
|
readonly interpreterAddress: z.ZodOptional<z.ZodString>;
|
|
1140
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
1141
|
+
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
|
1036
1142
|
readonly rpcUrl: z.ZodOptional<z.ZodString>;
|
|
1037
1143
|
readonly baseFee: z.ZodOptional<z.ZodNumber>;
|
|
1038
1144
|
};
|
|
1039
1145
|
/** Flat ZodRawShape for `revoke_policy`. */
|
|
1040
1146
|
export declare const RevokePolicyToolShape: {
|
|
1041
|
-
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
1042
|
-
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
|
1043
1147
|
readonly ruleId: z.ZodOptional<z.ZodNumber>;
|
|
1044
1148
|
readonly interpreterAddress: z.ZodOptional<z.ZodString>;
|
|
1149
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
1150
|
+
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
|
1151
|
+
readonly rpcUrl: z.ZodOptional<z.ZodString>;
|
|
1152
|
+
readonly baseFee: z.ZodOptional<z.ZodNumber>;
|
|
1153
|
+
};
|
|
1154
|
+
/** Flat ZodRawShape for `merge_policy`: the tightening remedy when two rules
|
|
1155
|
+
* our interpreter polices can serve the same calls. Two steps, in order. */
|
|
1156
|
+
export declare const MergePolicyToolShape: {
|
|
1157
|
+
readonly ruleId: z.ZodNumber;
|
|
1158
|
+
readonly incomingPredicateBlobBase64: z.ZodString;
|
|
1159
|
+
readonly step: z.ZodEnum<["detach", "reinstall"]>;
|
|
1160
|
+
readonly smartAccount: z.ZodOptional<z.ZodString>;
|
|
1161
|
+
readonly sourceAccount: z.ZodOptional<z.ZodString>;
|
|
1045
1162
|
readonly rpcUrl: z.ZodOptional<z.ZodString>;
|
|
1046
1163
|
readonly baseFee: z.ZodOptional<z.ZodNumber>;
|
|
1047
1164
|
};
|
package/dist/src/schemas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-mcp/src/schemas.ts
|
|
2
2
|
//
|
|
3
3
|
// MCP-only tool shapes. The CORE input/output schemas and the MCP server
|
|
4
4
|
// registrations all live on `@crediolabs/policy-synth` (the tool-body glue
|
|
@@ -82,6 +82,17 @@ export const VerifyPolicyToolShape = {
|
|
|
82
82
|
validUntilLedger: z.number().int().positive().optional(),
|
|
83
83
|
oraclePricesByAsset: z.record(z.string(), OraclePriceFixtureSchema).optional(),
|
|
84
84
|
};
|
|
85
|
+
/** Common base for `install_policy` and `revoke_policy`: smartAccount,
|
|
86
|
+
* sourceAccount, optional RPC URL, optional base fee. Both share the
|
|
87
|
+
* same smart-account context, so the SDK-emitted JSON Schema stays
|
|
88
|
+
* identical for those fields. The body re-validates against the strict
|
|
89
|
+
* schemas in `@crediolabs/policy-synth/run`. */
|
|
90
|
+
const SmartAccountToolShape = {
|
|
91
|
+
smartAccount: z.string().min(1).optional(),
|
|
92
|
+
sourceAccount: z.string().min(1).optional(),
|
|
93
|
+
rpcUrl: z.string().url().optional(),
|
|
94
|
+
baseFee: z.number().int().positive().optional(),
|
|
95
|
+
};
|
|
85
96
|
/** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
|
|
86
97
|
* at the tool boundary because the rule schema is a discriminated union
|
|
87
98
|
* the SDK does not accept at registration; the body re-validates
|
|
@@ -90,22 +101,24 @@ export const VerifyPolicyToolShape = {
|
|
|
90
101
|
* (the policy already carries the encoded predicate); the run-layer
|
|
91
102
|
* extracts them from there. */
|
|
92
103
|
export const InstallPolicyToolShape = {
|
|
93
|
-
|
|
94
|
-
sourceAccount: z.string().min(1).optional(),
|
|
104
|
+
...SmartAccountToolShape,
|
|
95
105
|
rule: z.unknown().optional(),
|
|
96
106
|
installNonce: z.number().int().positive().optional(),
|
|
97
107
|
interpreterAddress: z.string().optional(),
|
|
98
|
-
rpcUrl: z.string().url().optional(),
|
|
99
|
-
baseFee: z.number().int().positive().optional(),
|
|
100
108
|
};
|
|
101
109
|
/** Flat ZodRawShape for `revoke_policy`. */
|
|
102
110
|
export const RevokePolicyToolShape = {
|
|
103
|
-
|
|
104
|
-
sourceAccount: z.string().min(1).optional(),
|
|
111
|
+
...SmartAccountToolShape,
|
|
105
112
|
ruleId: z.number().int().nonnegative().optional(),
|
|
106
113
|
interpreterAddress: z.string().optional(),
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
};
|
|
115
|
+
/** Flat ZodRawShape for `merge_policy`: the tightening remedy when two rules
|
|
116
|
+
* our interpreter polices can serve the same calls. Two steps, in order. */
|
|
117
|
+
export const MergePolicyToolShape = {
|
|
118
|
+
...SmartAccountToolShape,
|
|
119
|
+
ruleId: z.number().int().nonnegative(),
|
|
120
|
+
incomingPredicateBlobBase64: z.string().min(1),
|
|
121
|
+
step: z.enum(['detach', 'reinstall']),
|
|
109
122
|
};
|
|
110
123
|
/** Flat ZodRawShape for `get_interpreter_info`. `verifyLive` triggers an
|
|
111
124
|
* optional RPC `grammar_version()` call so the caller can verify the
|
package/dist/src/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-mcp/src/server.ts
|
|
2
2
|
//
|
|
3
3
|
// Registers the T1 tool surface on a fresh McpServer instance. The
|
|
4
4
|
// registration uses the official MCP SDK's `tool()` API with ZodRawShape
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
// Stateless: a fresh McpServer is constructed per transport (stdio/HTTP). No
|
|
11
11
|
// shared mutable state across calls; nothing here caches, queues, or holds
|
|
12
12
|
// key material.
|
|
13
|
-
import { runGetInterpreterInfo, runInstallPolicy, runRecordTransaction, runRevokePolicy, runSimulatePolicy, runSynthesizePolicy, runVerifyPolicy, } from '@crediolabs/policy-synth/run';
|
|
13
|
+
import { runGetInterpreterInfo, runInstallPolicy, runMergePolicy, runRecordTransaction, runRevokePolicy, runSimulatePolicy, runSynthesizePolicy, runVerifyPolicy, } from '@crediolabs/policy-synth/run';
|
|
14
14
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
-
import { GetInterpreterInfoToolShape, InstallPolicyToolShape, RecordTransactionToolShape, RevokePolicyToolShape, SimulatePolicyToolShape, SynthesizePolicyToolShape, VerifyPolicyToolShape, } from "./schemas.js";
|
|
15
|
+
import { GetInterpreterInfoToolShape, InstallPolicyToolShape, MergePolicyToolShape, RecordTransactionToolShape, RevokePolicyToolShape, SimulatePolicyToolShape, SynthesizePolicyToolShape, VerifyPolicyToolShape, } from "./schemas.js";
|
|
16
16
|
import { mcpResultFromCore } from "./tools/result.js";
|
|
17
|
+
/** Our envelope types `structuredContent` precisely (T / ToolError); the SDK's
|
|
18
|
+
* CallToolResult widens it to Record<string, unknown>, so the nominal types
|
|
19
|
+
* do not overlap. The runtime shapes match, so we assert through `unknown`
|
|
20
|
+
* at the transport boundary - in one place, used by every tool. */
|
|
21
|
+
const toCallToolResult = (res) => mcpResultFromCore(res);
|
|
17
22
|
/** Build a fresh, stateless MCP server. The caller owns the returned object
|
|
18
23
|
* and connects it to a single transport (stdio or Streamable HTTP). */
|
|
19
24
|
export function createMcpServer() {
|
|
@@ -23,36 +28,12 @@ export function createMcpServer() {
|
|
|
23
28
|
}
|
|
24
29
|
/** Idempotent registration of the T1 tool set on the given server. */
|
|
25
30
|
export function registerTools(server) {
|
|
26
|
-
server.tool('record_transaction', 'Decode a Soroban transaction (on-chain hash OR base64 envelope XDR) into a RecordedTransaction. Returns a machine-readable ToolError on validation failure.', RecordTransactionToolShape,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from either a deterministic MandateSpec (`source: mandate`) or a RecordedTransaction (`source: recording`). The discriminated `source` field selects the front-end.', SynthesizePolicyToolShape, async (args) => {
|
|
35
|
-
const res = await runSynthesizePolicy(args);
|
|
36
|
-
return mcpResultFromCore(res);
|
|
37
|
-
});
|
|
38
|
-
server.tool('simulate_policy', 'Replay a RecordedTransaction against a proposed PredicateNode (or null for an OZ-only policy) and emit the SimulationResult envelope (permit verdict + deny-case battery). Returns a SIMULATION_ERROR ToolError on runtime evaluation failure.', SimulatePolicyToolShape, async (args) => {
|
|
39
|
-
const res = await runSimulatePolicy(args);
|
|
40
|
-
return mcpResultFromCore(res);
|
|
41
|
-
});
|
|
42
|
-
server.tool('verify_policy', 'Run the static minimality check on a proposed PredicateNode against a RecordedTransaction (no conjunct is load-bearing-free). Returns VERIFICATION_FAILED with the dropped-constraint fingerprints when the predicate is over-broad.', VerifyPolicyToolShape, async (args) => {
|
|
43
|
-
const res = await runVerifyPolicy(args);
|
|
44
|
-
return mcpResultFromCore(res);
|
|
45
|
-
});
|
|
46
|
-
server.tool('install_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair). Only CALL 1 is emitted; the interpreter `install` follow-up needs the rule id the account assigns in call 1 and is documented in `followUp` in the response.', InstallPolicyToolShape, async (args) => {
|
|
47
|
-
const res = await runInstallPolicy(args);
|
|
48
|
-
return mcpResultFromCore(res);
|
|
49
|
-
});
|
|
50
|
-
server.tool('revoke_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.remove_context_rule(ruleId)` that removes a policy rule from the given smart account. The smart account handles uninstalling each attached policy itself. Auth is master-only - the source account MUST be the master signer set; delegated signers cannot uninstall.', RevokePolicyToolShape, async (args) => {
|
|
51
|
-
const res = await runRevokePolicy(args);
|
|
52
|
-
return mcpResultFromCore(res);
|
|
53
|
-
});
|
|
54
|
-
server.tool('get_interpreter_info', 'Read-only fingerprint lookup for the policy interpreter contract: returns the pinned address, grammar version, and wasm sha256 (from DEPLOYMENTS.md + SELF_VERSION). When `verifyLive=true`, performs an additional `grammar_version()` RPC call against the pinned address and reports whether the deployed contract matches the pin - a live mismatch check is more useful than a fabricated audit field.', GetInterpreterInfoToolShape, async (args) => {
|
|
55
|
-
const res = await runGetInterpreterInfo(args);
|
|
56
|
-
return mcpResultFromCore(res);
|
|
57
|
-
});
|
|
31
|
+
server.tool('record_transaction', 'Decode a Soroban transaction (on-chain hash OR base64 envelope XDR) into a RecordedTransaction. Returns a machine-readable ToolError on validation failure.', RecordTransactionToolShape, (args) => runRecordTransaction(args).then(toCallToolResult));
|
|
32
|
+
server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from either a deterministic MandateSpec (`source: mandate`) or a RecordedTransaction (`source: recording`). The discriminated `source` field selects the front-end.', SynthesizePolicyToolShape, (args) => runSynthesizePolicy(args).then(toCallToolResult));
|
|
33
|
+
server.tool('simulate_policy', 'Replay a RecordedTransaction against a proposed PredicateNode (or null for an OZ-only policy) and emit the SimulationResult envelope (permit verdict + deny-case battery). Returns a SIMULATION_ERROR ToolError on runtime evaluation failure.', SimulatePolicyToolShape, (args) => runSimulatePolicy(args).then(toCallToolResult));
|
|
34
|
+
server.tool('verify_policy', 'Run the static minimality check on a proposed PredicateNode against a RecordedTransaction (no conjunct is load-bearing-free). Returns VERIFICATION_FAILED with the dropped-constraint fingerprints when the predicate is over-broad.', VerifyPolicyToolShape, (args) => runVerifyPolicy(args).then(toCallToolResult));
|
|
35
|
+
server.tool('install_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. The wallet signs the returned XDR - the signature IS the user-confirmation step (this server is stateless and holds no key material, so there is no two-call confirm pair). Only CALL 1 is emitted; the interpreter `install` follow-up needs the rule id the account assigns in call 1 and is documented in `followUp` in the response.', InstallPolicyToolShape, (args) => runInstallPolicy(args).then(toCallToolResult));
|
|
36
|
+
server.tool('revoke_policy', 'Build an UNSIGNED Soroban transaction XDR for `account.remove_context_rule(ruleId)` that removes a policy rule from the given smart account. The smart account handles uninstalling each attached policy itself. Auth is master-only - the source account MUST be the master signer set; delegated signers cannot uninstall.', RevokePolicyToolShape, (args) => runRevokePolicy(args).then(toCallToolResult));
|
|
37
|
+
server.tool('merge_policy', "Tighten a rule by replacing its predicate with the conjunction of the installed one and a new one. This is the remedy for the overlap `install_policy` reports between two rules the interpreter polices: because OpenZeppelin enforces only the rule a signer names, adding a second, tighter rule restricts nothing, so the restriction has to become one predicate. TWO transactions, in order: call with step 'detach' to remove the current attachment, wait for it to confirm, then call with step 'reinstall'. Detaching uninstalls the policy, which RESETS every counter on the rule, and the rule is unpoliced in between - both are reported in `warnings`.", MergePolicyToolShape, (args) => runMergePolicy(args).then(toCallToolResult));
|
|
38
|
+
server.tool('get_interpreter_info', 'Read-only fingerprint lookup for the policy interpreter contract: returns the pinned address, grammar version, and wasm sha256 (from the pinned constants + SELF_VERSION). When `verifyLive=true`, performs an additional `grammar_version()` RPC call against the pinned address and reports whether the deployed contract matches the pin - a live mismatch check is more useful than a fabricated audit field.', GetInterpreterInfoToolShape, (args) => runGetInterpreterInfo(args).then(toCallToolResult));
|
|
58
39
|
}
|
package/dist/src/tools/result.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-mcp/src/tools/result.ts
|
|
2
2
|
//
|
|
3
3
|
// Transport-layer mapping between the core's ToolResponse<T> envelope and the
|
|
4
4
|
// MCP result envelope. This is the ONLY place the two envelopes meet; no
|
|
@@ -15,14 +15,13 @@
|
|
|
15
15
|
/** Map a core ToolResponse<T> to the MCP result envelope. The handler in
|
|
16
16
|
* src/server.ts wraps this in the SDK's `{ content, isError }` shape. */
|
|
17
17
|
export function mcpResultFromCore(res) {
|
|
18
|
-
if (res.ok)
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
return mcpErrorFromCore(res.error);
|
|
18
|
+
if (!res.ok)
|
|
19
|
+
return mcpErrorFromCore(res.error);
|
|
20
|
+
return {
|
|
21
|
+
isError: false,
|
|
22
|
+
content: [{ type: 'text', text: JSON.stringify(res.data) }],
|
|
23
|
+
structuredContent: res.data,
|
|
24
|
+
};
|
|
26
25
|
}
|
|
27
26
|
/** Map a core ToolError directly (for inputs that already failed validation
|
|
28
27
|
* inside the handler). */
|
|
@@ -3,6 +3,12 @@ export interface StartHttpServerOptions {
|
|
|
3
3
|
host?: string;
|
|
4
4
|
/** Path the server mounts the MCP endpoint at. Default `/mcp`. */
|
|
5
5
|
path?: string;
|
|
6
|
+
/** Opt-in to binding a NON-loopback host (e.g. `0.0.0.0` to expose the
|
|
7
|
+
* server on a LAN / public NIC). The MCP surface is unauthenticated, so
|
|
8
|
+
* this is gated behind a flag: the default refuse-then-opt-in shape
|
|
9
|
+
* keeps the security boundary auditable in code review. A caller that
|
|
10
|
+
* sets this is taking responsibility for downstream auth / firewall. */
|
|
11
|
+
allowExternalHost?: boolean;
|
|
6
12
|
}
|
|
7
13
|
export interface RunningHttpServer {
|
|
8
14
|
port: number;
|
|
@@ -1,28 +1,49 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-mcp/src/transports/http.ts
|
|
2
2
|
//
|
|
3
3
|
// Streamable HTTP transport (hosted). Uses the Node http module directly so
|
|
4
4
|
// the package stays thin - no express / hono dep. We run in STATELESS mode
|
|
5
|
-
// (sessionIdGenerator
|
|
6
|
-
//
|
|
5
|
+
// (no `sessionIdGenerator` option), and the SDK 1.26+ requires a FRESH
|
|
6
|
+
// transport per request in stateless mode: reusing one across requests lets
|
|
7
|
+
// concurrent clients share internal message-id state, which is exactly the
|
|
8
|
+
// cross-client data leak GHSA-345p-7cg4-v4c7 warns about. We therefore build
|
|
9
|
+
// a transport inside the request handler, connect it, dispatch the request,
|
|
10
|
+
// then close it (close() resets the McpServer's transport slot so the next
|
|
11
|
+
// request can reconnect cleanly).
|
|
7
12
|
//
|
|
8
13
|
// Single endpoint: POST /mcp (the SDK also accepts GET for SSE streaming, but
|
|
9
14
|
// the T1 surface does not emit server-initiated messages so we omit it).
|
|
10
|
-
// Listens on 127.0.0.1 by default
|
|
15
|
+
// Listens on 127.0.0.1 by default.
|
|
16
|
+
//
|
|
17
|
+
// SECURITY BOUNDARY: the MCP server has NO auth - any request is processed.
|
|
18
|
+
// Binding to a non-loopback interface (e.g. `0.0.0.0`, `::`, an external
|
|
19
|
+
// NIC) would expose that unauthenticated surface to every reachable host
|
|
20
|
+
// (LAN peers, public cloud metadata, the open internet on a misconfigured
|
|
21
|
+
// VPS). The host boundary is therefore FAIL-CLOSED: only loopback
|
|
22
|
+
// (`127.0.0.1`, `::1`, `localhost`) is accepted by default. A caller that
|
|
23
|
+
// KNOWS they want to expose the server MUST pass `allowExternalHost: true`
|
|
24
|
+
// explicitly - the flag is the auditable intent. Test runners and the
|
|
25
|
+
// in-process CLI client do not need it; they bind 127.0.0.1 already.
|
|
11
26
|
import { createServer } from 'node:http';
|
|
12
27
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
13
28
|
import { createMcpServer } from "../server.js";
|
|
29
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
|
|
14
30
|
/** Stateless Streamable HTTP server. Resolves once the server is listening.
|
|
15
31
|
* The returned handle exposes `close()` for tests + clean shutdown. */
|
|
16
32
|
export async function startHttpServer(opts) {
|
|
17
33
|
const host = opts.host ?? '127.0.0.1';
|
|
18
34
|
const path = opts.path ?? '/mcp';
|
|
35
|
+
// Default-deny: refuse to bind a non-loopback host unless the caller has
|
|
36
|
+
// explicitly opted in. The MCP server has no auth, so the only thing
|
|
37
|
+
// standing between this binary and an open attack surface on `0.0.0.0` is
|
|
38
|
+
// this check; we would rather fail loudly here than silently expose it.
|
|
39
|
+
if (!LOOPBACK_HOSTS.has(host) && opts.allowExternalHost !== true) {
|
|
40
|
+
throw new Error(`startHttpServer: refusing to bind host ${host}: the MCP surface is unauthenticated, so only loopback (127.0.0.1, ::1, localhost) is permitted by default. Pass \`allowExternalHost: true\` to opt in to a non-loopback bind.`);
|
|
41
|
+
}
|
|
42
|
+
// The McpServer is stateless and per-process; we keep ONE instance alive
|
|
43
|
+
// for the lifetime of the listener, but each request creates + connects a
|
|
44
|
+
// fresh StreamableHTTPServerTransport (SDK 1.26+ refuses to reuse a
|
|
45
|
+
// stateless transport across requests).
|
|
19
46
|
const server = createMcpServer();
|
|
20
|
-
// One transport per server (the SDK reuses the transport for every request
|
|
21
|
-
// in stateless mode). We connect it once at startup and reuse it.
|
|
22
|
-
const transport = new StreamableHTTPServerTransport({
|
|
23
|
-
sessionIdGenerator: undefined,
|
|
24
|
-
});
|
|
25
|
-
await server.connect(transport);
|
|
26
47
|
const httpServer = createServer(async (req, res) => {
|
|
27
48
|
if (!req.url) {
|
|
28
49
|
sendJson(res, 400, { error: 'missing url' });
|
|
@@ -68,6 +89,20 @@ export async function startHttpServer(opts) {
|
|
|
68
89
|
});
|
|
69
90
|
return;
|
|
70
91
|
}
|
|
92
|
+
// One transport per request. Omitting `sessionIdGenerator` selects
|
|
93
|
+
// stateless mode in SDK 1.26+ (the previous `sessionIdGenerator:
|
|
94
|
+
// undefined` form is rejected because the option type no longer
|
|
95
|
+
// includes `undefined` under `exactOptionalPropertyTypes`).
|
|
96
|
+
//
|
|
97
|
+
// The cast `as unknown as Transport` is narrowly scoped to the SDK
|
|
98
|
+
// boundary: the Node wrapper exposes `onclose` via getter/setter with
|
|
99
|
+
// type `(() => void) | undefined`, while the `Transport` interface
|
|
100
|
+
// declares `onclose?: () => void`. Under `exactOptionalPropertyTypes:
|
|
101
|
+
// true` those are not structurally assignable. The runtime contract is
|
|
102
|
+
// the same (the setter accepts `() => void`), so we cast once here.
|
|
103
|
+
const transport = new StreamableHTTPServerTransport();
|
|
104
|
+
const sdkTransport = transport;
|
|
105
|
+
await server.connect(sdkTransport);
|
|
71
106
|
try {
|
|
72
107
|
// `handleRequest` writes the response and returns once the message has
|
|
73
108
|
// been dispatched. No shared state across calls in stateless mode.
|
|
@@ -76,7 +111,10 @@ export async function startHttpServer(opts) {
|
|
|
76
111
|
catch {
|
|
77
112
|
// The SDK normally writes structured errors itself; this is a belt +
|
|
78
113
|
// braces guard so a thrown error does not leave the socket hanging.
|
|
79
|
-
if (
|
|
114
|
+
if (res.headersSent) {
|
|
115
|
+
res.end();
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
80
118
|
const error = {
|
|
81
119
|
code: 'SYNTHESIS_ERROR',
|
|
82
120
|
message: 'internal server error',
|
|
@@ -85,8 +123,14 @@ export async function startHttpServer(opts) {
|
|
|
85
123
|
};
|
|
86
124
|
sendJson(res, 500, { error });
|
|
87
125
|
}
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
// Closing the transport fires its `onclose`, which resets
|
|
129
|
+
// `server._transport` to undefined so the next request can connect
|
|
130
|
+
// a fresh transport. We swallow the close error: a request that
|
|
131
|
+
// already wrote its response does not care whether the close path
|
|
132
|
+
// threw.
|
|
133
|
+
await transport.close().catch(() => { });
|
|
90
134
|
}
|
|
91
135
|
});
|
|
92
136
|
await new Promise((resolve, reject) => {
|
|
@@ -102,7 +146,6 @@ export async function startHttpServer(opts) {
|
|
|
102
146
|
path,
|
|
103
147
|
close: async () => {
|
|
104
148
|
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
105
|
-
await transport.close().catch(() => { });
|
|
106
149
|
await server.close().catch(() => { });
|
|
107
150
|
},
|
|
108
151
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// packages/policy-builder-mcp/src/transports/stdio.ts
|
|
2
2
|
//
|
|
3
3
|
// stdio transport (Claude Desktop / local agents). The MCP SDK reads JSON-RPC
|
|
4
4
|
// from stdin and writes to stdout. Each process serves ONE client and exits
|