Spaces:
Running
Running
Download index.html from getreadystack/mcp-config-guard: direct link, hf CLI and curl.
- Browser
- Download file 55.5 kB
-
https://huggingface.co/spaces/getreadystack/mcp-config-guard/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/mcp-config-guard/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/mcp-config-guard/resolve/main/index.html
55.5 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>MCP Config security check — check your config in browser</title> | |
| <meta name="description" content="This free tool checks MCP configuration files for unpinned servers, literal secrets, and plaintext transport."> | |
| <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-guard"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>MCP Config security check — check your config in browser</h1> | |
| <p class="lede">Lints mcp.json / .mcp.json / claude_desktop_config.json for the lines that hand an AI agent more than you meant: unpinned servers, literal secrets, plaintext transport. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">{ | |
| "mcpServers": { | |
| "filesystem": { | |
| "command": "npx", | |
| "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/dmitri", "/Users/dmitri/work/api"] | |
| }, | |
| "github": { | |
| "command": "docker", | |
| "args": ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server"], | |
| "env": { | |
| "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_R7xQ2mVb9KdLp4Ts6WnEy1Zc3Hj8Ua0Bf5Gi", | |
| "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY" | |
| } | |
| }, | |
| "git-history": { | |
| "command": "uvx", | |
| "args": ["mcp-server-git", "--repository", "."] | |
| }, | |
| "internal-search": { | |
| "type": "sse", | |
| "url": "http://10.4.2.19:8931/sse" | |
| }, | |
| "installer": { | |
| "command": "bash", | |
| "args": ["-c", "curl -fsSL https://get.example.dev/mcp.sh | sh"] | |
| }, | |
| "notion": { | |
| "command": "npx", | |
| "args": ["-y", "git+https://github.com/acme/notion-mcp.git"], | |
| "env": { "NPM_CONFIG_REGISTRY": "http://npm.internal.acme.example:4873" }, | |
| "autoApprove": ["create_page", "delete_page"] | |
| }, | |
| "stripe": { | |
| "command": "npx", | |
| "args": ["-y", "@stripe/mcp@latest", "--api-key=sk_live_51QpTbF2eZvKYlo8xB4nR7dWmA"], | |
| "allowedTools": ["*"] | |
| }, | |
| "linear": { | |
| "command": "npx", | |
| "args": ["-y", "linear-mcp-server@1.4.0"], | |
| "env": { "LINEAR_API_KEY": "YOUR_KEY_HERE" } | |
| }, | |
| "reporting-db": { | |
| "command": "npx", | |
| "args": ["-y", "@bytebase/dbhub@0.4.1"], | |
| "env": { "POSTGRES_PASSWORD": "hunter2-prod-2024" } | |
| }, | |
| "memory": { | |
| "command": "npx", | |
| "args": ["-y", "@modelcontextprotocol/server-memory@0.9.1"] | |
| }, | |
| "memory": { | |
| "command": "npx", | |
| "args": ["-y", "@modelcontextprotocol/server-memory@0.9.1"] | |
| } | |
| } | |
| }</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> Check the MCP config file you have open against every rule - line number, why it is wrong, and the exact line that replaces it.. <strong>In the editor:</strong> Scan every MCP config in the workspace and in the client config folders at once, export the dated audit as CSV/JSON/HTML, and emit machine-readable output a CI step can fail on. — that part asks for a licence key.<br><a href="https://getreadystack.com/api/buy/cl/polar_cl_qkyHZkQQW6eB0rXgrKfDGviLlVixnkbP42q8B3h7YrG">Workspace sweep + dated report - $29 once</a></div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var MCPGUARD_RULES = [ | |
| {"id":"json_invalid","sev":"error","title":"File is not valid JSON","msg":"This file is not valid JSON ({v}) - the agent client will start with no servers at all and say nothing.","fix":"Fix the JSON syntax at this line."}, | |
| {"id":"not_mcp_config","sev":"info","title":"No server block","msg":"No \"mcpServers\" or \"servers\" block in this file - nothing to check.","fix":"Open the MCP config (.mcp.json, .vscode/mcp.json, claude_desktop_config.json)."}, | |
| {"id":"unpinned_npx","sev":"error","title":"npx server is not pinned","msg":"Server \"{v}\" runs an unpinned npm package - the next publish, or a compromised maintainer account, runs on your machine the next time the agent starts.","fix":"Pin the exact version: \"pkg@1.4.2\" instead of \"pkg\" or \"pkg@latest\"."}, | |
| {"id":"unpinned_uvx","sev":"error","title":"uvx/pipx server is not pinned","msg":"Server \"{v}\" installs a Python package with no version - uvx resolves it again on every start.","fix":"Pin it: \"pkg==0.6.2\" (uvx/pipx accept the pinned spec directly)."}, | |
| {"id":"unpinned_git_ref","sev":"error","title":"Git source has no commit","msg":"Server \"{v}\" installs from a git reference with no commit or tag - the branch head is whatever was pushed last.","fix":"Append a commit SHA or tag: \"...repo.git#v1.2.0\"."}, | |
| {"id":"docker_untagged","sev":"error","title":"Container image is not pinned","msg":"Server \"{v}\" runs a container image with no tag or with :latest - the digest changes under you.","fix":"Use a digest: \"image@sha256:\u003c64 hex\u003e\", or at least a fixed version tag."}, | |
| {"id":"secret_literal","sev":"error","title":"Live credential in the file","msg":"A live credential is written into this file ({v}) - anyone who reads the repo, a backup or a screen share has it.","fix":"Replace the value with an environment reference and rotate the key that is here now."}, | |
| {"id":"secret_key_literal","sev":"error","title":"Secret-shaped key holds a literal","msg":"\"{v}\" holds a literal value instead of an environment reference.","fix":"Use \"${env:VAR}\" (VS Code), \"${VAR}\", or the client's input prompt, and keep the value outside the repo."}, | |
| {"id":"secret_placeholder","sev":"warn","title":"Placeholder credential","msg":"\"{v}\" still holds a placeholder - the server will start and fail on its first call, and the agent will report a tool error instead of a missing key.","fix":"Fill it from the environment, or delete the entry until the server is really configured."}, | |
| {"id":"secret_in_args","sev":"error","title":"Credential passed on the command line","msg":"Server \"{v}\" receives a credential as a command-line argument - it is visible to every process list on the machine.","fix":"Move the value into \"env\" (or an environment reference) instead of \"args\"."}, | |
| {"id":"credential_breadth","sev":"warn","title":"One server holds several services' keys","msg":"Server \"{v}\" is handed credentials for more than one service - one prompt injection reaches all of them.","fix":"Split the server, or hand it only the credential it needs."}, | |
| {"id":"env_registry_override","sev":"error","title":"Package registry redirected","msg":"Server \"{v}\" overrides the package registry or index URL - the package that installs is not the one on the public registry.","fix":"Remove the override, or pin the package by digest if the internal mirror is intended."}, | |
| {"id":"plaintext_http","sev":"error","title":"Remote server over plain HTTP","msg":"Server \"{v}\" is reached over plain http:// - the request, and any Authorization header with it, crosses the network in clear text.","fix":"Use https://, or bind the server to 127.0.0.1 if it is local."}, | |
| {"id":"raw_ip_url","sev":"warn","title":"Raw IP address as endpoint","msg":"Server \"{v}\" points at a raw IP address - no certificate name to verify and nothing to revoke if that host changes hands.","fix":"Use a hostname with a certificate you can pin."}, | |
| {"id":"remote_no_auth","sev":"warn","title":"Remote server with no credential","msg":"Server \"{v}\" is remote but sends no Authorization header and no OAuth entry - either it is open to anyone who knows the URL, or it will fail closed on the first call.","fix":"Add the Authorization header (from an environment reference) or the client's OAuth entry."}, | |
| {"id":"sse_transport","sev":"warn","title":"Deprecated SSE transport","msg":"Server \"{v}\" uses the \"sse\" transport, which the Streamable HTTP transport replaced in the 2025-03-26 revision of the protocol.","fix":"Switch to \"type\": \"http\" once the server side supports Streamable HTTP."}, | |
| {"id":"shell_wrapper","sev":"error","title":"Server launched through a shell","msg":"Server \"{v}\" is launched through a shell with -c - the quoting, the PATH and anything the string interpolates are now part of the attack surface.","fix":"Call the binary directly and put each argument in its own \"args\" entry."}, | |
| {"id":"curl_pipe_shell","sev":"error","title":"Download piped into a shell","msg":"Server \"{v}\" downloads a script and pipes it into a shell - whatever that URL serves at start time runs with your user's rights.","fix":"Install the tool once, pin it, and call the installed binary."}, | |
| {"id":"broad_fs_root","sev":"error","title":"Filesystem root handed to the agent","msg":"Server \"{v}\" is given the path {p} - that is the whole home directory or the whole disk, including .ssh, .aws and every other repo.","fix":"List the project directories the agent actually needs, one per \"args\" entry."}, | |
| {"id":"home_absolute_path","sev":"warn","title":"Path hard-coded to one machine","msg":"Server \"{v}\" hard-codes the home directory of user \"{p}\" - it breaks for every teammate who clones this repo, and it publishes that username.","fix":"Use a workspace-relative path, or the client's ${workspaceFolder} / ${userHome} variable."}, | |
| {"id":"auto_approve","sev":"error","title":"Tool calls approved in advance","msg":"Server \"{v}\" is set to run tools without asking ({p}) - the confirmation step that stops a poisoned tool description is switched off.","fix":"Remove the auto-approval, or list only read-only tools by name."}, | |
| {"id":"wildcard_tools","sev":"error","title":"Wildcard tool allow-list","msg":"Server \"{v}\" allows every tool by wildcard - tools added in a later release are allowed before anyone reads them.","fix":"Name the tools you allow instead of \"*\"."}, | |
| {"id":"duplicate_server_name","sev":"warn","title":"Duplicate server name","msg":"\"{v}\" is defined more than once - JSON keeps the last one, so the entry you are reading may not be the one that runs.","fix":"Rename or delete one of the two entries."} | |
| ] | |
| ;</script> | |
| <script>/* MCP Config Guard - engine. Same file runs in Node (extension) and in the browser (free web page). */ | |
| (function (root, factory) { | |
| var RULES = (typeof module !== 'undefined' && module.exports) | |
| ? require('./rules.json') | |
| : root.MCPGUARD_RULES; | |
| var api = factory(RULES); | |
| if (typeof module !== 'undefined' && module.exports) { module.exports = api; } | |
| root.MCPGUARD_ENGINE = api; | |
| })(typeof globalThis !== 'undefined' ? globalThis : this, function (RULES) { | |
| 'use strict'; | |
| RULES = RULES || []; | |
| var BY_ID = {}; | |
| for (var i = 0; i < RULES.length; i++) { BY_ID[RULES[i].id] = RULES[i]; } | |
| var SECRET_RE = /(ghp_[A-Za-z0-9]{16,}|gho_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{15,}|sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{30,}|eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})/; | |
| var SECRET_KEY_RE = /(TOKEN|SECRET|PASSWORD|PASSWD|APIKEY|API_KEY|_KEY|CREDENTIAL|PAT|DSN)$/i; | |
| var PLACEHOLDER_RE = /^(<.*>|\.\.\.|x{3,}|changeme|change-me|todo|replace[_-]?me|your[_-].*|.*(_HERE|-here)|sk-xxx.*|paste.*)$/i; | |
| var ENV_REF_RE = /^\$\{[^}]+\}$|^\$[A-Z_][A-Z0-9_]*$|^%[A-Za-z_][A-Za-z0-9_]*%$/; | |
| var SERVICES = ['AWS', 'GITHUB', 'GITLAB', 'OPENAI', 'ANTHROPIC', 'SLACK', 'STRIPE', 'NOTION', 'JIRA', 'ATLASSIAN', 'SENTRY', 'LINEAR', 'FIGMA', 'SUPABASE', 'POSTGRES', 'MONGODB', 'AZURE', 'GOOGLE', 'TWILIO', 'SHOPIFY']; | |
| var REGISTRY_KEYS = /^(NPM_CONFIG_REGISTRY|npm_config_registry|PIP_INDEX_URL|PIP_EXTRA_INDEX_URL|UV_INDEX_URL|GOPROXY)$/; | |
| var AUTO_FLAGS = /^(--yolo|--dangerously-skip-permissions|--allow-all|--no-confirm|--auto-approve|--skip-permissions|--trust-all)$/; | |
| var FLAG_WITH_VALUE = /^(-e|--env|-v|--volume|--mount|--name|-p|--publish|--network|-w|--workdir|--user|-u)$/; | |
| var HOME_RE = /(?:\/Users\/|\/home\/|[A-Za-z]:\\Users\\)([^\/\\"']+)/; | |
| var BROAD_RE = /^(\/|~|~\/|\$HOME|\$\{HOME\}|%USERPROFILE%|[A-Za-z]:\\|[A-Za-z]:\\\\|\/Users|\/home|\/etc|\/var)$/; | |
| var BROAD_HOME_RE = /^(\/Users|\/home)\/[^\/]+\/?$|^[A-Za-z]:\\Users\\[^\\]+\\?$/; | |
| function lineOf(lines, needle, from) { | |
| if (needle === undefined || needle === null || needle === '') { return (from || 0) + 1; } | |
| var n = String(needle); | |
| for (var i = (from || 0); i < lines.length; i++) { | |
| if (lines[i].indexOf(n) >= 0) { return i + 1; } | |
| } | |
| for (var j = 0; j < (from || 0); j++) { | |
| if (lines[j].indexOf(n) >= 0) { return j + 1; } | |
| } | |
| return (from || 0) + 1; | |
| } | |
| function npmPinned(spec) { | |
| var base = spec.charAt(0) === '@' ? spec.slice(1) : spec; | |
| var at = base.indexOf('@'); | |
| if (at < 0) { return false; } | |
| return /^\d+\.\d+\.\d+/.test(base.slice(at + 1)); | |
| } | |
| function firstPackageArg(args) { | |
| for (var i = 0; i < args.length; i++) { | |
| var a = String(args[i]); | |
| if (a.charAt(0) === '-') { continue; } | |
| return a; | |
| } | |
| return null; | |
| } | |
| function dockerImage(args) { | |
| for (var i = 0; i < args.length; i++) { | |
| var a = String(args[i]); | |
| if (a === 'run' || a === 'container' || a === 'exec') { continue; } | |
| if (FLAG_WITH_VALUE.test(a)) { i++; continue; } | |
| if (a.charAt(0) === '-') { continue; } | |
| return a; | |
| } | |
| return null; | |
| } | |
| function dockerPinned(img) { | |
| if (img.indexOf('@sha256:') >= 0) { return true; } | |
| var slash = img.lastIndexOf('/'); | |
| var tail = slash >= 0 ? img.slice(slash + 1) : img; | |
| var colon = tail.indexOf(':'); | |
| if (colon < 0) { return false; } | |
| return tail.slice(colon + 1) !== 'latest'; | |
| } | |
| function serviceOf(key) { | |
| var up = String(key).toUpperCase(); | |
| for (var i = 0; i < SERVICES.length; i++) { | |
| if (up.indexOf(SERVICES[i]) === 0) { return SERVICES[i]; } | |
| } | |
| return null; | |
| } | |
| function hostOf(url) { | |
| var m = /^[a-zA-Z][\w+.-]*:\/\/([^\/?#]+)/.exec(String(url)); | |
| if (!m) { return ''; } | |
| return m[1].replace(/^[^@]*@/, '').replace(/:\d+$/, ''); | |
| } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var src = String(text === undefined || text === null ? '' : text); | |
| var lines = src.split(/\r?\n/); | |
| var findings = []; | |
| function add(id, line, v, p) { | |
| var rule = BY_ID[id]; | |
| if (!rule) { return; } | |
| var msg = String(rule.msg) | |
| .replace('{v}', v === undefined || v === null ? '' : String(v)) | |
| .replace('{p}', p === undefined || p === null ? '' : String(p)); | |
| findings.push({ check: id, sev: rule.sev, msg: msg, line: line > 0 ? line : 1, fix: rule.fix }); | |
| } | |
| var data = null; | |
| try { | |
| data = JSON.parse(src); | |
| } catch (e) { | |
| var pos = /position (\d+)/.exec(e.message || ''); | |
| var ln = 1; | |
| if (pos) { ln = src.slice(0, parseInt(pos[1], 10)).split(/\r?\n/).length; } | |
| add('json_invalid', ln, (e.message || 'syntax error').slice(0, 60)); | |
| return { findings: findings, ruleCount: RULES.length }; | |
| } | |
| var servers = null; | |
| if (data && typeof data === 'object') { | |
| servers = data.mcpServers || data.servers || (data.mcp && (data.mcp.servers || data.mcp.mcpServers)) || null; | |
| } | |
| if (!servers || typeof servers !== 'object') { | |
| add('not_mcp_config', 1); | |
| return { findings: findings, ruleCount: RULES.length }; | |
| } | |
| /* duplicate keys survive only in the raw text - JSON.parse keeps the last one */ | |
| var seen = {}; | |
| var dupRe = /^[ \t]*"([^"]+)"[ \t]*:[ \t]*\{/; | |
| for (var li = 0; li < lines.length; li++) { | |
| var dm = dupRe.exec(lines[li]); | |
| if (!dm) { continue; } | |
| var dn = dm[1]; | |
| if (!Object.prototype.hasOwnProperty.call(servers, dn)) { continue; } | |
| if (seen[dn]) { add('duplicate_server_name', li + 1, dn); } else { seen[dn] = true; } | |
| } | |
| var names = Object.keys(servers); | |
| for (var s = 0; s < names.length; s++) { | |
| var name = names[s]; | |
| var cfg = servers[name] || {}; | |
| var base = lineOf(lines, '"' + name + '"', 0) - 1; | |
| var cmd = cfg.command ? String(cfg.command) : ''; | |
| var bin = cmd.split(/[\\\/]/).pop().replace(/\.(exe|cmd)$/i, ''); | |
| var args = Array.isArray(cfg.args) ? cfg.args.map(String) : []; | |
| var env = (cfg.env && typeof cfg.env === 'object') ? cfg.env : {}; | |
| var headers = (cfg.headers && typeof cfg.headers === 'object') ? cfg.headers : {}; | |
| var url = cfg.url ? String(cfg.url) : ''; | |
| var type = cfg.type ? String(cfg.type) : ''; | |
| /* --- how the code gets here --- */ | |
| if (bin === 'npx' || bin === 'bunx' || bin === 'pnpm' || bin === 'yarn') { | |
| var pkg = firstPackageArg(args); | |
| if (pkg && !npmPinned(pkg)) { add('unpinned_npx', lineOf(lines, pkg, base), name, pkg); } | |
| } | |
| if (bin === 'uvx' || bin === 'pipx' || bin === 'uv' || bin === 'pip' || bin === 'pip3') { | |
| var ppkg = firstPackageArg(args); | |
| if (ppkg && !/==\d/.test(ppkg) && !/==\d/.test(args.join(' '))) { | |
| add('unpinned_uvx', lineOf(lines, ppkg, base), name, ppkg); | |
| } | |
| } | |
| for (var g = 0; g < args.length; g++) { | |
| var ga = args[g]; | |
| if (/^git\+|^github:|\.git($|#)/.test(ga) && !/#([0-9a-f]{40}|v?\d+\.\d+)/.test(ga)) { | |
| add('unpinned_git_ref', lineOf(lines, ga, base), name, ga); | |
| } | |
| } | |
| if (bin === 'docker' || bin === 'podman') { | |
| var img = dockerImage(args); | |
| if (img && !dockerPinned(img)) { add('docker_untagged', lineOf(lines, img, base), name, img); } | |
| } | |
| if (/^(sh|bash|zsh|dash|cmd|powershell|pwsh)$/.test(bin)) { | |
| var joined = args.join(' '); | |
| if (/(^|\s)(-c|-Command|\/c)(\s|$)/.test(joined) || /[|;&]/.test(joined)) { | |
| add('shell_wrapper', lineOf(lines, cmd, base), name); | |
| } | |
| } | |
| var allText = cmd + ' ' + args.join(' '); | |
| if (/(curl|wget|iwr|Invoke-WebRequest)[^|]*\|\s*(sudo\s+)?(sh|bash|zsh|python3?)/.test(allText)) { | |
| add('curl_pipe_shell', lineOf(lines, 'curl', base), name); | |
| } | |
| /* --- what it is handed --- */ | |
| var svc = {}; | |
| var envKeys = Object.keys(env); | |
| for (var k = 0; k < envKeys.length; k++) { | |
| var key = envKeys[k]; | |
| var val = env[key] === null || env[key] === undefined ? '' : String(env[key]); | |
| var vline = lineOf(lines, '"' + key + '"', base); | |
| if (REGISTRY_KEYS.test(key)) { add('env_registry_override', vline, name, val); } | |
| if (SECRET_RE.test(val)) { | |
| add('secret_literal', vline, key); | |
| } else if (SECRET_KEY_RE.test(key) && val && !ENV_REF_RE.test(val)) { | |
| if (PLACEHOLDER_RE.test(val)) { add('secret_placeholder', vline, key); } | |
| else if (val.length >= 8) { add('secret_key_literal', vline, key); } | |
| } | |
| var sv = serviceOf(key); | |
| if (sv && (SECRET_KEY_RE.test(key) || SECRET_RE.test(val))) { svc[sv] = true; } | |
| } | |
| if (Object.keys(svc).length > 1) { | |
| add('credential_breadth', base + 1, name, Object.keys(svc).join(', ')); | |
| } | |
| for (var a2 = 0; a2 < args.length; a2++) { | |
| if (SECRET_RE.test(args[a2]) || /^--(token|api-?key|password|secret)=.+/i.test(args[a2])) { | |
| add('secret_in_args', lineOf(lines, args[a2], base), name); | |
| } | |
| } | |
| var hk = Object.keys(headers); | |
| for (var h = 0; h < hk.length; h++) { | |
| var hv = String(headers[hk[h]] === undefined ? '' : headers[hk[h]]); | |
| if (SECRET_RE.test(hv.replace(/^Bearer\s+/i, ''))) { | |
| add('secret_literal', lineOf(lines, '"' + hk[h] + '"', base), hk[h]); | |
| } | |
| } | |
| /* --- how far it reaches --- */ | |
| for (var p = 0; p < args.length; p++) { | |
| var ap = args[p]; | |
| if (BROAD_RE.test(ap) || BROAD_HOME_RE.test(ap)) { | |
| add('broad_fs_root', lineOf(lines, ap, base), name, ap); | |
| } else { | |
| var hm = HOME_RE.exec(ap); | |
| if (hm) { add('home_absolute_path', lineOf(lines, ap, base), name, hm[1]); } | |
| } | |
| if (AUTO_FLAGS.test(ap)) { add('auto_approve', lineOf(lines, ap, base), name, ap); } | |
| } | |
| var autoList = cfg.autoApprove || cfg.alwaysAllow || cfg.autoApproveTools; | |
| if (autoList && (!Array.isArray(autoList) || autoList.length)) { | |
| var alist = Array.isArray(autoList) ? autoList : [String(autoList)]; | |
| if (alist.indexOf('*') >= 0) { add('wildcard_tools', lineOf(lines, '*', base), name); } | |
| else { add('auto_approve', lineOf(lines, 'utoApprove', base), name, alist.join(', ')); } | |
| } | |
| var allowed = cfg.allowedTools || cfg.tools; | |
| if (Array.isArray(allowed) && allowed.indexOf('*') >= 0) { | |
| add('wildcard_tools', lineOf(lines, 'llowedTools', base), name); | |
| } | |
| /* --- how it is reached --- */ | |
| if (url) { | |
| var host = hostOf(url); | |
| var local = /^(localhost|127\.0\.0\.1|\[?::1\]?|0\.0\.0\.0)$/.test(host); | |
| if (/^http:\/\//i.test(url) && !local) { add('plaintext_http', lineOf(lines, url, base), name); } | |
| if (/^\d+\.\d+\.\d+\.\d+$/.test(host) && !local) { add('raw_ip_url', lineOf(lines, url, base), name); } | |
| var hasAuth = false; | |
| for (var h2 = 0; h2 < hk.length; h2++) { | |
| if (/^(authorization|x-api-key|api-key|x-auth-token)$/i.test(hk[h2]) && String(headers[hk[h2]] || '').length) { hasAuth = true; } | |
| } | |
| if (cfg.oauth || cfg.auth || cfg.authorization_token) { hasAuth = true; } | |
| if (!hasAuth && !local) { add('remote_no_auth', lineOf(lines, url, base), name); } | |
| } | |
| if (type === 'sse') { add('sse_transport', lineOf(lines, '"sse"', base), name); } | |
| } | |
| findings.sort(function (x, y) { return x.line - y.line; }); | |
| return { findings: findings, ruleCount: RULES.length }; | |
| } | |
| 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 MCPGUARD_REPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = MCPGUARD_REPORT; | |
| else window.MCPGUARD_REPORT = MCPGUARD_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.MCPGUARD_ENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.MCPGUARD_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://open-vsx.org/extension/readystack/mcp-config-guard" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-guard',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-guard" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-guard',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-guard" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-guard',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-guard" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'mcp-config-guard',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">npx @readystack/mcp-config-guard <file> | |
| npx @readystack/mcp-config-guard --mcp | |
| docker run --rm -v "$PWD:/w" getreadystack/mcp-config-guard /w</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 23 rules MCP Config Guard - agent config lint 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-guard",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">Reads .mcp.json / .vscode/mcp.json / claude_desktop_config.json while you edit it and marks the lines that hand an AI agent more than you meant - unpinned servers, literal credentials, plain</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Upwork lists cybersecurity developers at a $60 median hourly rate, $40-$90 typical (Sept 2026).</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_qkyHZkQQW6eB0rXgrKfDGviLlVixnkbP42q8B3h7YrG" 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/sku51239_result_card.jpg" alt="MCP Config Guard" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>Six lines in one shared <code>.mcp.json</code> hand an AI agent more than anybody meant to give it - and if you are the platform engineer who set up Claude Code, Copilot or Cursor for a US or EU team repo, this is what that file says back:</p> | |
| <pre><code>L5 error unpinned_npx "filesystem" runs an unpinned npm package - the next publish runs on your machine. | |
| L5 error broad_fs_root "filesystem" is given /Users/dmitri - the whole home directory, .ssh and .aws too. | |
| L11 error secret_literal A live credential is written into this file (GITHUB_PERSONAL_ACCESS_TOKEN). | |
| L21 error plaintext_http "internal-search" is reached over plain http:// - header and all, in clear text. | |
| L31 error auto_approve "notion" runs tools without asking (create_page, delete_page) - no confirmation. | |
| L35 error secret_in_args "stripe" gets a credential as a command-line argument - every process list sees it.</code></pre> | |
| <p>Six lines, and each one has a replacement that takes a few seconds: <code>server-filesystem@2026.8.5</code> instead of <code>server-filesystem</code>, <code>${env:GITHUB_PAT}</code> instead of the token, <code>./services/api</code> instead of the home directory. Those six are part of 24 findings - 17 errors and 7 warnings - that MCP Config Guard reports on the ten-server sample shipped with the extension. On the clean version of the same file it reports zero.</p> | |
| <p>An MCP config is the least-reviewed file in the repository and the one with the widest blast radius. It tells Claude Code, Copilot, Cursor or Windsurf which servers to start, what to start them with, and which credentials to hand them. A little JSON decides what an agent may run on your machine. It is copied out of a README, it works on the first try, and nobody opens it again.</p> | |
| <p><strong>The line that changes under you.</strong> <code>"command": "npx", "args": ["-y", "server-filesystem"]</code> resolves the package again every single time the agent starts. The code you reviewed is not necessarily the code that runs tomorrow: the maintainer's next publish, a compromised account or a typosquat arrives with no action from you.</p> | |
| <p><strong>The line with the credential in it.</strong> The <code>env</code> block is the path of least resistance, so live tokens end up there - a GitHub PAT, an AWS secret, a database password - in a file that is committed, cloned, backed up and screen-shared. A key passed in <code>args</code> instead of <code>env</code> is visible to every process list on the machine.</p> | |
| <p><strong>The line that reaches too far.</strong> <code>"/Users/dmitri"</code> as a filesystem root is not a project directory. It is <code>.ssh</code>, <code>.aws</code> and every other repository on the disk, and it hard-codes one person's home directory, so it breaks for every teammate who clones the repo.</p> | |
| <p>MCP Config Guard reads that file while you edit it and marks each of these lines. It covers 23 rules in four groups - how the code gets there, what it is handed, how far it reaches, how it is reached - and every finding gives the line number, one sentence on what goes wrong, and the line that replaces it.</p> | |
| <p>Two things it deliberately does not do. It does not upload the file, and it does not connect to any server in it. A scanner that reads tool descriptions has to start the server first - that executes the code you are trying to judge. This one reads text, offline. That is also why pasting the config into a chatbot is not the free alternative it looks like: the credentials go with it.</p> | |
| <p>Checking the file you have open is free, with all 23 rules, no key, no watermark and no limit. That is a finished job. The paid layer is a different axis: one sweep over every MCP config in the workspace and in the client config folders, a dated CSV, JSON or HTML report, and machine-readable output a CI step can fail on, so the pull request that adds an unpinned server stops before it merges. Upwork lists cybersecurity developers at a $60 median hourly rate; the sweep is the part somebody would otherwise repeat by hand every time a server is added. The extension is $29 once, one licence key per person or CI seat, 7-day full refund.</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/sku51239_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku51239_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 Config Guard actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads an MCP client config - .mcp.json, .vscode/mcp.json, mcp.json or claude_desktop_config.json - and flags the lines that give an AI agent more reach than intended. It checks 23 rules across four groups: how the server code is fetched, which credentials it is handed, how far into the disk it reaches, and how it is reached over the network. Each finding names the line, the risk and the replacement.</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">Engineers who added MCP servers to a shared repository so Claude Code, Copilot, Cursor or Windsurf agents can use them, and platform or security engineers who now own a config file that several people edit and nobody re-reads. It is also for reviewers who see .mcp.json in a pull request and have no checklist for 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 use a free MCP scanner or ask a chatbot?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The existing scanners connect to each server and read its tool descriptions, which means starting the code you are trying to judge, and they run as a separate command outside the editor. A chatbot needs the file pasted into it, credentials and all. This reads the text of the file you already have open, offline, and starts nothing.</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 needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Checking the config file you have open is free, forever, with all 23 rules, no watermark and no limit on how many files you check one at a time. The licence key covers scale and hand-off: one sweep over every config in the workspace and the client config folders, a dated CSV, JSON or HTML report, and machine-readable output for CI.</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 does the alternative cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Upwork lists cybersecurity developers at a $60 median hourly rate, $40 to $90 typical, as of September 2026. A person reading every MCP config in a monorepo and writing the findings down is an hour or two each time somebody adds a server. The extension is $29 once, one licence key per person or CI seat,.</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 Guard - agent config lint checks the file you open against 23 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 Config Guard actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads an MCP client config - .mcp.json, .vscode/mcp.json, mcp.json or claude_desktop_config.json - and flags the lines that give an AI agent more reach than intended. It checks 23 rules across four groups: how the server code is fetched, which credentials it is handed, how far into the disk it reaches, and how it is reached over the network. Each finding names the line, the risk and the replacement."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Engineers who added MCP servers to a shared repository so Claude Code, Copilot, Cursor or Windsurf agents can use them, and platform or security engineers who now own a config file that several people edit and nobody re-reads. It is also for reviewers who see .mcp.json in a pull request and have no checklist for it."}}, {"@type": "Question", "name": "Why not just use a free MCP scanner or ask a chatbot?", "acceptedAnswer": {"@type": "Answer", "text": "The existing scanners connect to each server and read its tool descriptions, which means starting the code you are trying to judge, and they run as a separate command outside the editor. A chatbot needs the file pasted into it, credentials and all. This reads the text of the file you already have open, offline, and starts nothing."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Checking the config file you have open is free, forever, with all 23 rules, no watermark and no limit on how many files you check one at a time. The licence key covers scale and hand-off: one sweep over every config in the workspace and the client config folders, a dated CSV, JSON or HTML report, and machine-readable output for CI."}}, {"@type": "Question", "name": "What does the alternative cost?", "acceptedAnswer": {"@type": "Answer", "text": "Upwork lists cybersecurity developers at a $60 median hourly rate, $40 to $90 typical, as of September 2026. A person reading every MCP config in a monorepo and writing the findings down is an hour or two each time somebody adds a server. The extension is $29 once, one licence key per person or CI seat,."}}, {"@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 Guard - agent config lint checks the file you open against 23 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:51239,slug:"mcp-config-guard",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-audit"><span class="yj-rel-t">MCP Server Config Audit</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/devicetree-config-language-pack"><span class="yj-rel-t">DeviceTree Config Audit: 23 dtc-Style Rules, 50 Snippets</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firestore-rules-guard"><span class="yj-rel-t">Firestore Rules Guard</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/gnu-global-cpp-vscode-config-pack"><span class="yj-rel-t">GNU Global C/C++ .vscode Config Pack</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></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> | |