Dev #61

Merged
Latte merged 4 commits from dev into main 2026-06-27 19:36:45 +00:00
4 changed files with 317 additions and 1 deletions
Showing only changes of commit d65e9be5ff - Show all commits

No files matched your search

+2 -1
View File
@@ -42,6 +42,7 @@ Tests use [tests/conftest.py](tests/conftest.py): the `mock_esi` fixture install
## Git / CI workflow
- Work happens on `dev`; **`main` is not touched until the first complete version** (it triggers the real release publish). Feature branches → PR → fast-forward merge into `dev` → delete branch.
- Work happens on `dev`. Feature branches → PR → fast-forward merge into `dev` → delete branch. Cut a release by bumping the version in `pyproject.toml` / `src/familiar/__init__.py` and opening a `dev → main` fast-forward PR (publishes the clean version).
- `main` and `dev` are protected: no direct pushes, **signed commits required** (the user's git auto-signs), merge via PR.
- CI (`.gitea/workflows/ci.yml`) runs lint + test on every PR/push; on a merge to main/dev it builds and publishes to the Gitea PyPI registry (auth via the `REGISTRY_TOKEN` secret; twine must NOT use `--skip-existing`). dev publishes a unique `.devN` pre-release.
- **Do NOT attribute commits, merges, or PRs to Claude / Claude Code.** No `Co-Authored-By: Claude ...` trailer on commits and no "Generated with Claude Code" footer in PR descriptions. Keep messages plain and authored by the user.
+52
View File
@@ -0,0 +1,52 @@
# familiar — Documentation
> Your familiar across New Eden — an [MCP](https://modelcontextprotocol.io) server that gives a local AI a complete window into your EVE Online account and the New Eden universe, plus opt-in hands to act on it.
This folder contains the full English documentation for the **familiar** project. Start
with [Getting Started](getting-started.md) if you just want to run it, or [Architecture](architecture.md)
if you want to understand or extend the code.
## What is familiar?
`familiar` is a local-first MCP server, written in Python with
[FastMCP](https://github.com/jlowin/fastmcp), that connects a local AI assistant
(Claude Code, Claude Desktop, Cowork, or any MCP client) to two data sources:
- **ESI** — the [EVE Swagger Interface](https://esi.evetech.net/), CCP's official REST API. Public
endpoints (market, universe, industry indices) need no login; character **and corp** data flow
through EVE SSO with read **and write** OAuth2 scopes.
- **SDE** — the [Static Data Export](https://developers.eveonline.com/resource/resources), EVE's
static dataset of items, systems and stations, optionally cached locally for fast offline lookups.
It exposes roughly **80 tools** across market, universe, account, industry, trade, structures,
killmails, profile, corp, communications, contacts, fittings, fleet, UI, and planetary interaction —
the vast majority read-only, with a small set of **guarded write actions** (mail, waypoints, contacts,
fittings, fleet, UI windows, PI) gated behind a safety choke point.
## Design principles
- **Safe by default.** Reading is free. Every *write* is previewed before it runs (nothing happens
without `confirm=true`), recorded in an audit log, and can be turned off entirely. See
[Write Safety](write-safety.md).
- **Local-first.** Runs on your own machine. Your refresh token and account data never leave your
control — the token store is Fernet-encrypted on disk.
- **Everything, eventually.** The goal is full ESI coverage (character and corp, read and write) plus
the complete static universe, built in phases so the project stays shippable.
## Documentation map
| Document | What's in it |
| --- | --- |
| [Getting Started](getting-started.md) | Install with `uv`, log in via EVE SSO, connect to Claude, first queries. |
| [Architecture](architecture.md) | How the server is assembled, the ESI/auth/charutil/writes layers, the module pattern. |
| [Authentication](authentication.md) | EVE SSO PKCE flow, the encrypted multi-character store, scopes, CLI commands. |
| [Write Safety](write-safety.md) | The `writes.guard` choke point, dry-run previews, the audit log, enabling/disabling. |
| [Tool Reference](tool-reference.md) | Every MCP tool, grouped by module, with parameters and descriptions. |
| [Configuration](configuration.md) | Every environment variable and on-disk file. |
| [Contributing & Development](contributing.md) | Dev setup, testing conventions, lint/format, the Git/CI workflow. |
## Quick links
- Project root [README.md](../README.md) — the marketing-facing overview and roadmap.
- [CLAUDE.md](../CLAUDE.md) — guidance for AI coding assistants working in this repo.
- License: [MIT](../LICENSE).
+154
View File
@@ -0,0 +1,154 @@
# Architecture
familiar is a small, layered codebase. Every authenticated tool ultimately rests on four shared
pieces — the server assembly, the ESI client, the auth/token store, and the per-character helpers —
with a single write-safety choke point on top. This document explains each layer and the conventions
that keep ~80 tools consistent.
```
┌──────────────────────────────────────────────────┐
MCP client │ Claude Code / Claude Desktop / Cowork (stdio) │
└───────────────────────┬──────────────────────────┘
│ MCP over stdio
┌───────────────────────▼──────────────────────────┐
│ server.py — one FastMCP instance, ping + register │
└───────────────────────┬──────────────────────────┘
register(mcp) on each tool module (market, universe, account, …)
┌───────────────────────┼──────────────────────────┐
│ │ │
┌────────▼────────┐ ┌─────────▼─────────┐ ┌───────────▼──────────┐
│ charutil.py │ │ writes.py │ │ lookup.py / sde.py │
│ run_per_* │ │ guard() gate │ │ name → id resolution │
└────────┬────────┘ └─────────┬─────────┘ └───────────┬──────────┘
│ │ │
┌────────▼────────┐ ┌─────────▼─────────┐ │
│ auth.py │ │ esi.py / zkill │◄───────────────┘
│ EVE SSO + store │ │ async HTTP wrap │
└─────────────────┘ └───────────────────┘
```
## Server assembly — `server.py`
[`server.py`](../src/familiar/server.py) creates the single `FastMCP` instance and calls
`register(mcp)` on each tool module in turn. It also defines the one built-in tool, `ping`, a health
check that returns the server name, version and UTC time.
Adding tools is mechanical: **write a new module, import it in `server.py`, and call its
`register(mcp)`.** Nothing else wires it up.
> Note: the `instructions=` string passed to `FastMCP` still describes familiar as "read-only". That
> text predates the write actions and is historical framing; the real behaviour is read **and**
> guarded write (see [Write Safety](write-safety.md)).
## The tool-module pattern
Every tool module — market, universe, account, industry, trade, structures, killmails, profile, corp,
corp_economy, misc, comms, contacts, fittings, fleet, ui, planets, writes — follows the same shape:
```python
# 1. module-level async functions = the tools (plain, importable, testable)
async def get_something(character: str | int | None = None) -> dict[str, Any]:
"""One-line docstring becomes the tool description the AI sees."""
...
# 2. a TOOLS list naming the functions to expose
TOOLS = [get_something, ...]
# 3. register() loops mcp.tool(fn)
def register(mcp: FastMCP) -> None:
for fn in TOOLS:
mcp.tool(fn)
```
The key idea: **tools are plain functions; registration is separate.** Tests import and call the
functions directly without any MCP machinery. The function's docstring is what the AI reads to decide
when to call it, so docstrings are written for that audience.
## ESI access — `esi.py`
[`esi.py`](../src/familiar/esi.py) is a thin async wrapper around `httpx` for the
[EVE Swagger Interface](https://esi.evetech.net/):
- A shared singleton `EsiClient` via `client()`; `set_client()` lets tests inject an
`httpx.MockTransport`.
- `get` / `post` / `put` / `delete`, plus `get_all_pages` which follows the `X-Pages` header to
concatenate every page of a paginated endpoint.
- Bearer auth via a `token=` argument for authenticated calls; public calls omit it.
- A uniform `EsiError` carrying the HTTP status.
- A descriptive `User-Agent` (ESI asks for one).
- **Response caching** honoring the ESI `Expires` header — GET responses are cached until they
expire (disable with `FAMILIAR_DISABLE_CACHE`).
- **Error-limit back-off** — ESI publishes a rolling error budget in response headers; when it nears
exhaustion (`FAMILIAR_ERROR_LIMIT_THRESHOLD`, default 5) the client waits for the window to reset
before issuing more requests.
[`zkill.py`](../src/familiar/zkill.py) mirrors this design for the public
[zKillboard](https://zkillboard.com/) API, used by the killmail tools.
## Authentication — `auth.py`
[`auth.py`](../src/familiar/auth.py) implements EVE SSO with PKCE (no client secret) and a
**multi-character** encrypted store. Full details in [Authentication](authentication.md). The
short version:
- `interactive_login()` runs the browser flow once per character and persists a bundle.
- The store is a dict of characters keyed by id, in one Fernet-encrypted file; a legacy
single-character file is migrated on load.
- `get_auth(character_id)` returns a valid access token, transparently refreshing via the rotating
refresh token when the current one is within 60s of expiry.
- `list_characters` / `resolve_character` / `remove_character` manage the store; `status()` reports
it without any network call.
Config (client id, callback host/port, scopes, data dir) lives in
[`config.py`](../src/familiar/config.py), all environment-overridable — see
[Configuration](configuration.md).
## Per-character / per-corp pattern — `charutil.py`
[`charutil.py`](../src/familiar/charutil.py) is the backbone of every authenticated tool. Authenticated
read tools take an **optional** `character` argument and run over one named character or **all**
logged-in ones:
- `run_per_character(character, fetch)` — resolves the target(s), gets a refreshed token for each,
runs `fetch(token, character_id)`, and returns a uniform `{"characters": [...]}` list. Each result
is tagged with `character_id` and `character_name`.
- `run_per_corp(character, fetch)` — same, but also resolves each character's `corporation_id` first
(corp endpoints 403 without the right corp role; that surfaces as an `EsiError` for the affected
character). `fetch` receives `(token, corporation_id, character_id)`.
- `one_token(character)` — picks a single `(token, character_id)` for lookups about a *thing* (e.g. a
structure) rather than per character.
- `character_targets(character)` — `(id, name)` pairs without needing a token, for public lookups
keyed by character id (e.g. zKillboard).
- `summarize(items, limit)` — the uniform `{count, sample}` shape used to cap large lists.
Because of this layer, a typical read tool is just a few lines: resolve names to ids, call ESI inside
a small `fetch`, hand it to `run_per_character`.
## Name resolution — `lookup.py`, `hubs.py`, `sde.py`
- [`lookup.py`](../src/familiar/lookup.py) resolves names to ids via ESI's `/universe/ids/` endpoint,
so a user can say "Tritanium" or "Jita" instead of a numeric id. `resolve_id` short-circuits if the
value is already numeric and prefers the offline SDE for inventory types when one is present.
- [`hubs.py`](../src/familiar/hubs.py) holds the static table of NPC trade hubs (Jita, Amarr, Dodixie,
Rens, Hek) with their region/system/station ids, used by the market tools.
- [`sde.py`](../src/familiar/sde.py) is an **optional** local Static Data Export store. When a prepared
SQLite copy is on disk (`FAMILIAR_SDE_PATH` or `<data_dir>/sde.sqlite`), familiar resolves type
names and serves item details offline. It degrades gracefully: if the file or a needed table is
absent, `available()` reports `False` and callers fall back to ESI. `sde_status` is the diagnostic
tool reporting this.
## Write safety — `writes.py`
[`writes.py`](../src/familiar/writes.py) provides `guard(action, params, confirm, execute)`, the single
gate every mutating tool calls. It is the only place a write to ESI is permitted. See
[Write Safety](write-safety.md) for the full semantics. The rule for contributors: **never call a
mutating ESI endpoint directly — always route through `writes.guard`.**
## Conventions summary
- **Reads** take an *optional* `character` (omit → all logged-in characters); **writes** take a
*required* `character` plus a `confirm` flag.
- Authenticated tools build on `run_per_character` / `run_per_corp`; thing-scoped ones use `one_token`.
- Large lists are capped via `summarize(items, limit)`.
- Docstrings are the AI-facing tool descriptions — keep them one clear sentence.
- Tests call tool functions directly and inject mock transports via `set_client` / `set_connection`.
+109
View File
@@ -0,0 +1,109 @@
# Getting Started
This guide takes you from a fresh clone to a working AI assistant that can read your EVE Online
account and act on it when you ask.
## 1. Prerequisites
- **[uv](https://docs.astral.sh/uv/)** — the Python project/dependency manager familiar is built
around. Install it first (see the uv docs for your platform).
- **Python 3.11+** — uv will fetch a suitable interpreter if you don't have one.
- An **EVE Online account** (for the authenticated tools; public market/universe lookups work
without one).
- An MCP-capable client: **Claude Code**, **Claude Desktop**, **Cowork**, or any other MCP client.
## 2. Install
```bash
uv sync # install the project and all dependencies (incl. the dev group)
```
There is no separate build step for local use. `uv run familiar` runs the server straight from
source.
## 3. Log in (EVE SSO)
familiar uses EVE's Single Sign-On with PKCE — no client secret, nothing to configure. Run the
login once **per character**:
```bash
uv run familiar login # opens your browser to the EVE SSO consent page
```
The browser opens CCP's login page. Approve the requested scopes; the callback lands on
`localhost:8765`, the code is exchanged for tokens, and the (rotating) refresh token is stored
**encrypted** on disk. You'll see a "login complete" page — close the tab and return to the terminal.
To add more characters, switch EVE accounts in the browser and run `login` again. All characters live
in one encrypted store:
```bash
uv run familiar status # list logged-in characters
uv run familiar logout NAME # forget one character (by name or id)
uv run familiar logout # forget ALL characters
```
> **Changing scopes:** if the set of requested scopes changes (e.g. a new familiar version adds
> tools), you must log in again to grant them.
See [Authentication](authentication.md) for the full flow and the scope list.
## 4. Connect to your AI client
### Claude Code
```bash
claude mcp add familiar -- uv --directory /path/to/familiar run familiar
```
### Claude Desktop
Add this to `claude_desktop_config.json` (replace the directory with your clone's absolute path):
```json
{
"mcpServers": {
"familiar": {
"command": "uv",
"args": ["--directory", "/path/to/familiar", "run", "familiar"]
}
}
}
```
The client launches `uv run familiar` with no arguments, which starts the MCP server over **stdio** —
the transport MCP clients speak.
## 5. First queries
Ask in natural language. The AI picks the right tools:
- *"Is the familiar server up?"* → calls `ping`.
- *"What's Tritanium worth in Jita?"* → `get_market_price`.
- *"Compare Plex prices across all trade hubs."* → `compare_hubs`.
- *"How much ISK do I have across all my characters?"* → wallet tools, per character.
- *"Where are my characters right now?"* → location tools.
- *"Set a waypoint to Amarr for my main."* → `set_waypoint` (a **write** — previewed first; see below).
## 6. Write actions
Write tools (send mail, set waypoints, edit contacts/fittings, fleet commands, open windows, PI) are
**enabled by default**, but every write is still previewed unless you pass `confirm=true`, and each
executed write is logged. To block all writes globally:
```bash
uv run familiar writes disable # turn writes off
uv run familiar writes enable # turn them back on
uv run familiar writes # show current state
```
Full details in [Write Safety](write-safety.md).
## 7. Verify everything works
```bash
uv run pytest # full test suite (fails under 80% coverage)
uv run familiar # start the server manually (Ctrl-C to stop) — it waits on stdio
```
If `uv run familiar` starts without error and `familiar status` shows your characters, you're ready.