@crediolabs/policy-builder-mcp 0.1.6 → 0.1.7

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.
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ // apps/policy-builder-mcp/src/transports/http.ts
3
+ //
4
+ // Streamable HTTP transport (hosted). Uses the Node http module directly so
5
+ // the package stays thin - no express / hono dep. We run in STATELESS mode
6
+ // (sessionIdGenerator: undefined) so each POST /mcp is its own transaction:
7
+ // this matches the brief's "stateless across calls" invariant.
8
+ //
9
+ // Single endpoint: POST /mcp (the SDK also accepts GET for SSE streaming, but
10
+ // the T1 surface does not emit server-initiated messages so we omit it).
11
+ // Listens on 127.0.0.1 by default; pass `host: '0.0.0.0'` to expose.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.startHttpServer = startHttpServer;
14
+ const node_http_1 = require("node:http");
15
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
16
+ const server_ts_1 = require("../server.js");
17
+ /** Stateless Streamable HTTP server. Resolves once the server is listening.
18
+ * The returned handle exposes `close()` for tests + clean shutdown. */
19
+ async function startHttpServer(opts) {
20
+ const host = opts.host ?? '127.0.0.1';
21
+ const path = opts.path ?? '/mcp';
22
+ const server = (0, server_ts_1.createMcpServer)();
23
+ // One transport per server (the SDK reuses the transport for every request
24
+ // in stateless mode). We connect it once at startup and reuse it.
25
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
26
+ sessionIdGenerator: undefined,
27
+ });
28
+ await server.connect(transport);
29
+ const httpServer = (0, node_http_1.createServer)(async (req, res) => {
30
+ if (!req.url) {
31
+ sendJson(res, 400, { error: 'missing url' });
32
+ return;
33
+ }
34
+ const url = new URL(req.url, `http://${host}`);
35
+ if (url.pathname !== path) {
36
+ sendJson(res, 404, { error: 'not found', path: url.pathname });
37
+ return;
38
+ }
39
+ if (req.method !== 'POST') {
40
+ // The SDK ignores non-POST in stateless mode (no GET/SSE needed in T1).
41
+ sendJson(res, 405, { error: 'method not allowed', method: req.method ?? null });
42
+ return;
43
+ }
44
+ // Reject an over-cap body fast via Content-Length; the streaming reader is
45
+ // the backstop for chunked/unknown-length requests.
46
+ const declaredLength = Number(req.headers['content-length']);
47
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
48
+ sendJson(res, 413, { error: 'request body too large' });
49
+ return;
50
+ }
51
+ // Read the JSON-RPC body. The SDK accepts a pre-parsed body, so we parse
52
+ // it here rather than the SDK trying to re-stream the raw req.
53
+ let body;
54
+ try {
55
+ body = await readJsonBody(req);
56
+ }
57
+ catch (e) {
58
+ if (e instanceof BodyTooLargeError) {
59
+ sendJson(res, 413, { error: 'request body too large' });
60
+ }
61
+ else {
62
+ sendJson(res, 400, { error: 'invalid JSON body' });
63
+ }
64
+ return;
65
+ }
66
+ // The T1 surface is one request per call. A JSON-RPC batch (array) is
67
+ // rejected explicitly rather than silently dropping the extra calls.
68
+ if (Array.isArray(body)) {
69
+ sendJson(res, 400, {
70
+ error: 'JSON-RPC batch requests are not supported; send one request per call',
71
+ });
72
+ return;
73
+ }
74
+ try {
75
+ // `handleRequest` writes the response and returns once the message has
76
+ // been dispatched. No shared state across calls in stateless mode.
77
+ await transport.handleRequest(req, res, body);
78
+ }
79
+ catch {
80
+ // The SDK normally writes structured errors itself; this is a belt +
81
+ // braces guard so a thrown error does not leave the socket hanging.
82
+ if (!res.headersSent) {
83
+ const error = {
84
+ code: 'SYNTHESIS_ERROR',
85
+ message: 'internal server error',
86
+ severity: 'error',
87
+ retryable: false,
88
+ };
89
+ sendJson(res, 500, { error });
90
+ }
91
+ else
92
+ res.end();
93
+ }
94
+ });
95
+ await new Promise((resolve, reject) => {
96
+ httpServer.once('error', reject);
97
+ httpServer.listen(opts.port, host, () => {
98
+ httpServer.off('error', reject);
99
+ resolve();
100
+ });
101
+ });
102
+ return {
103
+ port: opts.port,
104
+ host,
105
+ path,
106
+ close: async () => {
107
+ await new Promise((resolve) => httpServer.close(() => resolve()));
108
+ await transport.close().catch(() => { });
109
+ await server.close().catch(() => { });
110
+ },
111
+ };
112
+ }
113
+ function sendJson(res, status, body) {
114
+ res.statusCode = status;
115
+ res.setHeader('content-type', 'application/json');
116
+ res.end(JSON.stringify(body));
117
+ }
118
+ /** Reject bodies larger than this before buffering the whole payload - a
119
+ * recorded transaction is far smaller, so this only stops abusive requests. */
120
+ const MAX_BODY_BYTES = 1_048_576;
121
+ /** Distinguishes an over-cap body from a malformed one so the handler can send
122
+ * a 413 rather than a misleading 400. */
123
+ class BodyTooLargeError extends Error {
124
+ }
125
+ async function readJsonBody(req) {
126
+ const chunks = [];
127
+ let total = 0;
128
+ for await (const chunk of req) {
129
+ total += chunk.length;
130
+ if (total > MAX_BODY_BYTES)
131
+ throw new BodyTooLargeError('request body too large');
132
+ chunks.push(chunk);
133
+ }
134
+ if (chunks.length === 0)
135
+ return {};
136
+ const raw = Buffer.concat(chunks).toString('utf8');
137
+ if (!raw)
138
+ return {};
139
+ return JSON.parse(raw);
140
+ }
@@ -0,0 +1 @@
1
+ export declare function startStdioServer(): Promise<void>;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ // apps/policy-builder-mcp/src/transports/stdio.ts
3
+ //
4
+ // stdio transport (Claude Desktop / local agents). The MCP SDK reads JSON-RPC
5
+ // from stdin and writes to stdout. Each process serves ONE client and exits
6
+ // when the client closes the stream. No key custody, no global state; the
7
+ // per-call state lives entirely in the McpServer instance.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.startStdioServer = startStdioServer;
10
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
11
+ const server_ts_1 = require("../server.js");
12
+ async function startStdioServer() {
13
+ const server = (0, server_ts_1.createMcpServer)();
14
+ const transport = new stdio_js_1.StdioServerTransport();
15
+ await server.connect(transport);
16
+ // The transport owns the process from here; no shutdown signal handling -
17
+ // SIGPIPE / EOF on stdin terminates the loop naturally.
18
+ }
package/package.json CHANGED
@@ -1,16 +1,43 @@
1
1
  {
2
2
  "name": "@crediolabs/policy-builder-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
7
7
  "main": "./dist/src/index.js",
8
8
  "types": "./dist/src/index.d.ts",
9
+ "engines": {
10
+ "node": ">=22.12",
11
+ "bun": ">=1.3.0"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/untangledfinance/oz-policy-builder.git",
16
+ "directory": "apps/policy-builder-mcp"
17
+ },
18
+ "homepage": "https://github.com/untangledfinance/oz-policy-builder#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/untangledfinance/oz-policy-builder/issues"
21
+ },
22
+ "keywords": [
23
+ "stellar",
24
+ "soroban",
25
+ "openzeppelin",
26
+ "policy",
27
+ "smart-account",
28
+ "authorization"
29
+ ],
30
+ "sideEffects": false,
31
+ "author": {
32
+ "name": "Untangled Finance Limited",
33
+ "url": "https://untangled.finance"
34
+ },
9
35
  "exports": {
10
36
  ".": {
11
37
  "types": "./dist/src/index.d.ts",
12
38
  "bun": "./src/index.ts",
13
39
  "import": "./dist/src/index.js",
40
+ "require": "./dist-cjs/src/index.js",
14
41
  "default": "./dist/src/index.js"
15
42
  },
16
43
  "./package.json": "./package.json"
@@ -20,6 +47,7 @@
20
47
  },
