@stratta/mcp 0.13.1 → 0.15.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.
@@ -358,6 +358,276 @@ export const applyTemplate = defineTool({
358
358
  templateId: args.templateId,
359
359
  }),
360
360
  });
361
+ /* ------------------------------------------------- questions (0.14.0) */
362
+ //
363
+ // Everything the dossier page can read and do to a question (ADR 36). The
364
+ // reads paginate with `cursor`; the two deletions go through the same
365
+ // confirmation gate as `record_decision` (`confirm.ts`), and `confirmed` is
366
+ // read there, never sent.
367
+ const questionStatus = z.enum(['open', 'decided', 'closed']);
368
+ const cursor = (tool) => z.string().min(1).optional().describe(param(tool, 'cursor'));
369
+ const pageLimit = (tool) => z.number().int().min(1).max(200).optional().describe(param(tool, 'limit'));
370
+ export const listQuestions = defineTool({
371
+ ...headOf('list_questions'),
372
+ inputSchema: {
373
+ dossierId: dossierId('list_questions'),
374
+ status: questionStatus
375
+ .optional()
376
+ .describe(param('list_questions', 'status')),
377
+ limit: pageLimit('list_questions'),
378
+ cursor: cursor('list_questions'),
379
+ },
380
+ run: (client, args) => client.action(api.dossiersApi.listQuestions, {
381
+ apiKey: requireApiKey(),
382
+ dossierId: args.dossierId,
383
+ status: args.status,
384
+ limit: args.limit,
385
+ cursor: args.cursor,
386
+ }),
387
+ });
388
+ export const getQuestion = defineTool({
389
+ ...headOf('get_question'),
390
+ inputSchema: {
391
+ questionId: questionId('get_question'),
392
+ limit: pageLimit('get_question'),
393
+ cursor: cursor('get_question'),
394
+ },
395
+ run: (client, args) => client.action(api.dossiersApi.getQuestion, {
396
+ apiKey: requireApiKey(),
397
+ questionId: args.questionId,
398
+ limit: args.limit,
399
+ cursor: args.cursor,
400
+ }),
401
+ });
402
+ export const getDossierActivity = defineTool({
403
+ ...headOf('get_dossier_activity'),
404
+ inputSchema: {
405
+ dossierId: dossierId('get_dossier_activity'),
406
+ limit: pageLimit('get_dossier_activity'),
407
+ cursor: cursor('get_dossier_activity'),
408
+ },
409
+ run: (client, args) => client.action(api.dossiersApi.getDossierActivity, {
410
+ apiKey: requireApiKey(),
411
+ dossierId: args.dossierId,
412
+ limit: args.limit,
413
+ cursor: args.cursor,
414
+ }),
415
+ });
416
+ export const listExports = defineTool({
417
+ ...headOf('list_exports'),
418
+ inputSchema: { dossierId: dossierId('list_exports') },
419
+ run: (client, args) => client.action(api.dossiersApi.listExports, {
420
+ apiKey: requireApiKey(),
421
+ dossierId: args.dossierId,
422
+ }),
423
+ });
424
+ export const updateQuestion = defineTool({
425
+ ...headOf('update_question'),
426
+ inputSchema: {
427
+ questionId: questionId('update_question'),
428
+ title: z
429
+ .string()
430
+ .min(1)
431
+ .max(200)
432
+ .optional()
433
+ .describe(param('update_question', 'title')),
434
+ body: z
435
+ .string()
436
+ .max(4000)
437
+ .optional()
438
+ .describe(param('update_question', 'body')),
439
+ assigneeEmail: z
440
+ .email()
441
+ .nullable()
442
+ .optional()
443
+ .describe(param('update_question', 'assigneeEmail')),
444
+ dueAt: z
445
+ .string()
446
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
447
+ .nullable()
448
+ .optional()
449
+ .describe(param('update_question', 'dueAt')),
450
+ },
451
+ run: (client, args) => client.action(api.dossiersApi.updateQuestion, {
452
+ apiKey: requireApiKey(),
453
+ questionId: args.questionId,
454
+ title: args.title,
455
+ body: args.body,
456
+ assigneeEmail: args.assigneeEmail,
457
+ dueAt: args.dueAt,
458
+ }),
459
+ });
460
+ export const addOption = defineTool({
461
+ ...headOf('add_option'),
462
+ inputSchema: {
463
+ questionId: questionId('add_option'),
464
+ name: z.string().min(1).max(120).describe(param('add_option', 'name')),
465
+ description: z
466
+ .string()
467
+ .max(2000)
468
+ .optional()
469
+ .describe(param('add_option', 'description')),
470
+ },
471
+ run: (client, args) => client.action(api.dossiersApi.addOption, {
472
+ apiKey: requireApiKey(),
473
+ questionId: args.questionId,
474
+ name: args.name,
475
+ description: args.description,
476
+ }),
477
+ });
478
+ const optionId = (tool) => z.string().min(1).describe(param(tool, 'optionId'));
479
+ export const updateOption = defineTool({
480
+ ...headOf('update_option'),
481
+ inputSchema: {
482
+ optionId: optionId('update_option'),
483
+ name: z
484
+ .string()
485
+ .min(1)
486
+ .max(120)
487
+ .optional()
488
+ .describe(param('update_option', 'name')),
489
+ description: z
490
+ .string()
491
+ .max(2000)
492
+ .optional()
493
+ .describe(param('update_option', 'description')),
494
+ retained: z
495
+ .boolean()
496
+ .optional()
497
+ .describe(param('update_option', 'retained')),
498
+ },
499
+ run: (client, args) => client.action(api.dossiersApi.updateOption, {
500
+ apiKey: requireApiKey(),
501
+ optionId: args.optionId,
502
+ name: args.name,
503
+ description: args.description,
504
+ retained: args.retained,
505
+ }),
506
+ });
507
+ export const deleteOption = defineTool({
508
+ ...headOf('delete_option'),
509
+ inputSchema: {
510
+ optionId: optionId('delete_option'),
511
+ confirmed: z
512
+ .boolean()
513
+ .optional()
514
+ .describe(param('delete_option', 'confirmed')),
515
+ },
516
+ run: (client, { confirmed: _confirmed, ...args }) => client.action(api.dossiersApi.deleteOption, {
517
+ apiKey: requireApiKey(),
518
+ optionId: args.optionId,
519
+ }),
520
+ });
521
+ export const deleteQuestion = defineTool({
522
+ ...headOf('delete_question'),
523
+ inputSchema: {
524
+ questionId: questionId('delete_question'),
525
+ confirmed: z
526
+ .boolean()
527
+ .optional()
528
+ .describe(param('delete_question', 'confirmed')),
529
+ },
530
+ run: (client, { confirmed: _confirmed, ...args }) => client.action(api.dossiersApi.deleteQuestion, {
531
+ apiKey: requireApiKey(),
532
+ questionId: args.questionId,
533
+ }),
534
+ });
535
+ export const addComment = defineTool({
536
+ ...headOf('add_comment'),
537
+ inputSchema: {
538
+ dossierId: dossierId('add_comment'),
539
+ body: z.string().min(1).max(4000).describe(param('add_comment', 'body')),
540
+ questionId: questionId('add_comment').optional(),
541
+ entryId: z
542
+ .string()
543
+ .min(1)
544
+ .optional()
545
+ .describe(param('add_comment', 'entryId')),
546
+ },
547
+ run: (client, args) => client.action(api.dossiersApi.addComment, {
548
+ apiKey: requireApiKey(),
549
+ dossierId: args.dossierId,
550
+ body: args.body,
551
+ questionId: args.questionId,
552
+ entryId: args.entryId,
553
+ }),
554
+ });
555
+ export const updateEntry = defineTool({
556
+ ...headOf('update_entry'),
557
+ inputSchema: {
558
+ entryId: z.string().min(1).describe(param('update_entry', 'entryId')),
559
+ title: z
560
+ .string()
561
+ .min(1)
562
+ .max(200)
563
+ .optional()
564
+ .describe(param('update_entry', 'title')),
565
+ detail: z
566
+ .string()
567
+ .max(4000)
568
+ .optional()
569
+ .describe(param('update_entry', 'detail')),
570
+ value: z
571
+ .string()
572
+ .max(120)
573
+ .optional()
574
+ .describe(param('update_entry', 'value')),
575
+ confidence: z
576
+ .enum(['established', 'judgement', 'to_confirm'])
577
+ .optional()
578
+ .describe(param('update_entry', 'confidence')),
579
+ normCode: z
580
+ .string()
581
+ .max(40)
582
+ .optional()
583
+ .describe(param('update_entry', 'normCode')),
584
+ sectionPath: z
585
+ .string()
586
+ .max(60)
587
+ .optional()
588
+ .describe(param('update_entry', 'sectionPath')),
589
+ page: z
590
+ .number()
591
+ .int()
592
+ .positive()
593
+ .nullable()
594
+ .optional()
595
+ .describe(param('update_entry', 'page')),
596
+ normEdition: z
597
+ .string()
598
+ .max(40)
599
+ .optional()
600
+ .describe(param('update_entry', 'normEdition')),
601
+ },
602
+ run: (client, args) => client.action(api.dossiersApi.updateEntry, {
603
+ apiKey: requireApiKey(),
604
+ ...args,
605
+ entryId: args.entryId,
606
+ }),
607
+ });
608
+ export const attachEntry = defineTool({
609
+ ...headOf('attach_entry'),
610
+ inputSchema: {
611
+ entryId: z.string().min(1).describe(param('attach_entry', 'entryId')),
612
+ questionId: z
613
+ .string()
614
+ .min(1)
615
+ .nullable()
616
+ .describe(param('attach_entry', 'questionId')),
617
+ optionId: z
618
+ .string()
619
+ .min(1)
620
+ .nullable()
621
+ .optional()
622
+ .describe(param('attach_entry', 'optionId')),
623
+ },
624
+ run: (client, args) => client.action(api.dossiersApi.attachEntry, {
625
+ apiKey: requireApiKey(),
626
+ entryId: args.entryId,
627
+ questionId: args.questionId,
628
+ optionId: args.optionId,
629
+ }),
630
+ });
361
631
  export const dossierTools = [
362
632
  listDossiers,
363
633
  openDossier,
@@ -372,4 +642,16 @@ export const dossierTools = [
372
642
  addAttachment,
373
643
  listTemplates,
374
644
  applyTemplate,
645
+ listQuestions,
646
+ getQuestion,
647
+ getDossierActivity,
648
+ listExports,
649
+ updateQuestion,
650
+ addOption,
651
+ updateOption,
652
+ deleteOption,
653
+ deleteQuestion,
654
+ addComment,
655
+ updateEntry,
656
+ attachEntry,
375
657
  ];
@@ -77,6 +77,7 @@ export declare const ingestPublish: import("./define.js").ToolDef<{
77
77
  }>;
78
78
  export declare const ingestDelete: import("./define.js").ToolDef<{
79
79
  documentId: z.ZodString;
80
+ confirmed: z.ZodOptional<z.ZodBoolean>;
80
81
  }>;
81
82
  export declare const ingestTools: (import("./define.js").ToolDef<{
82
83
  code: z.ZodString;
@@ -145,4 +146,7 @@ export declare const ingestTools: (import("./define.js").ToolDef<{
145
146
  }> | import("./define.js").ToolDef<{
146
147
  documentId: z.ZodString;
147
148
  force: z.ZodOptional<z.ZodBoolean>;
149
+ }> | import("./define.js").ToolDef<{
150
+ documentId: z.ZodString;
151
+ confirmed: z.ZodOptional<z.ZodBoolean>;
148
152
  }>)[];
@@ -8,8 +8,10 @@ import { defineTool } from './define.js';
8
8
  * its Stratta workspace, from the user's own agent. Every call is
9
9
  * authenticated by the user's API key and scoped server-side to their
10
10
  * organisation (see convex/ingestionApi.ts). Driven by the `ingest-norm`
11
- * skill. Absent from the remote transport on purpose: the pipeline starts
12
- * with a PDF on the caller's disk.
11
+ * skill. ⚠️ They are served on the remote connector too since 2026-09-05
12
+ * (`convex/mcpIngestRegistrations.ts`): the pre-pass still reads a PDF on the
13
+ * caller's disk, which rules out a browser client but not Claude Code, Codex,
14
+ * Cursor or Gemini CLI.
13
15
  *
14
16
  * Names, descriptions and annotations come from the generated catalogue. The
15
17
  * Zod shapes below are enforced by the SDK before a handler runs, so a bad
@@ -232,7 +234,14 @@ export const ingestPublish = defineTool({
232
234
  });
233
235
  export const ingestDelete = defineTool({
234
236
  ...headOf('ingest_delete'),
235
- inputSchema: { documentId: documentId('ingest_delete') },
237
+ inputSchema: {
238
+ documentId: documentId('ingest_delete'),
239
+ // Read by the confirmation gate in `confirm.ts`, never sent on.
240
+ confirmed: z
241
+ .boolean()
242
+ .optional()
243
+ .describe(param('ingest_delete', 'confirmed')),
244
+ },
236
245
  run: async (client, args) => {
237
246
  await client.action(api.ingestionApi.deleteDocument, {
238
247
  apiKey: requireApiKey(),
@@ -30,6 +30,41 @@ export declare const getBoreholes: import("./define.js").ToolDef<{
30
30
  limit: z.ZodOptional<z.ZodNumber>;
31
31
  withStrataOnly: z.ZodOptional<z.ZodBoolean>;
32
32
  }>;
33
+ /**
34
+ * The Site tab of a dossier, read by an agent (0.14.0, ADR 36).
35
+ *
36
+ * `get_site_context` is the summary shaped for an answer; these are the
37
+ * record: every fact with its key and notice, the section along the axis,
38
+ * the map areas, the piezometric contours, the photos, the neighbouring
39
+ * projects, and the list of sheets. Geometry is left out unless asked for:
40
+ * a water protection sector runs to hundreds of vertices, and an agent
41
+ * reads distances, not polygons.
42
+ */
43
+ export declare const listSites: import("./define.js").ToolDef<{
44
+ limit: z.ZodOptional<z.ZodNumber>;
45
+ }>;
46
+ export declare const getSite: import("./define.js").ToolDef<{
47
+ siteId: z.ZodString;
48
+ }>;
49
+ export declare const getSiteSection: import("./define.js").ToolDef<{
50
+ siteId: z.ZodString;
51
+ maxPoints: z.ZodOptional<z.ZodNumber>;
52
+ }>;
53
+ export declare const listSiteOverlays: import("./define.js").ToolDef<{
54
+ siteId: z.ZodString;
55
+ includeGeometry: z.ZodOptional<z.ZodBoolean>;
56
+ }>;
57
+ export declare const getGroundwaterContours: import("./define.js").ToolDef<{
58
+ siteId: z.ZodString;
59
+ includeGeometry: z.ZodOptional<z.ZodBoolean>;
60
+ }>;
61
+ export declare const listSitePhotos: import("./define.js").ToolDef<{
62
+ siteId: z.ZodString;
63
+ }>;
64
+ export declare const listSiteNeighbours: import("./define.js").ToolDef<{
65
+ siteId: z.ZodString;
66
+ radius: z.ZodOptional<z.ZodNumber>;
67
+ }>;
33
68
  export declare const siteTools: (import("./define.js").ToolDef<{
34
69
  dossierId: z.ZodString;
35
70
  address: z.ZodString;
@@ -50,4 +85,15 @@ export declare const siteTools: (import("./define.js").ToolDef<{
50
85
  */
51
86
  limit: z.ZodOptional<z.ZodNumber>;
52
87
  withStrataOnly: z.ZodOptional<z.ZodBoolean>;
88
+ }> | import("./define.js").ToolDef<{
89
+ limit: z.ZodOptional<z.ZodNumber>;
90
+ }> | import("./define.js").ToolDef<{
91
+ siteId: z.ZodString;
92
+ maxPoints: z.ZodOptional<z.ZodNumber>;
93
+ }> | import("./define.js").ToolDef<{
94
+ siteId: z.ZodString;
95
+ includeGeometry: z.ZodOptional<z.ZodBoolean>;
96
+ }> | import("./define.js").ToolDef<{
97
+ siteId: z.ZodString;
98
+ radius: z.ZodOptional<z.ZodNumber>;
53
99
  }>)[];
@@ -97,4 +97,124 @@ export const getBoreholes = defineTool({
97
97
  withStrataOnly: args.withStrataOnly,
98
98
  }),
99
99
  });
100
- export const siteTools = [scanSite, getSiteContext, getBoreholes];
100
+ const siteId = (tool) => z.string().min(1).describe(param(tool, 'siteId'));
101
+ /**
102
+ * The Site tab of a dossier, read by an agent (0.14.0, ADR 36).
103
+ *
104
+ * `get_site_context` is the summary shaped for an answer; these are the
105
+ * record: every fact with its key and notice, the section along the axis,
106
+ * the map areas, the piezometric contours, the photos, the neighbouring
107
+ * projects, and the list of sheets. Geometry is left out unless asked for:
108
+ * a water protection sector runs to hundreds of vertices, and an agent
109
+ * reads distances, not polygons.
110
+ */
111
+ export const listSites = defineTool({
112
+ ...headOf('list_sites'),
113
+ inputSchema: {
114
+ limit: z
115
+ .number()
116
+ .int()
117
+ .min(1)
118
+ .max(200)
119
+ .optional()
120
+ .describe(param('list_sites', 'limit')),
121
+ },
122
+ run: (client, args) => client.action(api.sitesApi.listSites, {
123
+ apiKey: requireApiKey(),
124
+ limit: args.limit,
125
+ }),
126
+ });
127
+ export const getSite = defineTool({
128
+ ...headOf('get_site'),
129
+ inputSchema: { siteId: siteId('get_site') },
130
+ run: (client, args) => client.action(api.sitesApi.getSite, {
131
+ apiKey: requireApiKey(),
132
+ siteId: args.siteId,
133
+ }),
134
+ });
135
+ export const getSiteSection = defineTool({
136
+ ...headOf('get_site_section'),
137
+ inputSchema: {
138
+ siteId: siteId('get_site_section'),
139
+ maxPoints: z
140
+ .number()
141
+ .int()
142
+ .min(2)
143
+ .max(200)
144
+ .optional()
145
+ .describe(param('get_site_section', 'maxPoints')),
146
+ },
147
+ run: (client, args) => client.action(api.sitesApi.getSiteSection, {
148
+ apiKey: requireApiKey(),
149
+ siteId: args.siteId,
150
+ maxPoints: args.maxPoints,
151
+ }),
152
+ });
153
+ export const listSiteOverlays = defineTool({
154
+ ...headOf('list_site_overlays'),
155
+ inputSchema: {
156
+ siteId: siteId('list_site_overlays'),
157
+ includeGeometry: z
158
+ .boolean()
159
+ .optional()
160
+ .describe(param('list_site_overlays', 'includeGeometry')),
161
+ },
162
+ run: (client, args) => client.action(api.sitesApi.listSiteOverlays, {
163
+ apiKey: requireApiKey(),
164
+ siteId: args.siteId,
165
+ includeGeometry: args.includeGeometry,
166
+ }),
167
+ });
168
+ export const getGroundwaterContours = defineTool({
169
+ ...headOf('get_groundwater_contours'),
170
+ inputSchema: {
171
+ siteId: siteId('get_groundwater_contours'),
172
+ includeGeometry: z
173
+ .boolean()
174
+ .optional()
175
+ .describe(param('get_groundwater_contours', 'includeGeometry')),
176
+ },
177
+ run: (client, args) => client.action(api.sitesApi.getGroundwaterContours, {
178
+ apiKey: requireApiKey(),
179
+ siteId: args.siteId,
180
+ includeGeometry: args.includeGeometry,
181
+ }),
182
+ });
183
+ export const listSitePhotos = defineTool({
184
+ ...headOf('list_site_photos'),
185
+ inputSchema: { siteId: siteId('list_site_photos') },
186
+ run: (client, args) => client.action(api.sitesApi.listSitePhotos, {
187
+ apiKey: requireApiKey(),
188
+ siteId: args.siteId,
189
+ }),
190
+ });
191
+ export const listSiteNeighbours = defineTool({
192
+ ...headOf('list_site_neighbours'),
193
+ inputSchema: {
194
+ siteId: siteId('list_site_neighbours'),
195
+ radius: z
196
+ .number()
197
+ .int()
198
+ .min(50)
199
+ .max(5000)
200
+ .optional()
201
+ .describe(param('list_site_neighbours', 'radius')),
202
+ },
203
+ run: (client, args) => client.action(api.sitesApi.listSiteNeighbours, {
204
+ apiKey: requireApiKey(),
205
+ siteId: args.siteId,
206
+ radius: args.radius,
207
+ }),
208
+ });
209
+ export const siteTools = [
210
+ scanSite,
211
+ getSiteContext,
212
+ getBoreholes,
213
+ listSites,
214
+ getSite,
215
+ getSiteSection,
216
+ listSiteOverlays,
217
+ getGroundwaterContours,
218
+ listSitePhotos,
219
+ listSiteNeighbours,
220
+ ];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stratta/mcp",
3
3
  "mcpName": "ch.stratta/mcp",
4
- "version": "0.13.1",
4
+ "version": "0.15.0",
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>",
@@ -46,7 +46,7 @@ A conversation disappears, a project lasts months. The dossier is what remains:
46
46
 
47
47
  **Open one** (`open_dossier`) when the user names a project, a structure, a site or a mandate, when a value is retained for a design rather than looked up, when a hypothesis is set, when a site observation is reported, or when a question is left open. Do not open a dossier for a one-off lookup with no project behind it.
48
48
 
49
- **Reload it first** (`list_dossiers`, `load_dossier`) BEFORE answering, as soon as the user returns to a project already named ("pick up dossier X", "where were we on Y"): reloading is what stops a decision already taken from being taken again. Read its entries and its open questions before answering anything about that project.
49
+ **Reload it first** (`list_dossiers`, `load_dossier`) BEFORE answering, as soon as the user returns to a project already named ("pick up dossier X", "where were we on Y"): reloading is what stops a decision already taken from being taken again. Read its entries and its open questions before answering anything about that project. To work on one question, `get_question` reads it in full (options, evidence with every citation field, comments); `attach_entry` files what `load_dossier` lists as unclassified. A question is edited with `update_question`, never deleted unless the user says so.
50
50
 
51
51
  **Read its attachments.** A dossier carries the project's own documents (site reports, borehole logs, meeting minutes): `list_attachments`, then `read_attachment` on what bears on the question, or `search_in_dossier` when the dossier holds many. A fact from a piece is evidence like a clause: cite it as [name, p. N] and file it with `attachmentId` and `attachmentPage`.
52
52