@stratta/mcp 1.20.0 → 1.21.0

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
@@ -333,9 +333,11 @@ contents to depth 2, code URL-encoded, e.g. `stratta://norm/SIA%20267/toc`),
333
333
  `stratta://skills/{name}` (one skill as Markdown, the office version when there
334
334
  is one). The same five are served by the remote connector.
335
335
 
336
- Three prompts appear as slash commands in clients that support them:
337
- `investigate-question` (question, project?), `resume-dossier` (name) and
338
- `check-value` (value, norm?).
336
+ Four prompts appear as slash commands in clients that support them, on the
337
+ local and the remote server alike: `investigate-question` (question,
338
+ project?), `resume-dossier` (name), `check-value` (value, norm?) and
339
+ `apply-method` (task, project?), which follows the method your office wrote
340
+ for the task.
339
341
 
340
342
  For depositing your own documents, see the bundled **`ingest-norm` skill** —
341
343
  a 2-phase hybrid pipeline (since 0.4.0): a Python pre-pass
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { SESSION_ID, usageDigest } from './usage.js';
18
18
  import { SERVER_INSTRUCTIONS } from './tools/catalog.gen.js';
19
19
  import { registerResources } from './resources.js';
20
20
  import { registerPrompts } from './prompts.js';
21
+ import { claudeMeta } from './tools/claude-meta.gen.js';
21
22
  import { TOOLSET_CHOICE } from './mode.js';
22
23
  import { toolInChoice } from './tools/toolsets.gen.js';
23
24
  /**
@@ -124,6 +125,8 @@ for (const def of [
124
125
  description: def.description,
125
126
  inputSchema: def.inputSchema,
126
127
  annotations: def.annotations,
128
+ // Claude Code keeps the norm reader loaded and takes a long clause whole.
129
+ _meta: claudeMeta(def.name),
127
130
  },
128
131
  // The SDK validates `args` against `inputSchema` before this runs, so the
129
132
  // handler receives values that already match the declared shape.
package/dist/prompts.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type PromptEntry } from './tools/catalog.gen.js';
3
- export declare function renderPrompt(p: PromptEntry, args: Record<string, unknown>): string;
2
+ /**
3
+ * Prompts: the ways a user starts a conversation with Stratta, as slash
4
+ * commands in the clients that support them (Claude Code, Claude Desktop,
5
+ * Cursor). The remote server serves the same ones (`convex/mcpPrompts.ts`).
6
+ *
7
+ * Text and arguments come from the catalogue, so a wording change is made
8
+ * once. `{{arg}}` is filled with the argument; an `optional` line is appended
9
+ * only when its argument was given.
10
+ */
4
11
  export declare function registerPrompts(server: McpServer): void;
package/dist/prompts.js CHANGED
@@ -1,31 +1,21 @@
1
1
  import { z } from 'zod';
2
- import { PROMPTS } from './tools/catalog.gen.js';
2
+ import { PROMPTS, renderPrompt } from './tools/catalog.gen.js';
3
3
  import { choiceHas } from './tools/toolsets.gen.js';
4
4
  import { TOOLSET_CHOICE } from './mode.js';
5
5
  /**
6
- * Prompts: the three ways a user starts a conversation with Stratta, as
7
- * slash commands in the clients that support them (Claude Code, Claude
8
- * Desktop, Cursor). stdio only: the remote gateway does not carry prompts yet.
6
+ * Prompts: the ways a user starts a conversation with Stratta, as slash
7
+ * commands in the clients that support them (Claude Code, Claude Desktop,
8
+ * Cursor). The remote server serves the same ones (`convex/mcpPrompts.ts`).
9
9
  *
10
10
  * Text and arguments come from the catalogue, so a wording change is made
11
11
  * once. `{{arg}}` is filled with the argument; an `optional` line is appended
12
12
  * only when its argument was given.
13
13
  */
