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,33 @@
1
+ /**
2
+ * Wrapper module for the search_plan functionality
3
+ *
4
+ * This wrapper extracts the results array from the API response format
5
+ * which is an object with { query, results, count } structure
6
+ */
7
+ const apiClient = require('./api-client');
8
+
9
+ /**
10
+ * Search a plan and return the results array directly
11
+ * @param {string} planId - The ID of the plan to search
12
+ * @param {string} query - The search query
13
+ * @returns {Promise<Array>} - Array of search results
14
+ */
15
+ async function searchPlan(planId, query) {
16
+ try {
17
+ // Call the API through the client
18
+ const response = await apiClient.search.searchPlan(planId, query);
19
+
20
+ // Extract just the results array
21
+ if (response && response.results && Array.isArray(response.results)) {
22
+ return response.results;
23
+ } else {
24
+ console.error('Unexpected search response format:', response);
25
+ return [];
26
+ }
27
+ } catch (error) {
28
+ console.error('Error in search_plan wrapper:', error.message);
29
+ return [];
30
+ }
31
+ }
32
+
33
+ module.exports = { searchPlan };
package/src/setup.js ADDED
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Agent Planner MCP - Automated Setup Wizard
5
+ *
6
+ * This script helps users configure the MCP server for Claude Desktop:
7
+ * 1. Checks API server availability
8
+ * 2. Guides token creation and validates it
9
+ * 3. Creates .env file
10
+ * 4. Detects and updates Claude Desktop config
11
+ * 5. Tests the connection
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const readline = require('readline');
17
+ const { exec } = require('child_process');
18
+ const axios = require('axios');
19
+
20
+ // Colors for terminal output
21
+ const colors = {
22
+ reset: '\x1b[0m',
23
+ bright: '\x1b[1m',
24
+ green: '\x1b[32m',
25
+ blue: '\x1b[34m',
26
+ yellow: '\x1b[33m',
27
+ red: '\x1b[31m',
28
+ cyan: '\x1b[36m',
29
+ };
30
+
31
+ function log(message, color = 'reset') {
32
+ console.log(`${colors[color]}${message}${colors.reset}`);
33
+ }
34
+
35
+ function logStep(step, total, message) {
36
+ log(`\nStep ${step}/${total}: ${message}`, 'bright');
37
+ }
38
+
39
+ function logSuccess(message) {
40
+ log(`✓ ${message}`, 'green');
41
+ }
42
+
43
+ function logError(message) {
44
+ log(`✗ ${message}`, 'red');
45
+ }
46
+
47
+ function logInfo(message) {
48
+ log(`ℹ ${message}`, 'cyan');
49
+ }
50
+
51
+ // Create readline interface
52
+ const rl = readline.createInterface({
53
+ input: process.stdin,
54
+ output: process.stdout
55
+ });
56
+
57
+ function question(query) {
58
+ return new Promise(resolve => rl.question(query, resolve));
59
+ }
60
+
61
+ // Detect Claude Desktop config location based on OS
62
+ function getClaudeConfigPath() {
63
+ const platform = process.platform;
64
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
65
+
66
+ if (platform === 'darwin') {
67
+ // macOS
68
+ return path.join(homeDir, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
69
+ } else if (platform === 'win32') {
70
+ // Windows
71
+ return path.join(homeDir, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
72
+ } else {
73
+ // Linux
74
+ return path.join(homeDir, '.config', 'Claude', 'claude_desktop_config.json');
75
+ }
76
+ }
77
+
78
+ // Check if API server is accessible
79
+ async function checkApiHealth(apiUrl) {
80
+ try {
81
+ const response = await axios.get(`${apiUrl}/health`, { timeout: 5000 });
82
+ return response.status === 200;
83
+ } catch (error) {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ // Validate API token
89
+ async function validateToken(apiUrl, token) {
90
+ try {
91
+ const response = await axios.get(`${apiUrl}/plans`, {
92
+ headers: {
93
+ 'Authorization': `ApiKey ${token}`
94
+ },
95
+ timeout: 5000
96
+ });
97
+ return response.status === 200;
98
+ } catch (error) {
99
+ if (error.response && error.response.status === 401) {
100
+ return false;
101
+ }
102
+ // If it's another error (like network), we can't validate
103
+ throw new Error(`Cannot validate token: ${error.message}`);
104
+ }
105
+ }
106
+
107
+ // Open URL in default browser
108
+ function openBrowser(url) {
109
+ const command = process.platform === 'darwin' ? 'open' :
110
+ process.platform === 'win32' ? 'start' : 'xdg-open';
111
+
112
+ exec(`${command} ${url}`, (error) => {
113
+ if (error) {
114
+ logInfo(`Could not open browser automatically. Please open: ${url}`);
115
+ }
116
+ });
117
+ }
118
+
119
+ // Create .env file
120
+ function createEnvFile(config) {
121
+ const envContent = `# Agent Planner MCP Configuration
122
+ # Generated on ${new Date().toISOString()}
123
+
124
+ API_URL=${config.apiUrl}
125
+ USER_API_TOKEN=${config.token}
126
+ MCP_SERVER_NAME=planning-system
127
+ MCP_SERVER_VERSION=0.2.0
128
+ NODE_ENV=production
129
+ `;
130
+
131
+ const envPath = path.join(__dirname, '..', '.env');
132
+ fs.writeFileSync(envPath, envContent);
133
+ return envPath;
134
+ }
135
+
136
+ // Update Claude Desktop config
137
+ function updateClaudeConfig(configPath, mcpServerPath, apiUrl, token) {
138
+ let config = {};
139
+
140
+ // Read existing config if it exists
141
+ if (fs.existsSync(configPath)) {
142
+ try {
143
+ const content = fs.readFileSync(configPath, 'utf8');
144
+ config = JSON.parse(content);
145
+ } catch (error) {
146
+ logInfo('Could not parse existing config, creating new one');
147
+ }
148
+ } else {
149
+ // Create directory if it doesn't exist
150
+ const configDir = path.dirname(configPath);
151
+ if (!fs.existsSync(configDir)) {
152
+ fs.mkdirSync(configDir, { recursive: true });
153
+ }
154
+ }
155
+
156
+ // Ensure mcpServers exists
157
+ if (!config.mcpServers) {
158
+ config.mcpServers = {};
159
+ }
160
+
161
+ // Add or update planning-system server
162
+ config.mcpServers['planning-system'] = {
163
+ command: 'node',
164
+ args: [path.join(mcpServerPath, 'src', 'index.js')],
165
+ env: {
166
+ API_URL: apiUrl,
167
+ USER_API_TOKEN: token
168
+ }
169
+ };
170
+
171
+ // Write config
172
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
173
+ return configPath;
174
+ }
175
+
176
+ // Main setup wizard
177
+ async function runSetup() {
178
+ log('\n🚀 Agent Planner MCP Setup Wizard\n', 'bright');
179
+
180
+ try {
181
+ // Step 1: API Configuration
182
+ logStep(1, 5, 'API Configuration');
183
+
184
+ log('\nWhere is your Agent Planner API running?');
185
+ log('1. Local development (localhost)');
186
+ log('2. Google Cloud Run');
187
+ log('3. Custom URL');
188
+
189
+ const deploymentType = await question('\nSelect option (1/2/3, default: 1): ');
190
+
191
+ let apiUrl;
192
+ let uiUrl;
193
+
194
+ if (deploymentType === '2') {
195
+ log('\nFor Cloud Run, you can find your URLs at:');
196
+ log('https://console.cloud.google.com/run?project=ta-agent-planner');
197
+
198
+ const apiUrlInput = await question('\nEnter API URL (e.g., https://agent-planner-api-xxx.run.app): ');
199
+ apiUrl = apiUrlInput.trim();
200
+
201
+ // Derive UI URL from API URL or ask for it
202
+ const suggestedUiUrl = apiUrl.replace('api-', 'ui-').replace('api.', 'ui.');
203
+ const uiUrlInput = await question(`Enter UI URL (default: ${suggestedUiUrl}): `);
204
+ uiUrl = uiUrlInput.trim() || suggestedUiUrl;
205
+ } else if (deploymentType === '3') {
206
+ const apiUrlInput = await question('\nEnter API URL: ');
207
+ apiUrl = apiUrlInput.trim();
208
+
209
+ const uiUrlInput = await question('Enter UI URL: ');
210
+ uiUrl = uiUrlInput.trim();
211
+ } else {
212
+ // Local development (default)
213
+ apiUrl = 'http://localhost:3000';
214
+ uiUrl = 'http://localhost:3001';
215
+ }
216
+
217
+ log('Checking API server...');
218
+ const apiAvailable = await checkApiHealth(apiUrl);
219
+
220
+ if (!apiAvailable) {
221
+ logError(`Cannot connect to API server at ${apiUrl}`);
222
+ logInfo('Make sure the API server is running: cd agent-planner && npm start');
223
+ process.exit(1);
224
+ }
225
+
226
+ logSuccess(`Found API server at ${apiUrl}`);
227
+
228
+ // Step 2: API Token Setup
229
+ logStep(2, 5, 'API Token Setup');
230
+
231
+ log('\nPlease generate an API token:');
232
+ log('1. Open ' + uiUrl + '/app/settings in your browser');
233
+ log('2. Navigate to the "API Tokens" section');
234
+ log('3. Click "Create MCP Token" or "Create New Token"');
235
+ log('4. Enter a name (e.g., "MCP Server")');
236
+ log('5. Copy the generated token\n');
237
+
238
+ logInfo('Opening settings page in your browser...');
239
+ openBrowser(uiUrl + '/app/settings');
240
+
241
+ await new Promise(resolve => setTimeout(resolve, 2000));
242
+
243
+ const token = await question('\nEnter your API token: ');
244
+
245
+ if (!token || token.trim().length < 10) {
246
+ logError('Invalid token provided');
247
+ process.exit(1);
248
+ }
249
+
250
+ log('Validating token...');
251
+ try {
252
+ const isValid = await validateToken(apiUrl, token.trim());
253
+
254
+ if (!isValid) {
255
+ logError('Token validation failed - please check the token and try again');
256
+ process.exit(1);
257
+ }
258
+
259
+ logSuccess('Token validated successfully');
260
+ } catch (error) {
261
+ logError(`Token validation failed: ${error.message}`);
262
+ process.exit(1);
263
+ }
264
+
265
+ // Step 3: Environment Configuration
266
+ logStep(3, 5, 'Environment Configuration');
267
+
268
+ const envPath = createEnvFile({ apiUrl, token: token.trim() });
269
+ logSuccess(`Created .env file at: ${envPath}`);
270
+
271
+ // Step 4: Claude Desktop Configuration
272
+ logStep(4, 5, 'Claude Desktop Configuration');
273
+
274
+ const configPath = getClaudeConfigPath();
275
+ const mcpServerPath = path.join(__dirname, '..');
276
+
277
+ log(`Detected Claude Desktop config at: ${configPath}`);
278
+
279
+ const shouldUpdateConfig = await question('Update Claude Desktop config? (y/n): ');
280
+
281
+ if (shouldUpdateConfig.toLowerCase() === 'y' || shouldUpdateConfig.toLowerCase() === 'yes') {
282
+ try {
283
+ updateClaudeConfig(configPath, mcpServerPath, apiUrl, token.trim());
284
+ logSuccess('Added planning-system MCP server to Claude Desktop config');
285
+ } catch (error) {
286
+ logError(`Failed to update config: ${error.message}`);
287
+ logInfo('You can manually add the configuration later');
288
+ }
289
+ } else {
290
+ logInfo('Skipped Claude Desktop config update');
291
+ log('\nManual configuration:');
292
+ log(JSON.stringify({
293
+ "mcpServers": {
294
+ "planning-system": {
295
+ "command": "node",
296
+ "args": [path.join(mcpServerPath, 'src', 'index.js')],
297
+ "env": {
298
+ "API_URL": apiUrl,
299
+ "USER_API_TOKEN": token.trim().substring(0, 10) + '...'
300
+ }
301
+ }
302
+ }
303
+ }, null, 2));
304
+ }
305
+
306
+ // Step 5: Testing Connection
307
+ logStep(5, 5, 'Testing Connection');
308
+
309
+ log('Testing MCP server connection...');
310
+
311
+ // Simple test by loading the API client
312
+ try {
313
+ process.env.API_URL = apiUrl;
314
+ process.env.USER_API_TOKEN = token.trim();
315
+
316
+ const apiClient = require('./api-client');
317
+ const plans = await apiClient.plans.getPlans();
318
+
319
+ logSuccess(`MCP server can connect to API`);
320
+ logSuccess(`Successfully retrieved ${plans.length} plan(s)`);
321
+ } catch (error) {
322
+ logError(`Connection test failed: ${error.message}`);
323
+ }
324
+
325
+ // Success!
326
+ log('\n🎉 Setup complete!\n', 'green');
327
+
328
+ log('Next steps:', 'bright');
329
+ log('1. Restart Claude Desktop to load the MCP server');
330
+ log('2. Look for the 🔨 icon in Claude Desktop - you should see planning tools');
331
+ log('3. Try asking: "List my plans" or "Create a new plan called \'Test Project\'"');
332
+ log('');
333
+ log('Configuration saved to:', 'bright');
334
+ log(` .env file: ${envPath}`);
335
+ log(` Claude config: ${configPath}`);
336
+ log('');
337
+
338
+ } catch (error) {
339
+ logError(`\nSetup failed: ${error.message}`);
340
+ process.exit(1);
341
+ } finally {
342
+ rl.close();
343
+ }
344
+ }
345
+
346
+ // Run the setup wizard
347
+ runSetup();
@@ -0,0 +1,3 @@
1
+ // This file has been removed as per code review recommendations
2
+ // The search functionality should be tested using the actual API endpoints
3
+ // after fixing the authentication and controller implementation.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Search wrapper for the agent-planner-mcp
3
+ *
4
+ * This module provides wrapper functions for the search API calls
5
+ * that properly handle the response format (extracting the results array
6
+ * from the {query, results, count} response structure).
7
+ */
8
+ const apiClient = require('../api-client');
9
+
10
+ /**
11
+ * Search within a plan and return only the results array
12
+ *
13
+ * @param {string} planId - ID of the plan to search
14
+ * @param {string} query - Search query text
15
+ * @returns {Promise<Array>} - Array of search results
16
+ */
17
+ async function searchPlan(planId, query) {
18
+ try {
19
+ // Call the original search function from the API client
20
+ const response = await apiClient.search.searchPlan(planId, query);
21
+
22
+ // Log the actual response for debugging
23
+ if (process.env.NODE_ENV === 'development') {
24
+ console.log('Search plan response:', typeof response, Object.keys(response || {}));
25
+ }
26
+
27
+ // Handle different response formats
28
+ if (!response) {
29
+ return [];
30
+ }
31
+
32
+ // If response is already an array, return it
33
+ if (Array.isArray(response)) {
34
+ return response;
35
+ }
36
+
37
+ // If response has results array
38
+ if (response.results && Array.isArray(response.results)) {
39
+ return response.results;
40
+ }
41
+
42
+ // If response has other properties that are arrays
43
+ const results = [];
44
+ Object.keys(response).forEach(key => {
45
+ if (Array.isArray(response[key])) {
46
+ response[key].forEach(item => {
47
+ results.push({
48
+ ...item,
49
+ type: item.type || key,
50
+ source: 'plan_search'
51
+ });
52
+ });
53
+ }
54
+ });
55
+
56
+ if (results.length > 0) {
57
+ return results;
58
+ }
59
+
60
+ // Try to parse response as a search result object
61
+ if (response.query !== undefined && response.count !== undefined) {
62
+ return response.results || [];
63
+ }
64
+
65
+ console.error('Unexpected search response format:', JSON.stringify(response).substring(0, 200));
66
+ return [];
67
+ } catch (error) {
68
+ console.error('Error in searchPlan wrapper:', error.message);
69
+ return [];
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Global search across all plans and return only the results
75
+ *
76
+ * @param {string} query - Search query text
77
+ * @returns {Promise<Array>} - Flattened array of all search results
78
+ */
79
+ async function globalSearch(query) {
80
+ try {
81
+ // Call the original global search function
82
+ const response = await apiClient.search.globalSearch(query);
83
+
84
+ if (process.env.NODE_ENV === 'development') {
85
+ console.log('Global search response type:', typeof response);
86
+ if (response) {
87
+ console.log('Response keys:', Object.keys(response));
88
+ }
89
+ }
90
+
91
+ // Handle different response formats
92
+ if (!response) {
93
+ return [];
94
+ }
95
+
96
+ // If response is already an array of results
97
+ if (Array.isArray(response)) {
98
+ return response.map(item => ({
99
+ ...item,
100
+ type: item.type || 'unknown',
101
+ source: 'global_search'
102
+ }));
103
+ }
104
+
105
+ // If response has a results property
106
+ if (response.results !== undefined) {
107
+ // If results is already an array, return it
108
+ if (Array.isArray(response.results)) {
109
+ return response.results.map(item => ({
110
+ ...item,
111
+ type: item.type || 'unknown',
112
+ source: 'global_search'
113
+ }));
114
+ }
115
+
116
+ // If results is an object with categories
117
+ if (typeof response.results === 'object') {
118
+ const allResults = [];
119
+
120
+ // Collect results from each category (plans, nodes, comments, etc.)
121
+ Object.keys(response.results).forEach(category => {
122
+ if (Array.isArray(response.results[category])) {
123
+ // Add category type to each result
124
+ const categoryResults = response.results[category].map(item => ({
125
+ ...item,
126
+ type: item.type || category,
127
+ category,
128
+ source: 'global_search'
129
+ }));
130
+ allResults.push(...categoryResults);
131
+ }
132
+ });
133
+
134
+ return allResults;
135
+ }
136
+ }
137
+
138
+ // Check if response has categorized results (plans, nodes, etc.)
139
+ const categories = ['plans', 'nodes', 'comments', 'logs', 'artifacts'];
140
+ const allResults = [];
141
+
142
+ categories.forEach(category => {
143
+ if (response[category] && Array.isArray(response[category])) {
144
+ response[category].forEach(item => {
145
+ allResults.push({
146
+ ...item,
147
+ type: item.type || category.slice(0, -1), // Remove 's' from category
148
+ category,
149
+ source: 'global_search'
150
+ });
151
+ });
152
+ }
153
+ });
154
+
155
+ if (allResults.length > 0) {
156
+ return allResults;
157
+ }
158
+
159
+ // Generic handler for any object with arrays
160
+ Object.keys(response).forEach(key => {
161
+ if (Array.isArray(response[key]) && key !== 'results') {
162
+ response[key].forEach(item => {
163
+ allResults.push({
164
+ ...item,
165
+ type: item.type || key,
166
+ category: key,
167
+ source: 'global_search'
168
+ });
169
+ });
170
+ }
171
+ });
172
+
173
+ return allResults;
174
+ } catch (error) {
175
+ console.error('Error in globalSearch wrapper:', error.message);
176
+ if (error.response) {
177
+ console.error('API error status:', error.response.status);
178
+ console.error('API error data:', error.response.data);
179
+ }
180
+ return [];
181
+ }
182
+ }
183
+
184
+ // Export the wrapper functions
185
+ module.exports = {
186
+ searchPlan,
187
+ globalSearch
188
+ };