@stratta/mcp 1.19.0 → 1.20.1

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
@@ -198,18 +198,18 @@ package announces 59 of them; the remote connector serves 61: everything minus
198
198
 
199
199
  **Read** (10 tools — query the norms of your workspace):
200
200
 
201
- | Tool | Purpose |
202
- | ----------------- | --------------------------------------------------------------------------------------- |
203
- | `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first. |
204
- | `whoami` | Which organisation, role and plan this connection reads as, and how many norms it sees. |
205
- | `list_norms` | List all norms published in your workspace (code, year, title, language, coverage). |
206
- | `get_toc` | Hierarchical TOC for a norm (default `maxDepth=1` = chapters). |
207
- | `get_subtree` | Drill into a chapter/section subtree (`path` + `maxDepth`). |
208
- | `get_section` | Full enriched content of a section (formulas, tables, figures, cross-refs). |
209
- | `search_in_norm` | Keyword search inside a norm. |
210
- | `search_corpus` | Keyword search across every norm of the workspace, grouped by norm. |
211
- | `get_figure` | Retrieve a figure inline (base64 ImageContent). |
212
- | `get_cross_refs` | Outgoing cross-refs from a section to other norms. |
201
+ | Tool | Purpose |
202
+ | ----------------- | ------------------------------------------------------------------------------------------------ |
203
+ | `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first. |
204
+ | `whoami` | Which organisation, role and plan this connection reads as, and how many norms it sees. |
205
+ | `list_norms` | List the norms of your workspace, filtered by `query` (code or title words), bounded by `limit`. |
206
+ | `get_toc` | Hierarchical TOC for a norm (default `maxDepth=1` = chapters). |
207
+ | `get_subtree` | Drill into a chapter/section subtree (`path` + `maxDepth`). |
208
+ | `get_section` | Full enriched content of a section (formulas, tables, figures, cross-refs). |
209
+ | `search_in_norm` | Keyword search inside a norm. |
210
+ | `search_corpus` | Keyword search across every norm of the workspace, grouped by norm. |
211
+ | `get_figure` | Retrieve a figure inline (base64 ImageContent). |
212
+ | `get_cross_refs` | Outgoing cross-refs from a section to other norms. |
213
213
 
214
214
  **Site** (10 tools — what public Swiss registers know about a plot, and the site sheet as the app shows it):
215
215
 
