agent-planner-mcp 0.2.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.
@@ -0,0 +1,540 @@
1
+ /**
2
+ * Client for interacting with the Planning System API
3
+ */
4
+ const axios = require('axios');
5
+ require('dotenv').config();
6
+
7
+ // Get token from environment
8
+ const userApiToken = process.env.USER_API_TOKEN || process.env.API_TOKEN; // Support both new and old env var names
9
+
10
+ // Determine proper authentication scheme
11
+ // If token looks like a JWT (has two dots), use Bearer scheme, otherwise use ApiKey
12
+ const getAuthScheme = (token) => {
13
+ if (!token) return null;
14
+ // Simple check if it's a JWT (contains two dots for header.payload.signature)
15
+ return token.split('.').length === 3 ? 'Bearer' : 'ApiKey';
16
+ };
17
+
18
+ const authScheme = getAuthScheme(userApiToken);
19
+
20
+ // Create API client instance
21
+ const apiClient = axios.create({
22
+ baseURL: process.env.API_URL || 'http://localhost:3000',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ 'Authorization': userApiToken ? `${authScheme} ${userApiToken}` : undefined
26
+ }
27
+ });
28
+
29
+ // Log API requests in debug mode
30
+ apiClient.interceptors.request.use(request => {
31
+ console.error(`API Request: ${request.method.toUpperCase()} ${request.url}`);
32
+ return request;
33
+ });
34
+
35
+ // Log API responses in debug mode
36
+ apiClient.interceptors.response.use(
37
+ response => {
38
+ console.error(`API Response: ${response.status} ${response.statusText}`);
39
+ return response;
40
+ },
41
+ error => {
42
+ if (error.response && error.response.status === 401) {
43
+ console.error('API Error: Authentication failed (401). Please check that your USER_API_TOKEN is correct, valid, and not revoked.');
44
+ console.error('If you are still using the old API_TOKEN, please generate a USER_API_TOKEN from the agent-planner UI.');
45
+ } else {
46
+ console.error('API Error:', error.response ? error.response.data : error.message);
47
+ }
48
+ return Promise.reject(error);
49
+ }
50
+ );
51
+
52
+ /**
53
+ * Plan-related API functions
54
+ */
55
+ const plans = {
56
+ /**
57
+ * Get a list of plans accessible to the current user
58
+ * @returns {Promise<Array>} - List of plans
59
+ */
60
+ getPlans: async () => {
61
+ const response = await apiClient.get('/plans');
62
+ return response.data;
63
+ },
64
+
65
+ /**
66
+ * Get a specific plan by ID
67
+ * @param {string} planId - Plan ID
68
+ * @returns {Promise<Object>} - Plan details
69
+ */
70
+ getPlan: async (planId) => {
71
+ const response = await apiClient.get(`/plans/${planId}`);
72
+ return response.data;
73
+ },
74
+
75
+ /**
76
+ * Create a new plan
77
+ * @param {Object} planData - Plan data (title, description, status)
78
+ * @returns {Promise<Object>} - Created plan
79
+ */
80
+ createPlan: async (planData) => {
81
+ const response = await apiClient.post('/plans', planData);
82
+ return response.data;
83
+ },
84
+
85
+ /**
86
+ * Update a plan
87
+ * @param {string} planId - Plan ID
88
+ * @param {Object} planData - Updated plan data
89
+ * @returns {Promise<Object>} - Updated plan
90
+ */
91
+ updatePlan: async (planId, planData) => {
92
+ const response = await apiClient.put(`/plans/${planId}`, planData);
93
+ return response.data;
94
+ },
95
+
96
+ /**
97
+ * Delete a plan
98
+ * @param {string} planId - Plan ID
99
+ * @returns {Promise<void>}
100
+ */
101
+ deletePlan: async (planId) => {
102
+ await apiClient.delete(`/plans/${planId}`);
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Node-related API functions
108
+ */
109
+ const nodes = {
110
+ /**
111
+ * Get nodes for a plan
112
+ * @param {string} planId - Plan ID
113
+ * @returns {Promise<Array>} - List of nodes
114
+ */
115
+ getNodes: async (planId) => {
116
+ const response = await apiClient.get(`/plans/${planId}/nodes`);
117
+ return response.data;
118
+ },
119
+
120
+ /**
121
+ * Get a specific node
122
+ * @param {string} planId - Plan ID
123
+ * @param {string} nodeId - Node ID
124
+ * @returns {Promise<Object>} - Node details
125
+ */
126
+ getNode: async (planId, nodeId) => {
127
+ const response = await apiClient.get(`/plans/${planId}/nodes/${nodeId}`);
128
+ return response.data;
129
+ },
130
+
131
+ /**
132
+ * Create a new node
133
+ * @param {string} planId - Plan ID
134
+ * @param {Object} nodeData - Node data
135
+ * @returns {Promise<Object>} - Created node
136
+ */
137
+ createNode: async (planId, nodeData) => {
138
+ const response = await apiClient.post(`/plans/${planId}/nodes`, nodeData);
139
+ return response.data;
140
+ },
141
+
142
+ /**
143
+ * Update a node
144
+ * @param {string} planId - Plan ID
145
+ * @param {string} nodeId - Node ID
146
+ * @param {Object} nodeData - Updated node data
147
+ * @returns {Promise<Object>} - Updated node
148
+ */
149
+ updateNode: async (planId, nodeId, nodeData) => {
150
+ const response = await apiClient.put(`/plans/${planId}/nodes/${nodeId}`, nodeData);
151
+ return response.data;
152
+ },
153
+
154
+ /**
155
+ * Update node status
156
+ * @param {string} planId - Plan ID
157
+ * @param {string} nodeId - Node ID
158
+ * @param {string} status - New status
159
+ * @returns {Promise<Object>} - Updated node
160
+ */
161
+ updateNodeStatus: async (planId, nodeId, status) => {
162
+ const response = await apiClient.put(`/plans/${planId}/nodes/${nodeId}/status`, { status });
163
+ return response.data;
164
+ },
165
+
166
+ /**
167
+ * Delete a node
168
+ * @param {string} planId - Plan ID
169
+ * @param {string} nodeId - Node ID
170
+ * @returns {Promise<void>}
171
+ */
172
+ deleteNode: async (planId, nodeId) => {
173
+ await apiClient.delete(`/plans/${planId}/nodes/${nodeId}`);
174
+ }
175
+ };
176
+
177
+ /**
178
+ * Comment-related API functions
179
+ */
180
+ const comments = {
181
+ /**
182
+ * Get comments for a node
183
+ * @param {string} planId - Plan ID
184
+ * @param {string} nodeId - Node ID
185
+ * @returns {Promise<Array>} - List of comments
186
+ */
187
+ getComments: async (planId, nodeId) => {
188
+ const response = await apiClient.get(`/plans/${planId}/nodes/${nodeId}/comments`);
189
+ return response.data;
190
+ },
191
+
192
+ /**
193
+ * Add a comment to a node
194
+ * @param {string} planId - Plan ID
195
+ * @param {string} nodeId - Node ID
196
+ * @param {Object} commentData - Comment data
197
+ * @returns {Promise<Object>} - Created comment
198
+ */
199
+ addComment: async (planId, nodeId, commentData) => {
200
+ const response = await apiClient.post(`/plans/${planId}/nodes/${nodeId}/comments`, commentData);
201
+ return response.data;
202
+ }
203
+ };
204
+
205
+ /**
206
+ * Log-related API functions
207
+ */
208
+ const logs = {
209
+ /**
210
+ * Get logs for a node
211
+ * @param {string} planId - Plan ID
212
+ * @param {string} nodeId - Node ID
213
+ * @returns {Promise<Array>} - List of logs
214
+ */
215
+ getLogs: async (planId, nodeId) => {
216
+ const response = await apiClient.get(`/plans/${planId}/nodes/${nodeId}/logs`);
217
+ return response.data;
218
+ },
219
+
220
+ /**
221
+ * Add a log entry to a node
222
+ * @param {string} planId - Plan ID
223
+ * @param {string} nodeId - Node ID
224
+ * @param {Object} logData - Log data
225
+ * @returns {Promise<Object>} - Created log entry
226
+ */
227
+ addLogEntry: async (planId, nodeId, logData) => {
228
+ const response = await apiClient.post(`/plans/${planId}/nodes/${nodeId}/log`, logData);
229
+ return response.data;
230
+ }
231
+ };
232
+
233
+ /**
234
+ * Artifact-related API functions
235
+ */
236
+ const artifacts = {
237
+ /**
238
+ * Get artifacts for a node
239
+ * @param {string} planId - Plan ID
240
+ * @param {string} nodeId - Node ID
241
+ * @returns {Promise<Array>} - List of artifacts
242
+ */
243
+ getArtifacts: async (planId, nodeId) => {
244
+ const response = await apiClient.get(`/plans/${planId}/nodes/${nodeId}/artifacts`);
245
+ return response.data;
246
+ },
247
+
248
+ /**
249
+ * Get a specific artifact by ID
250
+ * @param {string} planId - Plan ID
251
+ * @param {string} nodeId - Node ID
252
+ * @param {string} artifactId - Artifact ID
253
+ * @returns {Promise<Object>} - Artifact details
254
+ */
255
+ getArtifact: async (planId, nodeId, artifactId) => {
256
+ const response = await apiClient.get(`/plans/${planId}/nodes/${nodeId}/artifacts/${artifactId}`);
257
+ return response.data;
258
+ },
259
+
260
+ /**
261
+ * Get the content of an artifact
262
+ * @param {string} planId - Plan ID
263
+ * @param {string} nodeId - Node ID
264
+ * @param {string} artifactId - Artifact ID
265
+ * @returns {Promise<string>} - Artifact content
266
+ */
267
+ getArtifactContent: async (planId, nodeId, artifactId) => {
268
+ try {
269
+ // First, get artifact details to check the URL
270
+ const artifact = await artifacts.getArtifact(planId, nodeId, artifactId);
271
+
272
+ // If the artifact has a URL, fetch the content
273
+ if (artifact.url) {
274
+ try {
275
+ // For local file paths, use fs instead of HTTP request
276
+ if (artifact.url.startsWith('/') && !artifact.url.startsWith('/api/')) {
277
+ const fs = require('fs').promises;
278
+ try {
279
+ // Read the file directly from the filesystem
280
+ const content = await fs.readFile(artifact.url, 'utf8');
281
+ return content;
282
+ } catch (fsError) {
283
+ console.error('Error reading artifact file:', fsError);
284
+ throw new Error(`Cannot read file at ${artifact.url}: ${fsError.message}`);
285
+ }
286
+ } else {
287
+ // For internal URLs (API routes), append to base URL
288
+ const contentUrl = artifact.url.startsWith('/api/')
289
+ ? `${apiClient.defaults.baseURL}${artifact.url}`
290
+ : artifact.url;
291
+
292
+ const contentResponse = await axios.get(contentUrl, {
293
+ headers: {
294
+ 'Authorization': apiClient.defaults.headers['Authorization'],
295
+ 'Accept': artifact.content_type || 'text/plain'
296
+ },
297
+ responseType: 'text'
298
+ });
299
+
300
+ return contentResponse.data;
301
+ }
302
+ } catch (fetchError) {
303
+ console.error('Error fetching artifact content:', fetchError);
304
+ throw new Error(`Failed to fetch artifact content: ${fetchError.message}`);
305
+ }
306
+ } else {
307
+ throw new Error('Artifact does not have a content URL');
308
+ }
309
+ } catch (error) {
310
+ console.error('Error fetching artifact content:', error);
311
+ throw error;
312
+ }
313
+ },
314
+
315
+ /**
316
+ * Add an artifact to a node
317
+ * @param {string} planId - Plan ID
318
+ * @param {string} nodeId - Node ID
319
+ * @param {Object} artifactData - Artifact data
320
+ * @returns {Promise<Object>} - Created artifact
321
+ */
322
+ addArtifact: async (planId, nodeId, artifactData) => {
323
+ const response = await apiClient.post(`/plans/${planId}/nodes/${nodeId}/artifacts`, artifactData);
324
+ return response.data;
325
+ }
326
+ };
327
+
328
+ /**
329
+ * Activity-related API functions
330
+ */
331
+ const activity = {
332
+ /**
333
+ * Get activity feed for a plan
334
+ * @param {string} planId - Plan ID
335
+ * @returns {Promise<Array>} - Activity feed
336
+ */
337
+ getPlanActivity: async (planId) => {
338
+ const response = await apiClient.get(`/activity/plan/${planId}`);
339
+ return response.data;
340
+ },
341
+
342
+ /**
343
+ * Get global activity feed
344
+ * @returns {Promise<Array>} - Activity feed
345
+ */
346
+ getGlobalActivity: async () => {
347
+ const response = await apiClient.get('/activity');
348
+ return response.data;
349
+ }
350
+ };
351
+
352
+ /**
353
+ * Search-related API functions
354
+ */
355
+ const search = {
356
+ /**
357
+ * Search within a plan
358
+ * @param {string} planId - Plan ID
359
+ * @param {string} query - Search query
360
+ * @returns {Promise<Object>} - Search results
361
+ */
362
+ searchPlan: async (planId, query) => {
363
+ if (process.env.NODE_ENV === 'development') {
364
+ console.log(`Searching plan ${planId} for "${query}"`);
365
+ }
366
+
367
+ try {
368
+ // Try the documented endpoint first
369
+ const response = await apiClient.get(`/search/plan/${planId}`, {
370
+ params: { query: query } // API expects 'query' parameter
371
+ });
372
+
373
+ if (process.env.NODE_ENV === 'development') {
374
+ console.log('Search response status:', response.status);
375
+ console.log('Search response type:', typeof response.data);
376
+ }
377
+
378
+ return response.data;
379
+ } catch (error) {
380
+ // Try alternative endpoint format
381
+ if (error.response && error.response.status === 404) {
382
+ try {
383
+ const altResponse = await apiClient.get(`/plans/${planId}/search`, {
384
+ params: { query: query }
385
+ });
386
+ return altResponse.data;
387
+ } catch (altError) {
388
+ // Fallback to client-side search
389
+ console.error('Search endpoints not found, falling back to client-side search');
390
+
391
+ // Get all nodes and search client-side
392
+ try {
393
+ const nodes = await apiClient.get(`/plans/${planId}/nodes`);
394
+ const results = [];
395
+
396
+ const searchLower = query.toLowerCase();
397
+ const searchNodes = (nodeList) => {
398
+ nodeList.forEach(node => {
399
+ if (node.title?.toLowerCase().includes(searchLower) ||
400
+ node.description?.toLowerCase().includes(searchLower) ||
401
+ node.context?.toLowerCase().includes(searchLower)) {
402
+ results.push({
403
+ id: node.id,
404
+ type: 'node',
405
+ title: node.title,
406
+ content: node.description || node.context || '',
407
+ created_at: node.created_at,
408
+ user_id: node.created_by
409
+ });
410
+ }
411
+ if (node.children && node.children.length > 0) {
412
+ searchNodes(node.children);
413
+ }
414
+ });
415
+ };
416
+
417
+ searchNodes(nodes.data);
418
+
419
+ return {
420
+ query,
421
+ results,
422
+ count: results.length
423
+ };
424
+ } catch (fallbackError) {
425
+ console.error('Fallback search failed:', fallbackError.message);
426
+ }
427
+ }
428
+ }
429
+
430
+ console.error('Error searching plan:', error.message);
431
+ if (error.response) {
432
+ console.error('Response status:', error.response.status);
433
+ }
434
+
435
+ // Return empty results on error
436
+ return { results: [], count: 0, query };
437
+ }
438
+ },
439
+
440
+ /**
441
+ * Global search across all plans
442
+ * @param {string} query - Search query
443
+ * @returns {Promise<Object>} - Search results
444
+ */
445
+ globalSearch: async (query) => {
446
+ try {
447
+ const response = await apiClient.get('/search', {
448
+ params: { query: query } // API expects 'query' parameter
449
+ });
450
+ return response.data;
451
+ } catch (error) {
452
+ console.error('Global search error:', error.message);
453
+
454
+ // Fallback: search through all accessible plans
455
+ if (error.response && (error.response.status === 404 || error.response.status === 500)) {
456
+ try {
457
+ const plansResponse = await apiClient.get('/plans');
458
+ const plans = plansResponse.data;
459
+ const results = [];
460
+
461
+ const searchLower = query.toLowerCase();
462
+
463
+ // Search in plans
464
+ plans.forEach(plan => {
465
+ if (plan.title?.toLowerCase().includes(searchLower) ||
466
+ plan.description?.toLowerCase().includes(searchLower)) {
467
+ results.push({
468
+ id: plan.id,
469
+ type: 'plan',
470
+ title: plan.title,
471
+ content: plan.description || '',
472
+ created_at: plan.created_at
473
+ });
474
+ }
475
+ });
476
+
477
+ return {
478
+ query,
479
+ results,
480
+ count: results.length
481
+ };
482
+ } catch (fallbackError) {
483
+ console.error('Fallback global search failed:', fallbackError.message);
484
+ }
485
+ }
486
+
487
+ // Return empty results on error
488
+ return { results: [], count: 0, query };
489
+ }
490
+ }
491
+ };
492
+
493
+ /**
494
+ * API Token functions
495
+ */
496
+ const tokens = {
497
+ /**
498
+ * Get all API tokens
499
+ * @returns {Promise<Array>} - List of API tokens
500
+ */
501
+ getTokens: async () => {
502
+ const response = await apiClient.get('/tokens');
503
+ return response.data;
504
+ },
505
+
506
+ /**
507
+ * Create a new API token
508
+ * @param {Object} tokenData - Token data
509
+ * @returns {Promise<Object>} - Created token
510
+ */
511
+ createToken: async (tokenData) => {
512
+ const response = await apiClient.post('/tokens', tokenData);
513
+ return response.data;
514
+ },
515
+
516
+ /**
517
+ * Revoke an API token
518
+ * @param {string} tokenId - Token ID
519
+ * @returns {Promise<void>}
520
+ */
521
+ revokeToken: async (tokenId) => {
522
+ await apiClient.delete(`/tokens/${tokenId}`);
523
+ }
524
+ };
525
+
526
+ // Export API client functions
527
+ // Export the axios instance for direct use
528
+ const axiosInstance = apiClient;
529
+
530
+ module.exports = {
531
+ plans,
532
+ nodes,
533
+ comments,
534
+ logs,
535
+ artifacts,
536
+ activity,
537
+ search,
538
+ tokens,
539
+ axiosInstance // Export for direct API calls
540
+ };
package/src/index.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
3
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
4
+ const { setupTools } = require('./tools');
5
+ require('dotenv').config();
6
+
7
+ /**
8
+ * Initialize the Planning System MCP Server
9
+ *
10
+ * Features:
11
+ * - Simplified architecture with tools-only interface
12
+ * - Full CRUD operations on all entities
13
+ * - Unified search across all scopes
14
+ * - Batch operations for efficiency
15
+ * - Structured JSON responses
16
+ * - Comprehensive logging system
17
+ */
18
+ async function main() {
19
+ const isDev = process.env.NODE_ENV === 'development';
20
+
21
+ if (isDev) {
22
+ console.error('Initializing Planning System MCP Server...');
23
+ }
24
+
25
+ try {
26
+ // Log environment settings
27
+ console.error(`API URL: ${process.env.API_URL || 'http://localhost:3000'}`);
28
+
29
+ // Check for token
30
+ const userApiToken = process.env.USER_API_TOKEN || process.env.API_TOKEN;
31
+ console.error(`User API Token: ${userApiToken ? '***' + userApiToken.slice(-4) : 'NOT SET'}`);
32
+ console.error(`MCP Server Name: ${process.env.MCP_SERVER_NAME || 'planning-system-mcp'}`);
33
+ console.error(`MCP Server Version: ${process.env.MCP_SERVER_VERSION || '0.2.0'}`);
34
+
35
+ // Validate required environment variables
36
+ if (!userApiToken) {
37
+ throw new Error('USER_API_TOKEN environment variable is required. Please generate one from the Agent Planner UI and set it in .env file.');
38
+ }
39
+
40
+ // Create MCP server instance
41
+ const server = new Server({
42
+ name: process.env.MCP_SERVER_NAME || "planning-system-mcp",
43
+ version: process.env.MCP_SERVER_VERSION || "0.2.0"
44
+ }, {
45
+ capabilities: {
46
+ tools: {}
47
+ }
48
+ });
49
+
50
+ console.error('MCP Server created');
51
+
52
+ // Setup tools
53
+ setupTools(server);
54
+
55
+ // Connect transport
56
+ const transport = new StdioServerTransport();
57
+ await server.connect(transport);
58
+
59
+ console.error('MCP Server running on stdio transport');
60
+ console.error('Ready to accept connections from agents');
61
+ } catch (error) {
62
+ console.error('Failed to initialize MCP server:', error);
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ // Run the server
68
+ main();
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Search integration for the MCP system
3
+ *
4
+ * This module integrates the search wrapper with the MCP system,
5
+ * providing functions that can be called from other parts of the application.
6
+ */
7
+ const { searchPlan, globalSearch } = require('../tools/search-wrapper');
8
+
9
+ /**
10
+ * Perform a search within a plan and process the results
11
+ *
12
+ * @param {string} planId - ID of the plan to search
13
+ * @param {string} query - Search query
14
+ * @param {Function} [processResult] - Optional callback for each result
15
+ * @returns {Promise<Array>} - Array of processed search results
16
+ */
17
+ async function searchPlanAndProcess(planId, query, processResult = null) {
18
+ try {
19
+ console.log(`Searching plan ${planId} for: "${query}"`);
20
+
21
+ // Get results using the wrapper
22
+ const results = await searchPlan(planId, query);
23
+
24
+ // If a process function is provided, map over the results
25
+ if (typeof processResult === 'function') {
26
+ return results.map(processResult);
27
+ }
28
+
29
+ return results;
30
+ } catch (error) {
31
+ console.error('Error in searchPlanAndProcess:', error.message);
32
+ return [];
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Search for content within a plan and extract relevant information
38
+ *
39
+ * @param {string} planId - ID of the plan to search
40
+ * @param {string} query - Search query
41
+ * @returns {Promise<Object>} - Organized search results
42
+ */
43
+ async function findContentInPlan(planId, query) {
44
+ try {
45
+ const results = await searchPlan(planId, query);
46
+
47
+ // Organize results by type
48
+ const organizedResults = {
49
+ nodes: results.filter(r => r.type === 'node'),
50
+ comments: results.filter(r => r.type === 'comment'),
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))
54
+ };
55
+
56
+ // Add summary information
57
+ return {
58
+ query,
59
+ planId,
60
+ resultCount: results.length,
61
+ typeBreakdown: {
62
+ nodes: organizedResults.nodes.length,
63
+ comments: organizedResults.comments.length,
64
+ logs: organizedResults.logs.length,
65
+ artifacts: organizedResults.artifacts.length,
66
+ other: organizedResults.other.length
67
+ },
68
+ results: organizedResults
69
+ };
70
+ } catch (error) {
71
+ console.error('Error in findContentInPlan:', error.message);
72
+ return {
73
+ query,
74
+ planId,
75
+ resultCount: 0,
76
+ typeBreakdown: { nodes: 0, comments: 0, logs: 0, artifacts: 0, other: 0 },
77
+ results: { nodes: [], comments: [], logs: [], artifacts: [], other: [] }
78
+ };
79
+ }
80
+ }
81
+
82
+ module.exports = {
83
+ searchPlanAndProcess,
84
+ findContentInPlan,
85
+ // Also export the original wrapper functions
86
+ searchPlan,
87
+ globalSearch
88
+ };