agent-planner-mcp 0.3.1 → 0.5.0

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.
@@ -49,8 +49,7 @@ async function findContentInPlan(planId, query) {
49
49
  nodes: results.filter(r => r.type === 'node'),
50
50
  comments: results.filter(r => r.type === 'comment'),
51
51
  logs: results.filter(r => r.type === 'log'),
52
- artifacts: results.filter(r => r.type === 'artifact'),
53
- other: results.filter(r => !['node', 'comment', 'log', 'artifact'].includes(r.type))
52
+ other: results.filter(r => !['node', 'comment', 'log'].includes(r.type))
54
53
  };
55
54
 
56
55
  // Add summary information
@@ -62,7 +61,6 @@ async function findContentInPlan(planId, query) {
62
61
  nodes: organizedResults.nodes.length,
63
62
  comments: organizedResults.comments.length,
64
63
  logs: organizedResults.logs.length,
65
- artifacts: organizedResults.artifacts.length,
66
64
  other: organizedResults.other.length
67
65
  },
68
66
  results: organizedResults
@@ -73,8 +71,8 @@ async function findContentInPlan(planId, query) {
73
71
  query,
74
72
  planId,
75
73
  resultCount: 0,
76
- typeBreakdown: { nodes: 0, comments: 0, logs: 0, artifacts: 0, other: 0 },
77
- results: { nodes: [], comments: [], logs: [], artifacts: [], other: [] }
74
+ typeBreakdown: { nodes: 0, comments: 0, logs: 0, other: 0 },
75
+ results: { nodes: [], comments: [], logs: [], other: [] }
78
76
  };
79
77
  }
80
78
  }
