protect-mcp 0.7.2 → 0.7.4

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.
@@ -190,6 +190,7 @@ function signDecision(entry) {
190
190
  if (entry.timing) payload.timing = entry.timing;
191
191
  if (entry.swarm) payload.swarm = entry.swarm;
192
192
  if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
193
+ if (entry.action_readback) payload.action_readback = entry.action_readback;
193
194
  if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
194
195
  const result = artifactsModule.createSignedArtifact(
195
196
  artifactType,
@@ -656,6 +657,95 @@ function handleListApprovals(res, approvalStore) {
656
657
  res.end(JSON.stringify({ grants }));
657
658
  }
658
659
 
660
+ // src/action-readback.ts
661
+ import { createHash as createHash3 } from "crypto";
662
+ var SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|credential|password|secret|session|token|private[_-]?key)/i;
663
+ var DESTINATION_KEYS = [
664
+ "path",
665
+ "file_path",
666
+ "filePath",
667
+ "url",
668
+ "uri",
669
+ "endpoint",
670
+ "host",
671
+ "hostname",
672
+ "repo",
673
+ "repository",
674
+ "branch",
675
+ "channel",
676
+ "to",
677
+ "recipient",
678
+ "symbol",
679
+ "account",
680
+ "bucket",
681
+ "database",
682
+ "table",
683
+ "service"
684
+ ];
685
+ function stableStringify(value) {
686
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
687
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
688
+ const obj = value;
689
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
690
+ }
691
+ function redact(value, path = [], redacted = [], disclosed = [], depth = 0) {
692
+ if (depth > 4) return "[truncated-depth]";
693
+ if (value === null || value === void 0) return value;
694
+ if (typeof value !== "object") {
695
+ if (path.length > 0) disclosed.push(path.join("."));
696
+ if (typeof value === "string" && value.length > 240) return `${value.slice(0, 240)}...`;
697
+ return value;
698
+ }
699
+ if (Array.isArray(value)) {
700
+ return value.slice(0, 20).map((item, idx) => redact(item, [...path, String(idx)], redacted, disclosed, depth + 1));
701
+ }
702
+ const out = {};
703
+ for (const [key, child] of Object.entries(value)) {
704
+ const childPath = [...path, key];
705
+ if (SECRET_KEY_RE.test(key)) {
706
+ redacted.push(childPath.join("."));
707
+ out[key] = "[redacted]";
708
+ continue;
709
+ }
710
+ out[key] = redact(child, childPath, redacted, disclosed, depth + 1);
711
+ }
712
+ return out;
713
+ }
714
+ function firstStringValue(input, keys) {
715
+ for (const key of keys) {
716
+ const value = input[key];
717
+ if (typeof value === "string" && value.trim()) return value.trim();
718
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
719
+ }
720
+ return void 0;
721
+ }
722
+ function actionFor(tool, input) {
723
+ const explicit = firstStringValue(input, ["action", "operation", "method", "verb", "command"]);
724
+ if (explicit) return explicit.length > 90 ? `${explicit.slice(0, 90)}...` : explicit;
725
+ return tool;
726
+ }
727
+ function buildActionReadback(tool, input) {
728
+ const normalized = input && typeof input === "object" && !Array.isArray(input) ? input : { value: input };
729
+ const canonical = stableStringify(normalized);
730
+ const redactedFields = [];
731
+ const disclosedFields = [];
732
+ const payloadPreview = redact(normalized, [], redactedFields, disclosedFields);
733
+ const action = actionFor(tool, normalized);
734
+ const destination = firstStringValue(normalized, DESTINATION_KEYS);
735
+ const summary = destination ? `${tool} -> ${destination}` : `${tool} request`;
736
+ return {
737
+ tool,
738
+ action,
739
+ destination,
740
+ payload_preview: payloadPreview,
741
+ payload_hash: createHash3("sha256").update(canonical).digest("hex"),
742
+ payload_bytes: Buffer.byteLength(canonical, "utf-8"),
743
+ disclosed_fields: [...new Set(disclosedFields)].slice(0, 80),
744
+ redacted_fields: [...new Set(redactedFields)].slice(0, 80),
745
+ summary
746
+ };
747
+ }
748
+
659
749
  export {
660
750
  loadPolicy,
661
751
  getToolPolicy,
@@ -671,5 +761,6 @@ export {
671
761
  policySetFromSource,
672
762
  runEvaluatorSelfTest,
673
763
  ReceiptBuffer,
674
- startStatusServer
764
+ startStatusServer,
765
+ buildActionReadback
675
766
  };
@@ -0,0 +1,410 @@
1
+ import {
2
+ ed25519,
3
+ sha256
4
+ } from "./chunk-LYKNULYU.mjs";
5
+ import {
6
+ bytesToHex,
7
+ hexToBytes,
8
+ randomBytes
9
+ } from "./chunk-D733KAPG.mjs";
10
+
11
+ // node_modules/@noble/hashes/esm/sha256.js
12
+ var sha2562 = sha256;
13
+
14
+ // src/commitments/merkle.ts
15
+ var DOMAIN_LEAF = 0;
16
+ var DOMAIN_INTERNAL = 1;
17
+ function hashLeaf(leafBytes) {
18
+ const buf = new Uint8Array(leafBytes.length + 1);
19
+ buf[0] = DOMAIN_LEAF;
20
+ buf.set(leafBytes, 1);
21
+ return sha2562(buf);
22
+ }
23
+ function hashInternal(left, right) {
24
+ const buf = new Uint8Array(left.length + right.length + 1);
25
+ buf[0] = DOMAIN_INTERNAL;
26
+ buf.set(left, 1);
27
+ buf.set(right, 1 + left.length);
28
+ return sha2562(buf);
29
+ }
30
+ function merkleRoot(leafHashes) {
31
+ if (leafHashes.length === 0) {
32
+ throw new Error("merkleRoot: cannot compute root of empty leaf set");
33
+ }
34
+ if (leafHashes.length === 1) {
35
+ return leafHashes[0];
36
+ }
37
+ const n = leafHashes.length;
38
+ const k = largestPowerOfTwoLessThan(n);
39
+ const left = merkleRoot(leafHashes.slice(0, k));
40
+ const right = merkleRoot(leafHashes.slice(k));
41
+ return hashInternal(left, right);
42
+ }
43
+ function generateProof(leafHashes, index) {
44
+ if (leafHashes.length === 0) {
45
+ throw new Error("generateProof: empty tree");
46
+ }
47
+ if (index < 0 || index >= leafHashes.length) {
48
+ throw new Error(
49
+ `generateProof: index ${index} out of range [0, ${leafHashes.length})`
50
+ );
51
+ }
52
+ const siblings = [];
53
+ collectPath(leafHashes, index, siblings);
54
+ return {
55
+ index,
56
+ treeSize: leafHashes.length,
57
+ siblings: siblings.map((s) => bytesToHex(s))
58
+ };
59
+ }
60
+ function collectPath(leaves, index, out) {
61
+ if (leaves.length === 1) return;
62
+ const n = leaves.length;
63
+ const k = largestPowerOfTwoLessThan(n);
64
+ if (index < k) {
65
+ collectPath(leaves.slice(0, k), index, out);
66
+ out.push(merkleRoot(leaves.slice(k)));
67
+ } else {
68
+ collectPath(leaves.slice(k), index - k, out);
69
+ out.push(merkleRoot(leaves.slice(0, k)));
70
+ }
71
+ }
72
+ function verifyProof(expectedRootHex, leafHash, proof) {
73
+ if (proof.index < 0 || proof.index >= proof.treeSize) return false;
74
+ if (proof.treeSize === 1) {
75
+ return proof.siblings.length === 0 && bytesToHex(leafHash).toLowerCase() === expectedRootHex.toLowerCase();
76
+ }
77
+ let result;
78
+ try {
79
+ result = reconstructRoot(
80
+ leafHash,
81
+ proof.index,
82
+ proof.treeSize,
83
+ proof.siblings
84
+ );
85
+ } catch {
86
+ return false;
87
+ }
88
+ return bytesToHex(result).toLowerCase() === expectedRootHex.toLowerCase();
89
+ }
90
+ function reconstructRoot(leafHash, index, treeSize, siblings) {
91
+ if (treeSize === 1) {
92
+ if (siblings.length !== 0) {
93
+ throw new Error("reconstructRoot: extra siblings at single-leaf level");
94
+ }
95
+ return leafHash;
96
+ }
97
+ if (siblings.length === 0) {
98
+ throw new Error("reconstructRoot: ran out of siblings before single-leaf");
99
+ }
100
+ const k = largestPowerOfTwoLessThan(treeSize);
101
+ const outermostSibling = hexToBytes(siblings[siblings.length - 1]);
102
+ const innerSiblings = siblings.slice(0, -1);
103
+ if (index < k) {
104
+ const leftHash = reconstructRoot(leafHash, index, k, innerSiblings);
105
+ return hashInternal(leftHash, outermostSibling);
106
+ } else {
107
+ const rightHash = reconstructRoot(
108
+ leafHash,
109
+ index - k,
110
+ treeSize - k,
111
+ innerSiblings
112
+ );
113
+ return hashInternal(outermostSibling, rightHash);
114
+ }
115
+ }
116
+ function largestPowerOfTwoLessThan(n) {
117
+ if (n < 2) {
118
+ throw new Error(`largestPowerOfTwoLessThan: n must be >= 2 (got ${n})`);
119
+ }
120
+ let k = 1;
121
+ while (k * 2 < n) k *= 2;
122
+ return k;
123
+ }
124
+
125
+ // src/commitments/primitives.ts
126
+ function jcs(value) {
127
+ if (value === null || value === void 0) return "null";
128
+ if (typeof value === "boolean" || typeof value === "number")
129
+ return JSON.stringify(value);
130
+ if (typeof value === "string") return JSON.stringify(value);
131
+ if (Array.isArray(value))
132
+ return "[" + value.map(jcs).join(",") + "]";
133
+ const obj = value;
134
+ const keys = Object.keys(obj).sort();
135
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + jcs(obj[k])).join(",") + "}";
136
+ }
137
+
138
+ // src/commitments/leaf.ts
139
+ function base64urlNoPad(bytes) {
140
+ const std = typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : btoa(String.fromCharCode(...bytes));
141
+ return std.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
142
+ }
143
+ function base64urlDecode(s) {
144
+ const std = s.replace(/-/g, "+").replace(/_/g, "/");
145
+ const padded = std + "=".repeat((4 - std.length % 4) % 4);
146
+ if (typeof Buffer !== "undefined") {
147
+ return new Uint8Array(Buffer.from(padded, "base64"));
148
+ }
149
+ const bin = atob(padded);
150
+ const out = new Uint8Array(bin.length);
151
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
152
+ return out;
153
+ }
154
+ function encodeLeaf(field) {
155
+ const obj = {
156
+ name: field.name,
157
+ salt: base64urlNoPad(field.salt),
158
+ value: field.value
159
+ };
160
+ const canonical = jcs(obj);
161
+ return new TextEncoder().encode(canonical);
162
+ }
163
+ function sortFields(fields) {
164
+ const encoder = new TextEncoder();
165
+ const decorated = fields.map((f) => ({
166
+ field: f,
167
+ nameBytes: encoder.encode(f.name)
168
+ }));
169
+ decorated.sort((a, b) => compareBytes(a.nameBytes, b.nameBytes));
170
+ return decorated.map((d) => d.field);
171
+ }
172
+ function compareBytes(a, b) {
173
+ const len = Math.min(a.length, b.length);
174
+ for (let i = 0; i < len; i++) {
175
+ if (a[i] !== b[i]) return a[i] - b[i];
176
+ }
177
+ return a.length - b.length;
178
+ }
179
+ function leavesFromFields(fields) {
180
+ const sorted = sortFields(fields);
181
+ const leafBytes = sorted.map(encodeLeaf);
182
+ return { sorted, leafBytes };
183
+ }
184
+
185
+ // src/signing-committed.ts
186
+ function freshSalt() {
187
+ return randomBytes(32);
188
+ }
189
+ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey, kid, issuer) {
190
+ const allFields = {
191
+ tool: entry.tool,
192
+ decision: entry.decision,
193
+ reason_code: entry.reason_code,
194
+ policy_digest: entry.policy_digest,
195
+ scope: entry.request_id,
196
+ mode: entry.mode,
197
+ request_id: entry.request_id
198
+ };
199
+ if (entry.tier) allFields.tier = entry.tier;
200
+ if (entry.credential_ref) allFields.credential_ref = entry.credential_ref;
201
+ if (entry.rate_limit_remaining !== void 0) {
202
+ allFields.rate_limit_remaining = entry.rate_limit_remaining;
203
+ }
204
+ if (entry.policy_engine) allFields.policy_engine = entry.policy_engine;
205
+ if (entry.hook_event) allFields.hook_event = entry.hook_event;
206
+ if (entry.sandbox_state) allFields.sandbox_state = entry.sandbox_state;
207
+ if (entry.timing) allFields.timing = entry.timing;
208
+ if (entry.swarm) allFields.swarm = entry.swarm;
209
+ if (entry.payload_digest) allFields.payload_digest = entry.payload_digest;
210
+ if (entry.deny_iteration) allFields.deny_iteration = entry.deny_iteration;
211
+ const committedFields = [];
212
+ const cleartextFields = {};
213
+ const openings = {};
214
+ for (const [name, value] of Object.entries(allFields)) {
215
+ if (committedFieldNames.includes(name)) {
216
+ const salt = freshSalt();
217
+ committedFields.push({ name, salt, value });
218
+ } else {
219
+ cleartextFields[name] = value;
220
+ }
221
+ }
222
+ let committedFieldsRoot = null;
223
+ if (committedFields.length > 0) {
224
+ const { sorted, leafBytes } = leavesFromFields(committedFields);
225
+ const leafHashes = leafBytes.map(hashLeaf);
226
+ const root = merkleRoot(leafHashes);
227
+ committedFieldsRoot = bytesToHex(root);
228
+ sorted.forEach((f, i) => {
229
+ openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
230
+ });
231
+ }
232
+ const payload = {
233
+ type: "scopeblind.receipt.committed.v1",
234
+ spec: "draft-farley-acta-signed-receipts-01",
235
+ issuer_certification: "self-signed",
236
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
237
+ ...cleartextFields
238
+ };
239
+ if (committedFieldsRoot !== null) {
240
+ payload.committed_fields_root = committedFieldsRoot;
241
+ payload.committed_field_names = committedFields.map((f) => f.name);
242
+ }
243
+ const canonical = jcs(payload);
244
+ const messageHash = sha2562(new TextEncoder().encode(canonical));
245
+ const signatureBytes = ed25519.sign(messageHash, hexToBytes(signingKey));
246
+ const signedReceipt = {
247
+ ...payload,
248
+ signature: {
249
+ alg: "EdDSA",
250
+ kid,
251
+ issuer,
252
+ sig: base64urlNoPad(signatureBytes),
253
+ public_key: publicKey
254
+ // hex
255
+ }
256
+ };
257
+ const signedJson = JSON.stringify(signedReceipt);
258
+ const receiptHash = bytesToHex(sha2562(new TextEncoder().encode(jcs(signedReceipt))));
259
+ return {
260
+ signed: signedJson,
261
+ artifact_type: "decision_receipt_committed_v1",
262
+ openings,
263
+ receipt_hash: receiptHash
264
+ };
265
+ }
266
+ function discloseField(receiptHash, fieldName, openings) {
267
+ const o = openings[fieldName];
268
+ if (!o) {
269
+ throw new Error(`disclose: no opening recorded for field "${fieldName}"`);
270
+ }
271
+ const fields = Object.values(openings).map((op) => ({
272
+ name: op.name,
273
+ salt: op.salt,
274
+ value: op.value
275
+ }));
276
+ const { leafBytes } = leavesFromFields(fields);
277
+ const leafHashes = leafBytes.map(hashLeaf);
278
+ const proof = generateProof(leafHashes, o.index);
279
+ return {
280
+ parent_receipt_hash: receiptHash,
281
+ name: fieldName,
282
+ value: o.value,
283
+ salt: base64urlNoPad(o.salt),
284
+ proof
285
+ };
286
+ }
287
+ function createSelectiveDisclosurePackage(receipt, fieldNames, openings) {
288
+ const receiptHash = receiptHashHex(receipt);
289
+ const committedFieldsRoot = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
290
+ if (!committedFieldsRoot) {
291
+ throw new Error("selective disclosure requires a committed receipt with committed_fields_root");
292
+ }
293
+ const committedFieldNames = committedFieldNamesFromReceipt(receipt, openings);
294
+ const uniqueFields = Array.from(new Set(fieldNames));
295
+ for (const fieldName of uniqueFields) {
296
+ if (!committedFieldNames.includes(fieldName)) {
297
+ throw new Error(`selective disclosure: field "${fieldName}" is not committed by this receipt`);
298
+ }
299
+ }
300
+ const disclosures = uniqueFields.map((fieldName) => discloseField(receiptHash, fieldName, openings));
301
+ const hiddenFields = committedFieldNames.filter((fieldName) => !uniqueFields.includes(fieldName));
302
+ return {
303
+ type: "scopeblind.selective_disclosure.v0",
304
+ version: 0,
305
+ parent_receipt_hash: receiptHash,
306
+ committed_fields_root: committedFieldsRoot,
307
+ disclosed_fields: uniqueFields,
308
+ hidden_fields: hiddenFields,
309
+ disclosures,
310
+ verifier_explanation: {
311
+ summary: "This package opens selected committed receipt fields and leaves the rest hidden.",
312
+ disclosed: uniqueFields.length ? `Disclosed fields: ${uniqueFields.join(", ")}.` : "No fields were disclosed.",
313
+ hidden: hiddenFields.length ? `Hidden committed fields: ${hiddenFields.join(", ")}. Their salted commitments remain bound to the signed receipt root.` : "No committed fields remain hidden.",
314
+ limitation: "Selective Disclosure v0 uses salted SHA-256 commitments and Merkle proofs. It is not a full zero-knowledge proof."
315
+ }
316
+ };
317
+ }
318
+ function verifySelectiveDisclosurePackage(receipt, disclosure) {
319
+ const errors = [];
320
+ if (disclosure.type !== "scopeblind.selective_disclosure.v0") {
321
+ errors.push("disclosure.type is not scopeblind.selective_disclosure.v0");
322
+ }
323
+ const actualReceiptHash = receiptHashHex(receipt);
324
+ const receiptHashValid = disclosure.parent_receipt_hash === actualReceiptHash;
325
+ if (!receiptHashValid) {
326
+ errors.push("parent_receipt_hash does not match the supplied receipt");
327
+ }
328
+ const root = typeof receipt.committed_fields_root === "string" ? receipt.committed_fields_root : "";
329
+ const commitmentRootValid = Boolean(root) && disclosure.committed_fields_root === root;
330
+ if (!commitmentRootValid) {
331
+ errors.push("committed_fields_root does not match the supplied receipt");
332
+ }
333
+ const signatureValid = verifyCommittedReceiptSignature(receipt);
334
+ if (signatureValid === false) {
335
+ errors.push("receipt signature failed verification");
336
+ }
337
+ const committedFieldNames = committedFieldNamesFromReceipt(receipt, {});
338
+ const disclosed = /* @__PURE__ */ new Set();
339
+ for (const item of disclosure.disclosures || []) {
340
+ if (item.parent_receipt_hash !== disclosure.parent_receipt_hash) {
341
+ errors.push(`disclosure for "${item.name}" targets a different receipt hash`);
342
+ continue;
343
+ }
344
+ if (!committedFieldNames.includes(item.name)) {
345
+ errors.push(`field "${item.name}" is not listed in committed_field_names`);
346
+ continue;
347
+ }
348
+ const leafBytes = encodeLeaf({
349
+ name: item.name,
350
+ salt: base64urlDecode(item.salt),
351
+ value: item.value
352
+ });
353
+ const ok = root ? verifyProof(root, hashLeaf(leafBytes), item.proof) : false;
354
+ if (!ok) {
355
+ errors.push(`field "${item.name}" failed Merkle inclusion verification`);
356
+ } else {
357
+ disclosed.add(item.name);
358
+ }
359
+ }
360
+ const disclosedFields = Array.from(disclosed);
361
+ const hiddenFields = committedFieldNames.filter((fieldName) => !disclosed.has(fieldName));
362
+ const valid = errors.length === 0 && receiptHashValid && commitmentRootValid && signatureValid !== false;
363
+ const explanation = [
364
+ valid ? "Selective disclosure verified: the disclosed fields open to the signed receipt commitment root." : "Selective disclosure failed verification.",
365
+ signatureValid === true ? "Receipt signature verified against the embedded Ed25519 public key." : signatureValid === null ? "Receipt signature was not checked because the committed receipt did not carry an embedded Ed25519 signature object." : "Receipt signature did not verify.",
366
+ disclosedFields.length ? `Disclosed fields: ${disclosedFields.join(", ")}.` : "No fields were disclosed.",
367
+ hiddenFields.length ? `Hidden fields: ${hiddenFields.join(", ")}. These remain private but bound to the same commitment root.` : "No committed fields remain hidden.",
368
+ "Limitation: this is salted commitment disclosure, not full zero-knowledge."
369
+ ];
370
+ return {
371
+ valid,
372
+ receipt_hash_valid: receiptHashValid,
373
+ signature_valid: signatureValid,
374
+ commitment_root_valid: commitmentRootValid,
375
+ disclosed_fields: disclosedFields,
376
+ hidden_fields: hiddenFields,
377
+ errors,
378
+ explanation
379
+ };
380
+ }
381
+ function committedFieldNamesFromReceipt(receipt, openings) {
382
+ const fromReceipt = Array.isArray(receipt.committed_field_names) ? receipt.committed_field_names.filter((fieldName) => typeof fieldName === "string") : [];
383
+ const names = fromReceipt.length ? fromReceipt : Object.keys(openings);
384
+ return Array.from(new Set(names)).sort();
385
+ }
386
+ function receiptHashHex(receipt) {
387
+ return bytesToHex(sha2562(new TextEncoder().encode(jcs(receipt))));
388
+ }
389
+ function verifyCommittedReceiptSignature(receipt) {
390
+ const signature = receipt.signature;
391
+ if (!signature || typeof signature !== "object") return null;
392
+ const sig = signature;
393
+ if (sig.alg !== "EdDSA" || typeof sig.sig !== "string" || typeof sig.public_key !== "string") {
394
+ return null;
395
+ }
396
+ const { signature: _signature, ...payloadWithoutSig } = receipt;
397
+ const messageHash = sha2562(new TextEncoder().encode(jcs(payloadWithoutSig)));
398
+ try {
399
+ return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes(sig.public_key));
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+
405
+ export {
406
+ signCommittedDecision,
407
+ discloseField,
408
+ createSelectiveDisclosurePackage,
409
+ verifySelectiveDisclosurePackage
410
+ };
@@ -35040,6 +35040,48 @@ var TOOLS = [
35040
35040
  },
35041
35041
  required: ["environment"]
35042
35042
  }