21
48
  "files": [
22
49
  "dist",
50
+ "dist-cjs",
23
51
  "src",
24
52
  "!src/**/*.test.ts"
25
53
  ],
@@ -28,15 +56,20 @@
28
56
  },
29
57
  "scripts": {
30
58
  "test": "bun test",
31
- "build": "tsc -p tsconfig.build.json"
59
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
60
+ "build:esm": "tsc -p tsconfig.build.json",
61
+ "build:cjs": "tsc -p tsconfig.build.cjs.json && node scripts/write-cjs-package-json.cjs",
62
+ "prepublishOnly": "bun run build && bun test",
63
+ "prepack": "bun run build"
32
64
  },
33
65
  "dependencies": {
34
- "@crediolabs/policy-synth": "^0.1.4",
66
+ "@crediolabs/policy-synth": "0.1.5",
35
67
  "@modelcontextprotocol/sdk": "1.18.1",
36
68
  "zod": "3.25.76"
37
69
  },
38
70
  "devDependencies": {
39
71
  "@biomejs/biome": "2.5.5",
72
+ "@types/bun": "^1.3.0",
40
73
  "@types/node": "^26.1.1",
41
74
  "typescript": "5.9.3"
42
75
  }
package/src/index.ts CHANGED
@@ -1,21 +1,17 @@
1
1
  // apps/policy-builder-mcp/src/index.ts - public re-exports for the MCP server package.