@@ -37,7 +37,7 @@ export const CATALOG = [
37
37
  {
38
38
  "name": "list_norms",
39
39
  "title": "Available norms",
40
- "description": "List the norms and reference documents (standards, laws, regulations) published in the signed-in organization, with code, year, title, language and coverage (how much of the norm was ingested). Use this when a question may be settled by the documents of the organization and the norm is not yet known: a norm absent from this list cannot be read here. A demo entry is fictional and never a technical reference; a sparse entry carries a notice to relay.",
40
+ "description": "List the norms and reference documents (standards, laws, regulations) published in the signed-in organization, with code, year, title, edition, language and coverage (how much of the norm was ingested). Use this to check whether a norm is held, which editions are readable, or what a norm covers: a norm absent from this organization cannot be read here. Pass `query` with a code or words of the title; a large corpus is listed in part, and `total` says how many norms there are. A demo entry is fictional and never a technical reference; a sparse entry carries a notice to relay.",
41
41
  "annotations": {
42
42
  "readOnlyHint": true,
43
43
  "destructiveHint": false,
@@ -48,7 +48,10 @@ export const CATALOG = [
48
48
  "stdio",
49
49
  "remote"
50
50
  ],
51
- "params": {}
51
+ "params": {
52
+ "query": "A code or words of the title or scope, accents optional, e.g. \"SIA 267\" or \"geotechnique\". Every word must match. With a query, each norm comes with its scope.",
53
+ "limit": "Maximum number of norms listed, 1 to 500. Default 60."
54
+ }
52
55
  },
53
56
  {
54
57
  "name": "get_toc",
@@ -829,7 +832,7 @@ export const CATALOG = [
829
832
  ],
830
833
  "params": {
831
834
  "dossierId": "Dossier id from list_dossiers, open_dossier or load_dossier.",
832
- "limit": "Lines per page, 1 to 200. Default 50.",
835
+ "limit": "Lines per page, 1 to 200. Default 15, the most recent first.",
833
836
  "cursor": "The nextCursor of the previous page."
834
837
  }
835
838
  },
@@ -1,7 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  export declare const getMethodology: import("./define.js").ToolDef<{}>;
3
3
  export declare const whoami: import("./define.js").ToolDef<{}>;
4
- export declare const listNorms: import("./define.js").ToolDef<{}>;
4
+ export declare const listNorms: import("./define.js").ToolDef<{
5
+ query: z.ZodOptional<z.ZodString>;
6
+ limit: z.ZodOptional<z.ZodNumber>;
7
+ }>;
5
8
  export declare const getToc: import("./define.js").ToolDef<{
6
9
  code: z.ZodString;
7
10
  edition: z.ZodOptional<z.ZodString>;
@@ -42,6 +45,9 @@ export declare const getFigure: import("./define.js").ToolDef<{
42
45
  figureId: z.ZodString;
43
46
  }>;
44
47
  export declare const readTools: (import("./define.js").ToolDef<{}> | import("./define.js").ToolDef<{
48
+ query: z.ZodOptional<z.ZodString>;
49
+ limit: z.ZodOptional<z.ZodNumber>;
50
+ }> | import("./define.js").ToolDef<{
45
51
  code: z.ZodString;
46
52
  edition: z.ZodOptional<z.ZodString>;
47
53
  maxDepth: z.ZodOptional<z.ZodNumber>;
@@ -50,11 +50,24 @@ export const whoami = defineTool({
50
50
  });
51
51
  export const listNorms = defineTool({
52
52
  ...headOf('list_norms'),
53
- inputSchema: {},
54
- run: async (client) => ({
55
- norms: await client.action(api._mcp.listPublishedNorms, {
56
- apiKey: requireApiKey(),
57
- }),
53
+ inputSchema: {
54
+ query: z
55
+ .string()
56
+ .max(200)
57
+ .optional()
58
+ .describe(param('list_norms', 'query')),
59
+ limit: z
60
+ .number()
61
+ .int()
62
+ .min(1)
63
+ .max(500)
64
+ .optional()
65
+ .describe(param('list_norms', 'limit')),
66
+ },
67
+ run: (client, args) => client.action(api._mcp.listNorms, {
68
+ apiKey: requireApiKey(),
69
+ query: args.query,
70
+ limit: args.limit,
58
71
  }),
59
72
  });
60
73
  export const getToc = defineTool({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stratta/mcp",
3
3
  "mcpName": "ch.stratta/mcp",
4
- "version": "1.19.0",
4
+ "version": "1.20.1",
5
5
  "description": "MCP server exposing your organization’s norms and reference documents (standards, laws, regulations) to any MCP client, with exact citations, via Stratta TreeRAG.",
6
6
  "license": "UNLICENSED",
7
7
  "author": "SmartFlow <hello@stratta.ch>",
@@ -42,14 +42,14 @@
42
42
  "prepublishOnly": "npm run build"
43
43
  },
44
44
  "dependencies": {
45
- "@modelcontextprotocol/sdk": "^1.30.0",
45
+ "@modelcontextprotocol/sdk": "^1.32.1",
46
46
  "convex": "^1.42.3",
47
- "zod": "^4.0.0"
47
+ "zod": "^4.6.7"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^26.1.1",
51
51
  "typescript": "^6.0.3",
52
- "vitest": "^4.1.10"
52
+ "vitest": "^4.1.12"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=20"
@@ -51,7 +51,9 @@ TOC_DOT_RE = re.compile(r"\.\s*\.\s*\.")
51
51
  CLEAN_DOTS = re.compile(r"\s*(\.\s*){3,}.*$")
52
52
 
53
53
  CAPTION_RE = re.compile(
54
- r"^\s*(?:Figure|Fig\.|Bild|Abbildung|Figura)\s+(\d+[a-z]?)\b[\s:.\-—–]*(.*)$",
54
+ # « Figure 1.2 » garde tout son numéro : réduit à « 1 », il était dédoublonné avec « 1.1 »
55
+ # et ASIRI ne gardait qu'une figure par chapitre.
56
+ r"^\s*(?:Figure|Fig\.|Bild|Abbildung|Figura)\s+(\d+(?:\.\d+)*[a-z]?)\b[\s:.\-—–]*(.*)$",
55
57
  re.IGNORECASE | re.MULTILINE,
56
58
  )
57
59
 
@@ -199,10 +201,26 @@ def page_lines(doc: "pymupdf.Document", pageno: int) -> list[str]:
199
201
  text = OCR_DOTTED_NUMBER_RE.sub(r"\1.\2", text)
200
202
  lines.append(text)
201
203
  lines = [l.rstrip() for l in lines]
204
+ # ⚠️⚠️ Regrouper les mots par hauteur suppose que la hauteur dit la ligne. Sur certains PDF
205
+ # (RMP, LVMP, SUVA BauAV neu : chaque mot posé à sa propre hauteur) le regroupement rend une
206
+ # soupe de mots que le compte de mots ne voit pas, puisqu'il ignore l'ordre. Quand les paires
207
+ # de mots consécutifs ne recoupent pas à 30 % celles de la lecture native, la native l'emporte.
208
+ native = page.get_text("text")
209
+ ours = _word_pairs("\n".join(lines))
210
+ if len(ours) >= 20 and len(ours & _word_pairs(native)) / len(ours) < 0.3:
211
+ lines = [l.rstrip() for l in join_split_headings(native).splitlines()]
202
212
  _LINES_CACHE[key] = lines
203
213
  return lines
204
214
 
205
215
 
216
+ _PAIR_WORD = re.compile(r"[0-9A-Za-zÀ-ÿ]{2,}")
217
+
218
+
219
+ def _word_pairs(text: str) -> set[tuple[str, str]]:
220
+ w = [x.lower() for x in _PAIR_WORD.findall(text)]
221
+ return set(zip(w, w[1:]))
222
+
223
+
206
224
  def page_text(doc: "pymupdf.Document", pageno: int) -> str:
207
225
  return "\n".join(page_lines(doc, pageno))
208
226
 
@@ -1091,7 +1109,48 @@ def extract_figures(
1091
1109
  "renderDpi": dpi,
1092
1110
  "mimeType": "image/png",
1093
1111
  }
1094
- return list(seen.values())
1112
+ # ⚠️ Une page qui porte un grand dessin sans légende « Figure N » (« Beilage C : », « Abb. 3 »,
1113
+ # un schéma sans titre) n'était pas retenue : 65 normes du corpus KF perdaient 847 images.
1114
+ # Une image de 300 × 300 px couvrant 10 % de la page au moins en fait une figure ; la page 1
1115
+ # (couverture) et les logos en sont exclus.
1116
+ have = {f["page"] for f in seen.values()}
1117
+ for pageno in range(1, doc.page_count):
1118
+ if pageno in toc_pages or pageno + 1 in have:
1119
+ continue
1120
+ page = doc[pageno]
1121
+ area = page.rect.width * page.rect.height
1122
+ big = any(
1123
+ i.get("width", 0) >= 300
1124
+ and i.get("height", 0) >= 300
1125
+ and (i["bbox"][2] - i["bbox"][0]) * (i["bbox"][3] - i["bbox"][1]) >= 0.10 * area
1126
+ for i in page.get_image_info()
1127
+ )
1128
+ if not big:
1129
+ continue
1130
+ m = FREE_LABEL_RE.search(page_text(doc, pageno))
1131
+ label = re.sub(r"\s+", " ", m.group(1)).strip().rstrip(":") if m else None
1132
+ rest = re.sub(r"\s+", " ", m.group(2)).strip() if m else ""
1133
+ number = label or f"p. {pageno + 1}"
1134
+ while number in seen:
1135
+ number = f"{number} (p. {pageno + 1})"
1136
+ pix = page.get_pixmap(matrix=matrix, alpha=False)
1137
+ page_png = figs_dir / f"page-{pageno + 1:03d}.png"
1138
+ pix.save(str(page_png))
1139
+ seen[number] = {
1140
+ "figureNumber": number,
1141
+ "caption": (f"{label} — {rest}" if label and rest else label or f"Illustration, page {pageno + 1}")[:240],
1142
+ "page": pageno + 1,
1143
+ "fileName": f"figures/{page_png.name}",
1144
+ "renderDpi": dpi,
1145
+ "mimeType": "image/png",
1146
+ }
1147
+ return sorted(seen.values(), key=lambda f: f["page"])
1148
+
1149
+
1150
+ FREE_LABEL_RE = re.compile(
1151
+ r"^\s*((?:Beilage|Anhang|Annexe|Abb\.?|Bild|Schéma|Schema|Planche|Tafel)\s*[\w.]*)\s*[:\-–]?\s*(.{3,120})$",
1152
+ re.IGNORECASE | re.MULTILINE,
1153
+ )
1095
1154
 
1096
1155
 
1097
1156
  def guess_language(doc: "pymupdf.Document") -> str:
@@ -5,7 +5,7 @@ description: Use Stratta to answer professional questions from the norms and ref
5
5
 
6
6
  # Consult Stratta
7
7
 
8
- Use this skill when the user asks a technical civil engineering question that should be answered from the norms available in Stratta, or when the user asks to resume, create or update a Stratta project dossier.
8
+ Use this skill when the user asks a professional question that should be answered from the norms, laws or reference documents available in Stratta, or when the user asks to resume, create or update a Stratta project dossier.
9
9
 
10
10
  You answer professionals (engineers, lawyers, accountants, public administrations and others) from the norms and reference documents their organization holds in Stratta (technical standards, laws, ordinances, regulations, accounting standards, internal references), and only from them. The corpus below is the whole of what you can read: a document absent from it cannot be consulted, and a question outside it is answered by saying so, not by general knowledge. "Norm" below means any such document. Reply in the language the user writes in; quote the norm in the language it is written in and translate only when asked.
11
11