foundry-mcp 0.7.1

A comprehensive CLI tool and MCP server for deterministic project management and AI coding assistant integration
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! Implementation of the install command

use crate::{
    cli::args::InstallArgs,
    core::installation,
    types::responses::{InstallResponse, InstallationStatus},
    utils::formatting::format_install_output,
};
use anyhow::Result;

pub async fn execute(args: InstallArgs) -> Result<String> {
    // Validate installation target
    validate_target(&args.target)?;

    // Handle installation and response building in a single match statement
    let (result, binary_path) = match args.target.as_str() {
        "claude-code" => {
            // Claude Code uses "foundry" from PATH, no need for binary path detection
            let result = installation::install_for_claude_code()
                .await
                .map_err(|e| enhance_installation_error("claude-code", &e))?;
            (result, "foundry (from PATH)".to_string())
        }
        "cursor" => {
            // Cursor uses "foundry" from PATH, same as Claude Code
            let result = installation::install_for_cursor()
                .await
                .map_err(|e| enhance_installation_error("cursor", &e))?;
            (result, "foundry (from PATH)".to_string())
        }
        _ => {
            return Err(anyhow::anyhow!(
                "Unsupported installation target: {}. Supported targets: claude-code, cursor",
                args.target
            ));
        }
    };

    // Build response data for both JSON and human-readable output
    let response_data = InstallResponse {
        target: args.target.clone(),
        binary_path: binary_path.clone(),
        config_path: result.config_path.clone(),
        installation_status: if result.success {
            InstallationStatus::Success
        } else {
            InstallationStatus::Partial
        },
        actions_taken: result.actions_taken.clone(),
    };

    // Return formatted output based on --json flag
    if args.json {
        // Return JSON format - only include the data for clean CLI output
        Ok(serde_json::to_string_pretty(&response_data)?)
    } else {
        // Return human-readable format
        Ok(format_install_output(
            &args.target,
            &binary_path,
            &result.config_path,
            result.success,
            &result.actions_taken,
        ))
    }
}

/// Validate the installation target
fn validate_target(target: &str) -> Result<()> {
    match target {
        "claude-code" | "cursor" => Ok(()),
        _ => Err(anyhow::anyhow!(
            "Unsupported installation target: {}. Supported targets: claude-code, cursor",
            target
        )),
    }
}

