@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.
- package/README.md +63 -13
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +55 -0
- package/dist/index.js +69 -119
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +4 -0
- package/dist/tools/ingest.d.ts +140 -243
- package/dist/tools/ingest.js +225 -238
- package/dist/tools/read.d.ts +51 -0
- package/dist/tools/read.js +247 -0
- package/package.json +9 -9
- package/skills/ingest-norm/SKILL.md +140 -128
- package/dist/tools/get-cross-refs.d.ts +0 -24
- package/dist/tools/get-cross-refs.js +0 -23
- package/dist/tools/get-figure.d.ts +0 -24
- package/dist/tools/get-figure.js +0 -50
- package/dist/tools/get-methodology.d.ts +0 -18
- package/dist/tools/get-methodology.js +0 -21
- package/dist/tools/get-section.d.ts +0 -24
- package/dist/tools/get-section.js +0 -34
- package/dist/tools/get-subtree.d.ts +0 -29
- package/dist/tools/get-subtree.js +0 -39
- package/dist/tools/get-toc.d.ts +0 -24
- package/dist/tools/get-toc.js +0 -32
- package/dist/tools/list-norms.d.ts +0 -11
- package/dist/tools/list-norms.js +0 -17
- package/dist/tools/search-in-norm.d.ts +0 -29
- package/dist/tools/search-in-norm.js +0 -31
package/README.md
CHANGED
|
@@ -87,23 +87,52 @@ settings come from environment variables — see [`.env.example`](./.env.example
|
|
|
87
87
|
|
|
88
88
|
## Tools exposed
|
|
89
89
|
|
|
90
|
+
**Read** (8 tools — query norms in your workspace):
|
|
91
|
+
|
|
90
92
|
| Tool | Purpose |
|
|
91
93
|
|---|---|
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
94
|
+
| `get_methodology` | Behavioural contract: persona, workflow, meta-routing hints, answer rules. Call first. |
|
|
95
|
+
| `list_norms` | List all norms published in your workspace (code, year, title, language). |
|
|
96
|
+
| `get_toc` | Hierarchical TOC for a norm (default `maxDepth=1` = chapters). |
|
|
97
|
+
| `get_subtree` | Drill into a chapter/section subtree (`path` + `maxDepth`). |
|
|
98
|
+
| `get_section` | Full enriched content of a section (formulas, tables, figures, cross-refs). |
|
|
99
|
+
| `search_in_norm` | Keyword search inside a norm. |
|
|
100
|
+
| `get_figure` | Retrieve a figure inline (base64 ImageContent) + public URL. |
|
|
101
|
+
| `get_cross_refs` | Outgoing cross-refs from a section to other norms. |
|
|
102
|
+
|
|
103
|
+
**Ingest** (10 tools — add YOUR licensed norms; driven by the bundled `ingest-norm` skill):
|
|
104
|
+
|
|
105
|
+
| Tool | Purpose |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `ingest_status` | Check if a norm already exists in your workspace. |
|
|
108
|
+
| `ingest_create_document` | Create a draft norm document. |
|
|
109
|
+
| `ingest_create_sections` | Bulk-insert sections (returns `nodeId → sectionId` map). |
|
|
110
|
+
| `ingest_attach_formula` | Attach a LaTeX formula to a section. |
|
|
111
|
+
| `ingest_attach_table` | Attach a structured table `{headers, rows}` to a section. |
|
|
112
|
+
| `ingest_attach_cross_ref` | Attach an explicit cross-ref to another norm. |
|
|
113
|
+
| `ingest_upload_figure` | Upload a figure (base64 PNG/JPEG/WebP, ≤ 8 MB) to a section. |
|
|
114
|
+
| `ingest_normalize_cross_refs` | Auto-detect and rebuild cross-refs from section content. |
|
|
115
|
+
| `ingest_publish` | Flip a draft to published — visible via the read tools. |
|
|
116
|
+
| `ingest_delete` | Delete a document and all its children. |
|
|
98
117
|
|
|
99
118
|
## How agents should use it
|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
For querying:
|
|
121
|
+
|
|
122
|
+
1. Call `get_methodology` first — load the behavioural contract.
|
|
123
|
+
2. Call `list_norms` to see what's available in your workspace.
|
|
124
|
+
3. Call `get_toc(norm)` to navigate the structure; `get_subtree` to drill in.
|
|
125
|
+
4. Call `get_section(norm, path)` to read specific content.
|
|
126
|
+
5. Use `search_in_norm` when the section path is unknown.
|
|
127
|
+
6. Follow `crossRefs` for compound questions (e.g. SIA 261 → SIA 263 → EC).
|
|
128
|
+
7. Call `get_figure` when the section references a figure relevant to the answer.
|
|
129
|
+
|
|
130
|
+
For ingesting your own licensed norms, see the bundled **`ingest-norm` skill** —
|
|
131
|
+
a 2-phase hybrid pipeline (since 0.4.0): a Python pre-pass
|
|
132
|
+
(`scripts/ingest-prepass.py`, PyMuPDF) extracts the hierarchical tree and
|
|
133
|
+
rasterizes figures deterministically, then the agent enriches sections with
|
|
134
|
+
summaries, LaTeX formulas, tables and cross-refs via targeted visual reading.
|
|
135
|
+
Requires Python ≥ 3.10 with PyMuPDF (`python -m pip install --user pymupdf`).
|
|
107
136
|
|
|
108
137
|
## Troubleshooting
|
|
109
138
|
|
|
@@ -128,8 +157,29 @@ settings come from environment variables — see [`.env.example`](./.env.example
|
|
|
128
157
|
|
|
129
158
|
- Each user can create up to 50 active keys and 20 new keys per 24h. Revoke unused keys in the dashboard.
|
|
130
159
|
|
|
160
|
+
### `QUOTA_EXCEEDED`
|
|
161
|
+
|
|
162
|
+
Your organization reached one of its limits. The error names the dimension, your
|
|
163
|
+
current count and the plan limit. Retrying will fail identically.
|
|
164
|
+
|
|
165
|
+
| Limit | Free | Pro | Max |
|
|
166
|
+
|---|---|---|---|
|
|
167
|
+
| Norms | 1 | 15 | 60 |
|
|
168
|
+
| Sections | 500 | 5,000 | 21,000 |
|
|
169
|
+
| Figures | 60 | 750 | 3,000 |
|
|
170
|
+
| Queries / month | 500 | 15,000 | 100,000 |
|
|
171
|
+
| Members | 1 | 1 | 5 |
|
|
172
|
+
|
|
173
|
+
Beyond Max, an Enterprise plan scales to 100 members, 300 norms and a million
|
|
174
|
+
monthly queries; the calculator is at https://stratta.ch/tarifs
|
|
175
|
+
|
|
176
|
+
Stock limits free up when you delete a norm (`ingest_delete`). The monthly query
|
|
177
|
+
counter resets on its own. Gauges live on the Workspace page of your dashboard,
|
|
178
|
+
and an org admin can also set caps below the plan. Full details:
|
|
179
|
+
https://stratta.ch/docs/en/account/plans
|
|
180
|
+
|
|
131
181
|
## License
|
|
132
182
|
|
|
133
|
-
Proprietary — ©
|
|
183
|
+
Proprietary — © Stratta, Lausanne. All rights reserved. This package is the
|
|
134
184
|
official Stratta MCP client; redistribution, modification, or reuse of the source
|
|
135
185
|
is not permitted without prior written consent.
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a server error into something an agent can act on. A quota rejection
|
|
3
|
+
* is not a bug: the agent should stop retrying and tell the user which limit
|
|
4
|
+
* was hit, so it gets the numbers instead of a stack trace.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatToolError(err: unknown): string;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ConvexError } from 'convex/values';
|
|
2
|
+
function isQuotaPayload(data) {
|
|
3
|
+
return (typeof data === 'object' &&
|
|
4
|
+
data !== null &&
|
|
5
|
+
data.code === 'QUOTA_EXCEEDED');
|
|
6
|
+
}
|
|
7
|
+
function humanDuration(ms) {
|
|
8
|
+
const days = Math.floor(ms / 86_400_000);
|
|
9
|
+
if (days >= 1)
|
|
10
|
+
return `${days} jour${days > 1 ? 's' : ''}`;
|
|
11
|
+
const hours = Math.ceil(ms / 3_600_000);
|
|
12
|
+
return `${hours} heure${hours > 1 ? 's' : ''}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Turns a server error into something an agent can act on. A quota rejection
|
|
16
|
+
* is not a bug: the agent should stop retrying and tell the user which limit
|
|
17
|
+
* was hit, so it gets the numbers instead of a stack trace.
|
|
18
|
+
*/
|
|
19
|
+
export function formatToolError(err) {
|
|
20
|
+
if (err instanceof ConvexError) {
|
|
21
|
+
const data = err.data;
|
|
22
|
+
if (isQuotaPayload(data)) {
|
|
23
|
+
const lines = [
|
|
24
|
+
data.message,
|
|
25
|
+
`Dimension : ${data.label} (${data.current}/${data.limit}, demandé : ${data.requested}).`,
|
|
26
|
+
`Plan : ${data.plan}.`,
|
|
27
|
+
];
|
|
28
|
+
if (data.retryAfter) {
|
|
29
|
+
lines.push(`Réinitialisation dans ${humanDuration(data.retryAfter)}.`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('Ne relancez pas la même opération : elle échouera à l’identique tant que la limite est atteinte.');
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
if (typeof data === 'string') {
|
|
35
|
+
switch (data) {
|
|
36
|
+
case 'UNAUTHORIZED':
|
|
37
|
+
return 'Clé API invalide ou révoquée. Relancez `npx @stratta/mcp login` pour en enregistrer une autre.';
|
|
38
|
+
case 'NO_ORGANIZATION':
|
|
39
|
+
return "Cette clé n'est rattachée à aucune organisation. Ouvrez stratta.ch et recréez une clé depuis votre espace.";
|
|
40
|
+
case 'DOCUMENT_NOT_FOUND':
|
|
41
|
+
return "Cette norme n'existe pas dans votre organisation, ou elle appartient à une autre.";
|
|
42
|
+
case 'SECTION_NOT_FOUND':
|
|
43
|
+
return "Cette section n'existe pas dans votre organisation.";
|
|
44
|
+
case 'INSUFFICIENT_ROLE':
|
|
45
|
+
return "Cette action modifie le corpus de normes et demande un rôle propriétaire ou administrateur de l'organisation. Demandez à un administrateur de votre espace de l'exécuter (ou de vous accorder ce rôle).";
|
|
46
|
+
case 'RATE_LIMITED':
|
|
47
|
+
return 'Trop de tentatives en peu de temps. Patientez une minute avant de réessayer.';
|
|
48
|
+
default:
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(data);
|
|
53
|
+
}
|
|
54
|
+
return err instanceof Error ? err.message : String(err);
|
|
55
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,155 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
7
|
import { createConvexClient, api } from './client.js';
|
|
8
|
+
import { formatToolError } from './errors.js';
|
|
6
9
|
import { ensureAuthenticated, resolveApiKey } from './auth.js';
|
|
7
10
|
import { elicitApiKey } from './elicit.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
import { readTools } from './tools/read.js';
|
|
12
|
+
import { ingestTools } from './tools/ingest.js';
|
|
13
|
+
/**
|
|
14
|
+
* Read from package.json rather than repeated in a literal. The previous
|
|
15
|
+
* version announced `0.3.0` while the published package was `0.6.0`, so every
|
|
16
|
+
* client's logs and every bug report carried the wrong number.
|
|
17
|
+
*/
|
|
18
|
+
function packageVersion() {
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
for (const candidate of ['../package.json', '../../package.json']) {
|
|
21
|
+
try {
|
|
22
|
+
const raw = readFileSync(join(here, candidate), 'utf8');
|
|
23
|
+
const version = JSON.parse(raw).version;
|
|
24
|
+
if (version)
|
|
25
|
+
return version;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Try the next candidate: the file sits one level up from `dist/`, but
|
|
29
|
+
// two when running from source.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return '0.0.0';
|
|
33
|
+
}
|
|
34
|
+
const server = new McpServer({ name: 'stratta-mcp', version: packageVersion() }, { capabilities: { tools: {} } });
|
|
29
35
|
const client = createConvexClient();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Everything every tool call needs, in one place: resolve the key (env, then
|
|
38
|
+
* stored config, then prompt the user), validate it, run the tool, report
|
|
39
|
+
* usage, and turn a thrown error into a message the agent can act on.
|
|
40
|
+
*
|
|
41
|
+
* Usage is reported with the plaintext key as proof of possession — the server
|
|
42
|
+
* derives the identity from it, so a call cannot be attributed to somebody
|
|
43
|
+
* else's key.
|
|
44
|
+
*/
|
|
45
|
+
async function call(def, args) {
|
|
35
46
|
const startedAt = Date.now();
|
|
36
|
-
let apiKeyId = null;
|
|
37
47
|
let resolvedKey = null;
|
|
48
|
+
let authenticated = false;
|
|
38
49
|
let status = 'success';
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
// possession) — the server derives the identity, so usage can't be forged
|
|
42
|
-
// for another user's key.
|
|
43
|
-
if (!apiKeyId || !resolvedKey)
|
|
50
|
+
const reportUsage = () => {
|
|
51
|
+
if (!authenticated || !resolvedKey)
|
|
44
52
|
return;
|
|
45
53
|
void client
|
|
46
54
|
.action(api.apiKeys.recordToolUsage, {
|
|
47
55
|
apiKey: resolvedKey,
|
|
48
|
-
toolName: name,
|
|
56
|
+
toolName: def.name,
|
|
49
57
|
status,
|
|
50
58
|
durationMs: Date.now() - startedAt,
|
|
51
59
|
})
|
|
52
|
-
.catch((e) => console.error('[stratta-mcp]
|
|
60
|
+
.catch((e) => console.error('[stratta-mcp] usage report failed:', e));
|
|
53
61
|
};
|
|
54
62
|
try {
|
|
55
|
-
resolvedKey = resolveApiKey();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (isIngestTool(name)) {
|
|
63
|
-
result = await handleIngestTool(client, name, (args ?? {}));
|
|
64
|
-
recordUsage();
|
|
65
|
-
return {
|
|
66
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
switch (name) {
|
|
70
|
-
case 'get_methodology':
|
|
71
|
-
result = await handleGetMethodology(client, args);
|
|
72
|
-
break;
|
|
73
|
-
case 'list_norms':
|
|
74
|
-
result = await handleListNorms(client);
|
|
75
|
-
break;
|
|
76
|
-
case 'get_toc':
|
|
77
|
-
result = await handleGetToc(client, args);
|
|
78
|
-
break;
|
|
79
|
-
case 'get_subtree':
|
|
80
|
-
result = await handleGetSubtree(client, args);
|
|
81
|
-
break;
|
|
82
|
-
case 'get_section':
|
|
83
|
-
result = await handleGetSection(client, args);
|
|
84
|
-
break;
|
|
85
|
-
case 'search_in_norm':
|
|
86
|
-
result = await handleSearchInNorm(client, args);
|
|
87
|
-
break;
|
|
88
|
-
case 'get_figure':
|
|
89
|
-
result = await handleGetFigure(client, args);
|
|
90
|
-
break;
|
|
91
|
-
case 'get_cross_refs':
|
|
92
|
-
result = await handleGetCrossRefs(client, args);
|
|
93
|
-
break;
|
|
94
|
-
default:
|
|
95
|
-
return {
|
|
96
|
-
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
97
|
-
isError: true,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
// Special handling for get_figure: return ImageContent inline
|
|
101
|
-
if (name === 'get_figure' &&
|
|
102
|
-
typeof result === 'object' &&
|
|
103
|
-
result !== null &&
|
|
104
|
-
'image' in result) {
|
|
105
|
-
const r = result;
|
|
106
|
-
recordUsage();
|
|
107
|
-
const lines = [`${r.figureNumber} — ${r.caption}`];
|
|
108
|
-
if (r.url) {
|
|
109
|
-
lines.push(`Image URL: ${r.url}`);
|
|
110
|
-
lines.push(`To show this figure to the user, embed it inline in your reply as Markdown: `);
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
content: [
|
|
114
|
-
{
|
|
115
|
-
type: 'image',
|
|
116
|
-
data: r.image.base64,
|
|
117
|
-
mimeType: r.image.mimeType,
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
type: 'text',
|
|
121
|
-
text: lines.join('\n'),
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
recordUsage();
|
|
63
|
+
resolvedKey = resolveApiKey() ?? (await elicitApiKey(server.server, client));
|
|
64
|
+
await ensureAuthenticated(client, resolvedKey);
|
|
65
|
+
authenticated = true;
|
|
66
|
+
const result = await def.run(client, args);
|
|
67
|
+
reportUsage();
|
|
68
|
+
if (def.toContent)
|
|
69
|
+
return def.toContent(result);
|
|
127
70
|
return {
|
|
128
|
-
content: [
|
|
129
|
-
{ type: 'text', text: JSON.stringify(result, null, 2) },
|
|
130
|
-
],
|
|
71
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
131
72
|
};
|
|
132
73
|
}
|
|
133
74
|
catch (err) {
|
|
134
75
|
status = 'error';
|
|
135
|
-
|
|
136
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
reportUsage();
|
|
137
77
|
return {
|
|
138
|
-
content: [{ type: 'text', text: `Error: ${
|
|
78
|
+
content: [{ type: 'text', text: `Error: ${formatToolError(err)}` }],
|
|
139
79
|
isError: true,
|
|
140
80
|
};
|
|
141
81
|
}
|
|
142
|
-
}
|
|
82
|
+
}
|
|
83
|
+
for (const def of [...readTools, ...ingestTools]) {
|
|
84
|
+
server.registerTool(def.name, {
|
|
85
|
+
title: def.title,
|
|
86
|
+
description: def.description,
|
|
87
|
+
inputSchema: def.inputSchema,
|
|
88
|
+
annotations: def.annotations,
|
|
89
|
+
},
|
|
90
|
+
// The SDK validates `args` against `inputSchema` before this runs, so the
|
|
91
|
+
// handler receives values that already match the declared shape.
|
|
92
|
+
(args) => call(def, args));
|
|
93
|
+
}
|
|
143
94
|
async function main() {
|
|
144
95
|
if (process.argv[2] === 'login') {
|
|
145
96
|
const { runLogin } = await import('./login.js');
|
|
146
97
|
await runLogin();
|
|
147
98
|
return;
|
|
148
99
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.error('[stratta-mcp] started, listening on stdio');
|
|
100
|
+
await server.connect(new StdioServerTransport());
|
|
101
|
+
// Stderr only; stdout carries the MCP protocol.
|
|
102
|
+
console.error(`[stratta-mcp] v${packageVersion()} listening on stdio`);
|
|
153
103
|
}
|
|
154
104
|
main().catch((err) => {
|
|
155
105
|
console.error('[stratta-mcp] fatal:', err);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import type { z, ZodRawShape } from 'zod';
|
|
4
|
+
/**
|
|
5
|
+
* A tool, described once.
|
|
6
|
+
*
|
|
7
|
+
* The previous shape kept three things apart that always change together: a
|
|
8
|
+
* hand-written JSON Schema literal, a `handleX` function that re-declared the
|
|
9
|
+
* same argument types with `as` casts, and a `case 'x':` in a switch that
|
|
10
|
+
* wired them up. Nothing checked that the three agreed — and they did not:
|
|
11
|
+
* `args.year as number` happily accepted the string `"2020"` and failed later,
|
|
12
|
+
* inside Convex, with an error the agent could not act on.
|
|
13
|
+
*
|
|
14
|
+
* Here the Zod shape IS the schema (the SDK converts it to JSON Schema for the
|
|
15
|
+
* client), IS the runtime validation (the SDK rejects bad arguments before the
|
|
16
|
+
* handler runs), and IS the argument type (`z.infer`). One source, three uses.
|
|
17
|
+
*/
|
|
18
|
+
export type ToolDef<S extends ZodRawShape = ZodRawShape> = {
|
|
19
|
+
name: string;
|
|
20
|
+
/** Human-facing label shown by clients that display one. */
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
inputSchema: S;
|
|
24
|
+
/**
|
|
25
|
+
* Behaviour hints for the client. `readOnlyHint` lets an agent know a call
|
|
26
|
+
* is safe to retry or run speculatively; `destructiveHint` is what makes a
|
|
27
|
+
* client ask before running `ingest_delete`. Absent before — every tool
|
|
28
|
+
* looked equally dangerous, which means none did.
|
|
29
|
+
*/
|
|
30
|
+
annotations?: {
|
|
31
|
+
readOnlyHint?: boolean;
|
|
32
|
+
destructiveHint?: boolean;
|
|
33
|
+
idempotentHint?: boolean;
|
|
34
|
+
openWorldHint?: boolean;
|
|
35
|
+
};
|
|
36
|
+
run: (client: ConvexHttpClient, args: z.infer<z.ZodObject<S>>) => Promise<unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* Optional override for turning the result into MCP content. Only
|
|
39
|
+
* `get_figure` needs it, to return the image as an `ImageContent` block
|
|
40
|
+
* rather than as JSON.
|
|
41
|
+
*/
|
|
42
|
+
toContent?: (result: unknown) => CallToolResult;
|
|
43
|
+
};
|
|
44
|
+
/** Identity helper that keeps the generic bound to the literal shape. */
|
|
45
|
+
export declare function defineTool<S extends ZodRawShape>(def: ToolDef<S>): ToolDef<S>;
|