Graph Server

The Graph Server is an MCP server that builds a structural graph of your code and makes it queryable by any MCP client. No LLM inside — pure deterministic AST analysis.

Getting Started

No Docker and no database server. The default backend is GraphQLite — an embedded SQLite graph that persists to ~/.codesteward/graph.db between sessions. Requires uv (Python 3.12+).

1. Set up once, machine-wide

Auto-detects your installed AI tools (Claude Code, Cursor, Cline, Codex CLI, Gemini CLI), registers the MCP server globally, and merges workflow instructions into your existing config files — nothing is overwritten.

uvx --from "codesteward-mcp[graph-all,graphqlite]" codesteward-mcp setup

To remove everything again, run the same command with --uninstall.

2. Open any repo and ask

No .mcp.json, no per-project config, no repeated setup. The agent builds the graph on first use and queries it from then on.

# Any repository on your machine
cd /path/to/your/project
claude   # or: cursor, cline, codex, gemini

# The agent calls these for you:
graph_status(repo_id="my-project")
graph_rebuild(repo_path="/path/to/your/project", repo_id="my-project")
codebase_graph_query(query_type="referential", query="authenticate", repo_id="my-project")

Manual setup

If you would rather configure it yourself, add the server to your MCP client config directly. uvx downloads and caches the package on first run.

{
  "mcpServers": {
    "codesteward-graph": {
      "type": "stdio",
      "command": "uvx",
      "args": [
        "--from", "codesteward-mcp[graph-all,graphqlite]",
        "codesteward-mcp", "--transport", "stdio"
      ]
    }
  }
}

Note: Claude Code requires "type": "stdio" in the server config. Cursor and Cline do not need it.

MCP Tools

BUILDgraph_rebuild

Build or incrementally update the structural graph for any codebase. Parses 14 languages with tree-sitter. Supports full rebuild or fast incremental mode for changed files only.

repo_pathstringAbsolute path to the repository root
changed_filesstring[] | nullIncremental mode: re-parse only these files. null = full rebuild.
tenant_idstringNamespace for multi-repo isolation
await graph_rebuild({
  repo_path: "/workspace",
  changed_files: null   // null = full rebuild
})
QUERYcodebase_graph_query

Run any Cypher query against the structural graph. Get call chains, inheritance trees, blast radius, or auth coverage — all deterministic, all instant.

cypherstringCypher query to execute against the graph
repo_idstringTarget repository identifier
tenant_idstringTenant namespace
await codebase_graph_query({
  cypher: `
    MATCH (caller)-[:CALLS*1..5]->(fn:Function)
    WHERE fn.name = "processPayment"
    RETURN caller.name, caller.file
    ORDER BY caller.file
  `,
  repo_id: "payments-service"
})
ENRICHgraph_augment

Add agent-inferred edges to the graph — relationships you detected that the parser cannot derive statically. Always confidence < 1.0. Never used by the policy engine.

edgesAugmentEdge[]Inferred edges with confidence scores (0.0–0.99)
repo_idstringTarget repository
tenant_idstringTenant namespace
await graph_augment({
  edges: [{
    source_id: "fn::validatePayment",
    edge_type: "calls",
    target_name: "stripe.charges.create",
    confidence: 0.85,
    rationale: "Dynamic dispatch via SDK client"
  }],
  repo_id: "payments-service"
})
SECURITYtaint_analysis

Trace how untrusted input propagates to dangerous sinks across the codebase. Writes TAINT_FLOW edges to the graph. Requires codesteward-taint to be installed on PATH.

repo_idstringRepository identifier
frameworksstring[] | nullFramework catalog names — omit to auto-detect
level1 | 2 | 3Maximum analysis depth (default: 3)
await taint_analysis({
  repo_id:    "payments-service",
  frameworks: ["fastapi"],  // or null to auto-detect
  level:      3
})

Edge Types

IMPORTSModule and package import relationshipsdeterministic
CALLSFunction and method invocations, cross-file resolveddeterministic
EXTENDSClass inheritance and interface implementationdeterministic
GUARDED_BYAuth guards on the function itself — decorators, annotations, Depends() injectiondeterministic
PROTECTED_BYRouter/middleware scope guards — FastAPI APIRouter(dependencies=[...]) and Express router.use(middleware)deterministic
DEPENDS_ONPackage-level dependencies from manifest filesdeterministic
TAINT_FLOWTaint paths from user-controlled sources to dangerous sinks — written by codesteward-taintdeterministic
CUSTOMAgent-inferred edges with explicit confidence scoresinferred

Supported Languages

