Metadata-Version: 2.4
Name: gcontext-mcp
Version: 0.3.1
Summary: gcontext connector — local MCP bridge: cloud structure, local secret values
Project-URL: Homepage, https://gcontext.ai
License-Expression: MIT
Requires-Python: >=3.11
Requires-Dist: keyring>=25
Requires-Dist: mcp<2,>=1
Requires-Dist: python-dotenv>=1
Description-Content-Type: text/markdown

# mcp-minimal

A single local Python MCP server for a workspace of **integrations** (third-party
services the AI can use: docs, dependencies, secret names) and **tasks** (units of
work with goals, steps and progress). Both are typed folders in a virtual file tree,
backed by a secret-name registry and on-the-fly Python script execution with the
local `.env` injected. Secret VALUES never leave this machine and never enter the
database.

> **Self-hosting?** See [SELF_HOSTING.md](./SELF_HOSTING.md): one `docker compose up`, plus how to pull future updates.

## Setup

```bash
cd apps/mcp-minimal
cp .env.example .env   # fill in your secret values
```

Get a token first by running `uvx gcontext-mcp login` (browser sign-in at
https://app.gcontext.ai mints it).

### Add to Claude Code

Published connector (hosted cloud):

```bash
claude mcp add-json gcontext '{
  "type": "stdio",
  "command": "uvx",
  "args": ["gcontext-mcp"],
  "env": { "GCONTEXT_TOKEN": "<your token>" }
}'
```

Pure-local (no cloud, own SQLite):

```bash
claude mcp add mcp-minimal -- uv run --directory /ABS/PATH/TO/apps/mcp-minimal python server.py
```

### Add to Claude Desktop

The CLI commands above only work in Claude Code. For the Desktop app, edit the
config file directly (Settings → Developer → Edit Config, or open it from):

- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

Add your server under `mcpServers`, then fully quit and reopen Claude Desktop.

Published connector (hosted cloud):

```json
{
  "mcpServers": {
    "gcontext": {
      "command": "uvx",
      "args": ["gcontext-mcp"],
      "env": { "GCONTEXT_TOKEN": "<your token>" }
    }
  }
}
```

Pure-local (no cloud, own SQLite):

```json
{
  "mcpServers": {
    "mcp-minimal": {
      "command": "uv",
      "args": ["run", "--directory", "/ABS/PATH/TO/apps/mcp-minimal", "python", "server.py"]
    }
  }
}
```

Note: Desktop has no slash-command prompts (`/use-integration`, etc.) — you drive
the same tools by plain chat. Both modes need `uv`/`uvx` on your PATH.

## Tools

- `tool_list_dir(path="/")`, `tool_read_file(path)`, `tool_write_file(path, content)`,
  `tool_delete(path)` — `tool_write_file` auto-creates missing parent folders
- `tool_secrets()`, `tool_register_secret(name, description)`, `tool_unregister_secret(name)`,
  `tool_setup_secrets(form=True)`
- `tool_run_script(code)` - runs `uv run --env-file .env python -c "<code>"`

## The secret registry

The registry holds secret NAMES + descriptions only — it is for **setup and
verification**, not runtime. It does NOT gate `tool_run_script`, which injects the
whole `.env` regardless of what's registered.

1. `tool_register_secret(name, description)` - declare a required secret.
2. `tool_setup_secrets(form=False)` - append blank `NAME=` lines to `.env` for any registered
   secret not yet present, so the user just fills in the values (`form=True` opens a browser form).
3. `tool_secrets()` - shows `present` per name so you can confirm setup.

## How it works

1. Write a file describing a 3rd-party operation and which secret NAMES it needs;
   declare those names with `tool_register_secret`.
2. To act, read the file, generate Python, and call `tool_run_script`.
3. Secret values resolve from the local `.env` at run time - never stored in the DB.

## Security / trust model

`tool_run_script` runs **arbitrary Python locally with your real `.env` injected** —
there is no sandbox. It is exactly as trusted as whatever drives the server. Run it
on your own machine only; never expose this server remotely.

## Script contract

- Read secrets via `os.environ["VAR"]` - never hardcode, never `load_dotenv`.
- Use only registered names that show `present_locally: true`.
- Exit codes: `0` OK, `2` missing secret (`KeyError`), `1` any other failure.

## Config (env vars)

- `MCP_MINIMAL_DB` - SQLite path (default `db.sqlite` next to `server.py`).
- `MCP_MINIMAL_ENV_FILE` - secret-values file (default `.env` next to `server.py`).

## Testing

Every test file is a standalone self-check: run it alone and it either prints ok or an
assert blows up with the offending value. One OS process per file (several mutate
`os.environ` at import), no shared fixtures, no framework, except `test_billing.py`,
which is pytest because it needs `monkeypatch`.

Run everything: `make test`. The journey test needs the local docker Postgres
(`make pg`); it skips itself with a message when Postgres is down.

Three rings, inside out:

1. **Pure logic**, no I/O at all.
2. **One component, boundary faked**, the unit under test is real, the thing it talks
   to (cloud RPC, DB, keychain) is stubbed in-file.
3. **Full stack**, the real connector talking HTTP to the real backend on a throwaway
   database.

| File | What it verifies | Ring / boundary |
|---|---|---|
| `test_digest.py` | `digest.py`: frontmatter parsing, folder type detection, staleness tiers, `compute_digest` end to end | 1, pure |
| `test_run_script.py` | `server.py` script helpers: secret scrubbing (`_scrub`), PEP 723 inline deps (`_inline_deps`) | 1, pure |
| `test_read_tools.py` | `server.py` read path: partial reads (`_read_view`), `_glob_paths`, `read_many` batching + 200KB cap, grep truncation flag | 2, cloud `_rpc` stubbed |
| `test_commands.py` | `server.py` dynamic `/_commands/*.md` MCP prompts: frontmatter split, listing, `{{input}}` substitution, cloud-down fallback | 2, cloud stubbed |
| `test_secrets_form.py` | `server.py` secret store: keychain writes, `.env` fallback + legacy migration, blank-unsets | 2, in-memory keyring |
| `test_token_login.py` | `cloud_api.py` self-host token login (container shape: no `.env`, no OAuth creds) | 2, env cleared |
| `test_grep.py` | `cloud_api.py` search: substring/regex/context/glob/pagination, `_tree_entries` folder typing | 2, DB stubbed |
| `test_context_index.py` | `cloud_api.py` connect-payload index builder (`_index_lines`) | 2, DB stubbed |
| `test_dashboard_rpc.py` | `cloud_api.py` dashboard batch-read path: `read_many`, `overview.mtimes` | 2, DB stubbed |
| `test_billing.py` | `cloud_api.py` `billing_info`: free vs pro, payment link config (pytest) | 2, fake DB conn |
| `test_journey.py` | The whole create-integration flow an AI agent drives: write module files, register secret, scaffold `.env`, set value, presence, `run_script` probe with auto deps + scrubbing, typed-folder visibility, delete | 3, real `server.py` to HTTP `/rpc` to `cloud_api.py` to throwaway Postgres |
