@stratta/mcp 0.9.1 → 0.9.2
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 +1 -1
- package/dist/login.js +9 -9
- package/dist/tools/dossier.js +10 -10
- package/package.json +2 -2
- package/scripts/publish-release.ps1 +99 -0
- package/skills/ingest-norm/SKILL.md +22 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ package.json exactly, and it must be present in the *published* tarball —
|
|
|
4
4
|
the registry reads the README from npm, not from this repository. Without it
|
|
5
5
|
`mcp-publisher publish` rejects the server as unverified.
|
|
6
6
|
|
|
7
|
-
mcp-name:
|
|
7
|
+
mcp-name: ch.stratta/mcp
|
|
8
8
|
-->
|
|
9
9
|
|
|
10
10
|
# @stratta/mcp
|
package/dist/login.js
CHANGED
|
@@ -52,15 +52,15 @@ function openBrowser(url) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
function page(title, body) {
|
|
55
|
-
return `<!doctype html><html lang="fr"><meta charset="utf-8">
|
|
56
|
-
<title>${title}</title>
|
|
57
|
-
<style>
|
|
58
|
-
body{font:16px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
59
|
-
background:#12110f;color:#f5f1e8;display:grid;place-items:center;
|
|
60
|
-
min-height:100vh;margin:0;padding:2rem;text-align:center}
|
|
61
|
-
.c{max-width:26rem} h1{font-size:1.25rem;margin:0 0 .5rem}
|
|
62
|
-
p{margin:0;color:#a8a196}
|
|
63
|
-
</style>
|
|
55
|
+
return `<!doctype html><html lang="fr"><meta charset="utf-8">
|
|
56
|
+
<title>${title}</title>
|
|
57
|
+
<style>
|
|
58
|
+
body{font:16px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
59
|
+
background:#12110f;color:#f5f1e8;display:grid;place-items:center;
|
|
60
|
+
min-height:100vh;margin:0;padding:2rem;text-align:center}
|
|
61
|
+
.c{max-width:26rem} h1{font-size:1.25rem;margin:0 0 .5rem}
|
|
62
|
+
p{margin:0;color:#a8a196}
|
|
63
|
+
</style>
|
|
64
64
|
<div class="c"><h1>${title}</h1><p>${body}</p></div>`;
|
|
65
65
|
}
|
|
66
66
|
/**
|
package/dist/tools/dossier.js
CHANGED
|
@@ -59,16 +59,16 @@ export const openDossier = defineTool({
|
|
|
59
59
|
export const saveFinding = defineTool({
|
|
60
60
|
name: 'save_finding',
|
|
61
61
|
title: 'Save a finding to a dossier',
|
|
62
|
-
description: `Record ONE decision in a dossier. Call it as you work, not in a batch at the end — a finding saved when it is made carries the reasoning that produced it.
|
|
63
|
-
|
|
64
|
-
WHAT TO RECORD, by kind:
|
|
65
|
-
- reference: what a norm says, that the project relies on. ALWAYS fill normCode + sectionPath (+ page). If you cannot cite it, it is not a reference.
|
|
66
|
-
- hypothesis: a value the engineer RETAINS, and why. This is the one that matters most in geotechnics, where a retained value is a judgement between disagreeing measurements rather than the output of a formula. Put the number in \`value\` and the reasoning in \`detail\`.
|
|
67
|
-
- observation: what the site, a borehole, or a survey showed. Include the date in \`detail\` when known.
|
|
68
|
-
- question: something not settled. These surface FIRST when the dossier is reloaded, so record them even when you cannot answer — especially then.
|
|
69
|
-
|
|
70
|
-
WHAT NOT TO RECORD: your own prose, intermediate steps, anything the user did not treat as a decision. A dossier of forty entries where three mattered is worse than a dossier of three.
|
|
71
|
-
|
|
62
|
+
description: `Record ONE decision in a dossier. Call it as you work, not in a batch at the end — a finding saved when it is made carries the reasoning that produced it.
|
|
63
|
+
|
|
64
|
+
WHAT TO RECORD, by kind:
|
|
65
|
+
- reference: what a norm says, that the project relies on. ALWAYS fill normCode + sectionPath (+ page). If you cannot cite it, it is not a reference.
|
|
66
|
+
- hypothesis: a value the engineer RETAINS, and why. This is the one that matters most in geotechnics, where a retained value is a judgement between disagreeing measurements rather than the output of a formula. Put the number in \`value\` and the reasoning in \`detail\`.
|
|
67
|
+
- observation: what the site, a borehole, or a survey showed. Include the date in \`detail\` when known.
|
|
68
|
+
- question: something not settled. These surface FIRST when the dossier is reloaded, so record them even when you cannot answer — especially then.
|
|
69
|
+
|
|
70
|
+
WHAT NOT TO RECORD: your own prose, intermediate steps, anything the user did not treat as a decision. A dossier of forty entries where three mattered is worse than a dossier of three.
|
|
71
|
+
|
|
72
72
|
Set \`confidence\` whenever the entry is a value: established (computed or read directly), judgement (the engineer chose it), to_confirm (provisional, needs a test or a check). Confusing those three is the professional fault this field exists to prevent.`,
|
|
73
73
|
inputSchema: {
|
|
74
74
|
dossierId,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratta/mcp",
|
|
3
|
-
"mcpName": "
|
|
4
|
-
"version": "0.9.
|
|
3
|
+
"mcpName": "ch.stratta/mcp",
|
|
4
|
+
"version": "0.9.2",
|
|
5
5
|
"description": "MCP server exposing the engineering norms your firm is licensed for (SIA / Eurocodes) to any MCP client, via Stratta TreeRAG.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": "SmartFlow <hello@stratta.ch>",
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -16,6 +16,7 @@ writes go through the Stratta MCP `ingest_*` tools, scoped to your org.
|
|
|
16
16
|
> for. You are responsible for your usage rights (see Stratta's Terms).
|
|
17
17
|
|
|
18
18
|
## Prerequisites
|
|
19
|
+
|
|
19
20
|
- The Stratta MCP server is installed and your `STRATTA_API_KEY` resolves
|
|
20
21
|
(via env, `~/.stratta/config.json`, or first-call elicitation).
|
|
21
22
|
- **Python ≥ 3.10 with PyMuPDF**. Install once:
|
|
@@ -23,12 +24,14 @@ writes go through the Stratta MCP `ingest_*` tools, scoped to your org.
|
|
|
23
24
|
- The norm PDF is available locally.
|
|
24
25
|
|
|
25
26
|
## Tools used (all scoped to your workspace)
|
|
27
|
+
|
|
26
28
|
`ingest_status` · `ingest_create_document` · `ingest_create_sections` ·
|
|
27
29
|
`ingest_attach_formula` · `ingest_attach_table` · `ingest_attach_cross_ref` ·
|
|
28
30
|
`ingest_upload_figure` · `ingest_normalize_cross_refs` · `ingest_publish` ·
|
|
29
31
|
`ingest_delete`.
|
|
30
32
|
|
|
31
33
|
## Quotas — read before you start
|
|
34
|
+
|
|
32
35
|
The workspace has server-enforced limits on norms, sections and figures. A
|
|
33
36
|
`QUOTA_EXCEEDED` error names the dimension, the current count and the limit.
|
|
34
37
|
|
|
@@ -43,29 +46,37 @@ half-ingested document behind, which the user then has to delete by hand.
|
|
|
43
46
|
## Workflow
|
|
44
47
|
|
|
45
48
|
### 1. Locate the pre-pass script
|
|
49
|
+
|
|
46
50
|
It ships inside this package at `scripts/ingest-prepass.py`. Resolve its path:
|
|
51
|
+
|
|
47
52
|
```bash
|
|
48
53
|
node -e "console.log(require.resolve('@stratta/mcp/package.json'))"
|
|
49
54
|
# → <root>/package.json → <root>/scripts/ingest-prepass.py
|
|
50
55
|
```
|
|
56
|
+
|
|
51
57
|
If the user is working in the Stratta monorepo, the script also lives at
|
|
52
58
|
`packages/mcp/scripts/ingest-prepass.py`.
|
|
53
59
|
|
|
54
60
|
### 2. Check for an existing copy
|
|
61
|
+
|
|
55
62
|
`ingest_status { code }` (e.g. `"SIA 261"`). To re-ingest, call
|
|
56
63
|
`ingest_delete { documentId }` first.
|
|
57
64
|
|
|
58
65
|
### 3. Run the pre-pass
|
|
66
|
+
|
|
59
67
|
```bash
|
|
60
68
|
python <pkg-root>/scripts/ingest-prepass.py \
|
|
61
69
|
--pdf <path-to-pdf> \
|
|
62
70
|
--output .stratta-ingest/<code-slug>
|
|
63
71
|
```
|
|
72
|
+
|
|
64
73
|
Output under `.stratta-ingest/<code-slug>/`:
|
|
74
|
+
|
|
65
75
|
- `prepass.json` — full manifest (see below).
|
|
66
76
|
- `figures/page-NNN.png` — one PNG per page that contains a `Figure N` caption.
|
|
67
77
|
|
|
68
78
|
`prepass.json` structure:
|
|
79
|
+
|
|
69
80
|
- `doc` — `pageCount`, detected `language`, `tocSource`, raw `metadata`.
|
|
70
81
|
- `stats` — `sectionCount`, `byDepth`, `figureCount`.
|
|
71
82
|
- `sections[]` — full hierarchical tree (depth **0 = chapter**, 1+ = sub-sections),
|
|
@@ -79,12 +90,15 @@ The pre-pass is **exhaustive** (e.g. ~550 nodes on SIA 261). You decide what
|
|
|
79
90
|
to keep in the next step.
|
|
80
91
|
|
|
81
92
|
### 4. Create the document
|
|
93
|
+
|
|
82
94
|
Read `prepass.json`, then:
|
|
83
95
|
`ingest_create_document { code, year, title, language: <doc.language>, totalPages: <doc.pageCount> }`
|
|
84
96
|
→ returns `documentId`. Keep it for every subsequent call.
|
|
85
97
|
|
|
86
98
|
### 5. Decide section granularity + generate summaries
|
|
99
|
+
|
|
87
100
|
Iterate `sections[]` and decide what to keep. Two viable strategies:
|
|
101
|
+
|
|
88
102
|
- **Keep all** — most faithful, ~500 sections on a typical SIA norm. Great
|
|
89
103
|
for fine-grained navigation but verbose.
|
|
90
104
|
- **Aggregate trivial leaves** — fold paragraph-level nodes (`6.1.1`...`6.1.11`)
|
|
@@ -92,6 +106,7 @@ Iterate `sections[]` and decide what to keep. Two viable strategies:
|
|
|
92
106
|
100-150 sections. Recommended unless the user asks for max granularity.
|
|
93
107
|
|
|
94
108
|
For each kept section, prepare:
|
|
109
|
+
|
|
95
110
|
- `summary` — 1-3 sentences derived from `rawText` (mention formulas/values).
|
|
96
111
|
- `content` — enriched text with LaTeX inline where the source has math
|
|
97
112
|
(e.g. `$\sigma_d = f_{yd} \cdot \gamma$`). Open the PDF visually for pages
|
|
@@ -102,18 +117,22 @@ Keep the `nodeId` / `parentNodeId` / `path` / `pageStart` / `pageEnd` /
|
|
|
102
117
|
`orderIndex` / `depth` from the pre-pass — those are deterministic.
|
|
103
118
|
|
|
104
119
|
### 6. Insert sections (batched)
|
|
120
|
+
|
|
105
121
|
`ingest_create_sections { documentId, sections: [...] }` in batches of 30-50.
|
|
106
122
|
Parent links resolve via `parentNodeId` within the batch and across prior
|
|
107
123
|
batches. The call returns a `nodeId → sectionId` map — **use those `sectionId`s**
|
|
108
124
|
for every enrichment call below.
|
|
109
125
|
|
|
110
126
|
### 7. Enrich
|
|
127
|
+
|
|
111
128
|
- `ingest_attach_formula { sectionId, latex, description, formulaNumber }`
|
|
112
129
|
- `ingest_attach_table { sectionId, data: { headers, rows }, caption, tableNumber }`
|
|
113
130
|
- `ingest_attach_cross_ref { sourceSectionId, targetDocumentCode, targetSectionPath?, refText, refType }`
|
|
114
131
|
|
|
115
132
|
### 8. Upload figures
|
|
133
|
+
|
|
116
134
|
For each figure in `prepass.json#figures`:
|
|
135
|
+
|
|
117
136
|
- Read `.stratta-ingest/<code>/<fileName>` and base64-encode the bytes.
|
|
118
137
|
- Find the owning section: the kept section whose `pageStart..pageEnd`
|
|
119
138
|
range includes the figure's `page`.
|
|
@@ -121,15 +140,18 @@ For each figure in `prepass.json#figures`:
|
|
|
121
140
|
(≤ 8 MB per image).
|
|
122
141
|
|
|
123
142
|
### 9. Auto cross-references (optional but recommended)
|
|
143
|
+
|
|
124
144
|
`ingest_normalize_cross_refs { documentId }` scans every section's text for
|
|
125
145
|
references to other norms (SIA / SN EN / EN / ISO / DIN …) and rebuilds the
|
|
126
146
|
cross-ref index. Idempotent.
|
|
127
147
|
|
|
128
148
|
### 10. Publish
|
|
149
|
+
|
|
129
150
|
`ingest_publish { documentId }`. The norm is now queryable in your workspace
|
|
130
151
|
via `list_norms`, `get_toc`, `get_section`, `search_in_norm`, `get_figure`, etc.
|
|
131
152
|
|
|
132
153
|
## Quality bar
|
|
154
|
+
|
|
133
155
|
- Trust the pre-pass for `path` + `pageStart`/`pageEnd` — it's deterministic
|
|
134
156
|
and verified against the PDF bookmarks.
|
|
135
157
|
- Summaries drive navigation — be specific (mention key formulas/values).
|