14
- const fill = (template, args) => template.replace(/\{\{(\w+)\}\}/g, (_match, key) => String(args[key] ?? ''));
15
- export function renderPrompt(p, args) {
16
- let text = fill(p.text, args);
17
- for (const [arg, line] of Object.entries(p.optional ?? {})) {
18
- if (args[arg])
19
- text += `\n${fill(line, args)}`;
20
- }
21
- return text;
22
- }
23
14
  export function registerPrompts(server) {
24
15
  for (const p of PROMPTS) {
25
16
  // A prompt that drives dossier tools is not offered without them.
26
- if (p.name === 'resume-dossier' && !choiceHas(TOOLSET_CHOICE, 'dossiers')) {
17
+ if (p.needs && !choiceHas(TOOLSET_CHOICE, p.needs))
27
18
  continue;
28
- }
29
19
  const shape = {};
30
20
  for (const a of p.args) {
31
21
  const s = z.string().describe(a.description);
@@ -1,3 +1,4 @@
1
+ import type { Toolset } from './toolsets.gen.js';
1
2
  export type Transport = 'stdio' | 'remote';
2
3
  export type ToolAnnotations = {
3
4
  readOnlyHint: boolean;
@@ -13,7 +14,7 @@ export type CatalogEntry = {
13
14
  transports: Transport[];
14
15
  params: Record<string, string>;
15
16
  };
16
- export declare const SERVER_INSTRUCTIONS = "Stratta serves the norms and reference documents of the signed-in organization (technical standards, laws, ordinances, regulations, accounting standards, internal references), and only those. Call `get_methodology` before the first substantive question of a conversation: it carries the navigation order, the citation rules and the dossier policy. A question these documents are meant to settle is answered from them, not from memory; a norm absent from `list_norms` cannot be read.";
17
+ export declare const SERVER_INSTRUCTIONS = "Stratta serves the norms and reference documents of the signed-in organization (technical standards, laws, ordinances, regulations, accounting standards, internal references), and only those. `get_methodology` returns how these documents are organized, how one is walked down to a clause and how a passage is cited; it is meant for the start of a conversation that relies on them. A norm absent from `list_norms` is not readable here.";
17
18
  export declare const CATALOG: CatalogEntry[];
18
19
  export type PromptArg = {
19
20
  name: string;
@@ -27,8 +28,11 @@ export type PromptEntry = {
27
28
  args: PromptArg[];
28
29
  text: string;
29
30
  optional?: Record<string, string>;
31
+ needs?: Toolset;
30
32
  };
31
33
  export declare const PROMPTS: PromptEntry[];
32
34
  export declare function catalogEntry(name: string): CatalogEntry;
35
+ /** The message a prompt starts the conversation with, arguments filled in. */
36
+ export declare function renderPrompt(p: PromptEntry, args: Record<string, unknown>): string;
33
37
  /** Description of one parameter, as the catalogue words it. */
34
38
  export declare function param(tool: string, name: string): string;
@@ -1,11 +1,11 @@
1
1
  // Generated by scripts/build-mcp-catalog.mjs from convex/mcpCatalog.ts.
2
2
  // Do not edit: change the source and run the script.
3
- export const SERVER_INSTRUCTIONS = "Stratta serves the norms and reference documents of the signed-in organization (technical standards, laws, ordinances, regulations, accounting standards, internal references), and only those. Call `get_methodology` before the first substantive question of a conversation: it carries the navigation order, the citation rules and the dossier policy. A question these documents are meant to settle is answered from them, not from memory; a norm absent from `list_norms` cannot be read.";
3
+ export const SERVER_INSTRUCTIONS = "Stratta serves the norms and reference documents of the signed-in organization (technical standards, laws, ordinances, regulations, accounting standards, internal references), and only those. `get_methodology` returns how these documents are organized, how one is walked down to a clause and how a passage is cited; it is meant for the start of a conversation that relies on them. A norm absent from `list_norms` is not readable here.";
4
4
  export const CATALOG = [
5
5
  {
6
6
  "name": "get_methodology",
7
7
  "title": "Working method",
8
- "description": "Read the working contract this server expects an agent to follow: how to walk a norm from its table of contents down to a clause, how to cite what it reads, and when to record something in a project dossier. Use this at the start of a conversation that will rely on the documents of the organization; its rules hold for the rest of that conversation.",
8
+ "description": "Return the conventions of this server: how a norm is walked from its table of contents down to a clause, how a passage is cited, and what a project dossier records. Use this at the start of a conversation that will rely on the documents of the organization.",
9
9
  "annotations": {
10
10
  "readOnlyHint": true,
11
11
  "destructiveHint": false,
@@ -267,7 +267,7 @@ export const CATALOG = [
267
267
  {
268
268
  "name": "get_boreholes",
269
269
  "title": "Read the boreholes of a site",
270
- "description": "Read the boreholes a site scan found, nearest first, with their logged strata (depth interval, description, period), depth, year, SIA 261 ground class when logged, water table when logged, and links to the cantonal documents. Call it after get_site_context when the question turns on what the ground is actually made of. Each borehole is one observation at one point: quote it with its distance, never interpolate between two of them, and never derive a design value from a layer description.",
270
+ "description": "Read the boreholes a site scan found, nearest first, with their logged strata (depth interval, description, period), depth, year, SIA 261 ground class when logged, water table when logged, and links to the cantonal documents. Call it after get_site_context when the question turns on what the ground is actually made of. Each borehole is one observation at one point, at the distance given; the ground between two boreholes and the design values of a layer are not in this data.",
271
271
  "annotations": {
272
272
  "readOnlyHint": true,
273
273
  "destructiveHint": false,
@@ -287,7 +287,7 @@ export const CATALOG = [
287
287
  {
288
288
  "name": "get_site_context",
289
289
  "title": "Read a site scan",
290
- "description": "Read what a site scan found, grouped by topic, each value with its source and the date THE DATA was current. Returns two separate lists: what the registers settled, and what they could NOT settle, with the reason. Report the second list to the user in full: it is what a bid has to budget for. Never derive a design value (friction angle, bearing capacity, modulus) from a geological description returned here; state what the register says and that a local investigation is required.",
290
+ "description": "Read what a site scan found, grouped by topic, each value with its source and the date THE DATA was current. Returns two separate lists: what the registers settled, and what they could NOT settle, with the reason; the second list is what a bid has to budget for. A geological description returned here is not a design value (friction angle, bearing capacity, modulus): those come from a local investigation.",
291
291
  "annotations": {
292
292
  "readOnlyHint": true,
293
293
  "destructiveHint": false,
@@ -722,7 +722,7 @@ export const CATALOG = [
722
722
  {
723
723
  "name": "write_section",
724
724
  "title": "Write a section",
725
- "description": "Write one section of a deliverable: the text as it would be printed, and what it rests on. Every sentence of substance must come from a source the section lists: a decision or piece of evidence of the dossier (kind entry, ref the entry id), a fact of the site (kind fact, ref the fact key such as groundwater.level), a clause read in a norm (kind clause, ref the norm code and the section path as in \"SIA 267 s. 9.5.2\"), a page of an attachment (kind attachment, ref the id with #p12), or a reading recorded by the dossier (kind reading, ref the reading id). A section without a source is refused; when the dossier has nothing for it, pass gap true with a body that says what is missing, and the section is printed as something to establish. Replaces what the section held. Never invent a value: a section written from memory is worse than a gap.",
725
+ "description": "Write one section of a deliverable: the text as it would be printed, and what it rests on. Every sentence of substance must come from a source the section lists: a decision or piece of evidence of the dossier (kind entry, ref the entry id), a fact of the site (kind fact, ref the fact key such as groundwater.level), a clause read in a norm (kind clause, ref the norm code and the section path as in \"SIA 267 s. 9.5.2\"), a page of an attachment (kind attachment, ref the id with #p12), or a reading recorded by the dossier (kind reading, ref the reading id). A section without a source is refused; when the dossier has nothing for it, pass gap true with a body that says what is missing, and the section is printed as something to establish. Replaces what the section held.",
726
726
  "annotations": {
727
727
  "readOnlyHint": false,
728
728
  "destructiveHint": false,
@@ -979,7 +979,7 @@ export const CATALOG = [
979
979
  {
980
980
  "name": "update_entry",
981
981
  "title": "Correct an entry",
982
- "description": "Correct a piece of evidence or a decision in place: its wording, its value, its confidence, or the citation (norm, section path, page, edition) when the wrong clause was cited. Pass only the fields to change; pass page null to clear it. The kind and the question do not change here (attach_entry moves an entry). Only when the user asked for the correction; never rewrite what a colleague wrote without saying so.",
982
+ "description": "Correct a piece of evidence or a decision in place: its wording, its value, its confidence, or the citation (norm, section path, page, edition) when the wrong clause was cited. Pass only the fields to change; pass page null to clear it. The kind and the question do not change here (attach_entry moves an entry). Use this when the user asks for the correction.",
983
983
  "annotations": {
984
984
  "readOnlyHint": false,
985
985
  "destructiveHint": false,
@@ -1260,7 +1260,8 @@ export const PROMPTS = [
1260
1260
  "required": true
1261
1261
  }
1262
1262
  ],
1263
- "text": "Resume the project dossier \"{{name}}\": call list_dossiers, then load_dossier on the matching one. Summarise the open questions first, then the decisions taken with the clauses they rest on, then the evidence not yet filed. Do not re-decide what is decided. Ask which open question to work on next."
1263
+ "text": "Resume the project dossier \"{{name}}\": call list_dossiers, then load_dossier on the matching one. Summarise the open questions first, then the decisions taken with the clauses they rest on, then the evidence not yet filed. Do not re-decide what is decided. Ask which open question to work on next.",
1264
+ "needs": "dossiers"
1264
1265
  },
1265
1266
  {
1266
1267
  "name": "check-value",
@@ -1282,6 +1283,28 @@ export const PROMPTS = [
1282
1283
  "optional": {
1283
1284
  "norm": "Check against {{norm}} first."
1284
1285
  }
1286
+ },
1287
+ {
1288
+ "name": "apply-method",
1289
+ "title": "Do a task the way the office does it",
1290
+ "description": "Carry out a task (a project basis, a verification note, a site chapter) by following the method the office has written for it.",
1291
+ "args": [
1292
+ {
1293
+ "name": "task",
1294
+ "description": "What to produce, e.g. \"the project basis for the retaining wall\".",
1295
+ "required": true
1296
+ },
1297
+ {
1298
+ "name": "project",
1299
+ "description": "The project or dossier the task belongs to, if any.",
1300
+ "required": false
1301
+ }
1302
+ ],
1303
+ "text": "Do this task the way the office does it: {{task}}\n\nCall list_skills and pick the method that fits the task, then get_skill on it and follow its steps in order. If no method fits, say so, name the closest one, and ask before going on without one. Every value taken from a norm keeps its citation [CODE section path, p. N].",
1304
+ "optional": {
1305
+ "project": "The task belongs to the project \"{{project}}\": reload its dossier first (list_dossiers, then load_dossier) and work from what it already holds."
1306
+ },
1307
+ "needs": "dossiers"
1285
1308
  }
1286
1309
  ];
1287
1310
  export function catalogEntry(name) {
@@ -1290,6 +1313,16 @@ export function catalogEntry(name) {
1290
1313
  throw new Error(`No catalogue entry for tool "${name}"`);
1291
1314
  return entry;
1292
1315
  }
1316
+ const fill = (template, args) => template.replace(/\{\{(\w+)\}\}/g, (_match, key) => String(args[key] ?? ''));
1317
+ /** The message a prompt starts the conversation with, arguments filled in. */
1318
+ export function renderPrompt(p, args) {
1319
+ let text = fill(p.text, args);
1320
+ for (const [arg, line] of Object.entries(p.optional ?? {})) {
1321
+ if (args[arg])
1322
+ text += `\n${fill(line, args)}`;
1323
+ }
1324
+ return text;
1325
+ }
1293
1326
  /** Description of one parameter, as the catalogue words it. */
1294
1327
  export function param(tool, name) {
1295
1328
  const text = catalogEntry(tool).params[name];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * What Claude reads in a tool's `_meta`, on both transports.
3
+ *
4
+ * This module imports nothing: `scripts/build-mcp-catalog.mjs` copies it
5
+ * verbatim into the npm package (`claude-meta.gen.ts`), which is published
6
+ * alone and cannot import from here. A test holds the copy in step.
7
+ */
8
+ /**
9
+ * The tools Claude Code keeps loaded instead of deferring them.
10
+ *
11
+ * Claude Code defers every MCP tool and finds them by search: only the tool
12
+ * names and the server instructions load when a session starts
13
+ * (code.claude.com/docs/en/mcp, « Scale with MCP tool search »). The norm
14
+ * reader is what every conversation starts with, so it stays at hand; three
15
+ * to five tools, as the guidance asks, or deferral buys nothing.
16
+ */
17
+ export declare const ALWAYS_LOADED_TOOLS: ReadonlyArray<string>;
18
+ /**
19
+ * The reads that may legitimately run long, and how long.
20
+ *
21
+ * Past 50 000 characters Claude Code writes a result to a file and hands the
22
+ * model a reference; a long clause with its tables is exactly that case, and
23
+ * the agent then cites a text it has not read. The value raises that
24
+ * threshold for the tool (ceiling 500 000).
25
+ */
26
+ export declare const LARGE_RESULT_TOOLS: Readonly<Record<string, number>>;
27
+ /** The `_meta` of a tool, or nothing; other hosts ignore the keys. */
28
+ export declare function claudeMeta(name: string): Record<string, boolean | number> | undefined;
@@ -0,0 +1,48 @@
1
+ // Generated by scripts/build-mcp-catalog.mjs from convex/mcpClaudeMeta.ts.
2
+ // Do not edit: change the source and run the script.
3
+ /**
4
+ * What Claude reads in a tool's `_meta`, on both transports.
5
+ *
6
+ * This module imports nothing: `scripts/build-mcp-catalog.mjs` copies it
7
+ * verbatim into the npm package (`claude-meta.gen.ts`), which is published
8
+ * alone and cannot import from here. A test holds the copy in step.
9
+ */
10
+ /**
11
+ * The tools Claude Code keeps loaded instead of deferring them.
12
+ *
13
+ * Claude Code defers every MCP tool and finds them by search: only the tool
14
+ * names and the server instructions load when a session starts
15
+ * (code.claude.com/docs/en/mcp, « Scale with MCP tool search »). The norm
16
+ * reader is what every conversation starts with, so it stays at hand; three
17
+ * to five tools, as the guidance asks, or deferral buys nothing.
18
+ */
19
+ export const ALWAYS_LOADED_TOOLS = [
20
+ 'get_methodology',
21
+ 'list_norms',
22
+ 'search_corpus',
23
+ 'get_toc',
24
+ 'get_section',
25
+ ];
26
+ /**
27
+ * The reads that may legitimately run long, and how long.
28
+ *
29
+ * Past 50 000 characters Claude Code writes a result to a file and hands the
30
+ * model a reference; a long clause with its tables is exactly that case, and
31
+ * the agent then cites a text it has not read. The value raises that
32
+ * threshold for the tool (ceiling 500 000).
33
+ */
34
+ export const LARGE_RESULT_TOOLS = {
35
+ get_section: 200_000,
36
+ get_subtree: 200_000,
37
+ read_attachment: 200_000,
38
+ };
39
+ /** The `_meta` of a tool, or nothing; other hosts ignore the keys. */
40
+ export function claudeMeta(name) {
41
+ const meta = {};
42
+ if (ALWAYS_LOADED_TOOLS.includes(name))
43
+ meta['anthropic/alwaysLoad'] = true;
44
+ const chars = LARGE_RESULT_TOOLS[name];
45
+ if (chars !== undefined)
46
+ meta['anthropic/maxResultSizeChars'] = chars;
47
+ return Object.keys(meta).length > 0 ? meta : undefined;
48
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stratta/mcp",
3
3
  "mcpName": "ch.stratta/mcp",
4
- "version": "1.20.0",
4
+ "version": "1.21.0",
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"