/// Enhance installation errors with specific context and actionable guidance
fn enhance_installation_error(target: &str, original_error: &anyhow::Error) -> anyhow::Error {
    let error_msg = original_error.to_string();

    // Handle specific error cases with detailed guidance
    if error_msg.contains("already configured") {
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 Foundry MCP is already installed for {} but will be overwritten.\n\
            💡 Check current status: foundry status --detailed",
            target,
            error_msg,
            target
        )
    } else if error_msg.contains("Binary path does not exist") {
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 The Foundry binary could not be found. This can happen when:\n\
            💡   • Running via 'cargo run' (use explicit path: --binary-path /path/to/foundry)\n\
            💡   • Binary was moved or deleted after compilation\n\
            💡   • Permissions prevent access to the binary\n\
            💡 Try: foundry install {} --binary-path $(which foundry)",
            target,
            error_msg,
            target
        )
    } else if error_msg.contains("command does not exist") {
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 The configured binary path is invalid. This usually means:\n\
            💡   • The Foundry binary was moved or deleted\n\
            💡   • The configuration points to an old development build\n\
            💡 Try: foundry install {} --binary-path $(which foundry)",
            target,
            error_msg,
            target
        )
    } else if error_msg.contains("not found in PATH") || error_msg.contains("CLI not found") {
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 {} is not installed or not available in PATH.\n\
            💡 Please install {} first, then retry the installation.\n\
            💡 Check installation status: foundry status --detailed",
            target,
            error_msg,
            target,
            target
        )
    } else if error_msg.contains("Permission denied") || error_msg.contains("not writable") {
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 Permission denied accessing configuration directory.\n\
            💡 This can happen when:\n\
            💡   • Configuration directory is owned by another user\n\
            💡   • Disk is full or read-only\n\
            💡   • System security policies prevent file creation\n\
            💡 Try running with appropriate permissions or check disk space",
            target,
            error_msg
        )
    } else if error_msg.contains("Failed to read") || error_msg.contains("Failed to write") {
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 File system error during configuration. This can indicate:\n\
            💡   • Insufficient disk space\n\
            💡   • Corrupted configuration file\n\
            💡   • File system permissions issue\n\
            💡 Try: foundry status --detailed to diagnose the problem",
            target,
            error_msg
        )
    } else {
        // Generic enhancement for unknown errors
        anyhow::anyhow!(
            "Installation failed for {}: {}\n\n\
            💡 For detailed diagnosis: foundry status --detailed\n\
            💡 For help: foundry install --help\n\
            💡 To report this issue: include the error above and output of 'foundry status --detailed'",
            target,
            error_msg
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::installation::create_installation_result;

    #[test]
    fn test_validate_target_valid() {
        let valid_targets = vec!["claude-code", "cursor"];

        for target in valid_targets {
            assert!(
                validate_target(target).is_ok(),
                "Target '{}' should be valid",
                target
            );
        }
    }

    #[test]
    fn test_validate_target_invalid() {
        let invalid_targets = vec!["", "vscode", "claude-desktop"];

        for target in invalid_targets {
            assert!(
                validate_target(target).is_err(),
                "Target '{}' should be invalid",
                target
            );
        }
    }

    #[test]
    fn test_execute_invalid_target() {
        let args = InstallArgs {
            target: "invalid-target".to_string(),
            binary_path: None,
            json: false,
        };

        // Test the validation logic without calling execute()
        let validation_result = validate_target(&args.target);
        assert!(validation_result.is_err());
        assert!(
            validation_result
                .unwrap_err()
                .to_string()
                .contains("Unsupported installation target")
        );
    }

    #[test]
    fn test_install_args_creation() {
        let args = InstallArgs {
            target: "claude-code".to_string(),
            binary_path: Some("/custom/path/foundry".to_string()),
            json: false,
        };

        assert_eq!(args.target, "claude-code");
        assert_eq!(args.binary_path, Some("/custom/path/foundry".to_string()));
    }

    #[test]
    fn test_install_args_default_values() {
        let args = InstallArgs {
            target: "cursor".to_string(),
            binary_path: None,
            json: false,
        };

        assert_eq!(args.target, "cursor");
        assert!(args.binary_path.is_none());
    }

    #[test]
    fn test_create_installation_result() {
        let result = create_installation_result(
            true,
            "/path/to/config.json".to_string(),
            vec!["Action 1".to_string(), "Action 2".to_string()],
        );

        assert!(result.success);
        assert_eq!(result.config_path, "/path/to/config.json");
        assert_eq!(result.actions_taken.len(), 2);
        assert_eq!(result.actions_taken[0], "Action 1");
        assert_eq!(result.actions_taken[1], "Action 2");
    }

    #[test]
    fn test_execute_with_explicit_binary_path() {
        use crate::test_environment::TestEnvironment;

        let env = TestEnvironment::new().unwrap();

        env.with_env_async(|| async {
            let binary_path = env.create_mock_binary("foundry").unwrap();

            let args = InstallArgs {
                target: "cursor".to_string(),
                binary_path: Some(binary_path.to_string_lossy().to_string()),
                json: false,
            };

            // This test validates the CLI argument processing
            assert_eq!(args.target, "cursor");
            assert!(args.binary_path.is_some());

            // Test target validation
            assert!(validate_target(&args.target).is_ok());
        });
    }

    #[test]
    fn test_execute_response_structure() {
        use crate::test_environment::TestEnvironment;

        let env = TestEnvironment::new().unwrap();

        env.with_env_async(|| async {
            let binary_path = env.create_mock_binary("foundry").unwrap();

            let _args = InstallArgs {
                target: "cursor".to_string(),
                binary_path: Some(binary_path.to_string_lossy().to_string()),
                json: false,
            };

            // We can't easily test the full execute function due to path dependencies,
            // but we can test the response structure building
            let mock_result = create_installation_result(
                true,
                "/test/config/path".to_string(),
                vec!["Mock action".to_string()],
            );

            assert!(mock_result.success);
            assert_eq!(mock_result.config_path, "/test/config/path");
            assert_eq!(mock_result.actions_taken.len(), 1);
        });
    }

    #[test]
    fn test_validate_target_comprehensive() {
        // Test all supported targets
        let supported_targets = vec!["claude-code", "cursor"];
        for target in supported_targets {
            assert!(
                validate_target(target).is_ok(),
                "Target '{}' should be supported",
                target
            );
        }

        // Test unsupported targets
        let unsupported_targets = vec![
            "",
            "vscode",
            "claude-desktop",
            "intellij",
            "vim",
            "invalid-target",
            "CURSOR",      // Case sensitive
            "Claude-Code", // Case sensitive
        ];

        for target in unsupported_targets {
            let result = validate_target(target);
            assert!(
                result.is_err(),
                "Target '{}' should not be supported",
                target
            );
            assert!(
                result
                    .unwrap_err()
                    .to_string()
                    .contains("Unsupported installation target"),
                "Error message should mention unsupported target for '{}'",
                target
            );
        }
    }

    #[test]
    fn test_install_args_builder_pattern() {
        // Test different combinations of arguments
        let args1 = InstallArgs {
            target: "claude-code".to_string(),
            binary_path: None,
            json: false,
        };
        assert_eq!(args1.target, "claude-code");
        assert!(args1.binary_path.is_none());

        let args2 = InstallArgs {
            target: "cursor".to_string(),
            binary_path: Some("/custom/path".to_string()),
            json: true,
        };
        assert_eq!(args2.target, "cursor");
        assert_eq!(args2.binary_path, Some("/custom/path".to_string()));
    }

    #[test]
    fn test_enhance_installation_error_already_configured() {
        let original = anyhow::anyhow!("Foundry MCP server is already configured for Cursor.");

        // Test error enhancement
        let enhanced = enhance_installation_error("cursor", &original);
        let error_msg = enhanced.to_string();
        assert!(error_msg.contains("Installation failed for cursor"));
        assert!(error_msg.contains("already installed for cursor"));
        assert!(error_msg.contains("will be overwritten"));
    }

    #[test]
    fn test_enhance_installation_error_binary_path() {
        let original = anyhow::anyhow!("Binary path does not exist: /nonexistent/path");
        let enhanced = enhance_installation_error("cursor", &original);
        let error_msg = enhanced.to_string();

        assert!(error_msg.contains("Installation failed for cursor"));
        assert!(error_msg.contains("binary could not be found"));
        assert!(error_msg.contains("cargo run"));
        assert!(error_msg.contains("--binary-path"));
        assert!(error_msg.contains("$(which foundry)"));
    }

    #[test]
    fn test_enhance_installation_error_command_not_exist() {
        let original = anyhow::anyhow!("Server 'foundry' command does not exist: /old/path");
        let enhanced = enhance_installation_error("cursor", &original);
        let error_msg = enhanced.to_string();

        assert!(error_msg.contains("Installation failed for cursor"));
        assert!(error_msg.contains("configured binary path is invalid"));
        assert!(error_msg.contains("moved or deleted"));
    }

    #[test]
    fn test_enhance_installation_error_cli_not_found() {
        let original = anyhow::anyhow!("Claude Code CLI not found in PATH");
        let enhanced = enhance_installation_error("claude-code", &original);
        let error_msg = enhanced.to_string();

        assert!(error_msg.contains("Installation failed for claude-code"));
        assert!(error_msg.contains("not installed or not available"));
        assert!(error_msg.contains("Please install claude-code first"));
    }

    #[test]
    fn test_enhance_installation_error_permission_denied() {
        let original = anyhow::anyhow!("Permission denied accessing configuration directory");
        let enhanced = enhance_installation_error("cursor", &original);
        let error_msg = enhanced.to_string();

        assert!(error_msg.contains("Installation failed for cursor"));
        assert!(error_msg.contains("Permission denied"));
        assert!(error_msg.contains("owned by another user"));
        assert!(error_msg.contains("appropriate permissions"));
    }

    #[test]
    fn test_enhance_installation_error_generic() {
        let original = anyhow::anyhow!("Some unexpected error occurred");
        let enhanced = enhance_installation_error("cursor", &original);
        let error_msg = enhanced.to_string();

        assert!(error_msg.contains("Installation failed for cursor"));
        assert!(error_msg.contains("Some unexpected error occurred"));
        assert!(error_msg.contains("detailed diagnosis"));
        assert!(error_msg.contains("foundry status --detailed"));
        assert!(error_msg.contains("report this issue"));
    }
}