Architecture
Understand secure-mcp's stdio process boundary, filesystem safety policy, progressive knowledge packs, and the layers that keep audits read-only.
Overview
secure-mcp is a local, stdio MCP server written in TypeScript. Coding agents spawn it as a subprocess and call tools to perform defensive, remediation-focused secure code review of a target repository on disk.
┌─────────────────────┐ stdio (JSON-RPC) ┌──────────────────────┐
│ Coding agent │ ◄──────────────────────► │ secure-mcp process │
│ (Codex/Claude/…) │ │ src/index.ts │
└─────────────────────┘ │ ├ root allowlist │
│ ├ McpServer │
│ └ tools/* │
└──────────┬───────────┘
│ read-only FS
▼
┌──────────────────────┐
│ target project_root │
└──────────────────────┘
Design principles
- Defensive only: identify weaknesses → classify → remediate. No exploit/PoC generation.
- Agent-first: precise tool descriptions, structured JSON, severity + confidence.
- Stateless tools: no server-side session store; the agent holds intermediate artifacts.
- Safe by default: path confinement, ignore lists, size/depth caps, no code execution.
- Stable contracts: tool names and
Findingshape should not change casually. - Light abstractions: small modules agents can extend without a framework maze.
Layers
| Layer | Path | Role |
|---|---|---|
| Entry | src/index.ts |
Configuration, diagnostics, stdio transport |
| Server | src/server.ts |
McpServer + tool registration |
| Tools | src/tools/*.ts |
MCP tool handlers (defensive descriptions) |
| Knowledge | src/knowledge/packs/ + *.ts |
Progressive packs, patterns, findings schema |
| Lib | src/lib/* |
Filesystem safety, redaction, markdown, shared types |
| Config | src/config.ts |
Env-driven limits |
Transport
v1 supports stdio only (StdioServerTransport from @modelcontextprotocol/sdk).
- Do not log to stdout (corrupts the protocol).
- Use
console.errorfor startup and failure messages.
Filesystem authorization
Process-level configuration always supplies an explicit filesystem allowlist from SECURE_MCP_ALLOWED_ROOTS. The value uses the operating system path delimiter (: on macOS/Linux, ; on Windows).
An empty allowlist does not stop knowledge-only tools from starting, but every filesystem tool rejects project_root. Configured roots and requested project roots are canonicalized before containment checks; stale entries do not grant access. Programmatic test configurations may omit the field to exercise tool behavior against temporary fixtures.
Filesystem policy
src/lib/filesystem.ts centralizes:
- Absolute root normalization
- Path traversal rejection (
resolveSafePath) - Default ignores (
node_modules,.git,dist,.next,Pods, …) - Caps: max files, max depth, max bytes per file
- Response truncation (
CHARACTER_LIMIT)
Findings contract
Defined in src/lib/types.ts and src/knowledge/findings-schema.ts:
Required remediation structure:
evidenceseverity/confidence/category(+ optionalcwe)impact_if_unremediatedremediationresidual_riskverification_suggestion
Category tools emit findings; secure_mcp_produce_findings normalises them for reports.
Progressive knowledge packs
Agents should not load every stack checklist into context. Architecture returns recommended_packs and pack_batches (chunks of ≤6 ids for secure_mcp_get_knowledge_pack). Load pack_batches[0] first with detail=summary; load later batches only if needed. Multi-pack responses fair-sample checklist items (round-robin; default max 24, hard max 60) so core/secrets priority order does not zero out stack packs. Pack responses omit the global catalog unless include_index=true.
| Pack id | Content |
|---|---|
core |
Authz, injection, secrets, crypto, logging, paths, deps |
threat-model |
Trust boundaries / STRIDE-oriented control planning |
web-next |
Next.js App Router, middleware, Server Actions, NEXT_PUBLIC_ |
web-api |
General API route/handler hardening |
auth-web |
Cookies, CSRF, web session hardening |
swift-ios |
Keychain, biometrics, ATS, WebView, deep links |
apple-desktop |
macOS entitlements, sandbox, XPC, desktop logging |
expo-rn |
SecureStore, Expo config secrets, deep links, OTA |
secrets |
Rotation, env hygiene, client-bundle exposure |
Every pack item carries the full remediation narrative (impact_if_unremediated, remediation, verification_suggestion) so agents can lift items into findings without inventing copy. Packs hold ~10–13 items each: substantial, but small enough that a complete five-pack recommendation still fits the 60-item budget in one call. truncated_by_max_items compares the returned items against the category-filtered stream, so narrow categories filters are not reported as truncation.
Stack detection is deliberately conservative (looksLikeExpoOrReactNativeApp in src/lib/filesystem.ts): Expo/React Native routing needs an Expo dependency, an expo block in app.json/app.config.*, eas.json, or a react-native dependency plus app evidence (metro/RN config or android/ + ios/). A bare app.json or a stray react-native dependency in a web/library package does not route to expo-rn. Profiling is root-scoped: Expo apps under apps/ or packages/ in a monorepo are invisible until project_root points at that package (or you force stack: "expo").
Registry + routing + fair sampling: src/knowledge/packs/registry.ts (recommendPackPlan, filterPackItems).
Scan heuristics remain in common.ts / nextjs.ts / swift.ts (used server-side by category tools without dumping full packs).
Heuristics are intentionally imperfect. Confidence fields tell agents to verify before confirming.
Out of scope (v1)
- HTTP / remote MCP transport
- Database or persistent audit history
- Full multi-language SAST
- GUI dashboard
- Executing or building the target project
- Offensive exploit development
Extension points
- Add a tool under
src/tools/and register it insrc/tools/index.tswith defensive descriptions. - Add or extend packs under
src/knowledge/packs/and register them inregistry.ts.