recker 1.0.12 → 1.0.13
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/dist/cache/memory-limits.d.ts +21 -0
- package/dist/cache/memory-limits.d.ts.map +1 -0
- package/dist/cache/memory-limits.js +96 -0
- package/dist/cache/memory-storage.d.ts +125 -2
- package/dist/cache/memory-storage.d.ts.map +1 -1
- package/dist/cache/memory-storage.js +437 -14
- package/dist/cli/tui/shell-search.d.ts +46 -0
- package/dist/cli/tui/shell-search.d.ts.map +1 -0
- package/dist/cli/tui/shell-search.js +452 -0
- package/dist/cli/tui/shell.d.ts +4 -0
- package/dist/cli/tui/shell.d.ts.map +1 -1
- package/dist/cli/tui/shell.js +136 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/mcp/data/embeddings.json +1 -0
- package/dist/mcp/search/hybrid-search.d.ts +30 -0
- package/dist/mcp/search/hybrid-search.d.ts.map +1 -0
- package/dist/mcp/search/hybrid-search.js +332 -0
- package/dist/mcp/search/index.d.ts +4 -0
- package/dist/mcp/search/index.d.ts.map +1 -0
- package/dist/mcp/search/index.js +2 -0
- package/dist/mcp/search/math.d.ts +6 -0
- package/dist/mcp/search/math.d.ts.map +1 -0
- package/dist/mcp/search/math.js +59 -0
- package/dist/mcp/search/types.d.ts +49 -0
- package/dist/mcp/search/types.d.ts.map +1 -0
- package/dist/mcp/search/types.js +1 -0
- package/dist/mcp/server.d.ts +27 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +688 -33
- package/package.json +8 -3
package/dist/mcp/server.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
|
-
import { join, relative } from 'path';
|
|
3
|
+
import { join, relative, extname, basename, dirname } from 'path';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createHybridSearch } from './search/index.js';
|
|
5
7
|
export class MCPServer {
|
|
6
8
|
options;
|
|
7
9
|
server;
|
|
10
|
+
hybridSearch;
|
|
8
11
|
docsIndex = [];
|
|
12
|
+
codeExamples = [];
|
|
13
|
+
typeDefinitions = [];
|
|
9
14
|
sseClients = new Set();
|
|
10
15
|
initialized = false;
|
|
11
16
|
constructor(options = {}) {
|
|
@@ -13,10 +18,14 @@ export class MCPServer {
|
|
|
13
18
|
name: options.name || 'recker-docs',
|
|
14
19
|
version: options.version || '1.0.0',
|
|
15
20
|
docsPath: options.docsPath || this.findDocsPath(),
|
|
21
|
+
examplesPath: options.examplesPath || this.findExamplesPath(),
|
|
22
|
+
srcPath: options.srcPath || this.findSrcPath(),
|
|
16
23
|
port: options.port || 3100,
|
|
17
24
|
transport: options.transport || 'stdio',
|
|
18
25
|
debug: options.debug || false,
|
|
26
|
+
toolsFilter: options.toolsFilter || [],
|
|
19
27
|
};
|
|
28
|
+
this.hybridSearch = createHybridSearch({ debug: this.options.debug });
|
|
20
29
|
this.buildIndex();
|
|
21
30
|
}
|
|
22
31
|
log(message, data) {
|
|
@@ -34,9 +43,12 @@ export class MCPServer {
|
|
|
34
43
|
join(process.cwd(), 'docs'),
|
|
35
44
|
join(process.cwd(), '..', 'docs'),
|
|
36
45
|
];
|
|
37
|
-
|
|
46
|
+
try {
|
|
47
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
48
|
possiblePaths.push(join(__dirname, '..', '..', 'docs'), join(__dirname, '..', '..', '..', 'docs'));
|
|
39
49
|
}
|
|
50
|
+
catch {
|
|
51
|
+
}
|
|
40
52
|
for (const p of possiblePaths) {
|
|
41
53
|
if (existsSync(p)) {
|
|
42
54
|
return p;
|
|
@@ -44,13 +56,56 @@ export class MCPServer {
|
|
|
44
56
|
}
|
|
45
57
|
return join(process.cwd(), 'docs');
|
|
46
58
|
}
|
|
47
|
-
|
|
59
|
+
findExamplesPath() {
|
|
60
|
+
const docsPath = this.options?.docsPath || this.findDocsPath();
|
|
61
|
+
const possiblePaths = [
|
|
62
|
+
join(docsPath, 'examples'),
|
|
63
|
+
join(process.cwd(), 'examples'),
|
|
64
|
+
join(process.cwd(), 'docs', 'examples'),
|
|
65
|
+
];
|
|
66
|
+
for (const p of possiblePaths) {
|
|
67
|
+
if (existsSync(p)) {
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return join(docsPath, 'examples');
|
|
72
|
+
}
|
|
73
|
+
findSrcPath() {
|
|
74
|
+
const possiblePaths = [
|
|
75
|
+
join(process.cwd(), 'src'),
|
|
76
|
+
];
|
|
77
|
+
try {
|
|
78
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
79
|
+
possiblePaths.push(join(__dirname, '..'), join(__dirname, '..', '..', 'src'));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
}
|
|
83
|
+
for (const p of possiblePaths) {
|
|
84
|
+
if (existsSync(p)) {
|
|
85
|
+
return p;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return join(process.cwd(), 'src');
|
|
89
|
+
}
|
|
90
|
+
async buildIndex() {
|
|
91
|
+
await this.indexDocs();
|
|
92
|
+
this.indexCodeExamples();
|
|
93
|
+
this.indexTypeDefinitions();
|
|
94
|
+
await this.hybridSearch.initialize(this.docsIndex);
|
|
95
|
+
const stats = this.hybridSearch.getStats();
|
|
96
|
+
this.log(`Indexed ${stats.documents} docs, ${this.codeExamples.length} examples, ${this.typeDefinitions.length} types`);
|
|
97
|
+
if (stats.embeddings > 0) {
|
|
98
|
+
this.log(`Loaded ${stats.embeddings} embeddings (model: ${stats.model})`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async indexDocs() {
|
|
48
102
|
if (!existsSync(this.options.docsPath)) {
|
|
49
103
|
this.log(`Docs path not found: ${this.options.docsPath}`);
|
|
50
104
|
return;
|
|
51
105
|
}
|
|
52
106
|
const files = this.walkDir(this.options.docsPath);
|
|
53
|
-
for (
|
|
107
|
+
for (let i = 0; i < files.length; i++) {
|
|
108
|
+
const file = files[i];
|
|
54
109
|
if (!file.endsWith('.md'))
|
|
55
110
|
continue;
|
|
56
111
|
try {
|
|
@@ -60,6 +115,7 @@ export class MCPServer {
|
|
|
60
115
|
const title = this.extractTitle(content) || relativePath;
|
|
61
116
|
const keywords = this.extractKeywords(content);
|
|
62
117
|
this.docsIndex.push({
|
|
118
|
+
id: `doc-${i}`,
|
|
63
119
|
path: relativePath,
|
|
64
120
|
title,
|
|
65
121
|
category,
|
|
@@ -71,14 +127,239 @@ export class MCPServer {
|
|
|
71
127
|
this.log(`Failed to index ${file}:`, err);
|
|
72
128
|
}
|
|
73
129
|
}
|
|
74
|
-
|
|
130
|
+
}
|
|
131
|
+
indexCodeExamples() {
|
|
132
|
+
if (!existsSync(this.options.examplesPath)) {
|
|
133
|
+
this.log(`Examples path not found: ${this.options.examplesPath}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const files = this.walkDir(this.options.examplesPath);
|
|
137
|
+
for (let i = 0; i < files.length; i++) {
|
|
138
|
+
const file = files[i];
|
|
139
|
+
const ext = extname(file);
|
|
140
|
+
if (!['.ts', '.js', '.mjs'].includes(ext))
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
const content = readFileSync(file, 'utf-8');
|
|
144
|
+
const relativePath = relative(this.options.examplesPath, file);
|
|
145
|
+
const filename = basename(file, ext);
|
|
146
|
+
const example = this.parseCodeExample(content, relativePath, filename, i);
|
|
147
|
+
if (example) {
|
|
148
|
+
this.codeExamples.push(example);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
this.log(`Failed to index example ${file}:`, err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
parseCodeExample(content, path, filename, index) {
|
|
157
|
+
const docMatch = content.match(/^\/\*\*[\s\S]*?\*\//) || content.match(/^\/\/.*(?:\n\/\/.*)*/);
|
|
158
|
+
const docComment = docMatch ? docMatch[0] : '';
|
|
159
|
+
const titleMatch = docComment.match(/@title\s+(.+)/i) || docComment.match(/@example\s+(.+)/i);
|
|
160
|
+
const title = titleMatch ? titleMatch[1].trim() : this.humanizeFilename(filename);
|
|
161
|
+
const featureMatch = docComment.match(/@feature\s+(\w+)/i);
|
|
162
|
+
const feature = featureMatch ? featureMatch[1].toLowerCase() : this.inferFeature(path, content);
|
|
163
|
+
const complexity = this.inferComplexity(content, docComment);
|
|
164
|
+
const descMatch = docComment.match(/@description\s+(.+)/i);
|
|
165
|
+
const description = descMatch
|
|
166
|
+
? descMatch[1].trim()
|
|
167
|
+
: docComment.replace(/^\/\*\*|\*\/|^\s*\*\s?|^\/\/\s?/gm, '').trim().split('\n')[0] || '';
|
|
168
|
+
const keywords = this.extractCodeKeywords(content, feature);
|
|
169
|
+
return {
|
|
170
|
+
id: `example-${index}`,
|
|
171
|
+
path,
|
|
172
|
+
title,
|
|
173
|
+
feature,
|
|
174
|
+
complexity,
|
|
175
|
+
code: content,
|
|
176
|
+
description,
|
|
177
|
+
keywords,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
humanizeFilename(filename) {
|
|
181
|
+
return filename
|
|
182
|
+
.replace(/[-_]/g, ' ')
|
|
183
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
184
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
185
|
+
}
|
|
186
|
+
inferFeature(path, content) {
|
|
187
|
+
const pathParts = path.toLowerCase().split('/');
|
|
188
|
+
const featureKeywords = [
|
|
189
|
+
'retry', 'cache', 'streaming', 'sse', 'websocket', 'batch',
|
|
190
|
+
'pagination', 'middleware', 'hooks', 'auth', 'proxy', 'timeout',
|
|
191
|
+
'mcp', 'ai', 'scraping', 'whois', 'dns', 'udp', 'webrtc',
|
|
192
|
+
];
|
|
193
|
+
for (const keyword of featureKeywords) {
|
|
194
|
+
if (pathParts.some(p => p.includes(keyword))) {
|
|
195
|
+
return keyword;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const lowerContent = content.toLowerCase();
|
|
199
|
+
for (const keyword of featureKeywords) {
|
|
200
|
+
if (lowerContent.includes(keyword)) {
|
|
201
|
+
return keyword;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return 'general';
|
|
205
|
+
}
|
|
206
|
+
inferComplexity(content, docComment) {
|
|
207
|
+
if (/@complexity\s+(basic|intermediate|advanced)/i.test(docComment)) {
|
|
208
|
+
return docComment.match(/@complexity\s+(basic|intermediate|advanced)/i)[1].toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
const lines = content.split('\n').length;
|
|
211
|
+
const hasAsyncAwait = /async|await/.test(content);
|
|
212
|
+
const hasClasses = /class\s+\w+/.test(content);
|
|
213
|
+
const hasComplexTypes = /\<[^>]+\>/.test(content);
|
|
214
|
+
const hasTryCatch = /try\s*\{/.test(content);
|
|
215
|
+
const hasMultipleFunctions = (content.match(/(?:function|const\s+\w+\s*=\s*(?:async\s*)?\()/g) || []).length > 2;
|
|
216
|
+
let score = 0;
|
|
217
|
+
if (lines > 50)
|
|
218
|
+
score += 2;
|
|
219
|
+
else if (lines > 20)
|
|
220
|
+
score += 1;
|
|
221
|
+
if (hasAsyncAwait)
|
|
222
|
+
score += 1;
|
|
223
|
+
if (hasClasses)
|
|
224
|
+
score += 2;
|
|
225
|
+
if (hasComplexTypes)
|
|
226
|
+
score += 1;
|
|
227
|
+
if (hasTryCatch)
|
|
228
|
+
score += 1;
|
|
229
|
+
if (hasMultipleFunctions)
|
|
230
|
+
score += 1;
|
|
231
|
+
if (score >= 5)
|
|
232
|
+
return 'advanced';
|
|
233
|
+
if (score >= 2)
|
|
234
|
+
return 'intermediate';
|
|
235
|
+
return 'basic';
|
|
236
|
+
}
|
|
237
|
+
extractCodeKeywords(content, feature) {
|
|
238
|
+
const keywords = new Set([feature]);
|
|
239
|
+
const funcMatches = content.match(/(?:function|const|let|var)\s+(\w+)/g) || [];
|
|
240
|
+
for (const m of funcMatches) {
|
|
241
|
+
const name = m.split(/\s+/)[1];
|
|
242
|
+
if (name && name.length > 2)
|
|
243
|
+
keywords.add(name.toLowerCase());
|
|
244
|
+
}
|
|
245
|
+
const importMatches = content.match(/from\s+['"]([^'"]+)['"]/g) || [];
|
|
246
|
+
for (const m of importMatches) {
|
|
247
|
+
const mod = m.match(/['"]([^'"]+)['"]/)?.[1];
|
|
248
|
+
if (mod && !mod.startsWith('.')) {
|
|
249
|
+
keywords.add(mod.split('/')[0]);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const methodMatches = content.match(/\.(\w+)\(/g) || [];
|
|
253
|
+
for (const m of methodMatches) {
|
|
254
|
+
const method = m.slice(1, -1);
|
|
255
|
+
if (method.length > 2)
|
|
256
|
+
keywords.add(method.toLowerCase());
|
|
257
|
+
}
|
|
258
|
+
return Array.from(keywords).slice(0, 20);
|
|
259
|
+
}
|
|
260
|
+
indexTypeDefinitions() {
|
|
261
|
+
if (!existsSync(this.options.srcPath)) {
|
|
262
|
+
this.log(`Source path not found: ${this.options.srcPath}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const typePaths = [
|
|
266
|
+
join(this.options.srcPath, 'types'),
|
|
267
|
+
join(this.options.srcPath, 'core'),
|
|
268
|
+
];
|
|
269
|
+
for (const typePath of typePaths) {
|
|
270
|
+
if (!existsSync(typePath))
|
|
271
|
+
continue;
|
|
272
|
+
const files = this.walkDir(typePath);
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
if (!file.endsWith('.ts') || file.endsWith('.test.ts'))
|
|
275
|
+
continue;
|
|
276
|
+
try {
|
|
277
|
+
const content = readFileSync(file, 'utf-8');
|
|
278
|
+
const relativePath = relative(this.options.srcPath, file);
|
|
279
|
+
this.extractTypeDefinitions(content, relativePath);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
this.log(`Failed to parse types from ${file}:`, err);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
extractTypeDefinitions(content, path) {
|
|
288
|
+
const interfaceRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(export\s+)?interface\s+(\w+)(?:\s+extends\s+[\w,\s<>]+)?\s*\{[^}]*\}/g;
|
|
289
|
+
let match;
|
|
290
|
+
while ((match = interfaceRegex.exec(content)) !== null) {
|
|
291
|
+
const name = match[2];
|
|
292
|
+
const fullMatch = match[0];
|
|
293
|
+
const docMatch = fullMatch.match(/\/\*\*[\s\S]*?\*\//);
|
|
294
|
+
const description = docMatch
|
|
295
|
+
? docMatch[0].replace(/\/\*\*|\*\/|\*\s?/g, '').trim().split('\n')[0]
|
|
296
|
+
: '';
|
|
297
|
+
const propsMatch = fullMatch.match(/\{([^}]*)\}/);
|
|
298
|
+
const properties = propsMatch
|
|
299
|
+
? this.parseInterfaceProperties(propsMatch[1])
|
|
300
|
+
: [];
|
|
301
|
+
this.typeDefinitions.push({
|
|
302
|
+
name,
|
|
303
|
+
kind: 'interface',
|
|
304
|
+
path,
|
|
305
|
+
definition: fullMatch,
|
|
306
|
+
description,
|
|
307
|
+
properties,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const typeRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(export\s+)?type\s+(\w+)(?:<[^>]+>)?\s*=\s*[^;]+;/g;
|
|
311
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
312
|
+
const name = match[2];
|
|
313
|
+
const fullMatch = match[0];
|
|
314
|
+
const docMatch = fullMatch.match(/\/\*\*[\s\S]*?\*\//);
|
|
315
|
+
const description = docMatch
|
|
316
|
+
? docMatch[0].replace(/\/\*\*|\*\/|\*\s?/g, '').trim().split('\n')[0]
|
|
317
|
+
: '';
|
|
318
|
+
this.typeDefinitions.push({
|
|
319
|
+
name,
|
|
320
|
+
kind: 'type',
|
|
321
|
+
path,
|
|
322
|
+
definition: fullMatch,
|
|
323
|
+
description,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const enumRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(export\s+)?enum\s+(\w+)\s*\{[^}]*\}/g;
|
|
327
|
+
while ((match = enumRegex.exec(content)) !== null) {
|
|
328
|
+
const name = match[2];
|
|
329
|
+
const fullMatch = match[0];
|
|
330
|
+
const docMatch = fullMatch.match(/\/\*\*[\s\S]*?\*\//);
|
|
331
|
+
const description = docMatch
|
|
332
|
+
? docMatch[0].replace(/\/\*\*|\*\/|\*\s?/g, '').trim().split('\n')[0]
|
|
333
|
+
: '';
|
|
334
|
+
this.typeDefinitions.push({
|
|
335
|
+
name,
|
|
336
|
+
kind: 'enum',
|
|
337
|
+
path,
|
|
338
|
+
definition: fullMatch,
|
|
339
|
+
description,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
parseInterfaceProperties(propsStr) {
|
|
344
|
+
const props = [];
|
|
345
|
+
const propRegex = /(?:\/\*\*[\s\S]*?\*\/\s*)?(?:\/\/.*\n\s*)?(\w+)(\?)?:\s*([^;]+);/g;
|
|
346
|
+
let match;
|
|
347
|
+
while ((match = propRegex.exec(propsStr)) !== null) {
|
|
348
|
+
const name = match[1];
|
|
349
|
+
const optional = !!match[2];
|
|
350
|
+
const type = match[3].trim();
|
|
351
|
+
const commentMatch = propsStr.slice(0, match.index).match(/\/\/\s*(.+)$|\/\*\*\s*(.+?)\s*\*\//);
|
|
352
|
+
const description = commentMatch ? (commentMatch[1] || commentMatch[2] || '').trim() : '';
|
|
353
|
+
props.push({ name, type, optional, description });
|
|
354
|
+
}
|
|
355
|
+
return props;
|
|
75
356
|
}
|
|
76
357
|
walkDir(dir) {
|
|
77
358
|
const files = [];
|
|
78
359
|
try {
|
|
79
360
|
const entries = readdirSync(dir);
|
|
80
361
|
for (const entry of entries) {
|
|
81
|
-
if (entry.startsWith('_') || entry.startsWith('.'))
|
|
362
|
+
if (entry.startsWith('_') || entry.startsWith('.') || entry === 'node_modules')
|
|
82
363
|
continue;
|
|
83
364
|
const fullPath = join(dir, entry);
|
|
84
365
|
const stat = statSync(fullPath);
|
|
@@ -115,16 +396,16 @@ export class MCPServer {
|
|
|
115
396
|
return Array.from(keywords).slice(0, 50);
|
|
116
397
|
}
|
|
117
398
|
getTools() {
|
|
118
|
-
|
|
399
|
+
const allTools = [
|
|
119
400
|
{
|
|
120
401
|
name: 'search_docs',
|
|
121
|
-
description: 'Search Recker documentation
|
|
402
|
+
description: 'Search Recker documentation using hybrid search (fuzzy + semantic). Returns matching docs with relevance scores and snippets. Use this first to find relevant documentation.',
|
|
122
403
|
inputSchema: {
|
|
123
404
|
type: 'object',
|
|
124
405
|
properties: {
|
|
125
406
|
query: {
|
|
126
407
|
type: 'string',
|
|
127
|
-
description: 'Search query (e.g., "retry
|
|
408
|
+
description: 'Search query (e.g., "retry with exponential backoff", "streaming SSE responses", "cache strategies")',
|
|
128
409
|
},
|
|
129
410
|
category: {
|
|
130
411
|
type: 'string',
|
|
@@ -132,7 +413,12 @@ export class MCPServer {
|
|
|
132
413
|
},
|
|
133
414
|
limit: {
|
|
134
415
|
type: 'number',
|
|
135
|
-
description: 'Max results to return (default: 5)',
|
|
416
|
+
description: 'Max results to return (default: 5, max: 10)',
|
|
417
|
+
},
|
|
418
|
+
mode: {
|
|
419
|
+
type: 'string',
|
|
420
|
+
enum: ['hybrid', 'fuzzy', 'semantic'],
|
|
421
|
+
description: 'Search mode: hybrid (default), fuzzy (text matching), or semantic (meaning-based)',
|
|
136
422
|
},
|
|
137
423
|
},
|
|
138
424
|
required: ['query'],
|
|
@@ -152,7 +438,95 @@ export class MCPServer {
|
|
|
152
438
|
required: ['path'],
|
|
153
439
|
},
|
|
154
440
|
},
|
|
441
|
+
{
|
|
442
|
+
name: 'code_examples',
|
|
443
|
+
description: 'Get runnable code examples for Recker features. Returns complete, working examples with explanations.',
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
feature: {
|
|
448
|
+
type: 'string',
|
|
449
|
+
description: 'Feature to get examples for (e.g., "retry", "cache", "streaming", "websocket", "mcp", "batch", "pagination", "middleware")',
|
|
450
|
+
},
|
|
451
|
+
complexity: {
|
|
452
|
+
type: 'string',
|
|
453
|
+
enum: ['basic', 'intermediate', 'advanced'],
|
|
454
|
+
description: 'Complexity level of the example (default: all levels)',
|
|
455
|
+
},
|
|
456
|
+
limit: {
|
|
457
|
+
type: 'number',
|
|
458
|
+
description: 'Max examples to return (default: 3)',
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
required: ['feature'],
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: 'api_schema',
|
|
466
|
+
description: 'Get TypeScript types, interfaces, and API schemas for Recker. Useful for generating type-safe code.',
|
|
467
|
+
inputSchema: {
|
|
468
|
+
type: 'object',
|
|
469
|
+
properties: {
|
|
470
|
+
type: {
|
|
471
|
+
type: 'string',
|
|
472
|
+
description: 'Type/interface to look up (e.g., "Client", "RequestOptions", "RetryOptions", "CacheOptions", "MCPServer")',
|
|
473
|
+
},
|
|
474
|
+
include: {
|
|
475
|
+
type: 'string',
|
|
476
|
+
enum: ['definition', 'properties', 'both'],
|
|
477
|
+
description: 'What to include: just definition, properties breakdown, or both (default: both)',
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
required: ['type'],
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: 'suggest',
|
|
485
|
+
description: 'Get implementation suggestions based on use case description. Analyzes requirements and suggests the best Recker patterns.',
|
|
486
|
+
inputSchema: {
|
|
487
|
+
type: 'object',
|
|
488
|
+
properties: {
|
|
489
|
+
useCase: {
|
|
490
|
+
type: 'string',
|
|
491
|
+
description: 'Describe what you want to achieve (e.g., "call an API with retry and cache", "stream AI responses", "scrape multiple sites in parallel")',
|
|
492
|
+
},
|
|
493
|
+
constraints: {
|
|
494
|
+
type: 'array',
|
|
495
|
+
items: { type: 'string' },
|
|
496
|
+
description: 'Any constraints or requirements (e.g., ["must handle rate limits", "need progress tracking"])',
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
required: ['useCase'],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
155
502
|
];
|
|
503
|
+
if (this.options.toolsFilter.length > 0) {
|
|
504
|
+
return allTools.filter(tool => this.isToolEnabled(tool.name));
|
|
505
|
+
}
|
|
506
|
+
return allTools;
|
|
507
|
+
}
|
|
508
|
+
isToolEnabled(name) {
|
|
509
|
+
const filter = this.options.toolsFilter;
|
|
510
|
+
if (!filter.length)
|
|
511
|
+
return true;
|
|
512
|
+
const positive = filter.filter(p => !p.startsWith('!'));
|
|
513
|
+
const negative = filter.filter(p => p.startsWith('!')).map(p => p.slice(1));
|
|
514
|
+
if (negative.some(p => this.matchPattern(name, p)))
|
|
515
|
+
return false;
|
|
516
|
+
if (!positive.length)
|
|
517
|
+
return true;
|
|
518
|
+
return positive.some(p => this.matchPattern(name, p));
|
|
519
|
+
}
|
|
520
|
+
matchPattern(name, pattern) {
|
|
521
|
+
if (pattern === '*')
|
|
522
|
+
return true;
|
|
523
|
+
if (pattern.endsWith('*')) {
|
|
524
|
+
return name.startsWith(pattern.slice(0, -1));
|
|
525
|
+
}
|
|
526
|
+
if (pattern.startsWith('*')) {
|
|
527
|
+
return name.endsWith(pattern.slice(1));
|
|
528
|
+
}
|
|
529
|
+
return name === pattern;
|
|
156
530
|
}
|
|
157
531
|
handleToolCall(name, args) {
|
|
158
532
|
switch (name) {
|
|
@@ -160,6 +534,12 @@ export class MCPServer {
|
|
|
160
534
|
return this.searchDocs(args);
|
|
161
535
|
case 'get_doc':
|
|
162
536
|
return this.getDoc(args);
|
|
537
|
+
case 'code_examples':
|
|
538
|
+
return this.getCodeExamples(args);
|
|
539
|
+
case 'api_schema':
|
|
540
|
+
return this.getApiSchema(args);
|
|
541
|
+
case 'suggest':
|
|
542
|
+
return this.getSuggestions(args);
|
|
163
543
|
default:
|
|
164
544
|
return {
|
|
165
545
|
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
@@ -168,22 +548,24 @@ export class MCPServer {
|
|
|
168
548
|
}
|
|
169
549
|
}
|
|
170
550
|
searchDocs(args) {
|
|
171
|
-
const query = String(args.query || '')
|
|
172
|
-
const category = args.category ? String(args.category)
|
|
551
|
+
const query = String(args.query || '');
|
|
552
|
+
const category = args.category ? String(args.category) : undefined;
|
|
173
553
|
const limit = Math.min(Number(args.limit) || 5, 10);
|
|
554
|
+
const mode = args.mode || 'hybrid';
|
|
174
555
|
if (!query) {
|
|
175
556
|
return {
|
|
176
557
|
content: [{ type: 'text', text: 'Error: query is required' }],
|
|
177
558
|
isError: true,
|
|
178
559
|
};
|
|
179
560
|
}
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
561
|
+
const searchPromise = this.hybridSearch.search(query, { limit, category, mode });
|
|
562
|
+
let results = [];
|
|
563
|
+
const queryLower = query.toLowerCase();
|
|
564
|
+
const queryTerms = queryLower.split(/\s+/);
|
|
565
|
+
const scored = this.docsIndex
|
|
566
|
+
.filter(doc => !category || doc.category.toLowerCase().includes(category.toLowerCase()))
|
|
567
|
+
.map(doc => {
|
|
185
568
|
let score = 0;
|
|
186
|
-
const queryTerms = query.split(/\s+/);
|
|
187
569
|
for (const term of queryTerms) {
|
|
188
570
|
if (doc.title.toLowerCase().includes(term))
|
|
189
571
|
score += 10;
|
|
@@ -194,14 +576,21 @@ export class MCPServer {
|
|
|
194
576
|
if (doc.content.toLowerCase().includes(term))
|
|
195
577
|
score += 1;
|
|
196
578
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
results.
|
|
203
|
-
|
|
204
|
-
|
|
579
|
+
return { doc, score };
|
|
580
|
+
})
|
|
581
|
+
.filter(r => r.score > 0)
|
|
582
|
+
.sort((a, b) => b.score - a.score)
|
|
583
|
+
.slice(0, limit);
|
|
584
|
+
results = scored.map(r => ({
|
|
585
|
+
id: r.doc.id,
|
|
586
|
+
path: r.doc.path,
|
|
587
|
+
title: r.doc.title,
|
|
588
|
+
content: r.doc.content,
|
|
589
|
+
snippet: this.extractSnippet(r.doc.content, query),
|
|
590
|
+
score: Math.min(1, r.score / 20),
|
|
591
|
+
source: 'fuzzy',
|
|
592
|
+
}));
|
|
593
|
+
if (results.length === 0) {
|
|
205
594
|
return {
|
|
206
595
|
content: [{
|
|
207
596
|
type: 'text',
|
|
@@ -209,23 +598,33 @@ export class MCPServer {
|
|
|
209
598
|
}],
|
|
210
599
|
};
|
|
211
600
|
}
|
|
212
|
-
const
|
|
601
|
+
const stats = this.hybridSearch.getStats();
|
|
602
|
+
const searchMode = stats.embeddings > 0 ? mode : 'fuzzy';
|
|
603
|
+
const output = results.map((r, i) => `${i + 1}. **${r.title}** (${(r.score * 100).toFixed(0)}% match)\n Path: \`${r.path}\`\n ${r.snippet}`).join('\n\n');
|
|
213
604
|
return {
|
|
214
605
|
content: [{
|
|
215
606
|
type: 'text',
|
|
216
|
-
text: `Found ${
|
|
607
|
+
text: `Found ${results.length} result(s) for "${query}" (${searchMode} search):\n\n${output}\n\nUse get_doc with the path to read full content.`,
|
|
217
608
|
}],
|
|
218
609
|
};
|
|
219
610
|
}
|
|
220
611
|
extractSnippet(content, query) {
|
|
221
612
|
const lowerContent = content.toLowerCase();
|
|
222
|
-
const
|
|
223
|
-
|
|
613
|
+
const queryTerms = query.toLowerCase().split(/\s+/);
|
|
614
|
+
let bestIndex = -1;
|
|
615
|
+
for (const term of queryTerms) {
|
|
616
|
+
const idx = lowerContent.indexOf(term);
|
|
617
|
+
if (idx !== -1 && (bestIndex === -1 || idx < bestIndex)) {
|
|
618
|
+
bestIndex = idx;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (bestIndex === -1) {
|
|
224
623
|
const firstPara = content.split('\n\n')[1] || content.substring(0, 200);
|
|
225
624
|
return firstPara.substring(0, 150).trim() + '...';
|
|
226
625
|
}
|
|
227
|
-
const start = Math.max(0,
|
|
228
|
-
const end = Math.min(content.length,
|
|
626
|
+
const start = Math.max(0, bestIndex - 50);
|
|
627
|
+
const end = Math.min(content.length, bestIndex + 150);
|
|
229
628
|
let snippet = content.substring(start, end).trim();
|
|
230
629
|
if (start > 0)
|
|
231
630
|
snippet = '...' + snippet;
|
|
@@ -262,6 +661,252 @@ export class MCPServer {
|
|
|
262
661
|
}],
|
|
263
662
|
};
|
|
264
663
|
}
|
|
664
|
+
getCodeExamples(args) {
|
|
665
|
+
const feature = String(args.feature || '').toLowerCase();
|
|
666
|
+
const complexity = args.complexity;
|
|
667
|
+
const limit = Math.min(Number(args.limit) || 3, 5);
|
|
668
|
+
if (!feature) {
|
|
669
|
+
return {
|
|
670
|
+
content: [{ type: 'text', text: 'Error: feature is required' }],
|
|
671
|
+
isError: true,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
let examples = this.codeExamples.filter(ex => ex.feature === feature ||
|
|
675
|
+
ex.keywords.includes(feature) ||
|
|
676
|
+
ex.title.toLowerCase().includes(feature));
|
|
677
|
+
if (complexity) {
|
|
678
|
+
examples = examples.filter(ex => ex.complexity === complexity);
|
|
679
|
+
}
|
|
680
|
+
examples = examples.slice(0, limit);
|
|
681
|
+
if (examples.length === 0) {
|
|
682
|
+
const availableFeatures = [...new Set(this.codeExamples.map(ex => ex.feature))];
|
|
683
|
+
return {
|
|
684
|
+
content: [{
|
|
685
|
+
type: 'text',
|
|
686
|
+
text: `No examples found for feature "${feature}".\n\nAvailable features: ${availableFeatures.join(', ')}`,
|
|
687
|
+
}],
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const output = examples.map((ex, i) => {
|
|
691
|
+
return `## ${i + 1}. ${ex.title}\n\n**Complexity:** ${ex.complexity}\n**Path:** \`${ex.path}\`\n\n${ex.description ? `${ex.description}\n\n` : ''}\`\`\`typescript\n${ex.code}\n\`\`\``;
|
|
692
|
+
}).join('\n\n---\n\n');
|
|
693
|
+
return {
|
|
694
|
+
content: [{
|
|
695
|
+
type: 'text',
|
|
696
|
+
text: `Found ${examples.length} example(s) for "${feature}":\n\n${output}`,
|
|
697
|
+
}],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
getApiSchema(args) {
|
|
701
|
+
const typeName = String(args.type || '');
|
|
702
|
+
const include = args.include || 'both';
|
|
703
|
+
if (!typeName) {
|
|
704
|
+
return {
|
|
705
|
+
content: [{ type: 'text', text: 'Error: type is required' }],
|
|
706
|
+
isError: true,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const matches = this.typeDefinitions.filter(td => td.name.toLowerCase() === typeName.toLowerCase() ||
|
|
710
|
+
td.name.toLowerCase().includes(typeName.toLowerCase()));
|
|
711
|
+
if (matches.length === 0) {
|
|
712
|
+
const availableTypes = this.typeDefinitions
|
|
713
|
+
.map(td => td.name)
|
|
714
|
+
.slice(0, 20);
|
|
715
|
+
return {
|
|
716
|
+
content: [{
|
|
717
|
+
type: 'text',
|
|
718
|
+
text: `Type "${typeName}" not found.\n\nAvailable types: ${availableTypes.join(', ')}`,
|
|
719
|
+
}],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const output = matches.map(td => {
|
|
723
|
+
let result = `## ${td.name} (${td.kind})\n\n`;
|
|
724
|
+
if (td.description) {
|
|
725
|
+
result += `${td.description}\n\n`;
|
|
726
|
+
}
|
|
727
|
+
result += `**Path:** \`${td.path}\`\n\n`;
|
|
728
|
+
if (include === 'definition' || include === 'both') {
|
|
729
|
+
result += `### Definition\n\n\`\`\`typescript\n${td.definition}\n\`\`\`\n\n`;
|
|
730
|
+
}
|
|
731
|
+
if ((include === 'properties' || include === 'both') && td.properties && td.properties.length > 0) {
|
|
732
|
+
result += `### Properties\n\n`;
|
|
733
|
+
for (const prop of td.properties) {
|
|
734
|
+
const optionalMark = prop.optional ? '?' : '';
|
|
735
|
+
result += `- **${prop.name}${optionalMark}**: \`${prop.type}\``;
|
|
736
|
+
if (prop.description) {
|
|
737
|
+
result += ` - ${prop.description}`;
|
|
738
|
+
}
|
|
739
|
+
result += '\n';
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return result;
|
|
743
|
+
}).join('\n---\n\n');
|
|
744
|
+
return {
|
|
745
|
+
content: [{
|
|
746
|
+
type: 'text',
|
|
747
|
+
text: output,
|
|
748
|
+
}],
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
getSuggestions(args) {
|
|
752
|
+
const useCase = String(args.useCase || '').toLowerCase();
|
|
753
|
+
const constraints = args.constraints || [];
|
|
754
|
+
if (!useCase) {
|
|
755
|
+
return {
|
|
756
|
+
content: [{ type: 'text', text: 'Error: useCase is required' }],
|
|
757
|
+
isError: true,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
const suggestions = [];
|
|
761
|
+
const featurePatterns = [
|
|
762
|
+
{ keywords: ['retry', 'fail', 'error', 'resilient', 'reliable'], feature: 'retry', reason: 'Handle transient failures' },
|
|
763
|
+
{ keywords: ['cache', 'fast', 'repeated', 'memoize'], feature: 'cache', reason: 'Speed up repeated requests' },
|
|
764
|
+
{ keywords: ['stream', 'sse', 'real-time', 'live', 'event'], feature: 'streaming', reason: 'Handle streaming responses' },
|
|
765
|
+
{ keywords: ['parallel', 'concurrent', 'batch', 'multiple', 'bulk'], feature: 'batch', reason: 'Execute requests concurrently' },
|
|
766
|
+
{ keywords: ['paginate', 'page', 'cursor', 'next', 'all'], feature: 'pagination', reason: 'Fetch paginated data' },
|
|
767
|
+
{ keywords: ['websocket', 'ws', 'bidirectional', 'push'], feature: 'websocket', reason: 'Real-time bidirectional communication' },
|
|
768
|
+
{ keywords: ['rate limit', 'throttle', 'limit'], feature: 'rate-limiting', reason: 'Respect API rate limits' },
|
|
769
|
+
{ keywords: ['auth', 'token', 'bearer', 'api key'], feature: 'auth', reason: 'Handle authentication' },
|
|
770
|
+
{ keywords: ['progress', 'download', 'upload', 'track'], feature: 'progress', reason: 'Track transfer progress' },
|
|
771
|
+
{ keywords: ['timeout', 'slow', 'hang'], feature: 'timeout', reason: 'Prevent hanging requests' },
|
|
772
|
+
{ keywords: ['scrape', 'html', 'parse', 'extract'], feature: 'scraping', reason: 'Parse HTML content' },
|
|
773
|
+
{ keywords: ['ai', 'llm', 'openai', 'anthropic', 'gpt', 'claude'], feature: 'ai', reason: 'AI/LLM integrations' },
|
|
774
|
+
];
|
|
775
|
+
for (const pattern of featurePatterns) {
|
|
776
|
+
if (pattern.keywords.some(k => useCase.includes(k))) {
|
|
777
|
+
suggestions.push({
|
|
778
|
+
feature: pattern.feature,
|
|
779
|
+
reason: pattern.reason,
|
|
780
|
+
config: this.getFeatureConfig(pattern.feature),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const constraint of constraints) {
|
|
785
|
+
const lowerConstraint = constraint.toLowerCase();
|
|
786
|
+
for (const pattern of featurePatterns) {
|
|
787
|
+
if (pattern.keywords.some(k => lowerConstraint.includes(k))) {
|
|
788
|
+
if (!suggestions.some(s => s.feature === pattern.feature)) {
|
|
789
|
+
suggestions.push({
|
|
790
|
+
feature: pattern.feature,
|
|
791
|
+
reason: pattern.reason,
|
|
792
|
+
config: this.getFeatureConfig(pattern.feature),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (suggestions.length === 0) {
|
|
799
|
+
return {
|
|
800
|
+
content: [{
|
|
801
|
+
type: 'text',
|
|
802
|
+
text: `I couldn't identify specific features for "${useCase}".\n\nTry describing your use case with keywords like:\n- retry, fail, error (for resilience)\n- cache, fast, repeated (for caching)\n- stream, sse, live (for streaming)\n- parallel, batch, concurrent (for batching)\n- paginate, cursor, next (for pagination)`,
|
|
803
|
+
}],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const output = suggestions.map((s, i) => {
|
|
807
|
+
return `### ${i + 1}. ${s.feature.charAt(0).toUpperCase() + s.feature.slice(1)}\n\n**Why:** ${s.reason}\n\n**Configuration:**\n\`\`\`typescript\n${s.config}\n\`\`\``;
|
|
808
|
+
}).join('\n\n');
|
|
809
|
+
const combinedConfig = this.getCombinedConfig(suggestions.map(s => s.feature));
|
|
810
|
+
return {
|
|
811
|
+
content: [{
|
|
812
|
+
type: 'text',
|
|
813
|
+
text: `# Suggested Implementation for: "${useCase}"\n\n${output}\n\n---\n\n## Combined Configuration\n\n\`\`\`typescript\n${combinedConfig}\n\`\`\``,
|
|
814
|
+
}],
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
getFeatureConfig(feature) {
|
|
818
|
+
const configs = {
|
|
819
|
+
retry: `retry: {
|
|
820
|
+
attempts: 3,
|
|
821
|
+
backoff: 'exponential',
|
|
822
|
+
delay: 1000,
|
|
823
|
+
jitter: true
|
|
824
|
+
}`,
|
|
825
|
+
cache: `cache: {
|
|
826
|
+
ttl: 60000, // 1 minute
|
|
827
|
+
strategy: 'stale-while-revalidate'
|
|
828
|
+
}`,
|
|
829
|
+
streaming: `// Streaming with async iteration
|
|
830
|
+
for await (const chunk of client.get('/stream')) {
|
|
831
|
+
console.log(chunk);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// SSE parsing
|
|
835
|
+
for await (const event of client.get('/events').sse()) {
|
|
836
|
+
console.log(event.data);
|
|
837
|
+
}`,
|
|
838
|
+
batch: `// Batch requests
|
|
839
|
+
const { results } = await client.batch([
|
|
840
|
+
{ path: '/users/1' },
|
|
841
|
+
{ path: '/users/2' },
|
|
842
|
+
{ path: '/users/3' }
|
|
843
|
+
], { concurrency: 5 });`,
|
|
844
|
+
pagination: `// Auto-pagination
|
|
845
|
+
const allItems = await client.getAll('/items', {
|
|
846
|
+
paginate: { maxPages: 10 }
|
|
847
|
+
});`,
|
|
848
|
+
websocket: `const ws = await client.ws('wss://api.example.com/ws');
|
|
849
|
+
ws.on('message', (data) => console.log(data));
|
|
850
|
+
ws.send({ type: 'subscribe', channel: 'updates' });`,
|
|
851
|
+
'rate-limiting': `concurrency: {
|
|
852
|
+
requestsPerInterval: 100,
|
|
853
|
+
interval: 1000 // 100 req/sec
|
|
854
|
+
}`,
|
|
855
|
+
auth: `headers: {
|
|
856
|
+
Authorization: 'Bearer YOUR_TOKEN'
|
|
857
|
+
}
|
|
858
|
+
// or use beforeRequest hook
|
|
859
|
+
client.beforeRequest((req) => {
|
|
860
|
+
return req.withHeader('Authorization', getToken());
|
|
861
|
+
})`,
|
|
862
|
+
progress: `const response = await client.get('/large-file', {
|
|
863
|
+
onDownloadProgress: ({ percent, rate }) => {
|
|
864
|
+
console.log(\`\${percent}% at \${rate} bytes/sec\`);
|
|
865
|
+
}
|
|
866
|
+
});`,
|
|
867
|
+
timeout: `timeout: 30000, // 30 seconds`,
|
|
868
|
+
scraping: `const $ = await client.get('/page').scrape();
|
|
869
|
+
const title = $('h1').text();
|
|
870
|
+
const links = $('a').map((_, el) => $(el).attr('href')).get();`,
|
|
871
|
+
ai: `// OpenAI streaming
|
|
872
|
+
for await (const chunk of client.get('/chat/completions').sse()) {
|
|
873
|
+
process.stdout.write(chunk.choices[0].delta.content || '');
|
|
874
|
+
}`,
|
|
875
|
+
};
|
|
876
|
+
return configs[feature] || `// Configure ${feature}`;
|
|
877
|
+
}
|
|
878
|
+
getCombinedConfig(features) {
|
|
879
|
+
const parts = [
|
|
880
|
+
`import { createClient } from 'recker';
|
|
881
|
+
|
|
882
|
+
const client = createClient({
|
|
883
|
+
baseUrl: 'https://api.example.com',`,
|
|
884
|
+
];
|
|
885
|
+
if (features.includes('retry')) {
|
|
886
|
+
parts.push(` retry: {
|
|
887
|
+
attempts: 3,
|
|
888
|
+
backoff: 'exponential',
|
|
889
|
+
delay: 1000
|
|
890
|
+
},`);
|
|
891
|
+
}
|
|
892
|
+
if (features.includes('cache')) {
|
|
893
|
+
parts.push(` cache: {
|
|
894
|
+
ttl: 60000,
|
|
895
|
+
strategy: 'stale-while-revalidate'
|
|
896
|
+
},`);
|
|
897
|
+
}
|
|
898
|
+
if (features.includes('rate-limiting')) {
|
|
899
|
+
parts.push(` concurrency: {
|
|
900
|
+
requestsPerInterval: 100,
|
|
901
|
+
interval: 1000
|
|
902
|
+
},`);
|
|
903
|
+
}
|
|
904
|
+
if (features.includes('timeout')) {
|
|
905
|
+
parts.push(` timeout: 30000,`);
|
|
906
|
+
}
|
|
907
|
+
parts.push(`});`);
|
|
908
|
+
return parts.join('\n');
|
|
909
|
+
}
|
|
265
910
|
handleRequest(req) {
|
|
266
911
|
const { method, params, id } = req;
|
|
267
912
|
this.log(`Request: ${method}`, params);
|
|
@@ -440,12 +1085,16 @@ export class MCPServer {
|
|
|
440
1085
|
return;
|
|
441
1086
|
}
|
|
442
1087
|
if (req.method === 'GET' && url === '/health') {
|
|
1088
|
+
const stats = this.hybridSearch.getStats();
|
|
443
1089
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
444
1090
|
res.end(JSON.stringify({
|
|
445
1091
|
status: 'ok',
|
|
446
1092
|
name: this.options.name,
|
|
447
1093
|
version: this.options.version,
|
|
448
|
-
docsCount:
|
|
1094
|
+
docsCount: stats.documents,
|
|
1095
|
+
examplesCount: this.codeExamples.length,
|
|
1096
|
+
typesCount: this.typeDefinitions.length,
|
|
1097
|
+
embeddingsLoaded: stats.embeddings > 0,
|
|
449
1098
|
sseClients: this.sseClients.size,
|
|
450
1099
|
}));
|
|
451
1100
|
return;
|
|
@@ -494,6 +1143,12 @@ export class MCPServer {
|
|
|
494
1143
|
getDocsCount() {
|
|
495
1144
|
return this.docsIndex.length;
|
|
496
1145
|
}
|
|
1146
|
+
getExamplesCount() {
|
|
1147
|
+
return this.codeExamples.length;
|
|
1148
|
+
}
|
|
1149
|
+
getTypesCount() {
|
|
1150
|
+
return this.typeDefinitions.length;
|
|
1151
|
+
}
|
|
497
1152
|
getTransport() {
|
|
498
1153
|
return this.options.transport;
|
|
499
1154
|
}
|