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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/__tests__/helpers/test-server.d.ts +2 -0
- package/dist/__tests__/helpers/test-server.d.ts.map +1 -0
- package/dist/__tests__/helpers/test-server.js +27 -0
- package/dist/__tests__/helpers/test-server.js.map +1 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/clients/mcp-http.d.ts +36 -0
- package/dist/clients/mcp-http.d.ts.map +1 -0
- package/dist/clients/mcp-http.js +148 -0
- package/dist/clients/mcp-http.js.map +1 -0
- package/dist/clients/mcp-stdio.d.ts +38 -0
- package/dist/clients/mcp-stdio.d.ts.map +1 -0
- package/dist/clients/mcp-stdio.js +164 -0
- package/dist/clients/mcp-stdio.js.map +1 -0
- package/dist/clients/types.d.ts +104 -0
- package/dist/clients/types.d.ts.map +1 -0
- package/dist/clients/types.js +8 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/core/prompt-registry.d.ts +56 -0
- package/dist/core/prompt-registry.d.ts.map +1 -0
- package/dist/core/prompt-registry.js +100 -0
- package/dist/core/prompt-registry.js.map +1 -0
- package/dist/core/resource-registry.d.ts +79 -0
- package/dist/core/resource-registry.d.ts.map +1 -0
- package/dist/core/resource-registry.js +135 -0
- package/dist/core/resource-registry.js.map +1 -0
- package/dist/core/resource-uri-templates.d.ts +64 -0
- package/dist/core/resource-uri-templates.d.ts.map +1 -0
- package/dist/core/resource-uri-templates.js +168 -0
- package/dist/core/resource-uri-templates.js.map +1 -0
- package/dist/core/server-http.d.ts +15 -0
- package/dist/core/server-http.d.ts.map +1 -0
- package/dist/core/server-http.js +302 -0
- package/dist/core/server-http.js.map +1 -0
- package/dist/core/server-stdio.d.ts +8 -0
- package/dist/core/server-stdio.d.ts.map +1 -0
- package/dist/core/server-stdio.js +15 -0
- package/dist/core/server-stdio.js.map +1 -0
- package/dist/core/server.d.ts +29 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/core/server.js +265 -0
- package/dist/core/server.js.map +1 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/dynamic-resource-manager.d.ts +115 -0
- package/dist/utils/dynamic-resource-manager.d.ts.map +1 -0
- package/dist/utils/dynamic-resource-manager.js +460 -0
- package/dist/utils/dynamic-resource-manager.js.map +1 -0
- package/dist/utils/http-client.d.ts +29 -0
- package/dist/utils/http-client.d.ts.map +1 -0
- package/dist/utils/http-client.js +59 -0
- package/dist/utils/http-client.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/zod-to-mcp-schema.d.ts +42 -0
- package/dist/utils/zod-to-mcp-schema.d.ts.map +1 -0
- package/dist/utils/zod-to-mcp-schema.js +87 -0
- package/dist/utils/zod-to-mcp-schema.js.map +1 -0
- package/package.json +57 -0
- package/src/__tests__/helpers/test-server.ts +31 -0
- package/src/__tests__/plugin-system.basic.test.ts +137 -0
- package/src/__tests__/server.basic.test.ts +37 -0
- package/src/__tests__/stdio-roundtrip.basic.test.ts +67 -0
- package/src/__tests__/tool-registry.basic.test.ts +114 -0
- package/src/__tests__/zod-schema.basic.test.ts +105 -0
- package/src/cli.ts +58 -0
- package/src/clients/mcp-http.ts +192 -0
- package/src/clients/mcp-stdio.ts +209 -0
- package/src/clients/types.ts +136 -0
- package/src/core/prompt-registry.ts +114 -0
- package/src/core/resource-registry.ts +166 -0
- package/src/core/resource-uri-templates.ts +216 -0
- package/src/core/server-http.ts +407 -0
- package/src/core/server-stdio.ts +20 -0
- package/src/core/server.ts +320 -0
- package/src/core/types.ts +312 -0
- package/src/index.ts +92 -0
- package/src/utils/dynamic-resource-manager.ts +581 -0
- package/src/utils/http-client.ts +86 -0
- package/src/utils/logger.ts +138 -0
- 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();
|