CyClaw MCP server
Commit Nov 15, 2026An AI retrieval system that answers questions from local documents using a 12-node LangGraph and local LLMs for privacy-focused inference.
Also known as cgfixit/CyClaw
- Stars
- 5
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Nov 15, 2026
- License
- MIT
What is CyClaw MCP?
CyClaw is an AI retrieval system that answers questions from local documents using a 12-node LangGraph and local LLMs for privacy-focused inference.
Summary generated from the project's README
About CyClaw MCP server
CyClaw provides a privacy-focused retrieval system that allows users to query local Markdown and text documents using their own hardware. It utilizes a 12-node LangGraph to manage the retrieval process, combining ChromaDB and BM25 through RRF for hybrid search. The system is designed to run locally using Ollama for inference, ensuring that sensitive data remains on the user's machine.
Developers can set up the server on macOS, Linux, or Windows, binding it to a local port. While the system prioritizes local execution with embeddings and cross-encoders running on the CPU, it supports a hybrid mode. In this mode, users can opt into external fallbacks like Grok or Claude, though this requires explicit per-question consent and configured API keys. The server includes built-in guardrails via NeMo and maintains an audit ledger to track spend and question hashes for security and transparency.
What you can do with it
- 1Hybrid AI retrievalUse local models for standard queries but allow a fallback to Claude or Grok when higher reasoning is needed.
- 2Local knowledge base indexingBuild a searchable local library by running the indexer command on a directory of documents.
- 3Auditing AI interactionsMonitor token usage and question history through a local spend ledger and audit logs.
Written from the project's README. Every use case is checked against a quote from it.
Configuration
CYCLAW_API_KEY
| Name | Description | |
|---|---|---|
| CYCLAW_API_KEY | — |
Automated checks
Ran Nov 14, 2026Full report →Not verified yet. A server needs a score of 80, no serious finding, and enough data for us to check. How checks work · What to fix for more points
- Source repository linked and reachablecgfixit/cyclaw is public and reachablegithub.com/cgfixit/cyclaw ↗Pass
- Package names this repository as its sourceNot checked
- Build provenance (Sigstore attestation)Not checked
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit or release 2026-11-11github.com/cgfixit/cyclaw ↗Pass
- OpenSSF ScorecardNot checked
- Package name not close to a popular packageNot checked
- No critical advisory in the package or its dependenciesNot checked
- No hidden instructions to the model0 tool descriptions and the README read; no override phrases, credential paths or hidden charactersPass
- Destructive tools are labelledNot checked
- Remote endpoint answers the MCP handshakeNot checked
README
View on GitHubCyClaw
Watch the 60-second demo (MP4) — silent, with real local retrieval and inference using synthetic documents.
CyClaw answers questions from your documents on your hardware. Its
12-node LangGraph retrieves first and audits every path,
and requires per-question consent for paid Grok or Claude fallback. The
server binds to 127.0.0.1:8787.
- Embeddings, BM25, and the cross-encoder run locally on CPU; cache both retrieval models before going offline.
- Soul, ops, memory, and audit routes require operator access,
which fails closed without
CYCLAW_API_KEY(an enabled admin's login also works when per-user auth is on). - Audit records hash questions by default; the spend ledger records billed
tokens;
cyclaw-metricsreads both offline. - Guardrails (NeMo in every base install), the Numbat stream, and the spend ledger ship on. Auth, memory, connectors, and the Telegram/X channels ship off.
CyClaw serves one trusted operator, or a mutually trusted group with auth enabled; it provides neither tenant isolation nor a microVM (threat model).
Install and API recipes: Setup Guide.
Contributor contracts: CLAUDE.md, AGENTS.md,
Codex guidance, Actions guide.
Install the Git hooks with bash scripts/ensure-githooks.sh;
verify git config --show-origin --get core.hooksPath selects .githooks
unless an existing hook manager was preserved. Hook file guide.
Residual security decisions
record accepted limits and research; native Windows verification remains disabled.
Confirmed fallback sends questions externally. Context stays local unless
policy.fallback.send_local_context_to_grok or send_local_context_to_claude
is enabled; soul content is never added. Model downloads and optional
sync/channels have separate connections. For no-egress use, pre-cache models,
disable optional connections, and enforce an external network boundary.
Quick Start
macOS (Apple Silicon) — install, keys, Ollama, index, and startup:
git clone https://github.com/cgfixit/CyClaw && cd CyClaw
bash macos/setup-cyclaw.sh
Linux — Ollama already running on 127.0.0.1:11434. The shipped MLX
model tag targets Apple Silicon. Before starting the gateway, set both
models.local_llm.model and guardrails.model in config.yaml to the
Linux-compatible tag you pull below (or another installed model that fits
your hardware):
git clone https://github.com/cgfixit/CyClaw && cd CyClaw
python3.12 -m venv .venv && source .venv/bin/activate
pip install --require-hashes --no-deps -r locks/requirements-torch-lock-linux.txt --index-url https://download.pytorch.org/whl/cpu
pip install --require-hashes -r locks/requirements-lock-linux.txt
pip install --require-hashes -r locks/requirements-test-lock-linux.txt
ollama pull qwen3.8:27b
export CYCLAW_API_KEY="$(openssl rand -hex 20)" # operator routes; /query uses sessions/tokens when auth is on
python -m retrieval.indexer # once; without this, /query is 503
python gate.py # http://127.0.0.1:8787
CI checks every lock group against recorded inputs and hashes, and checks wheel dependency metadata. Refresh on arm64 macOS with the maintained lock scripts; generation failures leave tracked locks unchanged.
Windows uses the same pins with the -windows lock files
(py -3.12 -m venv .venv, .\.venv\Scripts\Activate.ps1):
Windows, Linux.
Choose a non-MLX local model and update both model keys there as well.
Confirm: curl http://127.0.0.1:8787/health, then open
http://127.0.0.1:8787/. A manual macOS install uses plain torch==2.14.1
(no +cpu wheel) then the hashed macOS lock
(macOS (Apple Silicon)); updating an
existing environment ends with the NeMo verification.
For newcomers
Python and install paths. Use Python 3.12 (>=3.12,<3.13):
macOS, Windows, Linux above,
Docker (linux/amd64, loopback), or conda (environment.yml).
The constrained editable install adds cyclaw-* commands;
source checkouts use python gate.py and python -m retrieval.indexer.
Lock maintenance.
Secrets. gate.py reads environment variables, never dotenv files
(Invoke-CyClaw.ps1 reads only non-secret settings from an owner-only
%USERPROFILE%\.CyClaw\.env or checkout dotenv).
Secret persistence.
| Platform | Setup | Operation | Default secret store | Plaintext opt-in |
|---|---|---|---|---|
| macOS | macos/setup-cyclaw-keys.sh |
Set up or preserve gateway key | Keychain | --write-env-file |
| Windows | powershell/Install-CyClaw.ps1 |
Migrate existing plaintext secrets | Credential Manager | -WriteEnvFile |
On Linux, macos/invoke-cyclaw.sh stores the gateway key in libsecret or an
owner-only file (macos/cyclaw-linux-key.sh). Services read secrets at
execution through macos/cyclaw-keychain-env.sh or
powershell/CyClaw-CredMan-Env.ps1, never from dotenv, plist, task XML,
config.yaml, shell rc files, or argv; a configured keystore lookup fails
closed when its item is missing. A missing provider key leaves that provider
unavailable, not the server down (provider keys).
Offline and hybrid. Shipped app.mode: hybrid needs an enabled,
available, selected provider and per-request confirmation for paid fallback.
Offline mode or declining stays local. Downloads have separate controls:
models.embeddings.offline_after_index: true requires cached embeddings;
pre-cache the ~91 MB reranker before its first query.
Ports. Gateway 127.0.0.1:8787 (api.port, launcher
CYCLAW_GATE_PORT; unquoted integer 1..65535, invalid values refuse startup), Ollama 127.0.0.1:11434. The retired :8790 console
became CG-agent-harness.
First-run checks and limits.
503 INDEX_NOT_FOUND: runpython -m retrieval.indexeror Build my library (POST /index/build,GET /index/status). Build requires loopback, same-origin, no forwarding headers; configured keys require unlock.- Console mode and Library/Engine chips expose readiness.
/health: degradedusually means Ollama is down; startupTELEMETRY KILLis expected. - External health probes ship off (
api.health_probe_external_providers). Key presence/model readiness does not establish successful generation. - Per-IP rate limit: 60/min, reset on restart unless
api.rate_limit.persist_pathor Postgres persists it./healthand/index/statusare public/unlimited. Bodies abovesecurity.max_request_body_bytes(1 MiB) return 413. - CORS allows
http://127.0.0.1:8787andhttp://localhost:8787;/queryrejects cross-site requests. Restart after sanitizer-config edits.CYCLAW_EMBED_CACHE_SIZE(2048) is an environment variable. - Run
python .claude/skills/invariant-guard/check_invariants.pyandpython .claude/skills/doc-sync/doc_sync.py; exercise changed behavior. Hosted CI owns broad suites. - At
api.max_concurrent_queries(4),/queryreturns503 GRAPH_BUSY; exceedingapi.graph_timeout_sec(780 s) returns504 GRAPH_TIMEOUT.
Local records. Paths are relative to the repository.
| Path | Git status | Contents and behavior |
|---|---|---|
logs/audit.jsonl |
ignored | Authoritative audit, written synchronously on the request thread. Questions use persistent HMAC-SHA256 fingerprints by default, including when logging.audit_fields is absent or empty. Explicit include_query_hash: false stores redacted query text |
logs/spend.jsonl |
ignored | Tokens for billed Grok/Claude calls, without query text or prices |
logs/numbat-events.ndjsonl |
ignored | Derived redacted audit and out-of-band events; observation only |
logs/cyclaw.log |
ignored | Application log; a bounded writer drops records rather than holding a request on stalled I/O |
logs/evals/ |
ignored | Opt-in dogfood and judge output, separate from production billing |
index/ |
ignored | Chroma and bm25.json; rebuild after corpus changes |
data/personality/soul.md |
tracked | Changes appear in git status and broad staging |
data/personality/cyclaw_soul.db |
ignored | Version DB; not yet generated in a clean checkout |
data/personality/soul.md.bak |
ignored | Vetted backup; not yet generated in a clean checkout |
Day-two:
python -m metrics # spend, audit aggregates, sequences
python -m retrieval.clear_cache # dry-run; --apply deletes .emb_cache
python -m retrieval.indexer # rebuild after editing data/corpus/
curl -s http://127.0.0.1:8787/index/status # build progress
What It Does
CyClaw retrieves Markdown and .txt documents before generation. Output
rails enforce a token-overlap floor on retrieved local answers; that heuristic
does not verify individual claims, which the evals measure.
-
Retrieval first.
retrievestarts the 12-node graph; a miss can still reachoffline_best_effortwith partial context. -
Hybrid search. ChromaDB and BM25 fuse through RRF (
retrieval.rrf_k). A vault hit needs the best cosine to clearretrieval.min_semantic_score(without cosines, the fused score must clear the RRF-scaleretrieval.min_score); the cross-encoder runs in shadow mode (retrieval.min_rerank_score: null). Retrieval reference. -
Local generation. Ollama uses
models.local_llm.model(shippedqwen3.8:27b-mlx). Tune context/sampling for your hardware inconfig.yaml. An optional derived tag storesnum_ctx;macos/Modelfile.cgbuilds the unpublishedqwen3.8:27b-mlx-cg. Keep a suffix to avoid overwriting the registry tag. Local bearer tokens come frommodels.local_llm.api_key_envormodels.local_llm.fallback.api_key_env(defaultsCYCLAW_LOCAL_LLM_API_KEYandCYCLAW_LOCAL_LLM_FALLBACK_API_KEY). Unset means no auth header. Startup rejects inlineapi_keyvalues, including in disabled fallback. -
Governed soul.
data/personality/soul.mdhas SHA-256 drift detection and atomic writes;POST /soul/applyrequires a humanreasonand an enforced injection scan. Restore audits an advisory scan of the vetted backup without enforcing it;/soul/reloadand startup drift recovery adopt on-disk content unscanned. A missing soul self-initializes (Soul invariants, Rules 4–5). -
Confirmed external fallback. Grok (
grok-4.5,api.x.ai) or Claude (claude-sonnet-5,api.anthropic.com) needs hybrid mode, that provider enabled and selected,user_confirmed_online: trueon the request, and a key. Calls share the remainingapi.graph_timeout_secbudget, andutils/endpoint_trust.pyrejects rewritten provider URLs. -
HTTP and MCP. FastAPI serves the browser at
/; the separate MCP server exposes sanitized retrieval only (sampling: None, no generation). -
Audit convergence. Every path converges at
audit_loggerbefore END;cyclaw-metricsreads the audit offline.
Six invariants include core import isolation;
INVARIANTS.md maps Rules 1–9 to tests.
Storage/console. Embedded chroma defaults, optional pgvector
(indexing.vector_backend); BM25 uses JSON, never pickle. static/terminal.html
provides queries, first-run build, health, Soul/Sync/Filesystem panels,
and authenticated Users/Audit.
Optional layers at a glance
| Layer | Purpose | Ships |
|---|---|---|
| Per-user auth | Passwords, sessions, device tokens, roles | off |
| Memory | SQLite/FTS5 facts and episodes, propose/apply, optional retrieval fusion and HTML export | off |
| NeMo Guardrails | Deny-only input/output checks, deterministic fallback on NeMo failure | on |
| Dropbox sync | Out-of-band rclone corpus pull |
CLI |
| Connectors | Scoped filesystem, passive LAN inventory | off |
| Telegram / OpenTweet | Phone remote and X drafts through loopback /query |
off |
| Numbat stream / pre-action hook | Derived NDJSON, observation only; deny-only gate before a confirmed external call | on / off |
| Spend ledger | Billed token counts | on |
| Fine-tune kit | Separate QLoRA toolkit | toolkit |
Architecture
gate.py initializes the soul at startup; requests pass the body cap, Host
allowlist, any required authentication, the 60/min per-IP limit, and the
injection filter (in that order), then enter the graph as GraphState.
flowchart TD
A(["Client\nHTTP POST /query"])
A --> B
subgraph GATEWAY ["gate.py — FastAPI 127.0.0.1:8787"]
B["TrustedHostMiddleware\nHost header allowlist"]
B --> C["Rate Limiter\n60 req/min per IP"]
C --> D["Prompt Injection Filter\n40 patterns · config-driven"]
D --> E["Build GraphState\nquery + user_confirmed_online"]
end
E --> F
subgraph GRAPH ["graph.py — LangGraph 12-node State Machine"]
F(["retrieve\nChroma + BM25 + RRF"])
F --> G["route_by_score\nbest cosine ≥ 0.30?\n(RRF ≥ 0.028 if no cosine)"]
G -->|"YES — local context"| X["guardrail_input\noffline rail · default on"]
X -->|"blocked"| L
X -->|"passed · high score"| H["local_llm\nOllama :11434"]
G -->|"NO — vault miss"| I["user_gate\nneeds_confirm = true"]
I -->|"confirmed + hybrid\n+ grok.enabled + provider=grok"| PG["pre_action_hook_grok\ndeny-only · off = pass-through"]
PG -->|"allow"| J["grok_fallback\ngrok-4.5 · triple-gated"]
I -->|"confirmed + hybrid\n+ claude.enabled + provider=claude"| PC["pre_action_hook_claude\ndeny-only · off = pass-through"]
PC -->|"allow"| W["claude_fallback\nclaude-sonnet-5 · triple-gated"]
I -->|"declined or offline"| X
X -->|"passed · vault miss"| K["offline_best_effort\nlocal LLM · no RAG gate"]
I -->|"confirmed=None — pause"| L
H --> Y["guardrail_output\noffline rail · default on\ngrounding: local_llm only"]
J --> Y
W --> Y
K --> Y
Y --> L
PG -.->|"deny"| L
PC -.->|"deny"| L
L(["audit_logger\nHMAC fingerprint · PII redact\nlogs/audit.jsonl\n+ derived Numbat stream"])
end
L --> M(["QueryResponse\nanswer · sources · model_used\nretrieval_mode · needs_confirm"])
subgraph RETRIEVAL ["retrieval/hybrid_search.py"]
N["ChromaDB\nsemantic · 384-dim cosine"]
O["BM25Okapi\nkeyword · Porter stemming"]
P["RRF fusion\nk=60 · equal weighting"]
N --> P
O --> P
end
F <-->|"hybrid search"| P
subgraph SOUL ["utils/personality.py"]
Q["soul.md\nSHA-256 drift detection"]
R["SQLite / Postgres\nversion history"]
Q <--> R
end
H <-->|"soul preamble\n≤ 8000 chars"| Q
K <-->|"soul preamble"| Q
The MCP server calls the retriever directly after sanitization and never
enters this gateway or graph. The diagram omits the per-answer NeMo check()
and the post-graph CEL monitor (Optional layers).
API Key Setup (Soul Mutations)
/soul/*, /ops/*, /memory/*, /query/export/html, and /audit/summary
require operator access. /health is public; /query requires a session
or device token when auth.enabled is true. Operator credentials are:
- Bearer
CYCLAW_API_KEY, for HTTP clients and scripts (compared withhmac.compare_digest). - The console cookie. "Unlock operator tools" trades the key once for
an HttpOnly,
SameSite=Strictcookie (security.console_session_ttl_sec, 1 h shipped); "Lock" deletes it with matching TLS attributes, and rotating the key revokes every cookie. - An admin login, when
auth.enabledis on (no key needed);operatorandauditaccounts do not get operator access.
Cookie writes require CSRF; cross-site requests fail. Tokens bind origin;
TLS uses __Host- cookies. Because cookies lack port isolation, use a dedicated
TLS hostname alongside untrusted services. Without CYCLAW_API_KEY, key/cookie
access fails closed; enabled admin sessions still work.
macos/invoke-cyclaw.sh and powershell\Invoke-CyClaw.ps1 generate a
missing key into the OS keystore and open a single-use #pair=... unlock
link (5 minutes). To unlock by hand, copy the key into the dialog:
# macOS
security find-generic-password -a "$(whoami)" -s com.cgfixit.cyclaw.api-key -w | pbcopy
# Windows, from the CyClaw folder
. .\powershell\CyClaw-SecretStore.ps1; (Read-CyclawCredential com.cgfixit.cyclaw.api-key).Secret | Set-Clipboard
Do not persist the key with setx or a user environment variable; the
launchers read it from the keystore into the gateway process only
(powershell/README.md).
Rotation, the macOS bootstrap, and 401 recovery:
macos/README.md.
security.api_key_optional ships false; when set, a request skips the key
only if the socket peer is loopback, no forwarding header is present, and the
request is not cross-site (inert under Docker NAT). It does not disable
per-user authentication or RBAC. Full boundary:
INVARIANTS.md Rule 6 and the
threat model (fourteenth amendment).
Per-User Authentication
Separate from the operator API key. gate_auth.py: scrypt passwords, a
session cookie plus CSRF for browsers, named device tokens for scripts, roles
admin, operator, and audit. Ships auth.enabled: false; while off,
every /auth/* route returns 503. Assigned roles survive restarts, including
demotions of the bootstrap account. Design, TLS, and the non-loopback bind
rule: docs/AUTHENTICATION_DESIGN.md;
first-boot curl and cyclaw-user:
setup-guide.md.
cyclaw-gen-cert writes the self-signed cert for the auth + TLS bind exception.
Spend Tracking
Billed Grok/Claude calls append tokens to logs/spend.jsonl; dollars are
computed at read time, so rate-card fixes re-price history. Rows exclude
queries, prompts, and keys, and a failed write drops the row, not the answer.
Core Grok/Claude requests also reserve a durable call allowance before each
POST attempt, retries included: 100 per UTC day and 1,000 per UTC month by
default (zero denies all). Dollar spend is not enforced.
source |
Writer | Covers |
|---|---|---|
query |
llm/client.py |
Confirmed /query fallback |
agentic |
none since October 2026 (the agentic coding loop was removed) | Historical rows only; still parsed and reported |
eval |
tests/judge_eval.py |
Opt-in judge runs, kept separately under logs/evals/ |
python -m metrics reports today and last_7d tokens and USD by provider
and source, and its forensic Sequences section flags a blocked injection
followed within 15 minutes by a paid call (no policy enforced).
tests/spend_live_probe.py spends real money and is opt-in only.
Ledger schema, rate bands, and probes.
Benchmarks and Evals
Only the retrieval gate blocks merges; none of these four paths is a graph node or a security control. Thresholds and limits.
| Evaluation | Command | Scope |
|---|---|---|
| Retrieval gate | python -m tests.ci_rag_smoke |
Every PR, no LLM. Corpus probes through route_by_score_node, then hit@5, Recall@5, and MRR on the 52-case fixture (44 scored, 8 out-of-corpus skipped). Metric floors and [FILTERED] injection-chunk assertions block CI |
| Local dogfood | CYCLAW_EVAL_DOGFOOD=1 python scripts/cyclaw-eval-dogfood.py |
Opt-in real loopback model, one case per category (recipe) |
| Anthropic judge | CYCLAW_EVAL_LIVE=1 python tests/judge_eval.py |
Paid opt-in Claude grades groundedness, completeness, abstention |
| Local judge | Same command with evals.local_judge.enabled: true |
Same rubric on a second loopback model with a different configured tag; choose a separate family and calibrate it |
Fixture retrieval scores 1.0 on all three metrics (no claim about arbitrary corpora). The reranker bake-off found no passing threshold, so vetoing stays off. The only published local run is the Qwen dogfood audit; no judge result is published yet.
Current development
Current changes and dated decisions live in the
merged PRs
and docs/changelog.txt. Install and contributor
contracts: locks, Git hooks.
CyClaw MCP questions
How does CyClaw handle document retrieval?
It uses a hybrid search approach combining ChromaDB and BM25 fused through RRF, supported by a 12-node LangGraph.
Can I use CyClaw offline?
Yes, it supports offline mode where embeddings, BM25, and the cross-encoder run locally on your CPU.
What happens if the local model fails to answer?
In hybrid mode, it can fall back to external providers like Grok or Claude, but this requires per-question user consent.
How do I build my document library?
You can run the retrieval indexer command or use the POST /index/build API endpoint.