2
2
 
3
3
  export {
4
- RecordTransactionInputSchema,
4
+ type RunRecordTransactionInput,
5
+ type RunSynthesizePolicyInput,
6
+ runRecordTransaction,
7
+ runSynthesizePolicy,
8
+ } from '@crediolabs/policy-synth/run'
9
+ export {
5
10
  RecordTransactionToolShape,
6
- SynthesizePolicyInputSchema,
7
- SynthesizePolicyMandateInputSchema,
8
- SynthesizePolicyRecordingInputSchema,
9
11
  SynthesizePolicyToolShape,
10
12
  } from './schemas.ts'
11
13
  export { createMcpServer, registerTools } from './server.ts'
12
14
  export type { McpToolError, McpToolResult } from './tools/result.ts'
13
15
  export { mcpErrorFromCore, mcpResultFromCore } from './tools/result.ts'
14
- export {
15
- type RunRecordTransactionInput,
16
- type RunSynthesizePolicyInput,
17
- runRecordTransaction,
18
- runSynthesizePolicy,
19
- } from './tools/run.ts'
20
16
  export { startHttpServer } from './transports/http.ts'
21
17
  export { startStdioServer } from './transports/stdio.ts'
package/src/schemas.ts CHANGED
@@ -1,172 +1,55 @@
1
1
  // apps/policy-builder-mcp/src/schemas.ts
2
2
  //
3
- // Zod schemas mirroring the policy-synth core domain types. These are the
4
- // public input / output shapes exposed over MCP and the CLI. They are kept
5
- // hand-written (rather than derived) because the MCP SDK needs a runtime
6
- // Zod object at the transport boundary; a drift test asserts they stay in
7
- // step with the TS source of truth.
3
+ // MCP-only tool shapes. The CORE input/output schemas and the MCP server
4
+ // registrations all live on `@crediolabs/policy-synth` (the tool-body glue
5
+ // sits at `@crediolabs/policy-synth/run`; the underlying Zod input schemas
6
+ // live at `@crediolabs/policy-synth/run`). What stays here is just the flat
7
+ // `ZodRawShape` needed by `@modelcontextprotocol/sdk`'s `tool()` registration
8
+ // API, which does not accept the strict discriminated union `synthesize_policy`
9
+ // needs.
8
10
  //
