Skip to main content
Glama
devenv

boris-mcp

by devenv
README.md
# boris-mcp

[![CI](https://github.com/devenv/boris-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/devenv/boris-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)

A public MCP server about **Boris Churzin** ([essl](https://github.com/devenv)) — what he
builds, how he decides, and the ops doctrine he runs on. Point an agent at it and interrogate
the judgment instead of parsing a PDF.

```sh
uvx boris-mcp
```

Or in an MCP client config:

```json
{
  "mcpServers": {
    "boris": { "command": "uvx", "args": ["boris-mcp"] }
  }
}
```

## A taste

**On always-on systems** (`design_decisions("dumb-evaluator")`):

> An always-on process is the one that runs when you are asleep and cannot supervise it.
> Every capability you give it is a capability that acts unsupervised. So the always-on half
> evaluates predicates and sends alerts; all judgment lives in the supervised half, which
> compiles rules for it.

**On tracking work** (`doctrine("ack-is-not-answer")`):

> An acknowledgment is not an answer, and may be a disguised blocker. Auto-replies arrive in
> seconds and mean nothing happened.

That's the whole idea: ask the questions, get the reasoning. The rest is how the server
serves it safely — which turns out to be the same discipline the answers are about.

## Tools

Nine tools are public — no auth required, callable over stdio (`uvx boris-mcp`) or anonymously
over HTTP:

| Tool | What it gives you |
|---|---|
| `about()` | Who, what, and the shape of the rest |
| `guide()` | What you can do here, and what to do next |
| `whoami()` | Who the server thinks you are, and what that gets you |
| `whats_new(since)` | What's shipped since a cursor you keep — poll this at session start |
| `projects()` | Systems built — and what's architecturally notable about each |
| `design_decisions(topic)` | How things were actually decided, and why |
| `doctrine(rule)` | Personal-ops rules, each with the failure it prevents |
| `stack()` | Languages, patterns, engineering discipline |
| `contact()` | How to reach him |

Beyond these, eight more tools exist behind authentication and the circles-of-trust gate: a
headhunter-tier pair (`send_message`, `my_messages`) for leaving the owner a message and
checking for a reply, and an owner-tier set (`inbox`, `reply`, `grant`, `revoke`,
`access_requests`, `activity`) for reading and answering the mailbox and managing who's let
in. See [Circles, auth, and the mailbox](#circles-auth-and-the-mailbox) below.

Public tools are read-only over a public corpus. The consent-carrying set above mutates
access or mailbox state — but only because the caller has already been proven to be the
owner (or, for `send_message`, an authenticated non-anonymous caller) by a fail-closed gate:
an unmapped or unauthenticated caller resolves to the public tier and nothing more, by
construction, not by policy. Content above the commit boundary — the private corpus — lives
off-repo on the operator's own disk and is served only through that same gate.

## Circles, auth, and the mailbox

Access is a ladder of circles, outer to inner, each one trusted with strictly more than the
last; a caller sees content gated at or below its own resolved circle and nothing above it —
not even that a deeper circle exists.

```mermaid
flowchart LR
  A["agent / caller"] -->|"stdio (uvx): offline, no auth"| P["public tier"]
  A -->|"HTTP: OAuth via Authelia"| RC["resolve circle<br/>(a signed claim can only<br/>lower trust, never raise)"]
  P --> G{"circle gate<br/>fail-closed"}
  RC --> G
  G -->|"at / below your circle"| C["public corpus in-repo<br/>+ private corpus off-repo"]
  G -->|"above your circle"| D["honest denial<br/>(logged to activity)"]
  C --> M["mailbox &amp; event log<br/>pull + cursor, no push"]
```

`public` is the anonymous default; everything at or above the commit boundary lives only in a
private corpus file on the operator's own disk, never in this repo. The stdio path
(`uvx boris-mcp`) always resolves to `public`. Over HTTP, a caller authenticates via OAuth
against an Authelia instance the operator runs, and the server clamps whatever circle the
token claims to what an explicit grant actually gives it — a signed claim can lower your
trust, never raise it. Forget to label a piece of content and it fails closed to `owner`
(the top of the ladder), hidden from everyone but the operator.

Full picture: [docs/ATP.md](docs/ATP.md) is the acceptance-test checklist behind every locked
decision (published for transparency; it reads like internal QA scaffolding, because it is),
and [deploy/README.md](deploy/README.md) runs the HTTP + Authelia stack yourself.

The message-passing here is the reference implementation of **MCPP — the Model Context
Peering Protocol** ([docs/MCPP.md](docs/MCPP.md)), a formal MCP extension (SEP-style) for
agent-to-agent messaging: MCP servers peer with each other the way SMTP MTAs do, with
pull-based delivery and a trust ladder. MCPP is to MCP what SMTP is to TCP — an application
protocol over a transport that already exists. The mailbox tools
(`send_message`/`inbox`/`reply`/`my_messages`) are what's built and running today;
[docs/MESSAGES.md](docs/MESSAGES.md) is the implementation-level walkthrough. The spec also
defines a consent-gated **`ask`** operation — an agent *drafts* an answer only from what the
asker is already cleared to see, and a human *discloses* it — which is specified but not yet
implemented here.

## Subscribing

There is no push, and that is deliberate. Call `whats_new()` at the start of a session; it
returns everything after your cursor and hands you a fresh one. There's also an
[Atom feed](https://devenv.github.io/boris-mcp/feed.atom) for anything that isn't an agent.

Short version of why: MCP notifications are session-scoped and non-durable, a stdio server
has no inbound network path, and the clients that matter don't implement subscriptions
anyway — so push would drop any event fired while you had no session open, which is most of
them. A cursor makes polling exact. [The long version](docs/EVENTS.md) has the spec citations.

## Using this for yourself

The reusable half lives in [`src/boris_mcp/_engine/`](src/boris_mcp/_engine/) — the event log
with cursor semantics, and Atom feed generation. It knows nothing about me: a test walks its
import graph and scans its code for personal nouns, so it stays extractable
([`tests/test_engine_boundary.py`](tests/test_engine_boundary.py)).

The tool surface deliberately *isn't* abstracted. `about()` / `projects()` /
`design_decisions()` is a good convention and a bad framework — a photographer wants a
portfolio, a writer wants a bibliography. **Abstract the plumbing, template the surface.**
Today that means: fork this, replace `content.py` and `events.json`, keep `_engine/`.

It'll be extracted as an installable package when someone actually wants that — see
[docs/GENERIC-SPLIT.md](docs/GENERIC-SPLIT.md) for the full split and why the repo keeps a
personal name while the engine won't.

## ops-lint

Two linter families for personal-ops text, both built and tested in
[`_engine/opslint/`](src/boris_mcp/_engine/opslint/). `leak` is a deterministic pre-publish
check for anyone running a public server like this one — because the dangerous part of
publishing a personal MCP isn't the code, it's the day you paraphrase something you
shouldn't — and it's already wired in as [`scripts/leak_check.py`](scripts/leak_check.py),
run by hand before every push. `hygiene` flags outbound items with no follow-up date,
acknowledgments recorded as answers, statuses older than their own chase dates, and
prepared-but-unsent work; the engine and its tests exist, but it isn't exposed as a callable
MCP tool yet — that's what's coming.

## Contributing

This repo is a personal instance, not a framework. The instance layer (`content.py`,
`events.json`, the tool surface) is meant to be **forked**, not contributed to — see
[CONTRIBUTING.md](CONTRIBUTING.md). Genuine bug reports against the generic engine
(`_engine/`) are welcome.

## License

MIT

TDQS

A3.8/5.0

Scored across 17 tools

Disambiguation5/5

Each tool targets a distinct resource or action: informational (guide, about, whoami), knowledge (projects, design_decisions, doctrine), messaging (send_message, my_messages, inbox, reply), and administration (access_requests, grant, revoke, activity). No two tools have overlapping purposes.

Naming Consistency5/5

All tool names follow a consistent pattern of lowercase snake_case, with single words or underscore concatenations (e.g., design_decisions, send_message, access_requests). No mixing of conventions like camelCase or PascalCase.

Tool Count4/5

17 tools is slightly above the typical well-scoped range (3-15), but each tool covers a necessary function for the server's stated purpose (personal knowledge, updates, messaging, access control). The count feels justified given the comprehensive scope.

Completeness5/5

The tool set covers all apparent use cases: getting to know Boris (guide, about, projects, design_decisions, doctrine, stack), staying updated (whats_new), communicating (send_message, my_messages, inbox, reply), and managing access (access_requests, activity, grant, revoke). No critical gaps are present.

Maintenance

ActivitySlowing
ResponsivenessNo issues