@lyrra/mcp-server 1.1.8 → 1.1.10
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 +18 -4
- package/dist/http-main.js +173 -36
- package/dist/lyrra-mcp-core.js +2 -2
- package/dist/openapi-parse.js +37 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -21,9 +21,11 @@ If you integrate **without** MCP (scripts, Postman, custom backend), call Lyrra
|
|
|
21
21
|
Published as **`@lyrra/mcp-server`**. No local clone required:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx -y @lyrra/mcp-server
|
|
24
|
+
npx -y @lyrra/mcp-server lyrra-mcp
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
(Paquet avec **plusieurs** binaires : `lyrra-mcp`, `lyrra-mcp-http`, `mcp-server` — sans nom explicite, `npx` peut répondre *could not determine executable to run*.)
|
|
28
|
+
|
|
27
29
|
In **Claude Desktop** (`claude_desktop_config.json`):
|
|
28
30
|
|
|
29
31
|
```json
|
|
@@ -31,7 +33,7 @@ In **Claude Desktop** (`claude_desktop_config.json`):
|
|
|
31
33
|
"mcpServers": {
|
|
32
34
|
"lyrra-studio": {
|
|
33
35
|
"command": "npx",
|
|
34
|
-
"args": ["-y", "@lyrra/mcp-server"],
|
|
36
|
+
"args": ["-y", "@lyrra/mcp-server", "lyrra-mcp"],
|
|
35
37
|
"env": {
|
|
36
38
|
"LYRRA_API_URL": "https://your-domain.com/api",
|
|
37
39
|
"LYRRA_CLIENT_ID": "rak_xxxxxxxx",
|
|
@@ -42,7 +44,7 @@ In **Claude Desktop** (`claude_desktop_config.json`):
|
|
|
42
44
|
}
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
`LYRRA_API_URL` must be your Lyrra **REST base** (usually `https://<host>/api`) so MCP tools can execute against the right environment.
|
|
47
|
+
`LYRRA_API_URL` must be your Lyrra **REST base** (usually `https://<host>/api`) so MCP tools can execute against the right environment. **Do not** use the Streamable HTTP MCP URL here (not `…/mcp/api`) — that is only for clients that speak MCP over HTTPS to path `/mcp`, not for `LYRRA_API_URL`.
|
|
46
48
|
|
|
47
49
|
Use **Header Auth** keys from the institution dashboard instead of client id/secret:
|
|
48
50
|
|
|
@@ -54,7 +56,18 @@ Use **Header Auth** keys from the institution dashboard instead of client id/sec
|
|
|
54
56
|
}
|
|
55
57
|
```
|
|
56
58
|
|
|
57
|
-
Global install (optional): `npm install -g @lyrra/mcp-server` then run **`lyrra-mcp`** (
|
|
59
|
+
Global install (optional): `npm install -g @lyrra/mcp-server` then run **`lyrra-mcp`** or **`mcp-server`** (binaries on `PATH`).
|
|
60
|
+
|
|
61
|
+
## Troubleshooting (logs Claude / Cursor)
|
|
62
|
+
|
|
63
|
+
| Symptôme | Cause fréquente | Correctif |
|
|
64
|
+
|----------|-----------------|-----------|
|
|
65
|
+
| `Cannot find module '/dist/index.js'` | `command` = `node` et args = `dist/index.js` **sans** répertoire de travail du paquet | Utiliser `npx` + args ci-dessus, **ou** `node` avec chemin **absolu** vers `…/node_modules/@lyrra/mcp-server/dist/index.js` |
|
|
66
|
+
| `node: bad option: -y` | `command` = `node` au lieu de `npx` | `command` doit être **`npx`**, pas `node` |
|
|
67
|
+
| `could not determine executable to run` | Plusieurs `bin` dans le paquet npm | Ajouter **`lyrra-mcp`** en dernier argument : `["-y", "@lyrra/mcp-server", "lyrra-mcp"]` |
|
|
68
|
+
| Réponses API en HTML (`Unexpected token '<'`) | `LYRRA_API_URL` pointe vers le **frontend** ou une URL sans `/api` | Mettre `https://<hôte>/api` (base REST réelle) |
|
|
69
|
+
| `OpenAPI introuvable …/mcp/api/openapi.json` | `LYRRA_API_URL` = `…/mcp/api` (confusion avec l’URL MCP HTTP) | Utiliser `…/api` uniquement ; optionnel : `LYRRA_OPENAPI_URL=https://<hôte>/api/openapi.json` |
|
|
70
|
+
| `@lyrra/mcp-server` 404 sur npm | Scope / publication | Vérifier que le paquet est bien public sur npm sous `@lyrra/mcp-server` |
|
|
58
71
|
|
|
59
72
|
## Streamable HTTP / n8n (MCP URL `https://…/mcp`)
|
|
60
73
|
|
|
@@ -63,6 +76,7 @@ Some clients need an **HTTPS MCP endpoint** (not stdio). They use [Streamable HT
|
|
|
63
76
|
- **Docker:** service **`mcp-http`** in `docker-compose.yml` and Nginx **`location /mcp`** (see `apps/frontend/nginx.default.conf` in the monorepo).
|
|
64
77
|
- **Auth:** n8n **Header Auth** — same header name and **full** secret as an institution **Header auth** key (e.g. `X-Lyrra-Api-Key` + `rak_…`).
|
|
65
78
|
- **Run locally:** `LYRRA_API_URL=http://localhost:3001/api npm run start:http` → listens on **`LYRRA_MCP_HTTP_PORT`** (default **3457**), MCP path `/mcp`.
|
|
79
|
+
- **n8n « Could not connect » :** the MCP SDK requires `Accept` to include **both** `application/json` and `text/event-stream` on POST. **`lyrra-mcp-http` patches missing values** so n8n can connect; ensure **`mcp-http`** + Nginx `/mcp` are deployed, and Header Auth **Value** is the **full** `rak_…` secret (same as at key creation).
|
|
66
80
|
|
|
67
81
|
Binaries after global install: **`lyrra-mcp`** (stdio MCP) and **`lyrra-mcp-http`** (HTTP MCP).
|
|
68
82
|
|
package/dist/http-main.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* MCP Streamable HTTP —
|
|
4
|
-
* (
|
|
3
|
+
* MCP Streamable HTTP — URL publique https://lyrrastudio.com/mcp
|
|
4
|
+
* Sessions stateful (comme ReadAudioPDF) : initialize → mcp-session-id → POST / GET SSE / DELETE.
|
|
5
|
+
* Auth : Header Auth (X-Lyrra-Api-Key, Authorization, …) validée via GET /api/auth/me.
|
|
5
6
|
*/
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
8
|
import express from 'express';
|
|
7
9
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
10
|
import { createLyrraMcpServer } from './lyrra-mcp-core.js';
|
|
9
11
|
import { runWithInboundAuthHeaders, requestOrigin } from './auth-session.js';
|
|
10
12
|
import { pickIncomingAuthHeaders } from './http-incoming-auth.js';
|
|
13
|
+
import { getOpenApiJsonCached } from './openapi-parse.js';
|
|
11
14
|
const PORT = parseInt(process.env.LYRRA_MCP_HTTP_PORT ?? '3457', 10);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const mcpSessions = new Map();
|
|
16
|
+
function authFingerprint(headers) {
|
|
17
|
+
const keys = Object.keys(headers).sort();
|
|
18
|
+
const payload = keys.map((k) => `${k}:${headers[k]}`).join('\n');
|
|
19
|
+
return createHash('sha256').update(payload).digest('hex');
|
|
16
20
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const next = mutexChain.then(() => fn());
|
|
21
|
-
mutexChain = next.then(() => undefined, () => undefined);
|
|
22
|
-
return next;
|
|
21
|
+
function setMcpCors(res) {
|
|
22
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
23
|
+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
|
|
23
24
|
}
|
|
24
25
|
async function validateAuth(headers) {
|
|
25
26
|
const origin = requestOrigin();
|
|
@@ -29,17 +30,37 @@ async function validateAuth(headers) {
|
|
|
29
30
|
});
|
|
30
31
|
return r.ok;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @modelcontextprotocol/sdk (Streamable HTTP) exige sur POST :
|
|
35
|
+
* Accept contenant à la fois `application/json` et `text/event-stream`.
|
|
36
|
+
*/
|
|
37
|
+
function patchAcceptForMcpStreamable(req) {
|
|
38
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
39
|
+
const a = String(req.headers.accept ?? '').toLowerCase();
|
|
40
|
+
if (method === 'POST') {
|
|
41
|
+
if (!a.includes('application/json') || !a.includes('text/event-stream')) {
|
|
42
|
+
req.headers.accept = 'application/json, text/event-stream';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (method === 'GET') {
|
|
46
|
+
if (!a.includes('text/event-stream')) {
|
|
47
|
+
const orig = typeof req.headers.accept === 'string' ? req.headers.accept : '';
|
|
48
|
+
req.headers.accept =
|
|
49
|
+
orig.trim() && a.includes('application/json')
|
|
50
|
+
? `${orig}, text/event-stream`
|
|
51
|
+
: 'text/event-stream, application/json';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function requireValidAuth(req, res) {
|
|
56
|
+
setMcpCors(res);
|
|
36
57
|
const auth = pickIncomingAuthHeaders(req.headers);
|
|
37
58
|
if (!auth) {
|
|
38
59
|
res.status(401).json({
|
|
39
60
|
success: false,
|
|
40
61
|
error: 'Missing auth: use n8n Header Auth with X-Lyrra-Api-Key (institution key) or Authorization Bearer.',
|
|
41
62
|
});
|
|
42
|
-
return;
|
|
63
|
+
return null;
|
|
43
64
|
}
|
|
44
65
|
const skipValidate = process.env.LYRRA_MCP_SKIP_AUTH_VALIDATE === '1';
|
|
45
66
|
if (!skipValidate) {
|
|
@@ -49,37 +70,107 @@ app.all('/mcp', async (req, res) => {
|
|
|
49
70
|
success: false,
|
|
50
71
|
error: 'Invalid Lyrra credentials (GET /api/auth/me failed). Check your Header Auth value.',
|
|
51
72
|
});
|
|
52
|
-
return;
|
|
73
|
+
return null;
|
|
53
74
|
}
|
|
54
75
|
}
|
|
76
|
+
return auth;
|
|
77
|
+
}
|
|
78
|
+
function sessionIdFromReq(req) {
|
|
79
|
+
const h = req.headers['mcp-session-id'];
|
|
80
|
+
if (typeof h === 'string' && h.length > 0)
|
|
81
|
+
return h;
|
|
82
|
+
if (Array.isArray(h) && h[0])
|
|
83
|
+
return h[0];
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function getSessionOr403(req, res, auth) {
|
|
87
|
+
setMcpCors(res);
|
|
88
|
+
const sid = sessionIdFromReq(req);
|
|
89
|
+
if (!sid || !mcpSessions.has(sid)) {
|
|
90
|
+
res.status(404).json({
|
|
91
|
+
error: 'Session not found. Please re-initialize.',
|
|
92
|
+
jsonrpc: '2.0',
|
|
93
|
+
id: null,
|
|
94
|
+
});
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const session = mcpSessions.get(sid);
|
|
98
|
+
if (session.authFp !== authFingerprint(auth)) {
|
|
99
|
+
res.status(403).json({
|
|
100
|
+
success: false,
|
|
101
|
+
error: 'Session credentials mismatch.',
|
|
102
|
+
});
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return session;
|
|
106
|
+
}
|
|
107
|
+
const app = express();
|
|
108
|
+
app.disable('x-powered-by');
|
|
109
|
+
app.use('/mcp', express.json({ limit: '50mb' }));
|
|
110
|
+
app.options('/mcp', (_req, res) => {
|
|
111
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
112
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
113
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, X-Lyrra-Api-Key, X-Api-Key, Mcp-Session-Id, Last-Event-ID');
|
|
114
|
+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
|
|
115
|
+
res.status(204).end();
|
|
116
|
+
});
|
|
117
|
+
app.post('/mcp', async (req, res) => {
|
|
118
|
+
patchAcceptForMcpStreamable(req);
|
|
119
|
+
const auth = await requireValidAuth(req, res);
|
|
120
|
+
if (!auth)
|
|
121
|
+
return;
|
|
122
|
+
const sessionId = sessionIdFromReq(req);
|
|
123
|
+
const body = req.body;
|
|
124
|
+
const isInitialize = body?.method === 'initialize';
|
|
55
125
|
try {
|
|
56
|
-
|
|
57
|
-
const
|
|
126
|
+
if (sessionId && mcpSessions.has(sessionId)) {
|
|
127
|
+
const session = mcpSessions.get(sessionId);
|
|
128
|
+
if (session.authFp !== authFingerprint(auth)) {
|
|
129
|
+
setMcpCors(res);
|
|
130
|
+
res.status(403).json({ success: false, error: 'Session credentials mismatch.' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setMcpCors(res);
|
|
134
|
+
await runWithInboundAuthHeaders(auth, () => session.transport.handleRequest(req, res, body));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (sessionId && !isInitialize) {
|
|
138
|
+
setMcpCors(res);
|
|
139
|
+
res.status(404).json({
|
|
140
|
+
jsonrpc: '2.0',
|
|
141
|
+
error: { code: -32000, message: 'Session not found. Please re-initialize.' },
|
|
142
|
+
id: body?.id ?? null,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const fp = authFingerprint(auth);
|
|
147
|
+
setMcpCors(res);
|
|
148
|
+
await runWithInboundAuthHeaders(auth, async () => {
|
|
149
|
+
const { mcp: server } = await createLyrraMcpServer();
|
|
58
150
|
const transport = new StreamableHTTPServerTransport({
|
|
59
|
-
sessionIdGenerator:
|
|
151
|
+
sessionIdGenerator: () => randomUUID(),
|
|
60
152
|
});
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
finally {
|
|
66
|
-
try {
|
|
67
|
-
await transport.close();
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
/* ignore */
|
|
71
|
-
}
|
|
153
|
+
transport.onclose = () => {
|
|
154
|
+
const sid = transport.sessionId;
|
|
155
|
+
if (sid)
|
|
156
|
+
mcpSessions.delete(sid);
|
|
72
157
|
try {
|
|
73
|
-
|
|
158
|
+
void server.close();
|
|
74
159
|
}
|
|
75
160
|
catch {
|
|
76
161
|
/* ignore */
|
|
77
162
|
}
|
|
163
|
+
};
|
|
164
|
+
await server.connect(transport);
|
|
165
|
+
await transport.handleRequest(req, res, body);
|
|
166
|
+
const newSid = transport.sessionId;
|
|
167
|
+
if (newSid && !mcpSessions.has(newSid)) {
|
|
168
|
+
mcpSessions.set(newSid, { transport, server, authFp: fp });
|
|
78
169
|
}
|
|
79
|
-
})
|
|
170
|
+
});
|
|
80
171
|
}
|
|
81
172
|
catch (e) {
|
|
82
|
-
process.stderr.write(`[lyrra-mcp-http] ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
173
|
+
process.stderr.write(`[lyrra-mcp-http] POST ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
83
174
|
if (!res.headersSent) {
|
|
84
175
|
res.status(500).json({
|
|
85
176
|
jsonrpc: '2.0',
|
|
@@ -89,11 +180,57 @@ app.all('/mcp', async (req, res) => {
|
|
|
89
180
|
}
|
|
90
181
|
}
|
|
91
182
|
});
|
|
183
|
+
app.get('/mcp', async (req, res) => {
|
|
184
|
+
patchAcceptForMcpStreamable(req);
|
|
185
|
+
const auth = await requireValidAuth(req, res);
|
|
186
|
+
if (!auth)
|
|
187
|
+
return;
|
|
188
|
+
const session = getSessionOr403(req, res, auth);
|
|
189
|
+
if (!session)
|
|
190
|
+
return;
|
|
191
|
+
try {
|
|
192
|
+
await runWithInboundAuthHeaders(auth, () => session.transport.handleRequest(req, res));
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
process.stderr.write(`[lyrra-mcp-http] GET ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
196
|
+
if (!res.headersSent) {
|
|
197
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
app.delete('/mcp', async (req, res) => {
|
|
202
|
+
patchAcceptForMcpStreamable(req);
|
|
203
|
+
const auth = await requireValidAuth(req, res);
|
|
204
|
+
if (!auth)
|
|
205
|
+
return;
|
|
206
|
+
const sid = sessionIdFromReq(req);
|
|
207
|
+
const session = getSessionOr403(req, res, auth);
|
|
208
|
+
if (!session)
|
|
209
|
+
return;
|
|
210
|
+
try {
|
|
211
|
+
setMcpCors(res);
|
|
212
|
+
await runWithInboundAuthHeaders(auth, () => session.transport.handleRequest(req, res));
|
|
213
|
+
if (sid)
|
|
214
|
+
mcpSessions.delete(sid);
|
|
215
|
+
}
|
|
216
|
+
catch (e) {
|
|
217
|
+
process.stderr.write(`[lyrra-mcp-http] DELETE ${e instanceof Error ? e.stack ?? e.message : e}\n`);
|
|
218
|
+
if (!res.headersSent) {
|
|
219
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
92
223
|
app.get('/health', (_req, res) => {
|
|
93
224
|
res.status(200).type('text/plain').send('ok');
|
|
94
225
|
});
|
|
95
226
|
async function main() {
|
|
96
|
-
|
|
227
|
+
try {
|
|
228
|
+
await getOpenApiJsonCached();
|
|
229
|
+
process.stderr.write('[lyrra-mcp-http] OpenAPI cache warmed.\n');
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
process.stderr.write(`[lyrra-mcp-http] OpenAPI warm skipped (first session will load): ${e instanceof Error ? e.message : e}\n`);
|
|
233
|
+
}
|
|
97
234
|
app.listen(PORT, '0.0.0.0', () => {
|
|
98
235
|
process.stderr.write(`[lyrra-mcp-http] listening 0.0.0.0:${PORT} path /mcp health /health\n`);
|
|
99
236
|
});
|
package/dist/lyrra-mcp-core.js
CHANGED
|
@@ -5,7 +5,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { requestOrigin } from './auth-session.js';
|
|
7
7
|
import { callLyrraOperation } from './lyrra-http.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getOpenApiJsonCached, parseOpenApiOperations } from './openapi-parse.js';
|
|
9
9
|
import { eduflowBlockDocToolCount, registerEduflowBlockDocumentationTools, } from './register-eduflow-block-tools.js';
|
|
10
10
|
const MAX_DESC_CHARS = 12_000;
|
|
11
11
|
const MAX_TOOLS_ENV = process.env.LYRRA_MCP_MAX_TOOLS;
|
|
@@ -49,7 +49,7 @@ function formatToolResult(result) {
|
|
|
49
49
|
}
|
|
50
50
|
export async function createLyrraMcpServer() {
|
|
51
51
|
process.stderr.write('[lyrra-mcp] Loading OpenAPI…\n');
|
|
52
|
-
const spec = await
|
|
52
|
+
const spec = await getOpenApiJsonCached();
|
|
53
53
|
let ops = parseOpenApiOperations(spec);
|
|
54
54
|
const maxTools = MAX_TOOLS_ENV ? parseInt(MAX_TOOLS_ENV, 10) : NaN;
|
|
55
55
|
if (!Number.isNaN(maxTools) && maxTools > 0) {
|
package/dist/openapi-parse.js
CHANGED
|
@@ -49,13 +49,44 @@ export function parseOpenApiOperations(spec) {
|
|
|
49
49
|
out.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/** URLs à essayer pour charger le spec OpenAPI (ordre = priorité). */
|
|
53
|
+
function openApiCandidateUrls() {
|
|
53
54
|
const override = process.env.LYRRA_OPENAPI_URL?.trim();
|
|
55
|
+
if (override)
|
|
56
|
+
return [override];
|
|
54
57
|
const origin = requestOrigin();
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const urls = [`${origin}/api/openapi.json`];
|
|
59
|
+
/**
|
|
60
|
+
* Si `LYRRA_API_URL` vaut `https://hôte/mcp/api` (confusion avec l’URL MCP HTTP),
|
|
61
|
+
* `requestOrigin()` devient `https://hôte/mcp` et l’OpenAPI testée est fausse (502, etc.).
|
|
62
|
+
* On retente alors la base API standard.
|
|
63
|
+
*/
|
|
64
|
+
const m = /^(.+)\/mcp$/i.exec(origin);
|
|
65
|
+
if (m?.[1]) {
|
|
66
|
+
urls.push(`${m[1]}/api/openapi.json`);
|
|
67
|
+
}
|
|
68
|
+
return urls;
|
|
69
|
+
}
|
|
70
|
+
export async function fetchOpenApiJson() {
|
|
71
|
+
const urls = openApiCandidateUrls();
|
|
72
|
+
let lastStatus = 0;
|
|
73
|
+
let lastUrl = '';
|
|
74
|
+
for (const url of urls) {
|
|
75
|
+
lastUrl = url;
|
|
76
|
+
const r = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
77
|
+
if (r.ok) {
|
|
78
|
+
return r.json();
|
|
79
|
+
}
|
|
80
|
+
lastStatus = r.status;
|
|
59
81
|
}
|
|
60
|
-
|
|
82
|
+
throw new Error(`OpenAPI introuvable (${lastStatus}) : ${lastUrl}` +
|
|
83
|
+
(urls.length > 1
|
|
84
|
+
? ` (essayé aussi : ${urls.slice(1).join(', ')})`
|
|
85
|
+
: ''));
|
|
86
|
+
}
|
|
87
|
+
let openApiJsonPromise = null;
|
|
88
|
+
/** Une seule récupération OpenAPI en mémoire — les sessions MCP HTTP créent chacune un McpServer mais partagent le spec. */
|
|
89
|
+
export function getOpenApiJsonCached() {
|
|
90
|
+
openApiJsonPromise ??= fetchOpenApiJson();
|
|
91
|
+
return openApiJsonPromise;
|
|
61
92
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyrra/mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "MCP server for Lyrra Studio — Claude/Cursor/n8n connect via MCP; tools call your Lyrra REST API (LYRRA_API_URL). Not a REST client substitute.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
+
"mcp-server": "./dist/index.js",
|
|
8
9
|
"lyrra-mcp": "./dist/index.js",
|
|
9
10
|
"lyrra-mcp-http": "./dist/http-main.js"
|
|
10
11
|
},
|