Skip to main content
Glama
osick
by osick
README.md
# sair-mcp

A local MCP (Model Context Protocol) server that exposes the [SAIR
Competition Public API](https://docs.sair.foundation/docs/) as tools for an
MCP client (Claude Code, Claude Desktop, or any other MCP-compatible
client).

It covers the full documented API surface: account introspection,
competitions (browsing, eligibility, leaderboards, submissions, IGP24 label
progress), Playground (models, problem sets, runs, results, live event
streaming, usage), your private cheatsheet and Lean solver-template
libraries, custom Lean problems, and the Contributor Network (browsing,
publishing, favoriting, commenting, lineage graphs, benchmarks).

## Installation

Requires Python 3.10+.

```bash
git clone <this-repo>
cd SAIRmcp
pip install -e .
```

## Getting a SAIR API key

1. Sign in to SAIR and open **Account settings -> API keys**.
2. Create a key and select the scopes you need. At minimum, for the
   examples below: `competition.read` and `competition.write`. Add
   `playground.read` / `playground.write` for Playground tools,
   `contributor-network.read` / `contributor-network.write` for
   Contributor Network tools.
3. Copy the key immediately -- it is shown only once. Keys look like
   `sair_a1b2c3d4_zR9...`.

## Configuration

Set these environment variables before starting the server:

| Variable | Required | Default | Purpose |
|---|---|---|---|
| `SAIR_API_KEY` | Yes | - | Your personal SAIR API key. The server refuses to start without it. |
| `SAIR_BASE_URL` | No | `https://api.sair.foundation/api/public/v1` | Override for testing against a different environment. |

## Running it standalone

```bash
python -m sair_mcp
```

This starts the server on stdio and blocks, waiting for an MCP client to
connect. There is no output on success; it's meant to be launched by an
MCP client, not run interactively.

## Registering with Claude Code and other mcp clients

Add to your project's `.mcp.json` (or user-level MCP config)

```json
{
  "mcpServers": {
    "sair": {
      "command": "python",
      "args": ["-m", "sair_mcp"],
      "env": {
        "SAIR_API_KEY": "sair_a1b2c3d4_zR9..."
      }
    }
  }
}
```

Or via the CLI, substituting the same absolute path:

```bash
claude mcp add sair -- python3 -m sair_mcp
```

(then set `SAIR_API_KEY` in your shell environment before launching Claude
Code, or add it to the `env` block above).

**Alternative:** `pyproject.toml` also declares a `sair-mcp` console-script
entry point, so once installed you can skip `-m sair_mcp` entirely and
point `command` straight at that script's absolute path instead -- again,
resolve it in the same command as the install so it can't drift to a
different environment's `sair-mcp`:

```bash
pip install -e . && which sair-mcp
```

No `args` needed when using this form.

**If you use a dedicated virtual environment** for this project (recommended
for isolation from other Python projects), the same rule applies: use
`<path-to-venv>/bin/python` (or `<path-to-venv>/bin/sair-mcp`) as `command`,
not a bare `python`.

## Registering with Claude Desktop

Add the same shape to `claude_desktop_config.json` under `mcpServers`,
using the same absolute-interpreter-path `command` as above -- Desktop's
subprocess environment is even less likely than a terminal's to have your
Python environment on `PATH`.

## Usage example

A typical end-to-end flow, as an MCP client would call it:

1. `get_me` -- confirm the key is valid and see its scopes.
2. `list_competitions` -- find a competition ID.
3. `get_competition("modular-arithmetic-challenge")` -- read `submissionSpec`
   to learn the required `payload` shape.
4. `get_my_participation("modular-arithmetic-challenge")` -- check
   `canSubmit` before attempting anything.
5. `submit_competition_entry("modular-arithmetic-challenge", payload={"modelName": "you/your-model", "commitHash": "0123...(40 hex chars)"})`.
6. `get_my_submission("modular-arithmetic-challenge")` -- confirm the
   active submission.

## Tool reference

All tools return the API's unwrapped `data` object as a dict (or `None` for
`204` responses, or a plain string for the two text-returning tools). On
error, the tool call fails with a `SairApiError`-derived message in the
form `[<http status> <error code>] <message>`; consult
[SAIR's Errors documentation](https://docs.sair.foundation/docs/errors/)
for the full code table.

### Account (1 tool)

| Tool | Scope | Purpose |
|---|---|---|
| `get_me` | none | Confirm the API key and see its owner + granted scopes. |

### Competitions (10 tools)

| Tool | Scope | Purpose |
|---|---|---|
| `list_competitions` | `competition.read` | List competitions visible through the API. |
| `get_competition` | `competition.read` | Get one competition's detail and `submissionSpec`. |
| `get_my_participation` | `competition.read` | Check `canSubmit` / `submitBlockedReason` for one competition. |
| `get_leaderboard` | `competition.read` | List published leaderboard rows. |
| `get_my_leaderboard_standing` | `competition.read` | Get the caller's own leaderboard entry. |
| `submit_competition_entry` | `competition.write` | Create or replace the caller's submission. |
| `get_my_submission` | `competition.read` | Read the caller's current submission (or history, for IGP24). |
| `get_submission_by_id` | `competition.read` | Read one submission by ID. |
| `download_submission_text` | `competition.read` | Download the stored text body of a submission. |
| `get_igp24_label_progress` | `competition.read` | Bulk aggregate IGP24 discovery progress by label. |

### Playground (10 tools)

| Tool | Scope | Purpose |
|---|---|---|
| `list_playground_models` | `playground.read` | List model IDs accepted in runs for a competition. |
| `list_playground_problem_sets` | `playground.read` | List available practice problem sets. |
| `get_playground_problem_set` | `playground.read` | Get every problem in one problem set. |
| `submit_playground_run` | `playground.write` | Create a practice run. |
| `list_playground_runs` | `playground.read` | List the caller's runs for a competition. |
| `get_playground_run` | `playground.read` | Get one run's params, summary, and timing. |
| `cancel_playground_run` | `playground.write` | Cancel a pending/running run. |
| `list_playground_run_results` | `playground.read` | List per-cell/per-problem result rows for a run. |
| `stream_playground_run_events` | `playground.read` | Collect live verdict events for a solver-participation run (blocks up to a timeout). |
| `get_playground_usage` | `playground.read` | Get the caller's practice credit/usage counters. |

### Cheatsheets (5 tools)

| Tool | Scope | Purpose |
|---|---|---|
| `list_cheatsheets` | `playground.read` | List the caller's cheatsheets. |
| `get_cheatsheet` | `playground.read` | Get one cheatsheet including its content. |
| `create_cheatsheet` | `playground.write` | Create a cheatsheet. |
| `update_cheatsheet` | `playground.write` | Update a cheatsheet's title/content. |
| `delete_cheatsheet` | `playground.write` | Delete a cheatsheet. |

### Solver templates (5 tools)

| Tool | Scope | Purpose |
|---|---|---|
| `list_solver_templates` | `playground.read` | List the caller's Lean solver templates. |
| `get_solver_template` | `playground.read` | Get one solver template including its source. |
| `create_solver_template` | `playground.write` | Create a solver template. |
| `update_solver_template` | `playground.write` | Update a solver template's title/source. |
| `delete_solver_template` | `playground.write` | Delete a solver template. |

### Custom Lean problems (4 tools)

| Tool | Scope | Purpose |
|---|---|---|
| `list_custom_problems` | `playground.read` | List the caller's private Lean statement pairs. |
| `get_custom_problem` | `playground.read` | Get one custom problem. |
| `create_custom_problem` | `playground.write` | Create an immutable custom Lean problem. |
| `delete_custom_problem` | `playground.write` | Delete a custom problem. |

### Contributor Network (13 tools)

| Tool | Scope | Purpose |
|---|---|---|
| `list_contributor_network_items` | `contributor-network.read` | Browse shared cheatsheets/solver templates/model references. |
| `get_contributor_network_item` | `contributor-network.read` | Get one item's full payload. |
| `publish_contributor_network_item` | `contributor-network.write` | Publish a saved resource, submission body, inline text, or model reference. |
| `withdraw_contributor_network_item` | `contributor-network.write` | Withdraw (unpublish) an item you authored. |
| `favorite_contributor_network_item` | `contributor-network.write` | Favorite an item. |
| `unfavorite_contributor_network_item` | `contributor-network.write` | Unfavorite an item. |
| `list_contributor_network_comments` | `contributor-network.read` | List comments on an item. |
| `create_contributor_network_comment` | `contributor-network.write` | Post a comment. |
| `delete_contributor_network_comment` | `contributor-network.write` | Delete a comment. |
| `get_contributor_network_item_graph` | `contributor-network.read` | Get the lineage subgraph rooted at one item. |
| `get_contributor_network_global_graph` | `contributor-network.read` | Get the lineage graph across all visible items. |
| `list_contributor_network_benchmarks` | `contributor-network.read` | List platform-run benchmark summaries. |
| `get_contributor_network_item_benchmark` | `contributor-network.read` | Get the per-model benchmark breakdown for one item. |

**Total: 48 tools.**

## Development

```bash
pip install -e ".[dev]"
pytest --cov=sair_mcp --cov-report=term-missing
```

All tests run against a mocked HTTP layer (`respx`) -- no test requires or
uses a real `SAIR_API_KEY` or makes a real network call.

## License

MIT -- see [LICENSE](./LICENSE).

TDQS

B3.4/5.0

Scored across 48 tools

Disambiguation4/5

The tools are mostly distinct, each targeting specific resources and actions. However, the high number of tools (48) introduces some complexity, and a few like 'get_my_submission' vs 'get_submission_by_id' could cause minor confusion despite different purposes.

Naming Consistency5/5

Tool names consistently follow the `verb_noun` snake_case pattern (e.g., create_cheatsheet, list_playground_runs). The verbs are appropriately descriptive (create, get, list, delete, update, submit, etc.), and there is no mixing of conventions.

Tool Count2/5

With 48 tools, the set is significantly larger than the recommended 3-15 range, indicating over-scoping. While the domain is broad, many operations could be consolidated, and the sheer volume risks overwhelming agents.

Completeness4/5

The tool surface covers CRUD operations for core resources (cheatsheets, solver templates, custom problems, contributor network items) and key workflows (competition submissions, playground runs). Minor gaps exist, such as the lack of a tool to list all runs across competitions, but the set is largely complete for its intended domain.

Maintenance

ActivityStale
ResponsivenessNo issues