recker 1.0.11 → 1.0.12-alpha.1ab8aa7
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/index.js +76 -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/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/search/hybrid-search.d.ts +28 -0
- package/dist/mcp/search/hybrid-search.d.ts.map +1 -0
- package/dist/mcp/search/hybrid-search.js +286 -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 +45 -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 +68 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1158 -0
- package/package.json +10 -4
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative, extname, basename, dirname } from 'path';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createHybridSearch } from './search/index.js';
|
|
7
|
+
export class MCPServer {
|
|
8
|
+
options;
|
|
9
|
+
server;
|
|
10
|
+
hybridSearch;
|
|
11
|
+
docsIndex = [];
|
|
12
|
+
codeExamples = [];
|
|
13
|
+
typeDefinitions = [];
|
|
14
|
+
sseClients = new Set();
|
|
15
|
+
initialized = false;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = {
|
|
18
|
+
name: options.name || 'recker-docs',
|
|
19
|
+
version: options.version || '1.0.0',
|
|
20
|
+
docsPath: options.docsPath || this.findDocsPath(),
|
|
21
|
+
examplesPath: options.examplesPath || this.findExamplesPath(),
|
|
22
|
+
srcPath: options.srcPath || this.findSrcPath(),
|
|
23
|
+
port: options.port || 3100,
|
|
24
|
+
transport: options.transport || 'stdio',
|
|
25
|
+
debug: options.debug || false,
|
|
26
|
+
toolsFilter: options.toolsFilter || [],
|
|
27
|
+
};
|
|
28
|
+
this.hybridSearch = createHybridSearch({ debug: this.options.debug });
|
|
29
|
+
this.buildIndex();
|
|
30
|
+
}
|
|
31
|
+
log(message, data) {
|
|
32
|
+
if (this.options.debug) {
|
|
33
|
+
if (this.options.transport === 'stdio') {
|
|
34
|
+
console.error(`[MCP] ${message}`, data ? JSON.stringify(data) : '');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log(`[MCP] ${message}`, data ? JSON.stringify(data) : '');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
findDocsPath() {
|
|
42
|
+
const possiblePaths = [
|
|
43
|
+
join(process.cwd(), 'docs'),
|
|
44
|
+
join(process.cwd(), '..', 'docs'),
|
|
45
|
+
];
|
|
46
|
+
try {
|
|
47
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
possiblePaths.push(join(__dirname, '..', '..', 'docs'), join(__dirname, '..', '..', '..', 'docs'));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
}
|
|
52
|
+
for (const p of possiblePaths) {
|
|
53
|
+
if (existsSync(p)) {
|
|
54
|
+
return p;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return join(process.cwd(), 'docs');
|
|
58
|
+
}
|
|
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() {
|
|
102
|
+
if (!existsSync(this.options.docsPath)) {
|
|
103
|
+
this.log(`Docs path not found: ${this.options.docsPath}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const files = this.walkDir(this.options.docsPath);
|
|
107
|
+
for (let i = 0; i < files.length; i++) {
|
|
108
|
+
const file = files[i];
|
|
109
|
+
if (!file.endsWith('.md'))
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
const content = readFileSync(file, 'utf-8');
|
|
113
|
+
const relativePath = relative(this.options.docsPath, file);
|
|
114
|
+
const category = relativePath.split('/')[0] || 'root';
|
|
115
|
+
const title = this.extractTitle(content) || relativePath;
|
|
116
|
+
const keywords = this.extractKeywords(content);
|
|
117
|
+
this.docsIndex.push({
|
|
118
|
+
id: `doc-${i}`,
|
|
119
|
+
path: relativePath,
|
|
120
|
+
title,
|
|
121
|
+
category,
|
|
122
|
+
content,
|
|
123
|
+
keywords,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
this.log(`Failed to index ${file}:`, err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
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;
|
|
356
|
+
}
|
|
357
|
+
walkDir(dir) {
|
|
358
|
+
const files = [];
|
|
359
|
+
try {
|
|
360
|
+
const entries = readdirSync(dir);
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
if (entry.startsWith('_') || entry.startsWith('.') || entry === 'node_modules')
|
|
363
|
+
continue;
|
|
364
|
+
const fullPath = join(dir, entry);
|
|
365
|
+
const stat = statSync(fullPath);
|
|
366
|
+
if (stat.isDirectory()) {
|
|
367
|
+
files.push(...this.walkDir(fullPath));
|
|
368
|
+
}
|
|
369
|
+
else if (stat.isFile()) {
|
|
370
|
+
files.push(fullPath);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
}
|
|
376
|
+
return files;
|
|
377
|
+
}
|
|
378
|
+
extractTitle(content) {
|
|
379
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
380
|
+
return match ? match[1].trim() : '';
|
|
381
|
+
}
|
|
382
|
+
extractKeywords(content) {
|
|
383
|
+
const keywords = new Set();
|
|
384
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm) || [];
|
|
385
|
+
for (const h of headings) {
|
|
386
|
+
keywords.add(h.replace(/^#+\s+/, '').toLowerCase());
|
|
387
|
+
}
|
|
388
|
+
const codePatterns = content.match(/`([a-zA-Z_][a-zA-Z0-9_]*(?:\(\))?)`/g) || [];
|
|
389
|
+
for (const c of codePatterns) {
|
|
390
|
+
keywords.add(c.replace(/`/g, '').toLowerCase());
|
|
391
|
+
}
|
|
392
|
+
const terms = content.match(/\b[A-Z][a-zA-Z]+(?:Client|Server|Error|Response|Request|Plugin|Transport)\b/g) || [];
|
|
393
|
+
for (const t of terms) {
|
|
394
|
+
keywords.add(t.toLowerCase());
|
|
395
|
+
}
|
|
396
|
+
return Array.from(keywords).slice(0, 50);
|
|
397
|
+
}
|
|
398
|
+
getTools() {
|
|
399
|
+
const allTools = [
|
|
400
|
+
{
|
|
401
|
+
name: 'search_docs',
|
|
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.',
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
query: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'Search query (e.g., "retry with exponential backoff", "streaming SSE responses", "cache strategies")',
|
|
409
|
+
},
|
|
410
|
+
category: {
|
|
411
|
+
type: 'string',
|
|
412
|
+
description: 'Optional: filter by category (http, cli, ai, protocols, reference, guides)',
|
|
413
|
+
},
|
|
414
|
+
limit: {
|
|
415
|
+
type: 'number',
|
|
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)',
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
required: ['query'],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'get_doc',
|
|
429
|
+
description: 'Get the full content of a specific documentation file. Use the path from search_docs results.',
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
path: {
|
|
434
|
+
type: 'string',
|
|
435
|
+
description: 'Documentation file path (e.g., "http/07-resilience.md", "cli/01-overview.md")',
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
required: ['path'],
|
|
439
|
+
},
|
|
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
|
+
},
|
|
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;
|
|
530
|
+
}
|
|
531
|
+
handleToolCall(name, args) {
|
|
532
|
+
switch (name) {
|
|
533
|
+
case 'search_docs':
|
|
534
|
+
return this.searchDocs(args);
|
|
535
|
+
case 'get_doc':
|
|
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);
|
|
543
|
+
default:
|
|
544
|
+
return {
|
|
545
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
546
|
+
isError: true,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
searchDocs(args) {
|
|
551
|
+
const query = String(args.query || '');
|
|
552
|
+
const category = args.category ? String(args.category) : undefined;
|
|
553
|
+
const limit = Math.min(Number(args.limit) || 5, 10);
|
|
554
|
+
const mode = args.mode || 'hybrid';
|
|
555
|
+
if (!query) {
|
|
556
|
+
return {
|
|
557
|
+
content: [{ type: 'text', text: 'Error: query is required' }],
|
|
558
|
+
isError: true,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
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 => {
|
|
568
|
+
let score = 0;
|
|
569
|
+
for (const term of queryTerms) {
|
|
570
|
+
if (doc.title.toLowerCase().includes(term))
|
|
571
|
+
score += 10;
|
|
572
|
+
if (doc.path.toLowerCase().includes(term))
|
|
573
|
+
score += 5;
|
|
574
|
+
if (doc.keywords.some(k => k.includes(term)))
|
|
575
|
+
score += 3;
|
|
576
|
+
if (doc.content.toLowerCase().includes(term))
|
|
577
|
+
score += 1;
|
|
578
|
+
}
|
|
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) {
|
|
594
|
+
return {
|
|
595
|
+
content: [{
|
|
596
|
+
type: 'text',
|
|
597
|
+
text: `No documentation found for "${query}". Try different keywords like: http, cache, retry, streaming, websocket, ai, cli, plugins`,
|
|
598
|
+
}],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
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');
|
|
604
|
+
return {
|
|
605
|
+
content: [{
|
|
606
|
+
type: 'text',
|
|
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.`,
|
|
608
|
+
}],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
extractSnippet(content, query) {
|
|
612
|
+
const lowerContent = content.toLowerCase();
|
|
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) {
|
|
623
|
+
const firstPara = content.split('\n\n')[1] || content.substring(0, 200);
|
|
624
|
+
return firstPara.substring(0, 150).trim() + '...';
|
|
625
|
+
}
|
|
626
|
+
const start = Math.max(0, bestIndex - 50);
|
|
627
|
+
const end = Math.min(content.length, bestIndex + 150);
|
|
628
|
+
let snippet = content.substring(start, end).trim();
|
|
629
|
+
if (start > 0)
|
|
630
|
+
snippet = '...' + snippet;
|
|
631
|
+
if (end < content.length)
|
|
632
|
+
snippet = snippet + '...';
|
|
633
|
+
return snippet.replace(/\n/g, ' ');
|
|
634
|
+
}
|
|
635
|
+
getDoc(args) {
|
|
636
|
+
const path = String(args.path || '');
|
|
637
|
+
if (!path) {
|
|
638
|
+
return {
|
|
639
|
+
content: [{ type: 'text', text: 'Error: path is required' }],
|
|
640
|
+
isError: true,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const doc = this.docsIndex.find(d => d.path === path || d.path.endsWith(path));
|
|
644
|
+
if (!doc) {
|
|
645
|
+
const suggestions = this.docsIndex
|
|
646
|
+
.filter(d => d.path.includes(path.split('/').pop() || ''))
|
|
647
|
+
.slice(0, 3)
|
|
648
|
+
.map(d => d.path);
|
|
649
|
+
return {
|
|
650
|
+
content: [{
|
|
651
|
+
type: 'text',
|
|
652
|
+
text: `Documentation not found: ${path}${suggestions.length ? `\n\nDid you mean:\n${suggestions.map(s => `- ${s}`).join('\n')}` : ''}`,
|
|
653
|
+
}],
|
|
654
|
+
isError: true,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
content: [{
|
|
659
|
+
type: 'text',
|
|
660
|
+
text: `# ${doc.title}\n\nPath: ${doc.path}\nCategory: ${doc.category}\n\n---\n\n${doc.content}`,
|
|
661
|
+
}],
|
|
662
|
+
};
|
|
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
|
+
}
|
|
910
|
+
handleRequest(req) {
|
|
911
|
+
const { method, params, id } = req;
|
|
912
|
+
this.log(`Request: ${method}`, params);
|
|
913
|
+
try {
|
|
914
|
+
switch (method) {
|
|
915
|
+
case 'initialize': {
|
|
916
|
+
this.initialized = true;
|
|
917
|
+
const response = {
|
|
918
|
+
protocolVersion: '2024-11-05',
|
|
919
|
+
capabilities: {
|
|
920
|
+
tools: { listChanged: false },
|
|
921
|
+
},
|
|
922
|
+
serverInfo: {
|
|
923
|
+
name: this.options.name,
|
|
924
|
+
version: this.options.version,
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
return { jsonrpc: '2.0', id: id, result: response };
|
|
928
|
+
}
|
|
929
|
+
case 'notifications/initialized': {
|
|
930
|
+
return { jsonrpc: '2.0', id: id, result: {} };
|
|
931
|
+
}
|
|
932
|
+
case 'ping':
|
|
933
|
+
return { jsonrpc: '2.0', id: id, result: {} };
|
|
934
|
+
case 'tools/list': {
|
|
935
|
+
const response = { tools: this.getTools() };
|
|
936
|
+
return { jsonrpc: '2.0', id: id, result: response };
|
|
937
|
+
}
|
|
938
|
+
case 'tools/call': {
|
|
939
|
+
const { name, arguments: args } = params;
|
|
940
|
+
const result = this.handleToolCall(name, args || {});
|
|
941
|
+
return { jsonrpc: '2.0', id: id, result };
|
|
942
|
+
}
|
|
943
|
+
case 'resources/list':
|
|
944
|
+
return { jsonrpc: '2.0', id: id, result: { resources: [] } };
|
|
945
|
+
case 'prompts/list':
|
|
946
|
+
return { jsonrpc: '2.0', id: id, result: { prompts: [] } };
|
|
947
|
+
default:
|
|
948
|
+
return {
|
|
949
|
+
jsonrpc: '2.0',
|
|
950
|
+
id: id,
|
|
951
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
return {
|
|
957
|
+
jsonrpc: '2.0',
|
|
958
|
+
id: id,
|
|
959
|
+
error: { code: -32603, message: String(err) },
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
sendNotification(notification) {
|
|
964
|
+
const data = JSON.stringify(notification);
|
|
965
|
+
for (const client of this.sseClients) {
|
|
966
|
+
client.write(`data: ${data}\n\n`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async startStdio() {
|
|
970
|
+
const rl = createInterface({
|
|
971
|
+
input: process.stdin,
|
|
972
|
+
output: process.stdout,
|
|
973
|
+
terminal: false,
|
|
974
|
+
});
|
|
975
|
+
this.log('Starting in stdio mode');
|
|
976
|
+
rl.on('line', (line) => {
|
|
977
|
+
if (!line.trim())
|
|
978
|
+
return;
|
|
979
|
+
try {
|
|
980
|
+
const request = JSON.parse(line);
|
|
981
|
+
const response = this.handleRequest(request);
|
|
982
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
983
|
+
}
|
|
984
|
+
catch (err) {
|
|
985
|
+
const errorResponse = {
|
|
986
|
+
jsonrpc: '2.0',
|
|
987
|
+
id: 0,
|
|
988
|
+
error: { code: -32700, message: 'Parse error' },
|
|
989
|
+
};
|
|
990
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
rl.on('close', () => {
|
|
994
|
+
this.log('stdin closed, exiting');
|
|
995
|
+
process.exit(0);
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
async startHttp() {
|
|
999
|
+
return new Promise((resolve) => {
|
|
1000
|
+
this.server = createServer((req, res) => {
|
|
1001
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1002
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
1003
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1004
|
+
if (req.method === 'OPTIONS') {
|
|
1005
|
+
res.writeHead(204);
|
|
1006
|
+
res.end();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (req.method !== 'POST') {
|
|
1010
|
+
res.writeHead(405);
|
|
1011
|
+
res.end('Method not allowed');
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
let body = '';
|
|
1015
|
+
req.on('data', chunk => body += chunk);
|
|
1016
|
+
req.on('end', () => {
|
|
1017
|
+
try {
|
|
1018
|
+
const request = JSON.parse(body);
|
|
1019
|
+
const response = this.handleRequest(request);
|
|
1020
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1021
|
+
res.end(JSON.stringify(response));
|
|
1022
|
+
}
|
|
1023
|
+
catch (err) {
|
|
1024
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1025
|
+
res.end(JSON.stringify({
|
|
1026
|
+
jsonrpc: '2.0',
|
|
1027
|
+
id: null,
|
|
1028
|
+
error: { code: -32700, message: 'Parse error' },
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
this.server.listen(this.options.port, () => {
|
|
1034
|
+
this.log(`HTTP server listening on http://localhost:${this.options.port}`);
|
|
1035
|
+
resolve();
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async startSSE() {
|
|
1040
|
+
return new Promise((resolve) => {
|
|
1041
|
+
this.server = createServer((req, res) => {
|
|
1042
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1043
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
1044
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
1045
|
+
const url = req.url || '/';
|
|
1046
|
+
if (req.method === 'OPTIONS') {
|
|
1047
|
+
res.writeHead(204);
|
|
1048
|
+
res.end();
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (req.method === 'GET' && url === '/sse') {
|
|
1052
|
+
res.writeHead(200, {
|
|
1053
|
+
'Content-Type': 'text/event-stream',
|
|
1054
|
+
'Cache-Control': 'no-cache',
|
|
1055
|
+
'Connection': 'keep-alive',
|
|
1056
|
+
});
|
|
1057
|
+
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
|
|
1058
|
+
this.sseClients.add(res);
|
|
1059
|
+
this.log(`SSE client connected (${this.sseClients.size} total)`);
|
|
1060
|
+
req.on('close', () => {
|
|
1061
|
+
this.sseClients.delete(res);
|
|
1062
|
+
this.log(`SSE client disconnected (${this.sseClients.size} total)`);
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (req.method === 'POST') {
|
|
1067
|
+
let body = '';
|
|
1068
|
+
req.on('data', chunk => body += chunk);
|
|
1069
|
+
req.on('end', () => {
|
|
1070
|
+
try {
|
|
1071
|
+
const request = JSON.parse(body);
|
|
1072
|
+
const response = this.handleRequest(request);
|
|
1073
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1074
|
+
res.end(JSON.stringify(response));
|
|
1075
|
+
}
|
|
1076
|
+
catch (err) {
|
|
1077
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1078
|
+
res.end(JSON.stringify({
|
|
1079
|
+
jsonrpc: '2.0',
|
|
1080
|
+
id: null,
|
|
1081
|
+
error: { code: -32700, message: 'Parse error' },
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (req.method === 'GET' && url === '/health') {
|
|
1088
|
+
const stats = this.hybridSearch.getStats();
|
|
1089
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1090
|
+
res.end(JSON.stringify({
|
|
1091
|
+
status: 'ok',
|
|
1092
|
+
name: this.options.name,
|
|
1093
|
+
version: this.options.version,
|
|
1094
|
+
docsCount: stats.documents,
|
|
1095
|
+
examplesCount: this.codeExamples.length,
|
|
1096
|
+
typesCount: this.typeDefinitions.length,
|
|
1097
|
+
embeddingsLoaded: stats.embeddings > 0,
|
|
1098
|
+
sseClients: this.sseClients.size,
|
|
1099
|
+
}));
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
res.writeHead(404);
|
|
1103
|
+
res.end('Not found');
|
|
1104
|
+
});
|
|
1105
|
+
this.server.listen(this.options.port, () => {
|
|
1106
|
+
this.log(`SSE server listening on http://localhost:${this.options.port}`);
|
|
1107
|
+
this.log(` POST / - JSON-RPC endpoint`);
|
|
1108
|
+
this.log(` GET /sse - Server-Sent Events`);
|
|
1109
|
+
this.log(` GET /health - Health check`);
|
|
1110
|
+
resolve();
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
async start() {
|
|
1115
|
+
switch (this.options.transport) {
|
|
1116
|
+
case 'stdio':
|
|
1117
|
+
return this.startStdio();
|
|
1118
|
+
case 'http':
|
|
1119
|
+
return this.startHttp();
|
|
1120
|
+
case 'sse':
|
|
1121
|
+
return this.startSSE();
|
|
1122
|
+
default:
|
|
1123
|
+
throw new Error(`Unknown transport: ${this.options.transport}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async stop() {
|
|
1127
|
+
for (const client of this.sseClients) {
|
|
1128
|
+
client.end();
|
|
1129
|
+
}
|
|
1130
|
+
this.sseClients.clear();
|
|
1131
|
+
return new Promise((resolve) => {
|
|
1132
|
+
if (this.server) {
|
|
1133
|
+
this.server.close(() => resolve());
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
resolve();
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
getPort() {
|
|
1141
|
+
return this.options.port;
|
|
1142
|
+
}
|
|
1143
|
+
getDocsCount() {
|
|
1144
|
+
return this.docsIndex.length;
|
|
1145
|
+
}
|
|
1146
|
+
getExamplesCount() {
|
|
1147
|
+
return this.codeExamples.length;
|
|
1148
|
+
}
|
|
1149
|
+
getTypesCount() {
|
|
1150
|
+
return this.typeDefinitions.length;
|
|
1151
|
+
}
|
|
1152
|
+
getTransport() {
|
|
1153
|
+
return this.options.transport;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
export function createMCPServer(options) {
|
|
1157
|
+
return new MCPServer(options);
|
|
1158
|
+
}
|