@stratta/mcp 0.5.0 → 0.7.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.
@@ -1,250 +1,237 @@
1
+ import { z } from 'zod';
1
2
  import { api } from '../client.js';
2
3
  import { requireApiKey } from '../auth.js';
3
- // Ingestion tools — let an engineering office add ITS OWN licensed norms to its
4
- // Stratta workspace, agentically, from the user's own Claude. Every call is
5
- // authenticated by the user's STRATTA_API_KEY and scoped server-side to their
6
- // organization (see convex/ingestionApi.ts). Driven by the `ingest-norm` skill.
7
- const sectionObject = {
8
- type: 'object',
9
- properties: {
10
- nodeId: { type: 'string', description: 'Stable unique id within the doc (e.g. "s-4-2-1").' },
11
- parentNodeId: { type: 'string', description: 'nodeId of the parent section, omit for roots.' },
12
- path: { type: 'string', description: 'Human path, e.g. "4.2.1" or "Annexe B.1".' },
13
- title: { type: 'string' },
14
- summary: { type: 'string', description: '1-3 sentence summary used for tree navigation.' },
15
- depth: { type: 'number', description: '0 = chapter, 1 = section, etc.' },
16
- content: { type: 'string', description: 'Full enriched text (LaTeX formulas inline ok).' },
17
- rawContent: { type: 'string', description: 'Raw extracted text.' },
18
- pageStart: { type: 'number' },
19
- pageEnd: { type: 'number' },
20
- orderIndex: { type: 'number' },
4
+ import { defineTool } from './define.js';
5
+ /**
6
+ * Ingestion tools — let an engineering office add ITS OWN licensed norms to
7
+ * its Stratta workspace, agentically, from the user's own Claude. Every call
8
+ * is authenticated by the user's API key and scoped server-side to their
9
+ * organization (see convex/ingestionApi.ts). Driven by the `ingest-norm` skill.
10
+ *
11
+ * These used to be JSON Schema literals paired with a 90-line switch of
12
+ * `args.year as number` casts. A cast is not a check: a client sending
13
+ * `"2020"` passed straight through and failed deep inside Convex with an error
14
+ * the agent could do nothing with. The Zod shapes below are enforced by the
15
+ * SDK before the handler is entered, so a bad call fails at the boundary with
16
+ * a message naming the field.
17
+ */
18
+ const documentId = z
19
+ .string()
20
+ .min(1)
21
+ .describe('Document id from ingest_create_document.');
22
+ const sectionId = z
23
+ .string()
24
+ .min(1)
25
+ .describe('Section id from ingest_create_sections.');
26
+ const sectionInput = z.object({
27
+ nodeId: z
28
+ .string()
29
+ .min(1)
30
+ .describe('Stable unique id within the doc (e.g. "s-4-2-1").'),
31
+ parentNodeId: z
32
+ .string()
33
+ .optional()
34
+ .describe('nodeId of the parent section, omit for roots.'),
35
+ path: z.string().min(1).describe('Human path, e.g. "4.2.1" or "Annexe B.1".'),
36
+ title: z.string().min(1),
37
+ summary: z
38
+ .string()
39
+ .describe('1-3 sentence summary used for tree navigation.'),
40
+ depth: z.number().int().min(0).describe('0 = chapter, 1 = section, etc.'),
41
+ content: z
42
+ .string()
43
+ .describe('Full enriched text (LaTeX formulas inline ok).'),
44
+ rawContent: z.string().describe('Raw extracted text.'),
45
+ pageStart: z.number().int().min(0),
46
+ pageEnd: z.number().int().min(0),
47
+ orderIndex: z.number().int().min(0),
48
+ });
49
+ /** Writes: not read-only, and re-running one is not automatically safe. */
50
+ const write = { readOnlyHint: false, openWorldHint: false };
51
+ export const ingestStatus = defineTool({
52
+ name: 'ingest_status',
53
+ title: 'Check ingestion status',
54
+ description: 'Check whether a norm (by code) already exists in YOUR workspace and how many sections it has. Call before creating to avoid duplicates.',
55
+ inputSchema: {
56
+ code: z.string().min(1).describe('Norm code, e.g. "SIA 261".'),
21
57
  },
22
- required: ['nodeId', 'path', 'title', 'summary', 'depth', 'content', 'rawContent', 'pageStart', 'pageEnd', 'orderIndex'],
23
- additionalProperties: false,
24
- };
25
- export const ingestTools = [
26
- {
27
- name: 'ingest_status',
28
- description: 'Check whether a norm (by code) already exists in YOUR workspace and how many sections it has. Call before creating to avoid duplicates.',
29
- inputSchema: {
30
- type: 'object',
31
- properties: { code: { type: 'string', description: 'Norm code, e.g. "SIA 261".' } },
32
- required: ['code'],
33
- additionalProperties: false,
34
- },
35
- },
36
- {
37
- name: 'ingest_create_document',
38
- description: 'Create a draft norm document in YOUR workspace. Returns the documentId used by subsequent ingest_* calls. You must hold a valid licence for the norm you ingest.',
39
- inputSchema: {
40
- type: 'object',
41
- properties: {
42
- code: { type: 'string' },
43
- year: { type: 'number' },
44
- title: { type: 'string' },
45
- language: { type: 'string', enum: ['fr', 'de', 'it', 'en'] },
46
- totalPages: { type: 'number' },
47
- },
48
- required: ['code', 'year', 'title', 'language', 'totalPages'],
49
- additionalProperties: false,
50
- },
58
+ annotations: { readOnlyHint: true, openWorldHint: false },
59
+ run: (client, args) => client.action(api.ingestionApi.ingestStatus, {
60
+ apiKey: requireApiKey(),
61
+ code: args.code,
62
+ }),
63
+ });
64
+ export const ingestCreateDocument = defineTool({
65
+ name: 'ingest_create_document',
66
+ title: 'Create norm document',
67
+ description: 'Create a draft norm document in YOUR workspace. Returns the documentId used by subsequent ingest_* calls. You must hold a valid licence for the norm you ingest.',
68
+ inputSchema: {
69
+ code: z.string().min(1),
70
+ year: z.number().int().min(1900).max(2100),
71
+ title: z.string().min(1),
72
+ language: z.enum(['fr', 'de', 'it', 'en']),
73
+ totalPages: z.number().int().min(1),
51
74
  },
52
- {
53
- name: 'ingest_create_sections',
54
- description: 'Bulk-insert sections into a draft document. Parent links resolve via parentNodeId within the batch + already-inserted sections. Returns a map of nodeId → sectionId (use those ids to attach formulas/tables/figures/cross-refs).',
55
- inputSchema: {
56
- type: 'object',
57
- properties: {
58
- documentId: { type: 'string' },
59
- sections: { type: 'array', items: sectionObject },
60
- },
61
- required: ['documentId', 'sections'],
62
- additionalProperties: false,
63
- },
75
+ annotations: write,
76
+ run: async (client, args) => ({
77
+ documentId: await client.action(api.ingestionApi.createDocument, {
78
+ apiKey: requireApiKey(),
79
+ ...args,
80
+ }),
81
+ }),
82
+ });
83
+ export const ingestCreateSections = defineTool({
84
+ name: 'ingest_create_sections',
85
+ title: 'Create sections',
86
+ description: 'Bulk-insert sections into a draft document. Parent links resolve via parentNodeId within the batch + already-inserted sections. Returns a map of nodeId → sectionId (use those ids to attach formulas/tables/figures/cross-refs).',
87
+ inputSchema: {
88
+ documentId,
89
+ sections: z.array(sectionInput).min(1).max(200),
64
90
  },
65
- {
66
- name: 'ingest_attach_formula',
67
- description: 'Attach a LaTeX formula to a section.',
68
- inputSchema: {
69
- type: 'object',
70
- properties: {
71
- sectionId: { type: 'string' },
72
- latex: { type: 'string' },
73
- description: { type: 'string' },
74
- formulaNumber: { type: 'string' },
75
- },
76
- required: ['sectionId', 'latex', 'description', 'formulaNumber'],
77
- additionalProperties: false,
78
- },
91
+ annotations: write,
92
+ run: async (client, args) => ({
93
+ sections: await client.action(api.ingestionApi.createSections, {
94
+ apiKey: requireApiKey(),
95
+ documentId: args.documentId,
96
+ sections: args.sections,
97
+ }),
98
+ }),
99
+ });
100
+ export const ingestAttachFormula = defineTool({
101
+ name: 'ingest_attach_formula',
102
+ title: 'Attach formula',
103
+ description: 'Attach a LaTeX formula to a section.',
104
+ inputSchema: {
105
+ sectionId,
106
+ latex: z.string().min(1),
107
+ description: z.string(),
108
+ formulaNumber: z.string(),
79
109
  },
80
- {
81
- name: 'ingest_attach_table',
82
- description: 'Attach a structured table to a section. `data` should be { headers: string[], rows: string[][] }.',
83
- inputSchema: {
84
- type: 'object',
85
- properties: {
86
- sectionId: { type: 'string' },
87
- data: { type: 'object' },
88
- caption: { type: 'string' },
89
- tableNumber: { type: 'string' },
90
- },
91
- required: ['sectionId', 'data', 'caption', 'tableNumber'],
92
- additionalProperties: false,
93
- },
110
+ annotations: write,
111
+ run: async (client, args) => ({
112
+ formulaId: await client.action(api.ingestionApi.attachFormula, {
113
+ apiKey: requireApiKey(),
114
+ ...args,
115
+ }),
116
+ }),
117
+ });
118
+ export const ingestAttachTable = defineTool({
119
+ name: 'ingest_attach_table',
120
+ title: 'Attach table',
121
+ description: 'Attach a structured table to a section. `data` is { headers: string[], rows: string[][] }.',
122
+ inputSchema: {
123
+ sectionId,
124
+ // Cells are coerced rather than required to be strings. Every table
125
+ // currently stored has string cells, but a norm table is full of figures
126
+ // and an agent will sooner or later send `2400` instead of `"2400"` —
127
+ // failing mid-ingestion over that would cost far more than normalising it.
128
+ // The shape itself stays strict: headers and rows are what the renderer
129
+ // reads.
130
+ data: z.object({
131
+ headers: z.array(z.coerce.string()),
132
+ rows: z.array(z.array(z.coerce.string())),
133
+ }),
134
+ caption: z.string(),
135
+ tableNumber: z.string(),
94
136
  },
95
- {
96
- name: 'ingest_attach_cross_ref',
97
- description: 'Attach a cross-reference from a section to another norm/section.',
98
- inputSchema: {
99
- type: 'object',
100
- properties: {
101
- sourceSectionId: { type: 'string' },
102
- targetDocumentCode: { type: 'string' },
103
- targetSectionPath: { type: 'string' },
104
- refText: { type: 'string' },
105
- refType: { type: 'string', enum: ['explicit', 'implicit'] },
106
- },
107
- required: ['sourceSectionId', 'targetDocumentCode', 'refText', 'refType'],
108
- additionalProperties: false,
109
- },
137
+ annotations: write,
138
+ run: async (client, args) => ({
139
+ tableId: await client.action(api.ingestionApi.attachTable, {
140
+ apiKey: requireApiKey(),
141
+ ...args,
142
+ }),
143
+ }),
144
+ });
145
+ export const ingestAttachCrossRef = defineTool({
146
+ name: 'ingest_attach_cross_ref',
147
+ title: 'Attach cross-reference',
148
+ description: 'Attach a cross-reference from a section to another norm/section.',
149
+ inputSchema: {
150
+ sourceSectionId: sectionId,
151
+ targetDocumentCode: z.string().min(1),
152
+ targetSectionPath: z.string().optional(),
153
+ refText: z.string(),
154
+ refType: z.enum(['explicit', 'implicit']),
110
155
  },
111
- {
112
- name: 'ingest_upload_figure',
113
- description: 'Upload a figure image (base64) and attach it to a section. mimeType must be image/png, image/jpeg or image/webp; max 8 MB.',
114
- inputSchema: {
115
- type: 'object',
116
- properties: {
117
- sectionId: { type: 'string' },
118
- base64: { type: 'string', description: 'Base64-encoded image bytes (data: prefix tolerated).' },
119
- mimeType: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp'] },
120
- caption: { type: 'string' },
121
- figureNumber: { type: 'string' },
122
- },
123
- required: ['sectionId', 'base64', 'mimeType', 'caption', 'figureNumber'],
124
- additionalProperties: false,
125
- },
156
+ annotations: write,
157
+ run: async (client, args) => ({
158
+ crossRefId: await client.action(api.ingestionApi.attachCrossRef, {
159
+ apiKey: requireApiKey(),
160
+ ...args,
161
+ }),
162
+ }),
163
+ });
164
+ export const ingestUploadFigure = defineTool({
165
+ name: 'ingest_upload_figure',
166
+ title: 'Upload figure',
167
+ description: 'Upload a figure image (base64) and attach it to a section. mimeType must be image/png, image/jpeg or image/webp; max 8 MB.',
168
+ inputSchema: {
169
+ sectionId,
170
+ base64: z
171
+ .string()
172
+ .min(1)
173
+ .describe('Base64-encoded image bytes (data: prefix tolerated).'),
174
+ mimeType: z.enum(['image/png', 'image/jpeg', 'image/webp']),
175
+ caption: z.string(),
176
+ figureNumber: z.string(),
126
177
  },
127
- {
128
- name: 'ingest_normalize_cross_refs',
129
- description: 'Scan all sections of a document and (re)build cross-references to other norms detected in the text. Idempotent.',
130
- inputSchema: {
131
- type: 'object',
132
- properties: { documentId: { type: 'string' } },
133
- required: ['documentId'],
134
- additionalProperties: false,
135
- },
178
+ annotations: write,
179
+ run: async (client, args) => ({
180
+ figureId: await client.action(api.ingestionApi.uploadFigure, {
181
+ apiKey: requireApiKey(),
182
+ ...args,
183
+ }),
184
+ }),
185
+ });
186
+ export const ingestNormalizeCrossRefs = defineTool({
187
+ name: 'ingest_normalize_cross_refs',
188
+ title: 'Normalize cross-references',
189
+ description: 'Scan all sections of a document and (re)build cross-references to other norms detected in the text. Idempotent.',
190
+ inputSchema: { documentId },
191
+ annotations: { ...write, idempotentHint: true },
192
+ run: (client, args) => client.action(api.ingestionApi.normalizeCrossRefs, {
193
+ apiKey: requireApiKey(),
194
+ documentId: args.documentId,
195
+ }),
196
+ });
197
+ export const ingestPublish = defineTool({
198
+ name: 'ingest_publish',
199
+ title: 'Publish document',
200
+ description: 'Publish a draft document so it becomes queryable via the read tools in YOUR workspace.',
201
+ inputSchema: { documentId },
202
+ annotations: { ...write, idempotentHint: true },
203
+ run: async (client, args) => {
204
+ await client.action(api.ingestionApi.publishDocument, {
205
+ apiKey: requireApiKey(),
206
+ documentId: args.documentId,
207
+ });
208
+ return { published: true };
136
209
  },
137
- {
138
- name: 'ingest_publish',
139
- description: 'Publish a draft document so it becomes queryable via the read tools in YOUR workspace.',
140
- inputSchema: {
141
- type: 'object',
142
- properties: { documentId: { type: 'string' } },
143
- required: ['documentId'],
144
- additionalProperties: false,
145
- },
146
- },
147
- {
148
- name: 'ingest_delete',
149
- description: 'Delete a document and all its sections/figures/tables/formulas/cross-refs. Use before re-ingesting.',
150
- inputSchema: {
151
- type: 'object',
152
- properties: { documentId: { type: 'string' } },
153
- required: ['documentId'],
154
- additionalProperties: false,
155
- },
210
+ });
211
+ export const ingestDelete = defineTool({
212
+ name: 'ingest_delete',
213
+ title: 'Delete document',
214
+ description: 'Delete a document and all its sections/figures/tables/formulas/cross-refs. Use before re-ingesting.',
215
+ inputSchema: { documentId },
216
+ // The one genuinely destructive tool. Clients use this hint to confirm.
217
+ annotations: { ...write, destructiveHint: true },
218
+ run: async (client, args) => {
219
+ await client.action(api.ingestionApi.deleteDocument, {
220
+ apiKey: requireApiKey(),
221
+ documentId: args.documentId,
222
+ });
223
+ return { deleted: true };
156
224
  },
225
+ });
226
+ export const ingestTools = [
227
+ ingestStatus,
228
+ ingestCreateDocument,
229
+ ingestCreateSections,
230
+ ingestAttachFormula,
231
+ ingestAttachTable,
232
+ ingestAttachCrossRef,
233
+ ingestUploadFigure,
234
+ ingestNormalizeCrossRefs,
235
+ ingestPublish,
236
+ ingestDelete,
157
237
  ];
158
- export function isIngestTool(name) {
159
- return ingestTools.some((t) => t.name === name);
160
- }
161
- export async function handleIngestTool(client, name, args) {
162
- const apiKey = requireApiKey();
163
- switch (name) {
164
- case 'ingest_status':
165
- return await client.action(api.ingestionApi.ingestStatus, {
166
- apiKey,
167
- code: args.code,
168
- });
169
- case 'ingest_create_document':
170
- return {
171
- documentId: await client.action(api.ingestionApi.createDocument, {
172
- apiKey,
173
- code: args.code,
174
- year: args.year,
175
- title: args.title,
176
- language: args.language,
177
- totalPages: args.totalPages,
178
- }),
179
- };
180
- case 'ingest_create_sections':
181
- return {
182
- sections: await client.action(api.ingestionApi.createSections, {
183
- apiKey,
184
- documentId: args.documentId,
185
- sections: args.sections,
186
- }),
187
- };
188
- case 'ingest_attach_formula':
189
- return {
190
- formulaId: await client.action(api.ingestionApi.attachFormula, {
191
- apiKey,
192
- sectionId: args.sectionId,
193
- latex: args.latex,
194
- description: args.description,
195
- formulaNumber: args.formulaNumber,
196
- }),
197
- };
198
- case 'ingest_attach_table':
199
- return {
200
- tableId: await client.action(api.ingestionApi.attachTable, {
201
- apiKey,
202
- sectionId: args.sectionId,
203
- data: args.data,
204
- caption: args.caption,
205
- tableNumber: args.tableNumber,
206
- }),
207
- };
208
- case 'ingest_attach_cross_ref':
209
- return {
210
- crossRefId: await client.action(api.ingestionApi.attachCrossRef, {
211
- apiKey,
212
- sourceSectionId: args.sourceSectionId,
213
- targetDocumentCode: args.targetDocumentCode,
214
- targetSectionPath: args.targetSectionPath,
215
- refText: args.refText,
216
- refType: args.refType,
217
- }),
218
- };
219
- case 'ingest_upload_figure':
220
- return {
221
- figureId: await client.action(api.ingestionApi.uploadFigure, {
222
- apiKey,
223
- sectionId: args.sectionId,
224
- base64: args.base64,
225
- mimeType: args.mimeType,
226
- caption: args.caption,
227
- figureNumber: args.figureNumber,
228
- }),
229
- };
230
- case 'ingest_normalize_cross_refs':
231
- return await client.action(api.ingestionApi.normalizeCrossRefs, {
232
- apiKey,
233
- documentId: args.documentId,
234
- });
235
- case 'ingest_publish':
236
- await client.action(api.ingestionApi.publishDocument, {
237
- apiKey,
238
- documentId: args.documentId,
239
- });
240
- return { published: true };
241
- case 'ingest_delete':
242
- await client.action(api.ingestionApi.deleteDocument, {
243
- apiKey,
244
- documentId: args.documentId,
245
- });
246
- return { deleted: true };
247
- default:
248
- return { error: `Unknown ingest tool: ${name}` };
249
- }
250
- }
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ export declare const getMethodology: import("./define.js").ToolDef<{
3
+ norm: z.ZodOptional<z.ZodString>;
4
+ }>;
5
+ export declare const listNorms: import("./define.js").ToolDef<{}>;
6
+ export declare const getToc: import("./define.js").ToolDef<{
7
+ norm: z.ZodString;
8
+ maxDepth: z.ZodOptional<z.ZodNumber>;
9
+ }>;
10
+ export declare const getSubtree: import("./define.js").ToolDef<{
11
+ norm: z.ZodString;
12
+ path: z.ZodString;
13
+ maxDepth: z.ZodOptional<z.ZodNumber>;
14
+ }>;
15
+ export declare const getSection: import("./define.js").ToolDef<{
16
+ norm: z.ZodString;
17
+ path: z.ZodString;
18
+ }>;
19
+ export declare const searchInNorm: import("./define.js").ToolDef<{
20
+ norm: z.ZodString;
21
+ keyword: z.ZodString;
22
+ limit: z.ZodOptional<z.ZodNumber>;
23
+ }>;
24
+ export declare const getCrossRefs: import("./define.js").ToolDef<{
25
+ norm: z.ZodString;
26
+ path: z.ZodString;
27
+ }>;
28
+ export declare const getFigure: import("./define.js").ToolDef<{
29
+ norm: z.ZodString;
30
+ figureId: z.ZodString;
31
+ }>;
32
+ export declare const readTools: (import("./define.js").ToolDef<{
33
+ norm: z.ZodOptional<z.ZodString>;
34
+ }> | import("./define.js").ToolDef<{}> | import("./define.js").ToolDef<{
35
+ norm: z.ZodString;
36
+ maxDepth: z.ZodOptional<z.ZodNumber>;
37
+ }> | import("./define.js").ToolDef<{
38
+ norm: z.ZodString;
39
+ path: z.ZodString;
40
+ maxDepth: z.ZodOptional<z.ZodNumber>;
41
+ }> | import("./define.js").ToolDef<{
42
+ norm: z.ZodString;
43
+ path: z.ZodString;
44
+ }> | import("./define.js").ToolDef<{
45
+ norm: z.ZodString;
46
+ keyword: z.ZodString;
47
+ limit: z.ZodOptional<z.ZodNumber>;
48
+ }> | import("./define.js").ToolDef<{
49
+ norm: z.ZodString;
50
+ figureId: z.ZodString;
51
+ }>)[];