@probelabs/probe 0.6.0-rc167 → 0.6.0-rc169

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.
@@ -5,12 +5,71 @@
5
5
 
6
6
  import { createServer } from 'http';
7
7
  import { EventEmitter } from 'events';
8
+ import { randomUUID } from 'crypto';
8
9
  import { Server as MCPServer } from '@modelcontextprotocol/sdk/server/index.js';
10
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
11
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
9
12
  import {
10
13
  CallToolRequestSchema,
11
- ListToolsRequestSchema
14
+ ListToolsRequestSchema,
15
+ isInitializeRequest
12
16
  } from '@modelcontextprotocol/sdk/types.js';
13
17
 
18
+ /**
19
+ * Simple in-memory event store for resumability
20
+ */
21
+ class InMemoryEventStore {
22
+ constructor() {
23
+ this.events = new Map();
24
+ }
25
+
26
+ generateEventId(streamId) {
27
+ return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
28
+ }
29
+
30
+ getStreamIdFromEventId(eventId) {
31
+ const parts = eventId.split('_');
32
+ return parts.length > 0 ? parts[0] : '';
33
+ }
34
+
35
+ async storeEvent(streamId, message) {
36
+ const eventId = this.generateEventId(streamId);
37
+ this.events.set(eventId, { streamId, message });
38
+ return eventId;
39
+ }
40
+
41
+ async replayEventsAfter(lastEventId, { send }) {
42
+ if (!lastEventId || !this.events.has(lastEventId)) {
43
+ return '';
44
+ }
45
+
46
+ const streamId = this.getStreamIdFromEventId(lastEventId);
47
+ if (!streamId) {
48
+ return '';
49
+ }
50
+
51
+ let foundLastEvent = false;
52
+ const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
53
+
54
+ for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) {
55
+ if (eventStreamId !== streamId) {
56
+ continue;
57
+ }
58
+
59
+ if (eventId === lastEventId) {
60
+ foundLastEvent = true;
61
+ continue;
62
+ }
63
+
64
+ if (foundLastEvent) {
65
+ await send(eventId, message);
66
+ }
67
+ }
68
+
69
+ return streamId;
70
+ }
71
+ }
72
+
14
73
  /**
15
74
  * Built-in MCP Server that runs in-process
16
75
  */
@@ -22,6 +81,8 @@ export class BuiltInMCPServer extends EventEmitter {
22
81
  this.host = options.host || '127.0.0.1';
23
82
  this.httpServer = null;
24
83
  this.mcpServer = null;
84
+ this.sseTransports = new Map(); // Map of sessionId -> SSEServerTransport (deprecated)
85
+ this.streamableTransports = new Map(); // Map of sessionId -> StreamableHTTPServerTransport
25
86
  this.connections = new Set();
26
87
  this.debug = options.debug || false;
27
88
  }
