mcp-tollbooth 0.1.0 → 0.2.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/dist/auditor.js +9 -2
- package/dist/heuristics.js +25 -6
- package/dist/registryInfo.js +41 -2
- package/package.json +12 -2
package/dist/auditor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { categorize, estimateTokens } from "./heuristics.js";
|
|
2
|
-
import { classifyServer, fetchGithubRepoInfo, fetchNpmPackageInfo } from "./registryInfo.js";
|
|
2
|
+
import { classifyServer, fetchGithubRepoInfo, fetchNpmPackageInfo, KNOWN_UVX_SERVER_REPOS } from "./registryInfo.js";
|
|
3
3
|
const OFFICIAL_SCOPES = ["@modelcontextprotocol", "@anthropic-ai", "@upstash", "@notionhq", "@playwright"];
|
|
4
4
|
function computeTrustScore(params) {
|
|
5
5
|
const notes = [];
|
|
@@ -72,10 +72,17 @@ export async function auditConfigFiles(configFiles) {
|
|
|
72
72
|
const { kind, packageName, rawCommandLine } = classifyServer(rawCfg);
|
|
73
73
|
let packageInfo;
|
|
74
74
|
let githubLookup;
|
|
75
|
-
if (packageName) {
|
|
75
|
+
if (packageName && kind === "npx-package") {
|
|
76
76
|
packageInfo = (await fetchNpmPackageInfo(packageName)) ?? { name: packageName };
|
|
77
77
|
githubLookup = await fetchGithubRepoInfo(packageInfo.repositoryUrl);
|
|
78
78
|
}
|
|
79
|
+
else if (packageName && kind === "uvx-package") {
|
|
80
|
+
// No PyPI metadata fetcher yet — only cross-check against the known-good
|
|
81
|
+
// PyPI repo map (never the npm one; see KNOWN_UVX_SERVER_REPOS comment).
|
|
82
|
+
packageInfo = { name: packageName };
|
|
83
|
+
const knownRepo = KNOWN_UVX_SERVER_REPOS[packageName];
|
|
84
|
+
githubLookup = knownRepo ? await fetchGithubRepoInfo(knownRepo) : { status: "not-found" };
|
|
85
|
+
}
|
|
79
86
|
const { tokens, confidence } = estimateTokens(packageName, kind);
|
|
80
87
|
const category = categorize(serverName, packageName);
|
|
81
88
|
const { score, notes } = computeTrustScore({ githubLookup, kind, packageName });
|
package/dist/heuristics.js
CHANGED
|
@@ -9,12 +9,10 @@ const KNOWN_SERVER_TOKEN_COSTS = {
|
|
|
9
9
|
"@modelcontextprotocol/server-filesystem": 900,
|
|
10
10
|
"@modelcontextprotocol/server-slack": 1200,
|
|
11
11
|
"@modelcontextprotocol/server-postgres": 800,
|
|
12
|
-
"@modelcontextprotocol/server-sqlite": 700,
|
|
13
12
|
"@modelcontextprotocol/server-puppeteer": 1400,
|
|
14
13
|
"@modelcontextprotocol/server-brave-search": 500,
|
|
15
14
|
"@modelcontextprotocol/server-memory": 400,
|
|
16
15
|
"@modelcontextprotocol/server-everything": 2000,
|
|
17
|
-
"@modelcontextprotocol/server-fetch": 350,
|
|
18
16
|
"@modelcontextprotocol/server-sequential-thinking": 300,
|
|
19
17
|
"@modelcontextprotocol/server-google-maps": 900,
|
|
20
18
|
"@modelcontextprotocol/server-everart": 500,
|
|
@@ -29,8 +27,24 @@ const KNOWN_SERVER_TOKEN_COSTS = {
|
|
|
29
27
|
"mongodb-mcp-server": 1500,
|
|
30
28
|
"@supabase/mcp-server-supabase": 1400,
|
|
31
29
|
"@cloudflare/mcp-server-cloudflare": 1900,
|
|
32
|
-
"stripe-mcp"
|
|
33
|
-
"@asana/mcp-server":
|
|
30
|
+
// NOTE: "stripe-mcp" isn't a real package — Stripe's official npm package is
|
|
31
|
+
// "@stripe/mcp" (repo: stripe/ai). "@asana/mcp-server" was dropped entirely:
|
|
32
|
+
// Asana's official MCP is remote-only (OAuth via mcp.asana.com), not something
|
|
33
|
+
// that shows up as an npx entry in a local config, so a hardcoded cost for it
|
|
34
|
+
// was never going to match anything real.
|
|
35
|
+
"@stripe/mcp": 1700,
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Known costs for uvx/pipx (PyPI) packages, kept in a separate table keyed by
|
|
39
|
+
* the *same string space* as KNOWN_SERVER_TOKEN_COSTS above — but never merged
|
|
40
|
+
* with it. "mcp-server-fetch" invoked via npx is a different, unrelated package
|
|
41
|
+
* (an npm-namesquat) from "mcp-server-fetch" invoked via uvx (the real PyPI
|
|
42
|
+
* tool); estimateTokens() below picks the right table based on `kind` so an
|
|
43
|
+
* npx-invoked squat can never inherit the trusted PyPI package's numbers.
|
|
44
|
+
*/
|
|
45
|
+
const KNOWN_UVX_TOKEN_COSTS = {
|
|
46
|
+
"mcp-server-fetch": 350,
|
|
47
|
+
"mcp-server-sqlite": 700,
|
|
34
48
|
};
|
|
35
49
|
/**
|
|
36
50
|
* Category keywords are matched as whole tokens against the server/package name,
|
|
@@ -60,14 +74,19 @@ function tokenize(s) {
|
|
|
60
74
|
.filter(Boolean));
|
|
61
75
|
}
|
|
62
76
|
export function estimateTokens(packageName, kind) {
|
|
63
|
-
if (packageName
|
|
64
|
-
|
|
77
|
+
if (packageName) {
|
|
78
|
+
const table = kind === "uvx-package" ? KNOWN_UVX_TOKEN_COSTS : KNOWN_SERVER_TOKEN_COSTS;
|
|
79
|
+
if (table[packageName] !== undefined) {
|
|
80
|
+
return { tokens: table[packageName], confidence: "known" };
|
|
81
|
+
}
|
|
65
82
|
}
|
|
66
83
|
// Heuristic fallback by server "shape"
|
|
67
84
|
if (kind === "remote-http")
|
|
68
85
|
return { tokens: 1500, confidence: "heuristic" };
|
|
69
86
|
if (kind === "local-binary")
|
|
70
87
|
return { tokens: 1000, confidence: "heuristic" };
|
|
88
|
+
if (kind === "uvx-package")
|
|
89
|
+
return { tokens: 700, confidence: "heuristic" };
|
|
71
90
|
return { tokens: 800, confidence: "heuristic" };
|
|
72
91
|
}
|
|
73
92
|
export function categorize(name, packageName) {
|
package/dist/registryInfo.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
2
2
|
const GITHUB_API = "https://api.github.com";
|
|
3
|
+
/**
|
|
4
|
+
* Fallback GitHub repo for packages whose npm registry metadata is missing (or
|
|
5
|
+
* inconsistent about) a `repository` field. Verified by hand against the live
|
|
6
|
+
* registry — several official @modelcontextprotocol/* packages simply don't
|
|
7
|
+
* publish this field even though they live in a real, well-known monorepo, and
|
|
8
|
+
* without this map their trust score always reads "no repo found" regardless of
|
|
9
|
+
* how well-maintained the actual project is.
|
|
10
|
+
*/
|
|
11
|
+
const KNOWN_SERVER_REPOS = {
|
|
12
|
+
"@modelcontextprotocol/server-github": "https://github.com/modelcontextprotocol/servers",
|
|
13
|
+
"@modelcontextprotocol/server-slack": "https://github.com/modelcontextprotocol/servers",
|
|
14
|
+
"@modelcontextprotocol/server-postgres": "https://github.com/modelcontextprotocol/servers",
|
|
15
|
+
"@modelcontextprotocol/server-puppeteer": "https://github.com/modelcontextprotocol/servers",
|
|
16
|
+
"@modelcontextprotocol/server-brave-search": "https://github.com/modelcontextprotocol/servers",
|
|
17
|
+
"@modelcontextprotocol/server-google-maps": "https://github.com/modelcontextprotocol/servers",
|
|
18
|
+
"@modelcontextprotocol/server-everart": "https://github.com/modelcontextprotocol/servers",
|
|
19
|
+
"@modelcontextprotocol/server-redis": "https://github.com/modelcontextprotocol/servers",
|
|
20
|
+
"@cloudflare/mcp-server-cloudflare": "https://github.com/cloudflare/mcp-server-cloudflare",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Repo fallback for known **uvx/pipx** (PyPI) packages only. Deliberately kept
|
|
24
|
+
* separate from KNOWN_SERVER_REPOS above: "mcp-server-fetch" and
|
|
25
|
+
* "mcp-server-sqlite" are real PyPI package names, but those exact strings are
|
|
26
|
+
* ALSO squattable (or already squatted — see e.g. the "npx confusion" security
|
|
27
|
+
* research canary published to npm under the literal name "mcp-server-fetch").
|
|
28
|
+
* If a config invokes a same-named package via `npx` instead of `uvx`, it is a
|
|
29
|
+
* *different* package in a *different* registry, and must never inherit this
|
|
30
|
+
* trust/repo mapping. Kind separation in classifyServer() is what keeps these
|
|
31
|
+
* two lookups from colliding.
|
|
32
|
+
*/
|
|
33
|
+
export const KNOWN_UVX_SERVER_REPOS = {
|
|
34
|
+
"mcp-server-fetch": "https://github.com/modelcontextprotocol/servers",
|
|
35
|
+
"mcp-server-sqlite": "https://github.com/modelcontextprotocol/servers",
|
|
36
|
+
};
|
|
3
37
|
/** Figures out roughly what kind of server this is, and pulls out an npm package name if present. */
|
|
4
38
|
export function classifyServer(cfg) {
|
|
5
39
|
if (cfg.url) {
|
|
@@ -16,9 +50,13 @@ export function classifyServer(cfg) {
|
|
|
16
50
|
}
|
|
17
51
|
}
|
|
18
52
|
if (command === "uvx" || command === "pipx") {
|
|
53
|
+
// Separate kind from npx: uvx/pipx pull from PyPI, not npm. Same package-name
|
|
54
|
+
// string can refer to two completely unrelated packages across the two
|
|
55
|
+
// registries (see the KNOWN_UVX_SERVER_REPOS comment), so these must never
|
|
56
|
+
// be looked up against the npm registry as if they were npx packages.
|
|
19
57
|
const pkg = args.find((a) => !a.startsWith("-"));
|
|
20
58
|
if (pkg) {
|
|
21
|
-
return { kind: "
|
|
59
|
+
return { kind: "uvx-package", packageName: stripVersion(pkg), rawCommandLine };
|
|
22
60
|
}
|
|
23
61
|
}
|
|
24
62
|
if (command) {
|
|
@@ -53,7 +91,8 @@ export async function fetchNpmPackageInfo(packageName) {
|
|
|
53
91
|
const latestVersion = data["dist-tags"]?.latest;
|
|
54
92
|
const versionData = latestVersion ? data.versions?.[latestVersion] : undefined;
|
|
55
93
|
const repository = versionData?.repository ?? data.repository;
|
|
56
|
-
const repoUrl = typeof repository === "string" ? repository : repository?.url
|
|
94
|
+
const repoUrl = (typeof repository === "string" ? repository : repository?.url) ??
|
|
95
|
+
KNOWN_SERVER_REPOS[packageName];
|
|
57
96
|
return {
|
|
58
97
|
name: packageName,
|
|
59
98
|
version: latestVersion,
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-tollbooth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "How much context are your MCP servers actually costing you? Token budget + duplicate-server detection for Claude Desktop, Claude Code, Cursor and VS Code.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"claude",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"cursor",
|
|
11
|
+
"ai-agents",
|
|
12
|
+
"context-window",
|
|
13
|
+
"token-budget",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
6
16
|
"license": "MIT",
|
|
7
17
|
"type": "module",
|
|
8
18
|
"author": "Ereb-Blade",
|