9
- // i128 amounts and other large integers are carried as base-10 decimal strings
10
- // end-to-end (no JS number coercion). Networks are pinned to the same closed
11
- // set the core defines. The discriminated union on `source` exposes BOTH
12
- // synthesize_policy front-ends through a single tool input.
13
-
11
+ // The body of every tool call re-validates against the strict schemas via
12
+ // `runRecordTransaction` / `runSynthesizePolicy`, so wire inputs still
13
+ // fail closed. The mutual-exclusion rules (e.g. exactly-one-of hash/xdr)
14
+ // live on the strict schemas, NOT on the tool shape; the SDK does not
15
+ // invoke `.refine()` at registration time, so any refined rule must be
16
+ // re-checked in the body.
17
+ //
18
+ // The tool-shape fields below are hand-rolled simple types rather than
19
+ // references into the refined strict schemas (`.refine()` returns
20
+ // `ZodEffects`, which has no `.shape`). They MUST stay in lockstep with
21
+ // the strict schemas - a drift in field type or optionality breaks the
22
+ // SDK's emitted JSON Schema.
23
+
24
+ import {
25
+ ComposeUserResponsesSchema,
26
+ InterpreterOptionsSchema,
27
+ MandateSpecSchema,
28
+ NetworkSchema,
29
+ OzAdapterConfigSchema,
30
+ RecordedTransactionSchema,
31
+ RecordTransactionInputSchema,
32
+ SynthesizePolicyInputSchema,
33
+ ToolErrorSchema,
34
+ } from '@crediolabs/policy-synth/run'
14
35
  import { z } from 'zod'
15
36
 
