Spaces:
Running
Running
Download index.html from getreadystack/mcp-config-audit: direct link, hf CLI and curl.
- Browser
- Download file 62.4 kB
-
https://huggingface.co/spaces/getreadystack/mcp-config-audit/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/mcp-config-audit/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/mcp-config-audit/resolve/main/index.html
62.4 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>MCP Server Config Audit — audit your config in browser</title> | |
| <meta name="description" content="This free tool audits mcp.json for inline API keys, unpinned npx launches, plaintext remote servers, shell wrappers, auto-approved tools, and filesystem roots."> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/mcp-config-audit"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>MCP Server Config Audit — audit your config in browser</h1> | |
| <p class="lede">Audits mcp.json for the mistakes AI-written agent configs carry: inline API keys, unpinned npx launches, plaintext remote servers, shell wrappers, auto-approved tools and filesystem roots. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">{ | |
| "servers": { | |
| "github": { | |
| "type": "stdio", | |
| "command": "npx", | |
| "args": ["-y", "@modelcontextprotocol/server-github"], | |
| "env": { | |
| "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_7Qm2Xd9Lb4VtA0eNc5Yw1PzHj8Uf3Sg6TiRk" | |
| } | |
| }, | |
| "filesystem": { | |
| "type": "stdio", | |
| "command": "npx", | |
| "args": ["-y", "@modelcontextprotocol/server-filesystem@0.6.2", "/"], | |
| "alwaysAllow": ["read_file", "write_file", "move_file"] | |
| }, | |
| "postgres": { | |
| "command": "docker", | |
| "args": ["run", "-i", "--rm", "mcp/postgres", "postgresql://app:hunter2@db.internal:5432/prod"] | |
| }, | |
| "ops": { | |
| "command": "bash", | |
| "args": ["-c", "node /Users/alice/tools/ops-mcp/index.js --verbose"] | |
| }, | |
| "analytics": { | |
| "type": "sse", | |
| "url": "http://analytics.internal.example/mcp", | |
| "headers": { | |
| "Authorization": "Bearer sk-live-9f2b7c41ae8d4a6e2b55" | |
| } | |
| } | |
| } | |
| }</textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Audit one mcp.json against all 18 rules and get every inline credential, unpinned launch and over-broad grant named with its line number and its replacement.. <strong>In the editor:</strong> Audit every agent config in a repository and in the user-scope locations for VS Code, Cursor, Claude Desktop and Windsurf in one pass, fail CI on any blocker, and export a dated evidence report. — that part asks for a licence key.</div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var MCP_AUDIT_RULES = [ | |
| { | |
| "id": "json_invalid", | |
| "sev": "error", | |
| "title": "The config is not valid JSON", | |
| "msg": "An agent client that cannot parse the file starts none of the servers in it, usually with no visible error beyond an empty tool list. A trailing comma or a comment is the usual cause; mcp.json is strict JSON.", | |
| "fix": "Remove trailing commas and comments, then reload the window.", | |
| "std": "MCP configuration (JSON)" | |
| }, | |
| { | |
| "id": "secret_inline", | |
| "sev": "error", | |
| "title": "A literal credential is written into the config", | |
| "msg": "The value is a credential typed straight into the file rather than a placeholder. Agent configs live beside the code and get committed, shared in screenshots and pasted into issues, and the token stays valid until somebody rotates it.", | |
| "fix": "Replace the value with ${input:\u003cid\u003e} and declare the id under \"inputs\" with \"password\": true, or read it from the environment.", | |
| "std": "VS Code mcp.json inputs" | |
| }, | |
| { | |
| "id": "input_not_password", | |
| "sev": "error", | |
| "title": "A promptString input for a secret is missing \"password\": true", | |
| "msg": "A promptString input without \"password\": true is typed in the clear and kept in plain storage rather than the secret store, so a value the file was careful not to hardcode is recorded anyway.", | |
| "fix": "Add \"password\": true to the input entry.", | |
| "std": "VS Code mcp.json inputs" | |
| }, | |
| { | |
| "id": "npx_unpinned", | |
| "sev": "error", | |
| "title": "npx launches a package with no version", | |
| "msg": "npx with no version resolves the newest published version at every launch, so the code the agent runs is decided by whoever published last, not by this file. There is no lockfile behind an mcp.json.", | |
| "fix": "Pin the package: \"@scope/server-name@1.2.3\".", | |
| "std": "npm / npx resolution" | |
| }, | |
| { | |
| "id": "uvx_unpinned", | |
| "sev": "warn", | |
| "title": "uvx launches a package with no version", | |
| "msg": "uvx resolves the newest release of the named package on each run, so the server binary can change between two launches of the same editor window.", | |
| "fix": "Pin it: \"package==1.2.3\", or use --from package==1.2.3.", | |
| "std": "uv / uvx resolution" | |
| }, | |
| { | |
| "id": "docker_mutable_tag", | |
| "sev": "warn", | |
| "title": "Docker image has no tag or uses :latest", | |
| "msg": "An untagged image and an image tagged :latest are both mutable references. The pull that runs next week can be a different image than the one reviewed today, with the same line in the config.", | |
| "fix": "Use an explicit version tag, or a digest: image@sha256:...", | |
| "std": "OCI image references" | |
| }, | |
| { | |
| "id": "remote_plaintext_http", | |
| "sev": "error", | |
| "title": "Remote server URL is plain http://", | |
| "msg": "A remote MCP server reached over http:// carries its Authorization header, its tool arguments and everything the tools return in clear text over the network. Loopback addresses are exempt; a hostname is not.", | |
| "fix": "Use https:// for any non-loopback MCP endpoint.", | |
| "std": "MCP transports (HTTP)" | |
| }, | |
| { | |
| "id": "sse_deprecated", | |
| "sev": "warn", | |
| "title": "Deprecated HTTP+SSE transport", | |
| "msg": "The 2025-03-26 revision of the MCP specification replaced the HTTP+SSE transport with Streamable HTTP and marks the SSE form as legacy. Clients still accept it, and each client will drop it on its own schedule.", | |
| "fix": "Move the server to Streamable HTTP and set \"type\": \"http\".", | |
| "std": "MCP spec revision 2025-03-26" | |
| }, | |
| { | |
| "id": "transport_ambiguous", | |
| "sev": "error", | |
| "title": "Server entry has both command and url, or neither", | |
| "msg": "A server entry describes exactly one transport: a command to spawn, or a url to connect to. Carrying both leaves the choice to the client, and carrying neither makes the entry unstartable while the file still parses.", | |
| "fix": "Keep either \"command\" (with \"type\": \"stdio\") or \"url\" (with \"type\": \"http\").", | |
| "std": "MCP configuration" | |
| }, | |
| { | |
| "id": "type_missing", | |
| "sev": "warn", | |
| "title": "Server entry under \"servers\" has no \"type\"", | |
| "msg": "In the VS Code mcp.json schema each server declares its transport with \"type\": \"stdio\", \"http\" or \"sse\". Leaving it out makes the entry depend on a client inferring the transport from the other keys.", | |
| "fix": "Add \"type\": \"stdio\" for a spawned command, \"http\" for a remote endpoint.", | |
| "std": "VS Code mcp.json schema" | |
| }, | |
| { | |
| "id": "auto_approve", | |
| "sev": "error", | |
| "title": "Tools are placed on an auto-approval list", | |
| "msg": "An alwaysAllow or autoApprove list removes the confirmation step for the tools named in it, so a tool call the model was talked into by text inside a file or a web page executes with no human in the loop.", | |
| "fix": "Empty the list, or keep only tools that cannot write, delete, spend or send.", | |
| "std": "MCP client auto-approval" | |
| }, | |
| { | |
| "id": "shell_wrapper", | |
| "sev": "error", | |
| "title": "Server is launched through a shell with -c", | |
| "msg": "bash -c, sh -c, cmd /c and powershell -Command turn the entry into an arbitrary command string: quoting, chained commands and shell expansion all apply, and nothing in the line is a reviewable executable and argument list.", | |
| "fix": "Invoke the interpreter or binary directly and pass arguments as separate array items.", | |
| "std": "Process launch hygiene" | |
| }, | |
| { | |
| "id": "fs_root_broad", | |
| "sev": "error", | |
| "title": "Filesystem server is rooted at the whole machine or home directory", | |
| "msg": "A root of /, ~, $HOME, %USERPROFILE% or a drive letter puts every SSH key, browser profile, .env file and unrelated repository on the machine inside the agent's reach for the whole session.", | |
| "fix": "Root the server at the workspace or a single directory: ${workspaceFolder}/docs.", | |
| "std": "Least privilege" | |
| }, | |
| { | |
| "id": "db_url_credentials", | |
| "sev": "error", | |
| "title": "Database connection string carries a username and password", | |
| "msg": "A DSN written as scheme://user:password@host in args or env is a production credential stored in a config file and echoed into process listings and agent logs whenever the server starts.", | |
| "fix": "Move the DSN behind ${input:\u003cid\u003e} with \"password\": true, or into the environment.", | |
| "std": "Credential handling" | |
| }, | |
| { | |
| "id": "no_readonly_db", | |
| "sev": "warn", | |
| "title": "Database server is started without a read-only flag", | |
| "msg": "A database MCP server started without a read-only option exposes write tools to the model. A generated UPDATE with a wrong or missing WHERE clause is applied by the same connection that answers questions.", | |
| "fix": "Add the server's read-only flag, or connect with a role that has no write grant.", | |
| "std": "Least privilege" | |
| }, | |
| { | |
| "id": "legacy_root_key", | |
| "sev": "warn", | |
| "title": "Root key is \"mcpServers\", not \"servers\"", | |
| "msg": "Claude Desktop and several other clients read \"mcpServers\", while the VS Code mcp.json schema reads \"servers\". A file with the wrong root key parses cleanly and contributes no servers at all.", | |
| "fix": "Use \"servers\" in .vscode/mcp.json; keep \"mcpServers\" for claude_desktop_config.json.", | |
| "std": "VS Code mcp.json schema" | |
| }, | |
| { | |
| "id": "absolute_local_path", | |
| "sev": "warn", | |
| "title": "Absolute path from one developer's machine", | |
| "msg": "A path under /Users/\u003cname\u003e, /home/\u003cname\u003e or C:\\Users\\\u003cname\u003e resolves on the machine it was written on and nowhere else, so a committed config starts a server for its author and fails silently for everybody who pulls it.", | |
| "fix": "Use ${workspaceFolder} or a package name the launcher can resolve.", | |
| "std": "VS Code variable substitution" | |
| }, | |
| { | |
| "id": "duplicate_server_key", | |
| "sev": "warn", | |
| "title": "Two servers share the same name", | |
| "msg": "JSON objects keep the last of two identical keys, so the earlier server entry is dropped without a warning and the config the client runs is not the config that was reviewed.", | |
| "fix": "Give each server a distinct name.", | |
| "std": "JSON object semantics" | |
| } | |
| ] | |
| ;</script> | |
| <script>/* MCP Server Config Audit - engine | |
| The same file runs in Node (the extension) and in the browser (the free web tool). */ | |
| (function (root, factory) { | |
| var RULES = (typeof module !== 'undefined' && module.exports) | |
| ? require('./rules.json') | |
| : root.MCP_AUDIT_RULES; | |
| var api = factory(RULES); | |
| if (typeof module !== 'undefined' && module.exports) module.exports = api; | |
| root.MCP_AUDIT_ENGINE = api; | |
| })(typeof self !== 'undefined' ? self : this, function (RULES_DOC) { | |
| var AS_OF = '2026-09-14'; | |
| var RULES = Array.isArray(RULES_DOC) ? RULES_DOC : ((RULES_DOC && RULES_DOC.rules) || []); | |
| var BY_ID = {}; | |
| RULES.forEach(function (r) { BY_ID[r.id] = r; }); | |
| /* ---- strict JSON parser that remembers where every value came from ------- | |
| mcp.json is strict JSON: no comments, no trailing commas. Parsing it here | |
| rather than with JSON.parse is what lets a finding carry a line number. */ | |
| function parse(text) { | |
| var s = String(text == null ? '' : text); | |
| var i = 0, line = 1; | |
| function fail(msg) { throw { __json: true, msg: msg, line: line }; } | |
| function ws() { | |
| while (i < s.length) { | |
| var c = s[i]; | |
| if (c === '\n') { line++; i++; } | |
| else if (c === ' ' || c === '\t' || c === '\r') { i++; } | |
| else if (c === '/' && (s[i + 1] === '/' || s[i + 1] === '*')) { fail('Comments are not allowed in JSON'); } | |
| else break; | |
| } | |
| } | |
| function readString() { | |
| i++; /* opening quote */ | |
| var out = ''; | |
| while (i < s.length) { | |
| var c = s[i]; | |
| if (c === '"') { i++; return out; } | |
| if (c === '\n') fail('Unterminated string'); | |
| if (c === '\\') { | |
| var e = s[i + 1]; | |
| if (e === 'u') { out += String.fromCharCode(parseInt(s.substr(i + 2, 4), 16) || 0); i += 6; continue; } | |
| var map = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', '"': '"', '\\': '\\', '/': '/' }; | |
| out += (map[e] !== undefined ? map[e] : e); i += 2; continue; | |
| } | |
| out += c; i++; | |
| } | |
| fail('Unterminated string'); | |
| } | |
| function readNumber() { | |
| var m = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(s.slice(i)); | |
| if (!m) fail('Malformed number'); | |
| i += m[0].length; | |
| return { k: 'number', line: line, v: Number(m[0]) }; | |
| } | |
| function readObject() { | |
| var startLine = line; i++; | |
| var entries = [], seen = {}, dups = []; | |
| ws(); | |
| if (s[i] === '}') { i++; return { k: 'object', line: startLine, entries: entries, dups: dups }; } | |
| for (;;) { | |
| ws(); | |
| if (s[i] !== '"') fail('Expected a quoted property name'); | |
| var kl = line, key = readString(); | |
| ws(); | |
| if (s[i] !== ':') fail('Expected ":" after property ' + JSON.stringify(key)); | |
| i++; | |
| var node = readValue(); | |
| if (Object.prototype.hasOwnProperty.call(seen, '#' + key)) dups.push({ key: key, line: kl }); | |
| seen['#' + key] = 1; | |
| entries.push({ key: key, keyLine: kl, node: node }); | |
| ws(); | |
| if (s[i] === ',') { i++; ws(); if (s[i] === '}') fail('Trailing comma before "}"'); continue; } | |
| if (s[i] === '}') { i++; return { k: 'object', line: startLine, entries: entries, dups: dups }; } | |
| fail('Expected "," or "}"'); | |
| } | |
| } | |
| function readArray() { | |
| var startLine = line; i++; | |
| var list = []; | |
| ws(); | |
| if (s[i] === ']') { i++; return { k: 'array', line: startLine, items: list }; } | |
| for (;;) { | |
| list.push(readValue()); | |
| ws(); | |
| if (s[i] === ',') { i++; ws(); if (s[i] === ']') fail('Trailing comma before "]"'); continue; } | |
| if (s[i] === ']') { i++; return { k: 'array', line: startLine, items: list }; } | |
| fail('Expected "," or "]"'); | |
| } | |
| } | |
| function readValue() { | |
| ws(); | |
| var c = s[i]; | |
| if (c === undefined) fail('Unexpected end of file'); | |
| if (c === '{') return readObject(); | |
| if (c === '[') return readArray(); | |
| if (c === '"') { var ln = line; return { k: 'string', line: ln, v: readString() }; } | |
| if (c === '-' || (c >= '0' && c <= '9')) return readNumber(); | |
| if (s.substr(i, 4) === 'true') { i += 4; return { k: 'bool', line: line, v: true }; } | |
| if (s.substr(i, 5) === 'false') { i += 5; return { k: 'bool', line: line, v: false }; } | |
| if (s.substr(i, 4) === 'null') { i += 4; return { k: 'null', line: line, v: null }; } | |
| fail('Unexpected character ' + JSON.stringify(c)); | |
| } | |
| try { | |
| var node = readValue(); | |
| ws(); | |
| if (i < s.length) fail('Unexpected content after the end of the object'); | |
| return { ok: true, node: node }; | |
| } catch (e) { | |
| if (e && e.__json) return { ok: false, line: e.line, message: e.msg }; | |
| throw e; | |
| } | |
| } | |
| /* ---- accessors ---------------------------------------------------------- */ | |
| function ent(node, key) { | |
| if (!node || node.k !== 'object') return null; | |
| for (var i = 0; i < node.entries.length; i++) if (node.entries[i].key === key) return node.entries[i]; | |
| return null; | |
| } | |
| function sub(node, key) { var e = ent(node, key); return e ? e.node : null; } | |
| function str(node, key) { var n = sub(node, key); return n && n.k === 'string' ? n.v : null; } | |
| function items(node) { return (node && node.k === 'array') ? node.items : []; } | |
| function strItems(node) { | |
| return items(node).filter(function (n) { return n.k === 'string'; }); | |
| } | |
| function base(cmd) { | |
| return String(cmd || '').replace(/\\/g, '/').split('/').pop().toLowerCase().replace(/\.(exe|cmd|bat)$/, ''); | |
| } | |
| /* ---- credential shapes -------------------------------------------------- */ | |
| var TOKEN_SHAPES = [ | |
| [/^gh[pousr]_[A-Za-z0-9]{16,}$/, 'a GitHub token (gh*_)'], | |
| [/^github_pat_[A-Za-z0-9_]{20,}$/, 'a GitHub fine-grained token (github_pat_)'], | |
| [/^sk-ant-[A-Za-z0-9\-_]{10,}$/, 'an Anthropic API key (sk-ant-)'], | |
| [/^sk-[A-Za-z0-9\-_]{12,}$/, 'an API key (sk-)'], | |
| [/^AKIA[0-9A-Z]{16}$/, 'an AWS access key id (AKIA)'], | |
| [/^xox[baprse]-[A-Za-z0-9-]{10,}$/, 'a Slack token (xox*-)'], | |
| [/^glpat-[A-Za-z0-9\-_]{10,}$/, 'a GitLab token (glpat-)'], | |
| [/^npm_[A-Za-z0-9]{20,}$/, 'an npm token (npm_)'], | |
| [/^AIza[0-9A-Za-z\-_]{30,}$/, 'a Google API key (AIza)'], | |
| [/^ey[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\./, 'a JSON Web Token'], | |
| [/^Bearer\s+\S{12,}$/i, 'a bearer token'] | |
| ]; | |
| var SECRET_KEY = /token|secret|password|passwd|api[-_ ]?key|apikey|credential|authorization|private[-_ ]?key/i; | |
| function secretHit(key, val) { | |
| if (typeof val !== 'string' || val.indexOf('${') !== -1 || !val) return null; | |
| for (var i = 0; i < TOKEN_SHAPES.length; i++) { | |
| if (TOKEN_SHAPES[i][0].test(val)) return TOKEN_SHAPES[i][1]; | |
| } | |
| if (key && SECRET_KEY.test(key) && val.length >= 16 && !/\s/.test(val) | |
| && !/^https?:\/\//i.test(val) && /[0-9]/.test(val) && /[A-Za-z]/.test(val)) { | |
| return 'a literal value under a key named "' + key + '"'; | |
| } | |
| return null; | |
| } | |
| var DSN_CREDS = /^(postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|rediss|mssql|sqlserver|amqps?|clickhouse):\/\/([^:/?#@\s]+):([^/?#@\s]+)@/i; | |
| var DB_HINT = /postgres|mysql|mariadb|mongo|sqlite|mssql|sqlserver|clickhouse|redis|bigquery|snowflake/i; | |
| var READONLY_HINT = /read[-_ ]?only|readonly|--ro\b|access[-_ ]?mode=?restricted|allow[-_]?write=?false/i; | |
| var BROAD_ROOTS = ['/', '~', '~/', '$HOME', '${HOME}', '%USERPROFILE%', '$USERPROFILE', | |
| '/Users', '/Users/', '/home', '/home/', '/root', '/root/', '/etc', '/var']; | |
| var LOCAL_ABS = /(^|[\s"'=])(\/(?:Users|home)\/[A-Za-z0-9._-]+\/|[A-Za-z]:\\Users\\[A-Za-z0-9._-]+\\)/; | |
| var SHELLS = ['bash', 'sh', 'zsh', 'dash', 'ksh', 'cmd', 'powershell', 'pwsh']; | |
| var SHELL_FLAG = /^(-c|-lc|-ic|--command|-Command|\/c|\/C|-EncodedCommand)$/; | |
| var LOOPBACK = /^(localhost|127(?:\.\d+){3}|\[::1\]|::1|0\.0\.0\.0|host\.docker\.internal)$/i; | |
| /* docker flags that swallow the next argument, so the image is not confused with them */ | |
| var DOCKER_VALUE_FLAGS = ['-e', '--env', '--env-file', '-v', '--volume', '--mount', '--name', | |
| '-w', '--workdir', '-u', '--user', '--network', '--net', '-p', '--publish', '--entrypoint', | |
| '--label', '-l', '--platform', '--add-host', '--cap-add', '--cap-drop']; | |
| function finding(id, line, extra) { | |
| var r = BY_ID[id] || { sev: 'warn', title: id, msg: id, fix: '' }; | |
| return { | |
| check: id, | |
| sev: r.sev, | |
| title: r.title, | |
| std: r.std || '', | |
| msg: extra ? r.msg + ' ' + extra : r.msg, | |
| fix: r.fix, | |
| line: line | |
| }; | |
| } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var findings = []; | |
| var parsed = parse(text); | |
| if (!parsed.ok) { | |
| findings.push(finding('json_invalid', parsed.line || 1, 'Parser stopped at line ' + (parsed.line || 1) + ': ' + parsed.message + '.')); | |
| return wrap(findings, opts); | |
| } | |
| var root = parsed.node; | |
| if (root.k !== 'object') { | |
| findings.push(finding('json_invalid', 1, 'The top level of an MCP config is an object, not ' + root.k + '.')); | |
| return wrap(findings, opts); | |
| } | |
| /* root key */ | |
| var serversEnt = ent(root, 'servers'); | |
| var legacyEnt = ent(root, 'mcpServers'); | |
| if (legacyEnt && !serversEnt) { | |
| findings.push(finding('legacy_root_key', legacyEnt.keyLine, 'Found "mcpServers" at line ' + legacyEnt.keyLine + '.')); | |
| } | |
| var vscodeStyle = !!serversEnt; | |
| var servers = serversEnt ? serversEnt.node : (legacyEnt ? legacyEnt.node : null); | |
| /* inputs: a secret prompted without password:true is stored in the clear */ | |
| var inputs = sub(root, 'inputs'); | |
| items(inputs).forEach(function (inp) { | |
| if (inp.k !== 'object') return; | |
| if (str(inp, 'type') !== 'promptString') return; | |
| var label = (str(inp, 'id') || '') + ' ' + (str(inp, 'description') || ''); | |
| var pw = sub(inp, 'password'); | |
| if (SECRET_KEY.test(label) && !(pw && pw.v === true)) { | |
| findings.push(finding('input_not_password', (ent(inp, 'id') || { keyLine: inp.line }).keyLine, | |
| 'Input "' + (str(inp, 'id') || '(no id)') + '".')); | |
| } | |
| }); | |
| if (servers && servers.k === 'object') { | |
| servers.dups.forEach(function (d) { | |
| findings.push(finding('duplicate_server_key', d.line, 'Server name "' + d.key + '" appears twice; the later entry wins.')); | |
| }); | |
| servers.entries.forEach(function (se) { | |
| auditServer(se.key, se.keyLine, se.node, vscodeStyle, findings); | |
| }); | |
| } | |
| findings.sort(function (a, b) { return (a.line - b.line) || a.check.localeCompare(b.check); }); | |
| return wrap(findings, opts); | |
| } | |
| function auditServer(name, nameLine, srv, vscodeStyle, findings) { | |
| if (!srv || srv.k !== 'object') return; | |
| var here = function (node) { return (node && node.line) || nameLine; }; | |
| var cmdEnt = ent(srv, 'command'); | |
| var urlEnt = ent(srv, 'url'); | |
| var typeEnt = ent(srv, 'type'); | |
| var type = str(srv, 'type'); | |
| var command = str(srv, 'command'); | |
| var argNodes = strItems(sub(srv, 'args')); | |
| var args = argNodes.map(function (n) { return n.v; }); | |
| /* transport */ | |
| if ((cmdEnt && urlEnt) || (!cmdEnt && !urlEnt)) { | |
| findings.push(finding('transport_ambiguous', nameLine, | |
| 'Server "' + name + '" declares ' + (cmdEnt && urlEnt ? 'both "command" and "url"' : 'neither "command" nor "url"') + '.')); | |
| } | |
| if (vscodeStyle && !typeEnt) { | |
| findings.push(finding('type_missing', nameLine, 'Server "' + name + '".')); | |
| } | |
| if (type === 'sse') { | |
| findings.push(finding('sse_deprecated', here(typeEnt && typeEnt.node), 'Server "' + name + '".')); | |
| } | |
| /* remote endpoint */ | |
| if (urlEnt && urlEnt.node.k === 'string') { | |
| var u = urlEnt.node.v; | |
| var m = /^http:\/\/([^/:?#\s]+)/i.exec(u); | |
| if (m && !LOOPBACK.test(m[1])) { | |
| findings.push(finding('remote_plaintext_http', urlEnt.node.line, 'Server "' + name + '" points at ' + u + '.')); | |
| } | |
| } | |
| /* auto-approval lists */ | |
| ['alwaysAllow', 'autoApprove'].forEach(function (k) { | |
| var e = ent(srv, k); | |
| if (e && e.node.k === 'array' && e.node.items.length) { | |
| findings.push(finding('auto_approve', e.keyLine, | |
| 'Server "' + name + '" auto-approves ' + e.node.items.length + ' tool' + (e.node.items.length === 1 ? '' : 's') + | |
| ': ' + strItems(e.node).map(function (n) { return n.v; }).join(', ') + '.')); | |
| } | |
| }); | |
| /* env and headers: literal credentials */ | |
| ['env', 'headers'].forEach(function (k) { | |
| var o = sub(srv, k); | |
| if (!o || o.k !== 'object') return; | |
| o.entries.forEach(function (e) { | |
| if (e.node.k !== 'string') return; | |
| var what = secretHit(e.key, e.node.v); | |
| if (what) { | |
| findings.push(finding('secret_inline', e.keyLine, | |
| 'Server "' + name + '", ' + k + '.' + e.key + ' holds ' + what + '.')); | |
| } | |
| if (DSN_CREDS.test(e.node.v)) { | |
| findings.push(finding('db_url_credentials', e.keyLine, 'Server "' + name + '", ' + k + '.' + e.key + '.')); | |
| } | |
| }); | |
| }); | |
| /* args: credentials, DSNs, roots, machine-local paths */ | |
| argNodes.forEach(function (n) { | |
| var what = secretHit('', n.v); | |
| if (what) findings.push(finding('secret_inline', n.line, 'Server "' + name + '" passes ' + what + ' as a command-line argument.')); | |
| if (DSN_CREDS.test(n.v)) { | |
| var dm = DSN_CREDS.exec(n.v); | |
| findings.push(finding('db_url_credentials', n.line, | |
| 'Server "' + name + '" passes ' + dm[1] + '://' + dm[2] + ':<password>@... on the command line.')); | |
| } | |
| if (BROAD_ROOTS.indexOf(n.v) !== -1) { | |
| findings.push(finding('fs_root_broad', n.line, 'Server "' + name + '" is given the root "' + n.v + '".')); | |
| } | |
| if (LOCAL_ABS.test(n.v)) { | |
| findings.push(finding('absolute_local_path', n.line, 'Server "' + name + '": ' + n.v)); | |
| } | |
| }); | |
| if (command && LOCAL_ABS.test(command)) { | |
| findings.push(finding('absolute_local_path', cmdEnt.node.line, 'Server "' + name + '" command: ' + command)); | |
| } | |
| /* launcher-specific checks */ | |
| var exe = base(command); | |
| if (exe === 'npx' || exe === 'bunx') { | |
| var spec = firstPositional(args, ['-y', '--yes', '-q', '--quiet', '--silent', '--']); | |
| if (spec !== null && spec.value.indexOf('${') === -1 && spec.value.indexOf('@', 1) < 1) { | |
| findings.push(finding('npx_unpinned', argNodes[spec.index].line, | |
| 'Server "' + name + '" runs "' + spec.value + '" with no version.')); | |
| } | |
| } | |
| if (exe === 'uvx' || exe === 'uv') { | |
| var pinned = args.some(function (a) { return a.indexOf('==') !== -1 || /@\d/.test(a); }); | |
| var uspec = firstPositional(args, ['--from', 'tool', 'run', '-q', '--quiet']); | |
| if (!pinned && uspec !== null && uspec.value.indexOf('${') === -1) { | |
| findings.push(finding('uvx_unpinned', argNodes[uspec.index].line, | |
| 'Server "' + name + '" runs "' + uspec.value + '" with no version.')); | |
| } | |
| } | |
| if (exe === 'docker' || exe === 'podman') { | |
| var img = dockerImage(args); | |
| if (img !== null) { | |
| var tail = img.value.split('/').pop(); | |
| var tagged = tail.indexOf(':') !== -1; | |
| if (!tagged || /:latest$/.test(tail)) { | |
| findings.push(finding('docker_mutable_tag', argNodes[img.index].line, | |
| 'Server "' + name + '" runs image "' + img.value + '".')); | |
| } | |
| } | |
| } | |
| if (SHELLS.indexOf(exe) !== -1 && args.some(function (a) { return SHELL_FLAG.test(a); })) { | |
| findings.push(finding('shell_wrapper', cmdEnt.node.line, | |
| 'Server "' + name + '" is launched as ' + exe + ' ' + args.filter(function (a) { return SHELL_FLAG.test(a); })[0] + ' "...".')); | |
| } | |
| /* database servers without a read-only switch */ | |
| var envNode = sub(srv, 'env'); | |
| var envText = envNode && envNode.k === 'object' | |
| ? envNode.entries.map(function (e) { return e.key + '=' + (e.node.k === 'string' ? e.node.v : ''); }).join(' ') | |
| : ''; | |
| var blob = [name, command || '', args.join(' '), envText].join(' '); | |
| if (DB_HINT.test(blob) && !READONLY_HINT.test(blob)) { | |
| findings.push(finding('no_readonly_db', nameLine, 'Server "' + name + '" exposes a database with no read-only flag in its arguments or environment.')); | |
| } | |
| } | |
| function firstPositional(args, flags) { | |
| for (var i = 0; i < args.length; i++) { | |
| var a = args[i]; | |
| if (a.charAt(0) === '-' || flags.indexOf(a) !== -1) continue; | |
| return { index: i, value: a }; | |
| } | |
| return null; | |
| } | |
| function dockerImage(args) { | |
| var i = 0; | |
| while (i < args.length && args[i] !== 'run') i++; | |
| if (i >= args.length) return null; | |
| i++; | |
| while (i < args.length) { | |
| var a = args[i]; | |
| if (DOCKER_VALUE_FLAGS.indexOf(a) !== -1) { i += 2; continue; } | |
| if (a.charAt(0) === '-') { i++; continue; } | |
| return { index: i, value: a }; | |
| } | |
| return null; | |
| } | |
| function wrap(findings, opts) { | |
| var errors = findings.filter(function (f) { return f.sev === 'error'; }).length; | |
| return { | |
| findings: findings, | |
| summary: { | |
| rules_run: RULES.length, | |
| findings: findings.length, | |
| blocking: errors, | |
| as_of: opts.today || AS_OF | |
| } | |
| }; | |
| } | |
| return { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length }; | |
| }); | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var MCP_AUDIT_REPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = MCP_AUDIT_REPORT; | |
| else window.MCP_AUDIT_REPORT = MCP_AUDIT_REPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.MCP_AUDIT_ENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.MCP_AUDIT_REPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) run(); | |
| })(); | |
| </script> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.mcp-config-audit" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-audit',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/mcp-config-audit" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-audit',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/mcp-config-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-audit',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/mcp-config-audit" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-audit',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/mcp-config-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-audit',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.mcp-config-audit | |
| npx @readystack/mcp-config-audit <file> | |
| npx @readystack/mcp-config-audit --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 18 rules MCP Server Config Audit checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"mcp-config-audit",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Audit one mcp.json for what an agent config gives away — inline API keys, unpinned npx launches, plaintext remote servers, shell wrappers, auto-approved tools, whole-machine filesystem roots</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Freelance application-security engineers commonly bill $100-$200 an hour; a single review hour costs more than three licences, and the config changes every time somebody adds a server.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_9L57rNpRm9rHntAhalWjzrWXftaA8sLKSAxkv2n1SKA" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script> | |
| <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-17">Updated 2026-09-17</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-17","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku72740_result_card.jpg" alt="MCP Server Config Audit" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>Eight blocking findings in one mcp.json — for developers and platform teams running AI coding agents in VS Code, Cursor or Claude Desktop. The audit's shipped sample fixture _fixtures/dirty.mcp.json (33 lines, five servers — github, filesystem, postgres, ops and analytics) returns 14 findings, 8 of them blocking:</p> | |
| <p>L6 ERROR npx_unpinned — "@modelcontextprotocol/server-github" runs with no version → pin …/server-github@0.6.2 L8 ERROR secret_inline — env.GITHUB_PERSONAL_ACCESS_TOKEN holds a GitHub token → "${input:github-pat}", password: true L14 ERROR fs_root_broad — the filesystem server is given the root "/" → "${workspaceFolder}/docs" L15 ERROR auto_approve — alwaysAllow: read_file, write_file, move_file → drop the two that write L19 ERROR db_url_credentials — postgresql://app:<password>@db.internal:5432/prod on the command line → "${input:pg-dsn}" L22 ERROR shell_wrapper — launched as bash -c "…" → "command": "node" with real argument items L27 ERROR remote_plaintext_http — http://analytics.internal.example/mcp → https:// L29 ERROR secret_inline — headers.Authorization holds a bearer token → "${input:analytics-token}"</p> | |
| <p>Six more are warnings: docker_mutable_tag L19, no_readonly_db L17, type_missing L17 and L21, absolute_local_path L23, sse_deprecated L26.</p> | |
| <p>The file is valid JSON. That is the problem with agent configs: a linter confirms it parses, and never reads it for what it is, which is a permission grant. An mcp.json says which processes get spawned on your machine, with which credentials, against which directories, and which of their tools run unasked.</p> | |
| <p>Read the eight blockers in order and you have the shape of it. npx with no version, and no lockfile behind it, resolves whatever was published most recently, at every launch: the code your agent runs is chosen by whoever published last. A token written straight into env sits in a file that gets committed, screenshotted and pasted into issues, and stays valid until somebody rotates it — the VS Code schema carries an inputs array and ${input:id} substitution for exactly this reason. A filesystem root of "/" puts every SSH key, browser profile and .env file on the machine inside the agent's reach. An auto-approval list removes the confirmation step, so a tool call the model was talked into by text inside a file or a web page executes with no human in it. A DSN on the command line is a production credential echoed into process listings and agent logs at every start, and that same server has no read-only flag, so a generated UPDATE with a missing WHERE clause lands on the connection that answers questions. bash -c is a shell, with chaining and expansion, not a reviewable executable and arguments. And a plain http:// server carries the literal token from L29 across the network in clear, with every tool argument and result.</p> | |
| <p>The dated warning is L26: MCP specification revision 2025-03-26 replaced HTTP+SSE with Streamable HTTP.</p> | |
| <p>The corrected twin of the same file returns zero findings from the same 18 rules. That is the design: every rule cites what it comes from, and a finding is a line number plus its replacement, not a score.</p> | |
| <p>These files are increasingly written by the assistant about to use them, from examples where the key was inline to keep the example short. Every line individually reads correct.</p> | |
| <p>On cost: freelance application-security engineers commonly bill $100 to $200 an hour, and reading one mcp.json against credential hygiene, pinning, transport and least privilege takes most of an hour the first time.</p> | |
| <p>The free version audits one config completely — all 18 rules, every line number, every replacement, in the editor or in the browser, nothing withheld and nothing uploaded. The paid version changes scope rather than depth: every agent config in a repository and in the user-scope locations for VS Code, Cursor, Claude Desktop and Windsurf in one pass, a non-zero exit code for CI, and a dated evidence report of file, rule and line.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku72740_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku72740_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does MCP Server Config Audit actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads one mcp.json and reports every server entry that leaks a credential or grants more machine than it needs: an API key in env or headers, npx or uvx launching a package with no version, an untagged Docker image, a plaintext endpoint, a bash -c wrapper, an auto-approval list, a filesystem root of slash. On the shipped sample it returns 14 findings, 8 blocking, each with a line number.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Developers who run AI coding agents in VS Code, Cursor, Windsurf or Claude Desktop and keep an mcp.json beside the code, platform engineers who review the configs their teams commit, and security reviewers who are handed an agent config and asked whether it is safe to merge. It assumes you wrote the file quickly, possibly with an assistant.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is a free tool not enough here?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A JSON linter or a schema check says the file is valid, and it is. Nothing in valid JSON knows that npx without a version resolves the newest published package at every launch, that alwaysAllow removes the confirmation step, or that a filesystem root of slash reaches your SSH keys. Secret scanners run after you have already pushed the file.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what costs money?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free covers one file completely: all 18 rules, every line number, every replacement, in the editor and in the browser, with nothing withheld and no watermark. The paid version changes scope rather than depth. It audits every agent config in a repository and in user-scope locations at once, fails CI with a non-zero exit code, and exports a dated evidence report.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost done by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Freelance application-security engineers commonly bill $100 to $200 an hour. Reading one mcp.json against credential hygiene, package pinning, transport and least-privilege questions takes a careful reviewer most of an hour the first time, and the file changes again the next time somebody adds a server to it.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. MCP Config Lint - mcp.json and claude_desktop_config securit checks the file you open against 18 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does MCP Server Config Audit actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads one mcp.json and reports every server entry that leaks a credential or grants more machine than it needs: an API key in env or headers, npx or uvx launching a package with no version, an untagged Docker image, a plaintext endpoint, a bash -c wrapper, an auto-approval list, a filesystem root of slash. On the shipped sample it returns 14 findings, 8 blocking, each with a line number."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Developers who run AI coding agents in VS Code, Cursor, Windsurf or Claude Desktop and keep an mcp.json beside the code, platform engineers who review the configs their teams commit, and security reviewers who are handed an agent config and asked whether it is safe to merge. It assumes you wrote the file quickly, possibly with an assistant."}}, {"@type": "Question", "name": "Why is a free tool not enough here?", "acceptedAnswer": {"@type": "Answer", "text": "A JSON linter or a schema check says the file is valid, and it is. Nothing in valid JSON knows that npx without a version resolves the newest published package at every launch, that alwaysAllow removes the confirmation step, or that a filesystem root of slash reaches your SSH keys. Secret scanners run after you have already pushed the file."}}, {"@type": "Question", "name": "What is free and what costs money?", "acceptedAnswer": {"@type": "Answer", "text": "Free covers one file completely: all 18 rules, every line number, every replacement, in the editor and in the browser, with nothing withheld and no watermark. The paid version changes scope rather than depth. It audits every agent config in a repository and in user-scope locations at once, fails CI with a non-zero exit code, and exports a dated evidence report."}}, {"@type": "Question", "name": "What would this cost done by hand?", "acceptedAnswer": {"@type": "Answer", "text": "Freelance application-security engineers commonly bill $100 to $200 an hour. Reading one mcp.json against credential hygiene, package pinning, transport and least-privilege questions takes a careful reviewer most of an hour the first time, and the file changes again the next time somebody adds a server to it."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. MCP Config Lint - mcp.json and claude_desktop_config securit checks the file you open against 18 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:72740,slug:"mcp-config-audit",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/mcp-config-guard"><span class="yj-rel-t">MCP Config Guard - agent config lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/csaf-advisory-check-cra-2026"><span class="yj-rel-t">CSAF Advisory Check for CRA 2026</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/mcp-2026-migration-lint"><span class="yj-rel-t">MCP 2026 Migration Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/angular-removed-api-lint"><span class="yj-rel-t">Angular Removed-API Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autorenew-signup-lint"><span class="yj-rel-t">Auto-Renewal Signup Lint (California ARL)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body> | |
| </html> | |