@lenso/mcp 0.2.1 → 0.4.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/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // @bun
2
2
  // src/index.ts
3
- import { Console } from "console";
3
+ import { Console as Console2 } from "console";
4
4
  import { join, resolve } from "path";
5
- import { Writable } from "stream";
6
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { Writable as Writable2 } from "stream";
6
+ import { Server as Server3 } from "@modelcontextprotocol/sdk/server/index.js";
7
+ import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  import {
9
- CallToolRequestSchema,
10
- ErrorCode,
11
- ListToolsRequestSchema,
12
- McpError,
13
- ToolSchema
9
+ CallToolRequestSchema as CallToolRequestSchema2,
10
+ ErrorCode as ErrorCode4,
11
+ ListToolsRequestSchema as ListToolsRequestSchema2,
12
+ McpError as McpError4,
13
+ ToolSchema as ToolSchema2
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
15
  import { invoke, CliError, diagnostic } from "@lenso/cli";
16
16
  import { readApplication } from "@lenso/engine/application";
@@ -19,22 +19,881 @@ import {
19
19
  redactOperationDescription,
20
20
  validateOperations
21
21
  } from "@lenso/engine/operations";
22
- import { environmentSecrets, redact, stableJson } from "@lenso/engine/diagnostics";
23
- var serving = false;
24
- function failure(code, phase, message) {
22
+ import { environmentSecrets as environmentSecrets2, redact as redact2, stableJson } from "@lenso/engine/diagnostics";
23
+
24
+ // src/borrowed-stdio.ts
25
+ import { Console } from "console";
26
+ import { Writable } from "stream";
27
+ import { finished } from "stream/promises";
28
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
29
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
30
+ import {
31
+ ErrorCode as ErrorCode3,
32
+ McpError as McpError3,
33
+ isJSONRPCErrorResponse,
34
+ isJSONRPCRequest,
35
+ isJSONRPCResultResponse
36
+ } from "@modelcontextprotocol/sdk/types.js";
37
+ import { environmentSecrets, redact } from "@lenso/engine/diagnostics";
38
+
39
+ // src/adapter.ts
40
+ import { createAgentTools, createManageSelection } from "@lenso/manage";
41
+ import {
42
+ boundedJson
43
+ } from "@lenso/engine/operations";
44
+ import { EngineError } from "@lenso/engine/diagnostics";
45
+ import {
46
+ ErrorCode,
47
+ McpError,
48
+ ToolSchema
49
+ } from "@modelcontextprotocol/sdk/types.js";
50
+ var messages = {
51
+ "invalid-input": "Input does not satisfy the operation schema.",
52
+ "input-too-large": "Operation input exceeds the host limit.",
53
+ "output-too-large": "Operation output exceeds the host limit.",
54
+ "serialization-failed": "Operation did not return finite JSON.",
55
+ "confirmation-required": "Required confirmation is unavailable.",
56
+ "approval-required": "Required approval is unavailable.",
57
+ "missing-context-binding": "Required operation context is unavailable.",
58
+ "forbidden-operation": "Operation is not available to this caller.",
59
+ "unavailable-operation": "Operation is unavailable.",
60
+ "invocation-failed": "Operation invocation failed.",
61
+ "request-cancelled": "Request cancelled; external effects may have occurred.",
62
+ "request-timeout": "Request timed out; work may continue and external effects may have occurred.",
63
+ "adapter-busy": "Adapter is busy; no request was queued.",
64
+ "adapter-closed": "Adapter is closed.",
65
+ UNAUTHORIZED: "Authentication required.",
66
+ REAUTHENTICATION_REQUIRED: "Reauthentication required.",
67
+ FORBIDDEN: "Access denied.",
68
+ SERVICE_UNAVAILABLE: "Service unavailable.",
69
+ "resource-not-found": "Resource not found.",
70
+ "not-found": "Resource not found.",
71
+ forbidden: "Access denied.",
72
+ "permission-denied": "Access denied.",
73
+ "authorization-denied": "Access denied.",
74
+ conflict: "Operation conflicts with current state.",
75
+ "deduplication-conflict": "Operation conflicts with current state.",
76
+ "invalid-key": "Invalid operation input.",
77
+ "invalid-task": "Invalid operation input.",
78
+ "invalid-options": "Invalid operation input.",
79
+ "too-large": "Operation exceeds the service limit.",
80
+ "provider-unavailable": "Service unavailable.",
81
+ closed: "Service unavailable.",
82
+ provider: "Provider operation failed.",
83
+ unsupported: "Operation is unsupported.",
84
+ aborted: "Request cancelled; external effects may have occurred."
85
+ };
86
+ function positive(value, fallback, min = 1) {
87
+ const actual = value ?? fallback;
88
+ if (!Number.isSafeInteger(actual) || actual < min)
89
+ throw new TypeError("Invalid MCP limit.");
90
+ return actual;
91
+ }
92
+ function failure(code) {
93
+ const safe = Object.hasOwn(messages, code) ? code : "invocation-failed";
94
+ const phase = safe === "invalid-input" || safe === "input-too-large" ? "input" : safe === "output-too-large" || safe === "serialization-failed" ? "output" : "invoke";
95
+ return {
96
+ isError: true,
97
+ content: [
98
+ { type: "text", text: JSON.stringify({ code: safe, phase, message: messages[safe] }) }
99
+ ]
100
+ };
101
+ }
102
+ async function createMcpAdapter(options) {
103
+ const { running, plugins, operations, ...policies } = options;
104
+ const selection = createManageSelection({ running, plugins, operations });
105
+ try {
106
+ return await adapterForSelection(selection, policies);
107
+ } catch (error) {
108
+ selection.close();
109
+ throw error;
110
+ }
111
+ }
112
+ async function adapterForSelection(selection, options) {
113
+ const maxInput = positive(options.maxInputBytes, 256 * 1024);
114
+ const maxOutput = positive(options.maxOutputBytes, 1024 * 1024, 256);
115
+ const maxCatalog = positive(options.maxCatalogBytes, 256 * 1024);
116
+ const maxConcurrent = positive(options.maxConcurrentCalls, 4);
117
+ const timeout = positive(options.requestTimeoutMs, 30000);
118
+ if ([options.binding, options.canList, options.authorize].some((fn) => typeof fn !== "function"))
119
+ throw new TypeError("MCP requires trusted binding, catalog and invocation policies.");
120
+ const canonical = selection.createAdapter({
121
+ canList: () => true,
122
+ binding: () => {
123
+ throw new Error("Metadata adapter cannot invoke.");
124
+ },
125
+ maxOutputBytes: maxCatalog
126
+ });
127
+ const [catalog, agentTools] = await Promise.all([
128
+ canonical.catalog(),
129
+ createAgentTools(canonical)
130
+ ]);
131
+ const tools = agentTools.map((item, index) => {
132
+ const operation = catalog[index];
133
+ const tool = {
134
+ name: `operation_${index}`,
135
+ title: `${operation.pluginId}.${operation.method}`,
136
+ description: [
137
+ item.description,
138
+ ...operation.outputDescription ? [`Output: ${operation.outputDescription}`] : [],
139
+ "Cancellation signals reach host bindings. Only cooperative services interrupt; cancellation does not roll back effects."
140
+ ].join(`
141
+ `),
142
+ inputSchema: item.inputSchema,
143
+ _meta: { "lenso/operation": operation },
144
+ execution: { taskSupport: "forbidden" },
145
+ annotations: {
146
+ readOnlyHint: operation.effect === "read",
147
+ destructiveHint: operation.destructive ?? true,
148
+ idempotentHint: operation.retry === "safe",
149
+ openWorldHint: true
150
+ }
151
+ };
152
+ if (!ToolSchema.safeParse(tool).success)
153
+ throw new TypeError("Operation cannot be represented as an MCP tool.");
154
+ return tool;
155
+ });
156
+ boundedJson({ tools }, maxCatalog);
157
+ const toolByKey = new Map(catalog.map((operation, index) => [operation.key, tools[index]]));
158
+ let closed = false;
159
+ let closePromise;
160
+ const pending = new Set;
161
+ const controllers = new Set;
162
+ function tracked(request, action) {
163
+ const controller = new AbortController;
164
+ const signal = AbortSignal.any([request.signal, controller.signal]);
165
+ controllers.add(controller);
166
+ let timer;
167
+ let abort;
168
+ let timedOut = false;
169
+ const interrupted = new Promise((_, reject) => {
170
+ abort = () => reject(new EngineError({
171
+ code: timedOut ? "request-timeout" : "request-cancelled",
172
+ phase: "invoke",
173
+ message: "MCP request interrupted."
174
+ }));
175
+ signal.addEventListener("abort", abort, { once: true });
176
+ if (signal.aborted)
177
+ abort();
178
+ timer = setTimeout(() => {
179
+ timedOut = true;
180
+ controller.abort();
181
+ }, timeout);
182
+ });
183
+ const work = Promise.resolve().then(() => {
184
+ signal.throwIfAborted();
185
+ return action({ ...request, signal });
186
+ });
187
+ pending.add(work);
188
+ const cleanup = () => {
189
+ clearTimeout(timer);
190
+ signal.removeEventListener("abort", abort);
191
+ controllers.delete(controller);
192
+ pending.delete(work);
193
+ };
194
+ work.then(cleanup, cleanup);
195
+ return Promise.race([work, interrupted]);
196
+ }
197
+ return Object.freeze({
198
+ async listTools(request) {
199
+ if (closed)
200
+ throw new McpError(ErrorCode.InvalidRequest, "Adapter is closed.");
201
+ if (pending.size >= maxConcurrent)
202
+ throw new McpError(ErrorCode.InternalError, "Adapter is busy.");
203
+ try {
204
+ return await tracked(request, async (context) => {
205
+ const scoped = selection.createAdapter({
206
+ maxOutputBytes: maxCatalog,
207
+ binding: () => {
208
+ throw new Error("Metadata adapter cannot invoke.");
209
+ },
210
+ canList: async (operation) => {
211
+ const visible = await options.canList(operation, context);
212
+ context.signal.throwIfAborted();
213
+ return visible;
214
+ }
215
+ });
216
+ const visible = (await scoped.catalog()).map((operation) => toolByKey.get(operation.key));
217
+ return JSON.parse(boundedJson({ tools: visible }, maxCatalog));
218
+ });
219
+ } catch {
220
+ throw new McpError(ErrorCode.InternalError, "Tool discovery failed or was interrupted.");
221
+ }
222
+ },
223
+ async callTool(name, input, request) {
224
+ const match = /^operation_(0|[1-9][0-9]*)$/.exec(name);
225
+ if (!match || !catalog[Number(match[1])])
226
+ throw new McpError(ErrorCode.InvalidParams, "Unknown tool.");
227
+ if (closed)
228
+ return failure("adapter-closed");
229
+ if (pending.size >= maxConcurrent)
230
+ return failure("adapter-busy");
231
+ if (request.signal.aborted)
232
+ return failure("request-cancelled");
233
+ try {
234
+ boundedJson(input, maxInput);
235
+ } catch (error) {
236
+ return failure(error instanceof EngineError && error.diagnostic.code === "output-too-large" ? "input-too-large" : "invalid-input");
237
+ }
238
+ try {
239
+ const value = await tracked(request, async (context) => {
240
+ const scoped = selection.createAdapter({
241
+ maxOutputBytes: maxOutput,
242
+ canList: async (operation) => {
243
+ const visible = await options.canList(operation, context);
244
+ const allowed = visible === true && await options.authorize(operation, context) === true;
245
+ context.signal.throwIfAborted();
246
+ return allowed;
247
+ },
248
+ binding: async (operation, validated) => {
249
+ const binding = await options.binding(operation, validated, context);
250
+ context.signal.throwIfAborted();
251
+ return {
252
+ ...binding,
253
+ signal: binding.signal ? AbortSignal.any([context.signal, binding.signal]) : context.signal
254
+ };
255
+ }
256
+ });
257
+ const result = await scoped.invokeEntry(catalog[Number(match[1])].key, input);
258
+ context.signal.throwIfAborted();
259
+ return result;
260
+ });
261
+ return { content: [{ type: "text", text: boundedJson(value, maxOutput) }] };
262
+ } catch (error) {
263
+ return failure(error instanceof EngineError ? error.diagnostic.code : "invocation-failed");
264
+ }
265
+ },
266
+ close() {
267
+ if (closePromise)
268
+ return closePromise;
269
+ closed = true;
270
+ for (const controller of controllers)
271
+ controller.abort();
272
+ closePromise = Promise.allSettled(pending).then(() => {
273
+ selection.close();
274
+ });
275
+ return closePromise;
276
+ }
277
+ });
278
+ }
279
+
280
+ // src/protocol.ts
281
+ import {
282
+ CallToolRequestSchema,
283
+ CancelledNotificationSchema,
284
+ ErrorCode as ErrorCode2,
285
+ ListToolsRequestSchema,
286
+ McpError as McpError2
287
+ } from "@modelcontextprotocol/sdk/types.js";
288
+ function bindMcpServer(server, adapter, evidence) {
289
+ const cancellations = new Map;
290
+ const prepare = (requestId) => {
291
+ const controller = new AbortController;
292
+ cancellations.set(requestId, controller);
293
+ return () => {
294
+ if (cancellations.get(requestId) === controller)
295
+ cancellations.delete(requestId);
296
+ };
297
+ };
298
+ const dispatch = async (extra, action) => {
299
+ const controller = cancellations.get(extra.requestId) ?? new AbortController;
300
+ cancellations.set(extra.requestId, controller);
301
+ try {
302
+ const request = evidence(extra);
303
+ return await action({
304
+ ...request,
305
+ signal: AbortSignal.any([extra.signal, controller.signal])
306
+ });
307
+ } finally {
308
+ cancellations.delete(extra.requestId);
309
+ }
310
+ };
311
+ server.setNotificationHandler(CancelledNotificationSchema, (notification) => {
312
+ if (notification.params.requestId !== undefined)
313
+ cancellations.get(notification.params.requestId)?.abort();
314
+ });
315
+ server.setRequestHandler(ListToolsRequestSchema, (_request, extra) => dispatch(extra, (context) => adapter.listTools(context)));
316
+ server.setRequestHandler(CallToolRequestSchema, (request, extra) => {
317
+ if (request.params.task)
318
+ throw new McpError2(ErrorCode2.InvalidParams, "Task-augmented calls are not supported.");
319
+ return dispatch(extra, (context) => adapter.callTool(request.params.name, request.params.arguments ?? {}, context));
320
+ });
321
+ return {
322
+ prepare,
323
+ finish: (requestId) => {
324
+ cancellations.delete(requestId);
325
+ }
326
+ };
327
+ }
328
+
329
+ // src/borrowed-stdio.ts
330
+ var stdioOwned = false;
331
+ function acquireStdioOwnership() {
332
+ if (stdioOwned)
333
+ throw new TypeError("Only one stdio adapter may own this process.");
334
+ stdioOwned = true;
335
+ let released = false;
336
+ return () => {
337
+ if (released)
338
+ return;
339
+ released = true;
340
+ stdioOwned = false;
341
+ };
342
+ }
343
+ async function serveBorrowedStdio(options) {
344
+ const maxFrame = options.maxFrameBytes ?? 1024 * 1024;
345
+ if (!Number.isSafeInteger(maxFrame) || maxFrame < 1)
346
+ throw new TypeError("Invalid MCP limit.");
347
+ const release = acquireStdioOwnership();
348
+ const originalConsole = globalThis.console;
349
+ let identity;
350
+ const logs = new Writable({
351
+ write(chunk, _encoding, done) {
352
+ process.stderr.write(String(redact(chunk.toString(), environmentSecrets())), done);
353
+ }
354
+ });
355
+ let adapter;
356
+ let server;
357
+ let transport;
358
+ let closing = false;
359
+ let closePromise;
360
+ const close = () => {
361
+ if (closePromise)
362
+ return closePromise;
363
+ closing = true;
364
+ closePromise = Promise.resolve().then(async () => {
365
+ const errors = [];
366
+ try {
367
+ try {
368
+ await adapter?.close();
369
+ } catch (error) {
370
+ errors.push(error);
371
+ }
372
+ try {
373
+ await server?.close();
374
+ } catch (error) {
375
+ errors.push(error);
376
+ }
377
+ try {
378
+ await transport?.close();
379
+ } catch (error) {
380
+ errors.push(error);
381
+ }
382
+ } finally {
383
+ process.stdin.off("end", onEnd);
384
+ process.off("SIGINT", onEnd);
385
+ process.off("SIGTERM", onEnd);
386
+ globalThis.console = originalConsole;
387
+ try {
388
+ logs.end();
389
+ await finished(logs);
390
+ } catch (error) {
391
+ errors.push(error);
392
+ }
393
+ release();
394
+ }
395
+ if (errors.length)
396
+ throw new AggregateError(errors, "MCP close failed.");
397
+ });
398
+ return closePromise;
399
+ };
400
+ const onEnd = () => {
401
+ close().catch(() => {
402
+ process.exitCode = 1;
403
+ });
404
+ };
405
+ const context = () => {
406
+ if (closing)
407
+ throw new McpError3(ErrorCode3.InvalidRequest, "Adapter is shutting down.");
408
+ return { identity, requestId: crypto.randomUUID() };
409
+ };
410
+ try {
411
+ identity = options.identity;
412
+ globalThis.console = Object.assign(new Console({ stdout: logs, stderr: logs }), {
413
+ write(...values) {
414
+ const text = values.map((value) => typeof value === "string" ? value : Buffer.from(ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value)).toString()).join("");
415
+ logs.write(text);
416
+ return Buffer.byteLength(text);
417
+ }
418
+ });
419
+ for (const level of [
420
+ "log",
421
+ "info",
422
+ "warn",
423
+ "error",
424
+ "debug",
425
+ "dir",
426
+ "dirxml",
427
+ "table",
428
+ "trace"
429
+ ]) {
430
+ globalThis.console[level] = (...values) => {
431
+ const safe = values.map((value) => value instanceof Error ? "[Application error text omitted]" : redact(value, environmentSecrets()));
432
+ logs.write(`${safe.map((value) => typeof value === "string" ? value : JSON.stringify(value)).join(" ")}
433
+ `);
434
+ };
435
+ }
436
+ globalThis.console.assert = (condition, ...values) => {
437
+ if (!condition)
438
+ globalThis.console.error("Assertion failed:", ...values);
439
+ };
440
+ adapter = await createMcpAdapter(options);
441
+ const borrowed = adapter;
442
+ server = new Server({ name: "lenso", version: "0.2.1" }, { capabilities: { tools: {} } });
443
+ const protocol = bindMcpServer(server, borrowed, context);
444
+ server.onerror = () => {
445
+ process.stderr.write(`MCP protocol error (details omitted).
446
+ `);
447
+ };
448
+ server.onclose = onEnd;
449
+ process.stdin.once("end", onEnd);
450
+ process.on("SIGINT", onEnd);
451
+ process.on("SIGTERM", onEnd);
452
+ transport = new StdioServerTransport(process.stdin, process.stdout, {
453
+ maxBufferSize: maxFrame
454
+ });
455
+ await server.connect(transport);
456
+ const incoming = transport.onmessage;
457
+ transport.onmessage = (message) => {
458
+ if (isJSONRPCRequest(message) && (message.method === "tools/list" || message.method === "tools/call"))
459
+ protocol.prepare(message.id);
460
+ incoming?.(message);
461
+ };
462
+ const send = transport.send.bind(transport);
463
+ transport.send = async (message) => {
464
+ try {
465
+ await send(message);
466
+ } finally {
467
+ if ((isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) && message.id !== undefined)
468
+ protocol.finish(message.id);
469
+ }
470
+ };
471
+ if (process.stdin.readableEnded)
472
+ onEnd();
473
+ if (closing) {
474
+ await close();
475
+ throw new McpError3(ErrorCode3.InvalidRequest, "Adapter closed during startup.");
476
+ }
477
+ return Object.freeze({ close });
478
+ } catch (error) {
479
+ try {
480
+ await close();
481
+ } catch (closeError) {
482
+ throw new AggregateError([error, closeError], "MCP startup and close failed.", {
483
+ cause: closeError
484
+ });
485
+ }
486
+ throw error;
487
+ }
488
+ }
489
+ // src/http.ts
490
+ import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
491
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
492
+ import {
493
+ SUPPORTED_PROTOCOL_VERSIONS,
494
+ isInitializeRequest
495
+ } from "@modelcontextprotocol/sdk/types.js";
496
+ function positive2(value, fallback) {
497
+ const result = value ?? fallback;
498
+ if (!Number.isSafeInteger(result) || result < 1)
499
+ throw new TypeError("Invalid MCP limit.");
500
+ return result;
501
+ }
502
+ function trustedUrl(value) {
503
+ const url = new URL(value);
504
+ const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
505
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.hash || url.search)
506
+ throw new TypeError("MCP URLs require HTTPS (HTTP only on loopback), without credentials, query or fragment.");
507
+ return url;
508
+ }
509
+ function reply(status, code, headers) {
510
+ return Response.json({ error: code }, { status, headers: { "cache-control": "no-store", ...headers } });
511
+ }
512
+ async function readBody(request, maxBytes, signal) {
513
+ const declared = request.headers.get("content-length");
514
+ if (declared && (!/^\d+$/.test(declared) || Number(declared) > maxBytes))
515
+ throw new RangeError("Body limit.");
516
+ const reader = request.body?.getReader();
517
+ if (!reader)
518
+ throw new SyntaxError("Missing body.");
519
+ const chunks = [];
520
+ let size = 0;
521
+ const abort = () => {
522
+ reader.cancel().catch(() => {});
523
+ };
524
+ signal.addEventListener("abort", abort, { once: true });
525
+ try {
526
+ while (true) {
527
+ signal.throwIfAborted();
528
+ const { value, done } = await reader.read();
529
+ signal.throwIfAborted();
530
+ if (done)
531
+ break;
532
+ size += value.byteLength;
533
+ if (size > maxBytes)
534
+ throw new RangeError("Body limit.");
535
+ chunks.push(value);
536
+ }
537
+ const bytes = new Uint8Array(size);
538
+ let offset = 0;
539
+ for (const chunk of chunks) {
540
+ bytes.set(chunk, offset);
541
+ offset += chunk.byteLength;
542
+ }
543
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
544
+ } finally {
545
+ signal.removeEventListener("abort", abort);
546
+ reader.cancel().catch(() => {});
547
+ reader.releaseLock();
548
+ }
549
+ }
550
+ async function protocolResponse(response, maxBytes, signal) {
551
+ if (!response.body)
552
+ return response;
553
+ const reader = response.body.getReader();
554
+ const abort = () => {
555
+ reader.cancel().catch(() => {});
556
+ };
557
+ signal.addEventListener("abort", abort, { once: true });
558
+ const chunks = [];
559
+ let size = 0;
560
+ try {
561
+ while (true) {
562
+ signal.throwIfAborted();
563
+ const { value, done } = await reader.read();
564
+ signal.throwIfAborted();
565
+ if (done)
566
+ break;
567
+ size += value.byteLength;
568
+ if (size > maxBytes)
569
+ return reply(500, "response-too-large");
570
+ chunks.push(value);
571
+ }
572
+ const bytes = new Uint8Array(size);
573
+ let offset = 0;
574
+ for (const chunk of chunks) {
575
+ bytes.set(chunk, offset);
576
+ offset += chunk.byteLength;
577
+ }
578
+ const text = new TextDecoder().decode(bytes);
579
+ const frames = response.headers.get("content-type")?.startsWith("text/event-stream") ? text.split(`
580
+ `).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6)) : [text];
581
+ if (frames.length !== 1)
582
+ return reply(500, "invalid-protocol-response");
583
+ const payload = JSON.parse(frames[0]);
584
+ if (payload && typeof payload === "object" && payload.error)
585
+ payload.error = { code: payload.error.code, message: "MCP request rejected." };
586
+ const headers = new Headers(response.headers);
587
+ headers.delete("content-length");
588
+ headers.set("content-type", "application/json");
589
+ headers.set("cache-control", "no-store");
590
+ if (Buffer.byteLength(JSON.stringify(payload)) > maxBytes)
591
+ return reply(500, "response-too-large");
592
+ return Response.json(payload, { status: response.status, headers });
593
+ } finally {
594
+ signal.removeEventListener("abort", abort);
595
+ reader.cancel().catch(() => {});
596
+ reader.releaseLock();
597
+ }
598
+ }
599
+ async function createHttpMcp(options) {
600
+ const resource = trustedUrl(options.resource);
601
+ if (!options.authorizationServers.length)
602
+ throw new TypeError("An authorization server is required.");
603
+ const issuers = options.authorizationServers.map((value) => {
604
+ trustedUrl(value);
605
+ return value;
606
+ });
607
+ const origins = new Set(options.allowedOrigins.map((value) => {
608
+ const url = trustedUrl(value);
609
+ if (url.href !== `${url.origin}/`)
610
+ throw new TypeError("Allowed origins must be origins.");
611
+ return url.origin;
612
+ }));
613
+ const scopes = [...options.requiredScopes];
614
+ if (scopes.some((scope) => !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)))
615
+ throw new TypeError("Invalid OAuth scope.");
616
+ if (typeof options.verifyToken !== "function")
617
+ throw new TypeError("A trusted verifier is required.");
618
+ const metadataUrl = new URL(`/.well-known/oauth-protected-resource${resource.pathname}`, resource);
619
+ const metadata = {
620
+ resource: resource.href,
621
+ authorization_servers: issuers,
622
+ scopes_supported: scopes,
623
+ bearer_methods_supported: ["header"]
624
+ };
625
+ const maxFrame = positive2(options.maxFrameBytes, 1024 * 1024);
626
+ const maxHttp = positive2(options.maxHttpRequests, 32);
627
+ const maxSessions = positive2(options.maxSessions, 128);
628
+ const idleMs = positive2(options.sessionIdleMs, 5 * 60000);
629
+ const timeoutMs = positive2(options.requestTimeoutMs, 30000);
630
+ const maxResponse = positive2(options.maxResponseBytes, 2 * 1024 * 1024 + 4096);
631
+ if (maxResponse < 256)
632
+ throw new TypeError("MCP response budget must fit a safe error.");
633
+ if (Buffer.byteLength(JSON.stringify(metadata)) > positive2(options.maxCatalogBytes, 256 * 1024))
634
+ throw new TypeError("MCP metadata exceeds the catalog budget.");
635
+ const rate = {
636
+ requests: positive2(options.rateLimit?.requests, 120),
637
+ windowMs: positive2(options.rateLimit?.windowMs, 60000),
638
+ maxIdentities: positive2(options.rateLimit?.maxIdentities, 1024)
639
+ };
640
+ const adapter = await createMcpAdapter(options);
641
+ const sessions = new Map;
642
+ const buckets = new Map;
643
+ const active = new Set;
644
+ const controllers = new Set;
645
+ let closing = false;
646
+ let reservations = 0;
647
+ let closePromise;
648
+ const challenge = (error) => ({
649
+ "www-authenticate": `Bearer resource_metadata="${metadataUrl.href}", scope="${scopes.join(" ")}"${error ? `, error="${error}"` : ""}`
650
+ });
651
+ const retire = async (session) => {
652
+ if (session.transport.sessionId)
653
+ sessions.delete(session.transport.sessionId);
654
+ await session.server.close();
655
+ };
656
+ const sweep = async () => {
657
+ const now = Date.now();
658
+ for (const [key, bucket] of buckets)
659
+ if (bucket.reset <= now)
660
+ buckets.delete(key);
661
+ for (const session of sessions.values())
662
+ if (!session.active && now - session.lastUsed > idleMs)
663
+ await retire(session);
664
+ };
665
+ const startSession = async (owner) => {
666
+ const transport = new WebStandardStreamableHTTPServerTransport({
667
+ sessionIdGenerator: () => crypto.randomUUID(),
668
+ enableJsonResponse: false,
669
+ maxRequestBodySize: maxFrame,
670
+ keepAliveMs: 0,
671
+ onsessioninitialized: (id) => {
672
+ sessions.set(id, session);
673
+ },
674
+ onsessionclosed: () => retire(session)
675
+ });
676
+ const server = new Server2({ name: "lenso", version: "0.2.1" }, { capabilities: { tools: {} } });
677
+ const protocol = bindMcpServer(server, adapter, (extra) => {
678
+ const request = extra.authInfo?.extra?.lenso;
679
+ if (!request)
680
+ throw new Error("Missing trusted MCP request context.");
681
+ return request;
682
+ });
683
+ const session = {
684
+ owner,
685
+ server,
686
+ transport,
687
+ protocol,
688
+ ids: new Set,
689
+ lastUsed: Date.now(),
690
+ active: 0
691
+ };
692
+ try {
693
+ await server.connect(transport);
694
+ return session;
695
+ } catch (error) {
696
+ await server.close();
697
+ throw error;
698
+ }
699
+ };
700
+ const handle = async (request, controller) => {
701
+ const url = new URL(request.url);
702
+ if (url.origin !== resource.origin)
703
+ return reply(403, "invalid-host");
704
+ const host = request.headers.get("host");
705
+ if (host && host !== resource.host)
706
+ return reply(403, "invalid-host");
707
+ const origin = request.headers.get("origin");
708
+ if (origin && !origins.has(origin))
709
+ return reply(403, "invalid-origin");
710
+ if (url.pathname === metadataUrl.pathname && request.method === "GET")
711
+ return Response.json(metadata, { headers: { "cache-control": "no-store" } });
712
+ if (url.pathname !== resource.pathname || url.search)
713
+ return reply(404, "not-found");
714
+ const credential = request.headers.get("authorization");
715
+ const match = credential?.match(/^Bearer ([\x21-\x7E]+)$/i);
716
+ if (!match || match[1].length > 8192)
717
+ return reply(401, "unauthorized", challenge());
718
+ let identity;
719
+ try {
720
+ const verified = await options.verifyToken(match[1], controller.signal);
721
+ if (!verified || typeof verified.subject !== "string" || !verified.subject || verified.subject.length > 512 || typeof verified.tenant !== "string" || !verified.tenant || verified.tenant.length > 512 || typeof verified.issuer !== "string" || !issuers.includes(verified.issuer) || !Array.isArray(verified.audience) || !verified.audience.includes(resource.href) || !Array.isArray(verified.scopes) || verified.scopes.some((scope) => typeof scope !== "string") || !Number.isFinite(verified.expiresAt) || verified.expiresAt <= Date.now() / 1000)
722
+ return reply(401, "invalid-token", challenge("invalid_token"));
723
+ identity = Object.freeze({
724
+ ...verified,
725
+ audience: Object.freeze([...verified.audience]),
726
+ scopes: Object.freeze([...verified.scopes])
727
+ });
728
+ } catch {
729
+ return reply(401, "invalid-token", challenge("invalid_token"));
730
+ }
731
+ if (scopes.some((scope) => !identity.scopes.includes(scope)))
732
+ return reply(403, "insufficient-scope", challenge("insufficient_scope"));
733
+ controller.signal.throwIfAborted();
734
+ await sweep();
735
+ const owner = JSON.stringify([
736
+ identity.issuer,
737
+ identity.subject,
738
+ identity.tenant,
739
+ resource.href
740
+ ]);
741
+ let bucket = buckets.get(owner);
742
+ if (!bucket) {
743
+ if (buckets.size >= rate.maxIdentities)
744
+ return reply(429, "rate-limit-capacity");
745
+ bucket = { reset: Date.now() + rate.windowMs, count: 0 };
746
+ buckets.set(owner, bucket);
747
+ }
748
+ if (++bucket.count > rate.requests)
749
+ return reply(429, "rate-limited", {
750
+ "retry-after": String(Math.ceil((bucket.reset - Date.now()) / 1000))
751
+ });
752
+ const sessionId = request.headers.get("mcp-session-id");
753
+ let session = sessionId ? sessions.get(sessionId) : undefined;
754
+ if (sessionId && (!session || session.owner !== owner))
755
+ return reply(404, "session-not-found");
756
+ if (request.method === "GET") {
757
+ const version = request.headers.get("mcp-protocol-version");
758
+ if (version && !SUPPORTED_PROTOCOL_VERSIONS.includes(version))
759
+ return reply(400, "unsupported-protocol");
760
+ return reply(405, "standalone-stream-not-supported", { allow: "POST, DELETE" });
761
+ }
762
+ if (request.method !== "POST" && request.method !== "DELETE")
763
+ return reply(405, "method-not-allowed", { allow: "POST, DELETE" });
764
+ let body;
765
+ if (request.method === "POST") {
766
+ try {
767
+ body = await readBody(request, maxFrame, controller.signal);
768
+ } catch (error) {
769
+ return reply(error instanceof RangeError ? 413 : 400, error instanceof RangeError ? "request-too-large" : "invalid-request");
770
+ }
771
+ if (!body || typeof body !== "object" || Array.isArray(body))
772
+ return reply(400, "invalid-request");
773
+ }
774
+ if (!session) {
775
+ controller.signal.throwIfAborted();
776
+ if (!isInitializeRequest(body))
777
+ return reply(400, "session-required");
778
+ if (sessions.size + reservations >= maxSessions)
779
+ return reply(503, "session-capacity");
780
+ reservations++;
781
+ try {
782
+ session = await startSession(owner);
783
+ } catch (error) {
784
+ reservations--;
785
+ throw error;
786
+ }
787
+ }
788
+ const id = body && typeof body === "object" && "id" in body ? JSON.stringify(body.id) : undefined;
789
+ if (id && session.ids.has(id))
790
+ return reply(409, "duplicate-request");
791
+ if (id)
792
+ session.ids.add(id);
793
+ session.active++;
794
+ session.lastUsed = Date.now();
795
+ const selected = session;
796
+ const release = body && typeof body === "object" && "method" in body && (body.method === "tools/call" || body.method === "tools/list") && "id" in body && (typeof body.id === "string" || typeof body.id === "number") ? session.protocol.prepare(body.id) : () => {};
797
+ const abort = () => {
798
+ retire(selected).catch(() => {});
799
+ };
800
+ controller.signal.addEventListener("abort", abort, { once: true });
801
+ try {
802
+ controller.signal.throwIfAborted();
803
+ const transportResponse = session.transport.handleRequest(request, {
804
+ parsedBody: body,
805
+ authInfo: {
806
+ token: "",
807
+ clientId: identity.subject,
808
+ scopes: [...identity.scopes],
809
+ expiresAt: identity.expiresAt,
810
+ resource,
811
+ extra: { lenso: { identity, requestId: crypto.randomUUID() } }
812
+ }
813
+ }).then((response) => protocolResponse(response, maxResponse, controller.signal));
814
+ let interrupt = () => {};
815
+ const interrupted = new Promise((resolve) => {
816
+ interrupt = () => resolve(reply(504, "request-interrupted"));
817
+ controller.signal.addEventListener("abort", interrupt, { once: true });
818
+ if (controller.signal.aborted)
819
+ interrupt();
820
+ });
821
+ let response;
822
+ try {
823
+ response = await Promise.race([transportResponse, interrupted]);
824
+ } finally {
825
+ controller.signal.removeEventListener("abort", interrupt);
826
+ }
827
+ if (!session.transport.sessionId)
828
+ await retire(session);
829
+ return response;
830
+ } finally {
831
+ release();
832
+ controller.signal.removeEventListener("abort", abort);
833
+ if (id)
834
+ selected.ids.delete(id);
835
+ selected.active--;
836
+ selected.lastUsed = Date.now();
837
+ if (!sessionId) {
838
+ reservations--;
839
+ if (!selected.transport.sessionId || controller.signal.aborted)
840
+ await retire(selected);
841
+ }
842
+ }
843
+ };
844
+ const fetch = (request) => {
845
+ if (closing)
846
+ return Promise.resolve(reply(503, "adapter-closed"));
847
+ if (active.size >= maxHttp)
848
+ return Promise.resolve(reply(503, "adapter-busy"));
849
+ const controller = new AbortController;
850
+ controllers.add(controller);
851
+ let timer;
852
+ const interrupted = new Promise((resolve) => {
853
+ controller.signal.addEventListener("abort", () => resolve(reply(504, "request-interrupted")), { once: true });
854
+ timer = setTimeout(() => controller.abort(), timeoutMs + 1000);
855
+ });
856
+ const execution = handle(request, controller).catch(() => reply(500, "mcp-failed"));
857
+ active.add(execution);
858
+ execution.finally(() => {
859
+ active.delete(execution);
860
+ controllers.delete(controller);
861
+ clearTimeout(timer);
862
+ });
863
+ return Promise.race([execution, interrupted]);
864
+ };
865
+ const close = () => {
866
+ if (closePromise)
867
+ return closePromise;
868
+ closing = true;
869
+ closePromise = (async () => {
870
+ for (const controller of controllers)
871
+ controller.abort();
872
+ await adapter.close();
873
+ await Promise.all([...sessions.values()].map(retire));
874
+ await Promise.all(active);
875
+ buckets.clear();
876
+ })();
877
+ return closePromise;
878
+ };
879
+ return Object.freeze({ fetch, close });
880
+ }
881
+
882
+ // src/index.ts
883
+ function failure2(code, phase, message) {
25
884
  return new CliError({ code, phase, message });
26
885
  }