16
- /** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
17
- * installed on-chain, so reject it at the boundary (fail-closed). */
18
- const U32_MAX = 4294967295
19
- /** Upper bound on top-level invocations in a recorded transaction. A real
20
- * Stellar tx caps operations well below this; the bound stops a hand-crafted
21
- * payload from turning one request into an unbounded synthesis (DoS). */
22
- const MAX_INVOCATIONS = 512
23
-
24
- export const NetworkSchema = z.enum(['mainnet', 'testnet'])
25
- export type Network = z.infer<typeof NetworkSchema>
26
-
27
- /** ScVal subset - normalised subset the synth consumes. Mirrors
28
- * `ScVal` in packages/policy-synth/src/types.ts. */
29
- export const ScValSchema: z.ZodType<unknown> = z.lazy(() =>
30
- z.union([
31
- z.object({ type: z.literal('address'), value: z.string() }),
32
- // i128 is SIGNED: real events carry negatives (e.g. a fee-adjustment/refund),
33
- // so the recorder's own output must round-trip through this schema. u64/u32
34
- // are unsigned and stay non-negative.
35
- z.object({ type: z.literal('i128'), value: z.string().regex(/^-?[0-9]+$/) }),
36
- z.object({ type: z.literal('u64'), value: z.string().regex(/^[0-9]+$/) }),
37
- z.object({ type: z.literal('u32'), value: z.string().regex(/^[0-9]+$/) }),
38
- z.object({ type: z.literal('symbol'), value: z.string() }),
39
- z.object({ type: z.literal('vec'), value: z.array(ScValSchema) }),
40
- z.object({ type: z.literal('bytes'), value: z.string() }),
41
- z.object({ type: z.literal('other'), value: z.string() }),
42
- ])
43
- )
44
-
45
- /** ContractInvocation mirrors the core. Annotated with an explicit
46
- * `z.ZodType<unknown>` (like ScValSchema above) so the self-referential
47
- * `subInvocations` field does not trip TS's circular type inference. */
48
- export const ContractInvocationSchema: z.ZodType<unknown> = z.object({
49
- contract: z.string(),
50
- fn: z.string(),
51
- args: z.array(ScValSchema),
52
- subInvocations: z.array(z.lazy(() => ContractInvocationSchema)),
53
- })
54
-
55
- export const TokenMovementSchema = z.object({
56
- token: z.string(),
57
- from: z.string(),
58
- to: z.string(),
59
- // The recorder reads the amount straight from the signed i128 event value
60
- // (record/movements.ts readAmount), so a non-standard token that emits a
61
- // negative transfer/mint/burn amount round-trips as a negative string. Mirror
62
- // that here; the synth gate, not the wire schema, decides what to do with it.
63
- amount: z.string().regex(/^-?[0-9]+$/),
64
- })
65
-
66
- export const OnChainEventSchema = z.object({
67
- contract: z.string(),
68
- topics: z.array(z.string()),
69
- data: ScValSchema,
70
- })
71
-
72
- export const ParseConfidenceSchema = z.object({
73
- overall: z.number().min(0).max(1),
74
- knownContracts: z.array(z.string()),
75
- unknownContracts: z.array(
76
- z.object({
77
- contract: z.string(),
78
- reason: z.enum(['no-abi', 'version-mismatch', 'opaque-result']),
79
- })
80
- ),
81
- opaqueScVals: z.array(z.object({ path: z.string(), type: z.string() })),
82
- thresholdUsed: z.number().min(0).max(1),
83
- })
84
-
85
- /** RecordedTransaction mirrors the core RecordedTransaction. The output shape
86
- * is referenced by name in the tool result structured content; we deliberately
87
- * type it loosely (`z.unknown()`) on the success path so the core remains the
88
- * single source of truth for the wire payload. */
89
- export const RecordedTransactionSchema = z
90
- .object({
91
- network: NetworkSchema,
92
- signers: z.array(z.string()),
93
- invocations: z.array(ContractInvocationSchema).max(MAX_INVOCATIONS),
94
- tokenMovements: z.array(TokenMovementSchema),
95
- events: z.array(OnChainEventSchema),
96
- authEntries: z.array(z.unknown()),
97
- ledgerSequence: z.number().int().nonnegative(),
98
- fetchedAt: z.number().int().nonnegative(),
99
- parseConfidence: ParseConfidenceSchema,
100
- sourceAccount: z.string(),
101
- })
102
- .passthrough()
103
-
104
- /** MandateSpec mirrors the core MandateSpec. The deterministic Mandate
105
- * front-end needs no parseConfidence; the tool adapter injects the full
106
- * confidence after synthesis so the orchestrator can compare. */
107
- export const MandateSpecSchema = z
108
- .object({
109
- chain: z.literal('stellar'),
110
- contract: z.string(),
111
- method: z.string().optional(),
112
- spendingLimit: z
113
- .object({
114
- token: z.string(),
115
- limit: z.string().regex(/^[0-9]+$/),
116
- windowSeconds: z.number().int().positive(),
117
- })
118
- .optional(),
119
- // A threshold of 0 means "0 approvals", which is not a real M-of-N gate.
120
- approvalThreshold: z.number().int().positive().optional(),
121
- recipients: z.array(z.string()).optional(),
122
- expiry: z
123
- .object({
124
- validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
125
- validUntilUnixSeconds: z.number().int().positive().optional(),
126
- })
127
- .optional(),
128
- })
129
- .passthrough()
130
-
131
- /** ComposeUserResponses mirrors the core. */
132
- export const ComposeUserResponsesSchema = z
133
- .object({
134
- windowSeconds: z.number().int().positive().optional(),
135
- validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
136
- limitAmount: z
137
- .string()
138
- .regex(/^[0-9]+$/)
139
- .optional(),
140
- invocationLimit: z.number().int().positive().optional(),
141
- })
142
- .passthrough()
143
-
144
- /** OzAdapterConfig - the per-network OZ built-in instance addresses. */
145
- export const OzAdapterConfigSchema = z.object({
146
- network: NetworkSchema,
147
- instances: z.object({
148
- spending_limit: z.string(),
149
- simple_threshold: z.string(),
150
- weighted_threshold: z.string(),
151
- }),
152
- })
153
-
154
- // ===== record_transaction =====
155
-
156
- export const RecordTransactionInputSchema = z
157
- .object({
158
- hash: z.string().min(1).optional(),
159
- xdr: z.string().min(1).optional(),
160
- network: NetworkSchema,
161
- confidenceOverride: z.number().min(0).max(1).optional(),
162
- })
163
- .refine((v) => !(v.hash && v.xdr), {
164
- message: 'provide exactly one of `hash` or `xdr`, not both',
165
- })
166
- .refine((v) => Boolean(v.hash) || Boolean(v.xdr), {
167
- message: 'one of `hash` or `xdr` is required',
168
- })
169
- export type RecordTransactionInput = z.infer<typeof RecordTransactionInputSchema>
37
+ // Re-export the strict schemas so MCP package consumers (and existing tests)
38
+ // still get them from this module. The canonical home is
39
+ // `@crediolabs/policy-synth/run`; the re-exports here are a shim kept for
40
+ // backward compatibility with downstream callers that imported from the MCP
41
+ // package directly.
42
+ export {
43
+ ComposeUserResponsesSchema,
44
+ InterpreterOptionsSchema,
45
+ MandateSpecSchema,
46
+ NetworkSchema,
47
+ OzAdapterConfigSchema,
48
+ RecordedTransactionSchema,
49
+ RecordTransactionInputSchema,
50
+ SynthesizePolicyInputSchema,
51
+ ToolErrorSchema,
52
+ }
170
53
 
