recker 1.0.11-alpha.0f357e4 → 1.0.11-alpha.d342d95
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/cli/index.js +54 -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/server.d.ts +30 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +370 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -326,6 +326,60 @@ ${pc.bold(pc.yellow('Examples:'))}
|
|
|
326
326
|
const { startLoadDashboard } = await import('./tui/load-dashboard.js');
|
|
327
327
|
await startLoadDashboard({ url, users, duration, mode, http2, rampUp });
|
|
328
328
|
});
|
|
329
|
+
program
|
|
330
|
+
.command('mcp')
|
|
331
|
+
.description('Start MCP server for AI agents to access Recker documentation')
|
|
332
|
+
.option('-p, --port <number>', 'Server port', '3100')
|
|
333
|
+
.option('-d, --docs <path>', 'Path to documentation folder')
|
|
334
|
+
.option('--debug', 'Enable debug logging')
|
|
335
|
+
.addHelpText('after', `
|
|
336
|
+
${pc.bold(pc.yellow('Usage:'))}
|
|
337
|
+
${pc.green('$ rek mcp')} ${pc.gray('Start server on port 3100')}
|
|
338
|
+
${pc.green('$ rek mcp -p 8080')} ${pc.gray('Start on custom port')}
|
|
339
|
+
${pc.green('$ rek mcp --debug')} ${pc.gray('Enable debug logging')}
|
|
340
|
+
|
|
341
|
+
${pc.bold(pc.yellow('Tools provided:'))}
|
|
342
|
+
${pc.cyan('search_docs')} Search documentation by keyword
|
|
343
|
+
${pc.cyan('get_doc')} Get full content of a doc file
|
|
344
|
+
|
|
345
|
+
${pc.bold(pc.yellow('Claude Code config (~/.claude.json):'))}
|
|
346
|
+
${pc.gray(`{
|
|
347
|
+
"mcpServers": {
|
|
348
|
+
"recker-docs": {
|
|
349
|
+
"command": "npx",
|
|
350
|
+
"args": ["recker", "mcp"]
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}`)}
|
|
354
|
+
`)
|
|
355
|
+
.action(async (options) => {
|
|
356
|
+
const { MCPServer } = await import('../mcp/server.js');
|
|
357
|
+
const server = new MCPServer({
|
|
358
|
+
port: parseInt(options.port),
|
|
359
|
+
docsPath: options.docs,
|
|
360
|
+
debug: options.debug,
|
|
361
|
+
});
|
|
362
|
+
await server.start();
|
|
363
|
+
console.log(pc.green(`
|
|
364
|
+
┌─────────────────────────────────────────────┐
|
|
365
|
+
│ ${pc.bold('Recker MCP Server')} │
|
|
366
|
+
├─────────────────────────────────────────────┤
|
|
367
|
+
│ Endpoint: ${pc.cyan(`http://localhost:${options.port}`)} │
|
|
368
|
+
│ Docs indexed: ${pc.yellow(String(server.getDocsCount()).padEnd(28))}│
|
|
369
|
+
│ │
|
|
370
|
+
│ Tools: │
|
|
371
|
+
│ • ${pc.cyan('search_docs')} - Search documentation │
|
|
372
|
+
│ • ${pc.cyan('get_doc')} - Get full doc content │
|
|
373
|
+
│ │
|
|
374
|
+
│ Press ${pc.bold('Ctrl+C')} to stop │
|
|
375
|
+
└─────────────────────────────────────────────┘
|
|
376
|
+
`));
|
|
377
|
+
process.on('SIGINT', async () => {
|
|
378
|
+
console.log(pc.yellow('\nShutting down MCP server...'));
|
|
379
|
+
await server.stop();
|
|
380
|
+
process.exit(0);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
329
383
|
program.parse();
|
|
330
384
|
}
|
|
331
385
|
main().catch((error) => {
|
package/dist/mcp/index.d.ts
CHANGED
package/dist/mcp/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAKA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":"AAKA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface MCPServerOptions {
|
|
2
|
+
name?: string;
|
|
3
|
+
version?: string;
|
|
4
|
+
docsPath?: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
debug?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class MCPServer {
|
|
9
|
+
private options;
|
|
10
|
+
private server?;
|
|
11
|
+
private docsIndex;
|
|
12
|
+
constructor(options?: MCPServerOptions);
|
|
13
|
+
private findDocsPath;
|
|
14
|
+
private buildIndex;
|
|
15
|
+
private walkDir;
|
|
16
|
+
private extractTitle;
|
|
17
|
+
private extractKeywords;
|
|
18
|
+
private getTools;
|
|
19
|
+
private handleToolCall;
|
|
20
|
+
private searchDocs;
|
|
21
|
+
private extractSnippet;
|
|
22
|
+
private getDoc;
|
|
23
|
+
private handleRequest;
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
stop(): Promise<void>;
|
|
26
|
+
getPort(): number;
|
|
27
|
+
getDocsCount(): number;
|
|
28
|
+
}
|
|
29
|
+
export declare function createMCPServer(options?: MCPServerOptions): MCPServer;
|
|
30
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiBD,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,SAAS,CAAkB;gBAEvB,OAAO,GAAE,gBAAqB;IAY1C,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,UAAU;IAuClB,OAAO,CAAC,OAAO;IAyBf,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,eAAe;IAyBvB,OAAO,CAAC,QAAQ;IAyChB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,UAAU;IA4DlB,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,MAAM;IAmCd,OAAO,CAAC,aAAa;IAiDf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA4DtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B,OAAO,IAAI,MAAM;IAIjB,YAAY,IAAI,MAAM;CAGvB;AAKD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAErE"}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
export class MCPServer {
|
|
5
|
+
options;
|
|
6
|
+
server;
|
|
7
|
+
docsIndex = [];
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = {
|
|
10
|
+
name: options.name || 'recker-docs',
|
|
11
|
+
version: options.version || '1.0.0',
|
|
12
|
+
docsPath: options.docsPath || this.findDocsPath(),
|
|
13
|
+
port: options.port || 3100,
|
|
14
|
+
debug: options.debug || false,
|
|
15
|
+
};
|
|
16
|
+
this.buildIndex();
|
|
17
|
+
}
|
|
18
|
+
findDocsPath() {
|
|
19
|
+
const possiblePaths = [
|
|
20
|
+
join(process.cwd(), 'docs'),
|
|
21
|
+
join(process.cwd(), '..', 'docs'),
|
|
22
|
+
join(__dirname, '..', '..', 'docs'),
|
|
23
|
+
join(__dirname, '..', '..', '..', 'docs'),
|
|
24
|
+
];
|
|
25
|
+
for (const p of possiblePaths) {
|
|
26
|
+
if (existsSync(p)) {
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return join(process.cwd(), 'docs');
|
|
31
|
+
}
|
|
32
|
+
buildIndex() {
|
|
33
|
+
if (!existsSync(this.options.docsPath)) {
|
|
34
|
+
if (this.options.debug) {
|
|
35
|
+
console.log(`[MCP] Docs path not found: ${this.options.docsPath}`);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const files = this.walkDir(this.options.docsPath);
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (!file.endsWith('.md'))
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
const content = readFileSync(file, 'utf-8');
|
|
45
|
+
const relativePath = relative(this.options.docsPath, file);
|
|
46
|
+
const category = relativePath.split('/')[0] || 'root';
|
|
47
|
+
const title = this.extractTitle(content) || relativePath;
|
|
48
|
+
const keywords = this.extractKeywords(content);
|
|
49
|
+
this.docsIndex.push({
|
|
50
|
+
path: relativePath,
|
|
51
|
+
title,
|
|
52
|
+
category,
|
|
53
|
+
content,
|
|
54
|
+
keywords,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (this.options.debug) {
|
|
59
|
+
console.log(`[MCP] Failed to index ${file}:`, err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (this.options.debug) {
|
|
64
|
+
console.log(`[MCP] Indexed ${this.docsIndex.length} documentation files`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
walkDir(dir) {
|
|
68
|
+
const files = [];
|
|
69
|
+
try {
|
|
70
|
+
const entries = readdirSync(dir);
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (entry.startsWith('_') || entry.startsWith('.'))
|
|
73
|
+
continue;
|
|
74
|
+
const fullPath = join(dir, entry);
|
|
75
|
+
const stat = statSync(fullPath);
|
|
76
|
+
if (stat.isDirectory()) {
|
|
77
|
+
files.push(...this.walkDir(fullPath));
|
|
78
|
+
}
|
|
79
|
+
else if (stat.isFile()) {
|
|
80
|
+
files.push(fullPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
}
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
extractTitle(content) {
|
|
89
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
90
|
+
return match ? match[1].trim() : '';
|
|
91
|
+
}
|
|
92
|
+
extractKeywords(content) {
|
|
93
|
+
const keywords = new Set();
|
|
94
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm) || [];
|
|
95
|
+
for (const h of headings) {
|
|
96
|
+
keywords.add(h.replace(/^#+\s+/, '').toLowerCase());
|
|
97
|
+
}
|
|
98
|
+
const codePatterns = content.match(/`([a-zA-Z_][a-zA-Z0-9_]*(?:\(\))?)`/g) || [];
|
|
99
|
+
for (const c of codePatterns) {
|
|
100
|
+
keywords.add(c.replace(/`/g, '').toLowerCase());
|
|
101
|
+
}
|
|
102
|
+
const terms = content.match(/\b[A-Z][a-zA-Z]+(?:Client|Server|Error|Response|Request|Plugin|Transport)\b/g) || [];
|
|
103
|
+
for (const t of terms) {
|
|
104
|
+
keywords.add(t.toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
return Array.from(keywords).slice(0, 50);
|
|
107
|
+
}
|
|
108
|
+
getTools() {
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
name: 'search_docs',
|
|
112
|
+
description: 'Search Recker documentation by keyword. Returns matching doc files with titles and snippets. Use this first to find relevant documentation.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
query: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'Search query (e.g., "retry", "cache", "streaming", "websocket")',
|
|
119
|
+
},
|
|
120
|
+
category: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: 'Optional: filter by category (http, cli, ai, protocols, reference, guides)',
|
|
123
|
+
},
|
|
124
|
+
limit: {
|
|
125
|
+
type: 'number',
|
|
126
|
+
description: 'Max results to return (default: 5)',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ['query'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'get_doc',
|
|
134
|
+
description: 'Get the full content of a specific documentation file. Use the path from search_docs results.',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
path: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Documentation file path (e.g., "http/07-resilience.md", "cli/01-overview.md")',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ['path'],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
handleToolCall(name, args) {
|
|
149
|
+
switch (name) {
|
|
150
|
+
case 'search_docs':
|
|
151
|
+
return this.searchDocs(args);
|
|
152
|
+
case 'get_doc':
|
|
153
|
+
return this.getDoc(args);
|
|
154
|
+
default:
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
157
|
+
isError: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
searchDocs(args) {
|
|
162
|
+
const query = String(args.query || '').toLowerCase();
|
|
163
|
+
const category = args.category ? String(args.category).toLowerCase() : null;
|
|
164
|
+
const limit = Math.min(Number(args.limit) || 5, 10);
|
|
165
|
+
if (!query) {
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: 'text', text: 'Error: query is required' }],
|
|
168
|
+
isError: true,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const results = [];
|
|
172
|
+
for (const doc of this.docsIndex) {
|
|
173
|
+
if (category && !doc.category.toLowerCase().includes(category)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
let score = 0;
|
|
177
|
+
const queryTerms = query.split(/\s+/);
|
|
178
|
+
for (const term of queryTerms) {
|
|
179
|
+
if (doc.title.toLowerCase().includes(term))
|
|
180
|
+
score += 10;
|
|
181
|
+
if (doc.path.toLowerCase().includes(term))
|
|
182
|
+
score += 5;
|
|
183
|
+
if (doc.keywords.some(k => k.includes(term)))
|
|
184
|
+
score += 3;
|
|
185
|
+
if (doc.content.toLowerCase().includes(term))
|
|
186
|
+
score += 1;
|
|
187
|
+
}
|
|
188
|
+
if (score > 0) {
|
|
189
|
+
const snippet = this.extractSnippet(doc.content, query);
|
|
190
|
+
results.push({ doc, score, snippet });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
results.sort((a, b) => b.score - a.score);
|
|
194
|
+
const topResults = results.slice(0, limit);
|
|
195
|
+
if (topResults.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: `No documentation found for "${query}". Try different keywords like: http, cache, retry, streaming, websocket, ai, cli, plugins`,
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const output = topResults.map((r, i) => `${i + 1}. **${r.doc.title}**\n Path: \`${r.doc.path}\`\n Category: ${r.doc.category}\n ${r.snippet}`).join('\n\n');
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: `Found ${topResults.length} result(s) for "${query}":\n\n${output}\n\nUse get_doc with the path to read full content.`,
|
|
208
|
+
}],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
extractSnippet(content, query) {
|
|
212
|
+
const lowerContent = content.toLowerCase();
|
|
213
|
+
const index = lowerContent.indexOf(query.split(/\s+/)[0]);
|
|
214
|
+
if (index === -1) {
|
|
215
|
+
const firstPara = content.split('\n\n')[1] || content.substring(0, 200);
|
|
216
|
+
return firstPara.substring(0, 150).trim() + '...';
|
|
217
|
+
}
|
|
218
|
+
const start = Math.max(0, index - 50);
|
|
219
|
+
const end = Math.min(content.length, index + 150);
|
|
220
|
+
let snippet = content.substring(start, end).trim();
|
|
221
|
+
if (start > 0)
|
|
222
|
+
snippet = '...' + snippet;
|
|
223
|
+
if (end < content.length)
|
|
224
|
+
snippet = snippet + '...';
|
|
225
|
+
return snippet.replace(/\n/g, ' ');
|
|
226
|
+
}
|
|
227
|
+
getDoc(args) {
|
|
228
|
+
const path = String(args.path || '');
|
|
229
|
+
if (!path) {
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: 'text', text: 'Error: path is required' }],
|
|
232
|
+
isError: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const doc = this.docsIndex.find(d => d.path === path || d.path.endsWith(path));
|
|
236
|
+
if (!doc) {
|
|
237
|
+
const suggestions = this.docsIndex
|
|
238
|
+
.filter(d => d.path.includes(path.split('/').pop() || ''))
|
|
239
|
+
.slice(0, 3)
|
|
240
|
+
.map(d => d.path);
|
|
241
|
+
return {
|
|
242
|
+
content: [{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: `Documentation not found: ${path}${suggestions.length ? `\n\nDid you mean:\n${suggestions.map(s => `- ${s}`).join('\n')}` : ''}`,
|
|
245
|
+
}],
|
|
246
|
+
isError: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: 'text',
|
|
252
|
+
text: `# ${doc.title}\n\nPath: ${doc.path}\nCategory: ${doc.category}\n\n---\n\n${doc.content}`,
|
|
253
|
+
}],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
handleRequest(req) {
|
|
257
|
+
const { method, params, id } = req;
|
|
258
|
+
try {
|
|
259
|
+
switch (method) {
|
|
260
|
+
case 'initialize': {
|
|
261
|
+
const response = {
|
|
262
|
+
protocolVersion: '2024-11-05',
|
|
263
|
+
capabilities: {
|
|
264
|
+
tools: { listChanged: false },
|
|
265
|
+
},
|
|
266
|
+
serverInfo: {
|
|
267
|
+
name: this.options.name,
|
|
268
|
+
version: this.options.version,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
return { jsonrpc: '2.0', id: id, result: response };
|
|
272
|
+
}
|
|
273
|
+
case 'ping':
|
|
274
|
+
return { jsonrpc: '2.0', id: id, result: {} };
|
|
275
|
+
case 'tools/list': {
|
|
276
|
+
const response = { tools: this.getTools() };
|
|
277
|
+
return { jsonrpc: '2.0', id: id, result: response };
|
|
278
|
+
}
|
|
279
|
+
case 'tools/call': {
|
|
280
|
+
const { name, arguments: args } = params;
|
|
281
|
+
const result = this.handleToolCall(name, args || {});
|
|
282
|
+
return { jsonrpc: '2.0', id: id, result };
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
return {
|
|
286
|
+
jsonrpc: '2.0',
|
|
287
|
+
id: id,
|
|
288
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
return {
|
|
294
|
+
jsonrpc: '2.0',
|
|
295
|
+
id: id,
|
|
296
|
+
error: { code: -32603, message: String(err) },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async start() {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
this.server = createServer((req, res) => {
|
|
303
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
304
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
305
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
306
|
+
if (req.method === 'OPTIONS') {
|
|
307
|
+
res.writeHead(204);
|
|
308
|
+
res.end();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (req.method !== 'POST') {
|
|
312
|
+
res.writeHead(405);
|
|
313
|
+
res.end('Method not allowed');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
let body = '';
|
|
317
|
+
req.on('data', chunk => body += chunk);
|
|
318
|
+
req.on('end', () => {
|
|
319
|
+
try {
|
|
320
|
+
const request = JSON.parse(body);
|
|
321
|
+
if (this.options.debug) {
|
|
322
|
+
console.log('[MCP] Request:', JSON.stringify(request, null, 2));
|
|
323
|
+
}
|
|
324
|
+
const response = this.handleRequest(request);
|
|
325
|
+
if (this.options.debug) {
|
|
326
|
+
console.log('[MCP] Response:', JSON.stringify(response, null, 2));
|
|
327
|
+
}
|
|
328
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
329
|
+
res.end(JSON.stringify(response));
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
333
|
+
res.end(JSON.stringify({
|
|
334
|
+
jsonrpc: '2.0',
|
|
335
|
+
id: null,
|
|
336
|
+
error: { code: -32700, message: 'Parse error' },
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
this.server.listen(this.options.port, () => {
|
|
342
|
+
if (this.options.debug) {
|
|
343
|
+
console.log(`[MCP] Server listening on http://localhost:${this.options.port}`);
|
|
344
|
+
console.log(`[MCP] Docs path: ${this.options.docsPath}`);
|
|
345
|
+
console.log(`[MCP] Indexed ${this.docsIndex.length} files`);
|
|
346
|
+
}
|
|
347
|
+
resolve();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
async stop() {
|
|
352
|
+
return new Promise((resolve) => {
|
|
353
|
+
if (this.server) {
|
|
354
|
+
this.server.close(() => resolve());
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
resolve();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
getPort() {
|
|
362
|
+
return this.options.port;
|
|
363
|
+
}
|
|
364
|
+
getDocsCount() {
|
|
365
|
+
return this.docsIndex.length;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
export function createMCPServer(options) {
|
|
369
|
+
return new MCPServer(options);
|
|
370
|
+
}
|