Every supported language contributes the same structural edges to the same graph. Polyglot monorepos get a single queryable model.

PythonTypeScriptJavaScriptJavaKotlinC#C++PHPGoRubyRustScalaSwiftTSX / JSX

Cypher Query Examples

Blast radius — who calls this function transitively?
MATCH (caller)-[:CALLS*1..5]->(fn:Function {name: "processPayment"})
RETURN caller.name, caller.file, caller.line_start
ORDER BY caller.file
Auth coverage — route handlers without auth guard
MATCH (fn:Function)
WHERE fn.name =~ "(?i).*(route|handler|controller).*"
  AND NOT (fn)-[:CALLS|GUARDED_BY]->()
RETURN fn.name, fn.file, fn.line_start
Inheritance tree — all subclasses of a base class
MATCH (child)-[:EXTENDS*1..]->(base:Class {name: "BaseController"})
RETURN child.name, child.file, child.language
ORDER BY child.language, child.file

Backends

GRAPH_BACKEND defaults to auto: Neo4j if a password is set, JanusGraph if its URL is changed, otherwise GraphQLite. For a laptop, the default needs no configuration at all.

GraphQLite default

Embedded SQLite. No external service, no server process. Persists to ~/.codesteward/graph.db — override with GRAPHQLITE_DB_PATH.

Neo4j

A shared, persistent graph for teams and CI. Requires a running Neo4j 5+ server; setting the password is what activates it.

NEO4J_URI=bolt://your-host:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your-password
JanusGraph

For large-scale deployments. Requires a running JanusGraph 1.0+ server. Raw query passthrough uses Gremlin instead of Cypher.

JANUSGRAPH_URL=ws://localhost:8182/gremlin

Supported MCP Clients

The setup command auto-detects and configures these. The table shows where each tool keeps its config if you are wiring it up by hand.

Claude Code~/.claude.json — under mcpServers
Cursor~/.cursor/mcp.json
Clinecline_mcp_settings.json in VS Code globalStorage
Codex CLI~/.codex/config.yaml — under mcp_servers
Gemini CLI~/.gemini/settings.json — under mcpServers

Any other MCP client works too — point it at the same stdio command, or run the server over sse / http on port 3000 via TRANSPORT.

Audit Proxy

A zero-config reverse proxy that intercepts LLM API traffic and writes structured audit logs to ClickHouse — thinking blocks, narration, and tool calls.

Setup

Prerequisites: Go 1.22+ and a running ClickHouse instance.

1. Apply the schema

clickhouse-client --multiquery < migrations/001_initial.sql

2. Start the proxy

CLICKHOUSE_DSN=clickhouse://localhost:9000/audit \
  go run ./cmd/proxy

3. Point your agent at the proxy

Claude Code
ANTHROPIC_BASE_URL=http://localhost:8080
OpenAI Codex
OPENAI_BASE_URL=http://localhost:8080/v1
Gemini CLI
GEMINI_API_BASE_URL=http://localhost:8080
Multi-tenant
AUDIT_PROJECT=myorg/myrepo \
AUDIT_BRANCH=feature/refactor \
CLICKHOUSE_DSN=clickhouse://localhost:9000/audit \
  go run ./cmd/proxy
API keys are never stored. Authorization headers are forwarded transparently but scrubbed from all audit records written to ClickHouse.

Configuration

All configuration via environment variables. No config files required.