35043
+ },
35044
+ {
35045
+ name: "github_create_pr",
35046
+ description: "Create a GitHub pull request",
35047
+ inputSchema: {
35048
+ type: "object",
35049
+ properties: {
35050
+ repo: { type: "string", description: "Repository name" },
35051
+ branch: { type: "string", description: "Source branch" },
35052
+ title: { type: "string", description: "Pull request title" }
35053
+ },
35054
+ required: ["repo", "branch", "title"]
35055
+ }
35056
+ },
35057
+ {
35058
+ name: "send_email",
35059
+ description: "Send an email",
35060
+ inputSchema: {
35061
+ type: "object",
35062
+ properties: {
35063
+ to: { type: "string", description: "Recipient email address" },
35064
+ subject: { type: "string", description: "Subject line" },
35065
+ body: { type: "string", description: "Message body" }
35066
+ },
35067
+ required: ["to", "subject"]
35068
+ }
35069
+ },
35070
+ {
35071
+ name: "pms_book_fill",
35072
+ description: "Book a fill into the mock portfolio management system",
35073
+ inputSchema: {
35074
+ type: "object",
35075
+ properties: {
35076
+ account: { type: "string", description: "Portfolio or fund account" },
35077
+ symbol: { type: "string", description: "Instrument symbol" },
35078
+ side: { type: "string", enum: ["BUY", "SELL"] },
35079
+ quantity: { type: "number" },
35080
+ price: { type: "number" },
35081
+ strategy: { type: "string" }
35082
+ },
35083
+ required: ["account", "symbol", "side", "quantity", "price"]
35084
+ }
35043
35085
  }
