codifier-mcp
A self-hosted MCP server that maintains a registry of project rules, enabling chats to query which rules apply to them in one call. It manages rule lifecycle with scopes, expiry, approval batches, and immutable history.
README
Codifier MCP <img align="right" src="https://img.shields.io/badge/License-MIT-yellow.svg">
<img src="https://img.shields.io/badge/version-1.0.3-blue.svg"> <img src="https://img.shields.io/badge/python-3.12-3776AB.svg?logo=python&logoColor=white"> <img src="https://img.shields.io/badge/Unraid-7-F15A2C.svg"> <img src="https://img.shields.io/badge/MCP-30%20tools-8A63D2.svg">
The rules your project runs on, in a registry instead of scattered Markdown — so a chat can answer "which rules am I under?" in one call.
Self-hosted. Nothing leaves your machine except towards the conversation that asked. Rules are never deleted, IDs are never reused, and history is written by the database itself.
Why it exists
Give an LLM project a set of rules and they start in one file. Then a role needs one of its own, then a second role, and eighteen months later there are 177 of them across three documents plus the roles' own memories. Every chat opens three files to use forty rules.
The context cost is the symptom. The disease is that nobody can answer quickly: this chat, right now, which rules is it under? Answering it today means reading three files, holding in your head which applies to whom, and trusting that nobody wrote the same thing twice in two places. It is a reading job, and because it is a reading job it gets done badly.
| Rules in Markdown | Codifier | |
|---|---|---|
| "Which rules apply to me?" | open three files, filter by hand | one call, ordered |
| Changing one rule that lives in three memories | three edits, and you forget the third | one edit |
| Reusing a retired rule's number | nothing stops you | the database refuses |
| "Why is this rule here?" | ask whoever wrote it | the reason is mandatory, and kept |
| A rule that stopped being needed | stays forever | expires unless renewed |
| Two rules that say the same thing | somebody notices, eventually | flagged as a candidate pair |
| Someone edits the file by hand | invisible | recorded by a trigger |
The real leap is not the lookup: it is that the database refuses things. The
ID cannot be reused, the reason cannot be omitted, deletion does not exist, and
history is written by triggers — so a change made by hand with sqlite3 is in
there too. What used to be discipline is now a constraint.
The model in five sentences
Consumers are whoever downloads rules: chats and skills. A skill acts, and what acts is under rules. A person is not a consumer — a rule that binds a person says so in its body.
Scopes are named sets of consumers. There is no separate notion of "group": a single consumer is a set with one element, and its singleton scope is created by a trigger the moment the consumer is born. One kind of pointer, no branch to get wrong.
The reading order is the breadth of the scope. A rule that reaches everyone
comes first, one that reaches only you comes last — and because breadth is a
COUNT, the order stays right by itself when a new consumer appears.
A rule points to a set of scopes. Widening it is one more row; the group it already belonged to is untouched, because that group has other tenants.
History is a photograph. Each version records both what was declared
(scopes) and who was actually reached that day (consumers), so changing a
group tomorrow cannot rewrite what was true yesterday.
How a rule gets in
proposed ──(signed batch)──> active + provisional ──(signature)──> permanent
│ │
│ └──> retired
└──> denied (with a reason, and the row STAYS)
Two mechanisms, and both exist because of the same diagnosis: a project went from 63 rules to 172, not because anyone wrote without permission, but because adding costs a call and removing costs a decision nobody takes.
Expiry inverts that. An approved rule is provisional and leaves the lists on its own unless somebody decides to keep it. Staying costs a decision, going is free.
Approval is by batch, and signed. A chat cannot ask you to sign in the middle of a conversation — proposals accumulate, and you see them together, which is the only moment three near-duplicates are visible as such. The signature is ed25519 over the batch digest; the registry holds only the public key, so even with the database in hand nobody can manufacture an approval. The private half never enters a conversation — not by discipline, by construction.
Denial needs no signature: refusing cannot do harm. And a denied rule's row stays, so the same idea cannot come back through a different chat in three weeks.
What it looks like
rules_list(project="<code>", consumer="tax monitor")
VA-02 Re-read the sources via _ALL_ breadth 7
PE-01 The method of the four via deliberativi breadth 4
FI-03 Estimating the bracket via tax monitor breadth 1
...
38 rules in force · 132 outside your perimeter
via says why a rule is in your list, which is exactly what you need in order
to decide whether it belongs somewhere else.
Installing
Built for Unraid with the Tailscale plugin, but it is an ordinary container: a mount for the database, one for state, and environment variables.
- A GitHub OAuth application of its own. Homepage
BASE_URL, callbackBASE_URL/auth/callback. Do not recycle another service's, or the two will fight over the callback. JWT_SIGNING_KEY:openssl rand -hex 32. Stable forever — change it and every issued token dies.- An ed25519 key pair, on your own machine:
python3 sign.py --keygen. It prints the public half, which goes inAPPROVAL_PUBKEY; the private half stays in~/.codifier/approval.keyat mode 0600 and never travels. The same script signs the batch digests later:python3 sign.py <digest>. It needscryptography, and recent macOS and Linux refuse a plainpip installinto the system Python — so make it a venv once,python3 -m venv ~/.codifier/venv, install there, and forget about it: sign.py finds that venv and re-executes itself inside it. While you are still setting up you can leave the key empty and setAPPROVAL_GRACE_UNTILto a near date instead — it is a date and not a switch, so it closes by itself. - The database directory must be local storage, never a network share: SQLite in WAL needs real file locking.
The template in this repository is the configuration, and its field descriptions are the real documentation of the deploy. Point Unraid at it, fill the fields, Apply.
Everything else is checked at boot. The preflight is blocking — a failed check exits 2 and the server is never reached, because a service that starts anyway and warns is a service whose warnings nobody reads.
Security
- OAuth 2.1 with GitHub, restricted to one username. That is the front door.
- Source IP filter on every call, on top of OAuth, not instead of it.
- The maintenance code travels on every call that writes: no session, so no mode is left open by accident. Reading your own rules and filing a proposal are both free — a working chat never needs the code.
- The process runs as root and the database is 0644. This is the opposite of the vault twin, deliberately: from the share you read and you do not touch, because a write by hand would bypass the triggers and break history in silence.
- Project codes are not a security boundary. They are opaque so projects cannot stumble into each other; no tool lists them and no error names one, and a wrong code answers exactly like a missing one. The real boundary is the OAuth gate in front.
Testing
Three suites. No network, no FastMCP, no Docker.
python3 test_collaudo.py # 161 cases — the engine, refusals included
python3 test_surface.py # 185 cases — the seam, the image, the template, the signer
python3 test_crash.py # SIGKILL mid-transaction, as Docker does
test_surface.py reads the source rather than running it: every call into the
engine must exist with a compatible signature, every tool that writes must pass
the maintenance gate, and no docstring may name a tool that does not exist.
Sibling
archivist-mcp — a document vault with per-dataset git versioning. Same architecture, same OAuth gate, same blocking preflight. That one keeps files; this one keeps rules.
Licence
MIT.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。