@srhot/mcp-guard 0.1.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CONTRIBUTING.md +22 -0
  3. package/LICENSE +21 -0
  4. package/README.md +202 -0
  5. package/SECURITY.md +11 -0
  6. package/dist/cli.d.ts +7 -0
  7. package/dist/cli.js +73 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/policy/loadPolicy.d.ts +3 -0
  13. package/dist/policy/loadPolicy.js +67 -0
  14. package/dist/policy/loadPolicy.js.map +1 -0
  15. package/dist/policy/policyEngine.d.ts +3 -0
  16. package/dist/policy/policyEngine.js +30 -0
  17. package/dist/policy/policyEngine.js.map +1 -0
  18. package/dist/reporters/consoleReporter.d.ts +2 -0
  19. package/dist/reporters/consoleReporter.js +23 -0
  20. package/dist/reporters/consoleReporter.js.map +1 -0
  21. package/dist/reporters/jsonReporter.d.ts +2 -0
  22. package/dist/reporters/jsonReporter.js +4 -0
  23. package/dist/reporters/jsonReporter.js.map +1 -0
  24. package/dist/reporters/markdownReporter.d.ts +2 -0
  25. package/dist/reporters/markdownReporter.js +33 -0
  26. package/dist/reporters/markdownReporter.js.map +1 -0
  27. package/dist/rules/MCP01_SECRET_EXPOSURE.d.ts +2 -0
  28. package/dist/rules/MCP01_SECRET_EXPOSURE.js +48 -0
  29. package/dist/rules/MCP01_SECRET_EXPOSURE.js.map +1 -0
  30. package/dist/rules/MCP03_TOOL_POISONING.d.ts +2 -0
  31. package/dist/rules/MCP03_TOOL_POISONING.js +37 -0
  32. package/dist/rules/MCP03_TOOL_POISONING.js.map +1 -0
  33. package/dist/rules/MCP05_COMMAND_EXECUTION.d.ts +2 -0
  34. package/dist/rules/MCP05_COMMAND_EXECUTION.js +39 -0
  35. package/dist/rules/MCP05_COMMAND_EXECUTION.js.map +1 -0
  36. package/dist/rules/MCP07_INSUFFICIENT_AUTH.d.ts +2 -0
  37. package/dist/rules/MCP07_INSUFFICIENT_AUTH.js +63 -0
  38. package/dist/rules/MCP07_INSUFFICIENT_AUTH.js.map +1 -0
  39. package/dist/rules/MCP08_AUDIT_GAP.d.ts +2 -0
  40. package/dist/rules/MCP08_AUDIT_GAP.js +30 -0
  41. package/dist/rules/MCP08_AUDIT_GAP.js.map +1 -0
  42. package/dist/rules/MCP09_SHADOW_SERVER.d.ts +2 -0
  43. package/dist/rules/MCP09_SHADOW_SERVER.js +64 -0
  44. package/dist/rules/MCP09_SHADOW_SERVER.js.map +1 -0
  45. package/dist/rules/MCP10_CONTEXT_OVERSHARING.d.ts +2 -0
  46. package/dist/rules/MCP10_CONTEXT_OVERSHARING.js +39 -0
  47. package/dist/rules/MCP10_CONTEXT_OVERSHARING.js.map +1 -0
  48. package/dist/rules/index.d.ts +10 -0
  49. package/dist/rules/index.js +18 -0
  50. package/dist/rules/index.js.map +1 -0
  51. package/dist/scanner/fileLoader.d.ts +3 -0
  52. package/dist/scanner/fileLoader.js +48 -0
  53. package/dist/scanner/fileLoader.js.map +1 -0
  54. package/dist/scanner/scanTarget.d.ts +2 -0
  55. package/dist/scanner/scanTarget.js +21 -0
  56. package/dist/scanner/scanTarget.js.map +1 -0
  57. package/dist/types.d.ts +58 -0
  58. package/dist/types.js +2 -0
  59. package/dist/types.js.map +1 -0
  60. package/dist/utils/paths.d.ts +2 -0
  61. package/dist/utils/paths.js +8 -0
  62. package/dist/utils/paths.js.map +1 -0
  63. package/dist/utils/severity.d.ts +4 -0
  64. package/dist/utils/severity.js +20 -0
  65. package/dist/utils/severity.js.map +1 -0
  66. package/docs/architecture.md +11 -0
  67. package/docs/release.md +42 -0
  68. package/docs/roadmap.md +34 -0
  69. package/docs/rules.md +21 -0
  70. package/examples/bad-mcp-server/.env.example +4 -0
  71. package/examples/bad-mcp-server/mcp.json +11 -0
  72. package/examples/bad-mcp-server/server.ts +13 -0
  73. package/examples/bad-mcp-server/tools.json +17 -0
  74. package/examples/safe-mcp-server/mcp.json +13 -0
  75. package/examples/safe-mcp-server/server.ts +9 -0
  76. package/examples/safe-mcp-server/tools.json +17 -0
  77. package/mcpguard.config.yml +29 -0
  78. package/package.json +80 -0
