@mcp-ts/sdk 1.4.0 → 1.5.1

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.
Files changed (80) hide show
  1. package/README.md +20 -27
  2. package/dist/adapters/agui-adapter.d.mts +16 -0
  3. package/dist/adapters/agui-adapter.d.ts +16 -0
  4. package/dist/adapters/agui-adapter.js +185 -0
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +185 -0
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +2 -0
  9. package/dist/adapters/agui-middleware.d.ts +2 -0
  10. package/dist/adapters/agui-middleware.js.map +1 -1
  11. package/dist/adapters/agui-middleware.mjs.map +1 -1
  12. package/dist/adapters/ai-adapter.d.mts +21 -0
  13. package/dist/adapters/ai-adapter.d.ts +21 -0
  14. package/dist/adapters/ai-adapter.js +175 -0
  15. package/dist/adapters/ai-adapter.js.map +1 -1
  16. package/dist/adapters/ai-adapter.mjs +175 -0
  17. package/dist/adapters/ai-adapter.mjs.map +1 -1
  18. package/dist/adapters/langchain-adapter.d.mts +16 -0
  19. package/dist/adapters/langchain-adapter.d.ts +16 -0
  20. package/dist/adapters/langchain-adapter.js +179 -0
  21. package/dist/adapters/langchain-adapter.js.map +1 -1
  22. package/dist/adapters/langchain-adapter.mjs +179 -0
  23. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  24. package/dist/client/index.d.mts +2 -2
  25. package/dist/client/index.d.ts +2 -2
  26. package/dist/client/react.d.mts +94 -8
  27. package/dist/client/react.d.ts +94 -8
  28. package/dist/client/react.js +364 -26
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +358 -27
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +4 -4
  33. package/dist/client/vue.d.ts +4 -4
  34. package/dist/client/vue.js +11 -2
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +11 -2
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/{index-CQr9q0bF.d.mts → index-DcYfpY3H.d.mts} +1 -1
  39. package/dist/{index-nE_7Io0I.d.ts → index-GfC_eNEv.d.ts} +1 -1
  40. package/dist/index.d.mts +4 -3
  41. package/dist/index.d.ts +4 -3
  42. package/dist/index.js +938 -12
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +923 -13
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/server/index.d.mts +2 -2
  47. package/dist/server/index.d.ts +2 -2
  48. package/dist/server/index.js +58 -12
  49. package/dist/server/index.js.map +1 -1
  50. package/dist/server/index.mjs +58 -12
  51. package/dist/server/index.mjs.map +1 -1
  52. package/dist/shared/index.d.mts +86 -4
  53. package/dist/shared/index.d.ts +86 -4
  54. package/dist/shared/index.js +874 -0
  55. package/dist/shared/index.js.map +1 -1
  56. package/dist/shared/index.mjs +865 -1
  57. package/dist/shared/index.mjs.map +1 -1
  58. package/dist/tool-router-Bo8qZbsD.d.ts +325 -0
  59. package/dist/tool-router-XnWVxPzv.d.mts +325 -0
  60. package/dist/{types-CW6lghof.d.mts → types-CfCoIsWI.d.mts} +27 -1
  61. package/dist/{types-CW6lghof.d.ts → types-CfCoIsWI.d.ts} +27 -1
  62. package/package.json +3 -2
  63. package/src/adapters/agui-adapter.ts +79 -0
  64. package/src/adapters/ai-adapter.ts +75 -0
  65. package/src/adapters/langchain-adapter.ts +74 -0
  66. package/src/client/react/index.ts +16 -0
  67. package/src/client/react/oauth-popup.tsx +446 -0
  68. package/src/client/react/use-mcp-apps.tsx +50 -32
  69. package/src/client/react/use-mcp.ts +36 -3
  70. package/src/client/vue/use-mcp.ts +38 -3
  71. package/src/server/handlers/sse-handler.ts +39 -0
  72. package/src/server/index.ts +2 -0
  73. package/src/server/mcp/oauth-client.ts +35 -15
  74. package/src/shared/index.ts +36 -0
  75. package/src/shared/meta-tools.ts +387 -0
  76. package/src/shared/schema-compressor.ts +124 -0
  77. package/src/shared/tool-index.ts +499 -0
  78. package/src/shared/tool-router.ts +469 -0
  79. package/src/shared/types.ts +30 -0
  80. package/supabase/migrations/20260421010000_add_session_cleanup_cron.sql +32 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * ToolRouter — Middleware layer for intelligent MCP tool selection.
