midnight-mcp 0.0.2 → 0.0.5
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/README.md +55 -54
- package/dist/pipeline/github.d.ts +18 -1
- package/dist/pipeline/github.js +132 -20
- package/dist/server.js +29 -7
- package/dist/tools/health.d.ts +91 -0
- package/dist/tools/health.js +91 -0
- package/dist/tools/index.d.ts +30 -4
- package/dist/tools/index.js +10 -3
- package/dist/tools/search.d.ts +5 -47
- package/dist/tools/search.js +167 -13
- package/dist/utils/cache.d.ts +77 -0
- package/dist/utils/cache.js +172 -0
- package/dist/utils/config.d.ts +16 -12
- package/dist/utils/config.js +25 -8
- package/dist/utils/errors.d.ts +45 -0
- package/dist/utils/errors.js +95 -0
- package/dist/utils/health.d.ts +29 -0
- package/dist/utils/health.js +132 -0
- package/dist/utils/hosted-api.d.ts +61 -0
- package/dist/utils/hosted-api.js +106 -0
- package/dist/utils/index.d.ts +12 -1
- package/dist/utils/index.js +12 -1
- package/dist/utils/logger.d.ts +30 -1
- package/dist/utils/logger.js +68 -3
- package/dist/utils/rate-limit.d.ts +61 -0
- package/dist/utils/rate-limit.js +148 -0
- package/dist/utils/validation.d.ts +52 -0
- package/dist/utils/validation.js +255 -0
- package/package.json +1 -2
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
export { searchTools, searchCompact, searchTypeScript, searchDocs } from "./search.js";
|
|
2
|
-
export type { SearchCompactInput, SearchTypeScriptInput, SearchDocsInput } from "./search.js";
|
|
1
|
+
export { searchTools, searchCompact, searchTypeScript, searchDocs, } from "./search.js";
|
|
2
|
+
export type { SearchCompactInput, SearchTypeScriptInput, SearchDocsInput, } from "./search.js";
|
|
3
3
|
export { analyzeTools, analyzeContract, explainCircuit } from "./analyze.js";
|
|
4
4
|
export type { AnalyzeContractInput, ExplainCircuitInput } from "./analyze.js";
|
|
5
|
-
export { repositoryTools, getFile, listExamples, getLatestUpdates } from "./repository.js";
|
|
6
|
-
export type { GetFileInput, ListExamplesInput, GetLatestUpdatesInput } from "./repository.js";
|
|
5
|
+
export { repositoryTools, getFile, listExamples, getLatestUpdates, } from "./repository.js";
|
|
6
|
+
export type { GetFileInput, ListExamplesInput, GetLatestUpdatesInput, } from "./repository.js";
|
|
7
|
+
export { healthTools, healthCheck, getStatus } from "./health.js";
|
|
8
|
+
export type { HealthCheckInput, GetStatusInput } from "./health.js";
|
|
7
9
|
export declare const allTools: ({
|
|
8
10
|
name: string;
|
|
9
11
|
description: string;
|
|
@@ -388,5 +390,29 @@ export declare const allTools: ({
|
|
|
388
390
|
required: never[];
|
|
389
391
|
};
|
|
390
392
|
handler: typeof import("./repository.js").getLatestSyntax;
|
|
393
|
+
} | {
|
|
394
|
+
name: string;
|
|
395
|
+
description: string;
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: "object";
|
|
398
|
+
properties: {
|
|
399
|
+
detailed: {
|
|
400
|
+
type: string;
|
|
401
|
+
description: string;
|
|
402
|
+
default: boolean;
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
handler: typeof import("./health.js").healthCheck;
|
|
407
|
+
} | {
|
|
408
|
+
name: string;
|
|
409
|
+
description: string;
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object";
|
|
412
|
+
properties: {
|
|
413
|
+
detailed?: undefined;
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
handler: typeof import("./health.js").getStatus;
|
|
391
417
|
})[];
|
|
392
418
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/tools/index.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
export { searchTools, searchCompact, searchTypeScript, searchDocs } from "./search.js";
|
|
1
|
+
export { searchTools, searchCompact, searchTypeScript, searchDocs, } from "./search.js";
|
|
2
2
|
export { analyzeTools, analyzeContract, explainCircuit } from "./analyze.js";
|
|
3
|
-
export { repositoryTools, getFile, listExamples, getLatestUpdates } from "./repository.js";
|
|
3
|
+
export { repositoryTools, getFile, listExamples, getLatestUpdates, } from "./repository.js";
|
|
4
|
+
export { healthTools, healthCheck, getStatus } from "./health.js";
|
|
4
5
|
// Combined tool list for MCP server
|
|
5
6
|
import { searchTools } from "./search.js";
|
|
6
7
|
import { analyzeTools } from "./analyze.js";
|
|
7
8
|
import { repositoryTools } from "./repository.js";
|
|
8
|
-
|
|
9
|
+
import { healthTools } from "./health.js";
|
|
10
|
+
export const allTools = [
|
|
11
|
+
...searchTools,
|
|
12
|
+
...analyzeTools,
|
|
13
|
+
...repositoryTools,
|
|
14
|
+
...healthTools,
|
|
15
|
+
];
|
|
9
16
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/search.d.ts
CHANGED
|
@@ -50,11 +50,11 @@ export declare const SearchDocsInputSchema: z.ZodObject<{
|
|
|
50
50
|
}, "strip", z.ZodTypeAny, {
|
|
51
51
|
query: string;
|
|
52
52
|
limit: number;
|
|
53
|
-
category: "
|
|
53
|
+
category: "all" | "guides" | "api" | "concepts";
|
|
54
54
|
}, {
|
|
55
55
|
query: string;
|
|
56
56
|
limit?: number | undefined;
|
|
57
|
-
category?: "
|
|
57
|
+
category?: "all" | "guides" | "api" | "concepts" | undefined;
|
|
58
58
|
}>;
|
|
59
59
|
export type SearchCompactInput = z.infer<typeof SearchCompactInputSchema>;
|
|
60
60
|
export type SearchTypeScriptInput = z.infer<typeof SearchTypeScriptInputSchema>;
|
|
@@ -62,57 +62,15 @@ export type SearchDocsInput = z.infer<typeof SearchDocsInputSchema>;
|
|
|
62
62
|
/**
|
|
63
63
|
* Search Compact smart contract code and patterns
|
|
64
64
|
*/
|
|
65
|
-
export declare function searchCompact(input: SearchCompactInput): Promise<{
|
|
66
|
-
results: {
|
|
67
|
-
code: string;
|
|
68
|
-
relevanceScore: number;
|
|
69
|
-
source: {
|
|
70
|
-
repository: string;
|
|
71
|
-
filePath: string;
|
|
72
|
-
lines: string;
|
|
73
|
-
};
|
|
74
|
-
codeType: string;
|
|
75
|
-
name: string;
|
|
76
|
-
}[];
|
|
77
|
-
totalResults: number;
|
|
78
|
-
query: string;
|
|
79
|
-
}>;
|
|
65
|
+
export declare function searchCompact(input: SearchCompactInput): Promise<{}>;
|
|
80
66
|
/**
|
|
81
67
|
* Search TypeScript SDK code, types, and API implementations
|
|
82
68
|
*/
|
|
83
|
-
export declare function searchTypeScript(input: SearchTypeScriptInput): Promise<{
|
|
84
|
-
results: {
|
|
85
|
-
code: string;
|
|
86
|
-
relevanceScore: number;
|
|
87
|
-
source: {
|
|
88
|
-
repository: string;
|
|
89
|
-
filePath: string;
|
|
90
|
-
lines: string;
|
|
91
|
-
};
|
|
92
|
-
codeType: string;
|
|
93
|
-
name: string;
|
|
94
|
-
isExported: boolean;
|
|
95
|
-
}[];
|
|
96
|
-
totalResults: number;
|
|
97
|
-
query: string;
|
|
98
|
-
}>;
|
|
69
|
+
export declare function searchTypeScript(input: SearchTypeScriptInput): Promise<{}>;
|
|
99
70
|
/**
|
|
100
71
|
* Full-text search across official Midnight documentation
|
|
101
72
|
*/
|
|
102
|
-
export declare function searchDocs(input: SearchDocsInput): Promise<{
|
|
103
|
-
results: {
|
|
104
|
-
content: string;
|
|
105
|
-
relevanceScore: number;
|
|
106
|
-
source: {
|
|
107
|
-
repository: string;
|
|
108
|
-
filePath: string;
|
|
109
|
-
section: string;
|
|
110
|
-
};
|
|
111
|
-
}[];
|
|
112
|
-
totalResults: number;
|
|
113
|
-
query: string;
|
|
114
|
-
category: "guides" | "api" | "concepts" | "all";
|
|
115
|
-
}>;
|
|
73
|
+
export declare function searchDocs(input: SearchDocsInput): Promise<{}>;
|
|
116
74
|
export declare const searchTools: ({
|
|
117
75
|
name: string;
|
|
118
76
|
description: string;
|
package/dist/tools/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { vectorStore } from "../db/index.js";
|
|
3
|
-
import { logger } from "../utils/index.js";
|
|
3
|
+
import { logger, validateQuery, validateNumber, searchCache, createCacheKey, isHostedMode, searchCompactHosted, searchTypeScriptHosted, searchDocsHosted, } from "../utils/index.js";
|
|
4
4
|
// Schema definitions for tool inputs
|
|
5
5
|
export const SearchCompactInputSchema = z.object({
|
|
6
6
|
query: z.string().describe("Natural language search query for Compact code"),
|
|
@@ -44,13 +44,59 @@ export const SearchDocsInputSchema = z.object({
|
|
|
44
44
|
* Search Compact smart contract code and patterns
|
|
45
45
|
*/
|
|
46
46
|
export async function searchCompact(input) {
|
|
47
|
-
|
|
47
|
+
// Validate input
|
|
48
|
+
const queryValidation = validateQuery(input.query);
|
|
49
|
+
if (!queryValidation.isValid) {
|
|
50
|
+
return {
|
|
51
|
+
error: "Invalid query",
|
|
52
|
+
details: queryValidation.errors,
|
|
53
|
+
suggestion: "Provide a valid search query with at least 2 characters",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const limitValidation = validateNumber(input.limit, {
|
|
57
|
+
min: 1,
|
|
58
|
+
max: 50,
|
|
59
|
+
defaultValue: 10,
|
|
60
|
+
});
|
|
61
|
+
const sanitizedQuery = queryValidation.sanitized;
|
|
62
|
+
const limit = limitValidation.value;
|
|
63
|
+
logger.debug("Searching Compact code", {
|
|
64
|
+
query: sanitizedQuery,
|
|
65
|
+
mode: isHostedMode() ? "hosted" : "local",
|
|
66
|
+
});
|
|
67
|
+
// Check cache first
|
|
68
|
+
const cacheKey = createCacheKey("compact", sanitizedQuery, limit, input.filter?.repository);
|
|
69
|
+
const cached = searchCache.get(cacheKey);
|
|
70
|
+
if (cached) {
|
|
71
|
+
logger.debug("Search cache hit", { cacheKey });
|
|
72
|
+
return cached;
|
|
73
|
+
}
|
|
74
|
+
// Use hosted API if in hosted mode
|
|
75
|
+
if (isHostedMode()) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await searchCompactHosted(sanitizedQuery, limit);
|
|
78
|
+
searchCache.set(cacheKey, response);
|
|
79
|
+
return {
|
|
80
|
+
...response,
|
|
81
|
+
...(queryValidation.warnings.length > 0 && {
|
|
82
|
+
warnings: queryValidation.warnings,
|
|
83
|
+
}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger.warn("Hosted API search failed, falling back to local", {
|
|
88
|
+
error: String(error),
|
|
89
|
+
});
|
|
90
|
+
// Fall through to local search
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Local search (fallback or when in local mode)
|
|
48
94
|
const filter = {
|
|
49
95
|
language: "compact",
|
|
50
96
|
...input.filter,
|
|
51
97
|
};
|
|
52
|
-
const results = await vectorStore.search(
|
|
53
|
-
|
|
98
|
+
const results = await vectorStore.search(sanitizedQuery, limit, filter);
|
|
99
|
+
const response = {
|
|
54
100
|
results: results.map((r) => ({
|
|
55
101
|
code: r.content,
|
|
56
102
|
relevanceScore: r.score,
|
|
@@ -63,24 +109,76 @@ export async function searchCompact(input) {
|
|
|
63
109
|
name: r.metadata.codeName,
|
|
64
110
|
})),
|
|
65
111
|
totalResults: results.length,
|
|
66
|
-
query:
|
|
112
|
+
query: sanitizedQuery,
|
|
113
|
+
...(queryValidation.warnings.length > 0 && {
|
|
114
|
+
warnings: queryValidation.warnings,
|
|
115
|
+
}),
|
|
67
116
|
};
|
|
117
|
+
// Cache the response
|
|
118
|
+
searchCache.set(cacheKey, response);
|
|
119
|
+
return response;
|
|
68
120
|
}
|
|
69
121
|
/**
|
|
70
122
|
* Search TypeScript SDK code, types, and API implementations
|
|
71
123
|
*/
|
|
72
124
|
export async function searchTypeScript(input) {
|
|
73
|
-
|
|
125
|
+
// Validate input
|
|
126
|
+
const queryValidation = validateQuery(input.query);
|
|
127
|
+
if (!queryValidation.isValid) {
|
|
128
|
+
return {
|
|
129
|
+
error: "Invalid query",
|
|
130
|
+
details: queryValidation.errors,
|
|
131
|
+
suggestion: "Provide a valid search query with at least 2 characters",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const limitValidation = validateNumber(input.limit, {
|
|
135
|
+
min: 1,
|
|
136
|
+
max: 50,
|
|
137
|
+
defaultValue: 10,
|
|
138
|
+
});
|
|
139
|
+
const sanitizedQuery = queryValidation.sanitized;
|
|
140
|
+
const limit = limitValidation.value;
|
|
141
|
+
logger.debug("Searching TypeScript code", {
|
|
142
|
+
query: sanitizedQuery,
|
|
143
|
+
mode: isHostedMode() ? "hosted" : "local",
|
|
144
|
+
});
|
|
145
|
+
// Check cache
|
|
146
|
+
const cacheKey = createCacheKey("typescript", sanitizedQuery, limit, input.includeTypes, input.includeExamples);
|
|
147
|
+
const cached = searchCache.get(cacheKey);
|
|
148
|
+
if (cached) {
|
|
149
|
+
logger.debug("Search cache hit", { cacheKey });
|
|
150
|
+
return cached;
|
|
151
|
+
}
|
|
152
|
+
// Use hosted API if in hosted mode
|
|
153
|
+
if (isHostedMode()) {
|
|
154
|
+
try {
|
|
155
|
+
const response = await searchTypeScriptHosted(sanitizedQuery, limit, input.includeTypes);
|
|
156
|
+
searchCache.set(cacheKey, response);
|
|
157
|
+
return {
|
|
158
|
+
...response,
|
|
159
|
+
...(queryValidation.warnings.length > 0 && {
|
|
160
|
+
warnings: queryValidation.warnings,
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logger.warn("Hosted API search failed, falling back to local", {
|
|
166
|
+
error: String(error),
|
|
167
|
+
});
|
|
168
|
+
// Fall through to local search
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Local search (fallback or when in local mode)
|
|
74
172
|
const filter = {
|
|
75
173
|
language: "typescript",
|
|
76
174
|
};
|
|
77
|
-
const results = await vectorStore.search(
|
|
175
|
+
const results = await vectorStore.search(sanitizedQuery, limit, filter);
|
|
78
176
|
// Filter based on type preferences
|
|
79
177
|
let filteredResults = results;
|
|
80
178
|
if (!input.includeTypes) {
|
|
81
179
|
filteredResults = results.filter((r) => r.metadata.codeType !== "type" && r.metadata.codeType !== "interface");
|
|
82
180
|
}
|
|
83
|
-
|
|
181
|
+
const response = {
|
|
84
182
|
results: filteredResults.map((r) => ({
|
|
85
183
|
code: r.content,
|
|
86
184
|
relevanceScore: r.score,
|
|
@@ -94,14 +192,65 @@ export async function searchTypeScript(input) {
|
|
|
94
192
|
isExported: r.metadata.isPublic,
|
|
95
193
|
})),
|
|
96
194
|
totalResults: filteredResults.length,
|
|
97
|
-
query:
|
|
195
|
+
query: sanitizedQuery,
|
|
196
|
+
...(queryValidation.warnings.length > 0 && {
|
|
197
|
+
warnings: queryValidation.warnings,
|
|
198
|
+
}),
|
|
98
199
|
};
|
|
200
|
+
searchCache.set(cacheKey, response);
|
|
201
|
+
return response;
|
|
99
202
|
}
|
|
100
203
|
/**
|
|
101
204
|
* Full-text search across official Midnight documentation
|
|
102
205
|
*/
|
|
103
206
|
export async function searchDocs(input) {
|
|
104
|
-
|
|
207
|
+
// Validate input
|
|
208
|
+
const queryValidation = validateQuery(input.query);
|
|
209
|
+
if (!queryValidation.isValid) {
|
|
210
|
+
return {
|
|
211
|
+
error: "Invalid query",
|
|
212
|
+
details: queryValidation.errors,
|
|
213
|
+
suggestion: "Provide a valid search query with at least 2 characters",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const limitValidation = validateNumber(input.limit, {
|
|
217
|
+
min: 1,
|
|
218
|
+
max: 50,
|
|
219
|
+
defaultValue: 10,
|
|
220
|
+
});
|
|
221
|
+
const sanitizedQuery = queryValidation.sanitized;
|
|
222
|
+
const limit = limitValidation.value;
|
|
223
|
+
logger.debug("Searching documentation", {
|
|
224
|
+
query: sanitizedQuery,
|
|
225
|
+
mode: isHostedMode() ? "hosted" : "local",
|
|
226
|
+
});
|
|
227
|
+
// Check cache
|
|
228
|
+
const cacheKey = createCacheKey("docs", sanitizedQuery, limit, input.category);
|
|
229
|
+
const cached = searchCache.get(cacheKey);
|
|
230
|
+
if (cached) {
|
|
231
|
+
logger.debug("Search cache hit", { cacheKey });
|
|
232
|
+
return cached;
|
|
233
|
+
}
|
|
234
|
+
// Use hosted API if in hosted mode
|
|
235
|
+
if (isHostedMode()) {
|
|
236
|
+
try {
|
|
237
|
+
const response = await searchDocsHosted(sanitizedQuery, limit, input.category);
|
|
238
|
+
searchCache.set(cacheKey, response);
|
|
239
|
+
return {
|
|
240
|
+
...response,
|
|
241
|
+
...(queryValidation.warnings.length > 0 && {
|
|
242
|
+
warnings: queryValidation.warnings,
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
logger.warn("Hosted API search failed, falling back to local", {
|
|
248
|
+
error: String(error),
|
|
249
|
+
});
|
|
250
|
+
// Fall through to local search
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Local search (fallback or when in local mode)
|
|
105
254
|
const filter = {
|
|
106
255
|
language: "markdown",
|
|
107
256
|
};
|
|
@@ -110,8 +259,8 @@ export async function searchDocs(input) {
|
|
|
110
259
|
// Docs are typically in the midnight-docs repo
|
|
111
260
|
filter.repository = "midnightntwrk/midnight-docs";
|
|
112
261
|
}
|
|
113
|
-
const results = await vectorStore.search(
|
|
114
|
-
|
|
262
|
+
const results = await vectorStore.search(sanitizedQuery, limit, filter);
|
|
263
|
+
const response = {
|
|
115
264
|
results: results.map((r) => ({
|
|
116
265
|
content: r.content,
|
|
117
266
|
relevanceScore: r.score,
|
|
@@ -122,9 +271,14 @@ export async function searchDocs(input) {
|
|
|
122
271
|
},
|
|
123
272
|
})),
|
|
124
273
|
totalResults: results.length,
|
|
125
|
-
query:
|
|
274
|
+
query: sanitizedQuery,
|
|
126
275
|
category: input.category,
|
|
276
|
+
...(queryValidation.warnings.length > 0 && {
|
|
277
|
+
warnings: queryValidation.warnings,
|
|
278
|
+
}),
|
|
127
279
|
};
|
|
280
|
+
searchCache.set(cacheKey, response);
|
|
281
|
+
return response;
|
|
128
282
|
}
|
|
129
283
|
// Tool definitions for MCP
|
|
130
284
|
export const searchTools = [
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic caching utilities for MCP server
|
|
3
|
+
* Provides TTL-based caching with memory management
|
|
4
|
+
*/
|
|
5
|
+
export interface CacheOptions {
|
|
6
|
+
ttl: number;
|
|
7
|
+
maxSize?: number;
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CacheEntry<T> {
|
|
11
|
+
value: T;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CacheStats {
|
|
16
|
+
hits: number;
|
|
17
|
+
misses: number;
|
|
18
|
+
size: number;
|
|
19
|
+
hitRate: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generic cache implementation with TTL and size limits
|
|
23
|
+
*/
|
|
24
|
+
export declare class Cache<T> {
|
|
25
|
+
private cache;
|
|
26
|
+
private options;
|
|
27
|
+
private stats;
|
|
28
|
+
constructor(options: CacheOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Get a value from the cache
|
|
31
|
+
*/
|
|
32
|
+
get(key: string): T | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Set a value in the cache
|
|
35
|
+
*/
|
|
36
|
+
set(key: string, value: T, ttl?: number): void;
|
|
37
|
+
/**
|
|
38
|
+
* Check if a key exists and is not expired
|
|
39
|
+
*/
|
|
40
|
+
has(key: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Delete a key from the cache
|
|
43
|
+
*/
|
|
44
|
+
delete(key: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Clear all entries from the cache
|
|
47
|
+
*/
|
|
48
|
+
clear(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Remove expired entries
|
|
51
|
+
*/
|
|
52
|
+
prune(): number;
|
|
53
|
+
/**
|
|
54
|
+
* Evict the oldest entry to make room
|
|
55
|
+
*/
|
|
56
|
+
private evictOldest;
|
|
57
|
+
/**
|
|
58
|
+
* Get cache statistics
|
|
59
|
+
*/
|
|
60
|
+
getStats(): CacheStats;
|
|
61
|
+
/**
|
|
62
|
+
* Get or set with a factory function
|
|
63
|
+
*/
|
|
64
|
+
getOrSet(key: string, factory: () => Promise<T>, ttl?: number): Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create a cache key from multiple parts
|
|
68
|
+
*/
|
|
69
|
+
export declare function createCacheKey(...parts: (string | number | boolean | undefined)[]): string;
|
|
70
|
+
export declare const searchCache: Cache<unknown>;
|
|
71
|
+
export declare const fileCache: Cache<string>;
|
|
72
|
+
export declare const metadataCache: Cache<unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* Prune all caches periodically
|
|
75
|
+
*/
|
|
76
|
+
export declare function pruneAllCaches(): void;
|
|
77
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic caching utilities for MCP server
|
|
3
|
+
* Provides TTL-based caching with memory management
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from "./logger.js";
|
|
6
|
+
/**
|
|
7
|
+
* Generic cache implementation with TTL and size limits
|
|
8
|
+
*/
|
|
9
|
+
export class Cache {
|
|
10
|
+
cache = new Map();
|
|
11
|
+
options;
|
|
12
|
+
stats = { hits: 0, misses: 0 };
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = {
|
|
15
|
+
ttl: options.ttl,
|
|
16
|
+
maxSize: options.maxSize || 1000,
|
|
17
|
+
name: options.name || "cache",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get a value from the cache
|
|
22
|
+
*/
|
|
23
|
+
get(key) {
|
|
24
|
+
const entry = this.cache.get(key);
|
|
25
|
+
if (!entry) {
|
|
26
|
+
this.stats.misses++;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
// Check if expired
|
|
30
|
+
if (Date.now() > entry.expiresAt) {
|
|
31
|
+
this.cache.delete(key);
|
|
32
|
+
this.stats.misses++;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
this.stats.hits++;
|
|
36
|
+
return entry.value;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Set a value in the cache
|
|
40
|
+
*/
|
|
41
|
+
set(key, value, ttl) {
|
|
42
|
+
// Enforce size limit
|
|
43
|
+
if (this.cache.size >= this.options.maxSize) {
|
|
44
|
+
this.evictOldest();
|
|
45
|
+
}
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
this.cache.set(key, {
|
|
48
|
+
value,
|
|
49
|
+
expiresAt: now + (ttl || this.options.ttl),
|
|
50
|
+
createdAt: now,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a key exists and is not expired
|
|
55
|
+
*/
|
|
56
|
+
has(key) {
|
|
57
|
+
const entry = this.cache.get(key);
|
|
58
|
+
if (!entry)
|
|
59
|
+
return false;
|
|
60
|
+
if (Date.now() > entry.expiresAt) {
|
|
61
|
+
this.cache.delete(key);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Delete a key from the cache
|
|
68
|
+
*/
|
|
69
|
+
delete(key) {
|
|
70
|
+
return this.cache.delete(key);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Clear all entries from the cache
|
|
74
|
+
*/
|
|
75
|
+
clear() {
|
|
76
|
+
this.cache.clear();
|
|
77
|
+
logger.debug(`Cache cleared: ${this.options.name}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Remove expired entries
|
|
81
|
+
*/
|
|
82
|
+
prune() {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
let pruned = 0;
|
|
85
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
86
|
+
if (now > entry.expiresAt) {
|
|
87
|
+
this.cache.delete(key);
|
|
88
|
+
pruned++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (pruned > 0) {
|
|
92
|
+
logger.debug(`Cache pruned: ${this.options.name}`, { pruned });
|
|
93
|
+
}
|
|
94
|
+
return pruned;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Evict the oldest entry to make room
|
|
98
|
+
*/
|
|
99
|
+
evictOldest() {
|
|
100
|
+
let oldestKey = null;
|
|
101
|
+
let oldestTime = Infinity;
|
|
102
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
103
|
+
if (entry.createdAt < oldestTime) {
|
|
104
|
+
oldestTime = entry.createdAt;
|
|
105
|
+
oldestKey = key;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (oldestKey) {
|
|
109
|
+
this.cache.delete(oldestKey);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get cache statistics
|
|
114
|
+
*/
|
|
115
|
+
getStats() {
|
|
116
|
+
const total = this.stats.hits + this.stats.misses;
|
|
117
|
+
return {
|
|
118
|
+
hits: this.stats.hits,
|
|
119
|
+
misses: this.stats.misses,
|
|
120
|
+
size: this.cache.size,
|
|
121
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get or set with a factory function
|
|
126
|
+
*/
|
|
127
|
+
async getOrSet(key, factory, ttl) {
|
|
128
|
+
const cached = this.get(key);
|
|
129
|
+
if (cached !== undefined) {
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
const value = await factory();
|
|
133
|
+
this.set(key, value, ttl);
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Create a cache key from multiple parts
|
|
139
|
+
*/
|
|
140
|
+
export function createCacheKey(...parts) {
|
|
141
|
+
return parts
|
|
142
|
+
.filter((p) => p !== undefined)
|
|
143
|
+
.map((p) => String(p))
|
|
144
|
+
.join(":");
|
|
145
|
+
}
|
|
146
|
+
// Pre-configured caches for common use cases
|
|
147
|
+
export const searchCache = new Cache({
|
|
148
|
+
ttl: 5 * 60 * 1000, // 5 minutes
|
|
149
|
+
maxSize: 500,
|
|
150
|
+
name: "search",
|
|
151
|
+
});
|
|
152
|
+
export const fileCache = new Cache({
|
|
153
|
+
ttl: 10 * 60 * 1000, // 10 minutes
|
|
154
|
+
maxSize: 200,
|
|
155
|
+
name: "file",
|
|
156
|
+
});
|
|
157
|
+
export const metadataCache = new Cache({
|
|
158
|
+
ttl: 15 * 60 * 1000, // 15 minutes
|
|
159
|
+
maxSize: 100,
|
|
160
|
+
name: "metadata",
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* Prune all caches periodically
|
|
164
|
+
*/
|
|
165
|
+
export function pruneAllCaches() {
|
|
166
|
+
searchCache.prune();
|
|
167
|
+
fileCache.prune();
|
|
168
|
+
metadataCache.prune();
|
|
169
|
+
}
|
|
170
|
+
// Auto-prune every 5 minutes
|
|
171
|
+
setInterval(pruneAllCaches, 5 * 60 * 1000);
|
|
172
|
+
//# sourceMappingURL=cache.js.map
|