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 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) => {
@@ -1,3 +1,4 @@
1
1
  export * from './types.js';
2
2
  export * from './client.js';
3
+ export * from './server.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -1,2 +1,3 @@
1
1
  export * from './types.js';
2
2
  export * from './client.js';
3
+ export * from './server.js';
@@ -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.11",
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
  }