171
54
  /** Flat ZodRawShape used for the MCP SDK tool registration. The body
172
55
  * re-validates against `RecordTransactionInputSchema` so the mutual-exclusion
@@ -178,57 +61,6 @@ export const RecordTransactionToolShape = {
178
61
  confidenceOverride: z.number().min(0).max(1).optional(),
179
62
  } as const
180
63
 
181
- // ===== synthesize_policy =====
182
- //
183
- // Discriminated union on `source` exposes BOTH front-ends through ONE tool.
184
- // - `source: 'mandate'` -> calls synthesizeFromMandate
185
- // - `source: 'recording'` -> calls synthesizeFromRecording
186
- //
187
- // The MCP SDK's `tool()` API only accepts a flat ZodRawShape (named
188
- // properties keyed by Zod schemas), so we ALSO export a flat shape used at
189
- // the transport boundary. The discriminated union is the strict body-side
190
- // validator; the tool handler runs BOTH and treats the flat shape as the
191
- // friendly wire contract.
192
-
193
- export const SynthesizePolicyMandateInputSchema = z.object({
194
- source: z.literal('mandate'),
195
- mandate: MandateSpecSchema,
196
- ozConfig: OzAdapterConfigSchema.optional(),
197
- })
198
-
199
- /** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
200
- * express (per-method scoping, invocation-count windows, oracle bounds, exact
201
- * hop paths) lower to a real interpreter predicate document instead of being
202
- * surfaced as warnings. The core deep-validates `smartAccountAddress` (a C...
203
- * contract, not the recording's G... source) and the tighten-only oracle
204
- * bounds; the schema stays light so the core owns the friendly ToolErrors. */
205
- export const InterpreterOptionsSchema = z.object({
206
- smartAccountAddress: z.string(),
207
- installNonce: z.number().int().positive().optional(),
208
- oracleParams: z
209
- .object({
210
- maxStalenessSeconds: z.number().int().positive().optional(),
211
- maxDeviationBps: z.number().int().positive().optional(),
212
- })
213
- .optional(),
214
- })
215
-
216
- export const SynthesizePolicyRecordingInputSchema = z.object({
217
- source: z.literal('recording'),
218
- recordedTx: RecordedTransactionSchema,
219
- network: NetworkSchema,
220
- userResponses: ComposeUserResponsesSchema.optional(),
221
- confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
222
- interpreter: InterpreterOptionsSchema.optional(),
223
- ozConfig: OzAdapterConfigSchema.optional(),
224
- })
225
-
226
- export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
227
- SynthesizePolicyMandateInputSchema,
228
- SynthesizePolicyRecordingInputSchema,
229
- ])
230
- export type SynthesizePolicyInput = z.infer<typeof SynthesizePolicyInputSchema>
231
-
232
64
  /** Flat ZodRawShape used for MCP tool registration. Every field is optional
233
65
  * so the JSON-Schema the SDK exposes to clients does not forbid either
234
66
  * front-end; the body re-validates against the discriminated union. */
