policylayer 0.1.2 → 0.1.4

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/dist/index.js +1124 -5
  2. package/package.json +6 -7
  3. package/README.md +0 -81
package/dist/index.js CHANGED
@@ -1,6 +1,1125 @@
1
1
  #!/usr/bin/env node
2
- import{Command as D}from"commander";import{readFileSync as $}from"fs";import{existsSync as v,readFileSync as j}from"fs";import{join as p}from"path";import{homedir as a}from"os";import{parse as h}from"smol-toml";function x(r){return[{path:p(r,".mcp.json"),source:".mcp.json (project)",type:"json-mcp"},{path:p(r,".cursor/mcp.json"),source:".cursor/mcp.json (project)",type:"json-mcp"},{path:p(r,".vscode/settings.json"),source:"VS Code (project)",type:"json-vscode"},{path:p(r,".codex/config.toml"),source:".codex/config.toml (project)",type:"toml-codex"},{path:p(a(),".claude.json"),source:"~/.claude.json (user)",type:"json-mcp"},{path:p(a(),"Library/Application Support/Claude/claude_desktop_config.json"),source:"Claude Desktop (user)",type:"json-mcp"},{path:p(a(),".codeium/windsurf/mcp_config.json"),source:"Windsurf (user)",type:"json-mcp"},{path:p(a(),".codex/config.toml"),source:"~/.codex/config.toml (user)",type:"toml-codex"}]}function R(r){return JSON.parse(r).mcpServers||{}}function w(r){let e=JSON.parse(r);return e.mcp?e.mcp.servers||{}:null}function C(r){return h(r).mcp_servers||{}}function u(r){let e=[],o=x(r);for(let{path:n,source:t,type:s}of o)if(v(n))try{let i=j(n,"utf-8"),c=null;switch(s){case"json-mcp":c=R(i);break;case"json-vscode":c=w(i);break;case"toml-codex":c=C(i);break}if(c===null)continue;e.push({source:t,path:n,servers:Object.keys(c),raw:{mcpServers:c}})}catch{}return e}var k=[/^sk-/,/^ghp_/,/^gho_/,/^ghu_/,/^ghs_/,/^glpat-/,/^xoxb-/,/^xoxp-/,/^Bearer\s/,/^pk_/,/^sk_live_/,/^sk_test_/,/^ict_/,/^[a-z]{2,6}_[a-f0-9]{16,}$/i,/^[a-f0-9]{32,}$/i],E=[/\/Users\//,/\/home\//,/\\Users\\/,/^[A-Z]:\\/,/^\//];function b(r){return k.some(e=>e.test(r))}function M(r){return E.some(e=>e.test(r))}function _(r){return!!(r.startsWith("@")&&r.includes("/")||/^[a-z][\w.-]*$/.test(r))}function g(r){let e={};for(let[o,n]of Object.entries(r)){let t={};if(n.command){let s=n.command.split(/[/\\]/);t.command=s[s.length-1]||n.command}Array.isArray(n.args)&&(t.args=n.args.filter(s=>typeof s!="string"||b(s)||M(s)||s.startsWith("-")?!1:_(s))),e[o]=t}return e}async function l(r,e){let o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({mcpServers:r})}),n=await o.json();if(!o.ok)throw new Error(n.error||`Scan failed with status ${o.status}`);return n}function m(r){let e=[];for(let o of r)e.push(` Found ${o.servers.length} server${o.servers.length===1?"":"s"} in ${o.source}`);return e.join(`
3
- `)}function d(r){return["No MCP configuration found.","","Checked:",...r.map(o=>` - ${o}`),"","Use --config <path> to specify a config file,","or try the web scanner at https://policylayer.com/scan"].join(`
4
- `)}function y(r){return[""," Report ready:",` ${r}`,""].join(`
5
- `)}function S(r){return["","Dry run -- would send:",JSON.stringify(r,null,2)].join(`
6
- `)}var f=new D;f.name("policylayer").description("Scan your MCP config for security risks").version("0.1.0");f.command("scan").description("Scan MCP server configuration").option("--config <path>","Path to MCP config file").option("--dry-run","Show what would be sent without sending").option("--api-url <url>","Scan API URL","https://policylayer.com/api/scan").action(async r=>{try{let e={};if(r.config)try{let t=JSON.parse($(r.config,"utf-8")),s=t.mcpServers||t;e=s;let i=Object.keys(s).length;console.log(` Found ${i} server${i===1?"":"s"} in ${r.config}`)}catch(t){console.error(`Error reading config: ${r.config}`),console.error(t.message),process.exit(1)}else{let t=u(process.cwd());t.length===0&&(console.log(d([".mcp.json (project root)","~/.claude.json (user-level)","Claude Desktop config (macOS)",".cursor/mcp.json (project)",".vscode/settings.json (project)","Windsurf config (user)",".codex/config.toml (project)","~/.codex/config.toml (user)"])),process.exit(0)),console.log(m(t));let s=t.filter(c=>c.source.includes("user")),i=t.filter(c=>!c.source.includes("user"));for(let c of s)Object.assign(e,c.raw.mcpServers);for(let c of i)Object.assign(e,c.raw.mcpServers)}let o=g(e);if(r.dryRun){console.log(S({mcpServers:o}));return}console.log(" Sending server identifiers only -- secrets stripped");let n=await l(o,r.apiUrl);console.log(y(n.url))}catch(e){console.error(`Error: ${e.message}`),process.exit(1)}});f.parse();
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+
28
+ // ../classifier/dist/verbs.js
29
+ var require_verbs = __commonJS({
30
+ "../classifier/dist/verbs.js"(exports) {
31
+ "use strict";
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.VERB_MAP = void 0;
34
+ exports.splitName = splitName;
35
+ exports.findFirstVerb = findFirstVerb;
36
+ exports.firstVerbIsRead = firstVerbIsRead;
37
+ exports.VERB_MAP = {};
38
+ var READ_VERBS = [
39
+ "get",
40
+ "list",
41
+ "search",
42
+ "read",
43
+ "fetch",
44
+ "query",
45
+ "browse",
46
+ "view",
47
+ "show",
48
+ "describe",
49
+ "find",
50
+ "check",
51
+ "lookup",
52
+ "retrieve",
53
+ "locate",
54
+ "inspect",
55
+ "ask",
56
+ "count",
57
+ "poll",
58
+ "download",
59
+ "extract",
60
+ "screenshot",
61
+ "introspect",
62
+ "detect",
63
+ "analyze",
64
+ "analyse",
65
+ "scan",
66
+ "validate",
67
+ "verify",
68
+ "monitor",
69
+ "watch",
70
+ "observe",
71
+ "consume",
72
+ "load",
73
+ "data",
74
+ "docs",
75
+ "enrich",
76
+ "translate",
77
+ "cognify",
78
+ "take",
79
+ "test",
80
+ "snapshot",
81
+ "geocode",
82
+ "status",
83
+ "support",
84
+ "matrix",
85
+ "expert",
86
+ "is"
87
+ ];
88
+ var DESTRUCTIVE_VERBS = [
89
+ "delete",
90
+ "remove",
91
+ "destroy",
92
+ "drop",
93
+ "truncate",
94
+ "purge",
95
+ "wipe",
96
+ "erase",
97
+ "revoke",
98
+ "cancel",
99
+ "uninstall",
100
+ "reset",
101
+ "clear",
102
+ "force",
103
+ "decompile",
104
+ "unpublish",
105
+ "discard"
106
+ ];
107
+ var EXECUTE_VERBS = [
108
+ "run",
109
+ "execute",
110
+ "invoke",
111
+ "trigger",
112
+ "start",
113
+ "deploy",
114
+ "build",
115
+ "restart",
116
+ "scale",
117
+ "compile",
118
+ "launch",
119
+ "emulate",
120
+ "initialize",
121
+ "initialise",
122
+ "navigate",
123
+ "parse",
124
+ "rephrase",
125
+ "transform",
126
+ "stop",
127
+ "performance",
128
+ "wait",
129
+ "new",
130
+ "produce"
131
+ ];
132
+ var WRITE_VERBS = [
133
+ "create",
134
+ "add",
135
+ "update",
136
+ "set",
137
+ "modify",
138
+ "put",
139
+ "patch",
140
+ "edit",
141
+ "configure",
142
+ "enable",
143
+ "disable",
144
+ "assign",
145
+ "upload",
146
+ "write",
147
+ "send",
148
+ "post",
149
+ "publish",
150
+ "manage",
151
+ "save",
152
+ "install",
153
+ "boot",
154
+ "setup",
155
+ "select",
156
+ "click",
157
+ "tap",
158
+ "press",
159
+ "scroll",
160
+ "swipe",
161
+ "drag",
162
+ "activate",
163
+ "handle",
164
+ "switch",
165
+ "generate",
166
+ "move",
167
+ "copy",
168
+ "import",
169
+ "export",
170
+ "connect",
171
+ "disconnect",
172
+ "close",
173
+ "open",
174
+ "attach",
175
+ "insert",
176
+ "apply",
177
+ "convert",
178
+ "merge",
179
+ "register",
180
+ "submit",
181
+ "approve",
182
+ "reject",
183
+ "mark",
184
+ "comment",
185
+ "annotate",
186
+ "pin",
187
+ "lock",
188
+ "unlock",
189
+ "archive",
190
+ "restore",
191
+ "suspend",
192
+ "resume",
193
+ "terminate",
194
+ "complete",
195
+ "uncomplete",
196
+ "rename",
197
+ "fill",
198
+ "invite",
199
+ "alter",
200
+ "vote",
201
+ "plan",
202
+ "planner",
203
+ "replace",
204
+ "upsert",
205
+ "type",
206
+ "resize",
207
+ "push",
208
+ "issue",
209
+ "autofix",
210
+ "login",
211
+ "checkout",
212
+ "commit",
213
+ "promote",
214
+ "resolve",
215
+ "unarchive",
216
+ "schedule",
217
+ "migrate"
218
+ ];
219
+ for (const v of READ_VERBS)
220
+ exports.VERB_MAP[v] = "Read";
221
+ for (const v of DESTRUCTIVE_VERBS)
222
+ exports.VERB_MAP[v] = "Destructive";
223
+ for (const v of EXECUTE_VERBS)
224
+ exports.VERB_MAP[v] = "Execute";
225
+ for (const v of WRITE_VERBS)
226
+ exports.VERB_MAP[v] = "Write";
227
+ function splitName(name) {
228
+ const parts = name.split(/[-_]/);
229
+ const words = [];
230
+ for (const part of parts) {
231
+ const camelParts = part.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().split("_");
232
+ words.push(...camelParts.filter(Boolean));
233
+ }
234
+ return words;
235
+ }
236
+ function findFirstVerb(words) {
237
+ for (const word of words) {
238
+ const cat = exports.VERB_MAP[word];
239
+ if (cat)
240
+ return cat;
241
+ }
242
+ return null;
243
+ }
244
+ function firstVerbIsRead(words) {
245
+ for (const word of words) {
246
+ if (exports.VERB_MAP[word])
247
+ return exports.VERB_MAP[word] === "Read";
248
+ }
249
+ return false;
250
+ }
251
+ }
252
+ });
253
+
254
+ // ../classifier/dist/schema.js
255
+ var require_schema = __commonJS({
256
+ "../classifier/dist/schema.js"(exports) {
257
+ "use strict";
258
+ Object.defineProperty(exports, "__esModule", { value: true });
259
+ exports.analyseSchema = analyseSchema;
260
+ var CODE_EXECUTION_PROPS = /* @__PURE__ */ new Set([
261
+ "sql",
262
+ "query",
263
+ "raw_sql",
264
+ "raw_query",
265
+ "statement",
266
+ "code",
267
+ "script",
268
+ "command",
269
+ "shell",
270
+ "shell_command",
271
+ "cmd",
272
+ "bash",
273
+ "exec",
274
+ "expression",
275
+ "eval",
276
+ "python",
277
+ "javascript",
278
+ "js",
279
+ "lua",
280
+ "ruby"
281
+ ]);
282
+ var FILESYSTEM_PROPS = /* @__PURE__ */ new Set([
283
+ "path",
284
+ "file",
285
+ "file_path",
286
+ "filepath",
287
+ "filename",
288
+ "directory",
289
+ "dir",
290
+ "folder",
291
+ "source_path",
292
+ "dest_path",
293
+ "destination",
294
+ "target_path",
295
+ "working_directory",
296
+ "cwd"
297
+ ]);
298
+ var NETWORK_PROPS = /* @__PURE__ */ new Set([
299
+ "url",
300
+ "uri",
301
+ "endpoint",
302
+ "host",
303
+ "hostname",
304
+ "webhook",
305
+ "webhook_url",
306
+ "callback_url",
307
+ "redirect_url",
308
+ "api_url",
309
+ "base_url"
310
+ ]);
311
+ var CREDENTIAL_PROPS = /* @__PURE__ */ new Set([
312
+ "password",
313
+ "secret",
314
+ "token",
315
+ "api_key",
316
+ "apikey",
317
+ "private_key",
318
+ "access_token",
319
+ "auth",
320
+ "credentials",
321
+ "connection_string"
322
+ ]);
323
+ var INJECTION_PROPS = /* @__PURE__ */ new Set([
324
+ "html",
325
+ "body",
326
+ "template",
327
+ "content",
328
+ "raw_body",
329
+ "raw_html",
330
+ "markup",
331
+ "payload"
332
+ ]);
333
+ function extractProperties(schema, prefix = "") {
334
+ const props = [];
335
+ const properties = schema.properties;
336
+ if (!properties || typeof properties !== "object")
337
+ return props;
338
+ for (const [key, value] of Object.entries(properties)) {
339
+ if (!value || typeof value !== "object")
340
+ continue;
341
+ const fullName = prefix ? `${prefix}.${key}` : key;
342
+ props.push({
343
+ name: fullName,
344
+ type: value.type,
345
+ description: value.description
346
+ });
347
+ if (value.type === "object" && value.properties) {
348
+ props.push(...extractProperties(value, fullName));
349
+ }
350
+ if (value.type === "array" && value.items && typeof value.items === "object") {
351
+ const items = value.items;
352
+ if (items.type === "object" && items.properties) {
353
+ props.push(...extractProperties(items, `${fullName}[]`));
354
+ }
355
+ }
356
+ }
357
+ return props;
358
+ }
359
+ function normalisePropName(name) {
360
+ const leaf = name.includes(".") ? name.split(".").pop() : name;
361
+ return leaf.toLowerCase().replace(/[-_\s]/g, "_");
362
+ }
363
+ function analyseSchema(schema) {
364
+ const signals = [];
365
+ const properties = extractProperties(schema);
366
+ if (properties.length === 0)
367
+ return signals;
368
+ const seen = /* @__PURE__ */ new Set();
369
+ for (const prop of properties) {
370
+ const norm = normalisePropName(prop.name);
371
+ if (CODE_EXECUTION_PROPS.has(norm) && !seen.has("code_exec")) {
372
+ seen.add("code_exec");
373
+ signals.push({
374
+ signal: `Accepts freeform code/query input (${prop.name})`,
375
+ weight: 0.15
376
+ });
377
+ }
378
+ if (FILESYSTEM_PROPS.has(norm) && !seen.has("filesystem")) {
379
+ seen.add("filesystem");
380
+ signals.push({
381
+ signal: `Accepts file system path (${prop.name})`,
382
+ weight: 0.1
383
+ });
384
+ }
385
+ if (NETWORK_PROPS.has(norm) && !seen.has("network")) {
386
+ seen.add("network");
387
+ signals.push({
388
+ signal: `Accepts URL/endpoint input (${prop.name})`,
389
+ weight: 0.08
390
+ });
391
+ }
392
+ if (CREDENTIAL_PROPS.has(norm) && !seen.has("credential")) {
393
+ seen.add("credential");
394
+ signals.push({
395
+ signal: `Handles credentials or secrets (${prop.name})`,
396
+ weight: 0.12
397
+ });
398
+ }
399
+ if (INJECTION_PROPS.has(norm) && !seen.has("injection")) {
400
+ seen.add("injection");
401
+ signals.push({
402
+ signal: `Accepts raw HTML/template content (${prop.name})`,
403
+ weight: 0.05
404
+ });
405
+ }
406
+ }
407
+ if (properties.length >= 10) {
408
+ signals.push({
409
+ signal: `High parameter count (${properties.length} properties)`,
410
+ weight: 0.05
411
+ });
412
+ }
413
+ return signals;
414
+ }
415
+ }
416
+ });
417
+
418
+ // ../classifier/dist/blast-radius.js
419
+ var require_blast_radius = __commonJS({
420
+ "../classifier/dist/blast-radius.js"(exports) {
421
+ "use strict";
422
+ Object.defineProperty(exports, "__esModule", { value: true });
423
+ exports.analyseBlastRadius = analyseBlastRadius;
424
+ var BULK_NAME_PATTERNS = [
425
+ /\ball[_-]?/i,
426
+ /\bbulk[_-]?/i,
427
+ /\bbatch[_-]?/i,
428
+ /\bmass[_-]?/i,
429
+ /\bglobal[_-]?/i,
430
+ /[_-]all$/i
431
+ ];
432
+ var SINGLE_NAME_PATTERNS = [
433
+ /by[_-]?id$/i,
434
+ /by[_-]?name$/i,
435
+ /by[_-]?slug$/i,
436
+ /by[_-]?key$/i,
437
+ /by[_-]?email$/i,
438
+ /[_-]one$/i,
439
+ /[_-]single$/i
440
+ ];
441
+ var BULK_DESC = /\b(all\s+(?:records?|items?|users?|entries|data|files?|messages?|documents?)|every\s+|entire\s+|everything|in\s+bulk|batch\s+(?:delete|remove|update)|mass\s+(?:delete|remove|update)|wipe\s+(?:all|everything)|clear\s+(?:all|everything))\b/i;
442
+ var SINGLE_DESC = /\b(a\s+(?:single|specific|particular|given)|(?:one|individual)\s+(?:record|item|user|entry|file|message|document)|by\s+(?:its?\s+)?(?:ID|identifier|key|name))\b/i;
443
+ var ADMIN_NAME = /^(?:admin|root|system|sudo|superuser|master)[_-]/i;
444
+ var ADMIN_DESC = /\b(admin(?:istrat(?:or|ive))?|root|system[_-]?level|superuser|privileged|elevated\s+permissions?)\b/i;
445
+ function analyseBlastRadius(name, description) {
446
+ const signals = [];
447
+ const isBulk = BULK_NAME_PATTERNS.some((p) => p.test(name)) || BULK_DESC.test(description);
448
+ const isSingle = SINGLE_NAME_PATTERNS.some((p) => p.test(name)) || SINGLE_DESC.test(description);
449
+ if (isBulk && !isSingle) {
450
+ signals.push({
451
+ signal: "Bulk/mass operation \u2014 affects multiple targets",
452
+ weight: 0.15
453
+ });
454
+ } else if (isSingle && !isBulk) {
455
+ signals.push({
456
+ signal: "Single-target operation",
457
+ weight: -0.05
458
+ });
459
+ }
460
+ if (ADMIN_NAME.test(name) || ADMIN_DESC.test(description)) {
461
+ signals.push({
462
+ signal: "Admin/system-level operation",
463
+ weight: 0.1
464
+ });
465
+ }
466
+ return signals;
467
+ }
468
+ }
469
+ });
470
+
471
+ // ../classifier/dist/classify.js
472
+ var require_classify = __commonJS({
473
+ "../classifier/dist/classify.js"(exports) {
474
+ "use strict";
475
+ Object.defineProperty(exports, "__esModule", { value: true });
476
+ exports.classifyTool = classifyTool2;
477
+ var verbs_1 = require_verbs();
478
+ var schema_1 = require_schema();
479
+ var blast_radius_1 = require_blast_radius();
480
+ var FINANCIAL_COMPOUND = /(?:^|[_-])(?:create[_-]?payment|create[_-]?charge|create[_-]?refund|create[_-]?payment[_-]?link|process[_-]?payment|finalize[_-]?invoice|finalise[_-]?invoice|send[_-]?invoice)(?:[_A-Z-]|$)/i;
481
+ var FINANCIAL_VERBS_SEGMENT = /^(pay|charge|refund|transfer|withdraw|deposit|finalize|finalise)$/i;
482
+ var DESTRUCTIVE_DESC = /\b(permanently|irreversible|cannot be undone|destroy|clear,?\s*(copy,?\s*)?delete|wipe|purge)\b/i;
483
+ var FINANCIAL_DESC = /\b(process\s+payment|initiate\s+transfer|send\s+money|charge\s+customer|collect\s+payment)\b/i;
484
+ var READ_DESC = /\b(lists?|retrieves?|gets?|fetche?s?|search(?:es)?|shows?|views?|browses?|quer(?:y|ies)|reads?)\b/i;
485
+ var WRITE_DESC = /\b(create|add|update|modify|edit|configure|set|save|write|send|upload|install|manage|assign)\b/i;
486
+ var EXECUTE_DESC = /\b(run|execute|invoke|trigger|start|deploy|build|restart|scale)\b/i;
487
+ var DESTRUCTIVE_DESC_FALLBACK = /\b(delete|remove|destroy|drop|purge|wipe|erase|revoke|cancel|uninstall|reset|clear)\b/i;
488
+ var SEVERITY_MAP = {
489
+ Read: "Low",
490
+ Write: "Medium",
491
+ Execute: "Medium",
492
+ Destructive: "High",
493
+ Financial: "Critical",
494
+ Other: "Low"
495
+ };
496
+ var BASE_WEIGHT = {
497
+ Read: 0.1,
498
+ Other: 0.15,
499
+ Write: 0.3,
500
+ Execute: 0.5,
501
+ Destructive: 0.7,
502
+ Financial: 0.85
503
+ };
504
+ function classifyTool2(input) {
505
+ const { name, description, inputSchema, serverContext } = input;
506
+ const signals = [];
507
+ const { category, confidence } = categorise(name, description, serverContext);
508
+ const schemaSignals = inputSchema ? (0, schema_1.analyseSchema)(inputSchema) : [];
509
+ signals.push(...schemaSignals);
510
+ const blastSignals = (0, blast_radius_1.analyseBlastRadius)(name, description);
511
+ signals.push(...blastSignals);
512
+ let riskWeight = BASE_WEIGHT[category];
513
+ const schemaModifier = Math.min(0.3, schemaSignals.reduce((sum, s) => sum + s.weight, 0));
514
+ if (schemaModifier > 0)
515
+ riskWeight += schemaModifier;
516
+ const blastModifier = blastSignals.reduce((sum, s) => sum + s.weight, 0);
517
+ riskWeight += blastModifier;
518
+ if (confidence === "low") {
519
+ riskWeight *= 0.8;
520
+ }
521
+ riskWeight = Math.max(0, Math.min(1, riskWeight));
522
+ riskWeight = Math.round(riskWeight * 100) / 100;
523
+ const severity = deriveSeverity(category, riskWeight);
524
+ return { category, severity, confidence, riskWeight, signals };
525
+ }
526
+ function categorise(name, description, serverContext) {
527
+ const words = (0, verbs_1.splitName)(name);
528
+ if (FINANCIAL_COMPOUND.test(name)) {
529
+ return { category: "Financial", confidence: "high" };
530
+ }
531
+ for (const word of words) {
532
+ if (FINANCIAL_VERBS_SEGMENT.test(word)) {
533
+ if (!(0, verbs_1.firstVerbIsRead)(words)) {
534
+ return { category: "Financial", confidence: "high" };
535
+ }
536
+ break;
537
+ }
538
+ }
539
+ if (DESTRUCTIVE_DESC.test(description)) {
540
+ return { category: "Destructive", confidence: "medium" };
541
+ }
542
+ if (FINANCIAL_DESC.test(description)) {
543
+ return { category: "Financial", confidence: "medium" };
544
+ }
545
+ const verbCat = (0, verbs_1.findFirstVerb)(words);
546
+ if (verbCat) {
547
+ return { category: verbCat, confidence: "high" };
548
+ }
549
+ if (DESTRUCTIVE_DESC_FALLBACK.test(description)) {
550
+ return { category: "Destructive", confidence: "medium" };
551
+ }
552
+ if (EXECUTE_DESC.test(description)) {
553
+ return { category: "Execute", confidence: "medium" };
554
+ }
555
+ if (WRITE_DESC.test(description)) {
556
+ return { category: "Write", confidence: "medium" };
557
+ }
558
+ if (READ_DESC.test(description)) {
559
+ return { category: "Read", confidence: "medium" };
560
+ }
561
+ if (serverContext && serverContext.toolCount >= 3) {
562
+ const safe = serverContext.dominantCategory !== "Financial" && serverContext.dominantCategory !== "Destructive";
563
+ if (safe) {
564
+ return { category: serverContext.dominantCategory, confidence: "low" };
565
+ }
566
+ }
567
+ return { category: "Other", confidence: "low" };
568
+ }
569
+ function deriveSeverity(category, riskWeight) {
570
+ if (riskWeight >= 0.85)
571
+ return "Critical";
572
+ if (riskWeight >= 0.6)
573
+ return "High";
574
+ if (riskWeight >= 0.35)
575
+ return "Medium";
576
+ return SEVERITY_MAP[category];
577
+ }
578
+ }
579
+ });
580
+
581
+ // ../classifier/dist/index.js
582
+ var require_dist = __commonJS({
583
+ "../classifier/dist/index.js"(exports) {
584
+ "use strict";
585
+ Object.defineProperty(exports, "__esModule", { value: true });
586
+ exports.classifyTool = void 0;
587
+ var classify_1 = require_classify();
588
+ Object.defineProperty(exports, "classifyTool", { enumerable: true, get: function() {
589
+ return classify_1.classifyTool;
590
+ } });
591
+ }
592
+ });
593
+
594
+ // src/index.ts
595
+ import { Command } from "commander";
596
+ import { join as join2 } from "path";
597
+
598
+ // src/discover.ts
599
+ import { existsSync, readFileSync } from "fs";
600
+ import { join } from "path";
601
+ import { homedir } from "os";
602
+ import { parse as parseToml } from "smol-toml";
603
+ function buildDescriptors(cwd) {
604
+ return [
605
+ { path: join(cwd, ".mcp.json"), source: ".mcp.json (project)", type: "json-mcp" },
606
+ { path: join(cwd, ".cursor/mcp.json"), source: ".cursor/mcp.json (project)", type: "json-mcp" },
607
+ { path: join(cwd, ".vscode/settings.json"), source: "VS Code (project)", type: "json-vscode" },
608
+ { path: join(cwd, ".codex/config.toml"), source: ".codex/config.toml (project)", type: "toml-codex" },
609
+ { path: join(homedir(), ".claude.json"), source: "~/.claude.json (user)", type: "json-mcp" },
610
+ { path: join(homedir(), "Library/Application Support/Claude/claude_desktop_config.json"), source: "Claude Desktop (user)", type: "json-mcp" },
611
+ { path: join(homedir(), ".codeium/windsurf/mcp_config.json"), source: "Windsurf (user)", type: "json-mcp" },
612
+ { path: join(homedir(), ".codex/config.toml"), source: "~/.codex/config.toml (user)", type: "toml-codex" }
613
+ ];
614
+ }
615
+ function discoverConfigs(cwd) {
616
+ const results = [];
617
+ for (const { path, source, type } of buildDescriptors(cwd)) {
618
+ if (!existsSync(path)) continue;
619
+ try {
620
+ const content = readFileSync(path, "utf-8");
621
+ let servers = null;
622
+ if (type === "json-mcp") {
623
+ const raw = JSON.parse(content);
624
+ servers = raw.mcpServers || {};
625
+ } else if (type === "json-vscode") {
626
+ const raw = JSON.parse(content);
627
+ if (!raw.mcp) continue;
628
+ servers = raw.mcp.servers || {};
629
+ } else if (type === "toml-codex") {
630
+ const raw = parseToml(content);
631
+ servers = raw.mcp_servers || {};
632
+ }
633
+ if (!servers || Object.keys(servers).length === 0) continue;
634
+ results.push({ source, path, servers });
635
+ } catch {
636
+ }
637
+ }
638
+ return results;
639
+ }
640
+ function mergeConfigs(results) {
641
+ const merged = {};
642
+ const userConfigs = results.filter((r) => r.source.includes("user"));
643
+ const projectConfigs = results.filter((r) => !r.source.includes("user"));
644
+ for (const c of userConfigs) Object.assign(merged, c.servers);
645
+ for (const c of projectConfigs) Object.assign(merged, c.servers);
646
+ return merged;
647
+ }
648
+
649
+ // src/live-scan.ts
650
+ import { spawn } from "child_process";
651
+ var DEFAULT_TIMEOUT_MS = 3e4;
652
+ async function liveScan(command, args, timeoutMs = DEFAULT_TIMEOUT_MS, env) {
653
+ return new Promise((resolve, reject) => {
654
+ const child = spawn(command, args, {
655
+ stdio: ["pipe", "pipe", "pipe"],
656
+ env: {
657
+ ...process.env,
658
+ ...env,
659
+ NODE_NO_WARNINGS: "1",
660
+ BROWSER: "none",
661
+ NO_BROWSER: "1"
662
+ }
663
+ });
664
+ let nextId = 1;
665
+ const pending = /* @__PURE__ */ new Map();
666
+ let buffer = "";
667
+ let settled = false;
668
+ const timer = setTimeout(() => {
669
+ if (!settled) {
670
+ settled = true;
671
+ cleanup();
672
+ reject(new Error(`Timed out after ${timeoutMs}ms`));
673
+ }
674
+ }, timeoutMs);
675
+ function cleanup() {
676
+ clearTimeout(timer);
677
+ try {
678
+ child.stdin.end();
679
+ } catch {
680
+ }
681
+ try {
682
+ child.kill("SIGKILL");
683
+ } catch {
684
+ }
685
+ }
686
+ function settle(fn) {
687
+ if (!settled) {
688
+ settled = true;
689
+ cleanup();
690
+ fn();
691
+ }
692
+ }
693
+ child.stdout.on("data", (chunk) => {
694
+ buffer += chunk.toString();
695
+ const lines = buffer.split("\n");
696
+ buffer = lines.pop() || "";
697
+ for (const line of lines) {
698
+ const trimmed = line.trim();
699
+ if (!trimmed || !trimmed.startsWith("{")) continue;
700
+ try {
701
+ const resp = JSON.parse(trimmed);
702
+ if (resp.id && pending.has(resp.id)) {
703
+ const p = pending.get(resp.id);
704
+ pending.delete(resp.id);
705
+ p.resolve(resp);
706
+ }
707
+ } catch {
708
+ }
709
+ }
710
+ });
711
+ child.on("error", (err) => {
712
+ settle(() => reject(new Error(`Failed to spawn ${command}: ${err.message}`)));
713
+ });
714
+ child.on("exit", (code) => {
715
+ settle(() => reject(new Error(`${command} exited with code ${code} before discovery completed`)));
716
+ });
717
+ function sendRequest(method, params) {
718
+ return new Promise((res, rej) => {
719
+ const id = nextId++;
720
+ pending.set(id, { resolve: res, reject: rej });
721
+ const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
722
+ child.stdin.write(msg);
723
+ });
724
+ }
725
+ function sendNotification(method) {
726
+ const msg = JSON.stringify({ jsonrpc: "2.0", method }) + "\n";
727
+ child.stdin.write(msg);
728
+ }
729
+ (async () => {
730
+ const initResp = await sendRequest("initialize", {
731
+ protocolVersion: "2024-11-05",
732
+ capabilities: {},
733
+ clientInfo: { name: "policylayer-scan", version: "0.1.0" }
734
+ });
735
+ if (initResp.error) {
736
+ throw new Error(`Initialize failed: ${initResp.error.message}`);
737
+ }
738
+ sendNotification("notifications/initialized");
739
+ const allTools = [];
740
+ let cursor;
741
+ let pages = 0;
742
+ do {
743
+ const params = {};
744
+ if (cursor) params.cursor = cursor;
745
+ const resp = await sendRequest("tools/list", params);
746
+ if (resp.error) {
747
+ throw new Error(`tools/list failed: ${resp.error.message}`);
748
+ }
749
+ const result = resp.result;
750
+ if (result.tools) allTools.push(...result.tools);
751
+ cursor = result.nextCursor || void 0;
752
+ pages++;
753
+ } while (cursor && pages < 100);
754
+ settle(() => resolve(allTools));
755
+ })().catch((err) => {
756
+ settle(() => reject(err));
757
+ });
758
+ });
759
+ }
760
+
761
+ // src/classify.ts
762
+ var import_classifier = __toESM(require_dist(), 1);
763
+ var API_BASE = process.env.POLICYLAYER_API_URL || "https://api.policylayer.com";
764
+ async function classifyViaApi(packages) {
765
+ try {
766
+ const res = await fetch(`${API_BASE}/api/scan/lookup`, {
767
+ method: "POST",
768
+ headers: { "Content-Type": "application/json" },
769
+ body: JSON.stringify({ packages }),
770
+ signal: AbortSignal.timeout(1e4)
771
+ });
772
+ if (!res.ok) return null;
773
+ return await res.json();
774
+ } catch {
775
+ return null;
776
+ }
777
+ }
778
+ function classifyLocally(serverName, pkg, tools) {
779
+ return {
780
+ name: serverName,
781
+ package: pkg,
782
+ source: "local",
783
+ tools: tools.map((t) => {
784
+ const result = (0, import_classifier.classifyTool)({
785
+ name: t.name,
786
+ description: t.description || "",
787
+ inputSchema: t.inputSchema
788
+ });
789
+ const signalsSummary = result.signals.length > 0 ? result.signals.map((s) => s.signal).join("; ") : null;
790
+ return {
791
+ name: t.name,
792
+ description: t.description || "",
793
+ category: result.category,
794
+ severity: result.severity,
795
+ riskNote: signalsSummary,
796
+ riskWeight: result.riskWeight,
797
+ riskType: null,
798
+ confidence: result.confidence,
799
+ inputSchema: t.inputSchema
800
+ };
801
+ })
802
+ };
803
+ }
804
+
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}:
816
+ rules:
817
+ - action: allow
818
+ rate_limit: 60/minute
819
+
820
+ `;
821
+ break;
822
+ case "Write":
823
+ case "Execute":
824
+ yaml += ` ${tool.name}:
825
+ rules:
826
+ - action: allow
827
+ rate_limit: 10/hour
828
+
829
+ `;
830
+ break;
831
+ case "Financial":
832
+ yaml += ` ${tool.name}:
833
+ rules:
834
+ - action: deny
835
+ on_deny: "Financial operation blocked by policy"
836
+
837
+ `;
838
+ break;
839
+ case "Destructive":
840
+ yaml += ` ${tool.name}:
841
+ rules:
842
+ - action: deny
843
+ on_deny: "Destructive operation blocked by policy"
844
+
845
+ `;
846
+ break;
847
+ default:
848
+ yaml += ` ${tool.name}:
849
+ rules:
850
+ - action: allow
851
+ rate_limit: 30/minute
852
+
853
+ `;
854
+ }
855
+ }
856
+ }
857
+ return yaml.trim();
858
+ }
859
+ function writePolicyFile(path, servers) {
860
+ const yaml = generatePolicyYaml(servers);
861
+ writeFileSync(path, yaml + "\n");
862
+ }
863
+
864
+ // src/report.ts
865
+ var API_BASE2 = process.env.POLICYLAYER_API_URL || "https://api.policylayer.com";
866
+ function buildSummary(servers) {
867
+ const categories = {};
868
+ const severities = {};
869
+ let totalTools = 0;
870
+ for (const s of servers) {
871
+ for (const t of s.tools) {
872
+ totalTools++;
873
+ categories[t.category] = (categories[t.category] || 0) + 1;
874
+ severities[t.severity] = (severities[t.severity] || 0) + 1;
875
+ }
876
+ }
877
+ return { totalServers: servers.length, totalTools, categories, severities };
878
+ }
879
+ async function submitReport(servers, policy) {
880
+ const summary = buildSummary(servers);
881
+ const unknownTools = servers.filter((s) => s.source === "local").flatMap(
882
+ (s) => s.tools.map((t) => ({
883
+ serverName: s.name,
884
+ serverPackage: s.package,
885
+ toolName: t.name,
886
+ toolDescription: t.description,
887
+ inputSchema: t.inputSchema || null,
888
+ suggestedCategory: t.category
889
+ }))
890
+ );
891
+ try {
892
+ const res = await fetch(`${API_BASE2}/api/scan/submit`, {
893
+ method: "POST",
894
+ headers: { "Content-Type": "application/json" },
895
+ body: JSON.stringify({
896
+ source: "cli",
897
+ summary,
898
+ servers: servers.map((s) => ({
899
+ name: s.name,
900
+ package: s.package,
901
+ toolCount: s.tools.length,
902
+ tools: s.tools
903
+ })),
904
+ policy,
905
+ unknownTools: unknownTools.length > 0 ? unknownTools : void 0
906
+ }),
907
+ signal: AbortSignal.timeout(1e4)
908
+ });
909
+ if (!res.ok) return null;
910
+ const liveServers = servers.filter((s) => s.source === "local" && s.tools.length > 0);
911
+ if (liveServers.length > 0) {
912
+ contributeTools(liveServers).catch(() => {
913
+ });
914
+ }
915
+ return await res.json();
916
+ } catch {
917
+ return null;
918
+ }
919
+ }
920
+ async function contributeTools(servers) {
921
+ await fetch(`${API_BASE2}/api/scan/contribute`, {
922
+ method: "POST",
923
+ headers: { "Content-Type": "application/json" },
924
+ body: JSON.stringify({
925
+ servers: servers.map((s) => ({
926
+ package: s.package,
927
+ tools: s.tools.map((t) => ({
928
+ name: t.name,
929
+ description: t.description,
930
+ inputSchema: t.inputSchema
931
+ }))
932
+ }))
933
+ }),
934
+ signal: AbortSignal.timeout(1e4)
935
+ });
936
+ }
937
+
938
+ // src/index.ts
939
+ 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);
941
+ async function run(opts) {
942
+ const cwd = opts.dir;
943
+ const timeoutMs = parseInt(opts.timeout, 10);
944
+ if (!opts.json) console.log("Discovering MCP config files...");
945
+ const configs = discoverConfigs(cwd);
946
+ if (configs.length === 0) {
947
+ if (opts.json) {
948
+ console.log(JSON.stringify({ error: "No MCP config files found", configs: [] }));
949
+ } else {
950
+ console.log("No MCP config files found. Checked:");
951
+ console.log(" .mcp.json, .cursor/mcp.json, .vscode/settings.json, .codex/config.toml");
952
+ console.log(" ~/.claude.json, Claude Desktop, Windsurf, ~/.codex/config.toml");
953
+ }
954
+ process.exit(0);
955
+ }
956
+ if (!opts.json) {
957
+ console.log(`Found ${configs.length} config file(s):`);
958
+ for (const c of configs) {
959
+ console.log(` ${c.source} \u2014 ${Object.keys(c.servers).length} server(s)`);
960
+ }
961
+ console.log();
962
+ }
963
+ const merged = mergeConfigs(configs);
964
+ const serverNames = Object.keys(merged);
965
+ if (!opts.json) {
966
+ console.log(`${serverNames.length} unique server(s) across all configs`);
967
+ console.log();
968
+ }
969
+ const liveResults = /* @__PURE__ */ new Map();
970
+ if (opts.live) {
971
+ if (!opts.json) console.log("Live scanning servers...");
972
+ for (const [name, entry] of Object.entries(merged)) {
973
+ const { command, args } = resolveCommand(entry);
974
+ if (!command) {
975
+ if (!opts.json) console.log(` ${name}: skipped (no command/url)`);
976
+ continue;
977
+ }
978
+ try {
979
+ if (!opts.json) process.stdout.write(` ${name}: scanning...`);
980
+ const tools = await liveScan(command, args, timeoutMs, entry.env);
981
+ liveResults.set(name, tools);
982
+ if (!opts.json) console.log(`\r ${name}: ${tools.length} tool(s) discovered`);
983
+ } catch (err) {
984
+ if (!opts.json) console.log(`\r ${name}: failed (${err.message})`);
985
+ }
986
+ }
987
+ if (!opts.json) console.log();
988
+ }
989
+ const classified = [];
990
+ const lookupKeys = [];
991
+ const keyToName = /* @__PURE__ */ new Map();
992
+ for (const name of serverNames) {
993
+ const keys = buildLookupKeys(name, merged[name]);
994
+ for (const key of keys) {
995
+ lookupKeys.push(key);
996
+ keyToName.set(key, name);
997
+ }
998
+ }
999
+ const uniqueKeys = [...new Set(lookupKeys)];
1000
+ const apiResult = uniqueKeys.length > 0 ? await classifyViaApi(uniqueKeys) : null;
1001
+ const matchedByPkg = /* @__PURE__ */ new Map();
1002
+ if (apiResult) {
1003
+ for (const m of apiResult.matched) {
1004
+ matchedByPkg.set(m.slug, m);
1005
+ matchedByPkg.set(m.name, m);
1006
+ for (const pkg of m.packages) {
1007
+ matchedByPkg.set(pkg, m);
1008
+ }
1009
+ }
1010
+ }
1011
+ for (const [name, entry] of Object.entries(merged)) {
1012
+ const pkg = resolvePackage(name, entry) || name;
1013
+ const liveTools = liveResults.get(name);
1014
+ const apiMatch = matchedByPkg.get(pkg) || matchedByPkg.get(name);
1015
+ if (apiMatch && !liveTools) {
1016
+ classified.push({
1017
+ name: apiMatch.name,
1018
+ package: pkg,
1019
+ source: "api",
1020
+ tools: apiMatch.tools.map((t) => ({
1021
+ name: t.name,
1022
+ description: t.description || "",
1023
+ category: t.category,
1024
+ severity: t.severity,
1025
+ riskNote: t.riskNote,
1026
+ riskWeight: t.riskWeight,
1027
+ riskType: t.riskType,
1028
+ confidence: t.confidence
1029
+ }))
1030
+ });
1031
+ } else if (liveTools && liveTools.length > 0) {
1032
+ classified.push(classifyLocally(name, pkg, liveTools));
1033
+ } else {
1034
+ if (!opts.json) console.log(` ${name}: no tools found, skipping`);
1035
+ }
1036
+ }
1037
+ if (classified.length === 0) {
1038
+ if (opts.json) {
1039
+ console.log(JSON.stringify({ error: "No tools discovered", servers: 0 }));
1040
+ } else {
1041
+ console.log("No tools discovered from any server.");
1042
+ }
1043
+ process.exit(0);
1044
+ }
1045
+ const policyYaml = generatePolicyYaml(classified);
1046
+ const outputPath = join2(cwd, opts.output);
1047
+ writePolicyFile(outputPath, classified);
1048
+ let reportResult = null;
1049
+ if (opts.report) {
1050
+ reportResult = await submitReport(classified, policyYaml);
1051
+ }
1052
+ const totalTools = classified.reduce((sum, s) => sum + s.tools.length, 0);
1053
+ const categories = {};
1054
+ for (const s of classified) {
1055
+ for (const t of s.tools) {
1056
+ categories[t.category] = (categories[t.category] || 0) + 1;
1057
+ }
1058
+ }
1059
+ if (opts.json) {
1060
+ console.log(JSON.stringify({
1061
+ servers: classified.length,
1062
+ tools: totalTools,
1063
+ categories,
1064
+ policyFile: outputPath,
1065
+ reportUrl: reportResult?.url || null,
1066
+ reportId: reportResult?.id || null,
1067
+ results: classified
1068
+ }, null, 2));
1069
+ } else {
1070
+ console.log("Scan complete.");
1071
+ console.log();
1072
+ console.log(` Servers: ${classified.length}`);
1073
+ console.log(` Tools: ${totalTools}`);
1074
+ console.log(` Categories: ${Object.entries(categories).map(([k, v]) => `${k}(${v})`).join(" ")}`);
1075
+ console.log();
1076
+ console.log(` Policy: ${outputPath}`);
1077
+ if (reportResult) {
1078
+ console.log(` Report: ${reportResult.url}`);
1079
+ }
1080
+ console.log();
1081
+ const highRisk = classified.flatMap(
1082
+ (s) => s.tools.filter((t) => t.category === "Financial" || t.category === "Destructive")
1083
+ );
1084
+ if (highRisk.length > 0) {
1085
+ console.log(` ${highRisk.length} high-risk tool(s) detected:`);
1086
+ for (const t of highRisk.slice(0, 10)) {
1087
+ console.log(` - ${t.name} (${t.category})`);
1088
+ }
1089
+ if (highRisk.length > 10) {
1090
+ console.log(` ... and ${highRisk.length - 10} more`);
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ function resolveCommand(entry) {
1096
+ if (entry.command) {
1097
+ return { command: entry.command, args: (entry.args || []).map(String) };
1098
+ }
1099
+ return { command: "", args: [] };
1100
+ }
1101
+ function resolvePackage(name, entry) {
1102
+ if (entry.command === "npx") {
1103
+ const args = (entry.args || []).map(String);
1104
+ for (const arg of args) {
1105
+ if (!arg.startsWith("-")) return arg;
1106
+ }
1107
+ }
1108
+ if (entry.args) {
1109
+ for (const arg of entry.args) {
1110
+ const s = String(arg);
1111
+ if (s.startsWith("@") && s.includes("/")) return s;
1112
+ }
1113
+ }
1114
+ return null;
1115
+ }
1116
+ function buildLookupKeys(name, entry) {
1117
+ const keys = [];
1118
+ const pkg = resolvePackage(name, entry);
1119
+ if (pkg) keys.push(pkg);
1120
+ keys.push(name);
1121
+ const stripped = name.replace(/[-_]mcp[-_]server$/i, "").replace(/[-_]mcp$/i, "").replace(/[-_]server$/i, "");
1122
+ if (stripped !== name) keys.push(stripped);
1123
+ return [...new Set(keys)];
1124
+ }
1125
+ program.parse();
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "policylayer",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
- "description": "Scan your MCP config for security risks",
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
  },
@@ -14,7 +14,6 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsup",
17
- "test": "vitest run",
18
17
  "prepublishOnly": "npm run build"
19
18
  },
20
19
  "keywords": [
@@ -22,7 +21,8 @@
22
21
  "security",
23
22
  "scan",
24
23
  "claude",
25
- "ai-agents"
24
+ "ai-agents",
25
+ "policylayer"
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
@@ -30,9 +30,8 @@
30
30
  "smol-toml": "^1.6.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@types/node": "^25.5.0",
33
+ "@types/node": "^22.0.0",
34
34
  "tsup": "^8.5.1",
35
- "typescript": "^5.9.3",
36
- "vitest": "^4.1.0"
35
+ "typescript": "^5.9.3"
37
36
  }
38
37
  }
package/README.md DELETED
@@ -1,81 +0,0 @@
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 scan
9
- ```
10
-
11
- That's it. The CLI finds your MCP config, analyses it against 115+ known servers, 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. **Strips secrets** -- only server names and package identifiers are sent. API keys, tokens, env vars, and file paths are removed before anything leaves your machine
17
- 3. **Sends to the scan API** -- analyses your servers against a database of 2,500+ tools with severity classifications
18
- 4. **Prints a report URL** -- permanent, shareable, no login required
19
-
20
- ## Commands
21
-
22
- ### `policylayer scan`
23
-
24
- Scan your MCP configuration.
25
-
26
- ```bash
27
- npx -y policylayer scan
28
- ```
29
-
30
- **Options:**
31
-
32
- | Flag | Description |
33
- |------|-------------|
34
- | `--config <path>` | Path to a specific config file (skips auto-detection) |
35
- | `--dry-run` | Show the stripped payload without sending it |
36
-
37
- ### Examples
38
-
39
- ```bash
40
- # Auto-detect config and scan
41
- npx -y policylayer scan
42
-
43
- # Scan a specific config file
44
- npx -y policylayer scan --config .mcp.json
45
-
46
- # See what would be sent (nothing leaves your machine)
47
- npx -y policylayer scan --dry-run
48
- ```
49
-
50
- ## Config detection
51
-
52
- The CLI searches these paths in order and uses the first one found:
53
-
54
- | Client | Path |
55
- |--------|------|
56
- | Claude Code (project) | `.mcp.json` |
57
- | Claude Code (user) | `~/.claude.json` |
58
- | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` |
59
- | Cursor | `.cursor/mcp.json` |
60
- | VS Code | `.vscode/settings.json` |
61
- | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
62
- | Codex (project) | `.codex/config.toml` |
63
- | Codex (user) | `~/.codex/config.toml` |
64
-
65
- ## Privacy
66
-
67
- The CLI **never sends** your raw config. Before anything leaves your machine:
68
-
69
- - Environment variables are stripped
70
- - Auth tokens (`sk-`, `ghp_`, `ict_`, Bearer tokens, etc.) are removed
71
- - Absolute file paths are removed
72
- - Command flags are removed
73
- - Only server names and npm package identifiers are sent
74
-
75
- Use `--dry-run` to see exactly what would be sent.
76
-
77
- ## Links
78
-
79
- - [Example report](https://policylayer.com/scan/report/65545482-5d1d-472f-9fca-472ff1181d0d)
80
- - [Scan your config online](https://policylayer.com/scan)
81
- - [Intercept](https://github.com/policylayer/intercept) -- enforce limits on every MCP tool call