@stratta/mcp 0.1.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 +113 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +42 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +142 -0
- package/dist/tools/get-cross-refs.d.ts +24 -0
- package/dist/tools/get-cross-refs.js +24 -0
- package/dist/tools/get-figure.d.ts +24 -0
- package/dist/tools/get-figure.js +50 -0
- package/dist/tools/get-methodology.d.ts +18 -0
- package/dist/tools/get-methodology.js +23 -0
- package/dist/tools/get-section.d.ts +24 -0
- package/dist/tools/get-section.js +35 -0
- package/dist/tools/get-subtree.d.ts +29 -0
- package/dist/tools/get-subtree.js +40 -0
- package/dist/tools/get-toc.d.ts +24 -0
- package/dist/tools/get-toc.js +33 -0
- package/dist/tools/ingest.d.ts +243 -0
- package/dist/tools/ingest.js +251 -0
- package/dist/tools/list-norms.d.ts +11 -0
- package/dist/tools/list-norms.js +18 -0
- package/dist/tools/search-in-norm.d.ts +29 -0
- package/dist/tools/search-in-norm.js +32 -0
- package/package.json +58 -0
- package/skills/ingest-norm/SKILL.md +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @stratta/mcp
|
|
2
|
+
|
|
3
|
+
MCP server exposing Swiss engineering norms (SIA / Eurocodes) to Claude clients via Stratta TreeRAG.
|
|
4
|
+
|
|
5
|
+
> Requires a free Stratta account. Sign up at https://stratta.ch and generate an API key at https://stratta.ch/api-keys.
|
|
6
|
+
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> Your `STRATTA_API_KEY` is a **secret** — it grants read/write access to your
|
|
9
|
+
> Stratta workspace. Never commit it to a repository, paste it into a shared/
|
|
10
|
+
> project-scoped MCP config, or share it in logs. Prefer a user-scoped config or
|
|
11
|
+
> a shell environment variable. If a key leaks, revoke it immediately at
|
|
12
|
+
> https://stratta.ch/api-keys.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
### Claude Code (recommended)
|
|
17
|
+
|
|
18
|
+
Export your key once, then register the server at **user** scope (so the key
|
|
19
|
+
never lands in a committable project `.mcp.json`):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export STRATTA_API_KEY=sk_strt_xxx # or set it in your shell profile
|
|
23
|
+
claude mcp add stratta --scope user --command "npx -y @stratta/mcp" --env STRATTA_API_KEY="$STRATTA_API_KEY"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Claude Desktop
|
|
27
|
+
|
|
28
|
+
Edit your `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"stratta": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["-y", "@stratta/mcp"],
|
|
36
|
+
"env": {
|
|
37
|
+
"STRATTA_API_KEY": "sk_strt_xxx"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Restart Claude Desktop. The Stratta tools should appear in the MCP indicator.
|
|
45
|
+
|
|
46
|
+
> `claude_desktop_config.json` stores the key in plaintext on your machine —
|
|
47
|
+
> keep the file private and out of any synced/backed-up repo.
|
|
48
|
+
|
|
49
|
+
### Global install (optional)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install -g @stratta/mcp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then use `"command": "stratta-mcp"` instead of `npx`.
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
The server reads its config from environment variables. See [`.env.example`](./.env.example) for a template.
|
|
60
|
+
|
|
61
|
+
| Variable | Required | Default | Purpose |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `STRATTA_API_KEY` | yes | – | API key from https://stratta.ch/api-keys. |
|
|
64
|
+
| `STRATTA_CONVEX_URL` | no | Stratta prod backend | Override only if you self-host. |
|
|
65
|
+
|
|
66
|
+
## Tools exposed
|
|
67
|
+
|
|
68
|
+
| Tool | Purpose |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `list_norms` | List all available norms (code, year, title, language). |
|
|
71
|
+
| `get_toc` | Get the hierarchical table of contents for a norm. |
|
|
72
|
+
| `get_section` | Fetch the full enriched content of a section (with formulas, tables, cross-refs). |
|
|
73
|
+
| `search_in_norm` | Search sections within a norm by keyword. |
|
|
74
|
+
| `get_figure` | Retrieve a figure (image) inline (base64) to reason about diagrams. |
|
|
75
|
+
| `get_cross_refs` | List outgoing cross-references from a section to other norms. |
|
|
76
|
+
|
|
77
|
+
## How agents should use it
|
|
78
|
+
|
|
79
|
+
1. Call `list_norms` to see what's available.
|
|
80
|
+
2. Call `get_toc(norm)` to navigate the structure.
|
|
81
|
+
3. Call `get_section(norm, path)` to read specific content.
|
|
82
|
+
4. Use `search_in_norm` when the section path is unknown.
|
|
83
|
+
5. Follow `crossRefs` for compound questions (e.g. SIA 261 → SIA 263 → EC).
|
|
84
|
+
6. Call `get_figure` when the section references a figure relevant to the answer.
|
|
85
|
+
|
|
86
|
+
## Troubleshooting
|
|
87
|
+
|
|
88
|
+
### `Authentication failed` / `Invalid API key`
|
|
89
|
+
|
|
90
|
+
- Verify the key starts with `sk_strt_` and is not revoked at https://stratta.ch/api-keys.
|
|
91
|
+
- Check the env var is reaching the process: `echo $STRATTA_API_KEY` (or `$env:STRATTA_API_KEY` on Windows PowerShell).
|
|
92
|
+
- If you copied from the UI, make sure no leading/trailing whitespace was added.
|
|
93
|
+
|
|
94
|
+
### `ECONNREFUSED` / network errors
|
|
95
|
+
|
|
96
|
+
- Confirm outbound HTTPS to `*.convex.cloud` is allowed by your firewall/VPN.
|
|
97
|
+
- Try `curl -I https://stratta.ch` to verify general internet reachability.
|
|
98
|
+
|
|
99
|
+
### Tools don't appear in Claude
|
|
100
|
+
|
|
101
|
+
- Restart your Claude client after editing the config.
|
|
102
|
+
- Check the MCP server logs (Claude Code: `claude mcp logs stratta`; Claude Desktop: `~/Library/Logs/Claude/mcp-server-stratta.log` on macOS).
|
|
103
|
+
- Make sure your Node.js version is `>=20` (`node --version`).
|
|
104
|
+
|
|
105
|
+
### Rate-limited
|
|
106
|
+
|
|
107
|
+
- Each user can create up to 50 active keys and 20 new keys per 24h. Revoke unused keys in the dashboard.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
Proprietary — © SmartFlow (Hugo Gebel). All rights reserved. This package is the
|
|
112
|
+
official Stratta MCP client; redistribution, modification, or reuse of the source
|
|
113
|
+
is not permitted without prior written consent.
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { api } from './client.js';
|
|
2
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1h
|
|
3
|
+
let state = { kind: 'unvalidated' };
|
|
4
|
+
export async function ensureAuthenticated(client) {
|
|
5
|
+
const plaintext = process.env.STRATTA_API_KEY;
|
|
6
|
+
if (!plaintext) {
|
|
7
|
+
throw new Error('STRATTA_API_KEY environment variable is not set. Generate a key at https://stratta.ch/api-keys');
|
|
8
|
+
}
|
|
9
|
+
if (state.kind === 'valid' &&
|
|
10
|
+
Date.now() - state.validatedAt < CACHE_TTL_MS) {
|
|
11
|
+
return {
|
|
12
|
+
userId: state.userId,
|
|
13
|
+
apiKeyId: state.apiKeyId,
|
|
14
|
+
organizationId: state.organizationId,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (state.kind === 'invalid') {
|
|
18
|
+
throw new Error(state.error);
|
|
19
|
+
}
|
|
20
|
+
const result = (await client.action(api.apiKeys.validateApiKey, {
|
|
21
|
+
plaintext,
|
|
22
|
+
}));
|
|
23
|
+
if (!result.valid || !result.userId || !result.apiKeyId) {
|
|
24
|
+
state = {
|
|
25
|
+
kind: 'invalid',
|
|
26
|
+
error: 'Invalid STRATTA_API_KEY. Generate a new one at https://stratta.ch/api-keys',
|
|
27
|
+
};
|
|
28
|
+
throw new Error(state.error);
|
|
29
|
+
}
|
|
30
|
+
state = {
|
|
31
|
+
kind: 'valid',
|
|
32
|
+
userId: result.userId,
|
|
33
|
+
apiKeyId: result.apiKeyId,
|
|
34
|
+
organizationId: result.organizationId,
|
|
35
|
+
validatedAt: Date.now(),
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
userId: result.userId,
|
|
39
|
+
apiKeyId: result.apiKeyId,
|
|
40
|
+
organizationId: result.organizationId,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
import { anyApi } from 'convex/server';
|
|
3
|
+
const DEFAULT_CONVEX_URL = 'https://blessed-salmon-237.convex.cloud';
|
|
4
|
+
export function createConvexClient() {
|
|
5
|
+
const url = process.env.STRATTA_CONVEX_URL ?? DEFAULT_CONVEX_URL;
|
|
6
|
+
return new ConvexHttpClient(url);
|
|
7
|
+
}
|
|
8
|
+
// Untyped api references — Convex resolves by string name at runtime.
|
|
9
|
+
// Trade-off: lose compile-time type safety, gain a standalone package
|
|
10
|
+
// that doesn't depend on the stratta workspace's generated types.
|
|
11
|
+
export const api = anyApi;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { createConvexClient, api } from './client.js';
|
|
6
|
+
import { ensureAuthenticated } from './auth.js';
|
|
7
|
+
import { getMethodologyTool, handleGetMethodology } from './tools/get-methodology.js';
|
|
8
|
+
import { listNormsTool, handleListNorms } from './tools/list-norms.js';
|
|
9
|
+
import { getTocTool, handleGetToc } from './tools/get-toc.js';
|
|
10
|
+
import { getSubtreeTool, handleGetSubtree } from './tools/get-subtree.js';
|
|
11
|
+
import { getSectionTool, handleGetSection } from './tools/get-section.js';
|
|
12
|
+
import { searchInNormTool, handleSearchInNorm } from './tools/search-in-norm.js';
|
|
13
|
+
import { getFigureTool, handleGetFigure } from './tools/get-figure.js';
|
|
14
|
+
import { getCrossRefsTool, handleGetCrossRefs } from './tools/get-cross-refs.js';
|
|
15
|
+
import { ingestTools, isIngestTool, handleIngestTool } from './tools/ingest.js';
|
|
16
|
+
const tools = [
|
|
17
|
+
getMethodologyTool,
|
|
18
|
+
listNormsTool,
|
|
19
|
+
getTocTool,
|
|
20
|
+
getSubtreeTool,
|
|
21
|
+
getSectionTool,
|
|
22
|
+
searchInNormTool,
|
|
23
|
+
getFigureTool,
|
|
24
|
+
getCrossRefsTool,
|
|
25
|
+
...ingestTools,
|
|
26
|
+
];
|
|
27
|
+
const server = new Server({ name: 'stratta-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
28
|
+
const client = createConvexClient();
|
|
29
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
30
|
+
tools,
|
|
31
|
+
}));
|
|
32
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
33
|
+
const { name, arguments: args } = request.params;
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
let apiKeyId = null;
|
|
36
|
+
let status = 'success';
|
|
37
|
+
const recordUsage = () => {
|
|
38
|
+
// Only record once authenticated. Send the plaintext key (proof of
|
|
39
|
+
// possession) — the server derives the identity, so usage can't be forged
|
|
40
|
+
// for another user's key.
|
|
41
|
+
const apiKey = process.env.STRATTA_API_KEY;
|
|
42
|
+
if (!apiKeyId || !apiKey)
|
|
43
|
+
return;
|
|
44
|
+
void client
|
|
45
|
+
.action(api.apiKeys.recordToolUsage, {
|
|
46
|
+
apiKey,
|
|
47
|
+
toolName: name,
|
|
48
|
+
status,
|
|
49
|
+
durationMs: Date.now() - startedAt,
|
|
50
|
+
})
|
|
51
|
+
.catch((e) => console.error('[stratta-mcp] track failed:', e));
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
const auth = await ensureAuthenticated(client);
|
|
55
|
+
apiKeyId = auth.apiKeyId;
|
|
56
|
+
let result;
|
|
57
|
+
if (isIngestTool(name)) {
|
|
58
|
+
result = await handleIngestTool(client, name, (args ?? {}));
|
|
59
|
+
recordUsage();
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
switch (name) {
|
|
65
|
+
case 'get_methodology':
|
|
66
|
+
result = await handleGetMethodology(client, args);
|
|
67
|
+
break;
|
|
68
|
+
case 'list_norms':
|
|
69
|
+
result = await handleListNorms(client);
|
|
70
|
+
break;
|
|
71
|
+
case 'get_toc':
|
|
72
|
+
result = await handleGetToc(client, args);
|
|
73
|
+
break;
|
|
74
|
+
case 'get_subtree':
|
|
75
|
+
result = await handleGetSubtree(client, args);
|
|
76
|
+
break;
|
|
77
|
+
case 'get_section':
|
|
78
|
+
result = await handleGetSection(client, args);
|
|
79
|
+
break;
|
|
80
|
+
case 'search_in_norm':
|
|
81
|
+
result = await handleSearchInNorm(client, args);
|
|
82
|
+
break;
|
|
83
|
+
case 'get_figure':
|
|
84
|
+
result = await handleGetFigure(client, args);
|
|
85
|
+
break;
|
|
86
|
+
case 'get_cross_refs':
|
|
87
|
+
result = await handleGetCrossRefs(client, args);
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Special handling for get_figure: return ImageContent inline
|
|
96
|
+
if (name === 'get_figure' &&
|
|
97
|
+
typeof result === 'object' &&
|
|
98
|
+
result !== null &&
|
|
99
|
+
'image' in result) {
|
|
100
|
+
const r = result;
|
|
101
|
+
recordUsage();
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'image',
|
|
106
|
+
data: r.image.base64,
|
|
107
|
+
mimeType: r.image.mimeType,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `${r.figureNumber} — ${r.caption}`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
recordUsage();
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{ type: 'text', text: JSON.stringify(result, null, 2) },
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
status = 'error';
|
|
125
|
+
recordUsage();
|
|
126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
129
|
+
isError: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
async function main() {
|
|
134
|
+
const transport = new StdioServerTransport();
|
|
135
|
+
await server.connect(transport);
|
|
136
|
+
// Stderr-only logging; stdout is reserved for MCP protocol.
|
|
137
|
+
console.error('[stratta-mcp] started, listening on stdio');
|
|
138
|
+
}
|
|
139
|
+
main().catch((err) => {
|
|
140
|
+
console.error('[stratta-mcp] fatal:', err);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
export declare const getCrossRefsTool: {
|
|
3
|
+
readonly name: "get_cross_refs";
|
|
4
|
+
readonly description: "List outgoing cross-references from a section to other norms (e.g. SIA 261 §4.2 → SIA 263). For compound questions you MUST follow these refs: fetch each target via get_section before concluding. Skipping cross-refs is the #1 cause of incomplete answers — SIA norms intentionally distribute the rule, the coefficient, and the action across separate norms (260 / 261 / domain).";
|
|
5
|
+
readonly inputSchema: {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly norm: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Norm code, e.g. \"SIA 261\".";
|
|
11
|
+
};
|
|
12
|
+
readonly path: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly description: "Section path, e.g. \"4.2.1\".";
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
readonly required: readonly ["norm", "path"];
|
|
18
|
+
readonly additionalProperties: false;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function handleGetCrossRefs(client: ConvexHttpClient, args: {
|
|
22
|
+
norm: string;
|
|
23
|
+
path: string;
|
|
24
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { api } from '../client.js';
|
|
2
|
+
import { ensureAuthenticated } from '../auth.js';
|
|
3
|
+
export const getCrossRefsTool = {
|
|
4
|
+
name: 'get_cross_refs',
|
|
5
|
+
description: "List outgoing cross-references from a section to other norms (e.g. SIA 261 §4.2 → SIA 263). For compound questions you MUST follow these refs: fetch each target via get_section before concluding. Skipping cross-refs is the #1 cause of incomplete answers — SIA norms intentionally distribute the rule, the coefficient, and the action across separate norms (260 / 261 / domain).",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
norm: { type: 'string', description: 'Norm code, e.g. "SIA 261".' },
|
|
10
|
+
path: { type: 'string', description: 'Section path, e.g. "4.2.1".' },
|
|
11
|
+
},
|
|
12
|
+
required: ['norm', 'path'],
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export async function handleGetCrossRefs(client, args) {
|
|
17
|
+
await ensureAuthenticated(client);
|
|
18
|
+
const result = await client.action(api._mcp.getCrossRefs, {
|
|
19
|
+
apiKey: process.env.STRATTA_API_KEY,
|
|
20
|
+
code: args.norm,
|
|
21
|
+
path: args.path,
|
|
22
|
+
});
|
|
23
|
+
return { norm: args.norm, path: args.path, crossRefs: result };
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
export declare const getFigureTool: {
|
|
3
|
+
readonly name: "get_figure";
|
|
4
|
+
readonly description: "Retrieve a figure (image) referenced in a section. Returns the image inline (base64) so you can see and reason about diagrams, charts, and technical drawings. Use the `id` returned by get_section in its `figures` array.";
|
|
5
|
+
readonly inputSchema: {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly norm: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Norm code, e.g. \"SIA 261\".";
|
|
11
|
+
};
|
|
12
|
+
readonly figureId: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly description: "Figure ID from get_section response (figures[].id).";
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
readonly required: readonly ["norm", "figureId"];
|
|
18
|
+
readonly additionalProperties: false;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function handleGetFigure(client: ConvexHttpClient, args: {
|
|
22
|
+
norm: string;
|
|
23
|
+
figureId: string;
|
|
24
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { api } from '../client.js';
|
|
2
|
+
import { ensureAuthenticated } from '../auth.js';
|
|
3
|
+
export const getFigureTool = {
|
|
4
|
+
name: 'get_figure',
|
|
5
|
+
description: 'Retrieve a figure (image) referenced in a section. Returns the image inline (base64) so you can see and reason about diagrams, charts, and technical drawings. Use the `id` returned by get_section in its `figures` array.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
norm: { type: 'string', description: 'Norm code, e.g. "SIA 261".' },
|
|
10
|
+
figureId: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Figure ID from get_section response (figures[].id).',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ['norm', 'figureId'],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export async function handleGetFigure(client, args) {
|
|
20
|
+
await ensureAuthenticated(client);
|
|
21
|
+
const apiKey = process.env.STRATTA_API_KEY;
|
|
22
|
+
const meta = (await client.action(api._mcp.getFigureMeta, {
|
|
23
|
+
apiKey,
|
|
24
|
+
code: args.norm,
|
|
25
|
+
figureId: args.figureId,
|
|
26
|
+
}));
|
|
27
|
+
if (!meta) {
|
|
28
|
+
return { error: `Figure not found.` };
|
|
29
|
+
}
|
|
30
|
+
const url = (await client.action(api._mcp.getStorageUrl, {
|
|
31
|
+
apiKey,
|
|
32
|
+
code: args.norm,
|
|
33
|
+
figureId: args.figureId,
|
|
34
|
+
}));
|
|
35
|
+
if (!url) {
|
|
36
|
+
return { error: 'Figure storage is unavailable.' };
|
|
37
|
+
}
|
|
38
|
+
const response = await fetch(url);
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
return { error: `Failed to download figure: HTTP ${response.status}` };
|
|
41
|
+
}
|
|
42
|
+
const buffer = await response.arrayBuffer();
|
|
43
|
+
const base64 = Buffer.from(buffer).toString('base64');
|
|
44
|
+
const mimeType = response.headers.get('content-type') ?? 'image/png';
|
|
45
|
+
return {
|
|
46
|
+
caption: meta.caption,
|
|
47
|
+
figureNumber: meta.figureNumber,
|
|
48
|
+
image: { base64, mimeType },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
export declare const getMethodologyTool: {
|
|
3
|
+
readonly name: "get_methodology";
|
|
4
|
+
readonly description: "MANDATORY FIRST CALL when answering any technical question about Swiss civil engineering norms via Stratta. Returns the canonical persona, navigation workflow, meta-routing hints (which SIA norms cover which topics), tree-navigation rules (where to look for formulas vs coefficients vs definitions), citation format and answer rules. Adopt these rules verbatim for the rest of the consultation. If you skip this call you WILL produce lower-quality answers (wrong citation format, missing cross-norm dependencies, hallucinated values).";
|
|
5
|
+
readonly inputSchema: {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly norm: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Optional: norm code the user is asking about, if already known. Used to scope methodology hints (currently informational only).";
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
readonly additionalProperties: false;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare function handleGetMethodology(client: ConvexHttpClient, args: {
|
|
17
|
+
norm?: string;
|
|
18
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { api } from '../client.js';
|
|
2
|
+
import { ensureAuthenticated } from '../auth.js';
|
|
3
|
+
export const getMethodologyTool = {
|
|
4
|
+
name: 'get_methodology',
|
|
5
|
+
description: "MANDATORY FIRST CALL when answering any technical question about Swiss civil engineering norms via Stratta. Returns the canonical persona, navigation workflow, meta-routing hints (which SIA norms cover which topics), tree-navigation rules (where to look for formulas vs coefficients vs definitions), citation format and answer rules. Adopt these rules verbatim for the rest of the consultation. If you skip this call you WILL produce lower-quality answers (wrong citation format, missing cross-norm dependencies, hallucinated values).",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
norm: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: "Optional: norm code the user is asking about, if already known. Used to scope methodology hints (currently informational only).",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export async function handleGetMethodology(client, args) {
|
|
18
|
+
await ensureAuthenticated(client);
|
|
19
|
+
const result = await client.query(api._mcp.getMethodology, {
|
|
20
|
+
norm: args.norm,
|
|
21
|
+
});
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
export declare const getSectionTool: {
|
|
3
|
+
readonly name: "get_section";
|
|
4
|
+
readonly description: "Fetch the full enriched content of a section: markdown text with formulas in LaTeX and tables inline, pageStart/pageEnd, figures (call get_figure for the image), attached tables/formulas, and cross-references to other norms. ALL technical claims in your answer MUST be backed by a [<norm> <path>, p. <pageStart>] citation pointing to a section you actually fetched via get_section — never cite from memory. When a section's crossRefs list non-empty targets, follow them with another get_section call if the answer depends on them.";
|
|
5
|
+
readonly inputSchema: {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly norm: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Norm code, e.g. \"SIA 261\".";
|
|
11
|
+
};
|
|
12
|
+
readonly path: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly description: "Section path as it appears in the document, e.g. \"4.2.1\" or \"Annexe A\".";
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
readonly required: readonly ["norm", "path"];
|
|
18
|
+
readonly additionalProperties: false;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function handleGetSection(client: ConvexHttpClient, args: {
|
|
22
|
+
norm: string;
|
|
23
|
+
path: string;
|
|
24
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { api } from '../client.js';
|
|
2
|
+
import { ensureAuthenticated } from '../auth.js';
|
|
3
|
+
export const getSectionTool = {
|
|
4
|
+
name: 'get_section',
|
|
5
|
+
description: "Fetch the full enriched content of a section: markdown text with formulas in LaTeX and tables inline, pageStart/pageEnd, figures (call get_figure for the image), attached tables/formulas, and cross-references to other norms. ALL technical claims in your answer MUST be backed by a [<norm> <path>, p. <pageStart>] citation pointing to a section you actually fetched via get_section — never cite from memory. When a section's crossRefs list non-empty targets, follow them with another get_section call if the answer depends on them.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
norm: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Norm code, e.g. "SIA 261".',
|
|
12
|
+
},
|
|
13
|
+
path: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Section path as it appears in the document, e.g. "4.2.1" or "Annexe A".',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['norm', 'path'],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export async function handleGetSection(client, args) {
|
|
23
|
+
await ensureAuthenticated(client);
|
|
24
|
+
const result = await client.action(api._mcp.getSection, {
|
|
25
|
+
apiKey: process.env.STRATTA_API_KEY,
|
|
26
|
+
code: args.norm,
|
|
27
|
+
path: args.path,
|
|
28
|
+
});
|
|
29
|
+
if (result === null) {
|
|
30
|
+
return {
|
|
31
|
+
error: `Section "${args.path}" not found in norm "${args.norm}".`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { norm: args.norm, section: result };
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
export declare const getSubtreeTool: {
|
|
3
|
+
readonly name: "get_subtree";
|
|
4
|
+
readonly description: "Drill down into a specific chapter or section. Returns the subtree rooted at `path` with optional depth limit (relative to the root). Use this after get_toc to explore one chapter in detail without fetching the entire TOC. Each node has nodeId, path, title, summary, depth, pageStart, pageEnd, and recursive children.";
|
|
5
|
+
readonly inputSchema: {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly norm: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Norm code, e.g. \"SIA 261-1\".";
|
|
11
|
+
};
|
|
12
|
+
readonly path: {
|
|
13
|
+
readonly type: "string";
|
|
14
|
+
readonly description: "Section path to root the subtree at, e.g. \"14\" or \"14.2\".";
|
|
15
|
+
};
|
|
16
|
+
readonly maxDepth: {
|
|
17
|
+
readonly type: "number";
|
|
18
|
+
readonly description: "Max nesting depth relative to the root. Default: unlimited. depth=1 returns the root + direct children only.";
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
readonly required: readonly ["norm", "path"];
|
|
22
|
+
readonly additionalProperties: false;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare function handleGetSubtree(client: ConvexHttpClient, args: {
|
|
26
|
+
norm: string;
|
|
27
|
+
path: string;
|
|
28
|
+
maxDepth?: number;
|
|
29
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { api } from '../client.js';
|
|
2
|
+
import { ensureAuthenticated } from '../auth.js';
|
|
3
|
+
export const getSubtreeTool = {
|
|
4
|
+
name: 'get_subtree',
|
|
5
|
+
description: 'Drill down into a specific chapter or section. Returns the subtree rooted at `path` with optional depth limit (relative to the root). Use this after get_toc to explore one chapter in detail without fetching the entire TOC. Each node has nodeId, path, title, summary, depth, pageStart, pageEnd, and recursive children.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
norm: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Norm code, e.g. "SIA 261-1".',
|
|
12
|
+
},
|
|
13
|
+
path: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Section path to root the subtree at, e.g. "14" or "14.2".',
|
|
16
|
+
},
|
|
17
|
+
maxDepth: {
|
|
18
|
+
type: 'number',
|
|
19
|
+
description: 'Max nesting depth relative to the root. Default: unlimited. depth=1 returns the root + direct children only.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ['norm', 'path'],
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export async function handleGetSubtree(client, args) {
|
|
27
|
+
await ensureAuthenticated(client);
|
|
28
|
+
const result = await client.action(api._mcp.getSubtree, {
|
|
29
|
+
apiKey: process.env.STRATTA_API_KEY,
|
|
30
|
+
code: args.norm,
|
|
31
|
+
path: args.path,
|
|
32
|
+
maxDepth: args.maxDepth,
|
|
33
|
+
});
|
|
34
|
+
if (result === null) {
|
|
35
|
+
return {
|
|
36
|
+
error: `Section "${args.path}" not found in norm "${args.norm}".`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { norm: args.norm, root: args.path, tree: result };
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConvexHttpClient } from 'convex/browser';
|
|
2
|
+
export declare const getTocTool: {
|
|
3
|
+
readonly name: "get_toc";
|
|
4
|
+
readonly description: "Get the high-level table of contents for a norm. By default returns only top-level chapters (depth=1) to stay light. Call get_subtree on a specific chapter's path to drill into sections + subsections. Increase maxDepth if you need a wider overview (cost: response size grows fast). Each node has nodeId, path, title, summary, pageStart, pageEnd, depth, and children (empty at the maxDepth boundary).";
|
|
5
|
+
readonly inputSchema: {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly norm: {
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly description: "Norm code, e.g. \"SIA 261\", \"SIA 263\", \"EN 1992-1-1\".";
|
|
11
|
+
};
|
|
12
|
+
readonly maxDepth: {
|
|
13
|
+
readonly type: "number";
|
|
14
|
+
readonly description: "Maximum nesting depth to include. Defaults to 1 (chapters only). depth=2 includes sections X.Y. depth=3 includes sub-subsections X.Y.Z (may exceed response size limit on large norms).";
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
readonly required: readonly ["norm"];
|
|
18
|
+
readonly additionalProperties: false;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function handleGetToc(client: ConvexHttpClient, args: {
|
|
22
|
+
norm: string;
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
}): Promise<unknown>;
|