3
+ *
4
+ * Sits between your AI framework adapter and MultiSessionClient to reduce
5
+ * context window usage. Supports three strategies:
6
+ *
7
+ * • `all` — Pass through every tool (backward-compatible default)
8
+ * • `search` — Expose only meta-tools; LLM discovers tools on-demand
9
+ * • `groups` — Expose tools from active groups only
10
+ *
11
+ * Inspired by Anthropic's `defer_loading` + `tool_search_tool` pattern.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { ToolRouter } from '@mcp-ts/sdk/shared';
16
+ * import { AIAdapter } from '@mcp-ts/sdk/adapters/ai';
17
+ *
18
+ * const router = new ToolRouter(multiSessionClient, {
19
+ * strategy: 'search',
20
+ * maxTools: 5,
21
+ * });
22
+ *
23
+ * const tools = await AIAdapter.getTools(multiSessionClient, { toolRouter: router });
24
+ * ```
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+
29
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
30
+ import type { ToolClient, ToolClientProvider } from './types.js';
31
+ import { ToolIndex, type IndexedTool, type ToolSummary, type EmbedFn } from './tool-index.js';
32
+ import { SchemaCompressor, type CompactTool } from './schema-compressor.js';
33
+ import {
34
+ createSearchToolDefinition,
35
+ createRegexSearchToolDefinition,
36
+ createGetSchemaToolDefinition,
37
+ createExecuteToolDefinition,
38
+ } from './meta-tools.js';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Types
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export type ToolRouterStrategy = 'all' | 'search' | 'groups';
45
+
46
+ export interface ToolRouterOptions {
47
+ /**
48
+ * Strategy for tool selection.
49
+ *
50
+ * • `all` — Expose all tools (default, backward-compatible)
51
+ * • `search` — Expose only meta-tools; LLM discovers real tools via search
52
+ * • `groups` — Expose only tools from active groups
53
+ *
54
+ * @default 'all'
55
+ */
56
+ strategy?: ToolRouterStrategy;
57
+
58
+ /**
59
+ * Maximum tools to expose to the LLM at once.
60
+ * Only applies to `groups` strategy and search results.
61
+ * @default 40
62
+ */
63
+ maxTools?: number;
64
+
65
+ /**
66
+ * Tool groups configuration — map of group name to tool names.
67
+ * When not provided, groups are auto-generated from server names.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * groups: {
72
+ * database: ['query_db', 'list_tables', 'describe_table'],
73
+ * github: ['create_pr', 'list_issues', 'search_code'],
74
+ * }
75
+ * ```
76
+ */
77
+ groups?: Record<string, string[]>;
78
+
79
+ /**
80
+ * Active groups (when `strategy='groups'`).
81
+ * Only tools in these groups are exposed. Empty = all groups active.
82
+ */
83
+ activeGroups?: string[];
84
+
85
+ /**
86
+ * Whether to use compact schemas (name + description + parameterHint only, no inputSchema).
87
+ * Reduces token usage but requires 2-turn flow: LLM picks tool → get schema → call.
88
+ * @default false
89
+ */
90
+ compactSchemas?: boolean;
91
+
92
+ /**
93
+ * Optional embedding function for semantic search.
94
+ * When not provided, keyword TF-IDF matching is used.
95
+ */
96
+ embedFn?: EmbedFn;
97
+
98
+ /**
99
+ * Weight of keyword score vs embedding score (0–1).
100
+ * Only relevant when `embedFn` is provided.
101
+ * @default 0.4
102
+ */
103
+ keywordWeight?: number;
104
+ }
105
+
106
+ /** Information about a tool group. */
107
+ export interface ToolGroupInfo {
108
+ tools: string[];
109
+ active: boolean;
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Client Input Types
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Accepted client input for ToolRouter.
118
+ * Pass a `ToolClientProvider` (e.g. MultiSessionClient), or an array of `ToolClient` instances.
119
+ */
120
+ export type ToolRouterClientInput = ToolClientProvider | ToolClient[];
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // ToolRouter
124
+ // ---------------------------------------------------------------------------
125
+
126
+ export class ToolRouter {
127
+ private index: ToolIndex;
128
+ private allTools: IndexedTool[] = [];
129
+ private groupsMap = new Map<string, ToolGroupInfo>();
130
+ private strategy: ToolRouterStrategy;
131
+ private maxTools: number;
132
+ private compactSchemas: boolean;
133
+ private activeGroups: Set<string>;
134
+ private customGroups?: Record<string, string[]>;
135
+ private initialized = false;
136
+
137
+ constructor(
138
+ private client: ToolRouterClientInput,
139
+ private options: ToolRouterOptions = {}
140
+ ) {
141
+ this.strategy = options.strategy ?? 'all';
142
+ this.maxTools = options.maxTools ?? 40;
143
+ this.compactSchemas = options.compactSchemas ?? false;
144
+ this.activeGroups = new Set(options.activeGroups ?? []);
145
+ this.customGroups = options.groups;
146
+
147
+ this.index = new ToolIndex({
148
+ embedFn: options.embedFn,
149
+ keywordWeight: options.keywordWeight,
150
+ });
151
+ }
152
+
153
+ // -----------------------------------------------------------------------
154
+ // Core Public API
155
+ // -----------------------------------------------------------------------
156
+
157
+ /**
158
+ * Get tools filtered by the current strategy.
159
+ * This is the main method adapters should call.
160
+ *
161
+ * - `all` → returns all tools (unchanged behavior)
162
+ * - `search` → returns only meta-tools (mcp_search_tool_bm25, mcp_get_tool_schema, mcp_execute_tool)
163
+ * - `groups` → returns tools from active groups only
164
+ */
165
+ async getFilteredTools(): Promise<Tool[]> {
166
+ await this.ensureInitialized();
167
+
168
+ switch (this.strategy) {
169
+ case 'search':
170
+ return this.getMetaToolDefinitions();
171
+
172
+ case 'groups':
173
+ return this.getGroupFilteredTools();
174
+
175
+ case 'all':
176
+ default:
177
+ if (this.compactSchemas) {
178
+ // Return tools with inputSchema stripped
179
+ return this.allTools.map((t) => {
180
+ const compact = SchemaCompressor.toCompact(t);
181
+ return {
182
+ name: compact.name,
183
+ description:
184
+ (compact.description ?? '') +
185
+ (compact.parameterHint ? ` Parameters: ${compact.parameterHint}` : ''),
186
+ inputSchema: { type: 'object' as const, properties: {} },
187
+ };
188
+ });
189
+ }
190
+ return [...this.allTools];
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Search tools by natural-language query.
196
+ * Works regardless of strategy.
197
+ */
198
+ async searchTools(query: string, topK?: number): Promise<ToolSummary[]> {
199
+ await this.ensureInitialized();
200
+ return this.index.search(query, topK ?? this.maxTools);
201
+ }
202
+
203
+ /**
204
+ * Search tools by regex pattern.
205
+ * Matches against name, description, and parameter metadata.
206
+ */
207
+ async searchToolsRegex(pattern: string, topK?: number): Promise<ToolSummary[]> {
208
+ await this.ensureInitialized();
209
+ return this.index.searchRegex(pattern, topK ?? this.maxTools);
210
+ }
211
+
212
+ /**
213
+ * Get the full tool definition by name.
214
+ * If tool name is ambiguous, use namespace to specify the server.
215
+ */
216
+ getToolSchema(toolName: string, namespace?: string): IndexedTool | undefined {
217
+ const matches = this.index.getTool(toolName, namespace);
218
+
219
+ if (matches.length === 0) return undefined;
220
+
221
+ if (matches.length > 1) {
222
+ const servers = matches.map((m) => m.serverName).join(', ');
223
+ throw new Error(
224
+ `Tool "${toolName}" is provided by multiple servers: [${servers}]. ` +
225
+ `Please specify the desired "serverName" as a namespace.`
226
+ );
227
+ }
228
+
229
+ return matches[0];
230
+ }
231
+
232
+ /**
233
+ * Get compact (schema-less) summaries for all tools.
234
+ */
235
+ getCompactTools(): CompactTool[] {
236
+ return SchemaCompressor.compactAll(this.allTools);
237
+ }
238
+
239
+ // -----------------------------------------------------------------------
240
+ // Group Management
241
+ // -----------------------------------------------------------------------
242
+
243
+ /** Get all available groups with their tool lists and active status. */
244
+ getGroups(): Map<string, ToolGroupInfo> {
245
+ return new Map(this.groupsMap);
246
+ }
247
+
248
+ /** Activate specific groups. Pass empty array to activate all. */
249
+ setActiveGroups(groups: string[]): void {
250
+ this.activeGroups = new Set(groups);
251
+ // Update groupsMap active flags
252
+ for (const [name, info] of this.groupsMap) {
253
+ info.active = this.activeGroups.size === 0 || this.activeGroups.has(name);
254
+ }
255
+ }
256
+
257
+ /** Get the names of currently active groups. */
258
+ getActiveGroups(): string[] {
259
+ return [...this.activeGroups];
260
+ }
261
+
262
+ // -----------------------------------------------------------------------
263
+ // Stats & Introspection
264
+ // -----------------------------------------------------------------------
265
+
266
+ /** Total token cost of all tools if loaded without filtering. */
267
+ getTotalTokenCost(): number {
268
+ return this.index.getTotalTokenCost();
269
+ }
270
+
271
+ /** Estimate token cost of the currently filtered tool set. */
272
+ async getFilteredTokenCost(): Promise<number> {
273
+ const tools = await this.getFilteredTools();
274
+ let total = 0;
275
+ for (const tool of tools) {
276
+ total += ToolIndex.estimateTokens(tool);
277
+ }
278
+ return total;
279
+ }
280
+
281
+ /** Get compression stats showing savings from current strategy. */
282
+ getCompressionStats() {
283
+ return SchemaCompressor.estimateSavings(this.allTools);
284
+ }
285
+
286
+ /** Number of total indexed tools. */
287
+ get totalToolCount(): number {
288
+ return this.allTools.length;
289
+ }
290
+
291
+ /** Change strategy at runtime. */
292
+ setStrategy(strategy: ToolRouterStrategy): void {
293
+ this.strategy = strategy;
294
+ }
295
+
296
+ /**
297
+ * Force a re-index of tools from all connected clients.
298
+ * Call this after adding/removing MCP server connections.
299
+ */
300
+ async refresh(): Promise<void> {
301
+ this.initialized = false;
302
+ await this.ensureInitialized();
303
+ }
304
+
305
+ /**
306
+ * Execute a tool by routing to the correct MCP client.
307
+ * Used by the `mcp_execute_tool` meta-tool to proxy tool calls.
308
+ */
309
+ async callTool(
310
+ toolName: string,
311
+ args: Record<string, unknown>,
312
+ namespace?: string
313
+ ): Promise<any> {
314
+ await this.ensureInitialized();
315
+
316
+ const indexedTool = this.getToolSchema(toolName, namespace);
317
+ if (!indexedTool) {
318
+ throw new Error(
319
+ `Tool "${toolName}" not found${
320
+ namespace ? ` on server "${namespace}"` : ''
321
+ }. Use mcp_search_tool_bm25 or mcp_search_tool_regex to discover available tools.`
322
+ );
323
+ }
324
+
325
+ const clients = this.getClients();
326
+ const targetClient =
327
+ clients.find(
328
+ (c) =>
329
+ typeof c.getSessionId === 'function' &&
330
+ c.getSessionId() === indexedTool.sessionId
331
+ ) ?? clients.find((c) => c.isConnected());
332
+
333
+ if (!targetClient) {
334
+ throw new Error(`No connected client found for tool "${toolName}"`);
335
+ }
336
+
337
+ return await targetClient.callTool(toolName, args);
338
+ }
339
+
340
+ // -----------------------------------------------------------------------
341
+ // Internals
342
+ // -----------------------------------------------------------------------
343
+
344
+ /** Lazy initialization — fetches tools from all connected clients. */
345
+ private async ensureInitialized(): Promise<void> {
346
+ if (this.initialized) return;
347
+
348
+ this.allTools = await this.fetchAllTools();
349
+ await this.index.buildIndex(this.allTools);
350
+ this.buildGroups();
351
+ this.initialized = true;
352
+ }
353
+
354
+ /** Fetch tools from all connected MCP clients. */
355
+ private async fetchAllTools(): Promise<IndexedTool[]> {
356
+ const clients = this.getClients();
357
+ const result: IndexedTool[] = [];
358
+
359
+ for (const client of clients) {
360
+ if (!client.isConnected()) continue;
361
+
362
+ try {
363
+ const { tools } = await client.listTools();
364
+ const serverId =
365
+ typeof client.getServerId === 'function' ? client.getServerId() ?? 'unknown' : 'unknown';
366
+ const serverName =
367
+ (typeof client.getServerName === 'function' ? client.getServerName() : undefined) ??
368
+ serverId;
369
+ const sessionId =
370
+ typeof client.getSessionId === 'function' ? client.getSessionId() ?? 'unknown' : 'unknown';
371
+
372
+ for (const tool of tools) {
373
+ result.push({
374
+ ...tool,
375
+ serverName: serverName,
376
+ sessionId,
377
+ });
378
+ }
379
+ } catch (err) {
380
+ console.warn('[ToolRouter] Failed to fetch tools from client:', err);
381
+ }
382
+ }
383
+
384
+ return result;
385
+ }
386
+
387
+ /** Resolve the client input to a flat array of ToolClient instances. */
388
+ private getClients(): ToolClient[] {
389
+ if (Array.isArray(this.client)) {
390
+ return this.client;
391
+ }
392
+ if (typeof (this.client as ToolClientProvider).getClients === 'function') {
393
+ return (this.client as ToolClientProvider).getClients();
394
+ }
395
+ // Single client
396
+ return [this.client as unknown as ToolClient];
397
+ }
398
+
399
+ /** Build group map from custom config or auto-detect from server names. */
400
+ private buildGroups(): void {
401
+ this.groupsMap.clear();
402
+
403
+ if (this.customGroups) {
404
+ // Explicit groups
405
+ for (const [name, tools] of Object.entries(this.customGroups)) {
406
+ this.groupsMap.set(name, {
407
+ tools,
408
+ active: this.activeGroups.size === 0 || this.activeGroups.has(name),
409
+ });
410
+ }
411
+ } else {
412
+ // Auto-group by server name
413
+ const serverTools = new Map<string, string[]>();
414
+ for (const tool of this.allTools) {
415
+ const group = tool.serverName;
416
+ if (!serverTools.has(group)) {
417
+ serverTools.set(group, []);
418
+ }
419
+ serverTools.get(group)!.push(tool.name);
420
+ }
421
+
422
+ for (const [serverName, tools] of serverTools) {
423
+ this.groupsMap.set(serverName, {
424
+ tools,
425
+ active: this.activeGroups.size === 0 || this.activeGroups.has(serverName),
426
+ });
427
+ }
428
+ }
429
+ }
430
+
431
+ /** Return only tools belonging to currently active groups. */
432
+ private getGroupFilteredTools(): Tool[] {
433
+ const activeToolNames = new Set<string>();
434
+ for (const [, info] of this.groupsMap) {
435
+ if (info.active) {
436
+ for (const name of info.tools) {
437
+ activeToolNames.add(name);
438
+ }
439
+ }
440
+ }
441
+
442
+ const filtered = this.allTools.filter((t) => activeToolNames.has(t.name));
443
+
444
+ if (this.compactSchemas) {
445
+ return filtered.slice(0, this.maxTools).map((t) => {
446
+ const compact = SchemaCompressor.toCompact(t);
447
+ return {
448
+ name: compact.name,
449
+ description:
450
+ (compact.description ?? '') +
451
+ (compact.parameterHint ? ` Parameters: ${compact.parameterHint}` : ''),
452
+ inputSchema: { type: 'object' as const, properties: {} },
453
+ };
454
+ });
455
+ }
456
+
457
+ return filtered.slice(0, this.maxTools);
458
+ }
459
+
460
+ /** The 4 meta-tool definitions exposed in `search` strategy. */
461
+ private getMetaToolDefinitions(): Tool[] {
462
+ return [
463
+ createSearchToolDefinition(),
464
+ createRegexSearchToolDefinition(),
465
+ createGetSchemaToolDefinition(),
466
+ createExecuteToolDefinition(),
467
+ ];
468
+ }
469
+ }
@@ -4,6 +4,36 @@
4
4
 
5
5
  import { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
6
 
7
+ // ---------------------------------------------------------------------------
8
+ // Core Capability Interfaces
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * A client that can list and execute MCP tools.
13
+ *
14
+ * This is the structural interface that `ToolRouter`, adapters, and other
15
+ * consumers use to interact with any MCP client implementation.
16
+ * Both `MCPClient` and `createMcpClient()` satisfy this interface.
17
+ */
18
+ export interface ToolClient {
19
+ isConnected(): boolean;
20
+ listTools(): Promise<{ tools: Tool[] }>;
21
+ callTool(name: string, args: Record<string, unknown>): Promise<any>;
22
+ getServerId?(): string | undefined;
23
+ getServerName?(): string | undefined;
24
+ getSessionId?(): string;
25
+ }
26
+
27
+ /**
28
+ * A provider that manages multiple `ToolClient` instances.
29
+ *
30
+ * `MultiSessionClient` satisfies this interface. Pass it directly
31
+ * to `ToolRouter` or adapters to aggregate tools from all connected servers.
32
+ */
33
+ export interface ToolClientProvider {
34
+ getClients(): ToolClient[];
35
+ }
36
+
7
37
  // Connect API types
8
38
  export interface ConnectRequest {
9
39
  serverUrl: string;
@@ -0,0 +1,32 @@
1
+ -- Enable the pg_cron extension (available on all Supabase plans).
2
+ -- This is idempotent and safe to run multiple times.
3
+ CREATE EXTENSION IF NOT EXISTS pg_cron;
4
+
5
+ -- ─────────────────────────────────────────────────────────────────────────────
6
+ -- Stage 1: Short-term Transient Purge (every 5 minutes)
7
+ -- ─────────────────────────────────────────────────────────────────────────────
8
+ -- Targets sessions that are NOT active (failed connections, abandoned OAuth
9
+ -- flows, mid-flow errors) whose TTL has expired. Active sessions are explicitly
10
+ -- excluded from this sweep to preserve automation credentials.
11
+ --
12
+ -- The idx_mcp_sessions_expires_at index ensures this is a fast indexed scan.
13
+ SELECT cron.schedule(
14
+ 'cleanup-transient-sessions',
15
+ '*/5 * * * *',
16
+ $$DELETE FROM public.mcp_sessions WHERE expires_at < now() AND active IS NOT TRUE;$$
17
+ );
18
+
19
+ -- ─────────────────────────────────────────────────────────────────────────────
20
+ -- Stage 2: Long-term Dormancy Eviction (daily at midnight UTC)
21
+ -- ─────────────────────────────────────────────────────────────────────────────
22
+ -- Safety net for sessions that were successfully established (active = true)
23
+ -- but have been completely untouched for 30+ days. This prevents "active"
24
+ -- records from persisting indefinitely if they are genuinely abandoned.
25
+ SELECT cron.schedule(
26
+ 'cleanup-dormant-sessions',
27
+ '0 0 * * *',
28
+ $$DELETE FROM public.mcp_sessions WHERE active = true AND updated_at < now() - interval '30 days';$$
29
+ );
30
+
31
+ -- Add a comment on the extension for visibility in Supabase Dashboard
32
+ COMMENT ON EXTENSION pg_cron IS 'Automated Session Lifecycle Management.';