Skip to content
secure-mcp
Esc
navigateopen⌘Jpreview
On this page

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

  1. Defensive only: identify weaknesses → classify → remediate. No exploit/PoC generation.
  2. Agent-first: precise tool descriptions, structured JSON, severity + confidence.
  3. Stateless tools: no server-side session store; the agent holds intermediate artifacts.
  4. Safe by default: path confinement, ignore lists, size/depth caps, no code execution.
  5. Stable contracts: tool names and Finding shape should not change casually.
  6. 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

The server speaks the stateless MCP protocol revision 2026-07-28 over stdio via serveStdio from @modelcontextprotocol/server/stdio (which owns a StdioServerTransport under the hood): no initialize handshake or session ID — each request is self-contained. The entry runs with legacy: "reject", so 2025-era initialize openings are answered with the unsupported-protocol-version error and never served.

  • Do not log to stdout (corrupts the protocol).
  • Use console.error for 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:

  • evidence
  • severity / confidence / category (+ optional cwe)
  • impact_if_unremediated
  • remediation
  • residual_risk
  • verification_suggestion

Category tools emit findings; secure_mcp_produce_findings normalises them for reports. Candidate dispositions include fixed for revalidated remediations (counted, but not prioritised over open work). Architecture responses include typed surfaces and coverage gaps so agents can prioritise entrypoints and sample zero-hit high-value paths; the architecture result is the security brief (no separate brief tool).

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. Next.js is claimed only from next.config.* or a next dependency — a top-level app/ or pages/ directory is not enough. The TypeScript/JS stack is claimed from .ts/.tsx/.js/.jsx files or a tsconfig.json/jsconfig.json; a package.json alone is not enough, but a plain-JS service still routes to the TS/JS packs. 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

  • 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

  1. Add a tool under src/tools/ and register it in src/tools/index.ts with defensive descriptions.
  2. Add or extend packs under src/knowledge/packs/ and register them in registry.ts.

Was this page helpful?