@@ -0,0 +1,37 @@
1
+ import { lineNumberForIndex } from "../scanner/fileLoader.js";
2
+ const poisoningPatterns = [
3
+ /ignore\s+(?:all\s+)?previous\s+instructions/gi,
4
+ /reveal\s+secrets/gi,
5
+ /send\s+private\s+files/gi,
6
+ /exfiltrate/gi,
7
+ /hidden\s+instruction/gi,
8
+ /system\s+prompt/gi,
9
+ /developer\s+message/gi,
10
+ /bypass\s+policy/gi,
11
+ /do\s+not\s+tell\s+the\s+user/gi
12
+ ];
13
+ export const MCP03_TOOL_POISONING = {
14
+ id: "MCP03_TOOL_POISONING",
15
+ scan(context) {
16
+ const findings = [];
17
+ for (const file of context.files) {
18
+ for (const regex of poisoningPatterns) {
19
+ for (const match of file.content.matchAll(regex)) {
20
+ findings.push({
21
+ id: "MCP03_TOOL_POISONING",
22
+ title: "Suspicious tool instruction",
23
+ severity: "HIGH",
24
+ category: "Tool Integrity",
25
+ message: "Tool metadata or source text contains instruction-like content that could poison an agent.",
26
+ filePath: file.filePath,
27
+ lineNumber: lineNumberForIndex(file.content, match.index ?? 0),
28
+ evidence: match[0],
29
+ recommendation: "Remove hidden instructions from tool descriptions and keep tool metadata limited to user-visible behavior."
30
+ });
31
+ }
32
+ }
33
+ }
34
+ return findings;
35
+ }
36
+ };
37
+ //# sourceMappingURL=MCP03_TOOL_POISONING.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MCP03_TOOL_POISONING.js","sourceRoot":"","sources":["../../src/rules/MCP03_TOOL_POISONING.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,iBAAiB,GAAG;IACxB,+CAA+C;IAC/C,oBAAoB;IACpB,0BAA0B;IAC1B,cAAc;IACd,wBAAwB;IACxB,mBAAmB;IACnB,uBAAuB;IACvB,mBAAmB;IACnB,gCAAgC;CACjC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAS;IACxC,EAAE,EAAE,sBAAsB;IAC1B,IAAI,CAAC,OAAO;QACV,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,KAAK,IAAI,iBAAiB,EAAE,CAAC;gBACtC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBACjD,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,sBAAsB;wBAC1B,KAAK,EAAE,6BAA6B;wBACpC,QAAQ,EAAE,MAAM;wBAChB,QAAQ,EAAE,gBAAgB;wBAC1B,OAAO,EACL,4FAA4F;wBAC9F,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;wBAC9D,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;wBAClB,cAAc,EACZ,4GAA4G;qBAC/G,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const MCP05_COMMAND_EXECUTION: Rule;
@@ -0,0 +1,39 @@
1
+ import { lineNumberForIndex } from "../scanner/fileLoader.js";
2
+ const commandPatterns = [
3
+ { label: "child_process usage", regex: /\bchild_process\b/gi },
4
+ { label: "exec call", regex: /\bexec\s*\(/gi },
5
+ { label: "spawn call", regex: /\bspawn\s*\(/gi },
6
+ { label: "shell true option", regex: /\bshell\s*:\s*true\b/gi },
7
+ { label: "arbitrary command input", regex: /arbitrary\s+command\s+input/gi },
8
+ { label: "run_command tool", regex: /\brun_command\b/gi },
9
+ { label: "run_shell tool", regex: /\brun_shell\b/gi },
10
+ { label: "PowerShell command", regex: /\bpowershell\b/gi },
11
+ { label: "bash -c command", regex: /\bbash\s+-c\b/gi },
12
+ { label: "recursive deletion", regex: /\brm\s+-rf\b/gi },
13
+ { label: "pipe to shell", regex: /\bcurl\b\s*\|\s*sh\b/gi }
14
+ ];
15
+ export const MCP05_COMMAND_EXECUTION = {
16
+ id: "MCP05_COMMAND_EXECUTION",
17
+ scan(context) {
18
+ const findings = [];
19
+ for (const file of context.files) {
20
+ for (const pattern of commandPatterns) {
21
+ for (const match of file.content.matchAll(pattern.regex)) {
22
+ findings.push({
23
+ id: "MCP05_COMMAND_EXECUTION",
24
+ title: "Risky command execution surface",
25
+ severity: "HIGH",
26
+ category: "Command Execution",
27
+ message: `Detected ${pattern.label}, which can expose the host to command execution risk.`,
28
+ filePath: file.filePath,
29
+ lineNumber: lineNumberForIndex(file.content, match.index ?? 0),
30
+ evidence: match[0],
31
+ recommendation: "Avoid arbitrary shell execution. If command execution is required, use a strict allowlist, fixed arguments, and explicit user consent."
32
+ });
33
+ }
34
+ }
35
+ }
36
+ return findings;
37
+ }
38
+ };
39
+ //# sourceMappingURL=MCP05_COMMAND_EXECUTION.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MCP05_COMMAND_EXECUTION.js","sourceRoot":"","sources":["../../src/rules/MCP05_COMMAND_EXECUTION.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,eAAe,GAA4C;IAC/D,EAAE,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,qBAAqB,EAAE;IAC9D,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE;IAC9C,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,gBAAgB,EAAE;IAChD,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,wBAAwB,EAAE;IAC/D,EAAE,KAAK,EAAE,yBAAyB,EAAE,KAAK,EAAE,+BAA+B,EAAE;IAC5E,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IACzD,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACrD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC1D,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,iBAAiB,EAAE;IACtD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACxD,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,wBAAwB,EAAE;CAC5D,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAS;IAC3C,EAAE,EAAE,yBAAyB;IAC7B,IAAI,CAAC,OAAO;QACV,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;gBACtC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzD,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,yBAAyB;wBAC7B,KAAK,EAAE,iCAAiC;wBACxC,QAAQ,EAAE,MAAM;wBAChB,QAAQ,EAAE,mBAAmB;wBAC7B,OAAO,EAAE,YAAY,OAAO,CAAC,KAAK,wDAAwD;wBAC1F,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;wBAC9D,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;wBAClB,cAAc,EACZ,wIAAwI;qBAC3I,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const MCP07_INSUFFICIENT_AUTH: Rule;
@@ -0,0 +1,63 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ const configExtensions = /\.(json|ya?ml)$/i;
3
+ export const MCP07_INSUFFICIENT_AUTH = {
4
+ id: "MCP07_INSUFFICIENT_AUTH",
5
+ scan(context) {
6
+ const findings = [];
7
+ for (const file of context.files.filter((candidate) => configExtensions.test(candidate.filePath))) {
8
+ const parsed = parseConfig(file);
9
+ const contentSuggestsRemote = /\bhttps?:\/\//i.test(file.content) ||
10
+ /"host"\s*:\s*"0\.0\.0\.0"/i.test(file.content) ||
11
+ /\btransport\s*:\s*http/i.test(file.content);
12
+ const explicitlyMissingAuth = /\bauth\s*[:=]\s*false\b/i.test(file.content) ||
13
+ /\bauthentication\s*[:=]\s*["']?none/i.test(file.content);
14
+ if ((contentSuggestsRemote && !hasAuth(parsed)) || explicitlyMissingAuth) {
15
+ findings.push({
16
+ id: "MCP07_INSUFFICIENT_AUTH",
17
+ title: "Remote MCP endpoint without sufficient auth",
18
+ severity: explicitlyMissingAuth ? "HIGH" : "MEDIUM",
19
+ category: "Authentication",
20
+ message: "Configuration exposes a remote HTTP surface without clear authentication controls.",
21
+ filePath: file.filePath,
22
+ lineNumber: 1,
23
+ evidence: explicitlyMissingAuth
24
+ ? "auth: false or authentication: none"
25
+ : "remote endpoint without auth",
26
+ recommendation: "Require authentication for remote transports and document the expected credential flow."
27
+ });
28
+ }
29
+ }
30
+ return findings;
31
+ }
32
+ };
33
+ function parseConfig(file) {
34
+ try {
35
+ return file.filePath.endsWith(".json") ? JSON.parse(file.content) : parseYaml(file.content);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ function hasAuth(value) {
42
+ if (!value || typeof value !== "object") {
43
+ return false;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ return value.some(hasAuth);
47
+ }
48
+ const record = value;
49
+ for (const [key, child] of Object.entries(record)) {
50
+ const normalizedKey = key.toLowerCase();
51
+ if (["auth", "authentication", "apikey", "tokenrequired", "requireauth"].includes(normalizedKey)) {
52
+ if (child === false || child === "none") {
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+ if (hasAuth(child)) {
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ //# sourceMappingURL=MCP07_INSUFFICIENT_AUTH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MCP07_INSUFFICIENT_AUTH.js","sourceRoot":"","sources":["../../src/rules/MCP07_INSUFFICIENT_AUTH.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const MCP08_AUDIT_GAP: Rule;
@@ -0,0 +1,30 @@
1
+ export const MCP08_AUDIT_GAP = {
2
+ id: "MCP08_AUDIT_GAP",
3
+ scan(context) {
4
+ if (!context.policy.require.auditLogging) {
5
+ return [];
6
+ }
7
+ const hasAuditLogging = context.files.some((file) => /(?:auditLogging|audit_logging|auditLog)\s*["']?\s*:\s*true/i.test(file.content));
8
+ if (hasAuditLogging) {
9
+ return [];
10
+ }
11
+ const anchor = context.files.find((file) => /mcp\.(json|ya?ml)$/i.test(file.filePath)) ?? context.files[0];
12
+ if (!anchor) {
13
+ return [];
14
+ }
15
+ return [
16
+ {
17
+ id: "MCP08_AUDIT_GAP",
18
+ title: "Audit logging requirement not satisfied",
19
+ severity: "MEDIUM",
20
+ category: "Audit",
21
+ message: "Policy requires audit logging, but the scanned project does not declare auditLogging: true.",
22
+ filePath: anchor.filePath,
23
+ lineNumber: 1,
24
+ evidence: "require.auditLogging: true",
25
+ recommendation: "Enable audit logging in the MCP server configuration and retain logs for security review."
26
+ }
27
+ ];
28
+ }
29
+ };
30
+ //# sourceMappingURL=MCP08_AUDIT_GAP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MCP08_AUDIT_GAP.js","sourceRoot":"","sources":["../../src/rules/MCP08_AUDIT_GAP.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAS;IACnC,EAAE,EAAE,iBAAiB;IACrB,IAAI,CAAC,OAAO;QACV,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;YACzC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,eAAe,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAClD,6DAA6D,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CACjF,CAAC;QAEF,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GACV,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAE9F,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL;gBACE,EAAE,EAAE,iBAAiB;gBACrB,KAAK,EAAE,yCAAyC;gBAChD,QAAQ,EAAE,QAAQ;gBAClB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EACL,6FAA6F;gBAC/F,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,UAAU,EAAE,CAAC;gBACb,QAAQ,EAAE,4BAA4B;gBACtC,cAAc,EACZ,2FAA2F;aAC9F;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const MCP09_SHADOW_SERVER: Rule;
@@ -0,0 +1,64 @@
1
+ import { parse as parseYaml } from "yaml";
2
+ export const MCP09_SHADOW_SERVER = {
3
+ id: "MCP09_SHADOW_SERVER",
4
+ scan(context) {
5
+ if (context.policy.allowlist.servers.length === 0) {
6
+ return [];
7
+ }
8
+ const allowlist = new Set(context.policy.allowlist.servers);
9
+ const findings = [];
10
+ for (const file of context.files.filter((candidate) => /mcp\.(json|ya?ml)$/i.test(candidate.filePath))) {
11
+ for (const serverName of extractServerNames(file)) {
12
+ if (!allowlist.has(serverName)) {
13
+ findings.push({
14
+ id: "MCP09_SHADOW_SERVER",
15
+ title: "Shadow MCP server",
16
+ severity: "MEDIUM",
17
+ category: "Inventory",
18
+ message: `Server "${serverName}" is not present in the policy allowlist.`,
19
+ filePath: file.filePath,
20
+ lineNumber: 1,
21
+ evidence: serverName,
22
+ recommendation: "Add expected MCP servers to the allowlist and investigate unrecognized server definitions."
23
+ });
24
+ }
25
+ }
26
+ }
27
+ return findings;
28
+ }
29
+ };
30
+ function extractServerNames(file) {
31
+ const parsed = parseConfig(file);
32
+ const names = new Set();
33
+ collectNames(parsed, names);
34
+ return [...names];
35
+ }
36
+ function parseConfig(file) {
37
+ try {
38
+ return file.filePath.endsWith(".json") ? JSON.parse(file.content) : parseYaml(file.content);
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ function collectNames(value, names) {
45
+ if (!value || typeof value !== "object") {
46
+ return;
47
+ }
48
+ if (Array.isArray(value)) {
49
+ value.forEach((child) => collectNames(child, names));
50
+ return;
51
+ }
52
+ const record = value;
53
+ if (typeof record.name === "string") {
54
+ names.add(record.name);
55
+ }
56
+ if (record.servers && typeof record.servers === "object" && !Array.isArray(record.servers)) {
57
+ for (const [name, child] of Object.entries(record.servers)) {
58
+ names.add(name);
59
+ collectNames(child, names);
60
+ }
61
+ }
62
+ Object.values(record).forEach((child) => collectNames(child, names));
63
+ }
64
+ //# sourceMappingURL=MCP09_SHADOW_SERVER.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MCP09_SHADOW_SERVER.js","sourceRoot":"","sources":["../../src/rules/MCP09_SHADOW_SERVER.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const MCP10_CONTEXT_OVERSHARING: Rule;
@@ -0,0 +1,39 @@
1
+ import { lineNumberForIndex } from "../scanner/fileLoader.js";
2
+ const broadExposurePatterns = [
3
+ { label: "filesystem root", regex: /["']\/["']/g },
4
+ { label: "root glob", regex: /["']\/\*\*?["']/g },
5
+ { label: "Windows drive root", regex: /["']C:\\\\?["']/gi },
6
+ { label: "Windows user directory", regex: /C:\\\\?Users\b/gi },
7
+ { label: "Unix home directory", regex: /["']\/home["']/gi },
8
+ { label: "macOS users directory", regex: /["']\/Users["']/g },
9
+ { label: "HOME variable", regex: /\$\{HOME\}/g },
10
+ {
11
+ label: "entire workspace read/write",
12
+ regex: /entire\s+workspace\s+with\s+read\/write\s+access/gi
13
+ }
14
+ ];
15
+ export const MCP10_CONTEXT_OVERSHARING = {
16
+ id: "MCP10_CONTEXT_OVERSHARING",
17
+ scan(context) {
18
+ const findings = [];
19
+ for (const file of context.files) {
20
+ for (const pattern of broadExposurePatterns) {
21
+ for (const match of file.content.matchAll(pattern.regex)) {
22
+ findings.push({
23
+ id: "MCP10_CONTEXT_OVERSHARING",
24
+ title: "Overly broad context exposure",
25
+ severity: "HIGH",
26
+ category: "Context Boundaries",
27
+ message: `Detected ${pattern.label}, which can expose more files or resources than an agent needs.`,
28
+ filePath: file.filePath,
29
+ lineNumber: lineNumberForIndex(file.content, match.index ?? 0),
30
+ evidence: match[0],
31
+ recommendation: "Restrict filesystem and resource access to the smallest explicit paths required by the MCP tool."
32
+ });
33
+ }
34
+ }
35
+ }
36
+ return findings;
37
+ }
38
+ };
39
+ //# sourceMappingURL=MCP10_CONTEXT_OVERSHARING.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MCP10_CONTEXT_OVERSHARING.js","sourceRoot":"","sources":["../../src/rules/MCP10_CONTEXT_OVERSHARING.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,MAAM,qBAAqB,GAA4C;IACrE,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,aAAa,EAAE;IAClD,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,kBAAkB,EAAE;IACjD,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,mBAAmB,EAAE;IAC3D,EAAE,KAAK,EAAE,wBAAwB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC9D,EAAE,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC3D,EAAE,KAAK,EAAE,uBAAuB,EAAE,KAAK,EAAE,kBAAkB,EAAE;IAC7D,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE;IAChD;QACE,KAAK,EAAE,6BAA6B;QACpC,KAAK,EAAE,oDAAoD;KAC5D;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAS;IAC7C,EAAE,EAAE,2BAA2B;IAC/B,IAAI,CAAC,OAAO;QACV,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,OAAO,IAAI,qBAAqB,EAAE,CAAC;gBAC5C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzD,QAAQ,CAAC,IAAI,CAAC;wBACZ,EAAE,EAAE,2BAA2B;wBAC/B,KAAK,EAAE,+BAA+B;wBACtC,QAAQ,EAAE,MAAM;wBAChB,QAAQ,EAAE,oBAAoB;wBAC9B,OAAO,EAAE,YAAY,OAAO,CAAC,KAAK,iEAAiE;wBACnG,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,UAAU,EAAE,kBAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;wBAC9D,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;wBAClB,cAAc,EACZ,kGAAkG;qBACrG,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { Rule } from "../types.js";
2
+ import { MCP01_SECRET_EXPOSURE } from "./MCP01_SECRET_EXPOSURE.js";
3
+ import { MCP03_TOOL_POISONING } from "./MCP03_TOOL_POISONING.js";
4
+ import { MCP05_COMMAND_EXECUTION } from "./MCP05_COMMAND_EXECUTION.js";
5
+ import { MCP07_INSUFFICIENT_AUTH } from "./MCP07_INSUFFICIENT_AUTH.js";
6
+ import { MCP08_AUDIT_GAP } from "./MCP08_AUDIT_GAP.js";
7
+ import { MCP09_SHADOW_SERVER } from "./MCP09_SHADOW_SERVER.js";
8
+ import { MCP10_CONTEXT_OVERSHARING } from "./MCP10_CONTEXT_OVERSHARING.js";
9
+ export declare const rules: Rule[];
10
+ export { MCP01_SECRET_EXPOSURE, MCP03_TOOL_POISONING, MCP05_COMMAND_EXECUTION, MCP07_INSUFFICIENT_AUTH, MCP08_AUDIT_GAP, MCP09_SHADOW_SERVER, MCP10_CONTEXT_OVERSHARING };
@@ -0,0 +1,18 @@
1
+ import { MCP01_SECRET_EXPOSURE } from "./MCP01_SECRET_EXPOSURE.js";
2
+ import { MCP03_TOOL_POISONING } from "./MCP03_TOOL_POISONING.js";
3
+ import { MCP05_COMMAND_EXECUTION } from "./MCP05_COMMAND_EXECUTION.js";
4
+ import { MCP07_INSUFFICIENT_AUTH } from "./MCP07_INSUFFICIENT_AUTH.js";
5
+ import { MCP08_AUDIT_GAP } from "./MCP08_AUDIT_GAP.js";
6
+ import { MCP09_SHADOW_SERVER } from "./MCP09_SHADOW_SERVER.js";
7
+ import { MCP10_CONTEXT_OVERSHARING } from "./MCP10_CONTEXT_OVERSHARING.js";
8
+ export const rules = [
9
+ MCP01_SECRET_EXPOSURE,
10
+ MCP03_TOOL_POISONING,
11
+ MCP05_COMMAND_EXECUTION,
12
+ MCP07_INSUFFICIENT_AUTH,
13
+ MCP08_AUDIT_GAP,
14
+ MCP09_SHADOW_SERVER,
15
+ MCP10_CONTEXT_OVERSHARING
16
+ ];
17
+ export { MCP01_SECRET_EXPOSURE, MCP03_TOOL_POISONING, MCP05_COMMAND_EXECUTION, MCP07_INSUFFICIENT_AUTH, MCP08_AUDIT_GAP, MCP09_SHADOW_SERVER, MCP10_CONTEXT_OVERSHARING };
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,MAAM,CAAC,MAAM,KAAK,GAAW;IAC3B,qBAAqB;IACrB,oBAAoB;IACpB,uBAAuB;IACvB,uBAAuB;IACvB,eAAe;IACf,mBAAmB;IACnB,yBAAyB;CAC1B,CAAC;AAEF,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,EACvB,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EAC1B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { LoadedFile } from "../types.js";
2
+ export declare function loadFiles(targetPath: string): Promise<LoadedFile[]>;
3
+ export declare function lineNumberForIndex(content: string, index: number): number;
@@ -0,0 +1,48 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import { toPosixPath } from "../utils/paths.js";
5
+ const ignoredDirectories = [
6
+ "**/node_modules/**",
7
+ "**/dist/**",
8
+ "**/.git/**",
9
+ "**/coverage/**",
10
+ "**/.turbo/**"
11
+ ];
12
+ export async function loadFiles(targetPath) {
13
+ const stat = await fs.stat(targetPath);
14
+ const root = stat.isDirectory() ? targetPath : path.dirname(targetPath);
15
+ const entries = stat.isDirectory()
16
+ ? await fg("**/*", {
17
+ cwd: root,
18
+ dot: true,
19
+ onlyFiles: true,
20
+ ignore: ignoredDirectories,
21
+ absolute: true
22
+ })
23
+ : [targetPath];
24
+ const files = [];
25
+ for (const filePath of entries.sort()) {
26
+ const content = await readTextFile(filePath);
27
+ if (content === undefined) {
28
+ continue;
29
+ }
30
+ files.push({
31
+ filePath,
32
+ relativePath: toPosixPath(path.relative(root, filePath)),
33
+ content
34
+ });
35
+ }
36
+ return files;
37
+ }
38
+ async function readTextFile(filePath) {
39
+ const buffer = await fs.readFile(filePath);
40
+ if (buffer.includes(0)) {
41
+ return undefined;
42
+ }
43
+ return buffer.toString("utf8");
44
+ }
45
+ export function lineNumberForIndex(content, index) {
46
+ return content.slice(0, Math.max(index, 0)).split(/\r?\n/).length;
47
+ }
48
+ //# sourceMappingURL=fileLoader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileLoader.js","sourceRoot":"","sources":["../../src/scanner/fileLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,WAAW,CAAC;AAE3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,kBAAkB,GAAG;IACzB,oBAAoB;IACpB,YAAY;IACZ,YAAY;IACZ,gBAAgB;IAChB,cAAc;CACf,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAkB;IAChD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE;QAChC,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE;YACf,GAAG,EAAE,IAAI;YACT,GAAG,EAAE,IAAI;YACT,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,kBAAkB;YAC1B,QAAQ,EAAE,IAAI;SACf,CAAC;QACJ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;IAEjB,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,KAAK,CAAC,IAAI,CAAC;YACT,QAAQ;YACR,YAAY,EAAE,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACxD,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,QAAgB;IAC1C,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,KAAa;IAC/D,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;AACpE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ScanOptions, ScanResult } from "../types.js";
2
+ export declare function scanTarget(options: ScanOptions): Promise<ScanResult>;
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { loadPolicy } from "../policy/loadPolicy.js";
3
+ import { applyPolicy, summarizeFindings } from "../policy/policyEngine.js";
4
+ import { rules } from "../rules/index.js";
5
+ import { loadFiles } from "./fileLoader.js";
6
+ export async function scanTarget(options) {
7
+ const targetPath = path.resolve(options.target);
8
+ const policy = await loadPolicy(options.policyPath);
9
+ const files = await loadFiles(targetPath);
10
+ const context = { targetPath, files, policy };
11
+ const rawFindings = rules.flatMap((rule) => rule.scan(context));
12
+ const findings = applyPolicy(rawFindings, policy);
13
+ const summary = summarizeFindings(findings);
14
+ return {
15
+ targetPath,
16
+ findings,
17
+ summary,
18
+ shouldFail: summary.failCount > 0
19
+ };
20
+ }
21
+ //# sourceMappingURL=scanTarget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanTarget.js","sourceRoot":"","sources":["../../src/scanner/scanTarget.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAoB;IACnD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC9C,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAChE,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAE5C,OAAO;QACL,UAAU;QACV,QAAQ;QACR,OAAO;QACP,UAAU,EAAE,OAAO,CAAC,SAAS,GAAG,CAAC;KAClC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,58 @@
1
+ export type Severity = "INFO" | "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
2
+ export type RuleMode = "off" | "warn" | "error";
3
+ export interface Finding {
4
+ id: string;
5
+ title: string;
6
+ severity: Severity;
7
+ category: string;
8
+ message: string;
9
+ filePath: string;
10
+ lineNumber?: number;
11
+ evidence: string;
12
+ recommendation: string;
13
+ policyLevel?: "warn" | "error";
14
+ failsScan?: boolean;
15
+ }
16
+ export interface LoadedFile {
17
+ filePath: string;
18
+ relativePath: string;
19
+ content: string;
20
+ }
21
+ export interface Policy {
22
+ rules: Record<string, RuleMode>;
23
+ allowlist: {
24
+ servers: string[];
25
+ };
26
+ deny: {
27
+ filesystemRoots: string[];
28
+ commands: string[];
29
+ };
30
+ require: {
31
+ auditLogging: boolean;
32
+ explicitToolConsent: boolean;
33
+ };
34
+ }
35
+ export interface RuleContext {
36
+ targetPath: string;
37
+ files: LoadedFile[];
38
+ policy: Policy;
39
+ }
40
+ export interface Rule {
41
+ id: string;
42
+ scan(context: RuleContext): Finding[];
43
+ }
44
+ export interface ScanOptions {
45
+ target: string;
46
+ policyPath?: string;
47
+ }
48
+ export interface ScanSummary {
49
+ total: number;
50
+ failCount: number;
51
+ bySeverity: Record<Severity, number>;
52
+ }
53
+ export interface ScanResult {
54
+ targetPath: string;
55
+ findings: Finding[];
56
+ summary: ScanSummary;
57
+ shouldFail: boolean;
58
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare function toPosixPath(filePath: string): string;
2
+ export declare function displayPath(filePath: string): string;
@@ -0,0 +1,8 @@
1
+ import path from "node:path";
2
+ export function toPosixPath(filePath) {
3
+ return filePath.split(path.sep).join("/");
4
+ }
5
+ export function displayPath(filePath) {
6
+ return toPosixPath(path.relative(process.cwd(), filePath) || filePath);
7
+ }
8
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,OAAO,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC;AACzE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Severity } from "../types.js";
2
+ export declare const severityRank: Record<Severity, number>;
3
+ export declare function shouldFailByDefault(severity: Severity): boolean;
4
+ export declare function createSeverityCounts(): Record<Severity, number>;
@@ -0,0 +1,20 @@
1
+ export const severityRank = {
2
+ INFO: 0,
3
+ LOW: 1,
4
+ MEDIUM: 2,
5
+ HIGH: 3,
6
+ CRITICAL: 4
7
+ };
8
+ export function shouldFailByDefault(severity) {
9
+ return severityRank[severity] >= severityRank.HIGH;
10
+ }
11
+ export function createSeverityCounts() {
12
+ return {
13
+ INFO: 0,
14
+ LOW: 0,
15
+ MEDIUM: 0,
16
+ HIGH: 0,
17
+ CRITICAL: 0
18
+ };
19
+ }
20
+ //# sourceMappingURL=severity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"severity.js","sourceRoot":"","sources":["../../src/utils/severity.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,IAAI,EAAE,CAAC;IACP,GAAG,EAAE,CAAC;IACN,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;CACZ,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,QAAkB;IACpD,OAAO,YAAY,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,IAAI,EAAE,CAAC;QACP,GAAG,EAAE,CAAC;QACN,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,CAAC;QACP,QAAQ,EAAE,CAAC;KACZ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ # Architecture
2
+
3
+ MCP-Guard is intentionally small in v0.1.
4
+
5
+ The CLI in `src/cli.ts` parses commands with Commander and delegates scans to `scanTarget`. The scanner loads target files, loads policy, runs every rule, applies policy, and returns a normalized `ScanResult`.
6
+
7
+ Rules are independent modules under `src/rules`. Each rule receives loaded files and policy context, then returns plain `Finding` objects. The policy engine is the only layer that decides whether a finding fails the scan.
8
+
9
+ Reporters are pure rendering functions. Console, JSON, and Markdown output are generated from the same scan result, which keeps CLI behavior deterministic and easy to test.
10
+
11
+ The examples under `examples/` are the local test laboratory. They are synthetic and safe; they do not require credentials, external APIs, or live MCP servers.
@@ -0,0 +1,42 @@
1
+ # Release Checklist
2
+
3
+ Use this checklist for publishing MCP-Guard to npm and GitHub.
4
+
5
+ ## npm
6
+
7
+ ```bash
8
+ npm login
9
+ npm whoami
10
+ npm run lint
11
+ npm test
12
+ npm run build
13
+ npm pack
14
+ ```
15
+
16
+ Test the generated tarball locally:
17
+
18
+ ```bash
19
+ npm install -g ./srhot-mcp-guard-0.1.0.tgz
20
+ mcp-guard scan examples/safe-mcp-server
21
+ mcp-guard scan examples/bad-mcp-server --no-fail
22
+ npm uninstall -g @srhot/mcp-guard
23
+ ```
24
+
25
+ Publish:
26
+
27
+ ```bash
28
+ npm publish --access public
29
+ ```
30
+
31
+ ## GitHub
32
+
33
+ ```bash
34
+ git tag v0.1.0
35
+ git push origin v0.1.0
36
+ ```
37
+
38
+ Then create a GitHub release for `v0.1.0` with a short summary, install commands, and a link to the README.
39
+
40
+ ## GitHub Actions History
41
+
42
+ Old failed GitHub Actions runs remain visible in the Actions tab after fixes are pushed. That is normal. Use the latest workflow run for the current release status.