protect-mcp 0.4.1 → 0.4.3
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/README.md +1 -1
- package/dist/{chunk-7HBHIKLN.mjs → chunk-VF3OCG4D.mjs} +422 -9
- package/dist/{chunk-VWUN6AI6.mjs → chunk-VIA2B65K.mjs} +1 -1
- package/dist/cli.js +697 -94
- package/dist/cli.mjs +190 -8
- package/dist/{http-transport-RIVV2RVQ.mjs → http-transport-VLIPOPIC.mjs} +1 -1
- package/dist/index.d.mts +1429 -2
- package/dist/index.d.ts +1429 -2
- package/dist/index.js +1728 -25
- package/dist/index.mjs +1273 -3
- package/package.json +4 -3
- package/policies/cedar/clinejection.cedar +50 -0
- package/policies/cedar/terraform-destroy.cedar +44 -0
- package/policies/clinejection.json +6 -0
- package/policies/data-exfiltration.json +6 -0
- package/policies/financial-safe.json +8 -0
- package/policies/github-mcp-hijack.json +6 -0
- package/policies/terraform-destroy.json +6 -0
package/dist/cli.js
CHANGED
|
@@ -618,6 +618,366 @@ var init_external_pdp = __esm({
|
|
|
618
618
|
}
|
|
619
619
|
});
|
|
620
620
|
|
|
621
|
+
// src/cedar-evaluator.ts
|
|
622
|
+
async function ensureCedarWasm() {
|
|
623
|
+
if (cedarWasm) return true;
|
|
624
|
+
if (loadAttempted) return false;
|
|
625
|
+
loadAttempted = true;
|
|
626
|
+
try {
|
|
627
|
+
const moduleName = "@cedar-policy/cedar-wasm";
|
|
628
|
+
cedarWasm = await import(
|
|
629
|
+
/* @vite-ignore */
|
|
630
|
+
moduleName
|
|
631
|
+
);
|
|
632
|
+
return true;
|
|
633
|
+
} catch {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function loadCedarPolicies(dirPath) {
|
|
638
|
+
if (!(0, import_node_fs4.existsSync)(dirPath)) {
|
|
639
|
+
throw new Error(`Cedar policy directory not found: ${dirPath}`);
|
|
640
|
+
}
|
|
641
|
+
const entries = (0, import_node_fs4.readdirSync)(dirPath).filter((f) => (0, import_node_path2.extname)(f) === ".cedar").sort();
|
|
642
|
+
if (entries.length === 0) {
|
|
643
|
+
throw new Error(`No .cedar files found in: ${dirPath}`);
|
|
644
|
+
}
|
|
645
|
+
const sources = [];
|
|
646
|
+
for (const file of entries) {
|
|
647
|
+
const content = (0, import_node_fs4.readFileSync)((0, import_node_path2.join)(dirPath, file), "utf-8");
|
|
648
|
+
sources.push(content);
|
|
649
|
+
}
|
|
650
|
+
const concatenated = sources.join("\n\n");
|
|
651
|
+
const digest = (0, import_node_crypto2.createHash)("sha256").update(concatenated).digest("hex").slice(0, 16);
|
|
652
|
+
return {
|
|
653
|
+
source: concatenated,
|
|
654
|
+
digest,
|
|
655
|
+
fileCount: entries.length,
|
|
656
|
+
files: entries
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function buildEntities(req) {
|
|
660
|
+
const agentId = req.agentId || req.tier;
|
|
661
|
+
return [
|
|
662
|
+
{
|
|
663
|
+
uid: { type: "Agent", id: agentId },
|
|
664
|
+
attrs: {
|
|
665
|
+
tier: req.tier,
|
|
666
|
+
...req.agentId ? { agent_id: req.agentId } : {}
|
|
667
|
+
},
|
|
668
|
+
parents: []
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
uid: { type: "Tool", id: req.tool },
|
|
672
|
+
attrs: {},
|
|
673
|
+
parents: []
|
|
674
|
+
}
|
|
675
|
+
];
|
|
676
|
+
}
|
|
677
|
+
async function evaluateCedar(policySet, req) {
|
|
678
|
+
const available = await ensureCedarWasm();
|
|
679
|
+
if (!available) {
|
|
680
|
+
return {
|
|
681
|
+
allowed: true,
|
|
682
|
+
reason: "cedar_wasm_not_available",
|
|
683
|
+
metadata: { fallback: true }
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const agentId = req.agentId || req.tier;
|
|
688
|
+
const authRequest = {
|
|
689
|
+
principal: { type: "Agent", id: agentId },
|
|
690
|
+
action: { type: "Action", id: "MCP::Tool::call" },
|
|
691
|
+
resource: { type: "Tool", id: req.tool },
|
|
692
|
+
context: {
|
|
693
|
+
tier: req.tier,
|
|
694
|
+
...req.context || {}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const entities = buildEntities(req);
|
|
698
|
+
let result;
|
|
699
|
+
if (typeof cedarWasm.isAuthorized === "function") {
|
|
700
|
+
result = cedarWasm.isAuthorized({
|
|
701
|
+
policies: policySet.source,
|
|
702
|
+
entities,
|
|
703
|
+
principal: authRequest.principal,
|
|
704
|
+
action: authRequest.action,
|
|
705
|
+
resource: authRequest.resource,
|
|
706
|
+
context: authRequest.context,
|
|
707
|
+
schema: null
|
|
708
|
+
// No schema enforcement — Cedar still evaluates correctly
|
|
709
|
+
});
|
|
710
|
+
} else if (typeof cedarWasm.checkAuthorization === "function") {
|
|
711
|
+
result = cedarWasm.checkAuthorization(
|
|
712
|
+
policySet.source,
|
|
713
|
+
JSON.stringify(entities),
|
|
714
|
+
JSON.stringify(authRequest)
|
|
715
|
+
);
|
|
716
|
+
} else {
|
|
717
|
+
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
718
|
+
if (typeof cedarEngine.isAuthorized === "function") {
|
|
719
|
+
result = cedarEngine.isAuthorized({
|
|
720
|
+
policies: policySet.source,
|
|
721
|
+
entities,
|
|
722
|
+
principal: authRequest.principal,
|
|
723
|
+
action: authRequest.action,
|
|
724
|
+
resource: authRequest.resource,
|
|
725
|
+
context: authRequest.context,
|
|
726
|
+
schema: null
|
|
727
|
+
});
|
|
728
|
+
} else {
|
|
729
|
+
return {
|
|
730
|
+
allowed: true,
|
|
731
|
+
reason: "cedar_wasm_api_unsupported",
|
|
732
|
+
metadata: { fallback: true, exports: Object.keys(cedarWasm) }
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const decision = parseWasmResult(result);
|
|
737
|
+
return {
|
|
738
|
+
allowed: decision.allowed,
|
|
739
|
+
reason: decision.allowed ? void 0 : `cedar_deny${decision.diagnostics ? ": " + decision.diagnostics : ""}`,
|
|
740
|
+
metadata: {
|
|
741
|
+
policy_digest: policySet.digest,
|
|
742
|
+
...decision.matchedPolicies ? { matched_policies: decision.matchedPolicies } : {}
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
} catch (err) {
|
|
746
|
+
return {
|
|
747
|
+
allowed: true,
|
|
748
|
+
reason: `cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
749
|
+
metadata: { fallback: true, error: true }
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function parseWasmResult(result) {
|
|
754
|
+
if (!result) {
|
|
755
|
+
return { allowed: true, diagnostics: "null result from Cedar WASM" };
|
|
756
|
+
}
|
|
757
|
+
if (result.type === "allow" || result.type === "Allow") {
|
|
758
|
+
return { allowed: true };
|
|
759
|
+
}
|
|
760
|
+
if (result.type === "deny" || result.type === "Deny") {
|
|
761
|
+
return {
|
|
762
|
+
allowed: false,
|
|
763
|
+
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0,
|
|
764
|
+
matchedPolicies: result.diagnostics?.reasons
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (result.decision === "Allow") {
|
|
768
|
+
return { allowed: true };
|
|
769
|
+
}
|
|
770
|
+
if (result.decision === "Deny") {
|
|
771
|
+
return {
|
|
772
|
+
allowed: false,
|
|
773
|
+
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (typeof result === "boolean") {
|
|
777
|
+
return { allowed: result };
|
|
778
|
+
}
|
|
779
|
+
return { allowed: true, diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
780
|
+
}
|
|
781
|
+
async function isCedarAvailable() {
|
|
782
|
+
return ensureCedarWasm();
|
|
783
|
+
}
|
|
784
|
+
var import_node_crypto2, import_node_fs4, import_node_path2, cedarWasm, loadAttempted;
|
|
785
|
+
var init_cedar_evaluator = __esm({
|
|
786
|
+
"src/cedar-evaluator.ts"() {
|
|
787
|
+
"use strict";
|
|
788
|
+
import_node_crypto2 = require("crypto");
|
|
789
|
+
import_node_fs4 = require("fs");
|
|
790
|
+
import_node_path2 = require("path");
|
|
791
|
+
cedarWasm = null;
|
|
792
|
+
loadAttempted = false;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// src/notifications.ts
|
|
797
|
+
async function sendApprovalNotification(config, notification) {
|
|
798
|
+
const promises = [];
|
|
799
|
+
if (config.sms) {
|
|
800
|
+
promises.push(sendSms(config.sms, notification));
|
|
801
|
+
}
|
|
802
|
+
if (config.webhook) {
|
|
803
|
+
promises.push(sendWebhook(config.webhook, notification));
|
|
804
|
+
}
|
|
805
|
+
if (config.email) {
|
|
806
|
+
promises.push(sendEmail(config.email, notification));
|
|
807
|
+
}
|
|
808
|
+
const results = await Promise.allSettled(promises);
|
|
809
|
+
for (const result of results) {
|
|
810
|
+
if (result.status === "rejected") {
|
|
811
|
+
console.error(`[protect-mcp] Notification failed: ${result.reason}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function sendSms(config, notification) {
|
|
816
|
+
const body = [
|
|
817
|
+
`\u{1F512} Approval Required`,
|
|
818
|
+
`Tool: ${notification.toolName}`,
|
|
819
|
+
notification.agentId ? `Agent: ${notification.agentId}` : null,
|
|
820
|
+
`Reason: ${notification.reason}`,
|
|
821
|
+
notification.approveUrl ? `Approve: ${notification.approveUrl}` : null,
|
|
822
|
+
notification.traceUrl ? `Trace: ${notification.traceUrl}` : null
|
|
823
|
+
].filter(Boolean).join("\n");
|
|
824
|
+
const params = new URLSearchParams({
|
|
825
|
+
To: config.to,
|
|
826
|
+
From: config.from,
|
|
827
|
+
Body: body
|
|
828
|
+
});
|
|
829
|
+
const response = await fetch(
|
|
830
|
+
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
831
|
+
{
|
|
832
|
+
method: "POST",
|
|
833
|
+
headers: {
|
|
834
|
+
Authorization: `Basic ${Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64")}`,
|
|
835
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
836
|
+
},
|
|
837
|
+
body: params.toString()
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
if (!response.ok) {
|
|
841
|
+
throw new Error(`Twilio SMS failed: ${response.status} ${await response.text()}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async function sendWebhook(config, notification) {
|
|
845
|
+
let payload;
|
|
846
|
+
if (config.template === "slack") {
|
|
847
|
+
payload = {
|
|
848
|
+
blocks: [
|
|
849
|
+
{
|
|
850
|
+
type: "header",
|
|
851
|
+
text: { type: "plain_text", text: "\u{1F512} Agent Approval Required" }
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
type: "section",
|
|
855
|
+
fields: [
|
|
856
|
+
{ type: "mrkdwn", text: `*Tool:*
|
|
857
|
+
\`${notification.toolName}\`` },
|
|
858
|
+
{ type: "mrkdwn", text: `*Agent:*
|
|
859
|
+
${notification.agentId || "unknown"}` },
|
|
860
|
+
{ type: "mrkdwn", text: `*Policy:*
|
|
861
|
+
${notification.policyName || "default"}` },
|
|
862
|
+
{ type: "mrkdwn", text: `*Time:*
|
|
863
|
+
${notification.timestamp}` }
|
|
864
|
+
]
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
type: "section",
|
|
868
|
+
text: { type: "mrkdwn", text: `*Reason:* ${notification.reason}` }
|
|
869
|
+
},
|
|
870
|
+
...notification.approveUrl || notification.traceUrl ? [
|
|
871
|
+
{
|
|
872
|
+
type: "actions",
|
|
873
|
+
elements: [
|
|
874
|
+
...notification.approveUrl ? [{ type: "button", text: { type: "plain_text", text: "\u2705 Approve" }, url: notification.approveUrl, style: "primary" }] : [],
|
|
875
|
+
...notification.traceUrl ? [{ type: "button", text: { type: "plain_text", text: "\u{1F50D} View Trace" }, url: notification.traceUrl }] : []
|
|
876
|
+
]
|
|
877
|
+
}
|
|
878
|
+
] : []
|
|
879
|
+
]
|
|
880
|
+
};
|
|
881
|
+
} else if (config.template === "pagerduty") {
|
|
882
|
+
payload = {
|
|
883
|
+
routing_key: config.headers?.["X-Routing-Key"] || "",
|
|
884
|
+
event_action: "trigger",
|
|
885
|
+
payload: {
|
|
886
|
+
summary: `Agent approval required: ${notification.toolName}`,
|
|
887
|
+
source: "protect-mcp",
|
|
888
|
+
severity: "warning",
|
|
889
|
+
custom_details: {
|
|
890
|
+
tool: notification.toolName,
|
|
891
|
+
agent: notification.agentId,
|
|
892
|
+
policy: notification.policyName,
|
|
893
|
+
reason: notification.reason,
|
|
894
|
+
trace_url: notification.traceUrl,
|
|
895
|
+
approve_url: notification.approveUrl
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
} else {
|
|
900
|
+
payload = notification;
|
|
901
|
+
}
|
|
902
|
+
const response = await fetch(config.url, {
|
|
903
|
+
method: config.method || "POST",
|
|
904
|
+
headers: {
|
|
905
|
+
"Content-Type": "application/json",
|
|
906
|
+
...config.headers
|
|
907
|
+
},
|
|
908
|
+
body: JSON.stringify(payload)
|
|
909
|
+
});
|
|
910
|
+
if (!response.ok) {
|
|
911
|
+
throw new Error(`Webhook failed: ${response.status}`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
async function sendEmail(config, notification) {
|
|
915
|
+
if (!config.resendApiKey) {
|
|
916
|
+
console.warn("[protect-mcp] Email notification skipped: no resendApiKey configured");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const html = `
|
|
920
|
+
<div style="font-family: monospace; padding: 20px; background: #0d1117; color: #c9d1d9; border-radius: 8px;">
|
|
921
|
+
<h2 style="color: #10b981;">\u{1F512} Agent Approval Required</h2>
|
|
922
|
+
<table style="font-size: 14px; margin: 16px 0;">
|
|
923
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Tool:</td><td>${notification.toolName}</td></tr>
|
|
924
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Agent:</td><td>${notification.agentId || "unknown"}</td></tr>
|
|
925
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Reason:</td><td>${notification.reason}</td></tr>
|
|
926
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Time:</td><td>${notification.timestamp}</td></tr>
|
|
927
|
+
</table>
|
|
928
|
+
${notification.approveUrl ? `<a href="${notification.approveUrl}" style="background: #10b981; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; margin-right: 8px;">\u2705 Approve</a>` : ""}
|
|
929
|
+
${notification.traceUrl ? `<a href="${notification.traceUrl}" style="background: #1f2937; color: #c9d1d9; padding: 8px 16px; border-radius: 6px; text-decoration: none; border: 1px solid #374151;">\u{1F50D} View Trace</a>` : ""}
|
|
930
|
+
</div>
|
|
931
|
+
`;
|
|
932
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
933
|
+
method: "POST",
|
|
934
|
+
headers: {
|
|
935
|
+
Authorization: `Bearer ${config.resendApiKey}`,
|
|
936
|
+
"Content-Type": "application/json"
|
|
937
|
+
},
|
|
938
|
+
body: JSON.stringify({
|
|
939
|
+
from: "ScopeBlind <noreply@scopeblind.com>",
|
|
940
|
+
to: config.to,
|
|
941
|
+
subject: `\u{1F512} Approval required: ${notification.toolName}`,
|
|
942
|
+
html
|
|
943
|
+
})
|
|
944
|
+
});
|
|
945
|
+
if (!response.ok) {
|
|
946
|
+
throw new Error(`Resend email failed: ${response.status}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function parseNotificationConfigFromEnv() {
|
|
950
|
+
const config = {};
|
|
951
|
+
let hasConfig = false;
|
|
952
|
+
const smsTo = process.env.SCOPEBLIND_SMS_TO;
|
|
953
|
+
const twilioSid = process.env.TWILIO_ACCOUNT_SID;
|
|
954
|
+
const twilioToken = process.env.TWILIO_AUTH_TOKEN;
|
|
955
|
+
const twilioFrom = process.env.TWILIO_FROM_NUMBER;
|
|
956
|
+
if (smsTo && twilioSid && twilioToken && twilioFrom) {
|
|
957
|
+
config.sms = { accountSid: twilioSid, authToken: twilioToken, from: twilioFrom, to: smsTo };
|
|
958
|
+
hasConfig = true;
|
|
959
|
+
}
|
|
960
|
+
const webhookUrl = process.env.SCOPEBLIND_WEBHOOK_URL;
|
|
961
|
+
if (webhookUrl) {
|
|
962
|
+
config.webhook = {
|
|
963
|
+
url: webhookUrl,
|
|
964
|
+
template: process.env.SCOPEBLIND_WEBHOOK_TEMPLATE || "custom"
|
|
965
|
+
};
|
|
966
|
+
hasConfig = true;
|
|
967
|
+
}
|
|
968
|
+
const emailTo = process.env.SCOPEBLIND_EMAIL_TO;
|
|
969
|
+
if (emailTo) {
|
|
970
|
+
config.email = { to: emailTo, resendApiKey: process.env.RESEND_API_KEY };
|
|
971
|
+
hasConfig = true;
|
|
972
|
+
}
|
|
973
|
+
return hasConfig ? config : null;
|
|
974
|
+
}
|
|
975
|
+
var init_notifications = __esm({
|
|
976
|
+
"src/notifications.ts"() {
|
|
977
|
+
"use strict";
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
|
|
621
981
|
// src/http-server.ts
|
|
622
982
|
function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
|
|
623
983
|
const startTime = Date.now();
|
|
@@ -684,13 +1044,13 @@ function handleHealth(res, startTime, config) {
|
|
|
684
1044
|
}));
|
|
685
1045
|
}
|
|
686
1046
|
function handleStatus(res, logDir) {
|
|
687
|
-
const logPath = (0,
|
|
688
|
-
if (!(0,
|
|
1047
|
+
const logPath = (0, import_node_path3.join)(logDir, LOG_FILE);
|
|
1048
|
+
if (!(0, import_node_fs5.existsSync)(logPath)) {
|
|
689
1049
|
res.writeHead(200);
|
|
690
1050
|
res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
|
|
691
1051
|
return;
|
|
692
1052
|
}
|
|
693
|
-
const raw = (0,
|
|
1053
|
+
const raw = (0, import_node_fs5.readFileSync)(logPath, "utf-8");
|
|
694
1054
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
695
1055
|
const entries = [];
|
|
696
1056
|
for (const line of lines) {
|
|
@@ -812,13 +1172,13 @@ function handleListApprovals(res, approvalStore) {
|
|
|
812
1172
|
res.writeHead(200);
|
|
813
1173
|
res.end(JSON.stringify({ grants }));
|
|
814
1174
|
}
|
|
815
|
-
var import_node_http,
|
|
1175
|
+
var import_node_http, import_node_fs5, import_node_path3, LOG_FILE, MAX_RECEIPTS, ReceiptBuffer;
|
|
816
1176
|
var init_http_server = __esm({
|
|
817
1177
|
"src/http-server.ts"() {
|
|
818
1178
|
"use strict";
|
|
819
1179
|
import_node_http = require("http");
|
|
820
|
-
|
|
821
|
-
|
|
1180
|
+
import_node_fs5 = require("fs");
|
|
1181
|
+
import_node_path3 = require("path");
|
|
822
1182
|
LOG_FILE = ".protect-mcp-log.jsonl";
|
|
823
1183
|
MAX_RECEIPTS = 100;
|
|
824
1184
|
ReceiptBuffer = class {
|
|
@@ -850,21 +1210,23 @@ var init_http_server = __esm({
|
|
|
850
1210
|
});
|
|
851
1211
|
|
|
852
1212
|
// src/gateway.ts
|
|
853
|
-
var import_node_child_process,
|
|
1213
|
+
var import_node_child_process, import_node_crypto3, import_node_readline, import_node_fs6, import_node_path4, LOG_FILE2, RECEIPTS_FILE, ProtectGateway;
|
|
854
1214
|
var init_gateway = __esm({
|
|
855
1215
|
"src/gateway.ts"() {
|
|
856
1216
|
"use strict";
|
|
857
1217
|
import_node_child_process = require("child_process");
|
|
858
|
-
|
|
1218
|
+
import_node_crypto3 = require("crypto");
|
|
859
1219
|
import_node_readline = require("readline");
|
|
860
|
-
|
|
861
|
-
|
|
1220
|
+
import_node_fs6 = require("fs");
|
|
1221
|
+
import_node_path4 = require("path");
|
|
862
1222
|
init_policy();
|
|
863
1223
|
init_admission();
|
|
864
1224
|
init_credentials();
|
|
865
1225
|
init_signing();
|
|
866
1226
|
init_external_pdp();
|
|
1227
|
+
init_cedar_evaluator();
|
|
867
1228
|
init_evidence_store();
|
|
1229
|
+
init_notifications();
|
|
868
1230
|
init_http_server();
|
|
869
1231
|
LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
870
1232
|
RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
@@ -880,18 +1242,30 @@ var init_gateway = __esm({
|
|
|
880
1242
|
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
881
1243
|
approvalStore = /* @__PURE__ */ new Map();
|
|
882
1244
|
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
883
|
-
approvalNonce = (0,
|
|
1245
|
+
approvalNonce = (0, import_node_crypto3.randomBytes)(16).toString("hex");
|
|
884
1246
|
currentTier = "unknown";
|
|
885
1247
|
admissionResult = null;
|
|
1248
|
+
/** Notification config for approval gates (SMS, webhook, email) */
|
|
1249
|
+
notificationConfig = null;
|
|
886
1250
|
/** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
|
|
887
1251
|
pendingResponses = /* @__PURE__ */ new Map();
|
|
888
1252
|
httpMode = false;
|
|
1253
|
+
/** Loaded Cedar policy set (when policy_engine is "cedar") */
|
|
1254
|
+
cedarPolicySet = null;
|
|
889
1255
|
constructor(config) {
|
|
890
1256
|
this.config = config;
|
|
891
|
-
this.logFilePath = (0,
|
|
892
|
-
this.receiptFilePath = (0,
|
|
1257
|
+
this.logFilePath = (0, import_node_path4.join)(process.cwd(), LOG_FILE2);
|
|
1258
|
+
this.receiptFilePath = (0, import_node_path4.join)(process.cwd(), RECEIPTS_FILE);
|
|
893
1259
|
this.evidenceStore = new EvidenceStore();
|
|
894
1260
|
this.receiptBuffer = new ReceiptBuffer();
|
|
1261
|
+
this.notificationConfig = parseNotificationConfigFromEnv();
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Set the Cedar policy set for local evaluation.
|
|
1265
|
+
* Called during CLI startup when --cedar flag is used.
|
|
1266
|
+
*/
|
|
1267
|
+
setCedarPolicies(policySet) {
|
|
1268
|
+
this.cedarPolicySet = policySet;
|
|
895
1269
|
}
|
|
896
1270
|
async start() {
|
|
897
1271
|
const { command, args, verbose } = this.config;
|
|
@@ -1022,7 +1396,7 @@ var init_gateway = __esm({
|
|
|
1022
1396
|
}
|
|
1023
1397
|
async interceptToolCall(request) {
|
|
1024
1398
|
const toolName = request.params?.name || "unknown";
|
|
1025
|
-
const requestId = (0,
|
|
1399
|
+
const requestId = (0, import_node_crypto3.randomUUID)().slice(0, 12);
|
|
1026
1400
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
1027
1401
|
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
1028
1402
|
let effectiveToolPolicy;
|
|
@@ -1060,6 +1434,27 @@ var init_gateway = __esm({
|
|
|
1060
1434
|
}
|
|
1061
1435
|
}
|
|
1062
1436
|
}
|
|
1437
|
+
if (this.config.policy?.policy_engine === "cedar" && this.cedarPolicySet) {
|
|
1438
|
+
try {
|
|
1439
|
+
const cedarDecision = await evaluateCedar(this.cedarPolicySet, {
|
|
1440
|
+
tool: toolName,
|
|
1441
|
+
tier: this.currentTier,
|
|
1442
|
+
agentId: this.admissionResult?.agent_id
|
|
1443
|
+
});
|
|
1444
|
+
if (!cedarDecision.allowed) {
|
|
1445
|
+
const reason = cedarDecision.reason || "cedar_deny";
|
|
1446
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1447
|
+
if (this.config.enforce) {
|
|
1448
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by Cedar policy`);
|
|
1449
|
+
}
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1453
|
+
return null;
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
if (this.config.verbose) this.log(`Cedar evaluation error: ${err instanceof Error ? err.message : err}`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1063
1458
|
if (this.config.policy?.external && (this.config.policy.policy_engine === "external" || this.config.policy.policy_engine === "hybrid")) {
|
|
1064
1459
|
try {
|
|
1065
1460
|
const ctx = buildDecisionContext(toolName, this.currentTier, {
|
|
@@ -1107,6 +1502,20 @@ var init_gateway = __esm({
|
|
|
1107
1502
|
return null;
|
|
1108
1503
|
}
|
|
1109
1504
|
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1505
|
+
if (this.notificationConfig) {
|
|
1506
|
+
sendApprovalNotification(this.notificationConfig, {
|
|
1507
|
+
requestId,
|
|
1508
|
+
toolName,
|
|
1509
|
+
agentId: this.admissionResult?.agent_id,
|
|
1510
|
+
policyName: "default",
|
|
1511
|
+
reason: `Policy requires human approval for "${toolName}"`,
|
|
1512
|
+
traceUrl: `https://scopeblind.com/trace`,
|
|
1513
|
+
approveUrl: void 0,
|
|
1514
|
+
// Approve URL provided when HTTP transport is active
|
|
1515
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1516
|
+
}).catch(() => {
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1110
1519
|
if (this.config.enforce) {
|
|
1111
1520
|
return {
|
|
1112
1521
|
jsonrpc: "2.0",
|
|
@@ -1154,8 +1563,19 @@ var init_gateway = __esm({
|
|
|
1154
1563
|
}
|
|
1155
1564
|
return policy.rate_limit;
|
|
1156
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Emit a decision log entry with OTel-compatible trace IDs and optional
|
|
1568
|
+
* signed receipt generation.
|
|
1569
|
+
*
|
|
1570
|
+
* @patent Patent-protected construction — decision receipts with configurable
|
|
1571
|
+
* disclosure and issuer-blind properties. Covered by Apache 2.0 patent grant
|
|
1572
|
+
* for users of this code. Clean-room reimplementation requires a patent license.
|
|
1573
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
|
|
1574
|
+
*/
|
|
1157
1575
|
emitDecisionLog(entry) {
|
|
1158
1576
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
1577
|
+
const otelTraceId = entry.otel_trace_id || (0, import_node_crypto3.randomBytes)(16).toString("hex");
|
|
1578
|
+
const otelSpanId = entry.otel_span_id || (0, import_node_crypto3.randomBytes)(8).toString("hex");
|
|
1159
1579
|
const log = {
|
|
1160
1580
|
v: 2,
|
|
1161
1581
|
tool: entry.tool || "unknown",
|
|
@@ -1163,17 +1583,19 @@ var init_gateway = __esm({
|
|
|
1163
1583
|
reason_code: entry.reason_code || "default_allow",
|
|
1164
1584
|
policy_digest: this.config.policyDigest,
|
|
1165
1585
|
policy_engine: this.config.policy?.policy_engine || "built-in",
|
|
1166
|
-
request_id: entry.request_id || (0,
|
|
1586
|
+
request_id: entry.request_id || (0, import_node_crypto3.randomUUID)().slice(0, 12),
|
|
1167
1587
|
timestamp: Date.now(),
|
|
1168
1588
|
mode,
|
|
1169
1589
|
...entry.rate_limit_remaining !== void 0 && { rate_limit_remaining: entry.rate_limit_remaining },
|
|
1170
1590
|
...entry.tier && { tier: entry.tier },
|
|
1171
|
-
...entry.credential_ref && { credential_ref: entry.credential_ref }
|
|
1591
|
+
...entry.credential_ref && { credential_ref: entry.credential_ref },
|
|
1592
|
+
otel_trace_id: otelTraceId,
|
|
1593
|
+
otel_span_id: otelSpanId
|
|
1172
1594
|
};
|
|
1173
1595
|
process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
|
|
1174
1596
|
`);
|
|
1175
1597
|
try {
|
|
1176
|
-
(0,
|
|
1598
|
+
(0, import_node_fs6.appendFileSync)(this.logFilePath, JSON.stringify(log) + "\n");
|
|
1177
1599
|
} catch {
|
|
1178
1600
|
}
|
|
1179
1601
|
if (isSigningEnabled()) {
|
|
@@ -1182,7 +1604,7 @@ var init_gateway = __esm({
|
|
|
1182
1604
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
1183
1605
|
`);
|
|
1184
1606
|
try {
|
|
1185
|
-
(0,
|
|
1607
|
+
(0, import_node_fs6.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
|
|
1186
1608
|
} catch {
|
|
1187
1609
|
}
|
|
1188
1610
|
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
@@ -4163,8 +4585,8 @@ function generateReport(logPath, receiptPath, periodDays) {
|
|
|
4163
4585
|
const now = /* @__PURE__ */ new Date();
|
|
4164
4586
|
const from = new Date(now.getTime() - periodDays * 864e5);
|
|
4165
4587
|
const entries = [];
|
|
4166
|
-
if ((0,
|
|
4167
|
-
const raw = (0,
|
|
4588
|
+
if ((0, import_node_fs8.existsSync)(logPath)) {
|
|
4589
|
+
const raw = (0, import_node_fs8.readFileSync)(logPath, "utf-8");
|
|
4168
4590
|
for (const line of raw.split("\n")) {
|
|
4169
4591
|
const trimmed = line.trim();
|
|
4170
4592
|
if (!trimmed) continue;
|
|
@@ -4184,8 +4606,8 @@ function generateReport(logPath, receiptPath, periodDays) {
|
|
|
4184
4606
|
let receiptsSigned = 0;
|
|
4185
4607
|
let signerKid = "";
|
|
4186
4608
|
let signerIssuer = "";
|
|
4187
|
-
if ((0,
|
|
4188
|
-
const raw = (0,
|
|
4609
|
+
if ((0, import_node_fs8.existsSync)(receiptPath)) {
|
|
4610
|
+
const raw = (0, import_node_fs8.readFileSync)(receiptPath, "utf-8");
|
|
4189
4611
|
for (const line of raw.split("\n")) {
|
|
4190
4612
|
const trimmed = line.trim();
|
|
4191
4613
|
if (!trimmed) continue;
|
|
@@ -4317,11 +4739,11 @@ function formatReportMarkdown(report) {
|
|
|
4317
4739
|
lines.push("*Generated by protect-mcp \xB7 scopeblind.com*");
|
|
4318
4740
|
return lines.join("\n");
|
|
4319
4741
|
}
|
|
4320
|
-
var
|
|
4742
|
+
var import_node_fs8;
|
|
4321
4743
|
var init_report = __esm({
|
|
4322
4744
|
"src/report.ts"() {
|
|
4323
4745
|
"use strict";
|
|
4324
|
-
|
|
4746
|
+
import_node_fs8 = require("fs");
|
|
4325
4747
|
}
|
|
4326
4748
|
});
|
|
4327
4749
|
|
|
@@ -4332,11 +4754,11 @@ init_signing();
|
|
|
4332
4754
|
init_credentials();
|
|
4333
4755
|
|
|
4334
4756
|
// src/simulate.ts
|
|
4335
|
-
var
|
|
4757
|
+
var import_node_fs7 = require("fs");
|
|
4336
4758
|
init_policy();
|
|
4337
4759
|
init_admission();
|
|
4338
4760
|
function parseLogFile(path) {
|
|
4339
|
-
const raw = (0,
|
|
4761
|
+
const raw = (0, import_node_fs7.readFileSync)(path, "utf-8");
|
|
4340
4762
|
const entries = [];
|
|
4341
4763
|
for (const line of raw.split("\n")) {
|
|
4342
4764
|
const trimmed = line.trim();
|
|
@@ -4465,6 +4887,7 @@ function formatSimulation(summary) {
|
|
|
4465
4887
|
}
|
|
4466
4888
|
|
|
4467
4889
|
// src/cli.ts
|
|
4890
|
+
init_cedar_evaluator();
|
|
4468
4891
|
function printHelp() {
|
|
4469
4892
|
process.stderr.write(`
|
|
4470
4893
|
protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
@@ -4484,6 +4907,7 @@ Usage:
|
|
|
4484
4907
|
|
|
4485
4908
|
Options:
|
|
4486
4909
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
4910
|
+
--cedar <dir> Cedar policy directory (alternative to --policy, evaluates locally via WASM)
|
|
4487
4911
|
--slug <slug> ScopeBlind tenant slug (optional)
|
|
4488
4912
|
--enforce Enable enforcement mode (default: shadow mode)
|
|
4489
4913
|
--http Start HTTP/SSE server instead of stdio proxy
|
|
@@ -4495,6 +4919,7 @@ Commands:
|
|
|
4495
4919
|
quickstart Zero-config onboarding: init + demo + show receipts in one command
|
|
4496
4920
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
4497
4921
|
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
4922
|
+
doctor Check your setup: keys, policies, verifier, API connectivity
|
|
4498
4923
|
trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
|
|
4499
4924
|
status Show tool call statistics from the local decision log
|
|
4500
4925
|
digest Generate a human-readable summary of agent activity
|
|
@@ -4517,6 +4942,7 @@ Examples:
|
|
|
4517
4942
|
}
|
|
4518
4943
|
function parseArgs(argv) {
|
|
4519
4944
|
let policyPath;
|
|
4945
|
+
let cedarDir;
|
|
4520
4946
|
let slug;
|
|
4521
4947
|
let enforce = false;
|
|
4522
4948
|
let verbose = false;
|
|
@@ -4541,6 +4967,8 @@ function parseArgs(argv) {
|
|
|
4541
4967
|
process.exit(0);
|
|
4542
4968
|
} else if (arg === "--policy" && i + 1 < options.length) {
|
|
4543
4969
|
policyPath = options[++i];
|
|
4970
|
+
} else if (arg === "--cedar" && i + 1 < options.length) {
|
|
4971
|
+
cedarDir = options[++i];
|
|
4544
4972
|
} else if (arg === "--slug" && i + 1 < options.length) {
|
|
4545
4973
|
slug = options[++i];
|
|
4546
4974
|
} else if (arg === "--enforce") {
|
|
@@ -4552,20 +4980,20 @@ function parseArgs(argv) {
|
|
|
4552
4980
|
`);
|
|
4553
4981
|
}
|
|
4554
4982
|
}
|
|
4555
|
-
return { policyPath, slug, enforce, verbose, childCommand };
|
|
4983
|
+
return { policyPath, cedarDir, slug, enforce, verbose, childCommand };
|
|
4556
4984
|
}
|
|
4557
4985
|
async function handleInit(argv) {
|
|
4558
|
-
const { writeFileSync: writeFileSync2, existsSync:
|
|
4559
|
-
const { join:
|
|
4986
|
+
const { writeFileSync: writeFileSync2, existsSync: existsSync6, mkdirSync } = await import("fs");
|
|
4987
|
+
const { join: join5 } = await import("path");
|
|
4560
4988
|
let dir = process.cwd();
|
|
4561
4989
|
const dirIdx = argv.indexOf("--dir");
|
|
4562
4990
|
if (dirIdx !== -1 && argv[dirIdx + 1]) {
|
|
4563
4991
|
dir = argv[dirIdx + 1];
|
|
4564
4992
|
}
|
|
4565
|
-
const configPath =
|
|
4566
|
-
const keysDir =
|
|
4567
|
-
const keyPath =
|
|
4568
|
-
if (
|
|
4993
|
+
const configPath = join5(dir, "protect-mcp.json");
|
|
4994
|
+
const keysDir = join5(dir, "keys");
|
|
4995
|
+
const keyPath = join5(keysDir, "gateway.json");
|
|
4996
|
+
if (existsSync6(configPath)) {
|
|
4569
4997
|
process.stderr.write(`[PROTECT_MCP] Config already exists at ${configPath}
|
|
4570
4998
|
`);
|
|
4571
4999
|
process.stderr.write("[PROTECT_MCP] Delete it first if you want to regenerate.\n");
|
|
@@ -4584,7 +5012,7 @@ async function handleInit(argv) {
|
|
|
4584
5012
|
kid: "generated"
|
|
4585
5013
|
};
|
|
4586
5014
|
}
|
|
4587
|
-
if (!
|
|
5015
|
+
if (!existsSync6(keysDir)) {
|
|
4588
5016
|
mkdirSync(keysDir, { recursive: true });
|
|
4589
5017
|
}
|
|
4590
5018
|
writeFileSync2(keyPath, JSON.stringify({
|
|
@@ -4594,8 +5022,8 @@ async function handleInit(argv) {
|
|
|
4594
5022
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4595
5023
|
warning: "KEEP THIS FILE SECRET. Never commit to version control."
|
|
4596
5024
|
}, null, 2) + "\n");
|
|
4597
|
-
const gitignorePath =
|
|
4598
|
-
if (!
|
|
5025
|
+
const gitignorePath = join5(keysDir, ".gitignore");
|
|
5026
|
+
if (!existsSync6(gitignorePath)) {
|
|
4599
5027
|
writeFileSync2(gitignorePath, "# Never commit signing keys\n*.json\n");
|
|
4600
5028
|
}
|
|
4601
5029
|
const config = {
|
|
@@ -4668,13 +5096,19 @@ Add --enforce when ready to block policy violations.
|
|
|
4668
5096
|
`);
|
|
4669
5097
|
}
|
|
4670
5098
|
async function handleDemo() {
|
|
4671
|
-
const { existsSync:
|
|
4672
|
-
const { join:
|
|
5099
|
+
const { existsSync: existsSync6 } = await import("fs");
|
|
5100
|
+
const { join: join5, dirname, resolve } = await import("path");
|
|
5101
|
+
const { realpathSync } = await import("fs");
|
|
4673
5102
|
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
5103
|
+
let cliDir;
|
|
5104
|
+
try {
|
|
5105
|
+
cliDir = dirname(realpathSync(cliPath));
|
|
5106
|
+
} catch {
|
|
5107
|
+
cliDir = dirname(cliPath);
|
|
5108
|
+
}
|
|
5109
|
+
const demoServerPath = join5(cliDir, "demo-server.js");
|
|
5110
|
+
const configPath = join5(process.cwd(), "protect-mcp.json");
|
|
5111
|
+
const hasConfig = existsSync6(configPath);
|
|
4678
5112
|
if (!hasConfig) {
|
|
4679
5113
|
process.stderr.write(`
|
|
4680
5114
|
${bold("protect-mcp demo")}
|
|
@@ -4748,15 +5182,15 @@ Starting demo server with 5 tools...
|
|
|
4748
5182
|
await gateway.start();
|
|
4749
5183
|
}
|
|
4750
5184
|
async function handleStatus2(argv) {
|
|
4751
|
-
const { readFileSync:
|
|
4752
|
-
const { join:
|
|
5185
|
+
const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
|
|
5186
|
+
const { join: join5 } = await import("path");
|
|
4753
5187
|
let dir = process.cwd();
|
|
4754
5188
|
const dirIdx = argv.indexOf("--dir");
|
|
4755
5189
|
if (dirIdx !== -1 && argv[dirIdx + 1]) {
|
|
4756
5190
|
dir = argv[dirIdx + 1];
|
|
4757
5191
|
}
|
|
4758
|
-
const logPath =
|
|
4759
|
-
if (!
|
|
5192
|
+
const logPath = join5(dir, ".protect-mcp-log.jsonl");
|
|
5193
|
+
if (!existsSync6(logPath)) {
|
|
4760
5194
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
4761
5195
|
|
|
4762
5196
|
`);
|
|
@@ -4766,7 +5200,7 @@ async function handleStatus2(argv) {
|
|
|
4766
5200
|
`);
|
|
4767
5201
|
process.exit(0);
|
|
4768
5202
|
}
|
|
4769
|
-
const raw =
|
|
5203
|
+
const raw = readFileSync8(logPath, "utf-8");
|
|
4770
5204
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4771
5205
|
if (lines.length === 0) {
|
|
4772
5206
|
process.stderr.write(`${bold("protect-mcp status")}
|
|
@@ -4845,10 +5279,10 @@ ${bold("protect-mcp status")}
|
|
|
4845
5279
|
process.stdout.write(` ${reason.padEnd(25)} ${count}
|
|
4846
5280
|
`);
|
|
4847
5281
|
}
|
|
4848
|
-
const evidencePath =
|
|
4849
|
-
if (
|
|
5282
|
+
const evidencePath = join5(dir, ".protect-mcp-evidence.json");
|
|
5283
|
+
if (existsSync6(evidencePath)) {
|
|
4850
5284
|
try {
|
|
4851
|
-
const evidenceRaw =
|
|
5285
|
+
const evidenceRaw = readFileSync8(evidencePath, "utf-8");
|
|
4852
5286
|
const evidence = JSON.parse(evidenceRaw);
|
|
4853
5287
|
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
4854
5288
|
process.stdout.write(`
|
|
@@ -4857,10 +5291,10 @@ ${bold("protect-mcp status")}
|
|
|
4857
5291
|
} catch {
|
|
4858
5292
|
}
|
|
4859
5293
|
}
|
|
4860
|
-
const keyPath =
|
|
4861
|
-
if (
|
|
5294
|
+
const keyPath = join5(dir, "keys", "gateway.json");
|
|
5295
|
+
if (existsSync6(keyPath)) {
|
|
4862
5296
|
try {
|
|
4863
|
-
const keyData = JSON.parse(
|
|
5297
|
+
const keyData = JSON.parse(readFileSync8(keyPath, "utf-8"));
|
|
4864
5298
|
if (keyData.publicKey) {
|
|
4865
5299
|
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
4866
5300
|
process.stdout.write(`
|
|
@@ -4899,21 +5333,21 @@ function yellow(s) {
|
|
|
4899
5333
|
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
4900
5334
|
}
|
|
4901
5335
|
async function handleDigest(argv) {
|
|
4902
|
-
const { readFileSync:
|
|
4903
|
-
const { join:
|
|
5336
|
+
const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
|
|
5337
|
+
const { join: join5 } = await import("path");
|
|
4904
5338
|
let dir = process.cwd();
|
|
4905
5339
|
const dirIdx = argv.indexOf("--dir");
|
|
4906
5340
|
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4907
5341
|
const today = argv.includes("--today");
|
|
4908
|
-
const logPath =
|
|
4909
|
-
if (!
|
|
5342
|
+
const logPath = join5(dir, ".protect-mcp-log.jsonl");
|
|
5343
|
+
if (!existsSync6(logPath)) {
|
|
4910
5344
|
process.stderr.write(`${bold("protect-mcp digest")}
|
|
4911
5345
|
|
|
4912
5346
|
No log file found. Run protect-mcp first.
|
|
4913
5347
|
`);
|
|
4914
5348
|
process.exit(0);
|
|
4915
5349
|
}
|
|
4916
|
-
const raw =
|
|
5350
|
+
const raw = readFileSync8(logPath, "utf-8");
|
|
4917
5351
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4918
5352
|
let entries = [];
|
|
4919
5353
|
for (const line of lines) {
|
|
@@ -4990,22 +5424,22 @@ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
|
4990
5424
|
`);
|
|
4991
5425
|
}
|
|
4992
5426
|
async function handleReceipts2(argv) {
|
|
4993
|
-
const { readFileSync:
|
|
4994
|
-
const { join:
|
|
5427
|
+
const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
|
|
5428
|
+
const { join: join5 } = await import("path");
|
|
4995
5429
|
let dir = process.cwd();
|
|
4996
5430
|
const dirIdx = argv.indexOf("--dir");
|
|
4997
5431
|
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4998
5432
|
const lastIdx = argv.indexOf("--last");
|
|
4999
5433
|
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
5000
|
-
const receiptsPath =
|
|
5001
|
-
if (!
|
|
5434
|
+
const receiptsPath = join5(dir, ".protect-mcp-receipts.jsonl");
|
|
5435
|
+
if (!existsSync6(receiptsPath)) {
|
|
5002
5436
|
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
5003
5437
|
|
|
5004
5438
|
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
5005
5439
|
`);
|
|
5006
5440
|
process.exit(0);
|
|
5007
5441
|
}
|
|
5008
|
-
const raw =
|
|
5442
|
+
const raw = readFileSync8(receiptsPath, "utf-8");
|
|
5009
5443
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
5010
5444
|
const recent = lines.slice(-count);
|
|
5011
5445
|
process.stdout.write(`
|
|
@@ -5028,32 +5462,32 @@ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
|
5028
5462
|
`);
|
|
5029
5463
|
}
|
|
5030
5464
|
async function handleBundle(argv) {
|
|
5031
|
-
const { readFileSync:
|
|
5032
|
-
const { join:
|
|
5465
|
+
const { readFileSync: readFileSync8, writeFileSync: writeFileSync2, existsSync: existsSync6 } = await import("fs");
|
|
5466
|
+
const { join: join5 } = await import("path");
|
|
5033
5467
|
const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
5034
5468
|
let dir = process.cwd();
|
|
5035
5469
|
const dirIdx = argv.indexOf("--dir");
|
|
5036
5470
|
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
5037
5471
|
const outputIdx = argv.indexOf("--output");
|
|
5038
|
-
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] :
|
|
5039
|
-
const receiptsPath =
|
|
5040
|
-
const keyPath =
|
|
5041
|
-
if (!
|
|
5472
|
+
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join5(dir, "audit-bundle.json");
|
|
5473
|
+
const receiptsPath = join5(dir, ".protect-mcp-receipts.jsonl");
|
|
5474
|
+
const keyPath = join5(dir, "keys", "gateway.json");
|
|
5475
|
+
if (!existsSync6(receiptsPath)) {
|
|
5042
5476
|
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
5043
5477
|
|
|
5044
5478
|
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
5045
5479
|
`);
|
|
5046
5480
|
process.exit(0);
|
|
5047
5481
|
}
|
|
5048
|
-
if (!
|
|
5482
|
+
if (!existsSync6(keyPath)) {
|
|
5049
5483
|
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
5050
5484
|
|
|
5051
5485
|
No key file found at ${keyPath}
|
|
5052
5486
|
`);
|
|
5053
5487
|
process.exit(1);
|
|
5054
5488
|
}
|
|
5055
|
-
const receipts =
|
|
5056
|
-
const keyData = JSON.parse(
|
|
5489
|
+
const receipts = readFileSync8(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
5490
|
+
const keyData = JSON.parse(readFileSync8(keyPath, "utf-8"));
|
|
5057
5491
|
const bundle = createAuditBundle2({
|
|
5058
5492
|
tenant: keyData.issuer || "protect-mcp",
|
|
5059
5493
|
receipts,
|
|
@@ -5079,10 +5513,10 @@ ${bold("protect-mcp bundle")}
|
|
|
5079
5513
|
`);
|
|
5080
5514
|
}
|
|
5081
5515
|
async function handleQuickstart() {
|
|
5082
|
-
const { mkdtempSync, writeFileSync: writeFileSync2, existsSync:
|
|
5083
|
-
const { join:
|
|
5516
|
+
const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync6, mkdirSync, readFileSync: readFileSync8 } = await import("fs");
|
|
5517
|
+
const { join: join5 } = await import("path");
|
|
5084
5518
|
const { tmpdir } = await import("os");
|
|
5085
|
-
const dir = mkdtempSync(
|
|
5519
|
+
const dir = mkdtempSync(join5(tmpdir(), "protect-mcp-quickstart-"));
|
|
5086
5520
|
process.stdout.write(`
|
|
5087
5521
|
${bold("protect-mcp quickstart")}
|
|
5088
5522
|
`);
|
|
@@ -5103,7 +5537,7 @@ ${bold("protect-mcp quickstart")}
|
|
|
5103
5537
|
process.stdout.write(` Working dir: ${dir}
|
|
5104
5538
|
|
|
5105
5539
|
`);
|
|
5106
|
-
const keysDir =
|
|
5540
|
+
const keysDir = join5(dir, "keys");
|
|
5107
5541
|
mkdirSync(keysDir, { recursive: true });
|
|
5108
5542
|
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
5109
5543
|
let keypair;
|
|
@@ -5124,13 +5558,13 @@ ${bold("protect-mcp quickstart")}
|
|
|
5124
5558
|
kid: `quickstart-${Date.now()}`
|
|
5125
5559
|
};
|
|
5126
5560
|
}
|
|
5127
|
-
writeFileSync2(
|
|
5561
|
+
writeFileSync2(join5(keysDir, "gateway.json"), JSON.stringify({
|
|
5128
5562
|
privateKey: keypair.privateKey,
|
|
5129
5563
|
publicKey: keypair.publicKey,
|
|
5130
5564
|
kid: keypair.kid,
|
|
5131
5565
|
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5132
5566
|
}, null, 2) + "\n");
|
|
5133
|
-
const configPath =
|
|
5567
|
+
const configPath = join5(dir, "protect-mcp.json");
|
|
5134
5568
|
const config = {
|
|
5135
5569
|
tools: {
|
|
5136
5570
|
"*": { rate_limit: "100/hour" },
|
|
@@ -5138,7 +5572,7 @@ ${bold("protect-mcp quickstart")}
|
|
|
5138
5572
|
},
|
|
5139
5573
|
default_tier: "unknown",
|
|
5140
5574
|
signing: {
|
|
5141
|
-
key_path:
|
|
5575
|
+
key_path: join5(keysDir, "gateway.json"),
|
|
5142
5576
|
issuer: "protect-mcp-quickstart",
|
|
5143
5577
|
enabled: true
|
|
5144
5578
|
}
|
|
@@ -5180,7 +5614,7 @@ async function handleTrace(argv) {
|
|
|
5180
5614
|
process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
|
|
5181
5615
|
process.exit(1);
|
|
5182
5616
|
}
|
|
5183
|
-
let endpoint = "https://
|
|
5617
|
+
let endpoint = "https://api.scopeblind.com/evidence";
|
|
5184
5618
|
let depth = 3;
|
|
5185
5619
|
for (let i = 1; i < argv.length; i++) {
|
|
5186
5620
|
if (argv[i] === "--endpoint" && argv[i + 1]) {
|
|
@@ -5310,25 +5744,25 @@ ${"\u2500".repeat(60)}
|
|
|
5310
5744
|
`);
|
|
5311
5745
|
}
|
|
5312
5746
|
async function traceLocal(receiptId) {
|
|
5313
|
-
const { readFileSync:
|
|
5314
|
-
const { join:
|
|
5747
|
+
const { readFileSync: readFileSync8, existsSync: existsSync6 } = await import("fs");
|
|
5748
|
+
const { join: join5 } = await import("path");
|
|
5315
5749
|
const dir = process.cwd();
|
|
5316
|
-
const receiptsDir =
|
|
5317
|
-
if (!
|
|
5750
|
+
const receiptsDir = join5(dir, ".protect-mcp", "receipts");
|
|
5751
|
+
if (!existsSync6(receiptsDir)) {
|
|
5318
5752
|
process.stdout.write(` No local receipts found in ${receiptsDir}
|
|
5319
5753
|
|
|
5320
5754
|
`);
|
|
5321
5755
|
return;
|
|
5322
5756
|
}
|
|
5323
|
-
const { readdirSync } = await import("fs");
|
|
5324
|
-
const files =
|
|
5757
|
+
const { readdirSync: readdirSync2 } = await import("fs");
|
|
5758
|
+
const files = readdirSync2(receiptsDir).filter((f) => f.endsWith(".json"));
|
|
5325
5759
|
process.stdout.write(` Scanning ${files.length} local receipts...
|
|
5326
5760
|
|
|
5327
5761
|
`);
|
|
5328
5762
|
const receipts = [];
|
|
5329
5763
|
for (const file of files) {
|
|
5330
5764
|
try {
|
|
5331
|
-
const content =
|
|
5765
|
+
const content = readFileSync8(join5(receiptsDir, file), "utf-8");
|
|
5332
5766
|
const receipt = JSON.parse(content);
|
|
5333
5767
|
receipts.push(receipt);
|
|
5334
5768
|
} catch {
|
|
@@ -5430,12 +5864,57 @@ async function main() {
|
|
|
5430
5864
|
await handleReport(args.slice(1));
|
|
5431
5865
|
process.exit(0);
|
|
5432
5866
|
}
|
|
5433
|
-
|
|
5867
|
+
if (args[0] === "doctor") {
|
|
5868
|
+
await handleDoctor();
|
|
5869
|
+
process.exit(0);
|
|
5870
|
+
}
|
|
5871
|
+
const { policyPath, cedarDir, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
5434
5872
|
let policy = null;
|
|
5435
5873
|
let policyDigest = "none";
|
|
5436
5874
|
let credentials;
|
|
5437
5875
|
let signing;
|
|
5438
|
-
|
|
5876
|
+
let cedarPolicySet = null;
|
|
5877
|
+
let effectiveCedarDir = cedarDir;
|
|
5878
|
+
if (!effectiveCedarDir && !policyPath) {
|
|
5879
|
+
const { existsSync: existsSync6, readdirSync: readdirSync2 } = await import("fs");
|
|
5880
|
+
for (const candidate of ["cedar", "policies", "."]) {
|
|
5881
|
+
try {
|
|
5882
|
+
if (existsSync6(candidate) && readdirSync2(candidate).some((f) => f.endsWith(".cedar"))) {
|
|
5883
|
+
effectiveCedarDir = candidate;
|
|
5884
|
+
process.stderr.write(`[PROTECT_MCP] Auto-detected Cedar policies in ./${candidate}/
|
|
5885
|
+
`);
|
|
5886
|
+
break;
|
|
5887
|
+
}
|
|
5888
|
+
} catch {
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
if (effectiveCedarDir) {
|
|
5893
|
+
try {
|
|
5894
|
+
const cedarAvailable = await isCedarAvailable();
|
|
5895
|
+
if (!cedarAvailable) {
|
|
5896
|
+
process.stderr.write("[PROTECT_MCP] Warning: @cedar-policy/cedar-wasm not installed. Install with: npm install @cedar-policy/cedar-wasm\n");
|
|
5897
|
+
process.stderr.write("[PROTECT_MCP] Cedar policies will be loaded but evaluated with fallback (allow-all).\n");
|
|
5898
|
+
}
|
|
5899
|
+
cedarPolicySet = loadCedarPolicies(effectiveCedarDir);
|
|
5900
|
+
policyDigest = cedarPolicySet.digest;
|
|
5901
|
+
policy = {
|
|
5902
|
+
tools: { "*": { require: "any" } },
|
|
5903
|
+
policy_engine: "cedar",
|
|
5904
|
+
cedar_dir: effectiveCedarDir
|
|
5905
|
+
};
|
|
5906
|
+
process.stderr.write(`[PROTECT_MCP] Cedar policy engine: loaded ${cedarPolicySet.fileCount} policies from ${effectiveCedarDir} (digest: ${policyDigest})
|
|
5907
|
+
`);
|
|
5908
|
+
if (verbose) {
|
|
5909
|
+
process.stderr.write(`[PROTECT_MCP] Cedar files: ${cedarPolicySet.files.join(", ")}
|
|
5910
|
+
`);
|
|
5911
|
+
}
|
|
5912
|
+
} catch (err) {
|
|
5913
|
+
process.stderr.write(`[PROTECT_MCP] Error loading Cedar policies: ${err instanceof Error ? err.message : err}
|
|
5914
|
+
`);
|
|
5915
|
+
process.exit(1);
|
|
5916
|
+
}
|
|
5917
|
+
} else if (policyPath) {
|
|
5439
5918
|
try {
|
|
5440
5919
|
const loaded = loadPolicy(policyPath);
|
|
5441
5920
|
policy = loaded.policy;
|
|
@@ -5486,6 +5965,9 @@ async function main() {
|
|
|
5486
5965
|
return;
|
|
5487
5966
|
}
|
|
5488
5967
|
const gateway = new ProtectGateway(config);
|
|
5968
|
+
if (cedarPolicySet) {
|
|
5969
|
+
gateway.setCedarPolicies(cedarPolicySet);
|
|
5970
|
+
}
|
|
5489
5971
|
await gateway.start();
|
|
5490
5972
|
}
|
|
5491
5973
|
async function handleSimulate(args) {
|
|
@@ -5508,8 +5990,8 @@ async function handleSimulate(args) {
|
|
|
5508
5990
|
process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
|
|
5509
5991
|
process.exit(1);
|
|
5510
5992
|
}
|
|
5511
|
-
const { existsSync:
|
|
5512
|
-
if (!
|
|
5993
|
+
const { existsSync: existsSync6 } = await import("fs");
|
|
5994
|
+
if (!existsSync6(logPath)) {
|
|
5513
5995
|
process.stderr.write(`Log file not found: ${logPath}
|
|
5514
5996
|
`);
|
|
5515
5997
|
process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
|
|
@@ -5530,6 +6012,127 @@ async function handleSimulate(args) {
|
|
|
5530
6012
|
process.stdout.write(formatSimulation(summary) + "\n");
|
|
5531
6013
|
}
|
|
5532
6014
|
}
|
|
6015
|
+
async function handleDoctor() {
|
|
6016
|
+
const { existsSync: existsSync6, readFileSync: readFileSync8, readdirSync: readdirSync2 } = await import("fs");
|
|
6017
|
+
const { join: join5 } = await import("path");
|
|
6018
|
+
const { execSync } = await import("child_process");
|
|
6019
|
+
const green2 = (s) => `\x1B[32m\u2713\x1B[0m ${s}`;
|
|
6020
|
+
const red2 = (s) => `\x1B[31m\u2717\x1B[0m ${s}`;
|
|
6021
|
+
const yellow2 = (s) => `\x1B[33m\u26A0\x1B[0m ${s}`;
|
|
6022
|
+
const dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
6023
|
+
process.stdout.write("\n\x1B[1mprotect-mcp doctor\x1B[0m\n");
|
|
6024
|
+
process.stdout.write(dim2("Checking your ScopeBlind setup...\n\n"));
|
|
6025
|
+
let issues = 0;
|
|
6026
|
+
const nodeVersion = process.version;
|
|
6027
|
+
const major = parseInt(nodeVersion.slice(1));
|
|
6028
|
+
if (major >= 18) {
|
|
6029
|
+
process.stdout.write(green2(`Node.js ${nodeVersion}
|
|
6030
|
+
`));
|
|
6031
|
+
} else {
|
|
6032
|
+
process.stdout.write(red2(`Node.js ${nodeVersion} \u2014 requires >= 18
|
|
6033
|
+
`));
|
|
6034
|
+
issues++;
|
|
6035
|
+
}
|
|
6036
|
+
const configPath = join5(process.cwd(), "scopeblind.config.json");
|
|
6037
|
+
if (existsSync6(configPath)) {
|
|
6038
|
+
try {
|
|
6039
|
+
const config = JSON.parse(readFileSync8(configPath, "utf-8"));
|
|
6040
|
+
if (config.signing?.private_key || config.signing?.key_file) {
|
|
6041
|
+
process.stdout.write(green2("Signing keys configured\n"));
|
|
6042
|
+
} else {
|
|
6043
|
+
process.stdout.write(yellow2("Config found but no signing keys \u2014 run: protect-mcp init\n"));
|
|
6044
|
+
issues++;
|
|
6045
|
+
}
|
|
6046
|
+
} catch {
|
|
6047
|
+
process.stdout.write(red2("Invalid scopeblind.config.json\n"));
|
|
6048
|
+
issues++;
|
|
6049
|
+
}
|
|
6050
|
+
} else {
|
|
6051
|
+
process.stdout.write(yellow2("No scopeblind.config.json \u2014 run: protect-mcp init\n"));
|
|
6052
|
+
}
|
|
6053
|
+
let policyFound = false;
|
|
6054
|
+
for (const dir of ["cedar", "policies", "."]) {
|
|
6055
|
+
try {
|
|
6056
|
+
if (existsSync6(dir) && readdirSync2(dir).some((f) => f.endsWith(".cedar"))) {
|
|
6057
|
+
process.stdout.write(green2(`Cedar policies found in ./${dir}/
|
|
6058
|
+
`));
|
|
6059
|
+
policyFound = true;
|
|
6060
|
+
break;
|
|
6061
|
+
}
|
|
6062
|
+
} catch {
|
|
6063
|
+
}
|
|
6064
|
+
}
|
|
6065
|
+
if (!policyFound) {
|
|
6066
|
+
for (const name of ["policy.json", "protect-mcp.policy.json", "scopeblind-policy.json"]) {
|
|
6067
|
+
if (existsSync6(name)) {
|
|
6068
|
+
process.stdout.write(green2(`JSON policy found: ${name}
|
|
6069
|
+
`));
|
|
6070
|
+
policyFound = true;
|
|
6071
|
+
break;
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
if (!policyFound) {
|
|
6076
|
+
process.stdout.write(yellow2("No policy files found \u2014 running in shadow mode (allow all)\n"));
|
|
6077
|
+
}
|
|
6078
|
+
try {
|
|
6079
|
+
const cedarAvailable = await isCedarAvailable();
|
|
6080
|
+
if (cedarAvailable) {
|
|
6081
|
+
process.stdout.write(green2("Cedar WASM engine available\n"));
|
|
6082
|
+
} else {
|
|
6083
|
+
process.stdout.write(dim2(" Cedar WASM not installed \u2014 install: npm install @cedar-policy/cedar-wasm\n"));
|
|
6084
|
+
}
|
|
6085
|
+
} catch {
|
|
6086
|
+
process.stdout.write(dim2(" Cedar WASM not installed\n"));
|
|
6087
|
+
}
|
|
6088
|
+
const logFile = join5(process.cwd(), "protect-mcp-decisions.jsonl");
|
|
6089
|
+
const receiptFile = join5(process.cwd(), "protect-mcp-receipts.jsonl");
|
|
6090
|
+
if (existsSync6(logFile)) {
|
|
6091
|
+
try {
|
|
6092
|
+
const lines = readFileSync8(logFile, "utf-8").trim().split("\n").length;
|
|
6093
|
+
process.stdout.write(green2(`Decision log: ${lines} entries
|
|
6094
|
+
`));
|
|
6095
|
+
} catch {
|
|
6096
|
+
process.stdout.write(green2("Decision log exists\n"));
|
|
6097
|
+
}
|
|
6098
|
+
} else {
|
|
6099
|
+
process.stdout.write(dim2(" No decision log yet \u2014 will be created on first tool call\n"));
|
|
6100
|
+
}
|
|
6101
|
+
if (existsSync6(receiptFile)) {
|
|
6102
|
+
try {
|
|
6103
|
+
const lines = readFileSync8(receiptFile, "utf-8").trim().split("\n").length;
|
|
6104
|
+
process.stdout.write(green2(`Receipt file: ${lines} signed receipts
|
|
6105
|
+
`));
|
|
6106
|
+
} catch {
|
|
6107
|
+
process.stdout.write(green2("Receipt file exists\n"));
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
try {
|
|
6111
|
+
execSync("npx @veritasacta/verify --version 2>/dev/null", { stdio: "pipe", timeout: 1e4 });
|
|
6112
|
+
process.stdout.write(green2("Verifier available: @veritasacta/verify\n"));
|
|
6113
|
+
} catch {
|
|
6114
|
+
process.stdout.write(dim2(" Verifier not cached \u2014 install: npm install -g @veritasacta/verify\n"));
|
|
6115
|
+
}
|
|
6116
|
+
try {
|
|
6117
|
+
const res = await fetch("https://api.scopeblind.com/health", { signal: AbortSignal.timeout(5e3) });
|
|
6118
|
+
if (res.ok) {
|
|
6119
|
+
process.stdout.write(green2("ScopeBlind API reachable\n"));
|
|
6120
|
+
} else {
|
|
6121
|
+
process.stdout.write(yellow2("ScopeBlind API returned non-200 \u2014 receipts will be stored locally\n"));
|
|
6122
|
+
}
|
|
6123
|
+
} catch {
|
|
6124
|
+
process.stdout.write(dim2(" ScopeBlind API not reachable \u2014 offline mode (receipts stored locally)\n"));
|
|
6125
|
+
}
|
|
6126
|
+
process.stdout.write("\n");
|
|
6127
|
+
if (issues === 0) {
|
|
6128
|
+
process.stdout.write("\x1B[32m\x1B[1mAll checks passed.\x1B[0m Ready to wrap MCP servers.\n");
|
|
6129
|
+
process.stdout.write(dim2("\n npx protect-mcp -- node your-server.js\n\n"));
|
|
6130
|
+
} else {
|
|
6131
|
+
process.stdout.write(`\x1B[33m\x1B[1m${issues} issue(s) found.\x1B[0m Fix them and run doctor again.
|
|
6132
|
+
|
|
6133
|
+
`);
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
5533
6136
|
async function handleReport(args) {
|
|
5534
6137
|
let period = 30;
|
|
5535
6138
|
let format = "json";
|
|
@@ -5548,9 +6151,9 @@ async function handleReport(args) {
|
|
|
5548
6151
|
}
|
|
5549
6152
|
}
|
|
5550
6153
|
const { generateReport: generateReport2, formatReportMarkdown: formatReportMarkdown2 } = await Promise.resolve().then(() => (init_report(), report_exports));
|
|
5551
|
-
const { join:
|
|
5552
|
-
const logPath =
|
|
5553
|
-
const receiptPath =
|
|
6154
|
+
const { join: join5 } = await import("path");
|
|
6155
|
+
const logPath = join5(dir, ".protect-mcp-log.jsonl");
|
|
6156
|
+
const receiptPath = join5(dir, ".protect-mcp-receipts.jsonl");
|
|
5554
6157
|
const report = generateReport2(logPath, receiptPath, period);
|
|
5555
6158
|
let output;
|
|
5556
6159
|
if (format === "md") {
|