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.
- package/README.md +89 -0
- package/dist/index.js +76 -35
- 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
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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
|
-
|
|
839
|
-
|
|
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
|
-
|
|
847
|
-
|
|
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
|
|
860
|
-
|
|
861
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
1047
|
-
|
|
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
|
-
|
|
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(`
|
|
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.
|
|
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"
|