@stratta/mcp 0.9.3 → 0.9.5

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,230 @@
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
+ ## Staying up to date
60
+
61
+ `npx -y @stratta/mcp` fetches the published package, but npx caches what it
62
+ resolved, so a server can keep launching a version npm replaced weeks ago with
63
+ nothing to tell you.
64
+
65
+ ```bash
66
+ npx -y @stratta/mcp update
67
+ ```
68
+
69
+ Prints the version you are running and the one npm serves, and fetches the new
70
+ one if they differ. Restart your agent afterwards. The server also mentions it
71
+ on stderr at startup when it notices it is behind.
72
+
73
+ ### Claude Desktop
74
+
75
+ Sign in once from a terminal — the desktop app cannot prompt you interactively:
76
+
77
+ ```bash
78
+ npx -y @stratta/mcp login
79
+ ```
80
+
81
+ Then add the server to `claude_desktop_config.json` (Settings → Developer →
82
+ Edit Config) — no key needed in the file:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "stratta": {
88
+ "command": "npx",
89
+ "args": ["-y", "@stratta/mcp"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ Restart Claude Desktop. The Stratta tools should appear in the MCP indicator.
96
+
97
+ > Prefer to keep the key in the config instead of `~/.stratta/config.json`? Add
98
+ > an `"env": { "STRATTA_API_KEY": "sk_strt_xxx" }` block to the server entry —
99
+ > but that file then stores the key in plaintext, so keep it private and unsynced.
100
+
101
+ ### Global install (optional)
102
+
103
+ ```bash
104
+ npm install -g @stratta/mcp
105
+ ```
106
+
107
+ Then use `"command": "stratta-mcp"` instead of `npx`.
108
+
109
+ ## Configuration
110
+
111
+ The API key is resolved in this order: the `STRATTA_API_KEY` env var, then
112
+ `~/.stratta/config.json` (written by `login` or the first-run prompt). Other
113
+ settings come from environment variables — see [`.env.example`](./.env.example).
114
+
115
+ | Variable | Required | Default | Purpose |
116
+ | -------------------- | -------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `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). |
118
+ | `STRATTA_CONVEX_URL` | no | Stratta prod backend | Override only if you self-host. |
119
+
120
+ ## Tools exposed
121
+
122
+ **Read** (8 tools — query norms in your workspace):
123
+
124
+ | Tool | Purpose |
125
+ | ----------------- | -------------------------------------------------------------------------------------- |
126
+ | `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first. |
127
+ | `list_norms` | List all norms published in your workspace (code, year, title, language). |
128
+ | `get_toc` | Hierarchical TOC for a norm (default `maxDepth=1` = chapters). |
129
+ | `get_subtree` | Drill into a chapter/section subtree (`path` + `maxDepth`). |
130
+ | `get_section` | Full enriched content of a section (formulas, tables, figures, cross-refs). |
131
+ | `search_in_norm` | Keyword search inside a norm. |
132
+ | `get_figure` | Retrieve a figure inline (base64 ImageContent) + public URL. |
133
+ | `get_cross_refs` | Outgoing cross-refs from a section to other norms. |
134
+
135
+ **Dossier** (5 tools — keep what was decided on a project):
136
+
137
+ | Tool | Purpose |
138
+ | ------------------ | ------------------------------------------------------------------------------------------------- |
139
+ | `list_dossiers` | Your organisation's dossiers, most recently touched first, with open counts. |
140
+ | `open_dossier` | Open a project's dossier, creating it if needed. Idempotent on the name. |
141
+ | `save_finding` | Record one decision: a cited article, a retained value and why, an observation, an open question. |
142
+ | `load_dossier` | Reload everything, unresolved questions first. Accepts the id or the name. |
143
+ | `resolve_question` | Mark a question settled. The entry stays; it stops surfacing at the top. |
144
+
145
+ A dossier is read, annotated, reviewed and exported from
146
+ [stratta.ch/dossiers](https://stratta.ch/dossiers).
147
+
148
+ **Ingest** (10 tools — add YOUR licensed norms; driven by the bundled `ingest-norm` skill):
149
+
150
+ | Tool | Purpose |
151
+ | ----------------------------- | ------------------------------------------------------------ |
152
+ | `ingest_status` | Check if a norm already exists in your workspace. |
153
+ | `ingest_create_document` | Create a draft norm document. |
154
+ | `ingest_create_sections` | Bulk-insert sections (returns `nodeId → sectionId` map). |
155
+ | `ingest_attach_formula` | Attach a LaTeX formula to a section. |
156
+ | `ingest_attach_table` | Attach a structured table `{headers, rows}` to a section. |
157
+ | `ingest_attach_cross_ref` | Attach an explicit cross-ref to another norm. |
158
+ | `ingest_upload_figure` | Upload a figure (base64 PNG/JPEG/WebP, ≤ 8 MB) to a section. |
159
+ | `ingest_normalize_cross_refs` | Auto-detect and rebuild cross-refs from section content. |
160
+ | `ingest_publish` | Flip a draft to published — visible via the read tools. |
161
+ | `ingest_delete` | Delete a document and all its children. |
162
+
163
+ ## How agents should use it
164
+
165
+ For querying:
166
+
167
+ 1. Call `get_methodology` first — load the behavioural contract.
168
+ 2. Call `list_norms` to see what's available in your workspace.
169
+ 3. Call `get_toc(norm)` to navigate the structure; `get_subtree` to drill in.
170
+ 4. Call `get_section(norm, path)` to read specific content.
171
+ 5. Use `search_in_norm` when the section path is unknown.
172
+ 6. Follow `crossRefs` for compound questions (e.g. SIA 261 → SIA 263 → EC).
173
+ 7. Call `get_figure` when the section references a figure relevant to the answer.
174
+
175
+ For ingesting your own licensed norms, see the bundled **`ingest-norm` skill** —
176
+ a 2-phase hybrid pipeline (since 0.4.0): a Python pre-pass
177
+ (`scripts/ingest-prepass.py`, PyMuPDF) extracts the hierarchical tree and
178
+ rasterizes figures deterministically, then the agent enriches sections with
179
+ summaries, LaTeX formulas, tables and cross-refs via targeted visual reading.
180
+ Requires Python ≥ 3.10 with PyMuPDF (`python -m pip install --user pymupdf`).
181
+
182
+ ## Troubleshooting
183
+
184
+ ### `Authentication failed` / `Invalid API key`
185
+
186
+ - Verify the key starts with `sk_strt_` and is not revoked at https://stratta.ch/api-keys.
187
+ - Check the env var is reaching the process: `echo $STRATTA_API_KEY` (or `$env:STRATTA_API_KEY` on Windows PowerShell).
188
+ - If you copied from the UI, make sure no leading/trailing whitespace was added.
189
+
190
+ ### `ECONNREFUSED` / network errors
191
+
192
+ - Confirm outbound HTTPS to `*.convex.cloud` is allowed by your firewall/VPN.
193
+ - Try `curl -I https://stratta.ch` to verify general internet reachability.
194
+
195
+ ### Tools don't appear in Claude
196
+
197
+ - Restart your Claude client after editing the config.
198
+ - Check the MCP server logs (Claude Code: `claude mcp logs stratta`; Claude Desktop: `~/Library/Logs/Claude/mcp-server-stratta.log` on macOS).
199
+ - Make sure your Node.js version is `>=20` (`node --version`).
200
+
201
+ ### Rate-limited
202
+
203
+ - Each user can create up to 50 active keys and 20 new keys per 24h. Revoke unused keys in the dashboard.
204
+
205
+ ### `QUOTA_EXCEEDED`
206
+
207
+ Your organization reached one of its limits. The error names the dimension, your
208
+ current count and the plan limit. Retrying will fail identically.
209
+
210
+ | Limit | Free | Pro | Max |
211
+ | --------------- | ---- | ------ | ------- |
212
+ | Norms | 1 | 15 | 60 |
213
+ | Sections | 500 | 5,000 | 21,000 |
214
+ | Figures | 60 | 750 | 3,000 |
215
+ | Queries / month | 500 | 15,000 | 100,000 |
216
+ | Members | 1 | 1 | 5 |
217
+
218
+ Beyond Max, an Enterprise plan scales to 100 members, 300 norms and a million
219
+ monthly queries; the calculator is at https://stratta.ch/tarifs
220
+
221
+ Stock limits free up when you delete a norm (`ingest_delete`). The monthly query
222
+ counter resets on its own. Gauges live on the Workspace page of your dashboard,
223
+ and an org admin can also set caps below the plan. Full details:
224
+ https://stratta.ch/docs/en/account/plans
225
+
226
+ ## License
227
+
228
+ Proprietary — © Stratta, Lausanne. All rights reserved. This package is the
229
+ official Stratta MCP client; redistribution, modification, or reuse of the source
230
+ is not permitted without prior written consent.
package/dist/index.js CHANGED
@@ -93,14 +93,31 @@ for (const def of [...readTools, ...dossierTools, ...ingestTools]) {
93
93
  (args) => call(def, args));
94
94
  }
95
95
  async function main() {
96
- if (process.argv[2] === 'login') {
96
+ const command = process.argv[2];
97
+ if (command === 'login') {
97
98
  const { runLogin } = await import('./login.js');
98
99
  await runLogin(process.argv.slice(3));
99
100
  return;
100
101
  }
102
+ if (command === 'update') {
103
+ const { runUpdate } = await import('./update.js');
104
+ await runUpdate(packageVersion());
105
+ return;
106
+ }
107
+ // `--version` because that is what everyone types first, and because
108
+ // `update` needs a cheap way to make npx materialise a given version.
109
+ if (command === '--version' || command === '-v' || command === 'version') {
110
+ console.log(packageVersion());
111
+ return;
112
+ }
101
113
  await server.connect(new StdioServerTransport());
102
114
  // Stderr only; stdout carries the MCP protocol.
103
115
  console.error(`[stratta-mcp] v${packageVersion()} listening on stdio`);
116
+ // After connect, never before: the check reaches the network, and a server
117
+ // that starts slowly because of a version lookup is a worse problem than an
118
+ // out-of-date one.
119
+ const { noticeIfOutdated } = await import('./update.js');
120
+ noticeIfOutdated(packageVersion());
104
121
  }
105
122
  main().catch((err) => {
106
123
  console.error('[stratta-mcp] fatal:', err);
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
  /**
@@ -0,0 +1,29 @@
1
+ /**
2
+ * What version npm would serve right now, or null if we could not ask.
3
+ *
4
+ * Deliberately quiet about failures: this runs on a path where being offline,
5
+ * behind a proxy, or on a flaky connection is ordinary, and none of that is
6
+ * worth an error. A missed check is not a problem; a server that refuses to
7
+ * start because a version check failed would be.
8
+ */
9
+ export declare function latestPublished(timeoutMs?: number): Promise<string | null>;
10
+ /** Numeric compare, so 0.9.10 sorts above 0.9.9. */
11
+ export declare function isNewer(candidate: string, current: string): boolean;
12
+ /**
13
+ * Tell the user, on stderr, when they are running something old.
14
+ *
15
+ * Fire-and-forget from `main`: the server is already listening by the time
16
+ * this resolves, so a slow registry delays nothing. stdout carries the MCP
17
+ * protocol and must stay clean.
18
+ */
19
+ export declare function noticeIfOutdated(current: string): void;
20
+ /**
21
+ * `npx @stratta/mcp update`.
22
+ *
23
+ * npx caches a resolved package and reuses it, so `npx -y @stratta/mcp` can
24
+ * keep launching a version that npm replaced weeks ago — the user has no way
25
+ * to tell, and nothing tells them. This says which version is running, which
26
+ * one is published, and clears the cache entry so the next launch fetches the
27
+ * new one.
28
+ */
29
+ export declare function runUpdate(current: string): Promise<void>;
package/dist/update.js ADDED
@@ -0,0 +1,88 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ const REGISTRY = 'https://registry.npmjs.org/@stratta/mcp/latest';
3
+ /**
4
+ * What version npm would serve right now, or null if we could not ask.
5
+ *
6
+ * Deliberately quiet about failures: this runs on a path where being offline,
7
+ * behind a proxy, or on a flaky connection is ordinary, and none of that is
8
+ * worth an error. A missed check is not a problem; a server that refuses to
9
+ * start because a version check failed would be.
10
+ */
11
+ export async function latestPublished(timeoutMs = 3000) {
12
+ try {
13
+ const res = await fetch(REGISTRY, {
14
+ signal: AbortSignal.timeout(timeoutMs),
15
+ headers: { accept: 'application/json' },
16
+ });
17
+ if (!res.ok)
18
+ return null;
19
+ const body = (await res.json());
20
+ return body.version ?? null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ /** Numeric compare, so 0.9.10 sorts above 0.9.9. */
27
+ export function isNewer(candidate, current) {
28
+ const a = candidate.split('.').map(Number);
29
+ const b = current.split('.').map(Number);
30
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
31
+ const x = a[i] ?? 0;
32
+ const y = b[i] ?? 0;
33
+ if (x !== y)
34
+ return x > y;
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * Tell the user, on stderr, when they are running something old.
40
+ *
41
+ * Fire-and-forget from `main`: the server is already listening by the time
42
+ * this resolves, so a slow registry delays nothing. stdout carries the MCP
43
+ * protocol and must stay clean.
44
+ */
45
+ export function noticeIfOutdated(current) {
46
+ void latestPublished().then((latest) => {
47
+ if (!latest || !isNewer(latest, current))
48
+ return;
49
+ console.error(`[stratta-mcp] version ${latest} disponible (vous avez ${current}) — ` +
50
+ `lancez : npx -y @stratta/mcp update`);
51
+ });
52
+ }
53
+ /**
54
+ * `npx @stratta/mcp update`.
55
+ *
56
+ * npx caches a resolved package and reuses it, so `npx -y @stratta/mcp` can
57
+ * keep launching a version that npm replaced weeks ago — the user has no way
58
+ * to tell, and nothing tells them. This says which version is running, which
59
+ * one is published, and clears the cache entry so the next launch fetches the
60
+ * new one.
61
+ */
62
+ export async function runUpdate(current) {
63
+ console.log(`Version installée : ${current}`);
64
+ const latest = await latestPublished(8000);
65
+ if (!latest) {
66
+ console.log('Impossible de joindre npm. Vérifiez votre connexion et réessayez.');
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+ console.log(`Dernière publiée : ${latest}`);
71
+ if (!isNewer(latest, current)) {
72
+ console.log('\nVous êtes à jour.');
73
+ return;
74
+ }
75
+ console.log('\nRécupération de la nouvelle version…');
76
+ // `npx -y pkg@version` bypasses whatever npx had cached for the bare name,
77
+ // and populates the cache with the new one in the same step.
78
+ const r = spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['-y', `@stratta/mcp@${latest}`, '--version'], { stdio: 'inherit' });
79
+ if (r.status === 0) {
80
+ console.log(`\n@stratta/mcp ${latest} est prêt.\n` +
81
+ 'Redémarrez votre agent pour que le serveur reparte sur cette version.');
82
+ }
83
+ else {
84
+ console.log(`\nLa récupération a échoué. À lancer à la main :\n` +
85
+ ` npx -y @stratta/mcp@${latest}`);
86
+ process.exitCode = 1;
87
+ }
88
+ }
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.5",
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).