Skip to main content
Glama
amr05008

personal-context

by amr05008
README.md
# Personal Context

A minimal system for giving LLMs your writing voice, opinions, and style. Curated markdown files served via [MCP](https://modelcontextprotocol.io/) using [FastMCP](https://gofastmcp.com/).

Inspired by [Karpathy's LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) and [nlwhittemore's Personal Context Portfolio](https://github.com/nlwhittemore/personal-context-portfolio).

More background on the why behind this project [here](https://aaronroy.com/giving-agents-personal-context/).

## How It Works

```
context/*.md  →  FastMCP server  →  MCP  →  Claude Code (or any MCP client)
             ↘ personal-context CLI → Pi (or any shell-capable agent)
```

You maintain 6 markdown files about yourself. The FastMCP server and local CLI read those same files directly. When you ask an LLM to write something, it can pull your context and match your voice.

### The 6 Context Files

| File | What It Captures |
|------|-----------------|
| `identity.md` | Background, career arc, personal details |
| `writing-style.md` | Voice, tone, sentence patterns, vocabulary, annotated examples |
| `opinions.md` | Stances on topics you write about |
| `expertise.md` | Domains of deep knowledge |
| `projects.md` | Current and notable past projects |
| `communication.md` | How you communicate in different contexts (Slack, email, docs) |

Each file has YAML frontmatter tracking `last_updated` and `source_refs` (which source materials informed the content).

## Getting Started

### Use this as a template

1. Fork or clone this repo
2. Delete everything in `context/` — those are my files, not yours
3. Install dependencies and start filling in your own context

### Setup

```bash
git clone <this-repo> personal-context
cd personal-context
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
```

The editable install is intentional: the installed command always reads the live `context/` directory in this clone. With a synced uv project, it also runs naturally as:

```bash
uv sync --extra dev
uv run personal-context list
```

### Local CLI

The CLI module is standard-library-only and reads Markdown from disk; it does not start MCP, make network calls, or require credentials. (Installing the project still pulls the MCP server's dependencies — the CLI just never imports them.)

```bash
personal-context list
personal-context get writing-style
personal-context get identity projects
personal-context get --all
personal-context get --all --include-private
```

`list` and `get --all` use only the six curated documents in the table above. Arbitrary extra files—including gitignored `private.md` and `travel.md`—are excluded unless `--include-private` is explicit. An explicitly named extra file is also gated, for example:

```bash
personal-context get private --include-private
personal-context list --include-private
```

Names may be supplied with or without `.md`. Multi-document output is Markdown with a source filename heading and separator for each document. Paths and nested names are rejected, so document *names* cannot traverse outside the context directory. (Symlinks deliberately placed inside `context/` — like a vault-managed `travel.md` — are followed, and `--context-dir` can point anywhere; both are intentional.)

By default the command resolves `context/` relative to the installed project, so it works from any current working directory. Override it when using another context checkout:

```bash
PERSONAL_CONTEXT_DIR=/path/to/context personal-context list
personal-context --context-dir /path/to/context get identity
# --context-dir is also accepted after the subcommand.
```

Precedence is `--context-dir`, then `PERSONAL_CONTEXT_DIR`, then this repository's `context/` directory.

### Using with Pi

A Pi skill can invoke `personal-context get ...` as a local shell command and pass the plain Markdown output to an agent. This change supplies only that portable CLI layer; a shared Pi/Claude skill can be added separately later.

### Bootstrap from existing writing (optional)

If you have a folder of markdown blog posts or writing samples, the ingest script can generate draft context files:

```bash
# Copy or symlink your markdown files
ln -s /path/to/your/blog/posts sources/blogs

# Generate drafts (--cutoff filters by date in frontmatter)
python scripts/ingest.py sources/blogs/ --cutoff 2024-01-01 --output drafts
```

This creates draft files in `drafts/` with excerpts grouped by category. Review them, extract what's useful, and write your curated versions into `context/`.

The ingest script expects markdown files with YAML frontmatter containing `title`, `pubDate`, and optionally `categories`. If your files don't have frontmatter, you can skip this step and write context files manually.

### Write your context files manually

If you don't have existing writing to ingest, just create the 6 files in `context/` yourself. Use this template:

```markdown
---
last_updated: 2026-05-30
source_refs: []
---

# Writing Style

## Voice
[How do you write? Conversational? Formal? Technical? Direct?]

## Patterns
[Recurring structures, transitions, vocabulary choices]

## Examples
[2-3 representative excerpts from your actual writing, with annotations]
```

A good approach: start a Claude Code session and ask it to interview you. Share writing samples and let it draft context files for you to review and edit.

### Connect to Claude Code

The existing FastMCP setup remains available for Claude Code and other MCP clients. The MCP server runs **entirely locally** — Claude Code spawns it as a subprocess on your machine, and it just reads markdown files from disk. No data is sent to external services beyond the normal Claude API calls.

Register the server as a **user-scoped** MCP so it's available in every Claude Code session, regardless of which project you're working in:

```bash
claude mcp add --scope user personal-context -- /absolute/path/to/personal-context/.venv/bin/python /absolute/path/to/personal-context/server.py
```

Restart Claude Code. The MCP tools will be available in every session.

### Setting up on another machine

To use the same context on a work machine or second computer:

1. Clone the repo: `git clone <your-fork> personal-context`
2. Install: `cd personal-context && uv venv && source .venv/bin/activate && uv pip install -e .`
3. Register the MCP server (update paths to match where you cloned it):
   ```bash
   claude mcp add --scope user personal-context -- /absolute/path/to/personal-context/.venv/bin/python /absolute/path/to/personal-context/server.py
   ```
4. Restart Claude Code

That's it — same context files, same tools, works in any repo you open. If you keep your context files committed, `git pull` on either machine keeps them in sync.

### Make it automatic

By default, Claude Code won't call MCP tools unless you ask. To have it pull your context automatically when drafting written content, add a rule to your global `~/.claude/CLAUDE.md`:

```markdown
## Personal Context (MCP)

When drafting any written content (emails, messages, docs, social posts, bios, etc.), call `get_writing_style` from the `personal-context` MCP server first to match my voice and tone. For tasks that benefit from broader context (introductions, project summaries, etc.), use `get_all_context` instead.
```

### Test it

In a new Claude Code session:
- Ask Claude to use your writing style to draft something
- It should automatically call `get_writing_style` or `get_all_context`
- Compare the output to how you actually write and iterate on your context files

## Adding Context Over Time

This system is manually curated, you update the files, this is not an automated pipeline.

### When to update

- **After publishing new writing** — review if it reveals patterns not yet captured in `writing-style.md`
- **After changing jobs/projects** — update `projects.md` and `identity.md`
- **After noticing the LLM gets your voice wrong** — the gap between output and expectation tells you what's missing
- **After adding new source material** — drop files in `sources/private/` or `sources/blogs/` (both gitignored), re-run ingest if helpful

### How to update

1. Edit the relevant `context/*.md` file directly
2. Update the `last_updated` date in frontmatter
3. Add any new source refs to `source_refs`
4. Re-read the **whole file**, not just your changes — see [Security](#security). Context files accumulate, so the risk is usually something added long ago and never re-read. When you find something, paraphrase rather than delete: the point being illustrated almost always survives, and the specifics are what create the exposure.
5. Check sibling files for contradictions. `get_all_context()` returns them together, so removing a claim from one file while another still asserts it produces an inconsistent picture.
6. Commit

### Private sources

Work emails, Slack exports, or other private writing go in `sources/private/` which is gitignored. You can reference them in `source_refs` for provenance without committing the content.

Since `sources/private/` is gitignored, these files are device-specific — they won't sync when you `git pull` on another machine. If you need the same private sources on multiple machines, copy them manually or sync via something outside git (e.g., iCloud, Dropbox).

### Private served context

`sources/private/` holds raw *source material* for ingest — it is **not** read by the MCP at runtime. If you have curated context that the MCP should serve but that must stay out of a public repo, put it in **`context/private.md`**, which is gitignored.

The MCP server globs `context/*.md`, so **any** gitignored file you add there (e.g. `context/private.md`, `context/travel.md`) is returned by MCP's `get_all_context()` and served locally, preserving its existing behavior. The CLI is deliberately more conservative: `get --all` returns only the six public documents unless `--include-private` is supplied. Git never commits these extra files. Like `sources/private/`, they are device-specific — copy them manually (or symlink them from a private repo) if you run the MCP on another machine.

Two things to know about this pattern:

- **Never put example/placeholder versions of private files in `context/`** — the glob would serve them to the model as if they were real. Public examples belong elsewhere, e.g. `docs/`.
- **A symlink works.** If you keep the real file in a private repo (for backup/sync), symlink it into `context/` — the server follows symlinks, and `.gitignore` keeps the link out of the public repo.

### Travel profile

An example of the private-served-context pattern: a durable travel profile (home airport, airline status, hard rules, output contract) so an agent planning a trip never starts cold. See [`docs/travel.example.md`](docs/travel.example.md) for the shape with placeholder values. The real one lives at `context/travel.md` (gitignored); loyalty account numbers stay in a password manager — the file points at where they live, it never stores digits.

### Re-running ingest

If you add new blog posts or writing samples to `sources/blogs/`:

```bash
python scripts/ingest.py sources/blogs/ --cutoff 2024-01-01 --output drafts
```

This regenerates drafts (in `drafts/`, also gitignored). Review the new excerpts and fold anything useful into your context files.

## MCP Tools

The server exposes:

| Tool/Resource | What It Does |
|--------------|-------------|
| `get_writing_style()` | Returns your writing-style.md — the most commonly needed file |
| `get_all_context()` | Returns all context files as a dict (includes a local `private.md` if present) |
| `context://{filename}` | Resource access to any individual file by name |

## Project Structure

```
personal-context/
├── context/           # Your curated context files (the product)
│   ├── private.md     # Optional private served context (GITIGNORED)
│   └── travel.md      # Optional travel profile, may be a symlink (GITIGNORED)
├── docs/
│   └── travel.example.md  # Public placeholder example (kept OUT of context/)
├── sources/
│   ├── blogs/         # Writing samples for ingest (GITIGNORED — symlink or copy)
│   └── private/       # Private writing samples (GITIGNORED)
├── scripts/
│   └── ingest.py      # Bootstrap drafts from existing writing
├── drafts/            # Generated drafts from ingest (GITIGNORED)
├── personal_context/
│   └── cli.py         # Local, standard-library CLI
├── tests/             # Tests for CLI, ingest script, and server
├── server.py          # FastMCP MCP server (unchanged interface)
├── pyproject.toml
└── README.md
```

## Running Tests

`pytest` is an optional `dev` dependency, so install it before the first run:

```bash
uv sync --extra dev      # or: pip install -e ".[dev]"
python -m pytest -v
```

## Security

This repo is designed to be public, but remember you are putting personal information in it. A few things to know:

- **Path traversal protection.** The `get_context` resource handler validates that requested filenames resolve inside the `context/` directory. Traversal attempts like `../../etc/passwd` are rejected.
- **Private sources are gitignored.** `sources/private/` is in `.gitignore` so work emails, Slack exports, etc. stay local. But be careful with `source_refs` in frontmatter — the filenames are committed even if the files aren't. Use opaque names like `work-email-1.md` instead of descriptive titles.
- **Private served context is gitignored.** `context/private.md` and `context/travel.md` are in `.gitignore` for curated context the MCP should serve locally but never commit (e.g. work-sensitive notes, travel preferences). They're the runtime-served counterpart to `sources/private/`. Keep placeholder examples of these files out of `context/` entirely — the server would serve them as real context.
- **Account numbers never go in served context.** Loyalty programs, traveler numbers, and similar identifiers stay in a password manager; context files may point at where they live but never contain the digits. An agent doesn't need them to plan, and you do the booking yourself.
- **Review your context files before committing.** These files are meant to be public, but watch for details you didn't intend to share: financial specifics, internal company information, health details, or anything useful for phishing. If in doubt, leave it out.
- **`.env` is gitignored.** If you extend this with API keys, they won't be committed accidentally.

## Philosophy

- **Start minimal.** 6 files was enough for me as a starting point. Add complexity only when you outgrow it.
- **Curate manually.** You know your voice better than any automated pipeline. The LLM can help draft, but you decide what stays.
- **Iterate from use.** The best edits come from noticing when the LLM gets something wrong about your writing.
- **Keep private things private.** The `sources/private/` directory exists so you can reference work writing without committing it.

TDQS

A3.8/5.0

Scored across 2 tools

Disambiguation4/5

The two tools are distinct: one retrieves all context files, the other retrieves a specific file. While get_all_context includes the writing style, the descriptions clarify the appropriate use case for each, so there is little confusion.

Naming Consistency5/5

Both tool names follow a consistent 'get_' prefix followed by a noun, making them predictable and easy to understand. The naming pattern is uniform.

Tool Count2/5

With only two tools, the set feels thin. The server's purpose is to provide personal context, but it only offers bulk retrieval and a single specific file. This is on the low end of the expected range and may be insufficient for flexible access.

Completeness2/5

The tools cover retrieval but lack any way to fetch individual context files other than writing style, forcing agents to fetch everything and parse. There is also no update or creation capability, so the surface is incomplete for a context management domain.

Maintenance

ActivityMaintained
ResponsivenessNo issues