VariableDefaultDescription
PROXY_ADDR127.0.0.1:8080Listen address
CLICKHOUSE_DSN(required)ClickHouse connection string
CLICKHOUSE_DBauditDatabase name
BATCH_SIZE100Events per flush
BATCH_INTERVAL1sMax time between flushes
AUDIT_PROJECT""Repository / project name tagged on every row
AUDIT_BRANCHauto-detectedGit branch; falls back to git rev-parse at startup
UPSTREAM_PROXY(none)Upstream proxy URL (overrides HTTPS_PROXY)
LOG_LEVELinfodebug | info | warn | error
OTEL_EXPORTER_OTLP_ENDPOINT(none)Activates OTel traces when set (e.g. http://localhost:4318)

Proxy Chaining

Audit Proxy slots into existing proxy chains. Set UPSTREAM_PROXY and it forwards through your existing proxy while capturing the full audit trail.

Agent -> [codesteward-audit-proxy :8080] -> [Your Existing Proxy] -> LLM API
Corporate firewall
UPSTREAM_PROXY=http://proxy.corp.internal:3128 \
  go run ./cmd/proxy
Portkey
UPSTREAM_PROXY=https://api.portkey.ai \
  go run ./cmd/proxy

If UPSTREAM_PROXY is not set, the proxy checks standard HTTPS_PROXY / HTTP_PROXY environment variables — companies with a system-wide proxy get chaining automatically.

ClickHouse Query Examples

Tool calls that touched sensitive paths in the last 7 days
SELECT session_id, ts, tool_name, tool_input
FROM audit_events
WHERE ts > now() - INTERVAL 7 DAY
  AND JSONExtractString(tool_input, 'file_path') LIKE '%/.env%'
ORDER BY ts DESC;
Tool call volume by project, branch, and agent today
SELECT project, branch, agent, tool_name, count() AS calls
FROM audit_events
WHERE toDate(ts) = today()
GROUP BY project, branch, agent, tool_name
ORDER BY calls DESC;

IDE Plugins

The Codesteward IDE plugins are the recommended way to set up the audit proxy. Install the plugin from the official marketplace and it handles everything — no manual environment variables needed.

Alternative to manual setup. If you use the VSCode extension or JetBrains plugin, you can skip step 3 above entirely. The plugin configures the proxy URL and injects identity headers for all supported agents automatically.

Installation

VSCode
Search "Codesteward Audit Proxy" in the Extensions Marketplace
# or
ext install bitkaio.codesteward-audit-proxy
JetBrains
Search "Codesteward Audit Proxy" in the JetBrains Marketplace
# Settings → Plugins → Marketplace

What the plugin does

  • Sets the proxy URL for each detected agent (ANTHROPIC_BASE_URL, OPENAI_BASE_URL, GEMINI_API_BASE_URL)
  • Injects identity headers automatically: user (from git), project, branch, session ID, and team
  • Tracks branch changes in real time — headers update when you switch branches
  • Status bar indicator shows proxy state and health
  • Header inspector panel lets you see exactly what metadata is being sent

Supported agents

AgentBase URLHeaders
Claude Code CLIANTHROPIC_BASE_URLANTHROPIC_CUSTOM_HEADERS
Codex CLIOPENAI_BASE_URL~/.codex/config.toml
Gemini CLIGEMINI_API_BASE_URLGEMINI_CLI_CUSTOM_HEADERS
ClineProvider settingsExtension settings
AiderANTHROPIC_BASE_URL--extra-headers
ContinueProvider configConfig headers

Team configuration

Commit a .codesteward.json to your repository root for shared team settings. The plugin reads it automatically — no per-developer setup needed.

{
  "proxy": {
    "url": "https://audit.corp.internal:8443",
    "enabled": true
  },
  "identity": {
    "team": "platform-engineering",
    "project": "payment-service"
  }
}

Session Summarizer

Automatically turns raw audit proxy events into structured session summaries — key decisions, files changed, bugs resolved, and security-relevant actions. Runs locally with Ollama or via hosted LLMs (OpenAI, Anthropic).

Setup

Prerequisites: ClickHouse with audit proxy events, and Docker.

1. Apply the migrations

clickhouse-client --host <host> --query "$(cat migrations/001_session_summaries.sql)"
clickhouse-client --host <host> --query "$(cat migrations/002_session_chunk_extractions.sql)"

2. Start the summarizer

# Docker Compose (includes Ollama sidecar)
docker compose up -d

# The model is auto-pulled on first startup
# Summaries appear in audit.session_summaries

Running without Docker

With Ollama (local)
ollama pull phi3:mini
uv sync
CLICKHOUSE_URL=http://localhost:8123 \
  uv run python -m summarizer.main
With OpenAI
uv sync --extra openai
LLM_PROVIDER=openai \
SUMMARIZER_MODEL=gpt-4o-mini \
OPENAI_API_KEY=sk-... \
CLICKHOUSE_URL=http://localhost:8123 \
  uv run python -m summarizer.main
With Anthropic
uv sync --extra anthropic
LLM_PROVIDER=anthropic \
SUMMARIZER_MODEL=claude-haiku-4-5-20251001 \
ANTHROPIC_API_KEY=sk-ant-... \
CLICKHOUSE_URL=http://localhost:8123 \
  uv run python -m summarizer.main
Audit data stays local by default. With Ollama, all inference runs on your infrastructure. No data is sent to external APIs unless you explicitly configure a hosted provider.

Configuration

All configuration via environment variables. No config files required.

VariableDefaultDescription
LLM_PROVIDERollamaLLM backend: ollama, openai, or anthropic
SUMMARIZER_MODELphi3:miniModel name (provider-specific)
CLICKHOUSE_URLhttp://localhost:8123ClickHouse HTTP interface URL
CLICKHOUSE_DATABASEauditDatabase containing audit_events and session_summaries
OLLAMA_URLhttp://localhost:11434Ollama API endpoint (ollama provider only)
RUN_MODEpollpoll (continuous loop) or once (single cycle)
POLL_INTERVAL_SECONDS300Seconds between polling cycles
SESSION_COOLDOWN_MINUTES30Minutes of inactivity before summarization
LOOKBACK_HOURS168How far back to look for unsummarized sessions (default: 7 days)
BATCH_SIZE10Max sessions to process per poll cycle
SUMMARIZER_VERSIONv1Bump to force re-summarization of all sessions
CONTEXT_MAX_TOKENS4096Model context window size in tokens
SESSION_LANGUAGEenISO 639-1 code of session content language
LOG_LEVELinfoLogging level: debug, info, warning, error

Run modes

  • poll (default) — continuous loop. Processes pending sessions, sleeps, repeats. Use for Docker / systemd.
  • once — single cycle, then exits. Use for cron jobs or Kubernetes CronJobs.

Version-gated re-summarization

Changing SUMMARIZER_VERSION (e.g., from v1 to v2) causes all sessions within the lookback window to be re-summarized. Useful after changing prompts, switching models, or adding new fact categories. Old summaries are preserved as revisions.

LLM Providers

Ollama (default)

Local inference. The model is auto-pulled on startup if not available. Recommended CPU-friendly models:

  • phi3:mini (3.8B params, ~2.3 GB) — good quality-to-size ratio
  • mistral (7B params, ~4.1 GB) — better quality, needs more RAM
  • llama3:8b (8B params, ~4.7 GB) — strong general performance

OpenAI

Set LLM_PROVIDER=openai and OPENAI_API_KEY. Install the SDK: uv sync --extra openai. Supports custom base URLs via OPENAI_BASE_URL for Azure OpenAI or local vLLM. Recommended: gpt-4o-mini, gpt-4o.

Anthropic

Set LLM_PROVIDER=anthropic and ANTHROPIC_API_KEY. Install the SDK: uv sync --extra anthropic. Recommended: claude-haiku-4-5-20251001, claude-sonnet-4-20250514.

Processing Pipeline

Every poll cycle, the summarizer discovers sessions with events in the lookback window that have been inactive for the cooldown period and haven't been summarized with the current version.

Single-pass (short sessions)

When events fit within the token budget, the summarizer compresses them into a timeline and sends them to the LLM in one call. The response is parsed as YAML containing the summary, key decisions, and tags.

Chunked extract-merge-synthesize (long sessions)

For sessions that exceed the budget, events are split into chunks at natural boundaries (time gaps > 5 minutes). Each chunk is sent to the LLM to extract 11 categories of structured facts. Facts are then merged deterministically (no LLM, no information loss) before a final synthesis pass produces the summary.

Why extract-merge-synthesize? Rolling up summaries through multiple LLM passes is lossy — each pass can drop details. Extracting structured facts first, then merging via set union, ensures no information is lost before final synthesis.

Token budget

The character budget is derived from the model's token limit with language-aware ratios (English ~4.0 chars/token, German ~3.0, Chinese ~1.5). 28 languages are supported. Set CONTEXT_MAX_CHARS to override the calculation entirely.

Security

  • Secrets are stripped via regex before any text reaches the LLM
  • Tool inputs (which may contain file contents) are never sent — only extracted file paths
  • API response bodies are excluded from context

Prompt Optimization

A closed-loop system that evaluates session summary quality, generates improved prompts, validates them against real data, and promotes winners — automatically or with human review. Optional add-on to the Session Summarizer.

Evaluation

The prompt evaluator is a separate batch service that uses a strong LLM (Claude or GPT-4o) to score summarizer outputs. It runs periodically (daily or weekly) on a configurable sample size.

Evaluation levels

  • Chunk-level — for each chunk extraction: did it capture all visible facts? Are they accurate? Did the model hallucinate? Each of the 11 fact categories is scored independently.
  • Summary-level — does the final summary accurately cover key activities? Are decisions complete and well-stated? Are tags meaningful?

Prompt provenance

Every summary and extraction is tagged with the exact prompt version (prompt_id), a hash of the prompt text (prompt_hash), and a hash of the input context (input_context_hash). This creates full traceability from output to instructions.

Evaluation data collection

When enabled, the summarizer stores the exact input context sent to the LLM alongside the output. The evaluator needs this to see the same context the original model saw — not a re-rendered version that might differ due to code changes or configuration drift.

Stored contexts expire automatically. ClickHouse TTL manages cleanup (default 90 days). Prompt history and evaluation scores are kept permanently — they're small and valuable for trend analysis.

Auto-Optimization

When enabled, the evaluator goes beyond scoring. It analyzes patterns across all scored outputs, generates a revised prompt, validates it, and promotes it if it outperforms the current one.

Optimization pipeline

  1. Analyze — identify which fact categories consistently underperform and what high-scoring outputs have in common
  2. Generate — feed the analysis to the strong model with the current prompt, low-scoring examples with feedback, and high-scoring examples as references
  3. Validate — re-process a sample of real sessions with the candidate prompt. Score the new outputs with the same evaluator
  4. Promote or reject — promote only if the candidate wins by a meaningful margin. Marginal gains are flagged for human review. Worse results are rejected outright

Safety guardrails

  • Validation gate — no prompt goes live without proving it's better than what it replaces
  • Auto-rollback — after promotion, outputs are continuously scored. If quality drops below historical baselines, the previous prompt is restored automatically
  • Lineage limits — configurable depth limit on automatic iterations. The system pauses for human review after N generations, preventing unbounded prompt drift
  • Full audit trail — every prompt links to its parent. Every optimization decision is logged with scores and reasoning

Operation Modes

Automatic (hands-off)

The system manages the full loop: evaluate, generate, validate, promote, monitor, rollback. Best for teams that want continuous improvement without dedicating engineering time to prompt maintenance.

Manual (human-in-the-loop)

The evaluator runs in evaluation-only mode, providing quality scores and detailed feedback. Engineers write improved prompts informed by the data. The system handles validation and promotion. Best for teams that prefer direct control over prompt changes.

Both modes coexist. Start with manual to build confidence in the evaluation scores, then switch to automatic once the scoring proves reliable.

Cost considerations

  • Summarization — unchanged, still uses the same small/local model
  • Evaluation — strong model on configurable samples (default 20 outputs), periodic schedule. Cost is bounded
  • Optimization — one candidate prompt per run (single LLM call)
  • Validation — re-processes the same sample with the candidate, bounded by sample size

Taint Analysis

Traces how untrusted input propagates through your codebase to dangerous operations — SQL sinks, shell commands, file I/O — and writes findings as TAINT_FLOW edges.

Setup

Taint analysis builds on the structural graph. Build the graph first, then run the analyzer.

# 1. Build the graph
codesteward-mcp graph_rebuild --repo-path /path/to/repo

# 2. Run taint analysis
codesteward-taint \
  --repo-id  my-repo \
  --repo-path /path/to/repo \
  --frameworks fastapi \
  --level 3

Output edges in the graph

(source:Function)-[:TAINT_FLOW {
  cwe:       "CWE-89",
  hops:      3,
  level:     3,
  sanitized: false
}]->(sink:Function)

CLI Flags

FlagTypeDefaultDescription
--repo-idstring(required)Repository identifier
--repo-pathstring(required)Absolute path to repository on disk
--neo4j-uristringbolt://localhost:7687Neo4j bolt URI
--neo4j-userstringneo4jNeo4j username
--neo4j-passwordstringNeo4j password
--tenant-idstringlocalTenant namespace
--frameworksstringauto-detectComma-separated catalog names, e.g. fastapi,express
--max-hopsint8Maximum call depth for Level 1 traversal
--levelint3Maximum analysis level: 1, 2, or 3
--persist-cfgboolfalseWrite CFG nodes to Neo4j for path-level queries
--include-safeboolfalseInclude sanitized paths in output
--catalog-dirstringPath to additional custom catalog YAML files
--no-builtin-catalogsboolfalseDisable built-in catalogs; use only --catalog-dir
--outputstringyamlOutput format: yaml | json | text
--stubboolfalseAnalyze without writing results to Neo4j
--log-levelstringinfodebug | info | warn | error

Custom Catalogs

Add custom source/sink definitions by creating YAML files in a catalog directory and passing --catalog-dir.

# Example custom catalog: my-catalogs/internal-api.yaml
sources:
  - name: get_user_input
    type: parameter
    language: python
    cwe: CWE-89

sinks:
  - name: execute_raw_query
    type: function_call
    language: python
    cwe: CWE-89
codesteward-taint \
  --repo-id my-repo \
  --repo-path /path/to/repo \
  --catalog-dir ./my-catalogs

Supported Frameworks

LanguageFrameworks
PythonFastAPI, Flask, Django
TypeScript / JavaScriptExpress, NestJS
JavaSpring MVC
Gonet/http, Gin, Echo