@@ -0,0 +1,510 @@
1
+ /**
2
+ * MCP HTTP/SSE Server
3
+ *
4
+ * Implements the MCP Streamable HTTP transport specification (2025-06-18)
5
+ * https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
6
+ *
7
+ * Features:
8
+ * - Single endpoint supporting POST and GET methods
9
+ * - Session management via Mcp-Session-Id header
10
+ * - Server-Sent Events (SSE) for streaming responses
11
+ * - JSON-RPC 2.0 protocol
12
+ * - Origin validation for security
13
+ */
14
+
15
+ const express = require('express');
16
+ const { SessionManager } = require('./session-manager');
17
+ const { setupTools } = require('./tools');
18
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
19
+ require('dotenv').config();
20
+
21
+ // MCP Protocol Version
22
+ const MCP_PROTOCOL_VERSION = '2025-03-26';
23
+
24
+ class MCPHTTPServer {
25
+ constructor(options = {}) {
26
+ this.port = options.port || process.env.PORT || 3100;
27
+ this.host = options.host || process.env.HOST || '127.0.0.1';
28
+
29
+ // Session manager
30
+ this.sessionManager = new SessionManager({
31
+ sessionTimeout: options.sessionTimeout || 30 * 60 * 1000,
32
+ cleanupInterval: options.cleanupInterval || 5 * 60 * 1000
33
+ });
34
+
35
+ // Store for pending SSE streams per session
36
+ this.sseStreams = new Map(); // sessionId -> { res, req }
37
+
38
+ // Create Express app
39
+ this.app = express();
40
+
41
+ // Setup middleware and routes
42
+ this.setupMiddleware();
43
+ this.setupRoutes();
44
+
45
+ console.error('MCPHTTPServer initialized');
46
+ }
47
+
48
+ /**
49
+ * Setup Express middleware
50
+ */
51
+ setupMiddleware() {
52
+ // Parse JSON bodies
53
+ this.app.use(express.json());
54
+
55
+ // Logging middleware
56
+ this.app.use((req, res, next) => {
57
+ console.error(`${req.method} ${req.path} - ${req.get('MCP-Protocol-Version') || 'no version'}`);
58
+ next();
59
+ });
60
+
61
+ // Protocol version validation
62
+ this.app.use((req, res, next) => {
63
+ // Skip version check for health endpoint
64
+ if (req.path === '/health') {
65
+ return next();
66
+ }
67
+
68
+ const version = req.get('MCP-Protocol-Version');
69
+
70
+ // Backwards compatibility: assume 2025-03-26 if not provided
71
+ if (!version) {
72
+ req.mcpVersion = '2025-03-26';
73
+ return next();
74
+ }
75
+
76
+ req.mcpVersion = version;
77
+ next();
78
+ });
79
+
80
+ // Origin validation for security (DNS rebinding protection)
81
+ this.app.use((req, res, next) => {
82
+ // Skip origin check for health endpoint
83
+ if (req.path === '/health') {
84
+ return next();
85
+ }
86
+
87
+ const origin = req.get('Origin');
88
+
89
+ // If Origin header is present, validate it
90
+ if (origin) {
91
+ // For localhost binding, accept localhost origins
92
+ const allowedOrigins = [
93
+ 'http://localhost',
94
+ 'http://127.0.0.1',
95
+ `http://localhost:${this.port}`,
96
+ `http://127.0.0.1:${this.port}`
97
+ ];
98
+
99
+ const originUrl = new URL(origin);
100
+ const isAllowed = allowedOrigins.some(allowed => origin.startsWith(allowed));
101
+
102
+ if (!isAllowed) {
103
+ console.error(`Rejected request from origin: ${origin}`);
104
+ return res.status(403).json({
105
+ jsonrpc: '2.0',
106
+ error: {
107
+ code: -32000,
108
+ message: 'Forbidden origin'
109
+ }
110
+ });
111
+ }
112
+ }
113
+
114
+ next();
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Setup Express routes
120
+ */
121
+ setupRoutes() {
122
+ // Health check endpoint
123
+ this.app.get('/health', (req, res) => {
124
+ const stats = this.sessionManager.getStats();
125
+ res.json({
126
+ status: 'ok',
127
+ version: MCP_PROTOCOL_VERSION,
128
+ server: {
129
+ name: process.env.MCP_SERVER_NAME || 'planning-tools',
130
+ version: process.env.MCP_SERVER_VERSION || '0.3.1'
131
+ },
132
+ sessions: {
133
+ total: stats.total,
134
+ initialized: stats.initialized
135
+ }
136
+ });
137
+ });
138
+
139
+ // Main MCP endpoint - handles both POST and GET
140
+ this.app.post('/mcp', this.handleMCPPost.bind(this));
141
+ this.app.get('/mcp', this.handleMCPGet.bind(this));
142
+
143
+ // Session termination endpoint
144
+ this.app.delete('/mcp', this.handleMCPDelete.bind(this));
145
+
146
+ // 404 handler
147
+ this.app.use((req, res) => {
148
+ res.status(404).json({
149
+ jsonrpc: '2.0',
150
+ error: {
151
+ code: -32000,
152
+ message: 'Not found'
153
+ }
154
+ });
155
+ });
156
+
157
+ // Error handler
158
+ this.app.use((err, req, res, next) => {
159
+ console.error('Express error:', err);
160
+ res.status(500).json({
161
+ jsonrpc: '2.0',
162
+ error: {
163
+ code: -32603,
164
+ message: 'Internal server error',
165
+ data: err.message
166
+ }
167
+ });
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Handle POST requests (client-to-server messages)
173
+ */
174
+ async handleMCPPost(req, res) {
175
+ try {
176
+ // Get or create session
177
+ let sessionId = req.get('Mcp-Session-Id');
178
+ let session = sessionId ? this.sessionManager.getSession(sessionId) : null;
179
+
180
+ // Validate session exists if session ID provided
181
+ if (sessionId && !session) {
182
+ return res.status(404).json({
183
+ jsonrpc: '2.0',
184
+ error: {
185
+ code: -32000,
186
+ message: 'Session not found'
187
+ }
188
+ });
189
+ }
190
+
191
+ // Parse JSON-RPC message
192
+ const message = req.body;
193
+
194
+ if (!message || !message.jsonrpc || message.jsonrpc !== '2.0') {
195
+ return res.status(400).json({
196
+ jsonrpc: '2.0',
197
+ error: {
198
+ code: -32600,
199
+ message: 'Invalid JSON-RPC request'
200
+ }
201
+ });
202
+ }
203
+
204
+ // Handle different message types
205
+ const isRequest = message.method && message.id !== undefined;
206
+ const isNotification = message.method && message.id === undefined;
207
+ const isResponse = message.result !== undefined || message.error !== undefined;
208
+
209
+ if (isNotification || isResponse) {
210
+ // For notifications and responses, return 202 Accepted
211
+ return res.status(202).send();
212
+ }
213
+
214
+ if (isRequest) {
215
+ // Handle JSON-RPC request
216
+ const response = await this.handleRequest(message, session, sessionId);
217
+
218
+ // If this is an initialize request, create session and include session ID
219
+ if (message.method === 'initialize' && response.result) {
220
+ sessionId = this.sessionManager.createSession();
221
+ this.sessionManager.initializeSession(sessionId, message.params?.capabilities);
222
+
223
+ // Set session ID header in response
224
+ res.setHeader('Mcp-Session-Id', sessionId);
225
+
226
+ console.error(`Session initialized: ${sessionId}`);
227
+ }
228
+
229
+ // Check if we should stream the response via SSE
230
+ const acceptHeader = req.get('Accept') || '';
231
+ const supportsSSE = acceptHeader.includes('text/event-stream');
232
+
233
+ // For now, we'll send simple JSON responses
234
+ // SSE streaming can be added later for long-running operations
235
+ if (supportsSSE && this.shouldStreamResponse(message)) {
236
+ // Send SSE stream
237
+ return this.streamResponse(req, res, response, sessionId);
238
+ } else {
239
+ // Send simple JSON response
240
+ res.setHeader('Content-Type', 'application/json');
241
+ return res.json(response);
242
+ }
243
+ }
244
+
245
+ // Unknown message type
246
+ return res.status(400).json({
247
+ jsonrpc: '2.0',
248
+ error: {
249
+ code: -32600,
250
+ message: 'Invalid request'
251
+ }
252
+ });
253
+ } catch (error) {
254
+ console.error('Error handling POST request:', error);
255
+ return res.status(500).json({
256
+ jsonrpc: '2.0',
257
+ error: {
258
+ code: -32603,
259
+ message: 'Internal error',
260
+ data: error.message
261
+ }
262
+ });
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Handle GET requests (SSE streams for server-to-client messages)
268
+ */
269
+ handleMCPGet(req, res) {
270
+ try {
271
+ // Validate Accept header
272
+ const acceptHeader = req.get('Accept') || '';
273
+ if (!acceptHeader.includes('text/event-stream')) {
274
+ return res.status(405).json({
275
+ jsonrpc: '2.0',
276
+ error: {
277
+ code: -32000,
278
+ message: 'Method not allowed. GET requires Accept: text/event-stream'
279
+ }
280
+ });
281
+ }
282
+
283
+ // Get session
284
+ const sessionId = req.get('Mcp-Session-Id');
285
+ if (!sessionId) {
286
+ return res.status(400).json({
287
+ jsonrpc: '2.0',
288
+ error: {
289
+ code: -32000,
290
+ message: 'Mcp-Session-Id header required'
291
+ }
292
+ });
293
+ }
294
+
295
+ const session = this.sessionManager.getSession(sessionId);
296
+ if (!session) {
297
+ return res.status(404).json({
298
+ jsonrpc: '2.0',
299
+ error: {
300
+ code: -32000,
301
+ message: 'Session not found'
302
+ }
303
+ });
304
+ }
305
+
306
+ // Setup SSE stream
307
+ res.setHeader('Content-Type', 'text/event-stream');
308
+ res.setHeader('Cache-Control', 'no-cache');
309
+ res.setHeader('Connection', 'keep-alive');
310
+ res.setHeader('X-Accel-Buffering', 'no'); // Disable buffering in nginx
311
+
312
+ // Store SSE stream for this session
313
+ this.sseStreams.set(sessionId, { res, req });
314
+
315
+ console.error(`SSE stream opened for session: ${sessionId}`);
316
+
317
+ // Send initial comment to establish connection
318
+ res.write(': connected\n\n');
319
+
320
+ // Handle client disconnect
321
+ req.on('close', () => {
322
+ console.error(`SSE stream closed for session: ${sessionId}`);
323
+ this.sseStreams.delete(sessionId);
324
+ });
325
+ } catch (error) {
326
+ console.error('Error handling GET request:', error);
327
+ res.status(500).json({
328
+ jsonrpc: '2.0',
329
+ error: {
330
+ code: -32603,
331
+ message: 'Internal error',
332
+ data: error.message
333
+ }
334
+ });
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Handle DELETE requests (session termination)
340
+ */
341
+ handleMCPDelete(req, res) {
342
+ const sessionId = req.get('Mcp-Session-Id');
343
+
344
+ if (!sessionId) {
345
+ return res.status(400).json({
346
+ jsonrpc: '2.0',
347
+ error: {
348
+ code: -32000,
349
+ message: 'Mcp-Session-Id header required'
350
+ }
351
+ });
352
+ }
353
+
354
+ // Close any SSE streams for this session
355
+ const stream = this.sseStreams.get(sessionId);
356
+ if (stream) {
357
+ stream.res.end();
358
+ this.sseStreams.delete(sessionId);
359
+ }
360
+
361
+ // Delete session
362
+ const deleted = this.sessionManager.deleteSession(sessionId);
363
+
364
+ if (deleted) {
365
+ return res.status(204).send();
366
+ } else {
367
+ return res.status(404).json({
368
+ jsonrpc: '2.0',
369
+ error: {
370
+ code: -32000,
371
+ message: 'Session not found'
372
+ }
373
+ });
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Handle JSON-RPC request
379
+ */
380
+ async handleRequest(message, session, sessionId) {
381
+ // Create MCP server instance for this request
382
+ const mcpServer = new Server({
383
+ name: process.env.MCP_SERVER_NAME || 'planning-tools',
384
+ version: process.env.MCP_SERVER_VERSION || '0.3.1'
385
+ }, {
386
+ capabilities: {
387
+ tools: {}
388
+ }
389
+ });
390
+
391
+ // Setup tools on the server
392
+ setupTools(mcpServer);
393
+
394
+ // Process the request through MCP server
395
+ try {
396
+ // Get the appropriate request handler
397
+ const handlers = mcpServer._requestHandlers;
398
+ const handler = handlers.get(message.method);
399
+
400
+ if (!handler) {
401
+ return {
402
+ jsonrpc: '2.0',
403
+ id: message.id,
404
+ error: {
405
+ code: -32601,
406
+ message: `Method not found: ${message.method}`
407
+ }
408
+ };
409
+ }
410
+
411
+ // Call the handler with the full request format expected by SDK
412
+ const result = await handler(message);
413
+
414
+ return {
415
+ jsonrpc: '2.0',
416
+ id: message.id,
417
+ result
418
+ };
419
+ } catch (error) {
420
+ console.error(`Error handling method ${message.method}:`, error);
421
+
422
+ return {
423
+ jsonrpc: '2.0',
424
+ id: message.id,
425
+ error: {
426
+ code: -32603,
427
+ message: 'Internal error',
428
+ data: error.message
429
+ }
430
+ };
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Determine if response should be streamed via SSE
436
+ */
437
+ shouldStreamResponse(message) {
438
+ // For now, we don't need streaming for planning tools
439
+ // All operations are relatively quick
440
+ // This can be enabled later for long-running operations
441
+ return false;
442
+ }
443
+
444
+ /**
445
+ * Stream response via SSE
446
+ */
447
+ streamResponse(req, res, response, sessionId) {
448
+ res.setHeader('Content-Type', 'text/event-stream');
449
+ res.setHeader('Cache-Control', 'no-cache');
450
+ res.setHeader('Connection', 'keep-alive');
451
+
452
+ // Send the response as an SSE event
453
+ res.write(`data: ${JSON.stringify(response)}\n\n`);
454
+
455
+ // Close the stream
456
+ res.end();
457
+ }
458
+
459
+ /**
460
+ * Start the HTTP server
461
+ */
462
+ async start() {
463
+ return new Promise((resolve, reject) => {
464
+ try {
465
+ this.server = this.app.listen(this.port, this.host, () => {
466
+ console.error(`MCP HTTP Server listening on ${this.host}:${this.port}`);
467
+ console.error(`MCP endpoint: http://${this.host}:${this.port}/mcp`);
468
+ console.error(`Health check: http://${this.host}:${this.port}/health`);
469
+ console.error(`Protocol version: ${MCP_PROTOCOL_VERSION}`);
470
+ resolve();
471
+ });
472
+
473
+ this.server.on('error', (error) => {
474
+ console.error('Server error:', error);
475
+ reject(error);
476
+ });
477
+ } catch (error) {
478
+ reject(error);
479
+ }
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Stop the HTTP server
485
+ */
486
+ async stop() {
487
+ return new Promise((resolve) => {
488
+ // Close all SSE streams
489
+ for (const [sessionId, stream] of this.sseStreams.entries()) {
490
+ stream.res.end();
491
+ }
492
+ this.sseStreams.clear();
493
+
494
+ // Destroy session manager
495
+ this.sessionManager.destroy();
496
+
497
+ // Close HTTP server
498
+ if (this.server) {
499
+ this.server.close(() => {
500
+ console.error('MCP HTTP Server stopped');
501
+ resolve();
502
+ });
503
+ } else {
504
+ resolve();
505
+ }
506
+ });
507
+ }
508
+ }
509
+
510
+ module.exports = { MCPHTTPServer };