mcp-orbit 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/dist/__tests__/helpers/test-server.d.ts +2 -0
  4. package/dist/__tests__/helpers/test-server.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/test-server.js +27 -0
  6. package/dist/__tests__/helpers/test-server.js.map +1 -0
  7. package/dist/cli.d.ts +23 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +56 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/clients/mcp-http.d.ts +36 -0
  12. package/dist/clients/mcp-http.d.ts.map +1 -0
  13. package/dist/clients/mcp-http.js +148 -0
  14. package/dist/clients/mcp-http.js.map +1 -0
  15. package/dist/clients/mcp-stdio.d.ts +38 -0
  16. package/dist/clients/mcp-stdio.d.ts.map +1 -0
  17. package/dist/clients/mcp-stdio.js +164 -0
  18. package/dist/clients/mcp-stdio.js.map +1 -0
  19. package/dist/clients/types.d.ts +104 -0
  20. package/dist/clients/types.d.ts.map +1 -0
  21. package/dist/clients/types.js +8 -0
  22. package/dist/clients/types.js.map +1 -0
  23. package/dist/core/prompt-registry.d.ts +56 -0
  24. package/dist/core/prompt-registry.d.ts.map +1 -0
  25. package/dist/core/prompt-registry.js +100 -0
  26. package/dist/core/prompt-registry.js.map +1 -0
  27. package/dist/core/resource-registry.d.ts +79 -0
  28. package/dist/core/resource-registry.d.ts.map +1 -0
  29. package/dist/core/resource-registry.js +135 -0
  30. package/dist/core/resource-registry.js.map +1 -0
  31. package/dist/core/resource-uri-templates.d.ts +64 -0
  32. package/dist/core/resource-uri-templates.d.ts.map +1 -0
  33. package/dist/core/resource-uri-templates.js +168 -0
  34. package/dist/core/resource-uri-templates.js.map +1 -0
  35. package/dist/core/server-http.d.ts +15 -0
  36. package/dist/core/server-http.d.ts.map +1 -0
  37. package/dist/core/server-http.js +302 -0
  38. package/dist/core/server-http.js.map +1 -0
  39. package/dist/core/server-stdio.d.ts +8 -0
  40. package/dist/core/server-stdio.d.ts.map +1 -0
  41. package/dist/core/server-stdio.js +15 -0
  42. package/dist/core/server-stdio.js.map +1 -0
  43. package/dist/core/server.d.ts +29 -0
  44. package/dist/core/server.d.ts.map +1 -0
  45. package/dist/core/server.js +265 -0
  46. package/dist/core/server.js.map +1 -0
  47. package/dist/core/types.d.ts +265 -0
  48. package/dist/core/types.d.ts.map +1 -0
  49. package/dist/core/types.js +9 -0
  50. package/dist/core/types.js.map +1 -0
  51. package/dist/index.d.ts +28 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +26 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/utils/dynamic-resource-manager.d.ts +115 -0
  56. package/dist/utils/dynamic-resource-manager.d.ts.map +1 -0
  57. package/dist/utils/dynamic-resource-manager.js +460 -0
  58. package/dist/utils/dynamic-resource-manager.js.map +1 -0
  59. package/dist/utils/http-client.d.ts +29 -0
  60. package/dist/utils/http-client.d.ts.map +1 -0
  61. package/dist/utils/http-client.js +59 -0
  62. package/dist/utils/http-client.js.map +1 -0
  63. package/dist/utils/logger.d.ts +25 -0
  64. package/dist/utils/logger.d.ts.map +1 -0
  65. package/dist/utils/logger.js +105 -0
  66. package/dist/utils/logger.js.map +1 -0
  67. package/dist/utils/zod-to-mcp-schema.d.ts +42 -0
  68. package/dist/utils/zod-to-mcp-schema.d.ts.map +1 -0
  69. package/dist/utils/zod-to-mcp-schema.js +87 -0
  70. package/dist/utils/zod-to-mcp-schema.js.map +1 -0
  71. package/package.json +57 -0
  72. package/src/__tests__/helpers/test-server.ts +31 -0
  73. package/src/__tests__/plugin-system.basic.test.ts +137 -0
  74. package/src/__tests__/server.basic.test.ts +37 -0
  75. package/src/__tests__/stdio-roundtrip.basic.test.ts +67 -0
  76. package/src/__tests__/tool-registry.basic.test.ts +114 -0
  77. package/src/__tests__/zod-schema.basic.test.ts +105 -0
  78. package/src/cli.ts +58 -0
  79. package/src/clients/mcp-http.ts +192 -0
  80. package/src/clients/mcp-stdio.ts +209 -0
  81. package/src/clients/types.ts +136 -0
  82. package/src/core/prompt-registry.ts +114 -0
  83. package/src/core/resource-registry.ts +166 -0
  84. package/src/core/resource-uri-templates.ts +216 -0
  85. package/src/core/server-http.ts +407 -0
  86. package/src/core/server-stdio.ts +20 -0
  87. package/src/core/server.ts +320 -0
  88. package/src/core/types.ts +312 -0
  89. package/src/index.ts +92 -0
  90. package/src/utils/dynamic-resource-manager.ts +581 -0
  91. package/src/utils/http-client.ts +86 -0
  92. package/src/utils/logger.ts +138 -0
  93. package/src/utils/zod-to-mcp-schema.ts +127 -0
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * mcp-orbit — Public API
3
+ *
4
+ * Everything a tool package needs to build its own MCP server.
5
+ */
6
+
7
+ // ── Server ────────────────────────────────────────────────────────────────────
8
+ export {
9
+ startServer,
10
+ registerTool,
11
+ registerPlugin,
12
+ executeTool,
13
+ createMCPServer,
14
+ getTool,
15
+ getToolCount,
16
+ getToolDefinitions,
17
+ } from "./core/server.js";
18
+ export type {ServerConfig, TransportMode} from "./core/server.js";
19
+
20
+ export {startHttpServer} from "./core/server-http.js";
21
+ export {startStdioServer} from "./core/server-stdio.js";
22
+
23
+ // ── Types ─────────────────────────────────────────────────────────────────────
24
+ export type {
25
+ Tool,
26
+ MCPToolResult,
27
+ MCPToolDefinition,
28
+ MCPContent,
29
+ JsonSchema,
30
+ APIProvider,
31
+ ToolBuilderConfig,
32
+ MCPResource,
33
+ ResourceCapabilities,
34
+ ResourceAnnotations,
35
+ ResourceSchema,
36
+ ResourceContent,
37
+ ResourceProvider,
38
+ PromptArgument,
39
+ MCPPromptDefinition,
40
+ PromptMessage,
41
+ PromptProvider,
42
+ MCPPlugin,
43
+ } from "./core/types.js";
44
+ export {toSerializableToolDefinition} from "./core/types.js";
45
+
46
+ // ── Registries ────────────────────────────────────────────────────────────────
47
+ export {resourceRegistry, createAndRegisterResource, unregisterResource} from "./core/resource-registry.js";
48
+ export {promptRegistry, createAndRegisterPrompt} from "./core/prompt-registry.js";
49
+
50
+ // ── URI Templates ─────────────────────────────────────────────────────────────
51
+ export {
52
+ parseURITemplate,
53
+ matchURITemplate,
54
+ resolveURITemplate,
55
+ TemplateRegistry,
56
+ } from "./core/resource-uri-templates.js";
57
+ export type {URITemplate, URIMatch} from "./core/resource-uri-templates.js";
58
+
59
+ // ── Clients ───────────────────────────────────────────────────────────────────
60
+ export {HttpMCPClient} from "./clients/mcp-http.js";
61
+ export {StdioMCPClient} from "./clients/mcp-stdio.js";
62
+ export type {
63
+ IMcpClient,
64
+ MCPToolSchema,
65
+ MCPToolResponse,
66
+ MCPResourceList,
67
+ MCPResourceContent,
68
+ MCPPrompt,
69
+ MCPPromptArgument,
70
+ MCPPromptList,
71
+ MCPPromptMessage,
72
+ MCPPromptResponse,
73
+ McpServerConfig,
74
+ McpHttpServerConfig,
75
+ McpStdioServerConfig,
76
+ } from "./clients/types.js";
77
+
78
+ // ── Utilities ─────────────────────────────────────────────────────────────────
79
+ export {default as logger, createLogger, log} from "./utils/logger.js";
80
+ export type {Logger, LogLevel} from "./utils/logger.js";
81
+
82
+ export {zodToMcpJsonSchema, isZodArray, fixAdditionalProperties} from "./utils/zod-to-mcp-schema.js";
83
+ export type {McpSchemaOptions} from "./utils/zod-to-mcp-schema.js";
84
+
85
+ export {HTTPClient, httpClient} from "./utils/http-client.js";
86
+ export type {HTTPClientConfig, HTTPClientResponse} from "./utils/http-client.js";
87
+
88
+ export {dynamicResourceManager, DynamicResourceManager} from "./utils/dynamic-resource-manager.js";
89
+ export type {ResourceMetadata, StoreOptions} from "./utils/dynamic-resource-manager.js";
90
+
91
+ // ── CLI helpers ───────────────────────────────────────────────────────────────
92
+ export {parseArgs} from "./cli.js";
@@ -0,0 +1,581 @@
1
+ /**
2
+ * Dynamic Resource Manager
3
+ *
4
+ * Manages automatic resource creation for large tool results.
5
+ * Key features:
6
+ * - Auto-detection of large responses
7
+ * - Dynamic resource registration
8
+ * - Persistent storage with TTL
9
+ * - Schema generation for safe querying
10
+ */
11
+
12
+ import {promises as fs} from "node:fs";
13
+ import path from "node:path";
14
+ import {resourceRegistry} from "../core/resource-registry.js";
15
+ import {getTool} from "../core/server.js";
16
+ import type {ResourceAnnotations, ResourceSchema} from "../core/types.js";
17
+ import logger from "./logger.js";
18
+
19
+ const resourceLogger = logger.child("resource-manager");
20
+
21
+ // ============================================================================
22
+ // TYPES
23
+ // ============================================================================
24
+
25
+ export interface ResourceMetadata {
26
+ uri: string;
27
+ toolName: string;
28
+ source?: string;
29
+ timestamp: number;
30
+ size: number;
31
+ expiresAt: number;
32
+ schema: ResourceSchema;
33
+ itemCount?: number;
34
+ filePath: string;
35
+ title?: string;
36
+ annotations?: ResourceAnnotations;
37
+ }
38
+
39
+ interface StoredResourceMetadata {
40
+ uri: string;
41
+ toolName: string;
42
+ source?: string;
43
+ timestamp: number;
44
+ size: number;
45
+ expiresAt: number;
46
+ schema?: ResourceSchema;
47
+ itemCount?: number;
48
+ }
49
+
50
+ export interface StoreOptions {
51
+ ttl?: number; // Time-to-live in milliseconds (default: 72 hours)
52
+ namespace?: string; // Resource namespace (default: 'cache')
53
+ schema?: ResourceSchema; // Optional override: provide known output schema of the dataset
54
+ toolDefinitionName?: string; // Optional: fully-qualified tool name (to pick up output schema)
55
+ source?: string; // Optional: resource origin (e.g., external)
56
+ }
57
+
58
+ // ============================================================================
59
+ // DYNAMIC RESOURCE MANAGER
60
+ // ============================================================================
61
+
62
+ export class DynamicResourceManager {
63
+ dataDir = path.join(process.cwd(), "data", "resources");
64
+ metadata: Map<string, ResourceMetadata> = new Map();
65
+ private defaultTTL = 72 * 60 * 60 * 1000; // 72 hours
66
+ private largeDataThreshold = 5000; // 5KB
67
+ private maxSchemaDepth = 4;
68
+ private maxSchemaProperties = 32;
69
+ private cacheDisabledLogged = false;
70
+ private readonly allowedStoredMetaKeys = new Set([
71
+ "uri",
72
+ "toolName",
73
+ "source",
74
+ "timestamp",
75
+ "size",
76
+ "expiresAt",
77
+ "schema",
78
+ "itemCount",
79
+ ]);
80
+
81
+ constructor() {
82
+ this.initialize();
83
+ }
84
+
85
+ /**
86
+ * Initialize resource manager
87
+ */
88
+ private async initialize(): Promise<void> {
89
+ try {
90
+ await fs.mkdir(this.dataDir, {recursive: true});
91
+ await this.loadResourcesFromDisk();
92
+ } catch (error) {
93
+ resourceLogger.error("Initialization failed:", error);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Load resources from disk (scan directory and read metadata from files)
99
+ */
100
+ private async loadResourcesFromDisk(): Promise<void> {
101
+ try {
102
+ const files = await fs.readdir(this.dataDir);
103
+
104
+ for (const file of files) {
105
+ if (!file.endsWith(".json")) continue;
106
+
107
+ const filePath = path.join(this.dataDir, file);
108
+ const resourceId = path.basename(file, ".json");
109
+
110
+ try {
111
+ const fileContent = await fs.readFile(filePath, "utf-8");
112
+ const parsed = JSON.parse(fileContent);
113
+ const data = parsed.data;
114
+ const rawMeta = parsed._meta ?? {};
115
+
116
+ const storedMeta = this.normalizeStoredMetadata(rawMeta, data, resourceId);
117
+ const metadata = this.buildRuntimeMetadata(storedMeta, filePath, data);
118
+
119
+ this.metadata.set(metadata.uri, metadata);
120
+ this.registerResource(metadata.uri, metadata, filePath);
121
+
122
+ const sanitizedMeta = this.toStoredMetadata(metadata);
123
+ const hasDeprecatedKeys = Object.keys(rawMeta ?? {}).some((key) => !this.allowedStoredMetaKeys.has(key));
124
+ const metaMismatch = JSON.stringify(rawMeta ?? {}) !== JSON.stringify(sanitizedMeta);
125
+
126
+ if (hasDeprecatedKeys || metaMismatch) {
127
+ await fs.writeFile(filePath, JSON.stringify({_meta: sanitizedMeta, data}, null, 2), "utf-8");
128
+ }
129
+ } catch (error) {
130
+ resourceLogger.error(`Failed to load resource ${file}:`, error);
131
+ }
132
+ }
133
+
134
+ if (this.metadata.size > 0) {
135
+ resourceLogger.info(`Loaded ${this.metadata.size} resources from disk`);
136
+ }
137
+
138
+ this.cleanupExpiredResources();
139
+ } catch (error) {
140
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
141
+ resourceLogger.error("Failed to load resources from disk:", error);
142
+ }
143
+ }
144
+ }
145
+
146
+ private normalizeStoredMetadata(rawMeta: any, data: unknown, resourceId: string): StoredResourceMetadata {
147
+ const toolName = this.resolveToolName(rawMeta?.toolName, resourceId);
148
+ const source = typeof rawMeta?.source === "string" ? rawMeta.source : undefined;
149
+ const timestamp = typeof rawMeta?.timestamp === "number" ? rawMeta.timestamp : Date.now();
150
+ const uri = typeof rawMeta?.uri === "string" ? rawMeta.uri : `cache://tools/${toolName}-${timestamp}`;
151
+ const size =
152
+ typeof rawMeta?.size === "number" ? rawMeta.size : data !== undefined ? JSON.stringify(data).length : 0;
153
+ const expiresAt = typeof rawMeta?.expiresAt === "number" ? rawMeta.expiresAt : timestamp + this.defaultTTL;
154
+ const schema =
155
+ rawMeta?.schema && typeof rawMeta.schema === "object" ? (rawMeta.schema as ResourceSchema) : undefined;
156
+ const itemCount =
157
+ typeof rawMeta?.itemCount === "number" ? rawMeta.itemCount : Array.isArray(data) ? data.length : undefined;
158
+
159
+ return {
160
+ uri,
161
+ toolName,
162
+ source,
163
+ timestamp,
164
+ size,
165
+ expiresAt,
166
+ schema,
167
+ itemCount,
168
+ };
169
+ }
170
+
171
+ private buildRuntimeMetadata(storedMeta: StoredResourceMetadata, filePath: string, data: unknown): ResourceMetadata {
172
+ const schema = storedMeta.schema ?? this.generateSchema(data);
173
+ const itemCount = storedMeta.itemCount ?? (Array.isArray(data) ? data.length : undefined);
174
+
175
+ return {
176
+ ...storedMeta,
177
+ schema,
178
+ itemCount,
179
+ filePath,
180
+ title: this.buildTitle(storedMeta.toolName),
181
+ annotations: this.createAnnotations(storedMeta.timestamp),
182
+ };
183
+ }
184
+
185
+ private toStoredMetadata(metadata: ResourceMetadata): StoredResourceMetadata {
186
+ return {
187
+ uri: metadata.uri,
188
+ toolName: metadata.toolName,
189
+ source: metadata.source,
190
+ timestamp: metadata.timestamp,
191
+ size: metadata.size,
192
+ expiresAt: metadata.expiresAt,
193
+ schema: metadata.schema,
194
+ itemCount: metadata.itemCount,
195
+ };
196
+ }
197
+
198
+ private resolveToolName(toolNameCandidate: unknown, resourceId: string): string {
199
+ if (typeof toolNameCandidate === "string" && toolNameCandidate.length > 0) {
200
+ return toolNameCandidate;
201
+ }
202
+
203
+ const segments = resourceId.split("-");
204
+ if (segments.length <= 1) {
205
+ return resourceId || "unknown-tool";
206
+ }
207
+ segments.pop(); // remove timestamp segment
208
+ const name = segments.join("-");
209
+ return name.length > 0 ? name : "unknown-tool";
210
+ }
211
+
212
+ private buildTitle(toolName: string): string {
213
+ return `Cached dataset from ${toolName}`;
214
+ }
215
+
216
+ /**
217
+ * Store tool result as MCP resource if it's large
218
+ * Returns resource URI if stored, undefined if result is small
219
+ */
220
+ async storeIfLarge(toolName: string, result: any, options: StoreOptions = {}): Promise<string | undefined> {
221
+ if (this.isCacheDisabled()) {
222
+ if (!this.cacheDisabledLogged) {
223
+ resourceLogger.warn("Caching disabled via MCP_DISABLE_CACHE");
224
+ this.cacheDisabledLogged = true;
225
+ }
226
+ return undefined;
227
+ }
228
+
229
+ const resultSize = JSON.stringify(result).length;
230
+
231
+ if (resultSize < this.largeDataThreshold) {
232
+ return undefined;
233
+ }
234
+
235
+ return await this.store(toolName, result, options);
236
+ }
237
+
238
+ /**
239
+ * Store tool result as MCP resource (regardless of size)
240
+ */
241
+ async store(toolName: string, result: any, options: StoreOptions = {}): Promise<string> {
242
+ const {ttl = this.defaultTTL, namespace = "cache"} = options;
243
+
244
+ const timestamp = Date.now();
245
+ const resourceId = `${toolName}-${timestamp}`;
246
+ const uri = `${namespace}://tools/${resourceId}`;
247
+ const filePath = path.join(this.dataDir, `${resourceId}.json`);
248
+
249
+ const schema = this.resolveSchema(result, options);
250
+ const itemCount = Array.isArray(result) ? result.length : undefined;
251
+ const annotations = this.createAnnotations(timestamp);
252
+
253
+ const metadata: ResourceMetadata = {
254
+ uri,
255
+ toolName,
256
+ source: options.source,
257
+ timestamp,
258
+ size: JSON.stringify(result).length,
259
+ filePath,
260
+ expiresAt: timestamp + ttl,
261
+ schema,
262
+ itemCount,
263
+ title: this.buildTitle(toolName),
264
+ annotations,
265
+ };
266
+
267
+ this.metadata.set(uri, metadata);
268
+
269
+ const fileContent = {
270
+ _meta: this.toStoredMetadata(metadata),
271
+ data: result,
272
+ };
273
+ await fs.writeFile(filePath, JSON.stringify(fileContent, null, 2), "utf-8");
274
+
275
+ this.registerResource(uri, metadata, filePath);
276
+
277
+ resourceLogger.info(`Stored ${metadata.size} bytes at ${uri} (expires in ${ttl / 1000}s)`);
278
+
279
+ return uri;
280
+ }
281
+
282
+ /**
283
+ * Register resource with MCP resource registry
284
+ */
285
+ private registerResource(uri: string, metadata: ResourceMetadata, filePath: string): void {
286
+ const title = metadata.title ?? `Cached result from ${metadata.toolName}`;
287
+
288
+ resourceRegistry.register({
289
+ uri,
290
+ name: `Tool result: ${metadata.toolName}`,
291
+ title,
292
+ description: this.describeSchema(metadata),
293
+ mimeType: "application/json",
294
+ size: metadata.size,
295
+ schema: metadata.schema,
296
+ annotations: metadata.annotations,
297
+ async read() {
298
+ const file = await fs.readFile(metadata.filePath ?? filePath, "utf-8");
299
+ const parsed = JSON.parse(file);
300
+ return {
301
+ text: JSON.stringify(parsed.data, null, 2),
302
+ };
303
+ },
304
+ });
305
+ }
306
+
307
+ /**
308
+ * Generate schema description for logs / chat messages
309
+ */
310
+ private describeSchema(metadata: ResourceMetadata): string {
311
+ const schemaText = this.describeSchemaNode(metadata.schema);
312
+ if (metadata.itemCount !== undefined) {
313
+ return `${schemaText} (${metadata.itemCount} items, ${this.formatBytes(metadata.size)})`;
314
+ }
315
+ return `${schemaText} (${this.formatBytes(metadata.size)})`;
316
+ }
317
+
318
+ private describeSchemaNode(schema: ResourceSchema | undefined, depth = 0): string {
319
+ if (!schema) {
320
+ return "unknown structure";
321
+ }
322
+
323
+ const type = Array.isArray(schema.type) ? schema.type.join("|") : schema.type;
324
+
325
+ if (type === "array" && schema.items) {
326
+ return `array of ${this.describeSchemaNode(schema.items, depth + 1)}`;
327
+ }
328
+
329
+ if (type === "object" && schema.properties) {
330
+ const keys = Object.keys(schema.properties).slice(0, 5);
331
+ if (keys.length > 0) {
332
+ return depth === 0 ? `object (keys: ${keys.join(", ")})` : `object (${keys.join(", ")})`;
333
+ }
334
+ return "object";
335
+ }
336
+
337
+ return type ?? "unknown";
338
+ }
339
+
340
+ private createAnnotations(timestamp: number): ResourceAnnotations {
341
+ return {
342
+ audience: ["assistant"],
343
+ priority: 0.5,
344
+ lastModified: new Date(timestamp).toISOString(),
345
+ };
346
+ }
347
+
348
+ private pickSample<T>(items: T[]): T | undefined {
349
+ for (const item of items) {
350
+ if (item !== undefined && item !== null) {
351
+ return item;
352
+ }
353
+ }
354
+ return undefined;
355
+ }
356
+
357
+ private generateSchema(data: unknown, depth = 0): ResourceSchema {
358
+ if (depth >= this.maxSchemaDepth) {
359
+ return {type: "unknown"};
360
+ }
361
+
362
+ if (data === null || data === undefined) {
363
+ return {type: "null"};
364
+ }
365
+
366
+ if (Array.isArray(data)) {
367
+ const sample = this.pickSample(data);
368
+ if (sample === undefined) {
369
+ return {type: "array"};
370
+ }
371
+ return {type: "array", items: this.generateSchema(sample, depth + 1)};
372
+ }
373
+
374
+ const valueType = typeof data;
375
+ if (valueType !== "object") {
376
+ return {type: valueType};
377
+ }
378
+
379
+ // Objects: capture up to maxSchemaProperties keys for better schema details
380
+ const entries = Object.entries(data as Record<string, unknown>);
381
+ if (entries.length === 0) {
382
+ return {type: "object"};
383
+ }
384
+
385
+ const properties: Record<string, ResourceSchema> = {};
386
+ for (const [key, value] of entries.slice(0, this.maxSchemaProperties)) {
387
+ properties[key] = this.generateSchema(value, depth + 1);
388
+ }
389
+
390
+ const schema: ResourceSchema = {
391
+ type: "object",
392
+ properties,
393
+ };
394
+
395
+ if (entries.length > this.maxSchemaProperties) {
396
+ const remainingSample = this.pickSample(entries.slice(this.maxSchemaProperties).map(([, value]) => value));
397
+ if (remainingSample !== undefined) {
398
+ schema.additionalProperties = this.generateSchema(remainingSample, depth + 1);
399
+ }
400
+ }
401
+
402
+ return schema;
403
+ }
404
+
405
+ /**
406
+ * Get metadata for a resource
407
+ */
408
+ getMetadata(uri: string): ResourceMetadata | undefined {
409
+ return this.metadata.get(uri);
410
+ }
411
+
412
+ /**
413
+ * Get sanitized metadata for clients (no internal paths)
414
+ */
415
+ getPublicMetadata(uri: string): StoredResourceMetadata | undefined {
416
+ const metadata = this.metadata.get(uri);
417
+ if (!metadata) {
418
+ return undefined;
419
+ }
420
+ return this.toStoredMetadata(metadata);
421
+ }
422
+
423
+ /**
424
+ * List all active resources
425
+ */
426
+ listResources(): ResourceMetadata[] {
427
+ this.cleanupExpiredResources();
428
+ return Array.from(this.metadata.values())
429
+ .filter((m) => m.expiresAt > Date.now())
430
+ .sort((a, b) => b.timestamp - a.timestamp);
431
+ }
432
+
433
+ /**
434
+ * Generate chat-friendly message about stored resource
435
+ */
436
+ getChatMessage(uri: string): string {
437
+ const metadata = this.metadata.get(uri);
438
+ if (!metadata) {
439
+ return [`resourceUri: "${uri}"`, "size: unknown", "data_schema: type: unknown, properties: none"].join("\n");
440
+ }
441
+
442
+ const resourceUriLine = `resourceUri: "${uri}"`;
443
+ const sizeLine = `size: ${this.formatBytes(metadata.size)}`;
444
+ const dataSchemaLine = this.buildDataSchemaLine(metadata.schema);
445
+
446
+ return [resourceUriLine, sizeLine, dataSchemaLine].join("\n");
447
+ }
448
+
449
+ private formatBytes(bytes: number): string {
450
+ if (bytes < 1024) return `${bytes} B`;
451
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
452
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
453
+ }
454
+
455
+ private buildDataSchemaLine(schema: ResourceSchema | undefined): string {
456
+ const type = this.describeSchemaType(schema);
457
+ const properties = this.extractSchemaPropertyNames(schema);
458
+ const propertiesText = properties.length > 0 ? properties.join(", ") : "none";
459
+
460
+ return `data_schema: type: ${type}, properties: ${propertiesText}`;
461
+ }
462
+
463
+ private describeSchemaType(schema: ResourceSchema | undefined): string {
464
+ if (!schema) {
465
+ return "unknown";
466
+ }
467
+
468
+ const types = this.normalizeSchemaTypes(schema.type);
469
+
470
+ if (types.includes("array")) {
471
+ const itemType = this.describeSchemaType(schema.items);
472
+ return itemType === "unknown" ? "array" : `array<${itemType}>`;
473
+ }
474
+
475
+ if (types.includes("object")) {
476
+ return "object";
477
+ }
478
+
479
+ return types[0] ?? "unknown";
480
+ }
481
+
482
+ private extractSchemaPropertyNames(schema: ResourceSchema | undefined): string[] {
483
+ if (!schema) {
484
+ return [];
485
+ }
486
+
487
+ const types = this.normalizeSchemaTypes(schema.type);
488
+
489
+ if (types.includes("array")) {
490
+ const fromItems = this.extractSchemaPropertyNames(schema.items);
491
+ if (fromItems.length > 0) {
492
+ return fromItems;
493
+ }
494
+ return ["items"];
495
+ }
496
+
497
+ if (types.includes("object")) {
498
+ if (schema.properties) {
499
+ return Object.keys(schema.properties).slice(0, this.maxSchemaProperties);
500
+ }
501
+ if (schema.additionalProperties) {
502
+ return this.extractSchemaPropertyNames(schema.additionalProperties);
503
+ }
504
+ }
505
+
506
+ return [];
507
+ }
508
+
509
+ private normalizeSchemaTypes(type: string | string[] | undefined): string[] {
510
+ if (!type) {
511
+ return [];
512
+ }
513
+
514
+ return Array.isArray(type) ? type : [type];
515
+ }
516
+
517
+ private resolveSchema(result: unknown, options: StoreOptions): ResourceSchema {
518
+ const provided = this.sanitizeSchema(options.schema);
519
+ if (provided) {
520
+ return provided;
521
+ }
522
+
523
+ const fromTool = this.getToolOutputSchema(options.toolDefinitionName);
524
+ if (fromTool) {
525
+ return fromTool;
526
+ }
527
+
528
+ return this.generateSchema(result);
529
+ }
530
+
531
+ private sanitizeSchema(schema: unknown): ResourceSchema | undefined {
532
+ if (!schema || typeof schema !== "object") {
533
+ return undefined;
534
+ }
535
+ try {
536
+ return JSON.parse(JSON.stringify(schema)) as ResourceSchema;
537
+ } catch {
538
+ return undefined;
539
+ }
540
+ }
541
+
542
+ private getToolOutputSchema(toolDefinitionName?: string): ResourceSchema | undefined {
543
+ if (!toolDefinitionName) {
544
+ return undefined;
545
+ }
546
+
547
+ const tool = getTool(toolDefinitionName);
548
+ if (!tool?.definition?.outputSchema) {
549
+ return undefined;
550
+ }
551
+
552
+ return this.sanitizeSchema(tool.definition.outputSchema);
553
+ }
554
+
555
+ private cleanupExpiredResources(): void {
556
+ const now = Date.now();
557
+ for (const [uri, metadata] of Array.from(this.metadata.entries())) {
558
+ if (metadata.expiresAt <= now) {
559
+ this.metadata.delete(uri);
560
+ resourceRegistry.unregister(uri);
561
+ void fs.unlink(metadata.filePath).catch(() => undefined);
562
+ resourceLogger.info(`Expired resource removed: ${uri}`);
563
+ }
564
+ }
565
+ }
566
+
567
+ private isCacheDisabled(): boolean {
568
+ const raw = process.env.MCP_DISABLE_CACHE;
569
+ if (!raw) {
570
+ return false;
571
+ }
572
+ const normalized = raw.trim().toLowerCase();
573
+ return normalized === "1" || normalized === "true" || normalized === "yes";
574
+ }
575
+ }
576
+
577
+ // ============================================================================
578
+ // SINGLETON INSTANCE
579
+ // ============================================================================
580
+
581
+ export const dynamicResourceManager = new DynamicResourceManager();