mcp-code-analysis
Enables scanning public GitHub repositories for code quality and security issues by cloning the repository and running Ruff and Semgrep static analysis.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-code-analysisanalyze this Python snippet for security issues: def auth(p): return p == 'secret'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Code Analysis Server (SSE transport)
A FastAPI/Starlette web server exposing a Model Context Protocol (MCP) server
over HTTP Server-Sent Events, with two tools backed by ruff and semgrep:
analyze_code_snippet(code_content, filename)— scans a raw source string.analyze_github_repo(repo_url)— shallow-clones a public GitHub repo and scans it.
Endpoints
GET /sse— opens the SSE stream; the server immediately emits anendpointevent telling the client where to POST JSON-RPC messages (/messages/?session_id=...).POST /messages/— client sends JSON-RPC tool calls here; responses come back over the open/ssestream, per the MCP spec.GET /healthz— plain liveness check for your hosting platform.
Related MCP server: SonarLint MCP Server
Run locally
pip install -r requirements.txt
export BEARER_TOKENS="dev-token-please-change"
python main.py # listens on :8000 (or $PORT)
curl http://localhost:8000/healthz
curl -H "Authorization: Bearer dev-token-please-change" http://localhost:8000/sseWithout BEARER_TOKENS set, the server refuses every request except
/healthz with a 503 — it will not silently run open. For quick local
testing without a token, set ALLOW_NO_AUTH=1 instead of BEARER_TOKENS.
Deploy
docker build -t mcp-code-analysis .
docker run -p 8000:8000 mcp-code-analysisWorks as-is on Render, Railway, Fly.io, or an EC2 instance with the container
runtime of your choice. Set PORT if your platform injects a different port.
Fly.io test deployment
The included fly.toml targets petsan-mcp-code-analysis in Los Angeles,
with one shared CPU and 1 GB RAM. The machine stops when idle and starts on
incoming requests. Fly.io usage charges apply.
After signing in with fly auth login, deploy updates from this directory:
fly deploy --remote-only --ha=falseSet BEARER_TOKENS through Fly secrets before the first deployment. Keep
the token out of Git. The deployed endpoints are:
The MCP client must support SSE and send Authorization: Bearer <token>
on both the SSE connection and message POSTs. A browser demo is available at
https://petsan-mcp-code-analysis.fly.dev/ (and at /sse for HTML requests).
Paste the bearer token into the demo to run a live snippet scan.
The setuptools pin preserves pkg_resources, which the pinned Semgrep
OpenTelemetry dependency still imports; setuptools 82 and later removed it.
Security model — read before exposing this publicly
This server does not execute untrusted code. ruff and semgrep are
static analyzers: they parse and pattern-match text, they never run it. That
is what makes it reasonable to hand them untrusted input directly inside a
plain subprocess, with no container-per-request sandbox.
Access control
Two independent layers gate every request except /healthz:
Bearer-token authentication (
BearerAuthMiddlewareinmain.py) — a raw ASGI middleware, notBaseHTTPMiddleware, specifically becauseBaseHTTPMiddlewareis known to interfere with long-lived streaming responses; this was confirmed safe against a real/ssestream during testing. It rejects any request to/sseor/messages/that doesn't carryAuthorization: Bearer <token>matching one ofBEARER_TOKENS(comma-separated, so you can rotate tokens with zero downtime), before the request reaches the SSE transport, the MCP session, or any subprocess. An unauthorized caller cannot consume a connection slot, spawn aruff/semgrepprocess, or trigger agit clone— the request is turned away at the door.Set
BEARER_TOKENSin production. If it's unset, the server responds503to everything except/healthzrather than running open — this is deliberate fail-closed behavior so a missing env var can't silently ship an unauthenticated server.For local development only, set
ALLOW_NO_AUTH=1to skip the token check entirely.
Host/Origin validation (
TransportSecuritySettings, native to themcpSDK) — mitigates DNS-rebinding attacks, where a malicious webpage running in a victim's browser tries to reach this server vialocalhostor an internal hostname. SetALLOWED_HOSTSto a comma-separated allow- list (e.g.myapp.onrender.com) to enable it; confirmed by testing that a mismatchedHostheader is rejected with421even when the bearer token is valid. Left unset by default since most platform routers (Render, Railway, etc.) already terminate on a fixed hostname before proxying here, making this a defense-in-depth layer rather than the primary control — the bearer token is the primary control.Known cosmetic side effect: a request rejected by this layer causes the underlying SDK to raise a
ValueErrorafter it has already sent the rejection response, which uvicorn logs as an "Exception in ASGI application" traceback. Confirmed by testing that this does not corrupt the response, affect the client, or impact subsequent requests — it's inherent to the pinned SDK's control flow (see the comment abovehandle_sseinmain.pyfor why it's intentionally left unhandled rather than "fixed" in a way that risks a worse bug).
What's actually enforced beyond access control:
Ephemeral files only. Snippets are written to a fresh
tempfile.mkdtemp()directory, scanned, and deleted in afinally:block — including on exceptions and timeouts.Filename allow-list.
filenamemust match^[A-Za-z0-9_.\-]{1,255}$and must not contain..— no path traversal, no absolute paths.Repo URL allow-list.
repo_urlmust matchhttps://github.com/<owner>/<repo>exactly — no other hosts, no SSH URLs, no query params that could smuggle git options.Clone hardening. Clones use
--depth=1 --single-branch --no-tagsand-c core.hooksPath=/dev/null, and.git/is deleted immediately after clone — before any scanner touches the checkout — so no hook, alternate, or packed-ref content is ever reachable by the analyzers.Size/file-count caps.
MAX_REPO_FILES(default 2000) andMAX_REPO_BYTES(default 200MB) are checked before scanning; oversized repos are rejected outright.Timeouts. Every subprocess (clone, ruff, semgrep) runs under
asyncio.wait_forwith a hard timeout (SUBPROCESS_TIMEOUT_SECONDS,CLONE_TIMEOUT_SECONDS, default 30s each) and is killed on expiry.Non-root container user. The Docker image drops to uid 1000 before running the app.
What this is not
This is process-level isolation, not sandbox-level isolation — there is no gVisor/Firecracker/VM boundary between the analyzer subprocess and the host. That's an acceptable trade-off for two static analyzers, but:
Do not add a "run this code" / arbitrary-execution tool to this server without first putting a real per-request sandbox in front of it. The ephemeral-tempfile pattern used here is not sufficient once the payload is actually executed rather than parsed.
Semgrep runs against a bundled, fully offline ruleset (
semgrep-rules.yml) by default — no outbound network access required. Every named registry config (auto,p/ci,p/security-audit, etc.) requires fetching from semgrep.dev at scan time; semgrep ships with no rulesets built in. Extendsemgrep-rules.ymlwith more rules, or setSEMGREP_CONFIGto a registry name if outbound access is available and you want semgrep's maintained rulesets instead.There's no per-tool authorization (e.g. different tokens with different scopes) — every valid bearer token can call every tool. If you need per-caller restrictions, add that logic in
BearerAuthMiddlewareor incall_tool().The per-request MCP session pattern (
app_server.run()invoked insidehandle_sse) follows the SDK's own documented example and is correct for this transport, where each SSE connection is an independent session with no shared server-loop state. Graceful shutdown behavior under SIGTERM during a platform redeploy was not specifically load-tested here — if your platform does rolling deploys under real traffic, verify in-flight SSE connections drain the way you expect.
Tested
Full auth matrix verified with real HTTP requests against the running server:
/healthzopen with no token;/sseand/messages/return401with no token, wrong token, or a malformedAuthorizationheader; both entries in a comma-separatedBEARER_TOKENSlist accepted (confirms rotation works); server returns503on every non-health route whenBEARER_TOKENSis unset andALLOW_NO_AUTHisn't set (fail-closed, not fail-open);ALLOW_NO_AUTH=1correctly bypasses the check for local dev.ALLOWED_HOSTS/TransportSecuritySettingsverified with a real mismatchedHostheader — rejected with421even with a valid bearer token; a matchingHostheader is accepted.Confirmed the real SSE stream still completes its handshake (
event: endpoint) correctly with the auth middleware in front of it — the middleware doesn't break or buffer the streaming response.main.pyimports cleanly and both tools were exercised directly (valid input, path-traversal filename rejection, reserved-filename rejection, non-GitHub URL rejection, temp directory cleanup verified).The live ASGI app was booted with
uvicornandGET /healthzandGET /sse(confirming the SSEendpointhandshake) were verified over real HTTP.Both
ruffandsemgrepwere run for real against deliberately flawed code, using the bundled offline ruleset — confirmed real findings (shell injection, hardcoded credential, unused import, etc.) parse correctly.Confirmed ruff is skipped entirely for non-
.pyfiles/single-file scans, and only targets.pyfiles within a repo scan — this avoids a real bug found during testing where ruff reports a spuriousE902"finding" on binary/non-UTF8 files it never actually linted.Confirmed semgrep's
errors[]field (e.g. a failed rule-config download, which it reports with exit code 0) is surfaced as a scan error rather than silently producing an empty, seemingly-clean findings list.Confirmed semgrep exits cleanly (code 0, empty
results/errors) when scanning a file type outside the bundled ruleset'slanguages:list (e.g..md,.yaml, a plainDockerfile) — it skips unrecognized files rather than erroring, so no extra flag is needed for that case.Confirmed ruff's directory-scan mode (used for repo scans) discovers and lints only
.py/.pyifiles on its own and silently ignores everything else in the tree, including raw binary content — tested with a mixed directory of.py,.md, and binary files. Repo scans pass the directory straight to ruff rather than enumerating file paths, which also avoids any risk of hitting an OS argument-length limit on very large repos.Package pins in
requirements.txtwere confirmed to exist and install from PyPI.Not tested here: the Docker build itself (no
dockerbinary available in this sandbox) — every dependency it installs was verified individually instead.
This server cannot be deployed
Maintenance
Related MCP Connectors
Enable secure connectivity between Sentry issues and debugging data, and LLM clients, using a Model Context Protocol (MCP) server.
Zero-install security baseline for AI coding agents — OWASP/CWE-cited rules over MCP.
Zero-config MCP security scanner for AI-generated apps. 25K+ vulnerability patterns.
Deep security scans of repos you own from your editor: dependency CVEs, SAST, git-history secrets.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables integration of Semgrep in development environments via the MCP protocol, supporting static code analysis, rule management, and scan result operations.2MIT
- AlicenseAqualityCmaintenanceEnables real-time code analysis for JavaScript, TypeScript, and Python through Claude Desktop and other MCP clients, detecting bugs, code smells, and security vulnerabilities with automated quick fixes.794 npm4MIT
- FlicenseNot gradedqualityDmaintenanceLocal MCP server that scans code for security issues (secrets, dependencies, configurations, risky patterns) and integrates with GitHub Copilot in VS Code for automated pre-commit reviews.-
- AlicenseAqualityBmaintenanceEnables triage of SAST findings by exposing a read-only MCP server with tools to access hash-verified source-to-sink code slices, unguarded sinks, and layered enrichment for local LLM analysis.10MIT