@stratta/mcp 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,216 +1,216 @@
1
- <!--
2
- Ownership marker for the official MCP registry. It must match `mcpName` in
3
- package.json exactly, and it must be present in the *published* tarball —
4
- the registry reads the README from npm, not from this repository. Without it
5
- `mcp-publisher publish` rejects the server as unverified.
6
-
7
- mcp-name: ch.stratta/mcp
8
- -->
9
-
10
- # @stratta/mcp
11
-
12
- MCP server exposing Swiss engineering norms (SIA / Eurocodes) to Claude clients via Stratta TreeRAG.
13
-
14
- > Requires a free Stratta account. Sign up at https://stratta.ch and generate an API key at https://stratta.ch/api-keys.
15
-
16
- > [!IMPORTANT]
17
- > Your `STRATTA_API_KEY` is a **secret** — it grants read/write access to your
18
- > Stratta workspace. Never commit it to a repository, paste it into a shared/
19
- > project-scoped MCP config, or share it in logs. Prefer a user-scoped config or
20
- > a shell environment variable. If a key leaks, revoke it immediately at
21
- > https://stratta.ch/api-keys.
22
-
23
- ## Install
24
-
25
- ### Claude Code (recommended)
26
-
27
- Add the server — no key needed up front:
28
-
29
- ```bash
30
- claude mcp add stratta --scope user -- npx -y @stratta/mcp
31
- ```
32
-
33
- `--scope user` registers it for your user, so the tools are there in every
34
- project. Without it, `claude mcp add` writes to the current folder's config and
35
- the server exists nowhere else.
36
-
37
- Then sign in. This opens stratta.ch in your browser, where you confirm which
38
- machine and organisation to authorise; the key comes back to the terminal on
39
- its own and is saved to `~/.stratta/config.json` (owner-only, `0600`). You
40
- never see it, and you only do this once:
41
-
42
- ```bash
43
- npx -y @stratta/mcp login
44
- ```
45
-
46
- No browser on this machine — remote server, SSH, CI? `login --paste` asks for a
47
- key from https://stratta.ch/api-keys instead, without echoing it.
48
-
49
- If you skip the step entirely, Claude Code prompts you for a key on the first
50
- tool call.
51
-
52
- You can also pass the key explicitly as an environment variable (it then takes
53
- precedence over the saved key):
54
-
55
- ```bash
56
- claude mcp add stratta --scope user --env STRATTA_API_KEY=sk_strt_xxx -- npx -y @stratta/mcp
57
- ```
58
-
59
- ### Claude Desktop
60
-
61
- Sign in once from a terminal — the desktop app cannot prompt you interactively:
62
-
63
- ```bash
64
- npx -y @stratta/mcp login
65
- ```
66
-
67
- Then add the server to `claude_desktop_config.json` (Settings → Developer →
68
- Edit Config) — no key needed in the file:
69
-
70
- ```json
71
- {
72
- "mcpServers": {
73
- "stratta": {
74
- "command": "npx",
75
- "args": ["-y", "@stratta/mcp"]
76
- }
77
- }
78
- }
79
- ```
80
-
81
- Restart Claude Desktop. The Stratta tools should appear in the MCP indicator.
82
-
83
- > Prefer to keep the key in the config instead of `~/.stratta/config.json`? Add
84
- > an `"env": { "STRATTA_API_KEY": "sk_strt_xxx" }` block to the server entry —
85
- > but that file then stores the key in plaintext, so keep it private and unsynced.
86
-
87
- ### Global install (optional)
88
-
89
- ```bash
90
- npm install -g @stratta/mcp
91
- ```
92
-
93
- Then use `"command": "stratta-mcp"` instead of `npx`.
94
-
95
- ## Configuration
96
-
97
- The API key is resolved in this order: the `STRATTA_API_KEY` env var, then
98
- `~/.stratta/config.json` (written by `login` or the first-run prompt). Other
99
- settings come from environment variables — see [`.env.example`](./.env.example).
100
-
101
- | Variable | Required | Default | Purpose |
102
- | -------------------- | -------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
- | `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). |
104
- | `STRATTA_CONVEX_URL` | no | Stratta prod backend | Override only if you self-host. |
105
-
106
- ## Tools exposed
107
-
108
- **Read** (8 tools — query norms in your workspace):
109
-
110
- | Tool | Purpose |
111
- | ----------------- | -------------------------------------------------------------------------------------- |
112
- | `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first. |
113
- | `list_norms` | List all norms published in your workspace (code, year, title, language). |
114
- | `get_toc` | Hierarchical TOC for a norm (default `maxDepth=1` = chapters). |
115
- | `get_subtree` | Drill into a chapter/section subtree (`path` + `maxDepth`). |
116
- | `get_section` | Full enriched content of a section (formulas, tables, figures, cross-refs). |
117
- | `search_in_norm` | Keyword search inside a norm. |
118
- | `get_figure` | Retrieve a figure inline (base64 ImageContent) + public URL. |
119
- | `get_cross_refs` | Outgoing cross-refs from a section to other norms. |
120
-
121
- **Dossier** (5 tools — keep what was decided on a project):
122
-
123
- | Tool | Purpose |
124
- | ------------------ | ------------------------------------------------------------------------------------------------- |
125
- | `list_dossiers` | Your organisation's dossiers, most recently touched first, with open counts. |
126
- | `open_dossier` | Open a project's dossier, creating it if needed. Idempotent on the name. |
127
- | `save_finding` | Record one decision: a cited article, a retained value and why, an observation, an open question. |
128
- | `load_dossier` | Reload everything, unresolved questions first. Accepts the id or the name. |
129
- | `resolve_question` | Mark a question settled. The entry stays; it stops surfacing at the top. |
130
-
131
- A dossier is read, annotated, reviewed and exported from
132
- [stratta.ch/dossiers](https://stratta.ch/dossiers).
133
-
134
- **Ingest** (10 tools — add YOUR licensed norms; driven by the bundled `ingest-norm` skill):
135
-
136
- | Tool | Purpose |
137
- | ----------------------------- | ------------------------------------------------------------ |
138
- | `ingest_status` | Check if a norm already exists in your workspace. |
139
- | `ingest_create_document` | Create a draft norm document. |
140
- | `ingest_create_sections` | Bulk-insert sections (returns `nodeId → sectionId` map). |
141
- | `ingest_attach_formula` | Attach a LaTeX formula to a section. |
142
- | `ingest_attach_table` | Attach a structured table `{headers, rows}` to a section. |
143
- | `ingest_attach_cross_ref` | Attach an explicit cross-ref to another norm. |
144
- | `ingest_upload_figure` | Upload a figure (base64 PNG/JPEG/WebP, ≤ 8 MB) to a section. |
145
- | `ingest_normalize_cross_refs` | Auto-detect and rebuild cross-refs from section content. |
146
- | `ingest_publish` | Flip a draft to published — visible via the read tools. |
147
- | `ingest_delete` | Delete a document and all its children. |
148
-
149
- ## How agents should use it
150
-
151
- For querying:
152
-
153
- 1. Call `get_methodology` first — load the behavioural contract.
154
- 2. Call `list_norms` to see what's available in your workspace.
155
- 3. Call `get_toc(norm)` to navigate the structure; `get_subtree` to drill in.
156
- 4. Call `get_section(norm, path)` to read specific content.
157
- 5. Use `search_in_norm` when the section path is unknown.
158
- 6. Follow `crossRefs` for compound questions (e.g. SIA 261 → SIA 263 → EC).
159
- 7. Call `get_figure` when the section references a figure relevant to the answer.
160
-
161
- For ingesting your own licensed norms, see the bundled **`ingest-norm` skill** —
162
- a 2-phase hybrid pipeline (since 0.4.0): a Python pre-pass
163
- (`scripts/ingest-prepass.py`, PyMuPDF) extracts the hierarchical tree and
164
- rasterizes figures deterministically, then the agent enriches sections with
165
- summaries, LaTeX formulas, tables and cross-refs via targeted visual reading.
166
- Requires Python ≥ 3.10 with PyMuPDF (`python -m pip install --user pymupdf`).
167
-
168
- ## Troubleshooting
169
-
170
- ### `Authentication failed` / `Invalid API key`
171
-
172
- - Verify the key starts with `sk_strt_` and is not revoked at https://stratta.ch/api-keys.
173
- - Check the env var is reaching the process: `echo $STRATTA_API_KEY` (or `$env:STRATTA_API_KEY` on Windows PowerShell).
174
- - If you copied from the UI, make sure no leading/trailing whitespace was added.
175
-
176
- ### `ECONNREFUSED` / network errors
177
-
178
- - Confirm outbound HTTPS to `*.convex.cloud` is allowed by your firewall/VPN.
179
- - Try `curl -I https://stratta.ch` to verify general internet reachability.
180
-
181
- ### Tools don't appear in Claude
182
-
183
- - Restart your Claude client after editing the config.
184
- - Check the MCP server logs (Claude Code: `claude mcp logs stratta`; Claude Desktop: `~/Library/Logs/Claude/mcp-server-stratta.log` on macOS).
185
- - Make sure your Node.js version is `>=20` (`node --version`).
186
-
187
- ### Rate-limited
188
-
189
- - Each user can create up to 50 active keys and 20 new keys per 24h. Revoke unused keys in the dashboard.
190
-
191
- ### `QUOTA_EXCEEDED`
192
-
193
- Your organization reached one of its limits. The error names the dimension, your
194
- current count and the plan limit. Retrying will fail identically.
195
-
196
- | Limit | Free | Pro | Max |
197
- | --------------- | ---- | ------ | ------- |
198
- | Norms | 1 | 15 | 60 |
199
- | Sections | 500 | 5,000 | 21,000 |
200
- | Figures | 60 | 750 | 3,000 |
201
- | Queries / month | 500 | 15,000 | 100,000 |
202
- | Members | 1 | 1 | 5 |
203
-
204
- Beyond Max, an Enterprise plan scales to 100 members, 300 norms and a million
205
- monthly queries; the calculator is at https://stratta.ch/tarifs
206
-
207
- Stock limits free up when you delete a norm (`ingest_delete`). The monthly query
208
- counter resets on its own. Gauges live on the Workspace page of your dashboard,
209
- and an org admin can also set caps below the plan. Full details:
210
- https://stratta.ch/docs/en/account/plans
211
-
212
- ## License
213
-
214
- Proprietary — © Stratta, Lausanne. All rights reserved. This package is the
215
- official Stratta MCP client; redistribution, modification, or reuse of the source
216
- is not permitted without prior written consent.
1
+ <!--
2
+ Ownership marker for the official MCP registry. It must match `mcpName` in
3
+ package.json exactly, and it must be present in the *published* tarball —
4
+ the registry reads the README from npm, not from this repository. Without it
5
+ `mcp-publisher publish` rejects the server as unverified.
6
+
7
+ mcp-name: ch.stratta/mcp
8
+ -->
9
+
10
+ # @stratta/mcp
11
+
12
+ MCP server exposing Swiss engineering norms (SIA / Eurocodes) to Claude clients via Stratta TreeRAG.
13
+
14
+ > Requires a free Stratta account. Sign up at https://stratta.ch and generate an API key at https://stratta.ch/api-keys.
15
+
16
+ > [!IMPORTANT]
17
+ > Your `STRATTA_API_KEY` is a **secret** — it grants read/write access to your
18
+ > Stratta workspace. Never commit it to a repository, paste it into a shared/
19
+ > project-scoped MCP config, or share it in logs. Prefer a user-scoped config or
20
+ > a shell environment variable. If a key leaks, revoke it immediately at
21
+ > https://stratta.ch/api-keys.
22
+
23
+ ## Install
24
+
25
+ ### Claude Code (recommended)
26
+
27
+ Add the server — no key needed up front:
28
+
29
+ ```bash
30
+ claude mcp add stratta --scope user -- npx -y @stratta/mcp
31
+ ```
32
+
33
+ `--scope user` registers it for your user, so the tools are there in every
34
+ project. Without it, `claude mcp add` writes to the current folder's config and
35
+ the server exists nowhere else.
36
+
37
+ Then sign in. This opens stratta.ch in your browser, where you confirm which
38
+ machine and organisation to authorise; the key comes back to the terminal on
39
+ its own and is saved to `~/.stratta/config.json` (owner-only, `0600`). You
40
+ never see it, and you only do this once:
41
+
42
+ ```bash
43
+ npx -y @stratta/mcp login
44
+ ```
45
+
46
+ No browser on this machine — remote server, SSH, CI? `login --paste` asks for a
47
+ key from https://stratta.ch/api-keys instead, without echoing it.
48
+
49
+ If you skip the step entirely, Claude Code prompts you for a key on the first
50
+ tool call.
51
+
52
+ You can also pass the key explicitly as an environment variable (it then takes
53
+ precedence over the saved key):
54
+
55
+ ```bash
56
+ claude mcp add stratta --scope user --env STRATTA_API_KEY=sk_strt_xxx -- npx -y @stratta/mcp
57
+ ```
58
+
59
+ ### Claude Desktop
60
+
61
+ Sign in once from a terminal — the desktop app cannot prompt you interactively:
62
+
63
+ ```bash
64
+ npx -y @stratta/mcp login
65
+ ```
66
+
67
+ Then add the server to `claude_desktop_config.json` (Settings → Developer →
68
+ Edit Config) — no key needed in the file:
69
+
70
+ ```json
71
+ {
72
+ "mcpServers": {
73
+ "stratta": {
74
+ "command": "npx",
75
+ "args": ["-y", "@stratta/mcp"]
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Restart Claude Desktop. The Stratta tools should appear in the MCP indicator.
82
+
83
+ > Prefer to keep the key in the config instead of `~/.stratta/config.json`? Add
84
+ > an `"env": { "STRATTA_API_KEY": "sk_strt_xxx" }` block to the server entry —
85
+ > but that file then stores the key in plaintext, so keep it private and unsynced.
86
+
87
+ ### Global install (optional)
88
+
89
+ ```bash
90
+ npm install -g @stratta/mcp
91
+ ```
92
+
93
+ Then use `"command": "stratta-mcp"` instead of `npx`.
94
+
95
+ ## Configuration
96
+
97
+ The API key is resolved in this order: the `STRATTA_API_KEY` env var, then
98
+ `~/.stratta/config.json` (written by `login` or the first-run prompt). Other
99
+ settings come from environment variables — see [`.env.example`](./.env.example).
100
+
101
+ | Variable | Required | Default | Purpose |
102
+ | -------------------- | -------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `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). |
104
+ | `STRATTA_CONVEX_URL` | no | Stratta prod backend | Override only if you self-host. |
105
+
106
+ ## Tools exposed
107
+
108
+ **Read** (8 tools — query norms in your workspace):
109
+
110
+ | Tool | Purpose |
111
+ | ----------------- | -------------------------------------------------------------------------------------- |
112
+ | `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first. |
113
+ | `list_norms` | List all norms published in your workspace (code, year, title, language). |
114
+ | `get_toc` | Hierarchical TOC for a norm (default `maxDepth=1` = chapters). |
115
+ | `get_subtree` | Drill into a chapter/section subtree (`path` + `maxDepth`). |
116
+ | `get_section` | Full enriched content of a section (formulas, tables, figures, cross-refs). |
117
+ | `search_in_norm` | Keyword search inside a norm. |
118
+ | `get_figure` | Retrieve a figure inline (base64 ImageContent) + public URL. |
119
+ | `get_cross_refs` | Outgoing cross-refs from a section to other norms. |
120
+
121
+ **Dossier** (5 tools — keep what was decided on a project):
122
+
123
+ | Tool | Purpose |
124
+ | ------------------ | ------------------------------------------------------------------------------------------------- |
125
+ | `list_dossiers` | Your organisation's dossiers, most recently touched first, with open counts. |
126
+ | `open_dossier` | Open a project's dossier, creating it if needed. Idempotent on the name. |
127
+ | `save_finding` | Record one decision: a cited article, a retained value and why, an observation, an open question. |
128
+ | `load_dossier` | Reload everything, unresolved questions first. Accepts the id or the name. |
129
+ | `resolve_question` | Mark a question settled. The entry stays; it stops surfacing at the top. |
130
+
131
+ A dossier is read, annotated, reviewed and exported from
132
+ [stratta.ch/dossiers](https://stratta.ch/dossiers).
133
+
134
+ **Ingest** (10 tools — add YOUR licensed norms; driven by the bundled `ingest-norm` skill):
135
+
136
+ | Tool | Purpose |
137
+ | ----------------------------- | ------------------------------------------------------------ |
138
+ | `ingest_status` | Check if a norm already exists in your workspace. |
139
+ | `ingest_create_document` | Create a draft norm document. |
140
+ | `ingest_create_sections` | Bulk-insert sections (returns `nodeId → sectionId` map). |
141
+ | `ingest_attach_formula` | Attach a LaTeX formula to a section. |
142
+ | `ingest_attach_table` | Attach a structured table `{headers, rows}` to a section. |
143
+ | `ingest_attach_cross_ref` | Attach an explicit cross-ref to another norm. |
144
+ | `ingest_upload_figure` | Upload a figure (base64 PNG/JPEG/WebP, ≤ 8 MB) to a section. |
145
+ | `ingest_normalize_cross_refs` | Auto-detect and rebuild cross-refs from section content. |
146
+ | `ingest_publish` | Flip a draft to published — visible via the read tools. |
147
+ | `ingest_delete` | Delete a document and all its children. |
148
+
149
+ ## How agents should use it
150
+
151
+ For querying:
152
+
153
+ 1. Call `get_methodology` first — load the behavioural contract.
154
+ 2. Call `list_norms` to see what's available in your workspace.
155
+ 3. Call `get_toc(norm)` to navigate the structure; `get_subtree` to drill in.
156
+ 4. Call `get_section(norm, path)` to read specific content.
157
+ 5. Use `search_in_norm` when the section path is unknown.
158
+ 6. Follow `crossRefs` for compound questions (e.g. SIA 261 → SIA 263 → EC).
159
+ 7. Call `get_figure` when the section references a figure relevant to the answer.
160
+
161
+ For ingesting your own licensed norms, see the bundled **`ingest-norm` skill** —
162
+ a 2-phase hybrid pipeline (since 0.4.0): a Python pre-pass
163
+ (`scripts/ingest-prepass.py`, PyMuPDF) extracts the hierarchical tree and
164
+ rasterizes figures deterministically, then the agent enriches sections with
165
+ summaries, LaTeX formulas, tables and cross-refs via targeted visual reading.
166
+ Requires Python ≥ 3.10 with PyMuPDF (`python -m pip install --user pymupdf`).
167
+
168
+ ## Troubleshooting
169
+
170
+ ### `Authentication failed` / `Invalid API key`
171
+
172
+ - Verify the key starts with `sk_strt_` and is not revoked at https://stratta.ch/api-keys.
173
+ - Check the env var is reaching the process: `echo $STRATTA_API_KEY` (or `$env:STRATTA_API_KEY` on Windows PowerShell).
174
+ - If you copied from the UI, make sure no leading/trailing whitespace was added.
175
+
176
+ ### `ECONNREFUSED` / network errors
177
+
178
+ - Confirm outbound HTTPS to `*.convex.cloud` is allowed by your firewall/VPN.
179
+ - Try `curl -I https://stratta.ch` to verify general internet reachability.
180
+
181
+ ### Tools don't appear in Claude
182
+
183
+ - Restart your Claude client after editing the config.
184
+ - Check the MCP server logs (Claude Code: `claude mcp logs stratta`; Claude Desktop: `~/Library/Logs/Claude/mcp-server-stratta.log` on macOS).
185
+ - Make sure your Node.js version is `>=20` (`node --version`).
186
+
187
+ ### Rate-limited
188
+
189
+ - Each user can create up to 50 active keys and 20 new keys per 24h. Revoke unused keys in the dashboard.
190
+
191
+ ### `QUOTA_EXCEEDED`
192
+
193
+ Your organization reached one of its limits. The error names the dimension, your
194
+ current count and the plan limit. Retrying will fail identically.
195
+
196
+ | Limit | Free | Pro | Max |
197
+ | --------------- | ---- | ------ | ------- |
198
+ | Norms | 1 | 15 | 60 |
199
+ | Sections | 500 | 5,000 | 21,000 |
200
+ | Figures | 60 | 750 | 3,000 |
201
+ | Queries / month | 500 | 15,000 | 100,000 |
202
+ | Members | 1 | 1 | 5 |
203
+
204
+ Beyond Max, an Enterprise plan scales to 100 members, 300 norms and a million
205
+ monthly queries; the calculator is at https://stratta.ch/tarifs
206
+
207
+ Stock limits free up when you delete a norm (`ingest_delete`). The monthly query
208
+ counter resets on its own. Gauges live on the Workspace page of your dashboard,
209
+ and an org admin can also set caps below the plan. Full details:
210
+ https://stratta.ch/docs/en/account/plans
211
+
212
+ ## License
213
+
214
+ Proprietary — © Stratta, Lausanne. All rights reserved. This package is the
215
+ official Stratta MCP client; redistribution, modification, or reuse of the source
216
+ is not permitted without prior written consent.
package/dist/login.js CHANGED
@@ -68,15 +68,15 @@ function openBrowser(url) {
68
68
  }
