policylayer 0.1.4 → 0.1.6

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 (3) hide show
  1. package/README.md +89 -0
  2. package/dist/index.js +76 -35
  3. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # policylayer
2
+
3
+ Scan your MCP config. See what your AI agent can do. Get a shareable report.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx -y policylayer
9
+ ```
10
+
11
+ That's it. The CLI finds your MCP config, live-scans each server, classifies every tool, and prints a report URL.
12
+
13
+ ## What it does
14
+
15
+ 1. **Finds your config** -- checks `.mcp.json`, `~/.claude.json`, Claude Desktop, Cursor, VS Code, Windsurf, and Codex configs automatically
16
+ 2. **Live-scans servers** -- spawns each MCP server, performs the JSON-RPC handshake, and discovers every tool with its full schema
17
+ 3. **Classifies tools** -- checks against the PolicyLayer database of 2,500+ classified tools. Unknown tools are classified locally using schema analysis, blast radius detection, and verb matching
18
+ 4. **Generates a policy YAML** -- suggested default rules for every tool, ready to use with [Intercept](https://github.com/policylayer/intercept)
19
+ 5. **Prints a report URL** -- permanent, shareable, no login required
20
+ 6. **Contributes back** -- live-scanned tool data is contributed to the database so future scans are faster for everyone
21
+
22
+ ## Commands
23
+
24
+ ```bash
25
+ npx -y policylayer
26
+ ```
27
+
28
+ **Options:**
29
+
30
+ | Flag | Description |
31
+ |------|-------------|
32
+ | `-d, --dir <path>` | Directory to scan for config files (default: cwd) |
33
+ | `-o, --output <path>` | Output path for policy YAML (default: `policylayer.yaml`) |
34
+ | `--no-live` | Skip live scanning, classify from config only |
35
+ | `--no-report` | Skip submitting report to PolicyLayer |
36
+ | `--timeout <ms>` | Timeout per server in milliseconds (default: 30000) |
37
+ | `--json` | Output results as JSON |
38
+
39
+ ### Examples
40
+
41
+ ```bash
42
+ # Auto-detect config and scan
43
+ npx -y policylayer
44
+
45
+ # Scan a specific directory
46
+ npx -y policylayer -d ~/projects/my-app
47
+
48
+ # Skip live scanning (faster, less detailed)
49
+ npx -y policylayer --no-live
50
+
51
+ # Output as JSON for piping
52
+ npx -y policylayer --json
53
+
54
+ # Custom policy output path
55
+ npx -y policylayer -o my-policy.yaml
56
+ ```
57
+
58
+ ## Config detection
59
+
60
+ The CLI searches these paths and uses all configs found:
61
+
62
+ | Client | Path |
63
+ |--------|------|
64
+ | Claude Code (project) | `.mcp.json` |
65
+ | Claude Code (user) | `~/.claude.json` |
66
+ | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` |
67
+ | Cursor | `.cursor/mcp.json` |
68
+ | VS Code | `.vscode/settings.json` |
69
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
70
+ | Codex (project) | `.codex/config.toml` |
71
+ | Codex (user) | `~/.codex/config.toml` |
72
+
73
+ ## Privacy
74
+
75
+ The CLI **never sends** your raw config. Before anything leaves your machine:
76
+
77
+ - Environment variables are stripped
78
+ - Auth tokens are removed
79
+ - Only server names, package identifiers, and tool schemas are sent
80
+ - Live-scanned tool data (names, descriptions, schemas) is contributed to improve the database
81
+
82
+ Use `--no-report` to skip sending anything.
83
+
84
+ ## Links
85
+
86
+ - [Example report](https://policylayer.com/scan/report/65545482-5d1d-472f-9fca-472ff1181d0d)
87
+ - [Scan your config online](https://policylayer.com/scan)
88
+ - [Policy library](https://policylayer.com/policies)
89
+ - [Intercept](https://github.com/policylayer/intercept) -- enforce limits on every MCP tool call
package/dist/index.js CHANGED
@@ -593,7 +593,7 @@ var require_dist = __commonJS({
593
593
 
594
594
  // src/index.ts
595
595
  import { Command } from "commander";
596
- import { join as join2 } from "path";
596
+ import { join as join3 } from "path";
597
597
 
598
598
  // src/discover.ts
599
599
  import { existsSync, readFileSync } from "fs";
@@ -803,62 +803,84 @@ function classifyLocally(serverName, pkg, tools) {
803
803
  }
804
804
 
805
805
  // src/policy.ts
806
- import { writeFileSync } from "fs";
807
- function generatePolicyYaml(servers) {
808
- let yaml = 'version: "1"\ndefault: deny\n\ntools:\n';
809
- for (const server of servers) {
810
- yaml += ` # ${server.name} (${server.tools.length} tools)
811
- `;
812
- for (const tool of server.tools) {
813
- switch (tool.category) {
814
- case "Read":
815
- yaml += ` ${tool.name}:
806
+ import { writeFileSync, mkdirSync } from "fs";
807
+ import { join as join2 } from "path";
808
+ function toolRules(tool) {
809
+ switch (tool.category) {
810
+ case "Read":
811
+ return ` ${tool.name}:
816
812
  rules:
817
813
  - action: allow
818
814
  rate_limit: 60/minute
819
815
 
820
816
  `;
821
- break;
822
- case "Write":
823
- case "Execute":
824
- yaml += ` ${tool.name}:
817
+ case "Write":
818
+ case "Execute":
819
+ return ` ${tool.name}:
825
820
  rules:
826
821
  - action: allow
827
822
  rate_limit: 10/hour
828
823
 
829
824
  `;
830
- break;
831
- case "Financial":
832
- yaml += ` ${tool.name}:
825
+ case "Financial":
826
+ return ` ${tool.name}:
833
827
  rules:
834
828
  - action: deny
835
829
  on_deny: "Financial operation blocked by policy"
836
830
 
837
831
  `;
838
- break;
839
- case "Destructive":
840
- yaml += ` ${tool.name}:
832
+ case "Destructive":
833
+ return ` ${tool.name}:
841
834
  rules:
842
835
  - action: deny
843
836
  on_deny: "Destructive operation blocked by policy"
844
837
 
845
838
  `;
846
- break;
847
- default:
848
- yaml += ` ${tool.name}:
839
+ default:
840
+ return ` ${tool.name}:
849
841
  rules:
850
842
  - action: allow
851
843
  rate_limit: 30/minute
852
844
 
853
845
  `;
854
- }
846
+ }
847
+ }
848
+ function generatePolicyYaml(servers) {
849
+ let yaml = 'version: "1"\ndefault: deny\n\ntools:\n';
850
+ for (const server of servers) {
851
+ yaml += ` # ${server.name} (${server.tools.length} tools)
852
+ `;
853
+ for (const tool of server.tools) {
854
+ yaml += toolRules(tool);
855
855
  }
856
856
  }
857
857
  return yaml.trim();
858
858
  }
859
- function writePolicyFile(path, servers) {
860
- const yaml = generatePolicyYaml(servers);
861
- writeFileSync(path, yaml + "\n");
859
+ function serverSlug(server) {
860
+ return server.package.replace(/^@/, "").replace(/\//g, "-").replace(/[^a-z0-9-]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
861
+ }
862
+ function generateServerYaml(server) {
863
+ let yaml = `version: "1"
864
+ default: deny
865
+
866
+ # ${server.name} (${server.package})
867
+ tools:
868
+ `;
869
+ for (const tool of server.tools) {
870
+ yaml += toolRules(tool);
871
+ }
872
+ return yaml.trim();
873
+ }
874
+ function writePolicyFiles(dir, servers) {
875
+ mkdirSync(dir, { recursive: true });
876
+ const written = [];
877
+ for (const server of servers) {
878
+ const filename = `${serverSlug(server)}.yaml`;
879
+ const filepath = join2(dir, filename);
880
+ writeFileSync(filepath, generateServerYaml(server) + "\n");
881
+ written.push(filepath);
882
+ }
883
+ return written;
862
884
  }
863
885
 
864
886
  // src/report.ts
@@ -906,14 +928,19 @@ async function submitReport(servers, policy) {
906
928
  }),
907
929
  signal: AbortSignal.timeout(1e4)
908
930
  });
909
- if (!res.ok) return null;
931
+ if (!res.ok) {
932
+ const text = await res.text().catch(() => "");
933
+ console.error(` Report submission failed: ${res.status} ${text.slice(0, 200)}`);
934
+ return null;
935
+ }
910
936
  const liveServers = servers.filter((s) => s.source === "local" && s.tools.length > 0);
911
937
  if (liveServers.length > 0) {
912
938
  contributeTools(liveServers).catch(() => {
913
939
  });
914
940
  }
915
941
  return await res.json();
916
- } catch {
942
+ } catch (err) {
943
+ console.error(` Report submission error: ${err.message}`);
917
944
  return null;
918
945
  }
919
946
  }
@@ -937,7 +964,20 @@ async function contributeTools(servers) {
937
964
 
938
965
  // src/index.ts
939
966
  var program = new Command();
940
- program.name("policylayer").description("Scan your MCP servers for security risks").version("0.1.0").option("-d, --dir <path>", "directory to scan for config files", process.cwd()).option("-o, --output <path>", "output path for policy YAML", "policylayer.yaml").option("--no-live", "skip live scanning, classify from config only").option("--no-report", "skip submitting report to PolicyLayer").option("--timeout <ms>", "timeout per server in milliseconds", "30000").option("--json", "output results as JSON").action(run);
967
+ var scanOpts = [
968
+ ["-d, --dir <path>", "directory to scan for config files", process.cwd()],
969
+ ["-o, --output <path>", "output directory for per-server policy files", "policies"],
970
+ ["--no-live", "skip live scanning, classify from config only"],
971
+ ["--no-report", "skip submitting report to PolicyLayer"],
972
+ ["--timeout <ms>", "timeout per server in milliseconds", "30000"],
973
+ ["--json", "output results as JSON"]
974
+ ];
975
+ program.name("policylayer").description("Scan your MCP servers for security risks").version("0.1.5");
976
+ for (const opt of scanOpts) program.option(...opt);
977
+ program.action(run);
978
+ var scanCmd = program.command("scan").description("Scan your MCP servers for security risks");
979
+ for (const opt of scanOpts) scanCmd.option(...opt);
980
+ scanCmd.action(run);
941
981
  async function run(opts) {
942
982
  const cwd = opts.dir;
943
983
  const timeoutMs = parseInt(opts.timeout, 10);
@@ -1043,8 +1083,8 @@ async function run(opts) {
1043
1083
  process.exit(0);
1044
1084
  }
1045
1085
  const policyYaml = generatePolicyYaml(classified);
1046
- const outputPath = join2(cwd, opts.output);
1047
- writePolicyFile(outputPath, classified);
1086
+ const outputDir = join3(cwd, opts.output);
1087
+ const perServerFiles = writePolicyFiles(outputDir, classified);
1048
1088
  let reportResult = null;
1049
1089
  if (opts.report) {
1050
1090
  reportResult = await submitReport(classified, policyYaml);
@@ -1061,7 +1101,8 @@ async function run(opts) {
1061
1101
  servers: classified.length,
1062
1102
  tools: totalTools,
1063
1103
  categories,
1064
- policyFile: outputPath,
1104
+ policyDir: outputDir,
1105
+ policyFiles: perServerFiles,
1065
1106
  reportUrl: reportResult?.url || null,
1066
1107
  reportId: reportResult?.id || null,
1067
1108
  results: classified
@@ -1073,7 +1114,7 @@ async function run(opts) {
1073
1114
  console.log(` Tools: ${totalTools}`);
1074
1115
  console.log(` Categories: ${Object.entries(categories).map(([k, v]) => `${k}(${v})`).join(" ")}`);
1075
1116
  console.log();
1076
- console.log(` Policy: ${outputPath}`);
1117
+ console.log(` Policies: ${perServerFiles.length} files in ${opts.output}/`);
1077
1118
  if (reportResult) {
1078
1119
  console.log(` Report: ${reportResult.url}`);
1079
1120
  }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "policylayer",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Scan your MCP servers for security risks — live tool discovery + classification + shareable report",
6
6
  "bin": {
7
7
  "policylayer": "./dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "README.md"
11
12
  ],
12
13
  "engines": {
13
14
  "node": ">=20"