35044
35086
  ];
35045
35087
  function handleRequest(request) {
@@ -35087,6 +35129,15 @@ Contents: Hello from protect-mcp demo server!`;
35087
35129
  case "deploy":
35088
35130
  resultText = `[demo] Deployed to ${args.environment || "staging"}${args.reason ? ` (reason: ${args.reason})` : ""}`;
35089
35131
  break;
35132
+ case "github_create_pr":
35133
+ resultText = `[demo] Created PR in ${args.repo || "scopeblind/demo"} from ${args.branch || "agent/demo"}: ${args.title || "Agent change"}`;
35134
+ break;
35135
+ case "send_email":
35136
+ resultText = `[demo] Drafted email to ${args.to || "pm@example.com"}: ${args.subject || "Agent update"}`;
35137
+ break;
35138
+ case "pms_book_fill":
35139
+ resultText = `[demo] Booked ${args.side || "BUY"} ${args.quantity || 0} ${args.symbol || "AAPL"} @ ${args.price || 0} into ${args.account || "Demo Fund"} (${args.strategy || "Unassigned"})`;
35140
+ break;
35090
35141
  default:
35091
35142
  resultText = `[demo] Unknown tool: ${toolName}`;
35092
35143
  }
@@ -35159,6 +35210,30 @@ function createSandboxServer() {
35159
35210
  { environment: z.enum(["staging", "production"]).describe("Target environment"), reason: z.string().optional().describe("Deployment reason") },
35160
35211
  async (args) => ({ content: [{ type: "text", text: `[demo] Deployed to ${args.environment}` }] })
35161
35212
  );
35213
+ server.tool("github_create_pr", "Create a GitHub pull request", {
35214
+ repo: z.string(),
35215
+ branch: z.string(),
35216
+ title: z.string()
35217
+ }, async (args) => ({
35218
+ content: [{ type: "text", text: `[demo] PR created in ${args.repo} from ${args.branch}: ${args.title}` }]
35219
+ }));
35220
+ server.tool("send_email", "Send an email", {
35221
+ to: z.string(),
35222
+ subject: z.string(),
35223
+ body: z.string().optional()
35224
+ }, async (args) => ({
35225
+ content: [{ type: "text", text: `[demo] Email prepared for ${args.to}: ${args.subject}` }]
35226
+ }));
35227
+ server.tool("pms_book_fill", "Book a fill into a mock PMS", {
35228
+ account: z.string(),
35229
+ symbol: z.string(),
35230
+ side: z.enum(["BUY", "SELL"]),
35231
+ quantity: z.number(),
35232
+ price: z.number(),
35233
+ strategy: z.string().optional()
35234
+ }, async (args) => ({
35235
+ content: [{ type: "text", text: `[demo] Booked ${args.side} ${args.quantity} ${args.symbol} @ ${args.price} into ${args.account}` }]
35236
+ }));
35162
35237
  return server;
35163
35238
  }
35164
35239