Skip to content

Etki as an MCP server

Etki ships a Model Context Protocol server, so any MCP client — Claude Desktop, Claude Code, or your own agent — can ask real scope questions against your contract, code graph and effort history:

"Is SAML single sign-on in scope? What effort would it take?"

and get the actual, evidence-backed Etki answer (decision + confidence + effort range + the cited contract clause), not a guess.

Tools

Tool What it returns
triage_request(text) The full decision tree: in scope / out of scope / CR candidate / gray area / maintenance, with confidence, an effort range, cited clauses (incl. explicit exclusions, frozen in full text), impacted modules, reasoning.
scope_lookup(query) The contract scope clauses closest to a query (included/excluded + similarity score).
impact_analysis(module) Impacted code regions for a module hint, with a high-churn warning.
similar_effort(description) Similar past work items and a ranged effort estimate by analogy.
baseline_summary() Contract baseline + code graph summary (clause counts, modules, dependencies, index freshness).
dependency_impact(package) Impact surface of a library add/upgrade: manifest declarations (raw version specs), modules importing it, the API symbols they call, one-hop blast radius, high-churn warnings, total LOC. Evidence only — the scope decision still comes from triage_request.
dependency_api_check(package) API-level audit for a version change (up- or downgrade): the exact symbols the code calls per module; with ETKI_DEPS_ONLINE, which recent GitHub release notes mention those symbols (deterministic word-boundary intersection) plus known vulnerabilities from OSV.dev (version-precise when the spec is ==x.y.z).
dependency_version_diff(package, old, new, level="api") Downloads BOTH exact pypi versions (never installed or executed — hardened extraction, ast parse only). The summary diffs the exported API by default (level="full" for every definition). Regardless of level, the your_code section checks this codebase's qualified import paths against the FULL definition surface — Python doesn't enforce privacy, so a non-exported import breaking is still flagged (broken, with moved-symbol hints); dynamic/getattr access lands in unresolved, never silently in "ok". Requires ETKI_DEPS_ONLINE.

Everything is deterministic and read-only: the server never calls an LLM itself (the MCP client is the LLM), and triage_request does not persist a case file or audit event — the approval workflow belongs to the web app. Treat MCP answers as exploration; decisions of record go through the UI.

Claude Code

claude mcp add etki -- uv run --directory /absolute/path/to/etki python -m etki.mcp_server

Then just ask: "Use etki to check whether adding push notifications is in scope, and what effort it would take."

Claude Desktop

Add to claude_desktop_config.json (Settings → Developer → Edit Config):

{
  "mcpServers": {
    "etki": {
      "command": "uv",
      "args": [
        "run", "--directory", "/absolute/path/to/etki",
        "python", "-m", "etki.mcp_server"
      ]
    }
  }
}

Which project does it answer for?

The server reads the same configuration as the app: the persisted index at ETKI_INDEX_PATH (default .etki/index.json) if present, otherwise it builds one live from ETKI_CONNECTORS_PATH (default config/connectors.example.yaml → the bundled demo corpus) using the dependency-free AST engine — no Joern/JVM needed. Point it at another corpus with environment variables in the MCP config:

"env": {
  "ETKI_CONNECTORS_PATH": "config/connectors.docker.yaml",
  "ETKI_INDEX_PATH": ".etki/index-demo.json"
}

Try it

With the bundled demo corpus, triage_request("We need SAML single sign-on") returns OUT_OF_SCOPE with high confidence, citing the contract's explicit exclusion clause — plus the effort range such a change request would take, estimated from similar past tickets.