@crediolabs/policy-builder-mcp 0.3.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.
@@ -834,6 +834,21 @@ export declare const VerifyPolicyToolShape: {
834
834
  readonly validUntilLedger: z.ZodOptional<z.ZodNumber>;
835
835
  };
836
836
  export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, SimulatePolicyInput, VerifyPolicyInput, } from '@crediolabs/policy-synth/run';
837
+ /** Flat ZodRawShape for `declare_policy`. The DECLARATIVE front-end: the
838
+ * constraint stated outright, with no transaction to decode. Every field is
839
+ * optional except `fn`; the body re-validates against the strict
840
+ * `DeclarePolicyInputSchema`, which is `.strict()` and so refuses the removed
841
+ * MandateSpec fields (`spendingLimit`, `approvalThreshold`) rather than
842
+ * accepting them in silence. */
843
+ export declare const DeclarePolicyToolShape: {
844
+ readonly fn: z.ZodString;
845
+ readonly contract: z.ZodOptional<z.ZodString>;
846
+ readonly maxAmount: z.ZodOptional<z.ZodString>;
847
+ readonly amountArgIndex: z.ZodOptional<z.ZodNumber>;
848
+ readonly recipients: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
849
+ readonly recipientArgIndex: z.ZodOptional<z.ZodNumber>;
850
+ readonly allowZeroCap: z.ZodOptional<z.ZodBoolean>;
851
+ };
837
852
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
838
853
  * at the tool boundary because the rule schema is a discriminated union
839
854
  * the SDK does not accept at registration; the body re-validates
@@ -842,11 +857,16 @@ export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, Si
842
857
  * (the policy already carries the encoded predicate); the run-layer
843
858
  * extracts them from there. */
844
859
  export declare const InstallPolicyToolShape: {
860
+ /** Rules already on the account. Supplying them turns on the cross-rule
861
+ * authority scan; omitting them returns `authorityScan: null`, which means
862
+ * "not checked" rather than "nothing found". */
863
+ readonly existingRules: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
845
864
  readonly rule: z.ZodOptional<z.ZodUnknown>;
846
865
  readonly installNonce: z.ZodOptional<z.ZodNumber>;
847
866
  readonly interpreterAddress: z.ZodOptional<z.ZodString>;
848
867
  readonly smartAccount: z.ZodOptional<z.ZodString>;
849
868
  readonly sourceAccount: z.ZodOptional<z.ZodString>;
869
+ readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
850
870
  readonly rpcUrl: z.ZodOptional<z.ZodString>;
851
871
  readonly baseFee: z.ZodOptional<z.ZodNumber>;
852
872
  };
@@ -856,6 +876,7 @@ export declare const RevokePolicyToolShape: {
856
876
  readonly interpreterAddress: z.ZodOptional<z.ZodString>;
857
877
  readonly smartAccount: z.ZodOptional<z.ZodString>;
858
878
  readonly sourceAccount: z.ZodOptional<z.ZodString>;
879
+ readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
859
880
  readonly rpcUrl: z.ZodOptional<z.ZodString>;
860
881
  readonly baseFee: z.ZodOptional<z.ZodNumber>;
861
882
  };
