@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 +230 -216
- package/dist/index.js +18 -1
- package/dist/login.js +9 -9
- package/dist/update.d.ts +29 -0
- package/dist/update.js +88 -0
- package/package.json +59 -59
- package/scripts/publish-release.ps1 +99 -99
- package/skills/ingest-norm/SKILL.md +162 -162
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
## 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
|
-
|
|
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
|
/**
|
package/dist/update.d.ts
ADDED
|
@@ -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.
|
|
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).
|