@@ -53,12 +114,14 @@ export class BuiltInMCPServer extends EventEmitter {
53
114
 
54
115
  // Start listening on ephemeral port
55
116
  return new Promise((resolve, reject) => {
56
- this.httpServer.listen(this.port, this.host, () => {
117
+ this.httpServer.listen(this.port, this.host, async () => {
57
118
  const address = this.httpServer.address();
58
119
  this.port = address.port;
59
120
 
60
121
  if (this.debug) {
61
122
  console.log(`[MCP] Built-in server started at http://${this.host}:${this.port}`);
123
+ console.log(`[MCP] SSE endpoint: http://${this.host}:${this.port}/sse`);
124
+ console.log(`[MCP] Messages endpoint: http://${this.host}:${this.port}/messages`);
62
125
  }
63
126
 
64
127
  this.emit('ready', { host: this.host, port: this.port });
@@ -75,6 +138,10 @@ export class BuiltInMCPServer extends EventEmitter {
75
138
  handleRequest(req, res) {
76
139
  const { method, url } = req;
77
140
 
141
+ if (this.debug) {
142
+ console.log(`[MCP] Request: ${method} ${url}`);
143
+ }
144
+
78
145
  // CORS headers for local development
79
146
  res.setHeader('Access-Control-Allow-Origin', '*');
80
147
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
@@ -86,9 +153,18 @@ export class BuiltInMCPServer extends EventEmitter {
86
153
  return;
87
154
  }
88
155
 
89
- // Handle SSE endpoint
156
+ // Handle SSE endpoint (GET) - create new transport per connection
90
157
  if (url === '/sse' && method === 'GET') {
91
- this.handleSSE(req, res);
158
+ if (this.debug) {
159
+ console.log('[MCP] Routing to handleSSEConnection');
160
+ }
161
+ this.handleSSEConnection(req, res);
162
+ return;
163
+ }
164
+
165
+ // Handle /messages endpoint (POST) - route to existing transport
166
+ if (url.startsWith('/messages') && method === 'POST') {
167
+ this.handleSSEMessage(req, res);
92
168
  return;
93
169
  }
94
170
 
@@ -98,9 +174,9 @@ export class BuiltInMCPServer extends EventEmitter {
98
174
  return;
99
175
  }
100
176
 
101
- // Handle stdio-like protocol over HTTP
102
- if (url === '/mcp' && method === 'POST') {
103
- this.handleMCPProtocol(req, res);
177
+ // Handle Streamable HTTP protocol (GET/POST/DELETE on /mcp)
178
+ if (url === '/mcp') {
179
+ this.handleStreamableHTTP(req, res);
104
180
  return;
105
181
  }
106
182
 
@@ -121,7 +197,227 @@ export class BuiltInMCPServer extends EventEmitter {
121
197
  }
122
198
 
123
199
  /**
124
- * Handle Server-Sent Events connection
200
+ * Handle SSE connection (GET /sse) - creates new transport
201
+ */
202
+ async handleSSEConnection(req, res) {
203
+ if (this.debug) {
204
+ console.log('[MCP] New SSE connection request');
205
+ }
206
+
207
+ // Create new SSEServerTransport for this connection
208
+ const transport = new SSEServerTransport('/messages', res);
209
+
210
+ // Store transport by sessionId
211
+ this.sseTransports.set(transport.sessionId, transport);
212
+
213
+ // Clean up on connection close
214
+ res.on('close', () => {
215
+ if (this.debug) {
216
+ console.log('[MCP] SSE connection closed, sessionId:', transport.sessionId);
217
+ }
218
+ this.sseTransports.delete(transport.sessionId);
219
+ });
220
+
221
+ // Connect MCP server to this transport
222
+ try {
223
+ await this.mcpServer.connect(transport);
224
+ if (this.debug) {
225
+ console.log('[MCP] MCP server connected to SSE transport, sessionId:', transport.sessionId);
226
+ }
227
+ } catch (error) {
228
+ if (this.debug) {
229
+ console.error('[MCP] Error connecting MCP server to transport:', error);
230
+ }
231
+ this.sseTransports.delete(transport.sessionId);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Handle SSE message (POST /messages?sessionId=...) - routes to existing transport
237
+ */
238
+ async handleSSEMessage(req, res) {
239
+ // Parse URL to get sessionId from query parameter
240
+ const url = new URL(req.url, `http://${req.headers.host}`);
241
+ const sessionId = url.searchParams.get('sessionId');
242
+
243
+ if (!sessionId) {
244
+ res.writeHead(400, { 'Content-Type': 'application/json' });
245
+ res.end(JSON.stringify({
246
+ jsonrpc: '2.0',
247
+ error: {
248
+ code: -32000,
249
+ message: 'Bad Request: sessionId query parameter is required'
250
+ },
251
+ id: null
252
+ }));
253
+ return;
254
+ }
255
+
256
+ // Find transport for this session
257
+ const transport = this.sseTransports.get(sessionId);
258
+ if (!transport) {
259
+ res.writeHead(400, { 'Content-Type': 'application/json' });
260
+ res.end(JSON.stringify({
261
+ jsonrpc: '2.0',
262
+ error: {
263
+ code: -32000,
264
+ message: `Bad Request: No transport found for sessionId: ${sessionId}`
265
+ },
266
+ id: null
267
+ }));
268
+ return;
269
+ }
270
+
271
+ // Read request body
272
+ let body = '';
273
+ req.on('data', chunk => {
274
+ body += chunk.toString();
275
+ });
276
+
277
+ req.on('end', async () => {
278
+ try {
279
+ const message = JSON.parse(body);
280
+ await transport.handlePostMessage(req, res, message);
281
+ } catch (error) {
282
+ res.writeHead(500, { 'Content-Type': 'application/json' });
283
+ res.end(JSON.stringify({
284
+ jsonrpc: '2.0',
285
+ error: {
286
+ code: -32603,
287
+ message: 'Internal error',
288
+ data: error.message
289
+ },
290
+ id: null
291
+ }));
292
+ }
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Handle Streamable HTTP protocol (GET/POST/DELETE on /mcp)
298
+ */
299
+ async handleStreamableHTTP(req, res) {
300
+ const { method } = req;
301
+
302
+ if (this.debug) {
303
+ console.log(`[MCP] Streamable HTTP ${method} request`);
304
+ }
305
+
306
+ try {
307
+ // Parse request body for POST requests
308
+ let body = null;
309
+ if (method === 'POST') {
310
+ body = await this.parseRequestBody(req);
311
+ }
312
+
313
+ // Check for existing session ID in header
314
+ const sessionId = req.headers['mcp-session-id'];
315
+ let transport;
316
+
317
+ if (sessionId && this.streamableTransports.has(sessionId)) {
318
+ // Reuse existing transport
319
+ transport = this.streamableTransports.get(sessionId);
320
+ if (this.debug) {
321
+ console.log(`[MCP] Reusing existing transport for session: ${sessionId}`);
322
+ }
323
+ } else if (!sessionId && method === 'POST' && body && isInitializeRequest(body)) {
324
+ // New session - create transport for initialization request
325
+ if (this.debug) {
326
+ console.log('[MCP] Creating new Streamable HTTP transport for initialization');
327
+ }
328
+
329
+ const eventStore = new InMemoryEventStore();
330
+ transport = new StreamableHTTPServerTransport({
331
+ sessionIdGenerator: () => randomUUID(),
332
+ eventStore, // Enable resumability
333
+ onsessioninitialized: (newSessionId) => {
334
+ // Store the transport by session ID
335
+ if (this.debug) {
336
+ console.log(`[MCP] Streamable HTTP session initialized: ${newSessionId}`);
337
+ }
338
+ this.streamableTransports.set(newSessionId, transport);
339
+ },
340
+ onsessionclosed: (closedSessionId) => {
341
+ // Remove transport when session is closed
342
+ if (this.debug) {
343
+ console.log(`[MCP] Streamable HTTP session closed: ${closedSessionId}`);
344
+ }
345
+ this.streamableTransports.delete(closedSessionId);
346
+ }
347
+ });
348
+
349
+ // Set up onclose handler
350
+ transport.onclose = () => {
351
+ const sid = transport.sessionId;
352
+ if (sid && this.streamableTransports.has(sid)) {
353
+ if (this.debug) {
354
+ console.log(`[MCP] Transport closed for session ${sid}`);
355
+ }
356
+ this.streamableTransports.delete(sid);
357
+ }
358
+ };
359
+
360
+ // Connect the transport to the MCP server
361
+ await this.mcpServer.connect(transport);
362
+ } else {
363
+ // Invalid request - no session ID or not an initialization request
364
+ res.writeHead(400, { 'Content-Type': 'application/json' });
365
+ res.end(JSON.stringify({
366
+ jsonrpc: '2.0',
367
+ error: {
368
+ code: -32000,
369
+ message: 'Bad Request: No valid session ID provided or not an initialization request'
370
+ },
371
+ id: null
372
+ }));
373
+ return;
374
+ }
375
+
376
+ // Handle the request with the transport
377
+ await transport.handleRequest(req, res, body);
378
+ } catch (error) {
379
+ if (this.debug) {
380
+ console.error('[MCP] Error handling Streamable HTTP request:', error);
381
+ }
382
+
383
+ if (!res.headersSent) {
384
+ res.writeHead(500, { 'Content-Type': 'application/json' });
385
+ res.end(JSON.stringify({
386
+ jsonrpc: '2.0',
387
+ error: {
388
+ code: -32603,
389
+ message: 'Internal server error',
390
+ data: error.message
391
+ },
392
+ id: null
393
+ }));
394
+ }
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Parse request body as JSON
400
+ */
401
+ async parseRequestBody(req) {
402
+ return new Promise((resolve, reject) => {
403
+ let body = '';
404
+ req.on('data', chunk => {
405
+ body += chunk.toString();
406
+ });
407
+ req.on('end', () => {
408
+ try {
409
+ const parsed = body ? JSON.parse(body) : null;
410
+ resolve(parsed);
411
+ } catch (error) {
412
+ reject(error);
413
+ }
414
+ });
415
+ req.on('error', reject);
416
+ });
417
+ }
418
+
419
+ /**
420
+ * Handle Server-Sent Events connection (DEPRECATED - use handleSSEConnection instead)
125
421
  */
126
422
  handleSSE(req, res) {
127
423
  res.writeHead(200, {
@@ -430,6 +726,36 @@ export class BuiltInMCPServer extends EventEmitter {
430
726
  * Stop the server
431
727
  */
432
728
  async stop() {
729
+ // Close all Streamable HTTP transports
730
+ for (const [sessionId, transport] of this.streamableTransports.entries()) {
731
+ try {
732
+ await transport.close();
733
+ if (this.debug) {
734
+ console.log(`[MCP] Closed Streamable HTTP transport for session: ${sessionId}`);
735
+ }
736
+ } catch (error) {
737
+ if (this.debug) {
738
+ console.error(`[MCP] Error closing Streamable HTTP transport ${sessionId}:`, error);
739
+ }
740
+ }
741
+ }
742
+ this.streamableTransports.clear();
743
+
744
+ // Close all SSE transports
745
+ for (const [sessionId, transport] of this.sseTransports.entries()) {
746
+ try {
747
+ await transport.close();
748
+ if (this.debug) {
749
+ console.log(`[MCP] Closed SSE transport for session: ${sessionId}`);
750
+ }
751
+ } catch (error) {
752
+ if (this.debug) {
753
+ console.error(`[MCP] Error closing SSE transport ${sessionId}:`, error);
754
+ }
755
+ }
756
+ }
757
+ this.sseTransports.clear();
758
+
433
759
  // Close all SSE connections
434
760
  for (const connection of this.connections) {
435
761
  connection.end();
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Base Session class for AI provider engines
3
+ * Manages conversation state and message counting
4
+ */
5
+ export class Session {
6
+ constructor(id, debug = false) {
7
+ this.id = id;
8
+ this.conversationId = null; // Provider-specific conversation/thread ID for resumption
9
+ this.messageCount = 0;
10
+ this.debug = debug;
11
+ }
12
+
13
+ /**
14
+ * Set the conversation ID for session resumption
15
+ * @param {string} conversationId - Provider's conversation/thread ID
16
+ */
17
+ setConversationId(conversationId) {
18
+ this.conversationId = conversationId;
19
+ if (this.debug) {
20
+ console.log(`[Session ${this.id}] Conversation ID: ${conversationId}`);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Increment the message count
26
+ */
27
+ incrementMessageCount() {
28
+ this.messageCount++;
29
+ }
30
+
31
+ /**
32
+ * Get session info as plain object
33
+ * @returns {Object} Session information
34
+ */
35
+ getInfo() {
36
+ return {
37
+ id: this.id,
38
+ conversationId: this.conversationId,
39
+ messageCount: this.messageCount
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Get resume arguments for CLI commands (used by Claude Code)
45
+ * @returns {Array<string>} CLI arguments for resuming conversation
46
+ */
47
+ getResumeArgs() {
48
+ if (this.conversationId && this.messageCount > 0) {
49
+ return ['--resume', this.conversationId];
50
+ }
51
+ return [];
52
+ }
53
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Shared predefined prompts for all AI providers
3
+ */
4
+
5
+ export const predefinedPrompts = {
6
+ 'code-explorer': `You are ProbeChat Code Explorer - an AI assistant focused on reading and explaining code using Probe's semantic search capabilities.
7
+
8
+ Your primary role is to explore codebases, understand code structure, and provide clear explanations. You should:
9
+
10
+ 1. Use the 'search' tool to find relevant code snippets across the codebase
11
+ 2. Use the 'query' tool to locate specific symbols, functions, or classes
12
+ 3. Use the 'extract' tool to get full context for files or specific code blocks
13
+ 4. Explain code behavior, architecture patterns, and relationships between components
14
+ 5. Provide concise summaries with key insights highlighted
15
+
16
+ When exploring code:
17
+ - Start with targeted searches to find relevant areas
18
+ - Extract full context when you need to understand implementation details
19
+ - Trace function calls and data flow to understand how components interact
20
+ - Focus on "why" and "how" rather than just describing what the code does
21
+
22
+ You should NOT:
23
+ - Make changes to the codebase (read-only access)
24
+ - Execute or run code
25
+ - Make assumptions without verifying through search/query/extract`,
26
+
27
+ 'architect': `You are ProbeChat Architect - a senior software architect specialized in analyzing and designing software systems.
28
+
29
+ Your role is to:
30
+
31
+ 1. Analyze existing codebases to understand architecture patterns and design decisions
32
+ 2. Identify architectural issues, technical debt, and improvement opportunities
33
+ 3. Propose refactoring strategies and architectural changes
34
+ 4. Design new features that fit well with existing architecture
35
+ 5. Create high-level architecture diagrams and documentation
36
+
37
+ When analyzing architecture:
38
+ - Use search/query/extract to understand component boundaries and dependencies
39
+ - Identify layers, modules, and their responsibilities
40
+ - Look for patterns like MVC, microservices, event-driven, etc.
41
+ - Assess coupling, cohesion, and separation of concerns
42
+ - Consider scalability, maintainability, and testability
43
+
44
+ When proposing changes:
45
+ - Provide clear rationale backed by architectural principles
46
+ - Consider migration paths and backwards compatibility
47
+ - Identify risks and tradeoffs
48
+ - Suggest incremental implementation steps`,
49
+
50
+ 'code-review': `You are ProbeChat Code Reviewer - a meticulous code reviewer focused on quality, best practices, and maintainability.
51
+
52
+ Your role is to:
53
+
54
+ 1. Review code changes for correctness, clarity, and best practices
55
+ 2. Identify bugs, security issues, and performance problems
56
+ 3. Suggest improvements for readability and maintainability
57
+ 4. Ensure consistency with project conventions and patterns
58
+ 5. Verify test coverage and edge case handling
59
+
60
+ When reviewing code:
61
+ - Use search to find similar patterns in the codebase for consistency
62
+ - Look for common issues: null checks, error handling, resource leaks
63
+ - Check for security vulnerabilities: injection, XSS, etc.
64
+ - Assess complexity and suggest simplifications
65
+ - Verify naming conventions and code organization
66
+
67
+ Provide constructive feedback:
68
+ - Start with what's good about the code
69
+ - Be specific about issues with examples
70
+ - Suggest concrete improvements with code snippets
71
+ - Prioritize critical issues over style preferences
72
+ - Explain the "why" behind your suggestions`,
73
+
74
+ 'engineer': `You are a senior engineer who helps implement features and fix bugs.
75
+
76
+ Your role is to:
77
+
78
+ 1. Implement new features following project conventions
79
+ 2. Fix bugs by understanding root causes
80
+ 3. Refactor code to improve quality
81
+ 4. Write clear, maintainable code
82
+ 5. Add appropriate tests and documentation
83
+
84
+ When implementing:
85
+ - Use search/query to understand existing patterns
86
+ - Follow the project's coding style and conventions
87
+ - Consider edge cases and error handling
88
+ - Write self-documenting code with clear names
89
+ - Add comments for complex logic
90
+
91
+ You have access to:
92
+ - search: Find relevant code patterns
93
+ - query: Locate specific symbols/functions
94
+ - extract: Get full file context
95
+ - implement: Create or modify files (use carefully)
96
+ - delegate: Break down complex tasks`,
97
+
98
+ 'support': `You are ProbeChat Support - a helpful assistant focused on answering questions about codebases.
99
+
100
+ Your role is to:
101
+
102
+ 1. Answer questions about how code works
103
+ 2. Help users locate specific functionality
104
+ 3. Explain error messages and debugging approaches
105
+ 4. Guide users to relevant documentation
106
+ 5. Provide examples and usage patterns
107
+
108
+ When helping users:
109
+ - Ask clarifying questions if the request is ambiguous
110
+ - Use search/query to find relevant code quickly
111
+ - Provide clear, step-by-step explanations
112
+ - Include code examples when helpful
113
+ - Point to relevant files and line numbers
114
+
115
+ You should be:
116
+ - Patient and encouraging
117
+ - Clear and concise
118
+ - Thorough but not overwhelming
119
+ - Honest about limitations (say "I don't know" when appropriate)`
120
+ };
121
+
122
+ /**
123
+ * Get a predefined prompt by type
124
+ * @param {string} type - The prompt type
125
+ * @returns {string|null} The prompt text or null if not found
126
+ */
127
+ export function getPredefinedPrompt(type) {
128
+ return predefinedPrompts[type] || null;
129
+ }
@@ -31,21 +31,39 @@ import { processXmlWithThinkingAndRecovery } from './xmlParsingUtils.js';
31
31
 
32
32
  // Create configured tool instances
33
33
  export function createTools(configOptions) {
34
- const tools = {
35
- searchTool: searchTool(configOptions),
36
- queryTool: queryTool(configOptions),
37
- extractTool: extractTool(configOptions),
38
- delegateTool: delegateTool(configOptions)
39
- };
34
+ const tools = {};
35
+
36
+ const isToolAllowed =
37
+ configOptions.isToolAllowed ||
38
+ ((toolName) => {
39
+ if (!configOptions.allowedTools) return true;
40
+ return configOptions.allowedTools.isEnabled(toolName);
41
+ });
42
+
43
+ // Core tools
44
+ if (isToolAllowed('search')) {
45
+ tools.searchTool = searchTool(configOptions);
46
+ }
47
+ if (isToolAllowed('query')) {
48
+ tools.queryTool = queryTool(configOptions);
49
+ }
50
+ if (isToolAllowed('extract')) {
51
+ tools.extractTool = extractTool(configOptions);
52
+ }
53
+ if (configOptions.enableDelegate && isToolAllowed('delegate')) {
54
+ tools.delegateTool = delegateTool(configOptions);
55
+ }
40
56
 
41
57
  // Add bash tool if enabled
42
- if (configOptions.enableBash) {
58
+ if (configOptions.enableBash && isToolAllowed('bash')) {
43
59
  tools.bashTool = bashTool(configOptions);
44
60
  }
45
61
 
46
62
  // Add edit and create tools if enabled
47
- if (configOptions.allowEdit) {
63
+ if (configOptions.allowEdit && isToolAllowed('edit')) {
48
64
  tools.editTool = editTool(configOptions);
65
+ }
66
+ if (configOptions.allowEdit && isToolAllowed('create')) {
49
67
  tools.createTool = createTool(configOptions);
50
68
  }
51
69
 
@@ -199,4 +217,3 @@ export function parseXmlToolCallWithThinking(xmlString, validTools) {
199
217
  // Otherwise, use the original parseXmlToolCall function to parse the cleaned XML string
200
218
  return parseXmlToolCall(cleanedXmlString, validTools);
201
219
  }
202
-