@@ -67,16 +67,38 @@ const PolicyCheckToolShape = {
67
67
  export const SimulatePolicyToolShape = { ...PolicyCheckToolShape };
68
68
  export const VerifyPolicyToolShape = { ...PolicyCheckToolShape };
69
69
  /** Common base for `install_policy` and `revoke_policy`: smartAccount,
70
- * sourceAccount, optional RPC URL, optional base fee. Both share the
71
- * same smart-account context, so the SDK-emitted JSON Schema stays
70
+ * sourceAccount, target network, optional RPC URL, optional base fee. Both
71
+ * share the same smart-account context, so the SDK-emitted JSON Schema stays
72
72
  * identical for those fields. The body re-validates against the strict
73
73
  * schemas in `@crediolabs/policy-synth/run`. */
74
74
  const SmartAccountToolShape = {
75
75
  smartAccount: z.string().min(1).optional(),
76
76
  sourceAccount: z.string().min(1).optional(),
77
+ /** Omitting this from the shape made both tools testnet-ONLY. The input
78
+ * schema defaults `network` to `testnet` and expects a mainnet caller to
79
+ * set it, but a field absent from the tool shape is STRIPPED before the
80
+ * body runs, so an MCP client could not reach the mainnet pin at all. The
81
+ * failure presented as a deliberate testnet pin rather than as a missing
82
+ * parameter, which sent integrators to build the install by hand. */
83
+ network: NetworkSchema.optional(),
77
84
  rpcUrl: z.string().url().optional(),
78
85
  baseFee: z.number().int().positive().optional(),
79
86
  };
87
+ /** Flat ZodRawShape for `declare_policy`. The DECLARATIVE front-end: the
88
+ * constraint stated outright, with no transaction to decode. Every field is
89
+ * optional except `fn`; the body re-validates against the strict
90
+ * `DeclarePolicyInputSchema`, which is `.strict()` and so refuses the removed
91
+ * MandateSpec fields (`spendingLimit`, `approvalThreshold`) rather than
92
+ * accepting them in silence. */
93
+ export const DeclarePolicyToolShape = {
94
+ fn: z.string().min(1),
95
+ contract: z.string().optional(),
96
+ maxAmount: z.string().optional(),
97
+ amountArgIndex: z.number().int().nonnegative().optional(),
98
+ recipients: z.array(z.string()).optional(),
99
+ recipientArgIndex: z.number().int().nonnegative().optional(),
100
+ allowZeroCap: z.boolean().optional(),
101
+ };
80
102
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
81
103
  * at the tool boundary because the rule schema is a discriminated union
82
104
  * the SDK does not accept at registration; the body re-validates
@@ -86,6 +108,10 @@ const SmartAccountToolShape = {
86
108
  * extracts them from there. */
87
109
  export const InstallPolicyToolShape = {
88
110
  ...SmartAccountToolShape,
111
+ /** Rules already on the account. Supplying them turns on the cross-rule
112
+ * authority scan; omitting them returns `authorityScan: null`, which means
113
+ * "not checked" rather than "nothing found". */
114
+ existingRules: z.array(z.unknown()).optional(),
89
115
  rule: z.unknown().optional(),
90
116
  installNonce: z.number().int().positive().optional(),
91
117
  interpreterAddress: z.string().optional(),
@@ -10,9 +10,9 @@
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 { runDeclarePolicy, runGetInterpreterInfo, runInstallPolicy, 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 { DeclarePolicyToolShape, GetInterpreterInfoToolShape, InstallPolicyToolShape, RecordTransactionToolShape, RevokePolicyToolShape, SimulatePolicyToolShape, SynthesizePolicyToolShape, VerifyPolicyToolShape, } from "./schemas.js";
16
16
  import { mcpResultFromCore } from "./tools/result.js";
17
17
  /** Our envelope types `structuredContent` precisely (T / ToolError); the SDK's
18
18
  * CallToolResult widens it to Record<string, unknown>, so the nominal types
@@ -32,7 +32,8 @@ export function registerTools(server) {
32
32
  server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from a RecordedTransaction (`source: recording`).', SynthesizePolicyToolShape, (args) => runSynthesizePolicy(args).then(toCallToolResult));
33
33
  server.tool('simulate_policy', 'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass the `predicate` returned by `synthesize_policy` under `explain`.', SimulatePolicyToolShape, (args) => toCallToolResult(runSimulatePolicy(args)));
34
34
  server.tool('verify_policy', 'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose.', VerifyPolicyToolShape, (args) => toCallToolResult(runVerifyPolicy(args)));
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));
35
+ server.tool('declare_policy', 'Build an interpreter predicate from a DECLARED constraint - the method to pin, and optionally the contract, a per-call amount cap and a recipient allowlist. Use this when there is no transaction to record, or when `record_transaction` refuses a contract it does not recognise. Returns the predicate tree, its canonical encoding and hash, ready for `install_policy`. `warnings` names any argument index that was GUESSED rather than supplied - a bound on the wrong argument constrains nothing while looking correct, so read them. There is no rolling spend window and no approval threshold: neither is expressible in grammar 3.', DeclarePolicyToolShape, (args) => Promise.resolve(runDeclarePolicy(args)).then(toCallToolResult));
36
+ 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. Pass `existingRules` to get an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. Omitting them returns `authorityScan: null`, meaning NOT CHECKED. 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
37
  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
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));
38
39
  }
@@ -834,6 +834,21 @@ export declare const VerifyPolicyToolShape: {
834
834
  readonly validUntilLedger: z.ZodOptional<z.ZodNumber>;
835
835
  };
836
836
  export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, SimulatePolicyInput, VerifyPolicyInput, } from '@crediolabs/policy-synth/run';
837
+ /** Flat ZodRawShape for `declare_policy`. The DECLARATIVE front-end: the
838
+ * constraint stated outright, with no transaction to decode. Every field is
839
+ * optional except `fn`; the body re-validates against the strict
840
+ * `DeclarePolicyInputSchema`, which is `.strict()` and so refuses the removed
841
+ * MandateSpec fields (`spendingLimit`, `approvalThreshold`) rather than
842
+ * accepting them in silence. */
843
+ export declare const DeclarePolicyToolShape: {
844
+ readonly fn: z.ZodString;
845
+ readonly contract: z.ZodOptional<z.ZodString>;
846
+ readonly maxAmount: z.ZodOptional<z.ZodString>;
847
+ readonly amountArgIndex: z.ZodOptional<z.ZodNumber>;
848
+ readonly recipients: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
849
+ readonly recipientArgIndex: z.ZodOptional<z.ZodNumber>;
850
+ readonly allowZeroCap: z.ZodOptional<z.ZodBoolean>;
851
+ };
837
852
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
838
853
  * at the tool boundary because the rule schema is a discriminated union
839
854
  * the SDK does not accept at registration; the body re-validates
@@ -842,11 +857,16 @@ export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, Si
842
857
  * (the policy already carries the encoded predicate); the run-layer
843
858
  * extracts them from there. */
844
859
  export declare const InstallPolicyToolShape: {
860
+ /** Rules already on the account. Supplying them turns on the cross-rule
861
+ * authority scan; omitting them returns `authorityScan: null`, which means
862
+ * "not checked" rather than "nothing found". */
863
+ readonly existingRules: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
845
864
  readonly rule: z.ZodOptional<z.ZodUnknown>;
846
865
  readonly installNonce: z.ZodOptional<z.ZodNumber>;
847
866
  readonly interpreterAddress: z.ZodOptional<z.ZodString>;
848
867
  readonly smartAccount: z.ZodOptional<z.ZodString>;
849
868
  readonly sourceAccount: z.ZodOptional<z.ZodString>;
869
+ readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
850
870
  readonly rpcUrl: z.ZodOptional<z.ZodString>;
851
871
  readonly baseFee: z.ZodOptional<z.ZodNumber>;
852
872
  };
@@ -856,6 +876,7 @@ export declare const RevokePolicyToolShape: {
856
876
  readonly interpreterAddress: z.ZodOptional<z.ZodString>;
857
877
  readonly smartAccount: z.ZodOptional<z.ZodString>;
858
878
  readonly sourceAccount: z.ZodOptional<z.ZodString>;
879
+ readonly network: z.ZodOptional<z.ZodEnum<["mainnet", "testnet"]>>;
859
880
  readonly rpcUrl: z.ZodOptional<z.ZodString>;
860
881
  readonly baseFee: z.ZodOptional<z.ZodNumber>;
861
882
  };
@@ -22,7 +22,7 @@
22
22
  // the strict schemas - a drift in field type or optionality breaks the
23
23
  // SDK's emitted JSON Schema.
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.GetInterpreterInfoToolShape = exports.RevokePolicyToolShape = exports.InstallPolicyToolShape = exports.VerifyPolicyToolShape = exports.SimulatePolicyToolShape = exports.SynthesizePolicyToolShape = exports.RecordTransactionToolShape = exports.ToolErrorSchema = exports.SynthesizePolicyInputSchema = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.NetworkSchema = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.ComposeUserResponsesSchema = void 0;
25
+ exports.GetInterpreterInfoToolShape = exports.RevokePolicyToolShape = exports.InstallPolicyToolShape = exports.DeclarePolicyToolShape = exports.VerifyPolicyToolShape = exports.SimulatePolicyToolShape = exports.SynthesizePolicyToolShape = exports.RecordTransactionToolShape = exports.ToolErrorSchema = exports.SynthesizePolicyInputSchema = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.NetworkSchema = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.ComposeUserResponsesSchema = void 0;
26
26
  const run_1 = require("@crediolabs/policy-synth/run");
27
27
  Object.defineProperty(exports, "ComposeUserResponsesSchema", { enumerable: true, get: function () { return run_1.ComposeUserResponsesSchema; } });
28
28
  Object.defineProperty(exports, "GetInterpreterInfoInputSchema", { enumerable: true, get: function () { return run_1.GetInterpreterInfoInputSchema; } });
@@ -75,16 +75,38 @@ const PolicyCheckToolShape = {
75
75
  exports.SimulatePolicyToolShape = { ...PolicyCheckToolShape };
76
76
  exports.VerifyPolicyToolShape = { ...PolicyCheckToolShape };
77
77
  /** Common base for `install_policy` and `revoke_policy`: smartAccount,
78
- * sourceAccount, optional RPC URL, optional base fee. Both share the
79
- * same smart-account context, so the SDK-emitted JSON Schema stays
78
+ * sourceAccount, target network, optional RPC URL, optional base fee. Both
79
+ * share the same smart-account context, so the SDK-emitted JSON Schema stays
80
80
  * identical for those fields. The body re-validates against the strict
81
81
  * schemas in `@crediolabs/policy-synth/run`. */
82
82
  const SmartAccountToolShape = {
83
83
  smartAccount: zod_1.z.string().min(1).optional(),
84
84
  sourceAccount: zod_1.z.string().min(1).optional(),
85
+ /** Omitting this from the shape made both tools testnet-ONLY. The input
86
+ * schema defaults `network` to `testnet` and expects a mainnet caller to
87
+ * set it, but a field absent from the tool shape is STRIPPED before the
88
+ * body runs, so an MCP client could not reach the mainnet pin at all. The
89
+ * failure presented as a deliberate testnet pin rather than as a missing
90
+ * parameter, which sent integrators to build the install by hand. */
91
+ network: run_1.NetworkSchema.optional(),
85
92
  rpcUrl: zod_1.z.string().url().optional(),
86
93
  baseFee: zod_1.z.number().int().positive().optional(),
87
94
  };
95
+ /** Flat ZodRawShape for `declare_policy`. The DECLARATIVE front-end: the
96
+ * constraint stated outright, with no transaction to decode. Every field is
97
+ * optional except `fn`; the body re-validates against the strict
98
+ * `DeclarePolicyInputSchema`, which is `.strict()` and so refuses the removed
99
+ * MandateSpec fields (`spendingLimit`, `approvalThreshold`) rather than
100
+ * accepting them in silence. */
101
+ exports.DeclarePolicyToolShape = {
102
+ fn: zod_1.z.string().min(1),
103
+ contract: zod_1.z.string().optional(),
104
+ maxAmount: zod_1.z.string().optional(),
105
+ amountArgIndex: zod_1.z.number().int().nonnegative().optional(),
106
+ recipients: zod_1.z.array(zod_1.z.string()).optional(),
107
+ recipientArgIndex: zod_1.z.number().int().nonnegative().optional(),
108
+ allowZeroCap: zod_1.z.boolean().optional(),
109
+ };
88
110
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
89
111
  * at the tool boundary because the rule schema is a discriminated union
90
112
  * the SDK does not accept at registration; the body re-validates
@@ -94,6 +116,10 @@ const SmartAccountToolShape = {
94
116
  * extracts them from there. */
95
117
  exports.InstallPolicyToolShape = {
96
118
  ...SmartAccountToolShape,
119
+ /** Rules already on the account. Supplying them turns on the cross-rule
120
+ * authority scan; omitting them returns `authorityScan: null`, which means
121
+ * "not checked" rather than "nothing found". */
122
+ existingRules: zod_1.z.array(zod_1.z.unknown()).optional(),
97
123
  rule: zod_1.z.unknown().optional(),
98
124
  installNonce: zod_1.z.number().int().positive().optional(),
99
125
  interpreterAddress: zod_1.z.string().optional(),
@@ -36,7 +36,8 @@ function registerTools(server) {
36
36
  server.tool('synthesize_policy', 'Synthesize a ProposedPolicy from a RecordedTransaction (`source: recording`).', schemas_ts_1.SynthesizePolicyToolShape, (args) => (0, run_1.runSynthesizePolicy)(args).then(toCallToolResult));
37
37
  server.tool('simulate_policy', 'Evaluate a predicate against one recorded call and report permit/deny with the deny reason. The evaluator is a second implementation of the on-chain semantics, cross-checked against the Rust interpreter by the conformance harness, so a verdict here is a claim about what the contract would do. Pass the `predicate` returned by `synthesize_policy` under `explain`.', schemas_ts_1.SimulatePolicyToolShape, (args) => toCallToolResult((0, run_1.runSimulatePolicy)(args)));
38
38
  server.tool('verify_policy', 'Check a predicate against the transaction it was synthesised from, plus a generated deny case per dimension. Reports `ok` only when the permit case is permitted AND every deny case is denied - a denied permit case means the policy is too strict, a permitted deny case means it is too loose.', schemas_ts_1.VerifyPolicyToolShape, (args) => toCallToolResult((0, run_1.runVerifyPolicy)(args)));
39
- 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.', schemas_ts_1.InstallPolicyToolShape, (args) => (0, run_1.runInstallPolicy)(args).then(toCallToolResult));
39
+ server.tool('declare_policy', 'Build an interpreter predicate from a DECLARED constraint - the method to pin, and optionally the contract, a per-call amount cap and a recipient allowlist. Use this when there is no transaction to record, or when `record_transaction` refuses a contract it does not recognise. Returns the predicate tree, its canonical encoding and hash, ready for `install_policy`. `warnings` names any argument index that was GUESSED rather than supplied - a bound on the wrong argument constrains nothing while looking correct, so read them. There is no rolling spend window and no approval threshold: neither is expressible in grammar 3.', schemas_ts_1.DeclarePolicyToolShape, (args) => Promise.resolve((0, run_1.runDeclarePolicy)(args)).then(toCallToolResult));
40
+ 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. Pass `existingRules` to get an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. Omitting them returns `authorityScan: null`, meaning NOT CHECKED. 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.', schemas_ts_1.InstallPolicyToolShape, (args) => (0, run_1.runInstallPolicy)(args).then(toCallToolResult));
40
41
  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.', schemas_ts_1.RevokePolicyToolShape, (args) => (0, run_1.runRevokePolicy)(args).then(toCallToolResult));
41
42
  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.', schemas_ts_1.GetInterpreterInfoToolShape, (args) => (0, run_1.runGetInterpreterInfo)(args).then(toCallToolResult));
42
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediolabs/policy-builder-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "description": "MCP server exposing the OZ policy-synth core (record_transaction + synthesize_policy) over stdio and Streamable HTTP transports.",
6
6
  "type": "module",
@@ -56,14 +56,14 @@
56
56
  },
57
57
  "scripts": {
58
58
  "test": "bun test",
59
- "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
59
+ "build": "rm -rf dist dist-cjs && tsc -p tsconfig.build.json && tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
60
60
  "build:esm": "tsc -p tsconfig.build.json",
61
61
  "build:cjs": "tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
62
62
  "prepublishOnly": "bun run build && bun test",
63
63
  "prepack": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "@crediolabs/policy-synth": "0.3.0",
66
+ "@crediolabs/policy-synth": "0.4.0",
67
67
  "@modelcontextprotocol/sdk": "1.30.0",
68
68
  "@stellar/stellar-sdk": "14.4.0",
69
69
  "zod": "3.25.76"
package/src/schemas.ts CHANGED
@@ -106,17 +106,40 @@ export type {
106
106
  } from '@crediolabs/policy-synth/run'
107
107
 
108
108
  /** Common base for `install_policy` and `revoke_policy`: smartAccount,
109
- * sourceAccount, optional RPC URL, optional base fee. Both share the
110
- * same smart-account context, so the SDK-emitted JSON Schema stays
109
+ * sourceAccount, target network, optional RPC URL, optional base fee. Both
110
+ * share the same smart-account context, so the SDK-emitted JSON Schema stays
111
111
  * identical for those fields. The body re-validates against the strict
112
112
  * schemas in `@crediolabs/policy-synth/run`. */
113
113
  const SmartAccountToolShape = {
114
114
  smartAccount: z.string().min(1).optional(),
115
115
  sourceAccount: z.string().min(1).optional(),
116
+ /** Omitting this from the shape made both tools testnet-ONLY. The input
117
+ * schema defaults `network` to `testnet` and expects a mainnet caller to
118
+ * set it, but a field absent from the tool shape is STRIPPED before the
119
+ * body runs, so an MCP client could not reach the mainnet pin at all. The
120
+ * failure presented as a deliberate testnet pin rather than as a missing
121
+ * parameter, which sent integrators to build the install by hand. */
122
+ network: NetworkSchema.optional(),
116
123
  rpcUrl: z.string().url().optional(),
117
124
  baseFee: z.number().int().positive().optional(),
118
125
  }
119
126
 
127
+ /** Flat ZodRawShape for `declare_policy`. The DECLARATIVE front-end: the
128
+ * constraint stated outright, with no transaction to decode. Every field is
129
+ * optional except `fn`; the body re-validates against the strict
130
+ * `DeclarePolicyInputSchema`, which is `.strict()` and so refuses the removed
131
+ * MandateSpec fields (`spendingLimit`, `approvalThreshold`) rather than
132
+ * accepting them in silence. */
133
+ export const DeclarePolicyToolShape = {
134
+ fn: z.string().min(1),
135
+ contract: z.string().optional(),
136
+ maxAmount: z.string().optional(),
137
+ amountArgIndex: z.number().int().nonnegative().optional(),
138
+ recipients: z.array(z.string()).optional(),
139
+ recipientArgIndex: z.number().int().nonnegative().optional(),
140
+ allowZeroCap: z.boolean().optional(),
141
+ } as const
142
+
120
143
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
121
144
  * at the tool boundary because the rule schema is a discriminated union
122
145
  * the SDK does not accept at registration; the body re-validates
@@ -126,6 +149,10 @@ const SmartAccountToolShape = {
126
149
  * extracts them from there. */
127
150
  export const InstallPolicyToolShape = {
128
151
  ...SmartAccountToolShape,
152
+ /** Rules already on the account. Supplying them turns on the cross-rule
153
+ * authority scan; omitting them returns `authorityScan: null`, which means
154
+ * "not checked" rather than "nothing found". */
155
+ existingRules: z.array(z.unknown()).optional(),
129
156
  rule: z.unknown().optional(),
130
157
  installNonce: z.number().int().positive().optional(),
131
158
  interpreterAddress: z.string().optional(),
package/src/server.ts CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type { ToolResponse } from '@crediolabs/policy-synth'
15
15
  import {
16
+ runDeclarePolicy,
16
17
  runGetInterpreterInfo,
17
18
  runInstallPolicy,
18
19
  runRecordTransaction,
@@ -24,6 +25,7 @@ import {
24
25
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
25
26
  import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
26
27
  import {
28
+ DeclarePolicyToolShape,
27
29
  GetInterpreterInfoToolShape,
28
30
  InstallPolicyToolShape,
29
31
  RecordTransactionToolShape,
@@ -83,9 +85,16 @@ export function registerTools(server: McpServer): void {
83
85
  (args) => toCallToolResult(runVerifyPolicy(args))
84
86
  )
85
87
 
88
+ server.tool(
89
+ 'declare_policy',
90
+ 'Build an interpreter predicate from a DECLARED constraint - the method to pin, and optionally the contract, a per-call amount cap and a recipient allowlist. Use this when there is no transaction to record, or when `record_transaction` refuses a contract it does not recognise. Returns the predicate tree, its canonical encoding and hash, ready for `install_policy`. `warnings` names any argument index that was GUESSED rather than supplied - a bound on the wrong argument constrains nothing while looking correct, so read them. There is no rolling spend window and no approval threshold: neither is expressible in grammar 3.',
91
+ DeclarePolicyToolShape,
92
+ (args) => Promise.resolve(runDeclarePolicy(args)).then(toCallToolResult)
93
+ )
94
+
86
95
  server.tool(
87
96
  'install_policy',
88
- '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.',
97
+ 'Build an UNSIGNED Soroban transaction XDR for `account.add_context_rule(...)` that installs a new policy rule on the given smart account. Pass `existingRules` to get an `authorityScan`: every rule already on the account that a signer of this install could name INSTEAD, including an unpoliced one against which the predicate never runs - a predicate only constrains a key when the policed rule is the only rule that key is on. Omitting them returns `authorityScan: null`, meaning NOT CHECKED. 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.',
89
98
  InstallPolicyToolShape,
90
99
  (args) => runInstallPolicy(args).then(toCallToolResult)
91
100
  )