<!--
Ownership marker for the official MCP registry. It must match `mcpName` in
package.json exactly, and it must be present in the *published* tarball —
the registry reads the README from npm, not from this repository. Without it
`mcp-publisher publish` rejects the server as unverified.

mcp-name: ch.stratta/mcp
-->

# @stratta/mcp

The local MCP server for [Stratta](https://stratta.ch): the norms and reference
documents your organization deposited (technical standards, laws, regulations,
accounting standards, internal references), cited to the section and page, inside
any agent that speaks MCP.

> Requires a free Stratta account: https://stratta.ch/sign-up.

> [!IMPORTANT]
> Your `STRATTA_API_KEY` is a **secret** — it grants read/write access to your
> Stratta workspace. Never commit it to a repository, paste it into a shared/
> project-scoped MCP config, or share it in logs. Prefer a user-scoped config or
> a shell environment variable. If a key leaks, revoke it immediately under
> Settings › API keys (https://stratta.ch/settings?tab=keys).

## Do you need this package?

Usually not. Stratta also runs as a **remote connector** — one address, a
browser sign-in, no key, no Node:

```
https://stratta.ch/mcp
```

That is the shorter path, the one the dashboard walks you through for Claude,
ChatGPT, Claude Code, Codex, Cursor, VS Code, Gemini CLI and Windsurf, and the
only one that works in an agent running in the cloud (claude.ai, ChatGPT). It
serves 61 of the 62 tools below, **ingestion included**: the pre-pass script is
downloaded from https://stratta.ch/ingest-prepass.py when it is not on disk.
See https://stratta.ch/docs/en/guides/connect-remote.

This package is what you want when:

- your agent **cannot open a browser** — CI, a scheduled task, a server — and
  authenticates with an API key instead;
- you want `add_attachment`, the one tool the connector does not serve: it
  uploads a file from your disk to a project dossier;
- you would simply rather run the server yourself.

## Lighter setup: `--simple`

By default the server announces all 59 tools (about 67,000 characters of
schemas). `--simple` announces only the 10 norm-reading tools (about 11,000),
which is enough for an agent that just reads norms:

```bash
claude mcp add stratta --scope user -- npx -y @stratta/mcp --simple
```

`--toolsets dossiers,sites` adds families (`norms` is always on; `dossiers`,
`sites`, `ingest`), `--full` restores everything, and the `STRATTA_TOOLSETS`
environment variable does the same. An unknown family is ignored. For the
remote connector, use the **Tools** menu on https://stratta.ch/connections.

## Install

### Claude Code (recommended)

Add the server — no key needed up front:

```bash
claude mcp add stratta --scope user -- npx -y @stratta/mcp
```

`--scope user` registers it for your user, so the tools are there in every
project. Without it, `claude mcp add` writes to the current folder's config and
the server exists nowhere else.

Then sign in. This opens stratta.ch in your browser, where you confirm which
machine and organisation to authorise; the key comes back to the terminal on
its own and is saved to `~/.stratta/config.json` (owner-only, `0600`). You
never see it, and you only do this once:

```bash
npx -y @stratta/mcp login
```

No browser on this machine — remote server, SSH, CI? `login --paste` asks for a
key from https://stratta.ch/settings?tab=keys instead, without echoing it.

If you skip the step entirely, Claude Code prompts you for a key on the first
tool call.

You can also pass the key explicitly as an environment variable (it then takes
precedence over the saved key):

```bash
claude mcp add stratta --scope user --env STRATTA_API_KEY=sk_strt_xxx -- npx -y @stratta/mcp
```

## Commands

| Command                             | Purpose                                            |
| ----------------------------------- | -------------------------------------------------- |
| `npx -y @stratta/mcp login`         | Authorise this machine from your browser           |
| `npx -y @stratta/mcp login --paste` | Paste a key, when there is no browser              |
| `npx -y @stratta/mcp logout`        | Remove the key stored on this machine              |
| `npx -y @stratta/mcp whoami`        | Which account and workspace this machine speaks as |
| `npx -y @stratta/mcp doctor`        | Diagnose an install that is not responding         |
| `npx -y @stratta/mcp update`        | Check your version and fetch the latest            |
| `npx -y @stratta/mcp --help`        | All of the above                                   |

Run `doctor` first when norms are missing: a revoked key, a stale
`STRATTA_API_KEY` in your shell and an unreachable backend all look identical
from inside the agent, and are fixed differently.

## Staying up to date

`npx -y @stratta/mcp` fetches the published package, but npx caches what it
resolved, so a server can keep launching a version npm replaced weeks ago with
nothing to tell you.

```bash
npx -y @stratta/mcp update
```

Prints the version you are running and the one npm serves, and fetches the new
one if they differ. Restart your agent afterwards. The server also mentions it
on stderr at startup when it notices it is behind.

### Claude Desktop

Sign in once from a terminal — the desktop app cannot prompt you interactively:

```bash
npx -y @stratta/mcp login
```

Then add the server to `claude_desktop_config.json` (Settings → Developer →
Edit Config) — no key needed in the file:

```json
{
  "mcpServers": {
    "stratta": {
      "command": "npx",
      "args": ["-y", "@stratta/mcp"]
    }
  }
}
```

Restart Claude Desktop. The Stratta tools should appear in the MCP indicator.

> Prefer to keep the key in the config instead of `~/.stratta/config.json`? Add
> an `"env": { "STRATTA_API_KEY": "sk_strt_xxx" }` block to the server entry —
> but that file then stores the key in plaintext, so keep it private and unsynced.

### Global install (optional)

```bash
npm install -g @stratta/mcp
```

Then use `"command": "stratta-mcp"` instead of `npx`.

## Configuration

The API key is resolved in this order: the `STRATTA_API_KEY` env var, then
`~/.stratta/config.json` (written by `login` or the first-run prompt). Other
settings come from environment variables — see [`.env.example`](./.env.example).

| Variable             | Required | Default              | Purpose                                                                                                                                                        |
| -------------------- | -------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `STRATTA_API_KEY`    | no¹      | –                    | API key from https://stratta.ch. ¹If unset, the server falls back to `~/.stratta/config.json`, or prompts you on first use (clients that support elicitation). |
| `STRATTA_CONVEX_URL` | no       | Stratta prod backend | Override only if you self-host.                                                                                                                                |

## Tests

`npm test` runs the unit tests. The two integration suites
(`tests/handlers.test.ts`, `tests/dossier-flow.e2e.test.ts`) run this checkout's
built server against the deployment you name, and are skipped when either
variable is missing:

```bash
npm run build
STRATTA_E2E_URL=https://<dev-deployment>.convex.cloud STRATTA_E2E_API_KEY=sk_strt_... npm test
```

Point them at a development deployment: the dossier flow creates a dossier
named `E2E dossier <timestamp>` and does not delete it.

## Tools exposed

62 tools, generated from one catalogue shared with the remote connector. This
package announces 59 of them; the remote connector serves 61: everything minus
`add_attachment`, plus the three tools a host calls for the person, listed last.

**Read** (10 tools — query the norms of your workspace):

| Tool              | Purpose                                                                                          |
| ----------------- | ------------------------------------------------------------------------------------------------ |
| `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first.           |
| `whoami`          | Which organisation, role and plan this connection reads as, and how many norms it sees.          |
| `list_norms`      | List the norms of your workspace, filtered by `query` (code or title words), bounded by `limit`. |
| `get_toc`         | Hierarchical TOC for a norm (default `maxDepth=1` = chapters).                                   |
| `get_subtree`     | Drill into a chapter/section subtree (`path` + `maxDepth`).                                      |
| `get_section`     | Full enriched content of a section (formulas, tables, figures, cross-refs).                      |
| `search_in_norm`  | Keyword search inside a norm.                                                                    |
| `search_corpus`   | Keyword search across every norm of the workspace, grouped by norm.                              |
| `get_figure`      | Retrieve a figure inline (base64 ImageContent).                                                  |
| `get_cross_refs`  | Outgoing cross-refs from a section to other norms.                                               |

**Site** (10 tools — what public Swiss registers know about a plot, and the site sheet as the app shows it):

| Tool                       | Purpose                                                                                                                             |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `scan_site`                | Collect municipality, parcel, elevation, geology, nearby boreholes, polluted sites, hazards and noise class around a Swiss address. |
| `get_site_context`         | Read what the scan settled, and — separately — what it could not, with the reason.                                                  |
| `get_boreholes`            | Read the boreholes nearest the site with their logged strata, SIA 261 ground class, water table, and links to cantonal documents.   |
| `list_sites`               | The site sheets of the organisation, with their dossier, canton and scan status.                                                    |
| `get_site`                 | The sheet as the app shows it: every fact with its key, status, source, notice and attribution, and the scan with its sources.      |
| `get_site_section`         | The section along the axis: terrain, modelled bedrock, geology bands and the boreholes of the corridor with their logs.             |
| `list_site_overlays`       | The map areas around the point (polluted sites, hazards, water protection), with distance and coverage of the site point.           |
| `get_groundwater_contours` | The cantonal piezometric contours stored by the scan, with the aquifer name and elevation.                                          |
| `list_site_photos`         | The photos uploaded on the sheet, with position, direction, instant and note. No image, no link.                                    |
| `list_site_neighbours`     | The organisation's other sheets within a radius, with the distance and the geological unit their scan established.                  |

**Dossier** (27 tools — what a project reads, retains and produces: questions, evidence, decisions, attachments, templates, and the deliverable written section by section):

| Tool                   | Purpose                                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| `list_dossiers`        | Your organisation's dossiers, most recently touched first, with open-question counts and site id.         |
| `open_dossier`         | Open a project's dossier, creating it if needed. Idempotent on the name.                                  |
| `open_question`        | Open one question to settle, with optional named options. Idempotent on the title.                        |
| `save_finding`         | Record one piece of evidence: a cited article, a retained value and why, an observation.                  |
| `record_decision`      | Settle a question with a decision the user has confirmed, and the retained option.                        |
| `load_dossier`         | Reload everything: questions with their evidence and decisions, open ones first.                          |
| `resolve_question`     | Close a question without a decision, or reopen one. The evidence stays.                                   |
| `list_attachments`     | The project attachments of a dossier: site reports, borehole logs, minutes, data sheets.                  |
| `read_attachment`      | Read an attachment's text as Markdown, page by page.                                                      |
| `search_in_dossier`    | Full-text search over a dossier's attachments, with the page of each hit.                                 |
| `add_attachment`       | Upload a file from the user's machine to a dossier (PDF, DOCX, XLSX, images, text). Local server only.    |
| `list_templates`       | The checklists the organisation wrote for its types of structure.                                         |
| `apply_template`       | Open a template's questions in a dossier and file the clauses that resolve in the corpus.                 |
| `draft_deliverable`    | Open the document a dossier produces (project basis, use agreement, report) and get its plan and sources. |
| `write_section`        | Write one section of a deliverable with what it rests on; a section without a source is refused.          |
| `list_questions`       | The questions of a dossier, paginated, with status, assignee, due date, options and decision title.       |
| `get_question`         | One question in full: options, decision, evidence with every citation field, comments with authors.       |
| `get_dossier_activity` | The history of a dossier, most recent first, paginated: who did what, from the app or an agent.           |
| `list_exports`         | The verification notes and journals exported from a dossier, with hash and trusted timestamp.             |
| `update_question`      | Reword a question, assign it by e-mail address, set or clear its due date.                                |
| `add_option`           | Add one way of settling a question. Idempotent on the name.                                               |
| `update_option`        | Rename or describe an option, or mark it retained (the others are released).                              |
| `delete_option`        | Remove an option; its evidence stays on the question. Asks the user first.                                |
| `delete_question`      | Delete a question opened by mistake; its evidence stays, unfiled. Asks the user first.                    |
| `add_comment`          | Leave a signed remark on a dossier, a question or an entry.                                               |
| `update_entry`         | Correct an entry's wording, value, confidence or citation in place.                                       |
| `attach_entry`         | File an entry under a question and optionally an option, or unfile it.                                    |

A dossier is read, annotated, reviewed and exported from
[stratta.ch/dossiers](https://stratta.ch/dossiers): four tabs (Terrain,
Lectures, Décisions, Livrables). Every section the agent reads for a named
project is logged under Lectures without a gesture; a decision can be recorded
in one sentence (`record_decision` with `dossierId` and `question`); the
deliverable is draft, in review or signed, exported to Word inside the office's
own report, and signing never goes through an agent.

**Library** (2 tools — the written procedures an agent follows for a repeatable task):

| Tool          | Purpose                                                                                                       |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| `list_skills` | The procedures the agent follows for a repeatable task: the ones Stratta ships and the ones the office wrote. |
| `get_skill`   | Read one procedure in full; the office version wins over the Stratta one of the same name.                    |

An office skill with the same name as a shipped one replaces it for every agent
of the organisation. A report template learned from an office Word file on
[stratta.ch/bibliotheque](https://stratta.ch/bibliotheque) is served as a skill
too, and its file is the layout the Word export is rendered into.

**Ingest** (10 tools — add YOUR documents; driven by the bundled `ingest-norm` skill; owner or admin role):

| Tool                          | Purpose                                                                          |
| ----------------------------- | -------------------------------------------------------------------------------- |
| `ingest_status`               | Check if a norm already exists in your workspace, and its coverage.              |
| `ingest_create_document`      | Create a draft norm document.                                                    |
| `ingest_create_sections`      | Bulk-insert sections (returns `nodeId → sectionId` map).                         |
| `ingest_attach_formula`       | Attach a LaTeX formula to a section.                                             |
| `ingest_attach_table`         | Attach a structured table `{headers, rows}` to a section.                        |
| `ingest_attach_cross_ref`     | Attach an explicit cross-ref to another norm.                                    |
| `ingest_upload_figure`        | Upload a figure (base64 PNG/JPEG/WebP, ≤ 8 MB) to a section.                     |
| `ingest_normalize_cross_refs` | Auto-detect and rebuild cross-refs from section content.                         |
| `ingest_publish`              | Score the document and flip it to published; refuses below 30/100 unless forced. |
| `ingest_delete`               | Delete a document and all its children.                                          |

**Remote only** (3 tools, served by `https://stratta.ch/mcp` and not by this package, which draws no interface):

| Tool               | Purpose                                                                                                      |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `show_citations`   | Show the clauses an answer cites as cards (an MCP App), each quote checked word for word against the clause. |
| `open_norm_reader` | The norm reader, opened by the person from the host's side panel or sidebar; hidden from the model.          |
| `get_profile`      | An opaque id and the organization of the connection, so a host tells two connected accounts apart.           |

## How agents should use it

For querying:

1. Call `get_methodology` first — load the behavioural contract.
2. Call `list_norms` to see what's available in your workspace.
3. Call `get_toc(norm)` to navigate the structure; `get_subtree` to drill in.
4. Call `get_section(norm, path)` to read specific content.
5. Use `search_corpus` when the norm is unknown, `search_in_norm` when the section path is.
6. Follow `crossRefs` for compound questions (e.g. SIA 261 → SIA 263 → EC).
7. Call `get_figure` when the section references a figure relevant to the answer.

Nine skills ship in the package, under `skills/`: **`consult-stratta`** (the
working method an agent follows to answer from the norms, generated from the
same text the server serves through `get_methodology`), **`ingest-norm`**,
**`project-basis`** and **`use-agreement`** (the two SIA 260 documents, written
with `draft_deliverable` and `write_section`), **`site-chapter`**,
**`instruct-structure`**, **`review-open-questions`**, **`handoff-to-word`** and
**`verification-note`**. The same nine are served to every client by
`list_skills` and `get_skill`, so a client that installs no skills folder reads
them anyway; an office skill of the same name replaces the shipped one.

## Resources and prompts

Five `stratta://` resources can be pinned to a conversation or read without a
tool call, each scoped to your workspace: `stratta://norms` (JSON),
`stratta://methodology` (Markdown), `stratta://norm/{code}/toc` (the table of
contents to depth 2, code URL-encoded, e.g. `stratta://norm/SIA%20267/toc`),
`stratta://dossier/{id}` (a project dossier as Markdown) and
`stratta://skills/{name}` (one skill as Markdown, the office version when there
is one). The same five are served by the remote connector.

Three prompts appear as slash commands in clients that support them:
`investigate-question` (question, project?), `resume-dossier` (name) and
`check-value` (value, norm?).

For depositing your own documents, see the bundled **`ingest-norm` skill** —
a 2-phase hybrid pipeline (since 0.4.0): a Python pre-pass
(`scripts/ingest-prepass.py`, PyMuPDF) extracts the hierarchical tree and
rasterizes figures deterministically, then the agent enriches sections with
summaries, LaTeX formulas, tables and cross-refs via targeted visual reading.
Requires Python ≥ 3.10 with PyMuPDF (`python -m pip install --user pymupdf`).

## Troubleshooting

### `Authentication failed` / `Invalid API key`

- Verify the key starts with `sk_strt_` and is not revoked at https://stratta.ch/settings?tab=keys.
- Check the env var is reaching the process: `echo $STRATTA_API_KEY` (or `$env:STRATTA_API_KEY` on Windows PowerShell).
- If you copied from the UI, make sure no leading/trailing whitespace was added.

### `ECONNREFUSED` / network errors

- Confirm outbound HTTPS to `*.convex.cloud` is allowed by your firewall/VPN.
- Try `curl -I https://stratta.ch` to verify general internet reachability.

### Tools don't appear in your agent

- Restart your Claude client after editing the config.
- Check the MCP server logs (Claude Code: `claude mcp logs stratta`; Claude Desktop: `~/Library/Logs/Claude/mcp-server-stratta.log` on macOS).
- Make sure your Node.js version is `>=20` (`node --version`).

### Rate-limited

- Each user can create up to 50 active keys and 20 new keys per 24h. Revoke unused keys in the dashboard.

### `QUOTA_EXCEEDED`

Your organization reached one of its limits. The error names the dimension, your
current count and the plan limit. Retrying will fail identically.

| Limit           | Free  | Pro    | Max    |
| --------------- | ----- | ------ | ------ |
| Norms           | 1     | 15     | 60     |
| Sections        | 1,000 | 15,000 | 60,000 |
| Figures         | 60    | 750    | 3,000  |
| Queries / month | 500   | 10,000 | 40,000 |
| Members         | 1     | 1      | 1      |
| Site sheet      | –     | –      | ✓      |

Beyond the included queries, paid plans bill the overage per thousand. An
Enterprise contract scales seats, norms and queries further and includes the
site sheet; the calculator is at https://stratta.ch/tarifs. Dossiers,
deliverables and the library are on every plan; `scan_site` and the site tools
answer `PLAN_REQUIRED` below Max. A tool call weighs a whole number of queries:
a light read 1, a section or a sheet 2, a search 5, a borehole PDF read 10, a
site scan 100.

Stock limits free up when you delete a norm (`ingest_delete`). The monthly query
counter resets on its own. Gauges live on the Workspace page of your dashboard,
and an org admin can also set caps below the plan. Full details:
https://stratta.ch/docs/en/account/plans

## License

Proprietary — © Stratta, Lausanne. All rights reserved. This package is the
official Stratta MCP client; redistribution, modification, or reuse of the source
is not permitted without prior written consent.
