@stratta/mcp 0.1.0 → 0.2.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 +35 -13
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +20 -5
- package/dist/config.d.ts +3 -0
- package/dist/config.js +23 -0
- package/dist/elicit.d.ts +3 -0
- package/dist/elicit.js +42 -0
- package/dist/index.js +16 -6
- package/dist/login.d.ts +1 -0
- package/dist/login.js +40 -0
- package/dist/tools/get-cross-refs.js +2 -3
- package/dist/tools/get-figure.js +2 -3
- package/dist/tools/get-methodology.js +0 -2
- package/dist/tools/get-section.js +2 -3
- package/dist/tools/get-subtree.js +2 -3
- package/dist/tools/get-toc.js +2 -3
- package/dist/tools/ingest.js +2 -3
- package/dist/tools/list-norms.js +2 -3
- package/dist/tools/search-in-norm.js +2 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,27 +15,46 @@ MCP server exposing Swiss engineering norms (SIA / Eurocodes) to Claude clients
|
|
|
15
15
|
|
|
16
16
|
### Claude Code (recommended)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
never lands in a committable project `.mcp.json`):
|
|
18
|
+
Add the server — no key needed up front:
|
|
20
19
|
|
|
21
20
|
```bash
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
claude mcp add stratta -- npx -y @stratta/mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
On the first call, Claude Code prompts you to paste your Stratta API key. It's
|
|
25
|
+
validated and saved to `~/.stratta/config.json` (owner-only, `0600`), so you
|
|
26
|
+
only do this once.
|
|
27
|
+
|
|
28
|
+
Prefer to set it up ahead of time? Log in via the CLI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y @stratta/mcp login
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
…or pass the key explicitly as an environment variable (it then takes
|
|
35
|
+
precedence over the saved key):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add stratta --scope user --env STRATTA_API_KEY=sk_strt_xxx -- npx -y @stratta/mcp
|
|
24
39
|
```
|
|
25
40
|
|
|
26
41
|
### Claude Desktop
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
Save your key once via the CLI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx -y @stratta/mcp login
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then add the server to `claude_desktop_config.json` (Settings → Developer →
|
|
50
|
+
Edit Config) — no key needed in the file:
|
|
29
51
|
|
|
30
52
|
```json
|
|
31
53
|
{
|
|
32
54
|
"mcpServers": {
|
|
33
55
|
"stratta": {
|
|
34
56
|
"command": "npx",
|
|
35
|
-
"args": ["-y", "@stratta/mcp"]
|
|
36
|
-
"env": {
|
|
37
|
-
"STRATTA_API_KEY": "sk_strt_xxx"
|
|
38
|
-
}
|
|
57
|
+
"args": ["-y", "@stratta/mcp"]
|
|
39
58
|
}
|
|
40
59
|
}
|
|
41
60
|
}
|
|
@@ -43,8 +62,9 @@ Edit your `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
|
43
62
|
|
|
44
63
|
Restart Claude Desktop. The Stratta tools should appear in the MCP indicator.
|
|
45
64
|
|
|
46
|
-
>
|
|
47
|
-
>
|
|
65
|
+
> Prefer to keep the key in the config instead of `~/.stratta/config.json`? Add
|
|
66
|
+
> an `"env": { "STRATTA_API_KEY": "sk_strt_xxx" }` block to the server entry —
|
|
67
|
+
> but that file then stores the key in plaintext, so keep it private and unsynced.
|
|
48
68
|
|
|
49
69
|
### Global install (optional)
|
|
50
70
|
|
|
@@ -56,11 +76,13 @@ Then use `"command": "stratta-mcp"` instead of `npx`.
|
|
|
56
76
|
|
|
57
77
|
## Configuration
|
|
58
78
|
|
|
59
|
-
The
|
|
79
|
+
The API key is resolved in this order: the `STRATTA_API_KEY` env var, then
|
|
80
|
+
`~/.stratta/config.json` (written by `login` or the first-run prompt). Other
|
|
81
|
+
settings come from environment variables — see [`.env.example`](./.env.example).
|
|
60
82
|
|
|
61
83
|
| Variable | Required | Default | Purpose |
|
|
62
84
|
|---|---|---|---|
|
|
63
|
-
| `STRATTA_API_KEY` |
|
|
85
|
+
| `STRATTA_API_KEY` | no¹ | – | API key from https://stratta.ch. ¹If unset, the server falls back to `~/.stratta/config.json`, or prompts you on first use (clients that support elicitation). |
|
|
64
86
|
| `STRATTA_CONVEX_URL` | no | Stratta prod backend | Override only if you self-host. |
|
|
65
87
|
|
|
66
88
|
## Tools exposed
|
package/dist/auth.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ export type AuthContext = {
|
|
|
4
4
|
apiKeyId: string;
|
|
5
5
|
organizationId: string | null;
|
|
6
6
|
};
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function resolveApiKey(): string | null;
|
|
8
|
+
export declare function requireApiKey(): string;
|
|
9
|
+
export declare function ensureAuthenticated(client: ConvexHttpClient, plaintext: string): Promise<AuthContext>;
|
package/dist/auth.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { api } from './client.js';
|
|
2
|
+
import { readStoredApiKey } from './config.js';
|
|
2
3
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1h
|
|
3
4
|
let state = { kind: 'unvalidated' };
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Resolve the API key from the environment first (explicit, ephemeral), then
|
|
6
|
+
// fall back to the persisted config file (written by `login` or elicitation).
|
|
7
|
+
export function resolveApiKey() {
|
|
8
|
+
const fromEnv = process.env.STRATTA_API_KEY?.trim();
|
|
9
|
+
if (fromEnv)
|
|
10
|
+
return fromEnv;
|
|
11
|
+
return readStoredApiKey();
|
|
12
|
+
}
|
|
13
|
+
export function requireApiKey() {
|
|
14
|
+
const key = resolveApiKey();
|
|
15
|
+
if (!key) {
|
|
16
|
+
throw new Error('No Stratta API key found. Set STRATTA_API_KEY or run: npx @stratta/mcp login');
|
|
8
17
|
}
|
|
18
|
+
return key;
|
|
19
|
+
}
|
|
20
|
+
export async function ensureAuthenticated(client, plaintext) {
|
|
9
21
|
if (state.kind === 'valid' &&
|
|
22
|
+
state.plaintext === plaintext &&
|
|
10
23
|
Date.now() - state.validatedAt < CACHE_TTL_MS) {
|
|
11
24
|
return {
|
|
12
25
|
userId: state.userId,
|
|
@@ -14,7 +27,7 @@ export async function ensureAuthenticated(client) {
|
|
|
14
27
|
organizationId: state.organizationId,
|
|
15
28
|
};
|
|
16
29
|
}
|
|
17
|
-
if (state.kind === 'invalid') {
|
|
30
|
+
if (state.kind === 'invalid' && state.plaintext === plaintext) {
|
|
18
31
|
throw new Error(state.error);
|
|
19
32
|
}
|
|
20
33
|
const result = (await client.action(api.apiKeys.validateApiKey, {
|
|
@@ -23,12 +36,14 @@ export async function ensureAuthenticated(client) {
|
|
|
23
36
|
if (!result.valid || !result.userId || !result.apiKeyId) {
|
|
24
37
|
state = {
|
|
25
38
|
kind: 'invalid',
|
|
39
|
+
plaintext,
|
|
26
40
|
error: 'Invalid STRATTA_API_KEY. Generate a new one at https://stratta.ch/api-keys',
|
|
27
41
|
};
|
|
28
42
|
throw new Error(state.error);
|
|
29
43
|
}
|
|
30
44
|
state = {
|
|
31
45
|
kind: 'valid',
|
|
46
|
+
plaintext,
|
|
32
47
|
userId: result.userId,
|
|
33
48
|
apiKeyId: result.apiKeyId,
|
|
34
49
|
organizationId: result.organizationId,
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
// The API key persists here so a user only enters it once. chmod 0600 on the
|
|
5
|
+
// file (best-effort on Windows) keeps it readable by the owner only. This is
|
|
6
|
+
// the same pattern used by gh / aws / auth0 CLIs.
|
|
7
|
+
export const CONFIG_PATH = join(homedir(), '.stratta', 'config.json');
|
|
8
|
+
export function readStoredApiKey(path = CONFIG_PATH) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
11
|
+
const key = parsed.apiKey?.trim();
|
|
12
|
+
return key && key.length > 0 ? key : null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function writeStoredApiKey(apiKey, path = CONFIG_PATH) {
|
|
19
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
20
|
+
writeFileSync(path, JSON.stringify({ apiKey }, null, 2) + '\n', {
|
|
21
|
+
mode: 0o600,
|
|
22
|
+
});
|
|
23
|
+
}
|
package/dist/elicit.d.ts
ADDED
package/dist/elicit.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { api } from './client.js';
|
|
2
|
+
import { writeStoredApiKey } from './config.js';
|
|
3
|
+
// Ask the user for their API key through the MCP client (an interactive
|
|
4
|
+
// dialog shown by Claude), validate it, and persist it so they only do this
|
|
5
|
+
// once. The key travels over the client<->user channel, not through the LLM.
|
|
6
|
+
export async function elicitApiKey(server, client) {
|
|
7
|
+
const caps = server.getClientCapabilities();
|
|
8
|
+
if (!caps?.elicitation) {
|
|
9
|
+
throw new Error('No Stratta API key found, and this MCP client cannot prompt for one. ' +
|
|
10
|
+
'Set STRATTA_API_KEY in the server env, or run: npx @stratta/mcp login');
|
|
11
|
+
}
|
|
12
|
+
const result = await server.elicitInput({
|
|
13
|
+
mode: 'form',
|
|
14
|
+
message: 'Stratta needs your API key. Generate one at https://stratta.ch (API keys page) and paste it below.',
|
|
15
|
+
requestedSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
apiKey: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
title: 'Stratta API key',
|
|
21
|
+
description: 'Starts with sk_strt_',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: ['apiKey'],
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (result.action !== 'accept' || !result.content) {
|
|
28
|
+
throw new Error('API key entry was cancelled. Set STRATTA_API_KEY or run: npx @stratta/mcp login');
|
|
29
|
+
}
|
|
30
|
+
const apiKey = String(result.content.apiKey ?? '').trim();
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new Error('No API key was provided.');
|
|
33
|
+
}
|
|
34
|
+
const res = (await client.action(api.apiKeys.validateApiKey, {
|
|
35
|
+
plaintext: apiKey,
|
|
36
|
+
}));
|
|
37
|
+
if (!res?.valid) {
|
|
38
|
+
throw new Error('That API key is invalid. Generate a new one at https://stratta.ch and try again.');
|
|
39
|
+
}
|
|
40
|
+
writeStoredApiKey(apiKey);
|
|
41
|
+
return apiKey;
|
|
42
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { createConvexClient, api } from './client.js';
|
|
6
|
-
import { ensureAuthenticated } from './auth.js';
|
|
6
|
+
import { ensureAuthenticated, resolveApiKey } from './auth.js';
|
|
7
|
+
import { elicitApiKey } from './elicit.js';
|
|
7
8
|
import { getMethodologyTool, handleGetMethodology } from './tools/get-methodology.js';
|
|
8
9
|
import { listNormsTool, handleListNorms } from './tools/list-norms.js';
|
|
9
10
|
import { getTocTool, handleGetToc } from './tools/get-toc.js';
|
|
@@ -24,7 +25,7 @@ const tools = [
|
|
|
24
25
|
getCrossRefsTool,
|
|
25
26
|
...ingestTools,
|
|
26
27
|
];
|
|
27
|
-
const server = new Server({ name: 'stratta-mcp', version: '0.
|
|
28
|
+
const server = new Server({ name: 'stratta-mcp', version: '0.2.0' }, { capabilities: { tools: {} } });
|
|
28
29
|
const client = createConvexClient();
|
|
29
30
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
30
31
|
tools,
|
|
@@ -33,17 +34,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
33
34
|
const { name, arguments: args } = request.params;
|
|
34
35
|
const startedAt = Date.now();
|
|
35
36
|
let apiKeyId = null;
|
|
37
|
+
let resolvedKey = null;
|
|
36
38
|
let status = 'success';
|
|
37
39
|
const recordUsage = () => {
|
|
38
40
|
// Only record once authenticated. Send the plaintext key (proof of
|
|
39
41
|
// possession) — the server derives the identity, so usage can't be forged
|
|
40
42
|
// for another user's key.
|
|
41
|
-
|
|
42
|
-
if (!apiKeyId || !apiKey)
|
|
43
|
+
if (!apiKeyId || !resolvedKey)
|
|
43
44
|
return;
|
|
44
45
|
void client
|
|
45
46
|
.action(api.apiKeys.recordToolUsage, {
|
|
46
|
-
apiKey,
|
|
47
|
+
apiKey: resolvedKey,
|
|
47
48
|
toolName: name,
|
|
48
49
|
status,
|
|
49
50
|
durationMs: Date.now() - startedAt,
|
|
@@ -51,7 +52,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
51
52
|
.catch((e) => console.error('[stratta-mcp] track failed:', e));
|
|
52
53
|
};
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
+
resolvedKey = resolveApiKey();
|
|
56
|
+
if (!resolvedKey) {
|
|
57
|
+
resolvedKey = await elicitApiKey(server, client);
|
|
58
|
+
}
|
|
59
|
+
const auth = await ensureAuthenticated(client, resolvedKey);
|
|
55
60
|
apiKeyId = auth.apiKeyId;
|
|
56
61
|
let result;
|
|
57
62
|
if (isIngestTool(name)) {
|
|
@@ -131,6 +136,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
131
136
|
}
|
|
132
137
|
});
|
|
133
138
|
async function main() {
|
|
139
|
+
if (process.argv[2] === 'login') {
|
|
140
|
+
const { runLogin } = await import('./login.js');
|
|
141
|
+
await runLogin();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
134
144
|
const transport = new StdioServerTransport();
|
|
135
145
|
await server.connect(transport);
|
|
136
146
|
// Stderr-only logging; stdout is reserved for MCP protocol.
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLogin(): Promise<void>;
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { createConvexClient, api } from './client.js';
|
|
3
|
+
import { writeStoredApiKey, CONFIG_PATH } from './config.js';
|
|
4
|
+
// Prompt for a secret on the TTY without echoing it back.
|
|
5
|
+
function promptSecret(query) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
let muted = false;
|
|
9
|
+
rl._writeToOutput =
|
|
10
|
+
(s) => {
|
|
11
|
+
if (!muted)
|
|
12
|
+
process.stdout.write(s);
|
|
13
|
+
};
|
|
14
|
+
rl.question(query, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
process.stdout.write('\n');
|
|
17
|
+
resolve(answer.trim());
|
|
18
|
+
});
|
|
19
|
+
muted = true;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function runLogin() {
|
|
23
|
+
const apiKey = await promptSecret('Paste your Stratta API key (from https://stratta.ch): ');
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
console.error('No key entered. Aborting.');
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const client = createConvexClient();
|
|
30
|
+
const res = (await client.action(api.apiKeys.validateApiKey, {
|
|
31
|
+
plaintext: apiKey,
|
|
32
|
+
}));
|
|
33
|
+
if (!res?.valid) {
|
|
34
|
+
console.error('That API key is invalid. Generate a new one at https://stratta.ch and try again.');
|
|
35
|
+
process.exitCode = 1;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
writeStoredApiKey(apiKey);
|
|
39
|
+
console.log(`Saved. Stratta MCP will use this key (stored at ${CONFIG_PATH}).`);
|
|
40
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const getCrossRefsTool = {
|
|
4
4
|
name: 'get_cross_refs',
|
|
5
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).",
|
|
@@ -14,9 +14,8 @@ export const getCrossRefsTool = {
|
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
export async function handleGetCrossRefs(client, args) {
|
|
17
|
-
await ensureAuthenticated(client);
|
|
18
17
|
const result = await client.action(api._mcp.getCrossRefs, {
|
|
19
|
-
apiKey:
|
|
18
|
+
apiKey: requireApiKey(),
|
|
20
19
|
code: args.norm,
|
|
21
20
|
path: args.path,
|
|
22
21
|
});
|
package/dist/tools/get-figure.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const getFigureTool = {
|
|
4
4
|
name: 'get_figure',
|
|
5
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.',
|
|
@@ -17,8 +17,7 @@ export const getFigureTool = {
|
|
|
17
17
|
},
|
|
18
18
|
};
|
|
19
19
|
export async function handleGetFigure(client, args) {
|
|
20
|
-
|
|
21
|
-
const apiKey = process.env.STRATTA_API_KEY;
|
|
20
|
+
const apiKey = requireApiKey();
|
|
22
21
|
const meta = (await client.action(api._mcp.getFigureMeta, {
|
|
23
22
|
apiKey,
|
|
24
23
|
code: args.norm,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import { ensureAuthenticated } from '../auth.js';
|
|
3
2
|
export const getMethodologyTool = {
|
|
4
3
|
name: 'get_methodology',
|
|
5
4
|
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).",
|
|
@@ -15,7 +14,6 @@ export const getMethodologyTool = {
|
|
|
15
14
|
},
|
|
16
15
|
};
|
|
17
16
|
export async function handleGetMethodology(client, args) {
|
|
18
|
-
await ensureAuthenticated(client);
|
|
19
17
|
const result = await client.query(api._mcp.getMethodology, {
|
|
20
18
|
norm: args.norm,
|
|
21
19
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const getSectionTool = {
|
|
4
4
|
name: 'get_section',
|
|
5
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.",
|
|
@@ -20,9 +20,8 @@ export const getSectionTool = {
|
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
export async function handleGetSection(client, args) {
|
|
23
|
-
await ensureAuthenticated(client);
|
|
24
23
|
const result = await client.action(api._mcp.getSection, {
|
|
25
|
-
apiKey:
|
|
24
|
+
apiKey: requireApiKey(),
|
|
26
25
|
code: args.norm,
|
|
27
26
|
path: args.path,
|
|
28
27
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const getSubtreeTool = {
|
|
4
4
|
name: 'get_subtree',
|
|
5
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.',
|
|
@@ -24,9 +24,8 @@ export const getSubtreeTool = {
|
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
export async function handleGetSubtree(client, args) {
|
|
27
|
-
await ensureAuthenticated(client);
|
|
28
27
|
const result = await client.action(api._mcp.getSubtree, {
|
|
29
|
-
apiKey:
|
|
28
|
+
apiKey: requireApiKey(),
|
|
30
29
|
code: args.norm,
|
|
31
30
|
path: args.path,
|
|
32
31
|
maxDepth: args.maxDepth,
|
package/dist/tools/get-toc.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const getTocTool = {
|
|
4
4
|
name: 'get_toc',
|
|
5
5
|
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).",
|
|
@@ -20,9 +20,8 @@ export const getTocTool = {
|
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
export async function handleGetToc(client, args) {
|
|
23
|
-
await ensureAuthenticated(client);
|
|
24
23
|
const result = await client.action(api._mcp.getDocumentToc, {
|
|
25
|
-
apiKey:
|
|
24
|
+
apiKey: requireApiKey(),
|
|
26
25
|
code: args.norm,
|
|
27
26
|
maxDepth: args.maxDepth,
|
|
28
27
|
});
|
package/dist/tools/ingest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
// Ingestion tools — let an engineering office add ITS OWN licensed norms to its
|
|
4
4
|
// Stratta workspace, agentically, from the user's own Claude. Every call is
|
|
5
5
|
// authenticated by the user's STRATTA_API_KEY and scoped server-side to their
|
|
@@ -159,8 +159,7 @@ export function isIngestTool(name) {
|
|
|
159
159
|
return ingestTools.some((t) => t.name === name);
|
|
160
160
|
}
|
|
161
161
|
export async function handleIngestTool(client, name, args) {
|
|
162
|
-
|
|
163
|
-
const apiKey = process.env.STRATTA_API_KEY;
|
|
162
|
+
const apiKey = requireApiKey();
|
|
164
163
|
switch (name) {
|
|
165
164
|
case 'ingest_status':
|
|
166
165
|
return await client.action(api.ingestionApi.ingestStatus, {
|
package/dist/tools/list-norms.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const listNormsTool = {
|
|
4
4
|
name: 'list_norms',
|
|
5
5
|
description: "List all engineering norms (SIA, Eurocodes, etc.) currently available in Stratta. Returns code, year, title, and language for each. Workflow: call get_methodology FIRST (persona + rules), then list_norms to know what is queryable, then meta-route the user's question to the relevant norms before drilling in.",
|
|
@@ -10,9 +10,8 @@ export const listNormsTool = {
|
|
|
10
10
|
},
|
|
11
11
|
};
|
|
12
12
|
export async function handleListNorms(client) {
|
|
13
|
-
await ensureAuthenticated(client);
|
|
14
13
|
const result = await client.action(api._mcp.listPublishedNorms, {
|
|
15
|
-
apiKey:
|
|
14
|
+
apiKey: requireApiKey(),
|
|
16
15
|
});
|
|
17
16
|
return { norms: result };
|
|
18
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { api } from '../client.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireApiKey } from '../auth.js';
|
|
3
3
|
export const searchInNormTool = {
|
|
4
4
|
name: 'search_in_norm',
|
|
5
5
|
description: 'Search for sections within a norm by keyword (case-insensitive). Returns up to `limit` matches with path, title, and a snippet of matching content. Use this when you do not yet know which section to read — e.g. "charges variables" or "béton armé".',
|
|
@@ -21,9 +21,8 @@ export const searchInNormTool = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
export async function handleSearchInNorm(client, args) {
|
|
24
|
-
await ensureAuthenticated(client);
|
|
25
24
|
const result = await client.action(api._mcp.searchInNorm, {
|
|
26
|
-
apiKey:
|
|
25
|
+
apiKey: requireApiKey(),
|
|
27
26
|
code: args.norm,
|
|
28
27
|
keyword: args.keyword,
|
|
29
28
|
limit: args.limit,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratta/mcp",
|
|
3
3
|
"mcpName": "io.github.hugogebel-boop/stratta",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "MCP server exposing Swiss engineering norms (SIA / Eurocodes) to Claude clients via Stratta TreeRAG.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": "Hugo Gebel <hugo.gebel@epfl.ch>",
|