toolstile 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +104 -2
- package/dist/card.d.ts +6 -0
- package/dist/card.js +51 -0
- package/dist/checks.d.ts +18 -0
- package/dist/checks.js +322 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +155 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +25 -0
- package/dist/parse.d.ts +3 -0
- package/dist/parse.js +76 -0
- package/dist/report.d.ts +18 -0
- package/dist/report.js +64 -0
- package/dist/secrets.d.ts +18 -0
- package/dist/secrets.js +122 -0
- package/dist/sources.d.ts +22 -0
- package/dist/sources.js +182 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +1 -0
- package/package.json +53 -4
package/dist/sources.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { parseConfig } from "./parse.js";
|
|
4
|
+
export const CLIENT_NAMES = {
|
|
5
|
+
"claude-code": "Claude Code",
|
|
6
|
+
"claude-desktop": "Claude Desktop",
|
|
7
|
+
cursor: "Cursor",
|
|
8
|
+
codex: "Codex",
|
|
9
|
+
vscode: "VS Code",
|
|
10
|
+
windsurf: "Windsurf",
|
|
11
|
+
gemini: "Gemini CLI",
|
|
12
|
+
cline: "Cline",
|
|
13
|
+
};
|
|
14
|
+
function appData({ home, platform, env }) {
|
|
15
|
+
if (platform === "darwin")
|
|
16
|
+
return join(home, "Library", "Application Support");
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
return env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
19
|
+
return env.XDG_CONFIG_HOME ?? join(home, ".config");
|
|
20
|
+
}
|
|
21
|
+
function candidates(opts) {
|
|
22
|
+
const { home, cwd, env } = opts;
|
|
23
|
+
const data = appData(opts);
|
|
24
|
+
const codexHome = env.CODEX_HOME ?? join(home, ".codex");
|
|
25
|
+
const clineSettings = ["globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"];
|
|
26
|
+
const list = [
|
|
27
|
+
{ client: "claude-code", path: join(home, ".claude.json"), kind: "claude-json" },
|
|
28
|
+
{ client: "claude-code", path: join(home, ".claude", "settings.json"), kind: "claude-settings" },
|
|
29
|
+
{ client: "claude-desktop", path: join(data, "Claude", "claude_desktop_config.json"), kind: "mcp" },
|
|
30
|
+
{ client: "cursor", path: join(home, ".cursor", "mcp.json"), kind: "mcp" },
|
|
31
|
+
{ client: "codex", path: join(codexHome, "config.toml"), kind: "codex" },
|
|
32
|
+
{ client: "windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json"), kind: "mcp" },
|
|
33
|
+
{ client: "gemini", path: join(home, ".gemini", "settings.json"), kind: "mcp" },
|
|
34
|
+
];
|
|
35
|
+
for (const edition of ["Code", "Code - Insiders"]) {
|
|
36
|
+
list.push({ client: "vscode", path: join(data, edition, "User", "settings.json"), kind: "vscode-settings" }, { client: "vscode", path: join(data, edition, "User", "mcp.json"), kind: "vscode-mcp" }, { client: "cline", path: join(data, edition, "User", ...clineSettings), kind: "mcp" });
|
|
37
|
+
}
|
|
38
|
+
list.push({ client: "cline", path: join(data, "Cursor", "User", ...clineSettings), kind: "mcp" });
|
|
39
|
+
if (cwd) {
|
|
40
|
+
const dir = resolve(cwd);
|
|
41
|
+
list.push({ client: "claude-code", path: join(dir, ".mcp.json"), kind: "mcp" }, { client: "claude-code", path: join(dir, ".claude", "settings.json"), kind: "claude-settings" }, { client: "claude-code", path: join(dir, ".claude", "settings.local.json"), kind: "claude-settings" }, { client: "cursor", path: join(dir, ".cursor", "mcp.json"), kind: "mcp" }, { client: "vscode", path: join(dir, ".vscode", "mcp.json"), kind: "vscode-mcp" }, { client: "gemini", path: join(dir, ".gemini", "settings.json"), kind: "mcp" }, { client: "codex", path: join(dir, ".codex", "config.toml"), kind: "codex" });
|
|
42
|
+
}
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
return list.filter((c) => (seen.has(c.path) ? false : (seen.add(c.path), true)));
|
|
45
|
+
}
|
|
46
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
47
|
+
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
48
|
+
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
49
|
+
function stringRecord(v) {
|
|
50
|
+
const out = {};
|
|
51
|
+
if (isObject(v))
|
|
52
|
+
for (const [k, x] of Object.entries(v))
|
|
53
|
+
if (typeof x === "string")
|
|
54
|
+
out[k] = x;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** Appends a key to a pointer, quoting keys that are not plain identifiers. */
|
|
58
|
+
export function pointerJoin(base, key) {
|
|
59
|
+
const part = /^[A-Za-z_$][\w$-]*$/.test(key) ? key : /^\d+$/.test(key) ? `[${key}]` : `[${JSON.stringify(key)}]`;
|
|
60
|
+
if (!base)
|
|
61
|
+
return part;
|
|
62
|
+
return part.startsWith("[") ? `${base}${part}` : `${base}.${part}`;
|
|
63
|
+
}
|
|
64
|
+
function toServer(client, file, pointer, name, v) {
|
|
65
|
+
if (!isObject(v))
|
|
66
|
+
return null;
|
|
67
|
+
const extraSecrets = {};
|
|
68
|
+
const bearer = str(v.bearer_token);
|
|
69
|
+
if (bearer)
|
|
70
|
+
extraSecrets.bearer_token = bearer;
|
|
71
|
+
return {
|
|
72
|
+
client,
|
|
73
|
+
file,
|
|
74
|
+
pointer,
|
|
75
|
+
name,
|
|
76
|
+
command: str(v.command),
|
|
77
|
+
args: strings(v.args),
|
|
78
|
+
env: stringRecord(v.env),
|
|
79
|
+
url: str(v.url) ?? str(v.serverUrl) ?? str(v.httpUrl),
|
|
80
|
+
headers: { ...stringRecord(v.headers), ...stringRecord(v.http_headers) },
|
|
81
|
+
extraSecrets,
|
|
82
|
+
autoApproved: [...strings(v.alwaysAllow), ...strings(v.autoApprove)],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function serversAt(client, file, base, v) {
|
|
86
|
+
if (!isObject(v))
|
|
87
|
+
return [];
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const [name, def] of Object.entries(v)) {
|
|
90
|
+
const server = toServer(client, file, pointerJoin(base, name), name, def);
|
|
91
|
+
if (server)
|
|
92
|
+
out.push(server);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
function claudeApprovals(file) {
|
|
97
|
+
const data = file.data;
|
|
98
|
+
if (!isObject(data))
|
|
99
|
+
return [];
|
|
100
|
+
const out = [];
|
|
101
|
+
if (data.enableAllProjectMcpServers === true) {
|
|
102
|
+
out.push({
|
|
103
|
+
client: file.client,
|
|
104
|
+
location: { file: file.path, pointer: "enableAllProjectMcpServers" },
|
|
105
|
+
subject: "every MCP server in project .mcp.json files",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const allow = isObject(data.permissions) ? strings(data.permissions.allow) : [];
|
|
109
|
+
allow.forEach((rule, i) => {
|
|
110
|
+
// `mcp__server` and `mcp__server__*` approve every tool of a server; named tools are deliberate.
|
|
111
|
+
if (!rule.startsWith("mcp__"))
|
|
112
|
+
return;
|
|
113
|
+
const rest = rule.slice("mcp__".length);
|
|
114
|
+
if (rest === "*" || !rest.includes("__") || rest.endsWith("__*")) {
|
|
115
|
+
out.push({
|
|
116
|
+
client: file.client,
|
|
117
|
+
location: { file: file.path, pointer: `permissions.allow[${i}]` },
|
|
118
|
+
subject: rule,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
export function discover(opts) {
|
|
125
|
+
const result = { files: [], unreadable: [], servers: [], approvals: [] };
|
|
126
|
+
for (const c of candidates(opts)) {
|
|
127
|
+
let text;
|
|
128
|
+
let mode;
|
|
129
|
+
try {
|
|
130
|
+
const stat = statSync(c.path);
|
|
131
|
+
if (!stat.isFile())
|
|
132
|
+
continue;
|
|
133
|
+
mode = opts.platform === "win32" ? undefined : stat.mode & 0o777;
|
|
134
|
+
text = readFileSync(c.path, "utf8");
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let data;
|
|
140
|
+
try {
|
|
141
|
+
data = parseConfig(c.path, text);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
result.unreadable.push({ client: c.client, path: c.path, error: error.message });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const file = { client: c.client, path: c.path, data, mode };
|
|
148
|
+
result.files.push(file);
|
|
149
|
+
if (!isObject(data))
|
|
150
|
+
continue;
|
|
151
|
+
switch (c.kind) {
|
|
152
|
+
case "mcp":
|
|
153
|
+
result.servers.push(...serversAt(c.client, c.path, "mcpServers", data.mcpServers));
|
|
154
|
+
break;
|
|
155
|
+
case "claude-json":
|
|
156
|
+
result.servers.push(...serversAt(c.client, c.path, "mcpServers", data.mcpServers));
|
|
157
|
+
if (isObject(data.projects)) {
|
|
158
|
+
for (const [project, conf] of Object.entries(data.projects)) {
|
|
159
|
+
if (!isObject(conf))
|
|
160
|
+
continue;
|
|
161
|
+
const base = pointerJoin(pointerJoin("projects", project), "mcpServers");
|
|
162
|
+
result.servers.push(...serversAt(c.client, c.path, base, conf.mcpServers));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
case "claude-settings":
|
|
167
|
+
result.approvals.push(...claudeApprovals(file));
|
|
168
|
+
break;
|
|
169
|
+
case "codex":
|
|
170
|
+
result.servers.push(...serversAt(c.client, c.path, "mcp_servers", data.mcp_servers));
|
|
171
|
+
break;
|
|
172
|
+
case "vscode-settings":
|
|
173
|
+
if (isObject(data.mcp))
|
|
174
|
+
result.servers.push(...serversAt(c.client, c.path, "mcp.servers", data.mcp.servers));
|
|
175
|
+
break;
|
|
176
|
+
case "vscode-mcp":
|
|
177
|
+
result.servers.push(...serversAt(c.client, c.path, "servers", data.servers));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type Severity = "high" | "medium" | "low";
|
|
2
|
+
export type ClientId = "claude-code" | "claude-desktop" | "cursor" | "codex" | "vscode" | "windsurf" | "gemini" | "cline";
|
|
3
|
+
export interface Location {
|
|
4
|
+
/** Absolute path of the config file. */
|
|
5
|
+
file: string;
|
|
6
|
+
/** Dotted path to the value inside the file, for example `mcpServers.github.env.TOKEN`. */
|
|
7
|
+
pointer: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ConfigFile {
|
|
10
|
+
client: ClientId;
|
|
11
|
+
path: string;
|
|
12
|
+
/** Parsed file contents. */
|
|
13
|
+
data: unknown;
|
|
14
|
+
/** Unix permission bits, when available. */
|
|
15
|
+
mode?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ServerEntry {
|
|
18
|
+
client: ClientId;
|
|
19
|
+
file: string;
|
|
20
|
+
/** Pointer to the server object, for example `mcpServers.github`. */
|
|
21
|
+
pointer: string;
|
|
22
|
+
name: string;
|
|
23
|
+
command?: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
url?: string;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
/** Other string fields that may carry credentials, such as Codex `bearer_token`. */
|
|
29
|
+
extraSecrets: Record<string, string>;
|
|
30
|
+
/** Tools listed under client auto-approval keys (`alwaysAllow`, `autoApprove`). */
|
|
31
|
+
autoApproved: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Client settings that approve MCP tools without asking. */
|
|
34
|
+
export interface ApprovalSetting {
|
|
35
|
+
client: ClientId;
|
|
36
|
+
location: Location;
|
|
37
|
+
/** What is approved, for example `mcp__github` or `all project servers`. */
|
|
38
|
+
subject: string;
|
|
39
|
+
}
|
|
40
|
+
export interface Finding {
|
|
41
|
+
rule: string;
|
|
42
|
+
severity: Severity;
|
|
43
|
+
title: string;
|
|
44
|
+
detail: string;
|
|
45
|
+
fix: string;
|
|
46
|
+
locations: Location[];
|
|
47
|
+
}
|
|
48
|
+
export interface AuditResult {
|
|
49
|
+
files: {
|
|
50
|
+
client: ClientId;
|
|
51
|
+
path: string;
|
|
52
|
+
}[];
|
|
53
|
+
/** Files that exist but could not be parsed. */
|
|
54
|
+
unreadable: {
|
|
55
|
+
client: ClientId;
|
|
56
|
+
path: string;
|
|
57
|
+
error: string;
|
|
58
|
+
}[];
|
|
59
|
+
servers: number;
|
|
60
|
+
clients: ClientId[];
|
|
61
|
+
findings: Finding[];
|
|
62
|
+
score: number | null;
|
|
63
|
+
grade: string | null;
|
|
64
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolstile",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Audit the MCP server configs your AI agents use for plaintext secrets, unpinned packages, auto-approved tools and duplicated setups. Local and read-only.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"security",
|
|
9
|
+
"audit",
|
|
10
|
+
"claude",
|
|
11
|
+
"cursor",
|
|
12
|
+
"codex",
|
|
13
|
+
"secrets"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/toolstile/cli#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/toolstile/cli.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"email": "support@toolstile.com"
|
|
22
|
+
},
|
|
23
|
+
"author": "Akshit Kr Nagpal <hello@akshit.io> (https://akshit.io)",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"toolstile": "dist/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli.js",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"prepublishOnly": "pnpm typecheck && pnpm build && pnpm test"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"smol-toml": "^1.9.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.19.0",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^4.0.0"
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "pnpm@11.20.0",
|
|
52
|
+
"optionalDependencies": {
|
|
53
|
+
"@resvg/resvg-js": "^2.6.2"
|
|
54
|
+
}
|
|
55
|
+
}
|