mcpwall 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +168 -105
- package/README.md +125 -11
- package/dist/index.js +654 -29
- package/package.json +5 -5
- package/rules/default.yml +14 -0
- package/rules/profiles/company-laptop.yaml +170 -0
- package/rules/profiles/local-dev.yaml +131 -0
- package/rules/profiles/strict.yaml +284 -0
- package/rules/servers/filesystem-mcp.yaml +92 -0
- package/rules/servers/github-mcp.yaml +70 -0
- package/rules/servers/shell-mcp.yaml +103 -0
- package/rules/strict.yml +54 -0
package/dist/index.js
CHANGED
|
@@ -59,15 +59,35 @@ var ruleSchema = z.object({
|
|
|
59
59
|
action: z.enum(["allow", "deny", "ask"]),
|
|
60
60
|
message: z.string().optional()
|
|
61
61
|
});
|
|
62
|
+
var outboundMatchSchema = z.object({
|
|
63
|
+
tool: z.string().optional(),
|
|
64
|
+
server: z.string().optional(),
|
|
65
|
+
secrets: z.boolean().optional(),
|
|
66
|
+
response_contains: z.array(z.string()).optional(),
|
|
67
|
+
response_contains_regex: z.array(validRegex).optional(),
|
|
68
|
+
response_size_exceeds: z.number().positive().optional()
|
|
69
|
+
}).refine(
|
|
70
|
+
(match) => Object.values(match).some((v) => v !== void 0),
|
|
71
|
+
{ message: "Outbound rule must have at least one match field" }
|
|
72
|
+
);
|
|
73
|
+
var outboundRuleSchema = z.object({
|
|
74
|
+
name: z.string(),
|
|
75
|
+
match: outboundMatchSchema,
|
|
76
|
+
action: z.enum(["allow", "deny", "redact", "log_only"]),
|
|
77
|
+
message: z.string().optional()
|
|
78
|
+
});
|
|
62
79
|
var configSchema = z.object({
|
|
63
80
|
version: z.number(),
|
|
64
81
|
settings: z.object({
|
|
65
82
|
log_dir: z.string(),
|
|
66
83
|
log_level: z.enum(["debug", "info", "warn", "error"]),
|
|
67
84
|
default_action: z.enum(["allow", "deny", "ask"]),
|
|
68
|
-
log_args: z.enum(["full", "none"]).optional()
|
|
85
|
+
log_args: z.enum(["full", "none"]).optional(),
|
|
86
|
+
outbound_default_action: z.enum(["allow", "deny", "redact", "log_only"]).optional(),
|
|
87
|
+
log_redacted: z.enum(["none", "hash", "full"]).optional()
|
|
69
88
|
}),
|
|
70
89
|
rules: z.array(ruleSchema),
|
|
90
|
+
outbound_rules: z.array(outboundRuleSchema).optional(),
|
|
71
91
|
secrets: z.object({
|
|
72
92
|
patterns: z.array(secretPatternSchema)
|
|
73
93
|
}).optional()
|
|
@@ -146,6 +166,20 @@ var DEFAULT_RULES = [
|
|
|
146
166
|
message: "Blocked: detected secret in arguments"
|
|
147
167
|
}
|
|
148
168
|
];
|
|
169
|
+
var DEFAULT_OUTBOUND_RULES = [
|
|
170
|
+
{
|
|
171
|
+
name: "redact-secrets-in-responses",
|
|
172
|
+
match: { secrets: true },
|
|
173
|
+
action: "redact",
|
|
174
|
+
message: "Secret detected in server response and redacted"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "flag-large-responses",
|
|
178
|
+
match: { response_size_exceeds: 102400 },
|
|
179
|
+
action: "log_only",
|
|
180
|
+
message: "Response exceeds 100KB"
|
|
181
|
+
}
|
|
182
|
+
];
|
|
149
183
|
var DEFAULT_CONFIG = {
|
|
150
184
|
version: 1,
|
|
151
185
|
settings: {
|
|
@@ -154,6 +188,7 @@ var DEFAULT_CONFIG = {
|
|
|
154
188
|
default_action: "allow"
|
|
155
189
|
},
|
|
156
190
|
rules: DEFAULT_RULES,
|
|
191
|
+
outbound_rules: DEFAULT_OUTBOUND_RULES,
|
|
157
192
|
secrets: {
|
|
158
193
|
patterns: DEFAULT_SECRET_PATTERNS
|
|
159
194
|
}
|
|
@@ -210,6 +245,10 @@ function mergeConfigs(global, project) {
|
|
|
210
245
|
},
|
|
211
246
|
// Project rules first (higher priority), then global rules
|
|
212
247
|
rules: [...project.rules, ...global.rules],
|
|
248
|
+
outbound_rules: [
|
|
249
|
+
...project.outbound_rules || [],
|
|
250
|
+
...global.outbound_rules || []
|
|
251
|
+
].length > 0 ? [...project.outbound_rules || [], ...global.outbound_rules || []] : void 0,
|
|
213
252
|
secrets: {
|
|
214
253
|
patterns: [
|
|
215
254
|
...project.secrets?.patterns || [],
|
|
@@ -310,6 +349,52 @@ function deepScanObject(obj, patterns) {
|
|
|
310
349
|
}
|
|
311
350
|
return null;
|
|
312
351
|
}
|
|
352
|
+
function redactSecrets(obj, patterns, marker = "[REDACTED BY MCPWALL]") {
|
|
353
|
+
const matchCounts = /* @__PURE__ */ new Map();
|
|
354
|
+
function redactString(str) {
|
|
355
|
+
let result = str;
|
|
356
|
+
for (const pattern of patterns) {
|
|
357
|
+
pattern.regex.lastIndex = 0;
|
|
358
|
+
let match;
|
|
359
|
+
const globalRegex = new RegExp(pattern.regex.source, "g");
|
|
360
|
+
while ((match = globalRegex.exec(result)) !== null) {
|
|
361
|
+
if (pattern.entropy_threshold !== void 0) {
|
|
362
|
+
const entropy = shannonEntropy(match[0]);
|
|
363
|
+
if (entropy < pattern.entropy_threshold) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
matchCounts.set(pattern.name, (matchCounts.get(pattern.name) || 0) + 1);
|
|
368
|
+
result = result.slice(0, match.index) + marker + result.slice(match.index + match[0].length);
|
|
369
|
+
globalRegex.lastIndex = match.index + marker.length;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
function walk(node) {
|
|
375
|
+
if (typeof node === "string") {
|
|
376
|
+
return redactString(node);
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(node)) {
|
|
379
|
+
return node.map(walk);
|
|
380
|
+
}
|
|
381
|
+
if (node && typeof node === "object") {
|
|
382
|
+
const result = {};
|
|
383
|
+
for (const [key, value] of Object.entries(node)) {
|
|
384
|
+
result[key] = walk(value);
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
return node;
|
|
389
|
+
}
|
|
390
|
+
const redacted = walk(obj);
|
|
391
|
+
const matches = Array.from(matchCounts.entries()).map(([pattern, count]) => ({ pattern, count }));
|
|
392
|
+
return {
|
|
393
|
+
redacted,
|
|
394
|
+
matches,
|
|
395
|
+
wasRedacted: matches.length > 0
|
|
396
|
+
};
|
|
397
|
+
}
|
|
313
398
|
function shannonEntropy(str) {
|
|
314
399
|
if (str.length === 0) {
|
|
315
400
|
return 0;
|
|
@@ -497,6 +582,128 @@ var PolicyEngine = class {
|
|
|
497
582
|
}
|
|
498
583
|
};
|
|
499
584
|
|
|
585
|
+
// src/engine/outbound-policy.ts
|
|
586
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
587
|
+
var OutboundPolicyEngine = class {
|
|
588
|
+
rules;
|
|
589
|
+
defaultAction;
|
|
590
|
+
compiledSecrets;
|
|
591
|
+
compiledRegexes = /* @__PURE__ */ new Map();
|
|
592
|
+
constructor(config) {
|
|
593
|
+
this.rules = config.outbound_rules || [];
|
|
594
|
+
this.defaultAction = config.settings.outbound_default_action || "allow";
|
|
595
|
+
this.compiledSecrets = compileSecretPatterns(config.secrets?.patterns || []);
|
|
596
|
+
for (const rule of this.rules) {
|
|
597
|
+
if (rule.match.response_contains_regex) {
|
|
598
|
+
const compiled = rule.match.response_contains_regex.map((pattern) => ({
|
|
599
|
+
source: pattern,
|
|
600
|
+
regex: new RegExp(pattern, "i")
|
|
601
|
+
}));
|
|
602
|
+
this.compiledRegexes.set(rule.name, compiled);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Evaluate a response message against outbound rules.
|
|
608
|
+
* Returns the decision (action + matched rule).
|
|
609
|
+
*/
|
|
610
|
+
evaluate(msg, toolName, serverName) {
|
|
611
|
+
for (const rule of this.rules) {
|
|
612
|
+
if (this.matchesRule(msg, rule, toolName, serverName)) {
|
|
613
|
+
return {
|
|
614
|
+
action: rule.action,
|
|
615
|
+
rule: rule.name,
|
|
616
|
+
message: rule.message
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
action: this.defaultAction,
|
|
622
|
+
rule: null
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Redact secrets from a response message.
|
|
627
|
+
* Returns the redacted message and match details.
|
|
628
|
+
*/
|
|
629
|
+
redactResponse(msg) {
|
|
630
|
+
const redactionResult = redactSecrets(msg.result, this.compiledSecrets);
|
|
631
|
+
const redactedMsg = {
|
|
632
|
+
...msg,
|
|
633
|
+
result: redactionResult.redacted
|
|
634
|
+
};
|
|
635
|
+
return { message: redactedMsg, result: redactionResult };
|
|
636
|
+
}
|
|
637
|
+
matchesRule(msg, rule, toolName, serverName) {
|
|
638
|
+
const match = rule.match;
|
|
639
|
+
if (match.tool) {
|
|
640
|
+
if (!toolName || !minimatch2(toolName, match.tool, { dot: true })) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (match.server) {
|
|
645
|
+
if (!serverName || !minimatch2(serverName, match.server, { dot: true })) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (match.secrets) {
|
|
650
|
+
const text = this.extractResponseText(msg);
|
|
651
|
+
if (!text) return false;
|
|
652
|
+
const hasSecrets = redactSecrets(msg.result, this.compiledSecrets).wasRedacted;
|
|
653
|
+
if (!hasSecrets) return false;
|
|
654
|
+
}
|
|
655
|
+
if (match.response_contains) {
|
|
656
|
+
const text = this.extractResponseText(msg);
|
|
657
|
+
if (!text) return false;
|
|
658
|
+
const lower = text.toLowerCase();
|
|
659
|
+
const found = match.response_contains.some((phrase) => lower.includes(phrase.toLowerCase()));
|
|
660
|
+
if (!found) return false;
|
|
661
|
+
}
|
|
662
|
+
if (match.response_contains_regex) {
|
|
663
|
+
const text = this.extractResponseText(msg);
|
|
664
|
+
if (!text) return false;
|
|
665
|
+
const compiled = this.compiledRegexes.get(rule.name) || [];
|
|
666
|
+
const found = compiled.some((c) => {
|
|
667
|
+
c.regex.lastIndex = 0;
|
|
668
|
+
return c.regex.test(text);
|
|
669
|
+
});
|
|
670
|
+
if (!found) return false;
|
|
671
|
+
}
|
|
672
|
+
if (match.response_size_exceeds !== void 0) {
|
|
673
|
+
const serialized = JSON.stringify(msg.result ?? msg.error ?? "");
|
|
674
|
+
const byteSize = Buffer.byteLength(serialized, "utf-8");
|
|
675
|
+
if (byteSize <= match.response_size_exceeds) return false;
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Extract text content from an MCP response message.
|
|
681
|
+
* Handles MCP standard content array format: { content: [{ type: "text", text: "..." }] }
|
|
682
|
+
* Falls back to JSON.stringify for non-standard formats.
|
|
683
|
+
*/
|
|
684
|
+
extractResponseText(msg) {
|
|
685
|
+
if (msg.result === void 0 && msg.error === void 0) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (msg.error) {
|
|
689
|
+
return msg.error.message || JSON.stringify(msg.error);
|
|
690
|
+
}
|
|
691
|
+
const result = msg.result;
|
|
692
|
+
if (result && Array.isArray(result.content)) {
|
|
693
|
+
const texts = [];
|
|
694
|
+
for (const block of result.content) {
|
|
695
|
+
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
696
|
+
texts.push(block.text);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (texts.length > 0) {
|
|
700
|
+
return texts.join("\n");
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return JSON.stringify(result);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
500
707
|
// src/logger.ts
|
|
501
708
|
import * as fs from "fs";
|
|
502
709
|
import * as path from "path";
|
|
@@ -560,11 +767,12 @@ var Logger = class {
|
|
|
560
767
|
writeToStderr(entry) {
|
|
561
768
|
const timestamp = new Date(entry.ts).toISOString().substring(11, 19);
|
|
562
769
|
const action = this.formatAction(entry.action);
|
|
770
|
+
const direction = entry.direction === "outbound" ? "outbound " : "";
|
|
563
771
|
const method = entry.method || "unknown";
|
|
564
772
|
const tool = entry.tool ? ` ${entry.tool}` : "";
|
|
565
773
|
const rule = entry.rule ? ` [${entry.rule}]` : "";
|
|
566
774
|
const message = entry.message ? ` - ${entry.message}` : "";
|
|
567
|
-
const logLine = `[${timestamp}] ${action} ${method}${tool}${rule}${message}
|
|
775
|
+
const logLine = `[${timestamp}] ${action} ${direction}${method}${tool}${rule}${message}
|
|
568
776
|
`;
|
|
569
777
|
process.stderr.write(logLine);
|
|
570
778
|
}
|
|
@@ -575,9 +783,11 @@ var Logger = class {
|
|
|
575
783
|
getLogLevel(action) {
|
|
576
784
|
switch (action) {
|
|
577
785
|
case "deny":
|
|
786
|
+
case "redact":
|
|
578
787
|
return "warn";
|
|
579
788
|
case "ask":
|
|
580
789
|
case "allow":
|
|
790
|
+
case "log_only":
|
|
581
791
|
return "info";
|
|
582
792
|
default:
|
|
583
793
|
return "info";
|
|
@@ -594,6 +804,12 @@ var Logger = class {
|
|
|
594
804
|
case "ask":
|
|
595
805
|
return "\x1B[33mASK\x1B[0m";
|
|
596
806
|
// yellow
|
|
807
|
+
case "redact":
|
|
808
|
+
return "\x1B[36mREDACT\x1B[0m";
|
|
809
|
+
// cyan
|
|
810
|
+
case "log_only":
|
|
811
|
+
return "\x1B[34mLOG\x1B[0m";
|
|
812
|
+
// blue
|
|
597
813
|
default:
|
|
598
814
|
return action.toUpperCase();
|
|
599
815
|
}
|
|
@@ -668,7 +884,33 @@ function createLineBuffer(onLine) {
|
|
|
668
884
|
|
|
669
885
|
// src/proxy.ts
|
|
670
886
|
function createProxy(options) {
|
|
671
|
-
const { command, args, policyEngine, logger, logArgs = "none" } = options;
|
|
887
|
+
const { command, args, policyEngine, logger, logArgs = "none", outboundPolicyEngine, logRedacted = "none", serverName } = options;
|
|
888
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
889
|
+
const REQUEST_TTL_MS = 6e4;
|
|
890
|
+
function trackRequest(msg) {
|
|
891
|
+
if (msg.id !== void 0 && msg.id !== null && msg.method === "tools/call") {
|
|
892
|
+
const params = msg.params;
|
|
893
|
+
pendingRequests.set(msg.id, {
|
|
894
|
+
tool: params?.name,
|
|
895
|
+
method: msg.method,
|
|
896
|
+
ts: Date.now()
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function resolveRequest(id) {
|
|
901
|
+
if (id === void 0 || id === null) return void 0;
|
|
902
|
+
const ctx = pendingRequests.get(id);
|
|
903
|
+
if (ctx) {
|
|
904
|
+
pendingRequests.delete(id);
|
|
905
|
+
}
|
|
906
|
+
const now = Date.now();
|
|
907
|
+
for (const [key, val] of pendingRequests) {
|
|
908
|
+
if (now - val.ts > REQUEST_TTL_MS) {
|
|
909
|
+
pendingRequests.delete(key);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return ctx;
|
|
913
|
+
}
|
|
672
914
|
const child = spawn(command, args, {
|
|
673
915
|
stdio: ["pipe", "pipe", "inherit"]
|
|
674
916
|
});
|
|
@@ -737,6 +979,7 @@ function createProxy(options) {
|
|
|
737
979
|
return;
|
|
738
980
|
}
|
|
739
981
|
evaluateMessage(msg, decision);
|
|
982
|
+
trackRequest(msg);
|
|
740
983
|
if (child.stdin && !child.stdin.destroyed) {
|
|
741
984
|
child.stdin.write(line + "\n");
|
|
742
985
|
}
|
|
@@ -754,6 +997,7 @@ function createProxy(options) {
|
|
|
754
997
|
}
|
|
755
998
|
} else {
|
|
756
999
|
evaluateMessage(msg, decision);
|
|
1000
|
+
trackRequest(msg);
|
|
757
1001
|
forwarded.push(msg);
|
|
758
1002
|
}
|
|
759
1003
|
}
|
|
@@ -789,22 +1033,113 @@ function createProxy(options) {
|
|
|
789
1033
|
child.stdin.end();
|
|
790
1034
|
}
|
|
791
1035
|
});
|
|
1036
|
+
function evaluateOutbound(msg) {
|
|
1037
|
+
if (!outboundPolicyEngine) {
|
|
1038
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1039
|
+
if (msg.result !== void 0 || msg.error !== void 0) {
|
|
1040
|
+
logger.log({
|
|
1041
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1042
|
+
method: "response",
|
|
1043
|
+
tool: void 0,
|
|
1044
|
+
action: "allow",
|
|
1045
|
+
rule: null,
|
|
1046
|
+
direction: "outbound",
|
|
1047
|
+
message: msg.error ? `Error: ${msg.error.message}` : void 0
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const ctx = resolveRequest(msg.id);
|
|
1053
|
+
const toolName = ctx?.tool;
|
|
1054
|
+
if (msg.result === void 0 && msg.error === void 0) {
|
|
1055
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const decision = outboundPolicyEngine.evaluate(msg, toolName, serverName);
|
|
1059
|
+
switch (decision.action) {
|
|
1060
|
+
case "allow": {
|
|
1061
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1062
|
+
logger.log({
|
|
1063
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1064
|
+
method: "response",
|
|
1065
|
+
tool: toolName,
|
|
1066
|
+
action: "allow",
|
|
1067
|
+
rule: decision.rule,
|
|
1068
|
+
direction: "outbound",
|
|
1069
|
+
message: msg.error ? `Error: ${msg.error.message}` : void 0
|
|
1070
|
+
});
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case "deny": {
|
|
1074
|
+
const blocked = {
|
|
1075
|
+
jsonrpc: "2.0",
|
|
1076
|
+
id: msg.id,
|
|
1077
|
+
result: {
|
|
1078
|
+
content: [{
|
|
1079
|
+
type: "text",
|
|
1080
|
+
text: `[BLOCKED BY MCPWALL] ${decision.message || "Response blocked by outbound policy"}`
|
|
1081
|
+
}]
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
process.stdout.write(JSON.stringify(blocked) + "\n");
|
|
1085
|
+
logger.log({
|
|
1086
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1087
|
+
method: "response",
|
|
1088
|
+
tool: toolName,
|
|
1089
|
+
action: "deny",
|
|
1090
|
+
rule: decision.rule,
|
|
1091
|
+
direction: "outbound",
|
|
1092
|
+
message: decision.message
|
|
1093
|
+
});
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
case "redact": {
|
|
1097
|
+
const { message: redactedMsg, result: redactionResult } = outboundPolicyEngine.redactResponse(msg);
|
|
1098
|
+
process.stdout.write(JSON.stringify(redactedMsg) + "\n");
|
|
1099
|
+
const patternNames = redactionResult.matches.map((m) => m.pattern);
|
|
1100
|
+
logger.log({
|
|
1101
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1102
|
+
method: "response",
|
|
1103
|
+
tool: toolName,
|
|
1104
|
+
action: "redact",
|
|
1105
|
+
rule: decision.rule,
|
|
1106
|
+
direction: "outbound",
|
|
1107
|
+
message: decision.message,
|
|
1108
|
+
redacted_patterns: patternNames.length > 0 ? patternNames : void 0
|
|
1109
|
+
});
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
case "log_only": {
|
|
1113
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1114
|
+
logger.log({
|
|
1115
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1116
|
+
method: "response",
|
|
1117
|
+
tool: toolName,
|
|
1118
|
+
action: "log_only",
|
|
1119
|
+
rule: decision.rule,
|
|
1120
|
+
direction: "outbound",
|
|
1121
|
+
message: decision.message
|
|
1122
|
+
});
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
792
1127
|
const outboundBuffer = createLineBuffer((line) => {
|
|
793
1128
|
try {
|
|
794
|
-
process.stdout.write(line + "\n");
|
|
795
1129
|
const result = parseJsonRpcLineEx(line);
|
|
796
|
-
if (result
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1130
|
+
if (!result) {
|
|
1131
|
+
process.stdout.write(line + "\n");
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (result.type === "single") {
|
|
1135
|
+
evaluateOutbound(result.message);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (result.type === "batch") {
|
|
1139
|
+
for (const msg of result.messages) {
|
|
1140
|
+
evaluateOutbound(msg);
|
|
807
1141
|
}
|
|
1142
|
+
return;
|
|
808
1143
|
}
|
|
809
1144
|
} catch (err) {
|
|
810
1145
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -863,13 +1198,80 @@ function createProxy(options) {
|
|
|
863
1198
|
}
|
|
864
1199
|
|
|
865
1200
|
// src/cli/init.ts
|
|
866
|
-
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
1201
|
+
import { readFile as readFile2, writeFile, mkdir, readdir } from "fs/promises";
|
|
867
1202
|
import { existsSync as existsSync2 } from "fs";
|
|
868
1203
|
import { homedir as homedir3 } from "os";
|
|
869
|
-
import { join as join3 } from "path";
|
|
1204
|
+
import { join as join3, resolve as resolve2, dirname as dirname2 } from "path";
|
|
1205
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
870
1206
|
import { createInterface } from "readline/promises";
|
|
871
1207
|
import { stringify as yamlStringify } from "yaml";
|
|
872
|
-
|
|
1208
|
+
function getProfilesDir() {
|
|
1209
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
1210
|
+
const packageRoot = dirname2(dirname2(thisFile));
|
|
1211
|
+
return join3(packageRoot, "rules", "profiles");
|
|
1212
|
+
}
|
|
1213
|
+
async function listAvailableProfiles() {
|
|
1214
|
+
const profilesDir = getProfilesDir();
|
|
1215
|
+
try {
|
|
1216
|
+
const entries = await readdir(profilesDir);
|
|
1217
|
+
return entries.filter((f) => f.endsWith(".yaml")).map((f) => f.replace(/\.yaml$/, ""));
|
|
1218
|
+
} catch {
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function loadProfile(profile) {
|
|
1223
|
+
if (!/^[a-z0-9-]+$/.test(profile)) {
|
|
1224
|
+
const available = await listAvailableProfiles();
|
|
1225
|
+
process.stderr.write(`[mcpwall] Invalid profile name "${profile}". Names must be lowercase letters, numbers, and hyphens only.
|
|
1226
|
+
`);
|
|
1227
|
+
if (available.length > 0) {
|
|
1228
|
+
process.stderr.write(` Available profiles: ${available.join(", ")}
|
|
1229
|
+
`);
|
|
1230
|
+
}
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
const profilesDir = getProfilesDir();
|
|
1234
|
+
const profilePath = join3(profilesDir, `${profile}.yaml`);
|
|
1235
|
+
const resolvedProfiles = resolve2(profilesDir);
|
|
1236
|
+
const resolvedProfile = resolve2(profilePath);
|
|
1237
|
+
if (!resolvedProfile.startsWith(resolvedProfiles + "/") && resolvedProfile !== resolvedProfiles) {
|
|
1238
|
+
process.stderr.write(`[mcpwall] Invalid profile name "${profile}".
|
|
1239
|
+
`);
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
return await readFile2(profilePath, "utf-8");
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
if (err.code === "ENOENT") {
|
|
1246
|
+
const available = await listAvailableProfiles();
|
|
1247
|
+
const profileList = available.length > 0 ? available.join(", ") : "(none found)";
|
|
1248
|
+
process.stderr.write(`[mcpwall] Unknown profile "${profile}". Available profiles: ${profileList}
|
|
1249
|
+
`);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
throw err;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async function runInit(profile) {
|
|
1256
|
+
if (profile) {
|
|
1257
|
+
if (!/^[a-z0-9-]+$/.test(profile)) {
|
|
1258
|
+
const available2 = await listAvailableProfiles();
|
|
1259
|
+
process.stderr.write(`[mcpwall] Invalid profile name "${profile}". Names must be lowercase letters, numbers, and hyphens only.
|
|
1260
|
+
`);
|
|
1261
|
+
if (available2.length > 0) {
|
|
1262
|
+
process.stderr.write(` Available profiles: ${available2.join(", ")}
|
|
1263
|
+
`);
|
|
1264
|
+
}
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
const available = await listAvailableProfiles();
|
|
1268
|
+
if (!available.includes(profile)) {
|
|
1269
|
+
const profileList = available.length > 0 ? available.join(", ") : "(none found)";
|
|
1270
|
+
process.stderr.write(`[mcpwall] Unknown profile "${profile}". Available profiles: ${profileList}
|
|
1271
|
+
`);
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
873
1275
|
process.stderr.write("\n\u{1F512} mcpwall setup wizard\n\n");
|
|
874
1276
|
const rl = createInterface({
|
|
875
1277
|
input: process.stdin,
|
|
@@ -878,7 +1280,12 @@ async function runInit() {
|
|
|
878
1280
|
try {
|
|
879
1281
|
const configPaths = [
|
|
880
1282
|
{ path: join3(homedir3(), ".claude.json"), name: "Claude Code global config" },
|
|
881
|
-
{ path: join3(process.cwd(), ".mcp.json"), name: "Claude Code project config" }
|
|
1283
|
+
{ path: join3(process.cwd(), ".mcp.json"), name: "Claude Code project config" },
|
|
1284
|
+
{ path: join3(homedir3(), ".cursor", "mcp.json"), name: "Cursor global config" },
|
|
1285
|
+
{ path: join3(process.cwd(), ".cursor", "mcp.json"), name: "Cursor project config" },
|
|
1286
|
+
{ path: join3(homedir3(), ".config", "windsurf", "mcp.json"), name: "Windsurf config" },
|
|
1287
|
+
{ path: join3(homedir3(), ".vscode", "mcp.json"), name: "VS Code global config" },
|
|
1288
|
+
{ path: join3(process.cwd(), ".vscode", "mcp.json"), name: "VS Code project config" }
|
|
882
1289
|
];
|
|
883
1290
|
const foundConfigs = [];
|
|
884
1291
|
for (const { path: path2, name } of configPaths) {
|
|
@@ -898,8 +1305,11 @@ async function runInit() {
|
|
|
898
1305
|
if (foundConfigs.length === 0) {
|
|
899
1306
|
process.stderr.write("No MCP server configurations found.\n");
|
|
900
1307
|
process.stderr.write("Looked for:\n");
|
|
901
|
-
|
|
902
|
-
|
|
1308
|
+
for (const { path: path2, name } of configPaths) {
|
|
1309
|
+
process.stderr.write(` - ${path2} (${name})
|
|
1310
|
+
`);
|
|
1311
|
+
}
|
|
1312
|
+
process.stderr.write("\n");
|
|
903
1313
|
process.stderr.write("You can manually configure mcpwall by wrapping your MCP server commands:\n");
|
|
904
1314
|
process.stderr.write(" Original: npx -y @some/server\n");
|
|
905
1315
|
process.stderr.write(" Wrapped: npx -y mcpwall -- npx -y @some/server\n\n");
|
|
@@ -955,11 +1365,39 @@ async function runInit() {
|
|
|
955
1365
|
}
|
|
956
1366
|
const firewallConfigDir = join3(homedir3(), ".mcpwall");
|
|
957
1367
|
const firewallConfigPath = join3(firewallConfigDir, "config.yml");
|
|
958
|
-
if (!existsSync2(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1368
|
+
if (!existsSync2(firewallConfigDir)) {
|
|
1369
|
+
await mkdir(firewallConfigDir, { recursive: true });
|
|
1370
|
+
}
|
|
1371
|
+
if (profile) {
|
|
1372
|
+
const profileContent = await loadProfile(profile);
|
|
1373
|
+
if (existsSync2(firewallConfigPath)) {
|
|
1374
|
+
const overwrite = await rl.question(
|
|
1375
|
+
`
|
|
1376
|
+
Config already exists: ${firewallConfigPath}
|
|
1377
|
+
Overwrite with "${profile}" profile? (y/n): `
|
|
1378
|
+
);
|
|
1379
|
+
if (overwrite.trim().toLowerCase() !== "y") {
|
|
1380
|
+
process.stderr.write(" Keeping existing config.\n");
|
|
1381
|
+
} else {
|
|
1382
|
+
await writeFile(firewallConfigPath, profileContent, "utf-8");
|
|
1383
|
+
process.stderr.write(`
|
|
1384
|
+
\u2713 Applied "${profile}" profile to ${firewallConfigPath}
|
|
1385
|
+
`);
|
|
1386
|
+
process.stderr.write(` Edit ${firewallConfigPath} to customize.
|
|
1387
|
+
`);
|
|
1388
|
+
}
|
|
1389
|
+
} else {
|
|
1390
|
+
process.stderr.write(`
|
|
1391
|
+
Creating firewall configuration from "${profile}" profile...
|
|
1392
|
+
`);
|
|
1393
|
+
await writeFile(firewallConfigPath, profileContent, "utf-8");
|
|
1394
|
+
process.stderr.write(` \u2713 Created ${firewallConfigPath} using "${profile}" profile
|
|
1395
|
+
`);
|
|
1396
|
+
process.stderr.write(` Edit ${firewallConfigPath} to customize.
|
|
1397
|
+
`);
|
|
962
1398
|
}
|
|
1399
|
+
} else if (!existsSync2(firewallConfigPath)) {
|
|
1400
|
+
process.stderr.write("\nCreating default firewall configuration...\n");
|
|
963
1401
|
const yamlConfig = yamlStringify(DEFAULT_CONFIG);
|
|
964
1402
|
await writeFile(firewallConfigPath, yamlConfig, "utf-8");
|
|
965
1403
|
process.stderr.write(` \u2713 Created ${firewallConfigPath}
|
|
@@ -988,7 +1426,12 @@ import { homedir as homedir4 } from "os";
|
|
|
988
1426
|
import { join as join4 } from "path";
|
|
989
1427
|
var CONFIG_PATHS = [
|
|
990
1428
|
() => join4(homedir4(), ".claude.json"),
|
|
991
|
-
() => join4(process.cwd(), ".mcp.json")
|
|
1429
|
+
() => join4(process.cwd(), ".mcp.json"),
|
|
1430
|
+
() => join4(homedir4(), ".cursor", "mcp.json"),
|
|
1431
|
+
() => join4(process.cwd(), ".cursor", "mcp.json"),
|
|
1432
|
+
() => join4(homedir4(), ".config", "windsurf", "mcp.json"),
|
|
1433
|
+
() => join4(homedir4(), ".vscode", "mcp.json"),
|
|
1434
|
+
() => join4(process.cwd(), ".vscode", "mcp.json")
|
|
992
1435
|
];
|
|
993
1436
|
async function runWrap(serverName) {
|
|
994
1437
|
for (const getPath of CONFIG_PATHS) {
|
|
@@ -1036,6 +1479,174 @@ async function runWrap(serverName) {
|
|
|
1036
1479
|
process.exit(1);
|
|
1037
1480
|
}
|
|
1038
1481
|
|
|
1482
|
+
// src/cli/check.ts
|
|
1483
|
+
var MAX_INPUT_BYTES = 10 * 1024 * 1024;
|
|
1484
|
+
function sanitizeForDisplay(value) {
|
|
1485
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
1486
|
+
return raw.replace(/\x1b\[[0-9;]*[mGKHF]/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
1487
|
+
}
|
|
1488
|
+
async function readStdin() {
|
|
1489
|
+
return new Promise((resolve3, reject) => {
|
|
1490
|
+
let data = "";
|
|
1491
|
+
process.stdin.setEncoding("utf-8");
|
|
1492
|
+
process.stdin.on("data", (chunk) => {
|
|
1493
|
+
data += chunk;
|
|
1494
|
+
});
|
|
1495
|
+
process.stdin.on("end", () => resolve3(data));
|
|
1496
|
+
process.stdin.on("error", reject);
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
function printInboundDecision(decision, msg) {
|
|
1500
|
+
const method = sanitizeForDisplay(msg.method ?? "(unknown)");
|
|
1501
|
+
const params = msg.params;
|
|
1502
|
+
const toolName = params?.name ? sanitizeForDisplay(params.name) : "";
|
|
1503
|
+
const firstArg = params?.arguments ? sanitizeForDisplay(Object.values(params.arguments)[0] ?? "") : "";
|
|
1504
|
+
const contextStr = [method, toolName, firstArg].filter(Boolean).join(" ");
|
|
1505
|
+
if (decision.action === "allow") {
|
|
1506
|
+
process.stdout.write(`\u2713 ALLOW ${contextStr}
|
|
1507
|
+
`);
|
|
1508
|
+
if (decision.rule) {
|
|
1509
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1510
|
+
`);
|
|
1511
|
+
} else {
|
|
1512
|
+
process.stdout.write(` No rule matched \u2014 default action: allow
|
|
1513
|
+
`);
|
|
1514
|
+
}
|
|
1515
|
+
} else if (decision.action === "deny") {
|
|
1516
|
+
process.stdout.write(`\u2717 DENY ${contextStr}
|
|
1517
|
+
`);
|
|
1518
|
+
if (decision.rule) {
|
|
1519
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1520
|
+
`);
|
|
1521
|
+
}
|
|
1522
|
+
if (decision.message) {
|
|
1523
|
+
process.stdout.write(` ${sanitizeForDisplay(decision.message)}
|
|
1524
|
+
`);
|
|
1525
|
+
}
|
|
1526
|
+
} else {
|
|
1527
|
+
process.stdout.write(`? ASK ${contextStr}
|
|
1528
|
+
`);
|
|
1529
|
+
if (decision.rule) {
|
|
1530
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1531
|
+
`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function printOutboundDecision(decision, _msg) {
|
|
1536
|
+
if (decision.action === "allow") {
|
|
1537
|
+
process.stdout.write(`\u2713 ALLOW (response)
|
|
1538
|
+
`);
|
|
1539
|
+
if (decision.rule) {
|
|
1540
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1541
|
+
`);
|
|
1542
|
+
} else {
|
|
1543
|
+
process.stdout.write(` No rule matched
|
|
1544
|
+
`);
|
|
1545
|
+
}
|
|
1546
|
+
} else if (decision.action === "deny") {
|
|
1547
|
+
process.stdout.write(`\u2717 DENY (response)
|
|
1548
|
+
`);
|
|
1549
|
+
if (decision.rule) {
|
|
1550
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1551
|
+
`);
|
|
1552
|
+
}
|
|
1553
|
+
if (decision.message) {
|
|
1554
|
+
process.stdout.write(` ${sanitizeForDisplay(decision.message)}
|
|
1555
|
+
`);
|
|
1556
|
+
}
|
|
1557
|
+
} else if (decision.action === "redact") {
|
|
1558
|
+
process.stdout.write(`~ REDACT (response)
|
|
1559
|
+
`);
|
|
1560
|
+
if (decision.rule) {
|
|
1561
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1562
|
+
`);
|
|
1563
|
+
}
|
|
1564
|
+
if (decision.message) {
|
|
1565
|
+
process.stdout.write(` ${sanitizeForDisplay(decision.message)}
|
|
1566
|
+
`);
|
|
1567
|
+
}
|
|
1568
|
+
} else if (decision.action === "log_only") {
|
|
1569
|
+
process.stdout.write(`! LOG (response)
|
|
1570
|
+
`);
|
|
1571
|
+
if (decision.rule) {
|
|
1572
|
+
process.stdout.write(` Rule: ${sanitizeForDisplay(decision.rule)}
|
|
1573
|
+
`);
|
|
1574
|
+
}
|
|
1575
|
+
if (decision.message) {
|
|
1576
|
+
process.stdout.write(` ${sanitizeForDisplay(decision.message)}
|
|
1577
|
+
`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function runCheck(inputStr, configPath) {
|
|
1582
|
+
if (!inputStr && process.stdin.isTTY) {
|
|
1583
|
+
process.stderr.write(`Usage: mcpwall check --input '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/tmp/test.txt"}}}'
|
|
1584
|
+
`);
|
|
1585
|
+
process.stderr.write(` echo '{"jsonrpc":"2.0",...}' | mcpwall check
|
|
1586
|
+
`);
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
const raw = inputStr ?? await readStdin();
|
|
1590
|
+
if (Buffer.byteLength(raw, "utf-8") > MAX_INPUT_BYTES) {
|
|
1591
|
+
process.stderr.write("[mcpwall] Error: input exceeds 10MB limit\n");
|
|
1592
|
+
process.exit(2);
|
|
1593
|
+
}
|
|
1594
|
+
const trimmed = raw.trim();
|
|
1595
|
+
if (!trimmed) {
|
|
1596
|
+
process.stderr.write("[mcpwall] Error: empty input\n");
|
|
1597
|
+
process.exit(2);
|
|
1598
|
+
}
|
|
1599
|
+
const parsed = parseJsonRpcLineEx(trimmed);
|
|
1600
|
+
if (!parsed) {
|
|
1601
|
+
process.stderr.write('[mcpwall] Error: invalid JSON-RPC message (must have jsonrpc: "2.0")\n');
|
|
1602
|
+
process.exit(2);
|
|
1603
|
+
}
|
|
1604
|
+
let config;
|
|
1605
|
+
try {
|
|
1606
|
+
config = await loadConfig(configPath);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1609
|
+
process.stderr.write(`[mcpwall] Error loading config: ${message}
|
|
1610
|
+
`);
|
|
1611
|
+
process.exit(2);
|
|
1612
|
+
}
|
|
1613
|
+
const inboundEngine = new PolicyEngine(config);
|
|
1614
|
+
const outboundEngine = config.outbound_rules?.length ? new OutboundPolicyEngine(config) : void 0;
|
|
1615
|
+
const messages = parsed.type === "batch" ? parsed.messages : [parsed.message];
|
|
1616
|
+
let anyDenied = false;
|
|
1617
|
+
for (const msg of messages) {
|
|
1618
|
+
if (msg.method) {
|
|
1619
|
+
const decision = inboundEngine.evaluate(msg);
|
|
1620
|
+
printInboundDecision(decision, msg);
|
|
1621
|
+
if (decision.action === "deny") {
|
|
1622
|
+
anyDenied = true;
|
|
1623
|
+
}
|
|
1624
|
+
} else if (msg.result !== void 0 || msg.error !== void 0) {
|
|
1625
|
+
if (outboundEngine) {
|
|
1626
|
+
const decision = outboundEngine.evaluate(msg);
|
|
1627
|
+
printOutboundDecision(decision, msg);
|
|
1628
|
+
if (decision.action === "deny" || decision.action === "redact") {
|
|
1629
|
+
anyDenied = true;
|
|
1630
|
+
}
|
|
1631
|
+
} else {
|
|
1632
|
+
process.stdout.write(`\u2713 ALLOW (response)
|
|
1633
|
+
`);
|
|
1634
|
+
process.stdout.write(` No outbound rules configured
|
|
1635
|
+
`);
|
|
1636
|
+
}
|
|
1637
|
+
} else {
|
|
1638
|
+
const decision = inboundEngine.evaluate(msg);
|
|
1639
|
+
printInboundDecision(decision, msg);
|
|
1640
|
+
if (decision.action === "deny") {
|
|
1641
|
+
anyDenied = true;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (anyDenied) {
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1039
1650
|
// src/index.ts
|
|
1040
1651
|
var require2 = createRequire(import.meta.url);
|
|
1041
1652
|
var { version } = require2("../package.json");
|
|
@@ -1058,6 +1669,7 @@ if (dashDashIndex !== -1) {
|
|
|
1058
1669
|
config.settings.log_level = options.logLevel;
|
|
1059
1670
|
}
|
|
1060
1671
|
const policyEngine = new PolicyEngine(config);
|
|
1672
|
+
const outboundPolicyEngine = config.outbound_rules?.length ? new OutboundPolicyEngine(config) : void 0;
|
|
1061
1673
|
const logger = new Logger({
|
|
1062
1674
|
logDir: config.settings.log_dir,
|
|
1063
1675
|
logLevel: config.settings.log_level
|
|
@@ -1067,7 +1679,9 @@ if (dashDashIndex !== -1) {
|
|
|
1067
1679
|
args,
|
|
1068
1680
|
policyEngine,
|
|
1069
1681
|
logger,
|
|
1070
|
-
logArgs: config.settings.log_args
|
|
1682
|
+
logArgs: config.settings.log_args,
|
|
1683
|
+
outboundPolicyEngine,
|
|
1684
|
+
logRedacted: config.settings.log_redacted
|
|
1071
1685
|
});
|
|
1072
1686
|
} catch (err) {
|
|
1073
1687
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1078,9 +1692,20 @@ if (dashDashIndex !== -1) {
|
|
|
1078
1692
|
})();
|
|
1079
1693
|
} else {
|
|
1080
1694
|
program.name("mcpwall").description("Deterministic security proxy for MCP tool calls").version(version);
|
|
1081
|
-
program.command("init").description("Interactive setup wizard to wrap existing MCP servers").action(async () => {
|
|
1695
|
+
program.command("init").description("Interactive setup wizard to wrap existing MCP servers").option("--profile <name>", "use a named security profile (local-dev, company-laptop, strict)").action(async (options) => {
|
|
1696
|
+
try {
|
|
1697
|
+
await runInit(options.profile);
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1700
|
+
process.stderr.write(`[mcpwall] Error: ${message}
|
|
1701
|
+
`);
|
|
1702
|
+
process.exit(1);
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
program.command("check").description("dry-run: test a JSON-RPC message against your rules without running the proxy").option("--input <json>", "JSON-RPC message as a string (reads from stdin if not provided)").action(async (options) => {
|
|
1706
|
+
const globalOptions = program.opts();
|
|
1082
1707
|
try {
|
|
1083
|
-
await
|
|
1708
|
+
await runCheck(options.input, globalOptions.config);
|
|
1084
1709
|
} catch (err) {
|
|
1085
1710
|
const message = err instanceof Error ? err.message : String(err);
|
|
1086
1711
|
process.stderr.write(`[mcpwall] Error: ${message}
|