@@ -242,27 +74,3 @@ export const SynthesizePolicyToolShape = {
242
74
  interpreter: InterpreterOptionsSchema.optional(),
243
75
  ozConfig: OzAdapterConfigSchema.optional(),
244
76
  } as const
245
-
246
- // ===== Error envelope (canonical) =====
247
- //
248
- // Mirrors ToolError from packages/policy-synth/src/errors.ts. We use a
249
- // `z.string()` for `code` (not an enum) because the core's ErrorCode union
250
- // evolves over time; the transport contract only promises a string code the
251
- // caller can dispatch on. A drift test asserts the canonical codes still
252
- // pass through unchanged.
253
- export const ToolErrorSchema = z
254
- .object({
255
- code: z.string(),
256
- message: z.string(),
257
- severity: z.enum(['info', 'warning', 'error', 'fatal']),
258
- retryable: z.boolean(),
259
- remediation: z
260
- .object({
261
- toolCall: z.object({ name: z.string(), args: z.record(z.unknown()) }).optional(),
262
- userQuestion: z.object({ code: z.string(), question: z.string() }).optional(),
263
- docsUrl: z.string().optional(),
264
- })
265
- .optional(),
266
- details: z.unknown().optional(),
267
- })
268
- .passthrough()
package/src/server.ts CHANGED
@@ -4,18 +4,18 @@
4
4
  // registration uses the official MCP SDK's `tool()` API with ZodRawShape
5
5
  // schemas (the SDK does not accept ZodEffects / discriminated unions at the
6
6
  // tool registration boundary - the known gotcha). The body re-validates
7
- // against the strict discriminated union in src/tools/run.ts so wire inputs
8
- // still fail closed.
7
+ // against the strict discriminated union in `@crediolabs/policy-synth/run`
8
+ // so wire inputs still fail closed.
9
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
13
 
14
+ import { runRecordTransaction, runSynthesizePolicy } from '@crediolabs/policy-synth/run'
14
15
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
15
16
  import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
16
17
  import { RecordTransactionToolShape, SynthesizePolicyToolShape } from './schemas.ts'
17
18
  import { mcpResultFromCore } from './tools/result.ts'
18
- import { runRecordTransaction, runSynthesizePolicy } from './tools/run.ts'
19
19
 
20
20
  /** Build a fresh, stateless MCP server. The caller owns the returned object
21
21
  * and connects it to a single transport (stdio or Streamable HTTP). */
@@ -1,15 +0,0 @@
1
- import { type ProposedPolicy, type RecordedTransaction, type ToolResponse } from '@crediolabs/policy-synth';
2
- import { type RecordTransactionInput, type SynthesizePolicyInput } from '../schemas.ts';
3
- export type RunRecordTransactionInput = RecordTransactionInput;
4
- export type RunSynthesizePolicyInput = SynthesizePolicyInput;
5
- /** `record_transaction` body - wraps `recordTransaction`. The tool input
6
- * matches the core RecordInput minus the injected `fetcher` (the transport
7
- * layer does not own the RPC). Returns the core ToolResponse unchanged. */
8
- export declare function runRecordTransaction(raw: unknown): Promise<ToolResponse<RecordedTransaction>>;
9
- /** `synthesize_policy` body - discriminated union on `source`:
10
- * - `mandate` -> synthesizeFromMandate
11
- * - `recording` -> synthesizeFromRecording
12
- * Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
13
- * letting the agent pick the deterministic or the inferred path. The CLI
14
- * routes the same way. */
15
- export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy>>;