recker 1.0.11 → 1.0.12
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 +76 -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 +41 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +503 -0
- package/package.json +4 -3
package/dist/cli/index.js
CHANGED
|
@@ -326,6 +326,82 @@ ${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('-t, --transport <mode>', 'Transport mode: stdio, http, sse', 'stdio')
|
|
333
|
+
.option('-p, --port <number>', 'Server port (for http/sse modes)', '3100')
|
|
334
|
+
.option('-d, --docs <path>', 'Path to documentation folder')
|
|
335
|
+
.option('--debug', 'Enable debug logging')
|
|
336
|
+
.addHelpText('after', `
|
|
337
|
+
${pc.bold(pc.yellow('Transport Modes:'))}
|
|
338
|
+
${pc.cyan('stdio')} ${pc.gray('(default)')} For Claude Code and other CLI tools
|
|
339
|
+
${pc.cyan('http')} Simple HTTP POST endpoint
|
|
340
|
+
${pc.cyan('sse')} HTTP + Server-Sent Events for real-time notifications
|
|
341
|
+
|
|
342
|
+
${pc.bold(pc.yellow('Usage:'))}
|
|
343
|
+
${pc.green('$ rek mcp')} ${pc.gray('Start in stdio mode (for Claude Code)')}
|
|
344
|
+
${pc.green('$ rek mcp -t http')} ${pc.gray('Start HTTP server on port 3100')}
|
|
345
|
+
${pc.green('$ rek mcp -t sse -p 8080')} ${pc.gray('Start SSE server on custom port')}
|
|
346
|
+
${pc.green('$ rek mcp --debug')} ${pc.gray('Enable debug logging')}
|
|
347
|
+
|
|
348
|
+
${pc.bold(pc.yellow('Tools provided:'))}
|
|
349
|
+
${pc.cyan('search_docs')} Search documentation by keyword
|
|
350
|
+
${pc.cyan('get_doc')} Get full content of a doc file
|
|
351
|
+
|
|
352
|
+
${pc.bold(pc.yellow('Claude Code config (~/.claude.json):'))}
|
|
353
|
+
${pc.gray(`{
|
|
354
|
+
"mcpServers": {
|
|
355
|
+
"recker-docs": {
|
|
356
|
+
"command": "npx",
|
|
357
|
+
"args": ["recker", "mcp"]
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}`)}
|
|
361
|
+
`)
|
|
362
|
+
.action(async (options) => {
|
|
363
|
+
const { MCPServer } = await import('../mcp/server.js');
|
|
364
|
+
const transport = options.transport;
|
|
365
|
+
const server = new MCPServer({
|
|
366
|
+
transport,
|
|
367
|
+
port: parseInt(options.port),
|
|
368
|
+
docsPath: options.docs,
|
|
369
|
+
debug: options.debug,
|
|
370
|
+
});
|
|
371
|
+
if (transport === 'stdio') {
|
|
372
|
+
await server.start();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await server.start();
|
|
376
|
+
const endpoints = transport === 'sse'
|
|
377
|
+
? `
|
|
378
|
+
│ POST / - JSON-RPC endpoint │
|
|
379
|
+
│ GET /sse - Server-Sent Events │
|
|
380
|
+
│ GET /health - Health check │`
|
|
381
|
+
: `
|
|
382
|
+
│ POST / - JSON-RPC endpoint │`;
|
|
383
|
+
console.log(pc.green(`
|
|
384
|
+
┌─────────────────────────────────────────────┐
|
|
385
|
+
│ ${pc.bold('Recker MCP Server')} │
|
|
386
|
+
├─────────────────────────────────────────────┤
|
|
387
|
+
│ Transport: ${pc.cyan(transport.padEnd(31))}│
|
|
388
|
+
│ Endpoint: ${pc.cyan(`http://localhost:${options.port}`.padEnd(32))}│
|
|
389
|
+
│ Docs indexed: ${pc.yellow(String(server.getDocsCount()).padEnd(28))}│
|
|
390
|
+
├─────────────────────────────────────────────┤${endpoints}
|
|
391
|
+
├─────────────────────────────────────────────┤
|
|
392
|
+
│ Tools: │
|
|
393
|
+
│ • ${pc.cyan('search_docs')} - Search documentation │
|
|
394
|
+
│ • ${pc.cyan('get_doc')} - Get full doc content │
|
|
395
|
+
│ │
|
|
396
|
+
│ Press ${pc.bold('Ctrl+C')} to stop │
|
|
397
|
+
└─────────────────────────────────────────────┘
|
|
398
|
+
`));
|
|
399
|
+
process.on('SIGINT', async () => {
|
|
400
|
+
console.log(pc.yellow('\nShutting down MCP server...'));
|
|
401
|
+
await server.stop();
|
|
402
|
+
process.exit(0);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
329
405
|
program.parse();
|
|
330
406
|
}
|
|
331
407
|
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,41 @@
|
|
|
1
|
+
import type { JsonRpcRequest, JsonRpcResponse } from './types.js';
|
|
2
|
+
export type MCPTransportMode = 'stdio' | 'http' | 'sse';
|
|
3
|
+
export interface MCPServerOptions {
|
|
4
|
+
name?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
docsPath?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
transport?: MCPTransportMode;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class MCPServer {
|
|
12
|
+
private options;
|
|
13
|
+
private server?;
|
|
14
|
+
private docsIndex;
|
|
15
|
+
private sseClients;
|
|
16
|
+
private initialized;
|
|
17
|
+
constructor(options?: MCPServerOptions);
|
|
18
|
+
private log;
|
|
19
|
+
private findDocsPath;
|
|
20
|
+
private buildIndex;
|
|
21
|
+
private walkDir;
|
|
22
|
+
private extractTitle;
|
|
23
|
+
private extractKeywords;
|
|
24
|
+
private getTools;
|
|
25
|
+
private handleToolCall;
|
|
26
|
+
private searchDocs;
|
|
27
|
+
private extractSnippet;
|
|
28
|
+
private getDoc;
|
|
29
|
+
handleRequest(req: JsonRpcRequest): JsonRpcResponse;
|
|
30
|
+
private sendNotification;
|
|
31
|
+
private startStdio;
|
|
32
|
+
private startHttp;
|
|
33
|
+
private startSSE;
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
getPort(): number;
|
|
37
|
+
getDocsCount(): number;
|
|
38
|
+
getTransport(): MCPTransportMode;
|
|
39
|
+
}
|
|
40
|
+
export declare function createMCPServer(options?: MCPServerOptions): MCPServer;
|
|
41
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAMhB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAExD,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,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAsBD,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,GAAE,gBAAqB;IAa1C,OAAO,CAAC,GAAG;IAWX,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,UAAU;IAiClB,OAAO,CAAC,OAAO;IAyBf,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,QAAQ;IAyChB,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,UAAU;IA2DlB,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,MAAM;IAmCd,aAAa,CAAC,GAAG,EAAE,cAAc,GAAG,eAAe;IAkEnD,OAAO,CAAC,gBAAgB;YAUV,UAAU;YAqCV,SAAS;YAkDT,QAAQ;IA2FhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAatB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B,OAAO,IAAI,MAAM;IAIjB,YAAY,IAAI,MAAM;IAItB,YAAY,IAAI,gBAAgB;CAGjC;AAoBD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAErE"}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
export class MCPServer {
|
|
6
|
+
options;
|
|
7
|
+
server;
|
|
8
|
+
docsIndex = [];
|
|
9
|
+
sseClients = new Set();
|
|
10
|
+
initialized = false;
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.options = {
|
|
13
|
+
name: options.name || 'recker-docs',
|
|
14
|
+
version: options.version || '1.0.0',
|
|
15
|
+
docsPath: options.docsPath || this.findDocsPath(),
|
|
16
|
+
port: options.port || 3100,
|
|
17
|
+
transport: options.transport || 'stdio',
|
|
18
|
+
debug: options.debug || false,
|
|
19
|
+
};
|
|
20
|
+
this.buildIndex();
|
|
21
|
+
}
|
|
22
|
+
log(message, data) {
|
|
23
|
+
if (this.options.debug) {
|
|
24
|
+
if (this.options.transport === 'stdio') {
|
|
25
|
+
console.error(`[MCP] ${message}`, data ? JSON.stringify(data) : '');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(`[MCP] ${message}`, data ? JSON.stringify(data) : '');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
findDocsPath() {
|
|
33
|
+
const possiblePaths = [
|
|
34
|
+
join(process.cwd(), 'docs'),
|
|
35
|
+
join(process.cwd(), '..', 'docs'),
|
|
36
|
+
];
|
|
37
|
+
if (typeof __dirname !== 'undefined') {
|
|
38
|
+
possiblePaths.push(join(__dirname, '..', '..', 'docs'), join(__dirname, '..', '..', '..', 'docs'));
|
|
39
|
+
}
|
|
40
|
+
for (const p of possiblePaths) {
|
|
41
|
+
if (existsSync(p)) {
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return join(process.cwd(), 'docs');
|
|
46
|
+
}
|
|
47
|
+
buildIndex() {
|
|
48
|
+
if (!existsSync(this.options.docsPath)) {
|
|
49
|
+
this.log(`Docs path not found: ${this.options.docsPath}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const files = this.walkDir(this.options.docsPath);
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (!file.endsWith('.md'))
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const content = readFileSync(file, 'utf-8');
|
|
58
|
+
const relativePath = relative(this.options.docsPath, file);
|
|
59
|
+
const category = relativePath.split('/')[0] || 'root';
|
|
60
|
+
const title = this.extractTitle(content) || relativePath;
|
|
61
|
+
const keywords = this.extractKeywords(content);
|
|
62
|
+
this.docsIndex.push({
|
|
63
|
+
path: relativePath,
|
|
64
|
+
title,
|
|
65
|
+
category,
|
|
66
|
+
content,
|
|
67
|
+
keywords,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
this.log(`Failed to index ${file}:`, err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
this.log(`Indexed ${this.docsIndex.length} documentation files`);
|
|
75
|
+
}
|
|
76
|
+
walkDir(dir) {
|
|
77
|
+
const files = [];
|
|
78
|
+
try {
|
|
79
|
+
const entries = readdirSync(dir);
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.startsWith('_') || entry.startsWith('.'))
|
|
82
|
+
continue;
|
|
83
|
+
const fullPath = join(dir, entry);
|
|
84
|
+
const stat = statSync(fullPath);
|
|
85
|
+
if (stat.isDirectory()) {
|
|
86
|
+
files.push(...this.walkDir(fullPath));
|
|
87
|
+
}
|
|
88
|
+
else if (stat.isFile()) {
|
|
89
|
+
files.push(fullPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
}
|
|
97
|
+
extractTitle(content) {
|
|
98
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
99
|
+
return match ? match[1].trim() : '';
|
|
100
|
+
}
|
|
101
|
+
extractKeywords(content) {
|
|
102
|
+
const keywords = new Set();
|
|
103
|
+
const headings = content.match(/^#{1,3}\s+(.+)$/gm) || [];
|
|
104
|
+
for (const h of headings) {
|
|
105
|
+
keywords.add(h.replace(/^#+\s+/, '').toLowerCase());
|
|
106
|
+
}
|
|
107
|
+
const codePatterns = content.match(/`([a-zA-Z_][a-zA-Z0-9_]*(?:\(\))?)`/g) || [];
|
|
108
|
+
for (const c of codePatterns) {
|
|
109
|
+
keywords.add(c.replace(/`/g, '').toLowerCase());
|
|
110
|
+
}
|
|
111
|
+
const terms = content.match(/\b[A-Z][a-zA-Z]+(?:Client|Server|Error|Response|Request|Plugin|Transport)\b/g) || [];
|
|
112
|
+
for (const t of terms) {
|
|
113
|
+
keywords.add(t.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
return Array.from(keywords).slice(0, 50);
|
|
116
|
+
}
|
|
117
|
+
getTools() {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
name: 'search_docs',
|
|
121
|
+
description: 'Search Recker documentation by keyword. Returns matching doc files with titles and snippets. Use this first to find relevant documentation.',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
query: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
description: 'Search query (e.g., "retry", "cache", "streaming", "websocket")',
|
|
128
|
+
},
|
|
129
|
+
category: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Optional: filter by category (http, cli, ai, protocols, reference, guides)',
|
|
132
|
+
},
|
|
133
|
+
limit: {
|
|
134
|
+
type: 'number',
|
|
135
|
+
description: 'Max results to return (default: 5)',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ['query'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'get_doc',
|
|
143
|
+
description: 'Get the full content of a specific documentation file. Use the path from search_docs results.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
path: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'Documentation file path (e.g., "http/07-resilience.md", "cli/01-overview.md")',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ['path'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
handleToolCall(name, args) {
|
|
158
|
+
switch (name) {
|
|
159
|
+
case 'search_docs':
|
|
160
|
+
return this.searchDocs(args);
|
|
161
|
+
case 'get_doc':
|
|
162
|
+
return this.getDoc(args);
|
|
163
|
+
default:
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
searchDocs(args) {
|
|
171
|
+
const query = String(args.query || '').toLowerCase();
|
|
172
|
+
const category = args.category ? String(args.category).toLowerCase() : null;
|
|
173
|
+
const limit = Math.min(Number(args.limit) || 5, 10);
|
|
174
|
+
if (!query) {
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: 'text', text: 'Error: query is required' }],
|
|
177
|
+
isError: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const doc of this.docsIndex) {
|
|
182
|
+
if (category && !doc.category.toLowerCase().includes(category)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
let score = 0;
|
|
186
|
+
const queryTerms = query.split(/\s+/);
|
|
187
|
+
for (const term of queryTerms) {
|
|
188
|
+
if (doc.title.toLowerCase().includes(term))
|
|
189
|
+
score += 10;
|
|
190
|
+
if (doc.path.toLowerCase().includes(term))
|
|
191
|
+
score += 5;
|
|
192
|
+
if (doc.keywords.some(k => k.includes(term)))
|
|
193
|
+
score += 3;
|
|
194
|
+
if (doc.content.toLowerCase().includes(term))
|
|
195
|
+
score += 1;
|
|
196
|
+
}
|
|
197
|
+
if (score > 0) {
|
|
198
|
+
const snippet = this.extractSnippet(doc.content, query);
|
|
199
|
+
results.push({ doc, score, snippet });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
results.sort((a, b) => b.score - a.score);
|
|
203
|
+
const topResults = results.slice(0, limit);
|
|
204
|
+
if (topResults.length === 0) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: `No documentation found for "${query}". Try different keywords like: http, cache, retry, streaming, websocket, ai, cli, plugins`,
|
|
209
|
+
}],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
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');
|
|
213
|
+
return {
|
|
214
|
+
content: [{
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: `Found ${topResults.length} result(s) for "${query}":\n\n${output}\n\nUse get_doc with the path to read full content.`,
|
|
217
|
+
}],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
extractSnippet(content, query) {
|
|
221
|
+
const lowerContent = content.toLowerCase();
|
|
222
|
+
const index = lowerContent.indexOf(query.split(/\s+/)[0]);
|
|
223
|
+
if (index === -1) {
|
|
224
|
+
const firstPara = content.split('\n\n')[1] || content.substring(0, 200);
|
|
225
|
+
return firstPara.substring(0, 150).trim() + '...';
|
|
226
|
+
}
|
|
227
|
+
const start = Math.max(0, index - 50);
|
|
228
|
+
const end = Math.min(content.length, index + 150);
|
|
229
|
+
let snippet = content.substring(start, end).trim();
|
|
230
|
+
if (start > 0)
|
|
231
|
+
snippet = '...' + snippet;
|
|
232
|
+
if (end < content.length)
|
|
233
|
+
snippet = snippet + '...';
|
|
234
|
+
return snippet.replace(/\n/g, ' ');
|
|
235
|
+
}
|
|
236
|
+
getDoc(args) {
|
|
237
|
+
const path = String(args.path || '');
|
|
238
|
+
if (!path) {
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: 'text', text: 'Error: path is required' }],
|
|
241
|
+
isError: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const doc = this.docsIndex.find(d => d.path === path || d.path.endsWith(path));
|
|
245
|
+
if (!doc) {
|
|
246
|
+
const suggestions = this.docsIndex
|
|
247
|
+
.filter(d => d.path.includes(path.split('/').pop() || ''))
|
|
248
|
+
.slice(0, 3)
|
|
249
|
+
.map(d => d.path);
|
|
250
|
+
return {
|
|
251
|
+
content: [{
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: `Documentation not found: ${path}${suggestions.length ? `\n\nDid you mean:\n${suggestions.map(s => `- ${s}`).join('\n')}` : ''}`,
|
|
254
|
+
}],
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
content: [{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: `# ${doc.title}\n\nPath: ${doc.path}\nCategory: ${doc.category}\n\n---\n\n${doc.content}`,
|
|
262
|
+
}],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
handleRequest(req) {
|
|
266
|
+
const { method, params, id } = req;
|
|
267
|
+
this.log(`Request: ${method}`, params);
|
|
268
|
+
try {
|
|
269
|
+
switch (method) {
|
|
270
|
+
case 'initialize': {
|
|
271
|
+
this.initialized = true;
|
|
272
|
+
const response = {
|
|
273
|
+
protocolVersion: '2024-11-05',
|
|
274
|
+
capabilities: {
|
|
275
|
+
tools: { listChanged: false },
|
|
276
|
+
},
|
|
277
|
+
serverInfo: {
|
|
278
|
+
name: this.options.name,
|
|
279
|
+
version: this.options.version,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
return { jsonrpc: '2.0', id: id, result: response };
|
|
283
|
+
}
|
|
284
|
+
case 'notifications/initialized': {
|
|
285
|
+
return { jsonrpc: '2.0', id: id, result: {} };
|
|
286
|
+
}
|
|
287
|
+
case 'ping':
|
|
288
|
+
return { jsonrpc: '2.0', id: id, result: {} };
|
|
289
|
+
case 'tools/list': {
|
|
290
|
+
const response = { tools: this.getTools() };
|
|
291
|
+
return { jsonrpc: '2.0', id: id, result: response };
|
|
292
|
+
}
|
|
293
|
+
case 'tools/call': {
|
|
294
|
+
const { name, arguments: args } = params;
|
|
295
|
+
const result = this.handleToolCall(name, args || {});
|
|
296
|
+
return { jsonrpc: '2.0', id: id, result };
|
|
297
|
+
}
|
|
298
|
+
case 'resources/list':
|
|
299
|
+
return { jsonrpc: '2.0', id: id, result: { resources: [] } };
|
|
300
|
+
case 'prompts/list':
|
|
301
|
+
return { jsonrpc: '2.0', id: id, result: { prompts: [] } };
|
|
302
|
+
default:
|
|
303
|
+
return {
|
|
304
|
+
jsonrpc: '2.0',
|
|
305
|
+
id: id,
|
|
306
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
return {
|
|
312
|
+
jsonrpc: '2.0',
|
|
313
|
+
id: id,
|
|
314
|
+
error: { code: -32603, message: String(err) },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
sendNotification(notification) {
|
|
319
|
+
const data = JSON.stringify(notification);
|
|
320
|
+
for (const client of this.sseClients) {
|
|
321
|
+
client.write(`data: ${data}\n\n`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async startStdio() {
|
|
325
|
+
const rl = createInterface({
|
|
326
|
+
input: process.stdin,
|
|
327
|
+
output: process.stdout,
|
|
328
|
+
terminal: false,
|
|
329
|
+
});
|
|
330
|
+
this.log('Starting in stdio mode');
|
|
331
|
+
rl.on('line', (line) => {
|
|
332
|
+
if (!line.trim())
|
|
333
|
+
return;
|
|
334
|
+
try {
|
|
335
|
+
const request = JSON.parse(line);
|
|
336
|
+
const response = this.handleRequest(request);
|
|
337
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
const errorResponse = {
|
|
341
|
+
jsonrpc: '2.0',
|
|
342
|
+
id: 0,
|
|
343
|
+
error: { code: -32700, message: 'Parse error' },
|
|
344
|
+
};
|
|
345
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\n');
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
rl.on('close', () => {
|
|
349
|
+
this.log('stdin closed, exiting');
|
|
350
|
+
process.exit(0);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async startHttp() {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
this.server = createServer((req, res) => {
|
|
356
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
357
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
358
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
359
|
+
if (req.method === 'OPTIONS') {
|
|
360
|
+
res.writeHead(204);
|
|
361
|
+
res.end();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (req.method !== 'POST') {
|
|
365
|
+
res.writeHead(405);
|
|
366
|
+
res.end('Method not allowed');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
let body = '';
|
|
370
|
+
req.on('data', chunk => body += chunk);
|
|
371
|
+
req.on('end', () => {
|
|
372
|
+
try {
|
|
373
|
+
const request = JSON.parse(body);
|
|
374
|
+
const response = this.handleRequest(request);
|
|
375
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
376
|
+
res.end(JSON.stringify(response));
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
380
|
+
res.end(JSON.stringify({
|
|
381
|
+
jsonrpc: '2.0',
|
|
382
|
+
id: null,
|
|
383
|
+
error: { code: -32700, message: 'Parse error' },
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
this.server.listen(this.options.port, () => {
|
|
389
|
+
this.log(`HTTP server listening on http://localhost:${this.options.port}`);
|
|
390
|
+
resolve();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async startSSE() {
|
|
395
|
+
return new Promise((resolve) => {
|
|
396
|
+
this.server = createServer((req, res) => {
|
|
397
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
398
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
399
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
400
|
+
const url = req.url || '/';
|
|
401
|
+
if (req.method === 'OPTIONS') {
|
|
402
|
+
res.writeHead(204);
|
|
403
|
+
res.end();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (req.method === 'GET' && url === '/sse') {
|
|
407
|
+
res.writeHead(200, {
|
|
408
|
+
'Content-Type': 'text/event-stream',
|
|
409
|
+
'Cache-Control': 'no-cache',
|
|
410
|
+
'Connection': 'keep-alive',
|
|
411
|
+
});
|
|
412
|
+
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
|
|
413
|
+
this.sseClients.add(res);
|
|
414
|
+
this.log(`SSE client connected (${this.sseClients.size} total)`);
|
|
415
|
+
req.on('close', () => {
|
|
416
|
+
this.sseClients.delete(res);
|
|
417
|
+
this.log(`SSE client disconnected (${this.sseClients.size} total)`);
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (req.method === 'POST') {
|
|
422
|
+
let body = '';
|
|
423
|
+
req.on('data', chunk => body += chunk);
|
|
424
|
+
req.on('end', () => {
|
|
425
|
+
try {
|
|
426
|
+
const request = JSON.parse(body);
|
|
427
|
+
const response = this.handleRequest(request);
|
|
428
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
429
|
+
res.end(JSON.stringify(response));
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
433
|
+
res.end(JSON.stringify({
|
|
434
|
+
jsonrpc: '2.0',
|
|
435
|
+
id: null,
|
|
436
|
+
error: { code: -32700, message: 'Parse error' },
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (req.method === 'GET' && url === '/health') {
|
|
443
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
444
|
+
res.end(JSON.stringify({
|
|
445
|
+
status: 'ok',
|
|
446
|
+
name: this.options.name,
|
|
447
|
+
version: this.options.version,
|
|
448
|
+
docsCount: this.docsIndex.length,
|
|
449
|
+
sseClients: this.sseClients.size,
|
|
450
|
+
}));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
res.writeHead(404);
|
|
454
|
+
res.end('Not found');
|
|
455
|
+
});
|
|
456
|
+
this.server.listen(this.options.port, () => {
|
|
457
|
+
this.log(`SSE server listening on http://localhost:${this.options.port}`);
|
|
458
|
+
this.log(` POST / - JSON-RPC endpoint`);
|
|
459
|
+
this.log(` GET /sse - Server-Sent Events`);
|
|
460
|
+
this.log(` GET /health - Health check`);
|
|
461
|
+
resolve();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
async start() {
|
|
466
|
+
switch (this.options.transport) {
|
|
467
|
+
case 'stdio':
|
|
468
|
+
return this.startStdio();
|
|
469
|
+
case 'http':
|
|
470
|
+
return this.startHttp();
|
|
471
|
+
case 'sse':
|
|
472
|
+
return this.startSSE();
|
|
473
|
+
default:
|
|
474
|
+
throw new Error(`Unknown transport: ${this.options.transport}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async stop() {
|
|
478
|
+
for (const client of this.sseClients) {
|
|
479
|
+
client.end();
|
|
480
|
+
}
|
|
481
|
+
this.sseClients.clear();
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
if (this.server) {
|
|
484
|
+
this.server.close(() => resolve());
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
resolve();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
getPort() {
|
|
492
|
+
return this.options.port;
|
|
493
|
+
}
|
|
494
|
+
getDocsCount() {
|
|
495
|
+
return this.docsIndex.length;
|
|
496
|
+
}
|
|
497
|
+
getTransport() {
|
|
498
|
+
return this.options.transport;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
export function createMCPServer(options) {
|
|
502
|
+
return new MCPServer(options);
|
|
503
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "AI & DevX focused HTTP client for Node.js 18+",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -105,7 +105,8 @@
|
|
|
105
105
|
"./package.json": "./package.json"
|
|
106
106
|
},
|
|
107
107
|
"bin": {
|
|
108
|
-
"rek": "./dist/cli/index.js"
|
|
108
|
+
"rek": "./dist/cli/index.js",
|
|
109
|
+
"recker": "./dist/cli/index.js"
|
|
109
110
|
},
|
|
110
111
|
"engines": {
|
|
111
112
|
"node": ">=18"
|
|
@@ -168,7 +169,7 @@
|
|
|
168
169
|
"test:coverage": "vitest run --coverage",
|
|
169
170
|
"bench": "tsx benchmark/index.ts",
|
|
170
171
|
"bench:all": "tsx benchmark/run-all.ts",
|
|
171
|
-
"docs": "serve docs -p 3000 -o",
|
|
172
|
+
"docs": "npx docsify-cli serve docs -p 3000 -o",
|
|
172
173
|
"lint": "echo \"No linting configured for this project.\" && exit 0",
|
|
173
174
|
"cli": "tsx src/cli/index.ts"
|
|
174
175
|
}
|