27
886
  function limit(value, fallback) {
28
887
  const bytes = value ?? fallback;
29
888
  if (!Number.isSafeInteger(bytes) || bytes < 1)
30
- throw failure("invalid-arguments", "arguments", "Limits must be positive safe integers.");
889
+ throw failure2("invalid-arguments", "arguments", "Limits must be positive safe integers.");
31
890
  return bytes;
32
891
  }
33
892
  function result(data, maxBytes) {
34
893
  stableJson(data);
35
- const text = stableJson(redact(data, environmentSecrets()));
894
+ const text = stableJson(redact2(data, environmentSecrets2()));
36
895
  if (Buffer.byteLength(text) > maxBytes)
37
- throw failure("output-too-large", "output", "Operation output exceeds the host limit.");
896
+ throw failure2("output-too-large", "output", "Operation output exceeds the host limit.");
38
897
  return { content: [{ type: "text", text }] };
39
898
  }
40
899
  var oversizedDiagnostic = stableJson({
@@ -46,14 +905,14 @@ function errorResult(error, maxBytes) {
46
905
  let text;
47
906
  try {
48
907
  const detail = diagnostic(error);
49
- text = stableJson(redact(detail, environmentSecrets()));
908
+ text = stableJson(redact2(detail, environmentSecrets2()));
50
909
  if (Buffer.byteLength(text) > maxBytes) {
51
- text = stableJson(redact({
910
+ text = stableJson(redact2({
52
911
  code: detail.code,
53
912
  phase: detail.phase,
54
913
  message: "Diagnostic truncated to the host output limit.",
55
914
  truncated: true
56
- }, environmentSecrets()));
915
+ }, environmentSecrets2()));
57
916
  }
58
917
  } catch {
59
918
  text = stableJson({
@@ -67,22 +926,25 @@ function errorResult(error, maxBytes) {
67
926
  return { isError: true, content: [{ type: "text", text }] };
68
927
  }
69
928
  async function serveStdio(options) {
70
- if (serving)
71
- throw failure("invalid-arguments", "arguments", "Only one stdio adapter may own this process.");
72
929
  const root = resolve(options.root);
73
930
  const maxInput = limit(options.maxInputBytes, 256 * 1024);
74
931
  const maxOutput = limit(options.maxOutputBytes, 1024 * 1024);
75
932
  if (maxOutput < Buffer.byteLength(oversizedDiagnostic))
76
- throw failure("invalid-arguments", "arguments", "Output limit must fit a bounded diagnostic.");
933
+ throw failure2("invalid-arguments", "arguments", "Output limit must fit a bounded diagnostic.");
77
934
  const maxFrame = limit(options.maxFrameBytes, 1024 * 1024);
78
- serving = true;
935
+ let releaseStdio;
936
+ try {
937
+ releaseStdio = acquireStdioOwnership();
938
+ } catch {
939
+ throw failure2("invalid-arguments", "arguments", "Only one stdio adapter may own this process.");
940
+ }
79
941
  const originalConsole = globalThis.console;
80
- const logs = new Writable({
942
+ const logs = new Writable2({
81
943
  write(chunk, _encoding, done) {
82
- process.stderr.write(String(redact(chunk.toString(), environmentSecrets())), done);
944
+ process.stderr.write(String(redact2(chunk.toString(), environmentSecrets2())), done);
83
945
  }
84
946
  });
85
- globalThis.console = Object.assign(new Console({ stdout: logs, stderr: logs }), {
947
+ globalThis.console = Object.assign(new Console2({ stdout: logs, stderr: logs }), {
86
948
  write(...values) {
87
949
  const text = values.map((value) => typeof value === "string" ? value : Buffer.from(ArrayBuffer.isView(value) ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength) : new Uint8Array(value)).toString()).join("");
88
950
  logs.write(text);
@@ -101,7 +963,7 @@ async function serveStdio(options) {
101
963
  "trace"
102
964
  ]) {
103
965
  globalThis.console[level] = (...values) => {
104
- const safe = values.map((value) => value instanceof Error ? "[Application error text omitted]" : redact(value, environmentSecrets()));
966
+ const safe = values.map((value) => value instanceof Error ? "[Application error text omitted]" : redact2(value, environmentSecrets2()));
105
967
  logs.write(`${safe.map((value) => typeof value === "string" ? value : JSON.stringify(value)).join(" ")}
106
968
  `);
107
969
  };
@@ -116,7 +978,7 @@ async function serveStdio(options) {
116
978
  let closePromise;
117
979
  const restore = () => {
118
980
  globalThis.console = originalConsole;
119
- serving = false;
981
+ releaseStdio();
120
982
  };
121
983
  const close = () => {
122
984
  if (closePromise)
@@ -149,15 +1011,15 @@ async function serveStdio(options) {
149
1011
  for (const allowed of options.allow) {
150
1012
  const key = stableJson([allowed.pluginId, allowed.method]);
151
1013
  if (seen.has(key))
152
- throw failure("duplicate-operation", "discovery", "Duplicate MCP allowlist entry.");
1014
+ throw failure2("duplicate-operation", "discovery", "Duplicate MCP allowlist entry.");
153
1015
  seen.add(key);
154
1016
  const ref = selected.find((item) => item.plugin.id === allowed.pluginId && item.method === allowed.method);
155
1017
  const operation = ref ? redactOperationDescription(describeOperation(ref, configPath)) : undefined;
156
1018
  if (!operation)
157
- throw failure("unknown-operation", "discovery", "MCP allowlist operation is not declared.");
1019
+ throw failure2("unknown-operation", "discovery", "MCP allowlist operation is not declared.");
158
1020
  const schema = operation.inputSchema;
159
1021
  if (!schema || schema.type !== "object")
160
- throw failure("unsupported-input-schema", "discovery", "MCP tools require a convertible object input schema.");
1022
+ throw failure2("unsupported-input-schema", "discovery", "MCP tools require a convertible object input schema.");
161
1023
  stableJson(schema);
162
1024
  const name = `operation_${tools.length}`;
163
1025
  const tool = {
@@ -179,38 +1041,38 @@ async function serveStdio(options) {
179
1041
  openWorldHint: true
180
1042
  }
181
1043
  };
182
- if (!ToolSchema.safeParse(tool).success)
183
- throw failure("unsupported-input-schema", "discovery", "Operation schema cannot be represented as an MCP tool.");
1044
+ if (!ToolSchema2.safeParse(tool).success)
1045
+ throw failure2("unsupported-input-schema", "discovery", "Operation schema cannot be represented as an MCP tool.");
184
1046
  tools.push(tool);
185
1047
  bindings.set(name, { pluginId: allowed.pluginId, method: allowed.method });
186
1048
  }
187
- server = new Server({ name: "lenso", version: "0.1.0" }, { capabilities: { tools: {} } });
188
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
189
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1049
+ server = new Server3({ name: "lenso", version: "0.1.0" }, { capabilities: { tools: {} } });
1050
+ server.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools }));
1051
+ server.setRequestHandler(CallToolRequestSchema2, async (request, extra) => {
190
1052
  const binding = bindings.get(request.params.name);
191
1053
  if (!binding)
192
- throw new McpError(ErrorCode.InvalidParams, "Tool is not allowlisted.", {
1054
+ throw new McpError4(ErrorCode4.InvalidParams, "Tool is not allowlisted.", {
193
1055
  code: "unknown-operation"
194
1056
  });
195
1057
  if (request.params.task)
196
- throw new McpError(ErrorCode.InvalidParams, "Task-augmented calls are not supported.");
1058
+ throw new McpError4(ErrorCode4.InvalidParams, "Task-augmented calls are not supported.");
197
1059
  if (closing)
198
- return errorResult(failure("adapter-closed", "invoke", "Adapter is shutting down."), maxOutput);
1060
+ return errorResult(failure2("adapter-closed", "invoke", "Adapter is shutting down."), maxOutput);
199
1061
  if (active)
200
- return errorResult(failure("adapter-busy", "invoke", "One operation is already running; no request was queued."), maxOutput);
1062
+ return errorResult(failure2("adapter-busy", "invoke", "One operation is already running; no request was queued."), maxOutput);
201
1063
  const execute = async () => {
202
1064
  try {
203
1065
  if (extra.signal.aborted)
204
- throw failure("request-cancelled", "invoke", "Request cancelled before invocation.");
1066
+ throw failure2("request-cancelled", "invoke", "Request cancelled before invocation.");
205
1067
  const input = request.params.arguments ?? {};
206
1068
  if (Buffer.byteLength(stableJson(input)) > maxInput)
207
- throw failure("input-too-large", "input", "Operation input exceeds the host limit.");
1069
+ throw failure2("input-too-large", "input", "Operation input exceeds the host limit.");
208
1070
  const data = await invoke({ ...app, operations: selected, operationBinding: undefined }, binding.pluginId, binding.method, input, async (operation, validatedInput, running) => ({
209
1071
  ...options.binding ? await options.binding(operation, validatedInput, running) : {},
210
1072
  maxOutputBytes: maxOutput
211
1073
  }));
212
1074
  if (extra.signal.aborted)
213
- throw failure("request-cancelled", "invoke", "Request cancelled; operation may have completed. Cleanup was awaited.");
1075
+ throw failure2("request-cancelled", "invoke", "Request cancelled; operation may have completed. Cleanup was awaited.");
214
1076
  return result(data, maxOutput);
215
1077
  } catch (error) {
216
1078
  return errorResult(error, maxOutput);
@@ -231,7 +1093,7 @@ async function serveStdio(options) {
231
1093
  process.stdin.once("end", onEnd);
232
1094
  process.on("SIGINT", onEnd);
233
1095
  process.on("SIGTERM", onEnd);
234
- await server.connect(new StdioServerTransport(process.stdin, process.stdout, { maxBufferSize: maxFrame }));
1096
+ await server.connect(new StdioServerTransport2(process.stdin, process.stdout, { maxBufferSize: maxFrame }));
235
1097
  return { close };
236
1098
  } catch (error) {
237
1099
  try {
@@ -245,5 +1107,8 @@ async function serveStdio(options) {
245
1107
  }
246
1108
  }
247
1109
  export {
1110
+ createHttpMcp,
1111
+ createMcpAdapter,
1112
+ serveBorrowedStdio,
248
1113
  serveStdio
249
1114
  };