69
69
  }
70
70
  function page(title, body) {
71
- return `<!doctype html><html lang="fr"><meta charset="utf-8">
72
- <title>${title}</title>
73
- <style>
74
- body{font:16px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
75
- background:#12110f;color:#f5f1e8;display:grid;place-items:center;
76
- min-height:100vh;margin:0;padding:2rem;text-align:center}
77
- .c{max-width:26rem} h1{font-size:1.25rem;margin:0 0 .5rem}
78
- p{margin:0;color:#a8a196}
79
- </style>
71
+ return `<!doctype html><html lang="fr"><meta charset="utf-8">
72
+ <title>${title}</title>
73
+ <style>
74
+ body{font:16px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
75
+ background:#12110f;color:#f5f1e8;display:grid;place-items:center;
76
+ min-height:100vh;margin:0;padding:2rem;text-align:center}
77
+ .c{max-width:26rem} h1{font-size:1.25rem;margin:0 0 .5rem}
78
+ p{margin:0;color:#a8a196}
79
+ </style>
80
80
  <div class="c"><h1>${title}</h1><p>${body}</p></div>`;
81
81
  }
82
82
  /**
package/package.json CHANGED
@@ -1,59 +1,59 @@
1
- {
2
- "name": "@stratta/mcp",
3
- "mcpName": "ch.stratta/mcp",
4
- "version": "0.9.3",
5
- "description": "MCP server exposing the engineering norms your firm is licensed for (SIA / Eurocodes) to any MCP client, via Stratta TreeRAG.",
6
- "license": "UNLICENSED",
7
- "author": "SmartFlow <hello@stratta.ch>",
8
- "homepage": "https://stratta.ch",
9
- "repository": {
10
- "type": "git",
11
- "url": "https://github.com/hugogebel-boop/stratta-v2.git",
12
- "directory": "packages/mcp"
13
- },
14
- "keywords": [
15
- "mcp",
16
- "model-context-protocol",
17
- "claude",
18
- "rag",
19
- "sia",
20
- "eurocodes",
21
- "civil-engineering",
22
- "stratta"
23
- ],
24
- "type": "module",
25
- "main": "dist/index.js",
26
- "bin": {
27
- "stratta-mcp": "dist/index.js"
28
- },
29
- "files": [
30
- "dist",
31
- "skills",
32
- "scripts",
33
- "README.md"
34
- ],
35
- "scripts": {
36
- "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
37
- "dev": "tsc --watch",
38
- "start": "node dist/index.js",
39
- "test": "vitest run",
40
- "test:watch": "vitest",
41
- "prepublishOnly": "npm run build"
42
- },
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.30.0",
45
- "convex": "^1.42.3",
46
- "zod": "^4.0.0"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^26.1.1",
50
- "typescript": "^6.0.3",
51
- "vitest": "^4.1.10"
52
- },
53
- "engines": {
54
- "node": ">=20"
55
- },
56
- "publishConfig": {
57
- "access": "public"
58
- }
59
- }
1
+ {
2
+ "name": "@stratta/mcp",
3
+ "mcpName": "ch.stratta/mcp",
4
+ "version": "0.9.4",
5
+ "description": "MCP server exposing the engineering norms your firm is licensed for (SIA / Eurocodes) to any MCP client, via Stratta TreeRAG.",
6
+ "license": "UNLICENSED",
7
+ "author": "SmartFlow <hello@stratta.ch>",
8
+ "homepage": "https://stratta.ch",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/hugogebel-boop/stratta-v2.git",
12
+ "directory": "packages/mcp"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "claude",
18
+ "rag",
19
+ "sia",
20
+ "eurocodes",
21
+ "civil-engineering",
22
+ "stratta"
23
+ ],
24
+ "type": "module",
25
+ "main": "dist/index.js",
26
+ "bin": {
27
+ "stratta-mcp": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "skills",
32
+ "scripts",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
37
+ "dev": "tsc --watch",
38
+ "start": "node dist/index.js",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "prepublishOnly": "npm run build"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.30.0",
45
+ "convex": "^1.42.3",
46
+ "zod": "^4.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.1.1",
50
+ "typescript": "^6.0.3",
51
+ "vitest": "^4.1.10"
52
+ },
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }
@@ -1,99 +1,99 @@
1
- <#
2
- .SYNOPSIS
3
- Publish @stratta/mcp to npm, then register it with the official MCP registry.
4
-
5
- .DESCRIPTION
6
- The two publications are ordered, not independent: the registry reads
7
- `mcpName` from the tarball npm serves, so a version that is not on npm yet
8
- cannot be registered. Doing it by hand in the wrong order fails with
9
-
10
- NPM package '@stratta/mcp' exists, but version 'x.y.z' was not found
11
-
12
- which reads like a transient error and is not one.
13
-
14
- Run this from an interactive terminal. `npm publish` triggers a 2FA prompt
15
- that opens a browser, and `npm login` needs one too if the session has
16
- lapsed. Neither works from a non-interactive shell -- by design, so that
17
- access to a machine is not access to the package.
18
-
19
- ASCII only, on purpose: Windows PowerShell 5.1 reads a UTF-8 file without a
20
- BOM as ANSI, and a single arrow or em dash in a comment breaks the parser
21
- with an error that points at the wrong line.
22
-
23
- .PARAMETER PrivateKey
24
- The Ed25519 key proving control of stratta.ch, 64 hex characters. Lives in
25
- Doppler under MCP_REGISTRY_PRIVATE_KEY. Its public half is served from
26
- /.well-known/mcp-registry-auth -- deleting that file breaks publishing.
27
-
28
- .PARAMETER PublisherPath
29
- Path to mcp-publisher. Download it from the registry releases if absent:
30
- https://github.com/modelcontextprotocol/registry/releases
31
-
32
- .EXAMPLE
33
- .\publish-release.ps1 -PrivateKey (doppler secrets get MCP_REGISTRY_PRIVATE_KEY --plain)
34
- #>
35
- param(
36
- [Parameter(Mandatory = $true)][string]$PrivateKey,
37
- [string]$PublisherPath = "mcp-publisher"
38
- )
39
-
40
- $ErrorActionPreference = "Stop"
41
- Push-Location (Join-Path $PSScriptRoot "..")
42
-
43
- try {
44
- $pkg = Get-Content package.json -Raw | ConvertFrom-Json
45
- $srv = Get-Content server.json -Raw | ConvertFrom-Json
46
-
47
- Write-Host "-> @stratta/mcp $($pkg.version) as $($pkg.mcpName)" -ForegroundColor Cyan
48
-
49
- # Fail before publishing, not after. The three names and the three versions
50
- # have to agree, or the registry rejects the server as unverified once npm
51
- # has already gone out, and a version number is burnt.
52
- if ($pkg.mcpName -ne $srv.name) {
53
- throw "package.json mcpName ($($pkg.mcpName)) does not match server.json name ($($srv.name))"
54
- }
55
- if ($pkg.version -ne $srv.version -or $pkg.version -ne $srv.packages[0].version) {
56
- throw "version mismatch: package.json $($pkg.version), server.json $($srv.version), packages[0] $($srv.packages[0].version)"
57
- }
58
- if (-not (Select-String -Path README.md -Pattern ("mcp-name: " + [regex]::Escape($pkg.mcpName)) -Quiet)) {
59
- throw "README.md is missing the marker 'mcp-name: $($pkg.mcpName)'"
60
- }
61
- Write-Host " manifest is coherent" -ForegroundColor DarkGray
62
-
63
- Write-Host "-> npm whoami" -ForegroundColor Cyan
64
- npm whoami
65
- if ($LASTEXITCODE -ne 0) { throw "Not logged in to npm. Run 'npm login' and retry." }
66
-
67
- Write-Host "-> npm publish (a 2FA prompt may open a browser)" -ForegroundColor Cyan
68
- npm publish
69
- if ($LASTEXITCODE -ne 0) { throw "npm publish failed" }
70
-
71
- # npm's CDN needs a moment before the registry can see the new version.
72
- Write-Host "-> waiting for npm to serve $($pkg.version)" -ForegroundColor Cyan
73
- $seen = $false
74
- foreach ($i in 1..20) {
75
- Start-Sleep -Seconds 3
76
- try {
77
- $served = Invoke-RestMethod "https://registry.npmjs.org/@stratta/mcp/$($pkg.version)"
78
- if ($served.version -eq $pkg.version) { $seen = $true; break }
79
- } catch { }
80
- }
81
- if (-not $seen) { throw "npm has not served $($pkg.version) after 60s; rerun the registry steps by hand" }
82
- Write-Host " npm serves $($pkg.version)" -ForegroundColor DarkGray
83
-
84
- # Domain proof rather than the GitHub device flow: nothing waits on a human
85
- # typing a code. The JWT it returns expires in under an hour, which is why
86
- # login and publish run back to back with nothing in between.
87
- Write-Host "-> mcp-publisher login (domain proof)" -ForegroundColor Cyan
88
- & $PublisherPath login http --domain stratta.ch --private-key $PrivateKey
89
- if ($LASTEXITCODE -ne 0) { throw "registry login failed. Check that https://stratta.ch/.well-known/mcp-registry-auth is still served." }
90
-
91
- Write-Host "-> mcp-publisher publish" -ForegroundColor Cyan
92
- & $PublisherPath publish
93
- if ($LASTEXITCODE -ne 0) { throw "registry publish failed" }
94
-
95
- Write-Host "OK $($pkg.version) published to npm and to registry.modelcontextprotocol.io" -ForegroundColor Green
96
- }
97
- finally {
98
- Pop-Location
99
- }
1
+ <#
2
+ .SYNOPSIS
3
+ Publish @stratta/mcp to npm, then register it with the official MCP registry.
4
+
5
+ .DESCRIPTION
6
+ The two publications are ordered, not independent: the registry reads
7
+ `mcpName` from the tarball npm serves, so a version that is not on npm yet
8
+ cannot be registered. Doing it by hand in the wrong order fails with
9
+
10
+ NPM package '@stratta/mcp' exists, but version 'x.y.z' was not found
11
+
12
+ which reads like a transient error and is not one.
13
+
14
+ Run this from an interactive terminal. `npm publish` triggers a 2FA prompt
15
+ that opens a browser, and `npm login` needs one too if the session has
16
+ lapsed. Neither works from a non-interactive shell -- by design, so that
17
+ access to a machine is not access to the package.
18
+
19
+ ASCII only, on purpose: Windows PowerShell 5.1 reads a UTF-8 file without a
20
+ BOM as ANSI, and a single arrow or em dash in a comment breaks the parser
21
+ with an error that points at the wrong line.
22
+
23
+ .PARAMETER PrivateKey
24
+ The Ed25519 key proving control of stratta.ch, 64 hex characters. Lives in
25
+ Doppler under MCP_REGISTRY_PRIVATE_KEY. Its public half is served from
26
+ /.well-known/mcp-registry-auth -- deleting that file breaks publishing.
27
+
28
+ .PARAMETER PublisherPath
29
+ Path to mcp-publisher. Download it from the registry releases if absent:
30
+ https://github.com/modelcontextprotocol/registry/releases
31
+
32
+ .EXAMPLE
33
+ .\publish-release.ps1 -PrivateKey (doppler secrets get MCP_REGISTRY_PRIVATE_KEY --plain)
34
+ #>
35
+ param(
36
+ [Parameter(Mandatory = $true)][string]$PrivateKey,
37
+ [string]$PublisherPath = "mcp-publisher"
38
+ )
39
+
40
+ $ErrorActionPreference = "Stop"
41
+ Push-Location (Join-Path $PSScriptRoot "..")
42
+
43
+ try {
44
+ $pkg = Get-Content package.json -Raw | ConvertFrom-Json
45
+ $srv = Get-Content server.json -Raw | ConvertFrom-Json
46
+
47
+ Write-Host "-> @stratta/mcp $($pkg.version) as $($pkg.mcpName)" -ForegroundColor Cyan
48
+
49
+ # Fail before publishing, not after. The three names and the three versions
50
+ # have to agree, or the registry rejects the server as unverified once npm
51
+ # has already gone out, and a version number is burnt.
52
+ if ($pkg.mcpName -ne $srv.name) {
53
+ throw "package.json mcpName ($($pkg.mcpName)) does not match server.json name ($($srv.name))"
54
+ }
55
+ if ($pkg.version -ne $srv.version -or $pkg.version -ne $srv.packages[0].version) {
56
+ throw "version mismatch: package.json $($pkg.version), server.json $($srv.version), packages[0] $($srv.packages[0].version)"
57
+ }
58
+ if (-not (Select-String -Path README.md -Pattern ("mcp-name: " + [regex]::Escape($pkg.mcpName)) -Quiet)) {
59
+ throw "README.md is missing the marker 'mcp-name: $($pkg.mcpName)'"
60
+ }
61
+ Write-Host " manifest is coherent" -ForegroundColor DarkGray
62
+
63
+ Write-Host "-> npm whoami" -ForegroundColor Cyan
64
+ npm whoami
65
+ if ($LASTEXITCODE -ne 0) { throw "Not logged in to npm. Run 'npm login' and retry." }
66
+
67
+ Write-Host "-> npm publish (a 2FA prompt may open a browser)" -ForegroundColor Cyan
68
+ npm publish
69
+ if ($LASTEXITCODE -ne 0) { throw "npm publish failed" }
70
+
71
+ # npm's CDN needs a moment before the registry can see the new version.
72
+ Write-Host "-> waiting for npm to serve $($pkg.version)" -ForegroundColor Cyan
73
+ $seen = $false
74
+ foreach ($i in 1..20) {
75
+ Start-Sleep -Seconds 3
76
+ try {
77
+ $served = Invoke-RestMethod "https://registry.npmjs.org/@stratta/mcp/$($pkg.version)"
78
+ if ($served.version -eq $pkg.version) { $seen = $true; break }
79
+ } catch { }
80
+ }
81
+ if (-not $seen) { throw "npm has not served $($pkg.version) after 60s; rerun the registry steps by hand" }
82
+ Write-Host " npm serves $($pkg.version)" -ForegroundColor DarkGray
83
+
84
+ # Domain proof rather than the GitHub device flow: nothing waits on a human
85
+ # typing a code. The JWT it returns expires in under an hour, which is why
86
+ # login and publish run back to back with nothing in between.
87
+ Write-Host "-> mcp-publisher login (domain proof)" -ForegroundColor Cyan
88
+ & $PublisherPath login http --domain stratta.ch --private-key $PrivateKey
89
+ if ($LASTEXITCODE -ne 0) { throw "registry login failed. Check that https://stratta.ch/.well-known/mcp-registry-auth is still served." }
90
+
91
+ Write-Host "-> mcp-publisher publish" -ForegroundColor Cyan
92
+ & $PublisherPath publish
93
+ if ($LASTEXITCODE -ne 0) { throw "registry publish failed" }
94
+
95
+ Write-Host "OK $($pkg.version) published to npm and to registry.modelcontextprotocol.io" -ForegroundColor Green
96
+ }
97
+ finally {
98
+ Pop-Location
99
+ }
@@ -1,162 +1,162 @@
1
- ---
2
- name: ingest-norm
3
- description: Use when the user wants to add an engineering norm (SIA, Eurocode, etc.) they are licensed for into THEIR Stratta workspace. A Python pre-pass (PyMuPDF) extracts the hierarchical tree and rasterizes figures; an agentic pass enriches sections (LaTeX formulas, tables, cross-references, summaries) and writes everything via the MCP `ingest_*` tools — scoped to the user's own organization. Trigger phrases: "ingère cette norme", "/ingest-norm", "ingest SIA", "ajoute la norme X à Stratta".
4
- ---
5
-
6
- # Ingest a norm into your Stratta workspace
7
-
8
- This skill turns a norm PDF **you are licensed to use** into a queryable
9
- TreeRAG inside **your own** Stratta workspace. A Python pre-pass (PyMuPDF)
10
- does the deterministic heavy lifting — TOC tree, per-section raw text, figure
11
- captions and full-page renders. Then an agentic pass enriches sections with
12
- summaries, LaTeX formulas, structured tables, and cross-references. Final
13
- writes go through the Stratta MCP `ingest_*` tools, scoped to your org.
14
-
15
- > ⚠️ **Licence**: only ingest norms your organization holds a valid licence
16
- > for. You are responsible for your usage rights (see Stratta's Terms).
17
-
18
- ## Prerequisites
19
-
20
- - The Stratta MCP server is installed and your `STRATTA_API_KEY` resolves
21
- (via env, `~/.stratta/config.json`, or first-call elicitation).
22
- - **Python ≥ 3.10 with PyMuPDF**. Install once:
23
- `python -m pip install --user pymupdf` (or `uv pip install pymupdf`).
24
- - The norm PDF is available locally.
25
-
26
- ## Tools used (all scoped to your workspace)
27
-
28
- `ingest_status` · `ingest_create_document` · `ingest_create_sections` ·
29
- `ingest_attach_formula` · `ingest_attach_table` · `ingest_attach_cross_ref` ·
30
- `ingest_upload_figure` · `ingest_normalize_cross_refs` · `ingest_publish` ·
31
- `ingest_delete`.
32
-
33
- ## Quotas — read before you start
34
-
35
- The workspace has server-enforced limits on norms, sections and figures. A
36
- `QUOTA_EXCEEDED` error names the dimension, the current count and the limit.
37
-
38
- **Do not retry it.** The limit will not move on its own. Stop the ingestion,
39
- report the numbers to the user, and offer the two ways out: delete a norm that is
40
- no longer needed (`ingest_delete`), or raise the plan.
41
-
42
- Check the budget up front on a large norm: the pre-pass output tells you how many
43
- sections and figures you are about to write. Failing at step 8 of 10 leaves a
44
- half-ingested document behind, which the user then has to delete by hand.
45
-
46
- ## Workflow
47
-
48
- ### 1. Locate the pre-pass script
49
-
50
- It ships inside this package at `scripts/ingest-prepass.py`. Resolve its path:
51
-
52
- ```bash
53
- node -e "console.log(require.resolve('@stratta/mcp/package.json'))"
54
- # → <root>/package.json → <root>/scripts/ingest-prepass.py
55
- ```
56
-
57
- If the user is working in the Stratta monorepo, the script also lives at
58
- `packages/mcp/scripts/ingest-prepass.py`.
59
-
60
- ### 2. Check for an existing copy
61
-
62
- `ingest_status { code }` (e.g. `"SIA 261"`). To re-ingest, call
63
- `ingest_delete { documentId }` first.
64
-
65
- ### 3. Run the pre-pass
66
-
67
- ```bash
68
- python <pkg-root>/scripts/ingest-prepass.py \
69
- --pdf <path-to-pdf> \
70
- --output .stratta-ingest/<code-slug>
71
- ```
72
-
73
- Output under `.stratta-ingest/<code-slug>/`:
74
-
75
- - `prepass.json` — full manifest (see below).
76
- - `figures/page-NNN.png` — one PNG per page that contains a `Figure N` caption.
77
-
78
- `prepass.json` structure:
79
-
80
- - `doc` — `pageCount`, detected `language`, `tocSource`, raw `metadata`.
81
- - `stats` — `sectionCount`, `byDepth`, `figureCount`.
82
- - `sections[]` — full hierarchical tree (depth **0 = chapter**, 1+ = sub-sections),
83
- each with `nodeId`, `parentNodeId`, `path` (`"4.2.1"` or `"Annexe B"`),
84
- `title`, `depth`, `pageStart`, `pageEnd`, `orderIndex`, `rawText` (concat
85
- of the pages the node spans).
86
- - `figures[]` — one entry per `Figure N` caption: `figureNumber`, `caption`,
87
- `page`, `fileName`, `mimeType`.
88
-
89
- The pre-pass is **exhaustive** (e.g. ~550 nodes on SIA 261). You decide what
90
- to keep in the next step.
91
-
92
- ### 4. Create the document
93
-
94
- Read `prepass.json`, then:
95
- `ingest_create_document { code, year, title, language: <doc.language>, totalPages: <doc.pageCount> }`
96
- → returns `documentId`. Keep it for every subsequent call.
97
-
98
- ### 5. Decide section granularity + generate summaries
99
-
100
- Iterate `sections[]` and decide what to keep. Two viable strategies:
101
-
102
- - **Keep all** — most faithful, ~500 sections on a typical SIA norm. Great
103
- for fine-grained navigation but verbose.
104
- - **Aggregate trivial leaves** — fold paragraph-level nodes (`6.1.1`...`6.1.11`)
105
- into their parent (`6.1`), concatenating their `rawText`. Typical result:
106
- 100-150 sections. Recommended unless the user asks for max granularity.
107
-
108
- For each kept section, prepare:
109
-
110
- - `summary` — 1-3 sentences derived from `rawText` (mention formulas/values).
111
- - `content` — enriched text with LaTeX inline where the source has math
112
- (e.g. `$\sigma_d = f_{yd} \cdot \gamma$`). Open the PDF visually for pages
113
- that contain formulas or multi-column tables — PyMuPDF mangles those.
114
- - `rawContent` — use the pre-pass `rawText` as-is.
115
-
116
- Keep the `nodeId` / `parentNodeId` / `path` / `pageStart` / `pageEnd` /
117
- `orderIndex` / `depth` from the pre-pass — those are deterministic.
118
-
119
- ### 6. Insert sections (batched)
120
-
121
- `ingest_create_sections { documentId, sections: [...] }` in batches of 30-50.
122
- Parent links resolve via `parentNodeId` within the batch and across prior
123
- batches. The call returns a `nodeId → sectionId` map — **use those `sectionId`s**
124
- for every enrichment call below.
125
-
126
- ### 7. Enrich
127
-
128
- - `ingest_attach_formula { sectionId, latex, description, formulaNumber }`
129
- - `ingest_attach_table { sectionId, data: { headers, rows }, caption, tableNumber }`
130
- - `ingest_attach_cross_ref { sourceSectionId, targetDocumentCode, targetSectionPath?, refText, refType }`
131
-
132
- ### 8. Upload figures
133
-
134
- For each figure in `prepass.json#figures`:
135
-
136
- - Read `.stratta-ingest/<code>/<fileName>` and base64-encode the bytes.
137
- - Find the owning section: the kept section whose `pageStart..pageEnd`
138
- range includes the figure's `page`.
139
- - `ingest_upload_figure { sectionId, base64, mimeType: "image/png", caption, figureNumber }`
140
- (≤ 8 MB per image).
141
-
142
- ### 9. Auto cross-references (optional but recommended)
143
-
144
- `ingest_normalize_cross_refs { documentId }` scans every section's text for
145
- references to other norms (SIA / SN EN / EN / ISO / DIN …) and rebuilds the
146
- cross-ref index. Idempotent.
147
-
148
- ### 10. Publish
149
-
150
- `ingest_publish { documentId }`. The norm is now queryable in your workspace
151
- via `list_norms`, `get_toc`, `get_section`, `search_in_norm`, `get_figure`, etc.
152
-
153
- ## Quality bar
154
-
155
- - Trust the pre-pass for `path` + `pageStart`/`pageEnd` — it's deterministic
156
- and verified against the PDF bookmarks.
157
- - Summaries drive navigation — be specific (mention key formulas/values).
158
- - Keep `content` faithful to the source; don't invent values.
159
- - If PyMuPDF mangled a formula (Greek letters, fractions, exponents broken),
160
- re-read the relevant PDF page visually and write proper LaTeX.
161
- - Never skip annexes — they hold key numeric values (zones, coefficients,
162
- characteristic loads).
1
+ ---
2
+ name: ingest-norm
3
+ description: Use when the user wants to add an engineering norm (SIA, Eurocode, etc.) they are licensed for into THEIR Stratta workspace. A Python pre-pass (PyMuPDF) extracts the hierarchical tree and rasterizes figures; an agentic pass enriches sections (LaTeX formulas, tables, cross-references, summaries) and writes everything via the MCP `ingest_*` tools — scoped to the user's own organization. Trigger phrases: "ingère cette norme", "/ingest-norm", "ingest SIA", "ajoute la norme X à Stratta".
4
+ ---
5
+
6
+ # Ingest a norm into your Stratta workspace
7
+
8
+ This skill turns a norm PDF **you are licensed to use** into a queryable
9
+ TreeRAG inside **your own** Stratta workspace. A Python pre-pass (PyMuPDF)
10
+ does the deterministic heavy lifting — TOC tree, per-section raw text, figure
11
+ captions and full-page renders. Then an agentic pass enriches sections with
12
+ summaries, LaTeX formulas, structured tables, and cross-references. Final
13
+ writes go through the Stratta MCP `ingest_*` tools, scoped to your org.
14
+
15
+ > ⚠️ **Licence**: only ingest norms your organization holds a valid licence
16
+ > for. You are responsible for your usage rights (see Stratta's Terms).
17
+
18
+ ## Prerequisites
19
+
20
+ - The Stratta MCP server is installed and your `STRATTA_API_KEY` resolves
21
+ (via env, `~/.stratta/config.json`, or first-call elicitation).
22
+ - **Python ≥ 3.10 with PyMuPDF**. Install once:
23
+ `python -m pip install --user pymupdf` (or `uv pip install pymupdf`).
24
+ - The norm PDF is available locally.
25
+
26
+ ## Tools used (all scoped to your workspace)
27
+
28
+ `ingest_status` · `ingest_create_document` · `ingest_create_sections` ·
29
+ `ingest_attach_formula` · `ingest_attach_table` · `ingest_attach_cross_ref` ·
30
+ `ingest_upload_figure` · `ingest_normalize_cross_refs` · `ingest_publish` ·
31
+ `ingest_delete`.
32
+
33
+ ## Quotas — read before you start
34
+
35
+ The workspace has server-enforced limits on norms, sections and figures. A
36
+ `QUOTA_EXCEEDED` error names the dimension, the current count and the limit.
37
+
38
+ **Do not retry it.** The limit will not move on its own. Stop the ingestion,
39
+ report the numbers to the user, and offer the two ways out: delete a norm that is
40
+ no longer needed (`ingest_delete`), or raise the plan.
41
+
42
+ Check the budget up front on a large norm: the pre-pass output tells you how many
43
+ sections and figures you are about to write. Failing at step 8 of 10 leaves a
44
+ half-ingested document behind, which the user then has to delete by hand.
45
+
46
+ ## Workflow
47
+
48
+ ### 1. Locate the pre-pass script
49
+
50
+ It ships inside this package at `scripts/ingest-prepass.py`. Resolve its path:
51
+
52
+ ```bash
53
+ node -e "console.log(require.resolve('@stratta/mcp/package.json'))"
54
+ # → <root>/package.json → <root>/scripts/ingest-prepass.py
55
+ ```
56
+
57
+ If the user is working in the Stratta monorepo, the script also lives at
58
+ `packages/mcp/scripts/ingest-prepass.py`.
59
+
60
+ ### 2. Check for an existing copy
61
+
62
+ `ingest_status { code }` (e.g. `"SIA 261"`). To re-ingest, call
63
+ `ingest_delete { documentId }` first.
64
+
65
+ ### 3. Run the pre-pass
66
+
67
+ ```bash
68
+ python <pkg-root>/scripts/ingest-prepass.py \
69
+ --pdf <path-to-pdf> \
70
+ --output .stratta-ingest/<code-slug>
71
+ ```
72
+
73
+ Output under `.stratta-ingest/<code-slug>/`:
74
+
75
+ - `prepass.json` — full manifest (see below).
76
+ - `figures/page-NNN.png` — one PNG per page that contains a `Figure N` caption.
77
+
78
+ `prepass.json` structure:
79
+
80
+ - `doc` — `pageCount`, detected `language`, `tocSource`, raw `metadata`.
81
+ - `stats` — `sectionCount`, `byDepth`, `figureCount`.
82
+ - `sections[]` — full hierarchical tree (depth **0 = chapter**, 1+ = sub-sections),
83
+ each with `nodeId`, `parentNodeId`, `path` (`"4.2.1"` or `"Annexe B"`),
84
+ `title`, `depth`, `pageStart`, `pageEnd`, `orderIndex`, `rawText` (concat
85
+ of the pages the node spans).
86
+ - `figures[]` — one entry per `Figure N` caption: `figureNumber`, `caption`,
87
+ `page`, `fileName`, `mimeType`.
88
+
89
+ The pre-pass is **exhaustive** (e.g. ~550 nodes on SIA 261). You decide what
90
+ to keep in the next step.
91
+
92
+ ### 4. Create the document
93
+
94
+ Read `prepass.json`, then:
95
+ `ingest_create_document { code, year, title, language: <doc.language>, totalPages: <doc.pageCount> }`
96
+ → returns `documentId`. Keep it for every subsequent call.
97
+
98
+ ### 5. Decide section granularity + generate summaries
99
+
100
+ Iterate `sections[]` and decide what to keep. Two viable strategies:
101
+
102
+ - **Keep all** — most faithful, ~500 sections on a typical SIA norm. Great
103
+ for fine-grained navigation but verbose.
104
+ - **Aggregate trivial leaves** — fold paragraph-level nodes (`6.1.1`...`6.1.11`)
105
+ into their parent (`6.1`), concatenating their `rawText`. Typical result:
106
+ 100-150 sections. Recommended unless the user asks for max granularity.
107
+
108
+ For each kept section, prepare:
109
+
110
+ - `summary` — 1-3 sentences derived from `rawText` (mention formulas/values).
111
+ - `content` — enriched text with LaTeX inline where the source has math
112
+ (e.g. `$\sigma_d = f_{yd} \cdot \gamma$`). Open the PDF visually for pages
113
+ that contain formulas or multi-column tables — PyMuPDF mangles those.
114
+ - `rawContent` — use the pre-pass `rawText` as-is.
115
+
116
+ Keep the `nodeId` / `parentNodeId` / `path` / `pageStart` / `pageEnd` /
117
+ `orderIndex` / `depth` from the pre-pass — those are deterministic.
118
+
119
+ ### 6. Insert sections (batched)
120
+
121
+ `ingest_create_sections { documentId, sections: [...] }` in batches of 30-50.
122
+ Parent links resolve via `parentNodeId` within the batch and across prior
123
+ batches. The call returns a `nodeId → sectionId` map — **use those `sectionId`s**
124
+ for every enrichment call below.
125
+
126
+ ### 7. Enrich
127
+
128
+ - `ingest_attach_formula { sectionId, latex, description, formulaNumber }`
129
+ - `ingest_attach_table { sectionId, data: { headers, rows }, caption, tableNumber }`
130
+ - `ingest_attach_cross_ref { sourceSectionId, targetDocumentCode, targetSectionPath?, refText, refType }`
131
+
132
+ ### 8. Upload figures
133
+
134
+ For each figure in `prepass.json#figures`:
135
+
136
+ - Read `.stratta-ingest/<code>/<fileName>` and base64-encode the bytes.
137
+ - Find the owning section: the kept section whose `pageStart..pageEnd`
138
+ range includes the figure's `page`.
139
+ - `ingest_upload_figure { sectionId, base64, mimeType: "image/png", caption, figureNumber }`
140
+ (≤ 8 MB per image).
141
+
142
+ ### 9. Auto cross-references (optional but recommended)
143
+
144
+ `ingest_normalize_cross_refs { documentId }` scans every section's text for
145
+ references to other norms (SIA / SN EN / EN / ISO / DIN …) and rebuilds the
146
+ cross-ref index. Idempotent.
147
+
148
+ ### 10. Publish
149
+
150
+ `ingest_publish { documentId }`. The norm is now queryable in your workspace
151
+ via `list_norms`, `get_toc`, `get_section`, `search_in_norm`, `get_figure`, etc.
152
+
153
+ ## Quality bar
154
+
155
+ - Trust the pre-pass for `path` + `pageStart`/`pageEnd` — it's deterministic
156
+ and verified against the PDF bookmarks.
157
+ - Summaries drive navigation — be specific (mention key formulas/values).
158
+ - Keep `content` faithful to the source; don't invent values.
159
+ - If PyMuPDF mangled a formula (Greek letters, fractions, exponents broken),
160
+ re-read the relevant PDF page visually and write proper LaTeX.
161
+ - Never skip annexes — they hold key numeric values (zones, coefficients,
162
+ characteristic loads).