@crediolabs/policy-builder-mcp 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -834,6 +834,37 @@ 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
+ readonly minOutputRatio: z.ZodOptional<z.ZodObject<{
852
+ num: z.ZodString;
853
+ den: z.ZodString;
854
+ inputArgIndex: z.ZodNumber;
855
+ outputArgIndex: z.ZodNumber;
856
+ }, "strip", z.ZodTypeAny, {
857
+ num: string;
858
+ den: string;
859
+ inputArgIndex: number;
860
+ outputArgIndex: number;
861
+ }, {
862
+ num: string;
863
+ den: string;
864
+ inputArgIndex: number;
865
+ outputArgIndex: number;
866
+ }>>;
867
+ };
837
868
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
838
869
  * at the tool boundary because the rule schema is a discriminated union
839
870
  * the SDK does not accept at registration; the body re-validates
@@ -842,6 +873,10 @@ export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, Si
842
873
  * (the policy already carries the encoded predicate); the run-layer
843
874
  * extracts them from there. */
844
875
  export declare const InstallPolicyToolShape: {
876
+ /** Rules already on the account. Supplying them turns on the cross-rule
877
+ * authority scan; omitting them returns `authorityScan: null`, which means
878
+ * "not checked" rather than "nothing found". */
879
+ readonly existingRules: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
845
880
  readonly rule: z.ZodOptional<z.ZodUnknown>;
846
881
  readonly installNonce: z.ZodOptional<z.ZodNumber>;
847
882
  readonly interpreterAddress: z.ZodOptional<z.ZodString>;
@@ -84,6 +84,29 @@ const SmartAccountToolShape = {
84
84
  rpcUrl: z.string().url().optional(),
85
85
  baseFee: z.number().int().positive().optional(),
86
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
+ minOutputRatio: z
102
+ .object({
103
+ num: z.string(),
104
+ den: z.string(),
105
+ inputArgIndex: z.number().int().nonnegative(),
106
+ outputArgIndex: z.number().int().nonnegative(),
107
+ })
108
+ .optional(),
109
+ };
87
110
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
88
111
  * at the tool boundary because the rule schema is a discriminated union
89
112
  * the SDK does not accept at registration; the body re-validates
@@ -93,6 +116,10 @@ const SmartAccountToolShape = {
93
116
  * extracts them from there. */
94
117
  export const InstallPolicyToolShape = {
95
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: z.array(z.unknown()).optional(),
96
123
  rule: z.unknown().optional(),
97
124
  installNonce: z.number().int().positive().optional(),
98
125
  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, a recipient allowlist, and a minimum-output ratio (a swap slippage floor, bounding the output argument against the call's own input). 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 4, and both belong to the OpenZeppelin account layer.", 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. The response carries 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. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". 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,37 @@ 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
+ readonly minOutputRatio: z.ZodOptional<z.ZodObject<{
852
+ num: z.ZodString;
853
+ den: z.ZodString;
854
+ inputArgIndex: z.ZodNumber;
855
+ outputArgIndex: z.ZodNumber;
856
+ }, "strip", z.ZodTypeAny, {
857
+ num: string;
858
+ den: string;
859
+ inputArgIndex: number;
860
+ outputArgIndex: number;
861
+ }, {
862
+ num: string;
863
+ den: string;
864
+ inputArgIndex: number;
865
+ outputArgIndex: number;
866
+ }>>;
867
+ };
837
868
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
838
869
  * at the tool boundary because the rule schema is a discriminated union
839
870
  * the SDK does not accept at registration; the body re-validates
@@ -842,6 +873,10 @@ export type { GetInterpreterInfoInput, InstallPolicyInput, RevokePolicyInput, Si
842
873
  * (the policy already carries the encoded predicate); the run-layer
843
874
  * extracts them from there. */
844
875
  export declare const InstallPolicyToolShape: {
876
+ /** Rules already on the account. Supplying them turns on the cross-rule
877
+ * authority scan; omitting them returns `authorityScan: null`, which means
878
+ * "not checked" rather than "nothing found". */
879
+ readonly existingRules: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
845
880
  readonly rule: z.ZodOptional<z.ZodUnknown>;
846
881
  readonly installNonce: z.ZodOptional<z.ZodNumber>;
847
882
  readonly interpreterAddress: z.ZodOptional<z.ZodString>;
@@ -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; } });
@@ -92,6 +92,29 @@ const SmartAccountToolShape = {
92
92
  rpcUrl: zod_1.z.string().url().optional(),
93
93
  baseFee: zod_1.z.number().int().positive().optional(),
94
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
+ minOutputRatio: zod_1.z
110
+ .object({
111
+ num: zod_1.z.string(),
112
+ den: zod_1.z.string(),
113
+ inputArgIndex: zod_1.z.number().int().nonnegative(),
114
+ outputArgIndex: zod_1.z.number().int().nonnegative(),
115
+ })
116
+ .optional(),
117
+ };
95
118
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
96
119
  * at the tool boundary because the rule schema is a discriminated union
97
120
  * the SDK does not accept at registration; the body re-validates
@@ -101,6 +124,10 @@ const SmartAccountToolShape = {
101
124
  * extracts them from there. */
102
125
  exports.InstallPolicyToolShape = {
103
126
  ...SmartAccountToolShape,
127
+ /** Rules already on the account. Supplying them turns on the cross-rule
128
+ * authority scan; omitting them returns `authorityScan: null`, which means
129
+ * "not checked" rather than "nothing found". */
130
+ existingRules: zod_1.z.array(zod_1.z.unknown()).optional(),
104
131
  rule: zod_1.z.unknown().optional(),
105
132
  installNonce: zod_1.z.number().int().positive().optional(),
106
133
  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, a recipient allowlist, and a minimum-output ratio (a swap slippage floor, bounding the output argument against the call's own input). 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 4, and both belong to the OpenZeppelin account layer.", 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. The response carries 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. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". 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.1",
3
+ "version": "0.5.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",
@@ -63,7 +63,7 @@
63
63
  "prepack": "bun run build"
64
64
  },
65
65
  "dependencies": {
66
- "@crediolabs/policy-synth": "0.3.1",
66
+ "@crediolabs/policy-synth": "0.5.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
@@ -124,6 +124,30 @@ const SmartAccountToolShape = {
124
124
  baseFee: z.number().int().positive().optional(),
125
125
  }
126
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
+ minOutputRatio: z
142
+ .object({
143
+ num: z.string(),
144
+ den: z.string(),
145
+ inputArgIndex: z.number().int().nonnegative(),
146
+ outputArgIndex: z.number().int().nonnegative(),
147
+ })
148
+ .optional(),
149
+ } as const
150
+
127
151
  /** Flat ZodRawShape for `install_policy`. `rule` is typed as `z.unknown()`
128
152
  * at the tool boundary because the rule schema is a discriminated union
129
153
  * the SDK does not accept at registration; the body re-validates
@@ -133,6 +157,10 @@ const SmartAccountToolShape = {
133
157
  * extracts them from there. */
134
158
  export const InstallPolicyToolShape = {
135
159
  ...SmartAccountToolShape,
160
+ /** Rules already on the account. Supplying them turns on the cross-rule
161
+ * authority scan; omitting them returns `authorityScan: null`, which means
162
+ * "not checked" rather than "nothing found". */
163
+ existingRules: z.array(z.unknown()).optional(),
136
164
  rule: z.unknown().optional(),
137
165
  installNonce: z.number().int().positive().optional(),
138
166
  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, a recipient allowlist, and a minimum-output ratio (a swap slippage floor, bounding the output argument against the call's own input). 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 4, and both belong to the OpenZeppelin account layer.",
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. The response carries 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. The account is READ over RPC to build it; pass `existingRules` to supply them yourself instead (useful offline). `authorityScan: null` means NOT CHECKED - the read failed, or it could not account for every live rule - and must never be read as "nothing found". 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
  )