@webda/mcp 0.0.0-stage → 4.0.0-beta.3

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.
@@ -0,0 +1,125 @@
1
+ const SCHEME = "webda://";
2
+ /**
3
+ * Registry of models exposed as `webda://{Model}/{pk...}` resources
4
+ */
5
+ export class ResourceRegistry {
6
+ filter;
7
+ entries = [];
8
+ /**
9
+ * @param filter - model short ids to expose, or ["*"]
10
+ */
11
+ constructor(filter) {
12
+ this.filter = filter;
13
+ }
14
+ /**
15
+ * Rebuild from operations: every visible `{Model}.Get` with `context.pkFields`
16
+ * becomes a model; its `.Query` operation shares the same `context.model`.
17
+ * @param ops - visible operations
18
+ */
19
+ build(ops) {
20
+ this.entries = [];
21
+ for (const [id, op] of Object.entries(ops)) {
22
+ const match = /^(.+)\.Get$/.exec(id);
23
+ const pkFields = op.context?.pkFields;
24
+ if (!match || op.hidden || op.mcp === false || !Array.isArray(pkFields) || pkFields.length === 0) {
25
+ continue;
26
+ }
27
+ const name = match[1];
28
+ if (!this.filter.includes("*") && !this.filter.includes(name)) {
29
+ continue;
30
+ }
31
+ const query = Object.entries(ops).find(([qid, q]) => qid.endsWith(".Query") && !q.hidden && q.mcp !== false && q.context?.model !== undefined && q.context.model === op.context.model);
32
+ this.entries.push({ name, getOperationId: id, queryOperationId: query?.[0], pkFields: pkFields.map(String) });
33
+ }
34
+ this.entries.sort((a, b) => a.name.localeCompare(b.name));
35
+ }
36
+ /**
37
+ * @returns exposed models, sorted by name
38
+ */
39
+ models() {
40
+ return this.entries;
41
+ }
42
+ /**
43
+ * @returns MCP resource templates
44
+ */
45
+ templates() {
46
+ return this.entries.map(m => ({
47
+ uriTemplate: `${SCHEME}${m.name}/${m.pkFields.map(f => `{${f}}`).join("/")}`,
48
+ name: m.name,
49
+ mimeType: "application/json"
50
+ }));
51
+ }
52
+ /**
53
+ * @param model - the model
54
+ * @param record - an instance (or key object)
55
+ * @returns its resource URI
56
+ */
57
+ uriFor(model, record) {
58
+ return `${SCHEME}${model.name}/${model.pkFields.map(f => encodeURIComponent(String(record[f]))).join("/")}`;
59
+ }
60
+ /**
61
+ * @param uri - a resource URI
62
+ * @returns the model and decoded key, or undefined when it does not match
63
+ */
64
+ parse(uri) {
65
+ if (!uri.startsWith(SCHEME)) {
66
+ return undefined;
67
+ }
68
+ const [name, ...segments] = uri.substring(SCHEME.length).split("/");
69
+ const model = this.entries.find(m => m.name === name);
70
+ if (!model || segments.length !== model.pkFields.length || segments.some(s => s === "")) {
71
+ return undefined;
72
+ }
73
+ const key = {};
74
+ try {
75
+ model.pkFields.forEach((field, i) => (key[field] = decodeURIComponent(segments[i])));
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ return { model, key };
81
+ }
82
+ }
83
+ /**
84
+ * @param cursor - model and continuation token
85
+ * @param cursor.model - model short id
86
+ * @param cursor.token - continuation token of that model's query
87
+ * @returns opaque cursor
88
+ */
89
+ export function encodeCursor(cursor) {
90
+ return Buffer.from(JSON.stringify(cursor)).toString("base64url");
91
+ }
92
+ /**
93
+ * @param cursor - opaque cursor
94
+ * @returns the decoded cursor, or undefined when absent or invalid
95
+ */
96
+ export function decodeCursor(cursor) {
97
+ if (!cursor) {
98
+ return undefined;
99
+ }
100
+ try {
101
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString());
102
+ if (typeof parsed?.model !== "string") {
103
+ return undefined;
104
+ }
105
+ if (parsed.token !== undefined && typeof parsed.token !== "string") {
106
+ return undefined;
107
+ }
108
+ return { model: parsed.model, ...(parsed.token !== undefined && { token: parsed.token }) };
109
+ }
110
+ catch {
111
+ return undefined;
112
+ }
113
+ }
114
+ /**
115
+ * @param token - continuation token
116
+ * @returns the WebdaQL query for one page
117
+ */
118
+ export function queryFor(token) {
119
+ if (token === undefined) {
120
+ return "LIMIT 100";
121
+ }
122
+ const escaped = token.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
123
+ return `LIMIT 100 OFFSET "${escaped}"`;
124
+ }
125
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Loose JSON schema shape used by MCP tool definitions
3
+ */
4
+ export type JsonSchema = Record<string, any>;
5
+ /**
6
+ * Resolve a schema by name (e.g. an application schema id like "WebdaSample/Post")
7
+ */
8
+ export type SchemaResolver = (name: string) => JsonSchema | undefined;
9
+ /**
10
+ * Return a self-contained copy of a JSON schema with every `$ref` inlined.
11
+ *
12
+ * MCP clients expect self-contained tool schemas. Local refs are looked up in
13
+ * the enclosing schema's `definitions`/`$defs`; anything else is resolved by
14
+ * name through `resolve` (Webda's dangling `#/definitions/<modelId>` refs).
15
+ * Cycles and unresolved refs become `{}` (accept anything).
16
+ *
17
+ * @param schema - the schema to inline
18
+ * @param resolve - resolver for refs not defined locally
19
+ * @param maxDepth - maximum number of nested refs to follow
20
+ * @returns a new schema without `$ref`, `definitions`, `$defs`, `$schema` or `$id`
21
+ */
22
+ export declare function inlineSchema(schema: JsonSchema, resolve: SchemaResolver, maxDepth?: number): JsonSchema;
23
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;AAItE;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,GAAE,MAAU,GAAG,UAAU,CAE1G"}
package/lib/schema.js ADDED
@@ -0,0 +1,69 @@
1
+ const REF_PREFIXES = ["#/definitions/", "#/$defs/"];
2
+ /**
3
+ * Return a self-contained copy of a JSON schema with every `$ref` inlined.
4
+ *
5
+ * MCP clients expect self-contained tool schemas. Local refs are looked up in
6
+ * the enclosing schema's `definitions`/`$defs`; anything else is resolved by
7
+ * name through `resolve` (Webda's dangling `#/definitions/<modelId>` refs).
8
+ * Cycles and unresolved refs become `{}` (accept anything).
9
+ *
10
+ * @param schema - the schema to inline
11
+ * @param resolve - resolver for refs not defined locally
12
+ * @param maxDepth - maximum number of nested refs to follow
13
+ * @returns a new schema without `$ref`, `definitions`, `$defs`, `$schema` or `$id`
14
+ */
15
+ export function inlineSchema(schema, resolve, maxDepth = 8) {
16
+ return inlineNode(schema, resolve, [schema?.definitions ?? schema?.$defs ?? {}], [], maxDepth);
17
+ }
18
+ /**
19
+ * Inline one schema node
20
+ * @param node - current node
21
+ * @param resolve - external resolver
22
+ * @param defs - stack of definition scopes, innermost last
23
+ * @param seen - ref names currently being expanded (cycle guard)
24
+ * @param maxDepth - remaining ref depth
25
+ * @returns the inlined node
26
+ */
27
+ function inlineNode(node, resolve, defs, seen, maxDepth) {
28
+ if (Array.isArray(node)) {
29
+ return node.map(item => inlineNode(item, resolve, defs, seen, maxDepth));
30
+ }
31
+ if (node === null || typeof node !== "object") {
32
+ return node;
33
+ }
34
+ if (typeof node.$ref === "string") {
35
+ const prefix = REF_PREFIXES.find(p => node.$ref.startsWith(p));
36
+ let name = prefix ? node.$ref.substring(prefix.length) : node.$ref;
37
+ if (prefix) {
38
+ try {
39
+ name = decodeURIComponent(name);
40
+ }
41
+ catch {
42
+ // malformed escape: fall back to the raw name
43
+ }
44
+ }
45
+ if (seen.includes(name) || seen.length >= maxDepth) {
46
+ return {};
47
+ }
48
+ let target;
49
+ for (let i = defs.length - 1; i >= 0 && !target; i--) {
50
+ target = defs[i][name];
51
+ }
52
+ target ??= resolve(name);
53
+ if (!target) {
54
+ return {};
55
+ }
56
+ const { $ref, ...siblings } = node;
57
+ const scopes = target.definitions || target.$defs ? [...defs, target.definitions ?? target.$defs] : defs;
58
+ return { ...inlineNode(target, resolve, scopes, [...seen, name], maxDepth), ...inlineNode(siblings, resolve, defs, seen, maxDepth) };
59
+ }
60
+ const out = {};
61
+ for (const [key, value] of Object.entries(node)) {
62
+ if (key === "definitions" || key === "$defs" || key === "$schema" || key === "$id") {
63
+ continue;
64
+ }
65
+ out[key] = inlineNode(value, resolve, defs, seen, maxDepth);
66
+ }
67
+ return out;
68
+ }
69
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import { Session } from "@webda/core";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
4
+ import { ResourceRegistry } from "./resources.js";
5
+ import { ToolRegistry } from "./tools.js";
6
+ /**
7
+ * JSON-RPC error code for a missing resource (MCP spec)
8
+ */
9
+ export declare const RESOURCE_NOT_FOUND = -32002;
10
+ /**
11
+ * Number of tools per tools/list page
12
+ */
13
+ export declare const TOOLS_PAGE_SIZE = 100;
14
+ /**
15
+ * Dependencies of an MCP server instance
16
+ */
17
+ export interface McpServerOptions {
18
+ /**
19
+ * Server name and version announced at initialize
20
+ */
21
+ info: {
22
+ name: string;
23
+ version: string;
24
+ };
25
+ /**
26
+ * Tools exposed
27
+ */
28
+ tools: ToolRegistry;
29
+ /**
30
+ * Resources exposed; undefined disables resources
31
+ */
32
+ resources?: ResourceRegistry;
33
+ /**
34
+ * Truncation threshold for tool and resource text
35
+ */
36
+ maxOutputBytes: number;
37
+ /**
38
+ * Caller session for a request
39
+ */
40
+ getSession: (extra: {
41
+ authInfo?: AuthInfo;
42
+ }) => Session;
43
+ /**
44
+ * Hook run before tools/resources requests (tool list change detection)
45
+ */
46
+ beforeRequest?: () => void;
47
+ }
48
+ /**
49
+ * Create an SDK server wired to Webda operations
50
+ * @param options - registries, session resolution and limits
51
+ * @returns an unconnected SDK Server
52
+ */
53
+ export declare function createMcpServer(options: McpServerOptions): Server;
54
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,OAAO,EAAc,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gDAAgD,CAAC;AAa/E,OAAO,EAAwC,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;GAEG;AACH,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC;;OAEG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;OAEG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,UAAU,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,EAAE,QAAQ,CAAA;KAAE,KAAK,OAAO,CAAC;IACxD;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AA6BD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CA4HjE"}
package/lib/server.js ADDED
@@ -0,0 +1,167 @@
1
+ import { canCallOperation, WebdaError } from "@webda/core";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { useLog } from "@webda/workout";
5
+ import { errorToToolResult, runOperation, sessionContext, toToolResult, truncateUtf8 } from "./invoke.js";
6
+ import { decodeCursor, encodeCursor, queryFor } from "./resources.js";
7
+ /**
8
+ * JSON-RPC error code for a missing resource (MCP spec)
9
+ */
10
+ export const RESOURCE_NOT_FOUND = -32002;
11
+ /**
12
+ * Number of tools per tools/list page
13
+ */
14
+ export const TOOLS_PAGE_SIZE = 100;
15
+ /**
16
+ * Map an error thrown by a resource operation to an MCP error.
17
+ *
18
+ * With a `uri` (resources/read) a NotFound becomes RESOURCE_NOT_FOUND; without
19
+ * one (resources/list) a NotFound is treated like any other client error.
20
+ * Other 4xx messages are forwarded, anything else is logged and masked.
21
+ * @param err - thrown error
22
+ * @param uri - requested resource uri, when reading one resource
23
+ * @returns the error to throw
24
+ */
25
+ function toResourceError(err, uri) {
26
+ if (err instanceof McpError) {
27
+ return err;
28
+ }
29
+ if (uri !== undefined && err instanceof WebdaError.NotFound) {
30
+ return new McpError(RESOURCE_NOT_FOUND, "Resource not found", { uri });
31
+ }
32
+ if (err instanceof WebdaError.CodeError) {
33
+ const code = err.getResponseCode();
34
+ if (code >= 400 && code < 500) {
35
+ return new McpError(ErrorCode.InternalError, err.message);
36
+ }
37
+ }
38
+ useLog("ERROR", "MCP resource operation failed", err);
39
+ return new McpError(ErrorCode.InternalError, "Internal error");
40
+ }
41
+ /**
42
+ * Create an SDK server wired to Webda operations
43
+ * @param options - registries, session resolution and limits
44
+ * @returns an unconnected SDK Server
45
+ */
46
+ export function createMcpServer(options) {
47
+ const { tools, resources } = options;
48
+ const server = new Server(options.info, {
49
+ capabilities: {
50
+ tools: { listChanged: true },
51
+ ...(resources ? { resources: { listChanged: false, subscribe: false } } : {})
52
+ }
53
+ });
54
+ const allowed = (session, operationId) => canCallOperation(sessionContext(session), operationId);
55
+ server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
56
+ options.beforeRequest?.();
57
+ const session = options.getSession(extra);
58
+ const visible = tools.list().filter(e => allowed(session, e.operationId));
59
+ const offset = Number(request.params?.cursor ?? 0) || 0;
60
+ const page = visible.slice(offset, offset + TOOLS_PAGE_SIZE);
61
+ const next = offset + TOOLS_PAGE_SIZE < visible.length ? String(offset + TOOLS_PAGE_SIZE) : undefined;
62
+ return { tools: page.map(e => e.tool), ...(next ? { nextCursor: next } : {}) };
63
+ });
64
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
65
+ options.beforeRequest?.();
66
+ const session = options.getSession(extra);
67
+ const entry = tools.get(request.params.name);
68
+ if (!entry || !allowed(session, entry.operationId)) {
69
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
70
+ }
71
+ const args = request.params.arguments ?? {};
72
+ const progressToken = request.params._meta?.progressToken;
73
+ const pending = [];
74
+ try {
75
+ const { value, streamed } = await runOperation(entry.operationId, {
76
+ session,
77
+ input: entry.wrapped ? args.value : args,
78
+ signal: extra.signal,
79
+ onChunk: progressToken === undefined
80
+ ? undefined
81
+ : (chunk, index) => {
82
+ const total = chunk?.total;
83
+ const sent = extra
84
+ .sendNotification({
85
+ method: "notifications/progress",
86
+ params: {
87
+ progressToken,
88
+ progress: index,
89
+ ...(typeof total === "number" ? { total } : {}),
90
+ message: JSON.stringify(chunk)
91
+ }
92
+ })
93
+ .catch(err => useLog("WARN", "MCP progress notification failed", err));
94
+ pending.push(sent);
95
+ }
96
+ });
97
+ await Promise.allSettled(pending);
98
+ return toToolResult(entry, value, streamed, options.maxOutputBytes);
99
+ }
100
+ catch (err) {
101
+ await Promise.allSettled(pending);
102
+ return errorToToolResult(err);
103
+ }
104
+ });
105
+ if (!resources) {
106
+ return server;
107
+ }
108
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
109
+ options.beforeRequest?.();
110
+ return { resourceTemplates: resources.templates() };
111
+ });
112
+ server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
113
+ options.beforeRequest?.();
114
+ const match = resources.parse(request.params.uri);
115
+ if (!match) {
116
+ throw new McpError(RESOURCE_NOT_FOUND, "Resource not found", { uri: request.params.uri });
117
+ }
118
+ try {
119
+ const { value } = await runOperation(match.model.getOperationId, { session: options.getSession(extra), input: match.key });
120
+ const cut = truncateUtf8(JSON.stringify(value ?? null), options.maxOutputBytes);
121
+ const text = cut.truncated
122
+ ? `${cut.text}\n[output truncated: ${cut.bytes} bytes exceeds maxOutputBytes ${options.maxOutputBytes}]`
123
+ : cut.text;
124
+ return { contents: [{ uri: request.params.uri, mimeType: "application/json", text }] };
125
+ }
126
+ catch (err) {
127
+ throw toResourceError(err, request.params.uri);
128
+ }
129
+ });
130
+ server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => {
131
+ options.beforeRequest?.();
132
+ const session = options.getSession(extra);
133
+ const listable = resources.models().filter(m => m.queryOperationId && allowed(session, m.queryOperationId));
134
+ const cursor = decodeCursor(request.params?.cursor);
135
+ const index = cursor ? listable.findIndex(m => m.name === cursor.model) : 0;
136
+ if (index < 0) {
137
+ throw new McpError(ErrorCode.InvalidParams, "Invalid cursor");
138
+ }
139
+ if (index >= listable.length) {
140
+ return { resources: [] };
141
+ }
142
+ const model = listable[index];
143
+ let value;
144
+ try {
145
+ ({ value } = await runOperation(model.queryOperationId, { session, input: { query: queryFor(cursor?.token) } }));
146
+ }
147
+ catch (err) {
148
+ throw toResourceError(err);
149
+ }
150
+ const result = (value ?? {});
151
+ const items = (result.results ?? []).map(record => ({
152
+ uri: resources.uriFor(model, record),
153
+ name: `${model.name} ${model.pkFields.map(f => String(record[f])).join("/")}`,
154
+ mimeType: "application/json"
155
+ }));
156
+ let nextCursor;
157
+ if (result.continuationToken) {
158
+ nextCursor = encodeCursor({ model: model.name, token: result.continuationToken });
159
+ }
160
+ else if (index + 1 < listable.length) {
161
+ nextCursor = encodeCursor({ model: listable[index + 1].name });
162
+ }
163
+ return { resources: items, ...(nextCursor ? { nextCursor } : {}) };
164
+ });
165
+ return server;
166
+ }
167
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ /**
3
+ * An open MCP HTTP session
4
+ */
5
+ export interface McpSessionEntry {
6
+ /**
7
+ * Mcp-Session-Id
8
+ */
9
+ id: string;
10
+ /**
11
+ * SDK server bound to this session
12
+ */
13
+ server: Server;
14
+ /**
15
+ * SDK transport bound to this session
16
+ */
17
+ transport: {
18
+ close(): Promise<void>;
19
+ };
20
+ /**
21
+ * User who initialized the session (undefined = anonymous)
22
+ */
23
+ userId?: string;
24
+ /**
25
+ * Last activity timestamp (ms)
26
+ */
27
+ lastSeen: number;
28
+ }
29
+ /**
30
+ * In-memory MCP sessions with idle eviction
31
+ */
32
+ export declare class McpSessionManager {
33
+ protected timeoutMs: number;
34
+ protected now: () => number;
35
+ protected sessions: Map<string, McpSessionEntry>;
36
+ /**
37
+ * @param timeoutMs - idle time before eviction
38
+ * @param now - clock (injectable for tests)
39
+ */
40
+ constructor(timeoutMs: number, now?: () => number);
41
+ /**
42
+ * @param entry - session to track
43
+ */
44
+ add(entry: McpSessionEntry): void;
45
+ /**
46
+ * Find a session and refresh its activity
47
+ * @param id - session id
48
+ * @returns the session or undefined
49
+ */
50
+ get(id: string): McpSessionEntry | undefined;
51
+ /**
52
+ * @param id - session id to forget (does not close it)
53
+ */
54
+ remove(id: string): void;
55
+ /**
56
+ * @returns all sessions
57
+ */
58
+ all(): McpSessionEntry[];
59
+ /**
60
+ * Close and remove sessions idle longer than the timeout
61
+ * @returns number of evicted sessions
62
+ */
63
+ evictIdle(): Promise<number>;
64
+ /**
65
+ * Close and remove the least recently used session
66
+ * @returns the evicted session id, or undefined when there is none
67
+ */
68
+ evictOldest(): Promise<string | undefined>;
69
+ /**
70
+ * Close every session
71
+ */
72
+ closeAll(): Promise<void>;
73
+ /**
74
+ * @param entry - session to close and remove
75
+ */
76
+ protected close(entry: McpSessionEntry): Promise<void>;
77
+ }
78
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGxE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IACX;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,SAAS,EAAE;QAAE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IACtC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAQ1B,SAAS,CAAC,SAAS,EAAE,MAAM;IAC3B,SAAS,CAAC,GAAG,EAAE,MAAM,MAAM;IAR7B,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAa;IAE7D;;;OAGG;IACH,YACY,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAM,MAAyB,EAC5C;IAEJ;;OAEG;IACH,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAGhC;IAED;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAM3C;IAED;;OAEG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEvB;IAED;;OAEG;IACH,GAAG,IAAI,eAAe,EAAE,CAEvB;IAED;;;OAGG;IACG,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAOjC;IAED;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAY/C;IAED;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAI9B;IAED;;OAEG;IACH,UAAgB,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3D;CACF"}
@@ -0,0 +1,99 @@
1
+ import { useLog } from "@webda/workout";
2
+ /**
3
+ * In-memory MCP sessions with idle eviction
4
+ */
5
+ export class McpSessionManager {
6
+ timeoutMs;
7
+ now;
8
+ sessions = new Map();
9
+ /**
10
+ * @param timeoutMs - idle time before eviction
11
+ * @param now - clock (injectable for tests)
12
+ */
13
+ constructor(timeoutMs, now = () => Date.now()) {
14
+ this.timeoutMs = timeoutMs;
15
+ this.now = now;
16
+ }
17
+ /**
18
+ * @param entry - session to track
19
+ */
20
+ add(entry) {
21
+ entry.lastSeen = this.now();
22
+ this.sessions.set(entry.id, entry);
23
+ }
24
+ /**
25
+ * Find a session and refresh its activity
26
+ * @param id - session id
27
+ * @returns the session or undefined
28
+ */
29
+ get(id) {
30
+ const entry = this.sessions.get(id);
31
+ if (entry) {
32
+ entry.lastSeen = this.now();
33
+ }
34
+ return entry;
35
+ }
36
+ /**
37
+ * @param id - session id to forget (does not close it)
38
+ */
39
+ remove(id) {
40
+ this.sessions.delete(id);
41
+ }
42
+ /**
43
+ * @returns all sessions
44
+ */
45
+ all() {
46
+ return [...this.sessions.values()];
47
+ }
48
+ /**
49
+ * Close and remove sessions idle longer than the timeout
50
+ * @returns number of evicted sessions
51
+ */
52
+ async evictIdle() {
53
+ const limit = this.now() - this.timeoutMs;
54
+ const idle = this.all().filter(e => e.lastSeen < limit);
55
+ for (const entry of idle) {
56
+ await this.close(entry);
57
+ }
58
+ return idle.length;
59
+ }
60
+ /**
61
+ * Close and remove the least recently used session
62
+ * @returns the evicted session id, or undefined when there is none
63
+ */
64
+ async evictOldest() {
65
+ let oldest;
66
+ for (const entry of this.sessions.values()) {
67
+ if (!oldest || entry.lastSeen < oldest.lastSeen) {
68
+ oldest = entry;
69
+ }
70
+ }
71
+ if (!oldest) {
72
+ return undefined;
73
+ }
74
+ await this.close(oldest);
75
+ return oldest.id;
76
+ }
77
+ /**
78
+ * Close every session
79
+ */
80
+ async closeAll() {
81
+ for (const entry of this.all()) {
82
+ await this.close(entry);
83
+ }
84
+ }
85
+ /**
86
+ * @param entry - session to close and remove
87
+ */
88
+ async close(entry) {
89
+ this.sessions.delete(entry.id);
90
+ try {
91
+ await entry.transport.close();
92
+ await entry.server.close();
93
+ }
94
+ catch (err) {
95
+ useLog("WARN", "Failed to close MCP session", entry.id, err);
96
+ }
97
+ }
98
+ }
99
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}