midnight-mcp 0.0.1 → 0.0.4
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 +40 -20
- 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 +3 -45
- package/dist/tools/search.js +101 -13
- package/dist/utils/cache.d.ts +77 -0
- package/dist/utils/cache.js +172 -0
- 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/index.d.ts +9 -0
- package/dist/utils/index.js +9 -0
- 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 +3 -2
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/index.js.map +0 -1
- package/dist/db/vectorStore.d.ts.map +0 -1
- package/dist/db/vectorStore.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/pipeline/embeddings.d.ts.map +0 -1
- package/dist/pipeline/embeddings.js.map +0 -1
- package/dist/pipeline/github.d.ts.map +0 -1
- package/dist/pipeline/github.js.map +0 -1
- package/dist/pipeline/index.d.ts.map +0 -1
- package/dist/pipeline/index.js.map +0 -1
- package/dist/pipeline/indexer.d.ts.map +0 -1
- package/dist/pipeline/indexer.js.map +0 -1
- package/dist/pipeline/parser.d.ts.map +0 -1
- package/dist/pipeline/parser.js.map +0 -1
- package/dist/pipeline/releases.d.ts.map +0 -1
- package/dist/pipeline/releases.js.map +0 -1
- package/dist/pipeline/repository.d.ts.map +0 -1
- package/dist/pipeline/repository.js.map +0 -1
- package/dist/prompts/index.d.ts.map +0 -1
- package/dist/prompts/index.js.map +0 -1
- package/dist/prompts/templates.d.ts.map +0 -1
- package/dist/prompts/templates.js.map +0 -1
- package/dist/resources/code.d.ts.map +0 -1
- package/dist/resources/code.js.map +0 -1
- package/dist/resources/docs.d.ts.map +0 -1
- package/dist/resources/docs.js.map +0 -1
- package/dist/resources/index.d.ts.map +0 -1
- package/dist/resources/index.js.map +0 -1
- package/dist/resources/schemas.d.ts.map +0 -1
- package/dist/resources/schemas.js.map +0 -1
- package/dist/scripts/index-repos.d.ts.map +0 -1
- package/dist/scripts/index-repos.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/tools/analyze.d.ts.map +0 -1
- package/dist/tools/analyze.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/repository.d.ts.map +0 -1
- package/dist/tools/repository.js.map +0 -1
- package/dist/tools/search.d.ts.map +0 -1
- package/dist/tools/search.js.map +0 -1
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
package/dist/tools/search.d.ts
CHANGED
|
@@ -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, } 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,39 @@ 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
|
+
originalQuery: input.query,
|
|
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
|
+
}
|
|
48
74
|
const filter = {
|
|
49
75
|
language: "compact",
|
|
50
76
|
...input.filter,
|
|
51
77
|
};
|
|
52
|
-
const results = await vectorStore.search(
|
|
53
|
-
|
|
78
|
+
const results = await vectorStore.search(sanitizedQuery, limit, filter);
|
|
79
|
+
const response = {
|
|
54
80
|
results: results.map((r) => ({
|
|
55
81
|
code: r.content,
|
|
56
82
|
relevanceScore: r.score,
|
|
@@ -63,24 +89,53 @@ export async function searchCompact(input) {
|
|
|
63
89
|
name: r.metadata.codeName,
|
|
64
90
|
})),
|
|
65
91
|
totalResults: results.length,
|
|
66
|
-
query:
|
|
92
|
+
query: sanitizedQuery,
|
|
93
|
+
...(queryValidation.warnings.length > 0 && {
|
|
94
|
+
warnings: queryValidation.warnings,
|
|
95
|
+
}),
|
|
67
96
|
};
|
|
97
|
+
// Cache the response
|
|
98
|
+
searchCache.set(cacheKey, response);
|
|
99
|
+
return response;
|
|
68
100
|
}
|
|
69
101
|
/**
|
|
70
102
|
* Search TypeScript SDK code, types, and API implementations
|
|
71
103
|
*/
|
|
72
104
|
export async function searchTypeScript(input) {
|
|
73
|
-
|
|
105
|
+
// Validate input
|
|
106
|
+
const queryValidation = validateQuery(input.query);
|
|
107
|
+
if (!queryValidation.isValid) {
|
|
108
|
+
return {
|
|
109
|
+
error: "Invalid query",
|
|
110
|
+
details: queryValidation.errors,
|
|
111
|
+
suggestion: "Provide a valid search query with at least 2 characters",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const limitValidation = validateNumber(input.limit, {
|
|
115
|
+
min: 1,
|
|
116
|
+
max: 50,
|
|
117
|
+
defaultValue: 10,
|
|
118
|
+
});
|
|
119
|
+
const sanitizedQuery = queryValidation.sanitized;
|
|
120
|
+
const limit = limitValidation.value;
|
|
121
|
+
logger.debug("Searching TypeScript code", { query: sanitizedQuery });
|
|
122
|
+
// Check cache
|
|
123
|
+
const cacheKey = createCacheKey("typescript", sanitizedQuery, limit, input.includeTypes, input.includeExamples);
|
|
124
|
+
const cached = searchCache.get(cacheKey);
|
|
125
|
+
if (cached) {
|
|
126
|
+
logger.debug("Search cache hit", { cacheKey });
|
|
127
|
+
return cached;
|
|
128
|
+
}
|
|
74
129
|
const filter = {
|
|
75
130
|
language: "typescript",
|
|
76
131
|
};
|
|
77
|
-
const results = await vectorStore.search(
|
|
132
|
+
const results = await vectorStore.search(sanitizedQuery, limit, filter);
|
|
78
133
|
// Filter based on type preferences
|
|
79
134
|
let filteredResults = results;
|
|
80
135
|
if (!input.includeTypes) {
|
|
81
136
|
filteredResults = results.filter((r) => r.metadata.codeType !== "type" && r.metadata.codeType !== "interface");
|
|
82
137
|
}
|
|
83
|
-
|
|
138
|
+
const response = {
|
|
84
139
|
results: filteredResults.map((r) => ({
|
|
85
140
|
code: r.content,
|
|
86
141
|
relevanceScore: r.score,
|
|
@@ -94,14 +149,42 @@ export async function searchTypeScript(input) {
|
|
|
94
149
|
isExported: r.metadata.isPublic,
|
|
95
150
|
})),
|
|
96
151
|
totalResults: filteredResults.length,
|
|
97
|
-
query:
|
|
152
|
+
query: sanitizedQuery,
|
|
153
|
+
...(queryValidation.warnings.length > 0 && {
|
|
154
|
+
warnings: queryValidation.warnings,
|
|
155
|
+
}),
|
|
98
156
|
};
|
|
157
|
+
searchCache.set(cacheKey, response);
|
|
158
|
+
return response;
|
|
99
159
|
}
|
|
100
160
|
/**
|
|
101
161
|
* Full-text search across official Midnight documentation
|
|
102
162
|
*/
|
|
103
163
|
export async function searchDocs(input) {
|
|
104
|
-
|
|
164
|
+
// Validate input
|
|
165
|
+
const queryValidation = validateQuery(input.query);
|
|
166
|
+
if (!queryValidation.isValid) {
|
|
167
|
+
return {
|
|
168
|
+
error: "Invalid query",
|
|
169
|
+
details: queryValidation.errors,
|
|
170
|
+
suggestion: "Provide a valid search query with at least 2 characters",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const limitValidation = validateNumber(input.limit, {
|
|
174
|
+
min: 1,
|
|
175
|
+
max: 50,
|
|
176
|
+
defaultValue: 10,
|
|
177
|
+
});
|
|
178
|
+
const sanitizedQuery = queryValidation.sanitized;
|
|
179
|
+
const limit = limitValidation.value;
|
|
180
|
+
logger.debug("Searching documentation", { query: sanitizedQuery });
|
|
181
|
+
// Check cache
|
|
182
|
+
const cacheKey = createCacheKey("docs", sanitizedQuery, limit, input.category);
|
|
183
|
+
const cached = searchCache.get(cacheKey);
|
|
184
|
+
if (cached) {
|
|
185
|
+
logger.debug("Search cache hit", { cacheKey });
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
105
188
|
const filter = {
|
|
106
189
|
language: "markdown",
|
|
107
190
|
};
|
|
@@ -110,8 +193,8 @@ export async function searchDocs(input) {
|
|
|
110
193
|
// Docs are typically in the midnight-docs repo
|
|
111
194
|
filter.repository = "midnightntwrk/midnight-docs";
|
|
112
195
|
}
|
|
113
|
-
const results = await vectorStore.search(
|
|
114
|
-
|
|
196
|
+
const results = await vectorStore.search(sanitizedQuery, limit, filter);
|
|
197
|
+
const response = {
|
|
115
198
|
results: results.map((r) => ({
|
|
116
199
|
content: r.content,
|
|
117
200
|
relevanceScore: r.score,
|
|
@@ -122,9 +205,14 @@ export async function searchDocs(input) {
|
|
|
122
205
|
},
|
|
123
206
|
})),
|
|
124
207
|
totalResults: results.length,
|
|
125
|
-
query:
|
|
208
|
+
query: sanitizedQuery,
|
|
126
209
|
category: input.category,
|
|
210
|
+
...(queryValidation.warnings.length > 0 && {
|
|
211
|
+
warnings: queryValidation.warnings,
|
|
212
|
+
}),
|
|
127
213
|
};
|
|
214
|
+
searchCache.set(cacheKey, response);
|
|
215
|
+
return response;
|
|
128
216
|
}
|
|
129
217
|
// Tool definitions for MCP
|
|
130
218
|
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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-friendly error messages and error handling utilities
|
|
3
|
+
*/
|
|
4
|
+
export declare class MCPError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly suggestion?: string;
|
|
7
|
+
readonly details?: Record<string, unknown>;
|
|
8
|
+
constructor(message: string, code: string, suggestion?: string, details?: Record<string, unknown>);
|
|
9
|
+
toJSON(): {
|
|
10
|
+
details?: Record<string, unknown> | undefined;
|
|
11
|
+
error: string;
|
|
12
|
+
code: string;
|
|
13
|
+
suggestion: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Common error codes
|
|
18
|
+
*/
|
|
19
|
+
export declare const ErrorCodes: {
|
|
20
|
+
readonly RATE_LIMIT: "RATE_LIMIT_EXCEEDED";
|
|
21
|
+
readonly NOT_FOUND: "RESOURCE_NOT_FOUND";
|
|
22
|
+
readonly NETWORK: "NETWORK_ERROR";
|
|
23
|
+
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
24
|
+
readonly UNKNOWN_REPO: "UNKNOWN_REPOSITORY";
|
|
25
|
+
readonly PARSE_ERROR: "PARSE_ERROR";
|
|
26
|
+
readonly CHROMADB_UNAVAILABLE: "CHROMADB_UNAVAILABLE";
|
|
27
|
+
readonly OPENAI_UNAVAILABLE: "OPENAI_UNAVAILABLE";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Create user-friendly error from various error types
|
|
31
|
+
*/
|
|
32
|
+
export declare function createUserError(error: unknown, context?: string): MCPError;
|
|
33
|
+
/**
|
|
34
|
+
* Format error for MCP response
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatErrorResponse(error: unknown, context?: string): {
|
|
37
|
+
error: string;
|
|
38
|
+
code: string;
|
|
39
|
+
suggestion?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Wrap a function with error handling
|
|
43
|
+
*/
|
|
44
|
+
export declare function withErrorHandling<T extends (...args: unknown[]) => Promise<unknown>>(fn: T, context: string): T;
|
|
45
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-friendly error messages and error handling utilities
|
|
3
|
+
*/
|
|
4
|
+
export class MCPError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
suggestion;
|
|
7
|
+
details;
|
|
8
|
+
constructor(message, code, suggestion, details) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "MCPError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.suggestion = suggestion;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
toJSON() {
|
|
16
|
+
return {
|
|
17
|
+
error: this.message,
|
|
18
|
+
code: this.code,
|
|
19
|
+
suggestion: this.suggestion,
|
|
20
|
+
...(this.details && { details: this.details }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Common error codes
|
|
26
|
+
*/
|
|
27
|
+
export const ErrorCodes = {
|
|
28
|
+
RATE_LIMIT: "RATE_LIMIT_EXCEEDED",
|
|
29
|
+
NOT_FOUND: "RESOURCE_NOT_FOUND",
|
|
30
|
+
NETWORK: "NETWORK_ERROR",
|
|
31
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
32
|
+
UNKNOWN_REPO: "UNKNOWN_REPOSITORY",
|
|
33
|
+
PARSE_ERROR: "PARSE_ERROR",
|
|
34
|
+
CHROMADB_UNAVAILABLE: "CHROMADB_UNAVAILABLE",
|
|
35
|
+
OPENAI_UNAVAILABLE: "OPENAI_UNAVAILABLE",
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Create user-friendly error from various error types
|
|
39
|
+
*/
|
|
40
|
+
export function createUserError(error, context) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
const ctx = context ? ` while ${context}` : "";
|
|
43
|
+
// Rate limit errors
|
|
44
|
+
if (message.includes("rate limit") ||
|
|
45
|
+
message.includes("403") ||
|
|
46
|
+
message.includes("API rate limit")) {
|
|
47
|
+
return new MCPError(`GitHub API rate limit exceeded${ctx}`, ErrorCodes.RATE_LIMIT, "Add GITHUB_TOKEN to your config to increase limits from 60 to 5000 requests/hour. " +
|
|
48
|
+
"Get a token at https://github.com/settings/tokens");
|
|
49
|
+
}
|
|
50
|
+
// Not found errors
|
|
51
|
+
if (message.includes("404") || message.includes("Not Found")) {
|
|
52
|
+
return new MCPError(`Resource not found${ctx}`, ErrorCodes.NOT_FOUND, "Check that the repository, file, or version exists and is publicly accessible.");
|
|
53
|
+
}
|
|
54
|
+
// Network errors
|
|
55
|
+
if (message.includes("network") ||
|
|
56
|
+
message.includes("ECONNREFUSED") ||
|
|
57
|
+
message.includes("ETIMEDOUT") ||
|
|
58
|
+
message.includes("timeout")) {
|
|
59
|
+
return new MCPError(`Network error${ctx}`, ErrorCodes.NETWORK, "Check your internet connection and try again. If the problem persists, " +
|
|
60
|
+
"the service may be temporarily unavailable.");
|
|
61
|
+
}
|
|
62
|
+
// ChromaDB errors
|
|
63
|
+
if (message.includes("chroma") || message.includes("8000")) {
|
|
64
|
+
return new MCPError(`ChromaDB is not available${ctx}`, ErrorCodes.CHROMADB_UNAVAILABLE, "ChromaDB is optional. Without it, search uses keyword matching instead of semantic search. " +
|
|
65
|
+
"To enable semantic search, run: docker run -d -p 8000:8000 chromadb/chroma");
|
|
66
|
+
}
|
|
67
|
+
// OpenAI errors
|
|
68
|
+
if (message.includes("openai") || message.includes("embedding")) {
|
|
69
|
+
return new MCPError(`OpenAI API error${ctx}`, ErrorCodes.OPENAI_UNAVAILABLE, "OpenAI is optional. Without it, search uses keyword matching. " +
|
|
70
|
+
"To enable semantic search, add OPENAI_API_KEY to your config.");
|
|
71
|
+
}
|
|
72
|
+
// Default error
|
|
73
|
+
return new MCPError(`An error occurred${ctx}: ${message}`, "UNKNOWN_ERROR", "If this problem persists, please report it at https://github.com/Olanetsoft/midnight-mcp/issues");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format error for MCP response
|
|
77
|
+
*/
|
|
78
|
+
export function formatErrorResponse(error, context) {
|
|
79
|
+
const mcpError = error instanceof MCPError ? error : createUserError(error, context);
|
|
80
|
+
return mcpError.toJSON();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Wrap a function with error handling
|
|
84
|
+
*/
|
|
85
|
+
export function withErrorHandling(fn, context) {
|
|
86
|
+
return (async (...args) => {
|
|
87
|
+
try {
|
|
88
|
+
return await fn(...args);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw createUserError(error, context);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health check utilities for MCP server monitoring
|
|
3
|
+
*/
|
|
4
|
+
export interface HealthStatus {
|
|
5
|
+
status: "healthy" | "degraded" | "unhealthy";
|
|
6
|
+
timestamp: string;
|
|
7
|
+
version: string;
|
|
8
|
+
uptime: number;
|
|
9
|
+
checks: {
|
|
10
|
+
name: string;
|
|
11
|
+
status: "pass" | "warn" | "fail";
|
|
12
|
+
message?: string;
|
|
13
|
+
latency?: number;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Perform a full health check
|
|
18
|
+
*/
|
|
19
|
+
export declare function getHealthStatus(): Promise<HealthStatus>;
|
|
20
|
+
/**
|
|
21
|
+
* Get a quick health check (no external calls)
|
|
22
|
+
*/
|
|
23
|
+
export declare function getQuickHealthStatus(): Omit<HealthStatus, "checks"> & {
|
|
24
|
+
checks: {
|
|
25
|
+
name: string;
|
|
26
|
+
status: "pass";
|
|
27
|
+
}[];
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=health.d.ts.map
|