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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
// Modern TestEnvironment for Isolated Test Execution
//
// This module provides the foundational TestEnvironment implementation used by both
// unit tests (via src/test_environment.rs) and integration tests (via tests/common/test_utils.rs).
//
// ## Key Features
//
// - **Complete Isolation**: Each test gets a unique temporary directory and isolated environment
// - **Modern Rust Testing**: Uses assert_fs and temp-env for reliable test isolation
// - **Cross-Platform**: Works on Unix and Windows with appropriate path handling
// - **Async Support**: Full async/await support with proper runtime management
// - **No Global State**: No mutexes or global locks - pure isolation via environment variables
//
// ## Usage Pattern
//
// All tests should follow this pattern:
//
// ```rust
// #[test]
// fn test_something() {
//     let env = TestEnvironment::new().unwrap();
//
//     let _ = env.with_env_async(|| async {
//         // Your test code here - fully isolated
//         env.create_test_project("my-project").await.unwrap();
//
//         // Use spawn_blocking for sync functions to avoid nested runtimes
//         let project_name_clone = "my-project".to_string();
//         let projects = tokio::task::spawn_blocking(move || {
//             list_projects(&project_name_clone)
//         }).await.unwrap().unwrap();
//         assert_eq!(projects.len(), 1);
//     });
// }
// ```
//
// This follows the testing patterns described in .cursor/rules/testing-patterns.mdc

// Imports are handled by the including file

// Base TestEnvironment implementation shared between unit and integration tests

/// Modern test environment using assert_fs + temp-env
/// Follows the established testing patterns without global mutexes
pub struct TestEnvironment {
    pub temp_dir: TempDir,
    home: PathBuf,
    cursor_config: PathBuf,
    claude_config: PathBuf,
    bin: PathBuf,
}

impl TestEnvironment {
    /// Create a new test environment with isolated directory
    pub fn new() -> Result<Self> {
        let temp_dir = TempDir::new()?;

        // Create isolated directory structure
        let home = temp_dir.path().join("home");
        let cursor_config = temp_dir.path().join(".cursor");
        let claude_config = temp_dir.path().join(".claude");
        let bin = temp_dir.path().join("bin");

        // Ensure directories exist
        fs::create_dir_all(&home)?;
        fs::create_dir_all(&cursor_config)?;
        fs::create_dir_all(&claude_config)?;
        fs::create_dir_all(&bin)?;

        Ok(TestEnvironment {
            temp_dir,
            home,
            cursor_config,
            claude_config,
            bin,
        })
    }

    /// Get the foundry directory path within the test environment
    pub fn foundry_dir(&self) -> PathBuf {
        self.home.join(".foundry")
    }

    /// Get the root path of the test environment
    pub fn root(&self) -> &Path {
        self.temp_dir.path()
    }

    /// Join a relative path to the test environment root
    pub fn join(&self, rel: impl AsRef<Path>) -> PathBuf {
        self.temp_dir.path().join(rel)
    }

    /// Get the bin directory for mock executables
    pub fn bin_dir(&self) -> &Path {
        &self.bin
    }

    /// Write a file within the test environment
    pub fn write_file(&self, rel: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Result<()> {
        let path = self.join(rel);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(path, contents)?;
        Ok(())
    }

    /// Read a file from within the test environment
    pub fn read_to_string(&self, rel: impl AsRef<Path>) -> Result<String> {
        let path = self.join(rel);
        fs::read_to_string(path).map_err(Into::into)
    }

    /// Create an executable file within the test environment
    pub fn make_executable(&self, rel: impl AsRef<Path>, contents: &str) -> Result<PathBuf> {
        let path = self.join(rel);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(&path, contents)?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&path)?.permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&path, perms)?;
        }

        Ok(path)
    }

    /// Get base environment variables for isolation
    fn base_vars(&self) -> Vec<(OsString, Option<OsString>)> {
        let mut vars = Vec::new();

        // Cross-platform HOME
        #[cfg(windows)]
        vars.push((
            OsString::from("USERPROFILE"),
            Some(self.home.clone().into_os_string()),
        ));
        #[cfg(not(windows))]
        vars.push((
            OsString::from("HOME"),
            Some(self.home.clone().into_os_string()),
        ));

        // Cursor and Claude config dirs
        vars.push((
            OsString::from("CURSOR_CONFIG_DIR"),
            Some(self.cursor_config.clone().into_os_string()),
        ));
        vars.push((
            OsString::from("CLAUDE_CONFIG_DIR"),
            Some(self.claude_config.clone().into_os_string()),
        ));

        // PATH: bin first, then original PATH for tool discovery
        let orig_path = std::env::var_os("PATH").unwrap_or_default();
        let mut new_path = OsString::new();
        new_path.push(self.bin.clone().into_os_string());
        #[cfg(windows)]
        new_path.push(";");
        #[cfg(not(windows))]
        new_path.push(":");
        new_path.push(orig_path);
        vars.push((OsString::from("PATH"), Some(new_path)));

        vars
    }

    /// Execute sync code within isolated environment
    pub fn with_env<F, T>(&self, f: F) -> T
    where
        F: FnOnce() -> T,
    {
        temp_env::with_vars(self.base_vars(), f)
    }

    /// Execute async code within isolated environment
    pub fn with_env_async<F, Fut, T>(&self, f: F) -> T
    where
        F: FnOnce() -> Fut,
        Fut: Future<Output = T>,
    {
        self.with_env(|| {
            // Create a new single-threaded runtime for simplicity and isolation
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("Failed to create tokio runtime for test");
            rt.block_on(f())
        })
    }

    /// Execute code with additional environment variables
    pub fn with_env_and_vars<F, T>(&self, extra: &[(OsString, Option<OsString>)], f: F) -> T
    where
        F: FnOnce() -> T,
    {
        let mut vars = self.base_vars();
        vars.extend_from_slice(extra);
        temp_env::with_vars(vars, f)
    }

    /// Execute async code with additional environment variables
    pub fn with_env_and_vars_async<F, Fut, T>(
        &self,
        extra: &[(OsString, Option<OsString>)],
        f: F,
    ) -> T
    where
        F: FnOnce() -> Fut,
        Fut: Future<Output = T>,
    {
        self.with_env_and_vars(extra, || {
            // Create a new single-threaded runtime for simplicity and isolation
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("Failed to create tokio runtime for test");
            rt.block_on(f())
        })
    }

    /// Execute async code with PATH environment including bin directory
    pub fn with_env_and_path_async<F, Fut, T>(&self, f: F) -> T
    where
        F: FnOnce() -> Fut,
        Fut: Future<Output = T>,
    {
        // Get current PATH and prepend our bin directory
        let current_path = std::env::var_os("PATH").unwrap_or_default();
        let mut path_vec = vec![self.bin.clone().into_os_string()];

        if !current_path.is_empty() {
            #[cfg(windows)]
            path_vec.push(OsString::from(";"));
            #[cfg(not(windows))]
            path_vec.push(OsString::from(":"));
            path_vec.push(current_path);
        }

        let new_path = path_vec.into_iter().collect::<OsString>();
        let extra_vars = &[(OsString::from("PATH"), Some(new_path))];

        self.with_env_and_vars(extra_vars, || {
            // Create a new single-threaded runtime for simplicity and isolation
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("Failed to create tokio runtime for test");
            rt.block_on(f())
        })
    }

    // Helper methods for common test patterns

    // Helper methods are implemented in the including file

    /// Get cursor config path within test environment
    pub fn cursor_config_path(&self) -> PathBuf {
        self.cursor_config.join("mcp.json")
    }

    /// Get cursor config directory within test environment
    pub fn cursor_config_dir(&self) -> PathBuf {
        self.cursor_config.clone()
    }

    /// Get claude code config path within test environment
    pub fn claude_code_config_path(&self) -> PathBuf {
        self.home.join(".claude.json")
    }

    /// Get Claude Code config directory path within test environment
    pub fn claude_config_dir(&self) -> PathBuf {
        self.claude_config.clone()
    }

    /// Get Claude Code agents directory path within test environment
    pub fn claude_agents_dir(&self) -> PathBuf {
        self.claude_config.join("agents")
    }

    /// Get Claude Code subagent file path within test environment
    pub fn claude_subagent_path(&self) -> PathBuf {
        self.claude_agents_dir().join("foundry-mcp-agent.md")
    }

    /// Get Cursor rules directory path within test environment
    pub fn cursor_rules_dir(&self) -> PathBuf {
        self.cursor_config.join("rules")
    }

    /// Get Cursor rules file path within test environment
    pub fn cursor_rules_path(&self) -> PathBuf {
        self.cursor_rules_dir().join("foundry.mdc")
    }

    /// Get Claude commands directory path within test environment
    pub fn claude_commands_dir(&self) -> PathBuf {
        self.claude_config.join("commands").join("foundry")
    }

    /// Get Cursor commands directory path within test environment
    pub fn cursor_commands_dir(&self) -> PathBuf {
        self.cursor_config.join("commands")
    }

    /// Create a cursor MCP configuration with the given server entries
    pub fn create_cursor_config(&self, servers: &[(&str, &str)]) -> Result<()> {
        fs::create_dir_all(&self.cursor_config)?;

        let mut config = serde_json::Map::new();
        let mut servers_config = serde_json::Map::new();

        // Always include mcpServers field, even if empty
        for (name, command) in servers {
            let mut server_config = serde_json::Map::new();
            server_config.insert(
                "command".to_string(),
                serde_json::Value::String(command.to_string()),
            );
            server_config.insert("args".to_string(), serde_json::Value::Array(vec![]));

            servers_config.insert(name.to_string(), serde_json::Value::Object(server_config));
        }

        config.insert(
            "mcpServers".to_string(),
            serde_json::Value::Object(servers_config),
        );

        let config_content = serde_json::to_string_pretty(&config)?;
        fs::write(self.cursor_config_path(), config_content)?;

        Ok(())
    }

    /// Create an existing cursor config with custom content for testing conflict scenarios
    pub fn create_existing_cursor_config(&self, content: &str) -> Result<()> {
        fs::create_dir_all(&self.cursor_config)?;
        fs::write(self.cursor_config_path(), content)?;
        Ok(())
    }

    /// Create a mock binary file for testing
    pub fn create_mock_binary(&self, name: &str) -> Result<PathBuf> {
        fs::create_dir_all(&self.bin)?;

        let binary_path = self.bin.join(name);
        fs::write(&binary_path, "#!/bin/bash\necho 'Mock binary'")?;

        // Make it executable (Unix systems)
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&binary_path)?.permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&binary_path, perms)?;
        }

        Ok(binary_path)
    }

    /// Create a mock claude command that handles the specific commands used by the installation process
    pub fn create_mock_claude_binary(&self) -> Result<PathBuf> {
        fs::create_dir_all(&self.bin)?;

        let binary_path = self.bin.join("claude");

        // Create a bash script that handles the specific claude commands
        let script_content = r#"#!/bin/bash
# Mock claude command for testing
case "$1" in
    "--version")
        echo "claude version 1.0.0"
        exit 0
        ;;
    "mcp")
        case "$2" in
            "add")
                # Mock successful MCP server registration
                echo "MCP server 'foundry' added successfully"
                exit 0
                ;;
            "remove")
                # Mock MCP server removal - fail if server doesn't exist
                echo "No MCP server found with name: 'foundry'" >&2
                exit 1
                ;;
            *)
                echo "Unknown mcp command: $2"
                exit 1
                ;;
        esac
        ;;
    *)
        echo "Unknown command: $1"
        exit 1
        ;;
esac
"#;

        fs::write(&binary_path, script_content)?;

        // Make it executable (Unix systems)
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(&binary_path)?.permissions();
            perms.set_mode(0o755);
            fs::set_permissions(&binary_path, perms)?;
        }

        Ok(binary_path)
    }

    /// Create an invalid binary path for error testing
    pub fn invalid_binary_path(&self) -> String {
        "/definitely/does/not/exist/foundry".to_string()
    }

    /// Create a binary path that exists but is not executable (for platforms that check)
    pub fn non_executable_binary_path(&self) -> String {
        let binary_path = self.temp_dir.path().join("non-executable");
        fs::write(&binary_path, b"not executable content").unwrap();
        binary_path.to_string_lossy().to_string()
    }

    /// Verify that Cursor rules template was created with expected content
    pub fn verify_cursor_rules_template(&self) -> Result<()> {
        let rules_path = self.cursor_rules_path();
        if !rules_path.exists() {
            anyhow::bail!("Cursor rules file should exist after installation");
        }

        let rules_content = fs::read_to_string(&rules_path)?;

        // Verify essential content sections
        if !rules_content.contains("# Foundry MCP Usage Guide") {
            anyhow::bail!("Rules should contain usage guide header");
        }
        if !rules_content.contains("create_project") || !rules_content.contains("update_spec") {
            anyhow::bail!("Rules should reference Foundry MCP tools");
        }
        if !rules_content.contains("Content Agnostic") {
            anyhow::bail!("Rules should contain core principles");
        }

        Ok(())
    }

    /// Verify that Claude subagent template was created with expected content
    pub fn verify_claude_subagent_template(&self) -> Result<()> {
        let subagent_path = self.claude_subagent_path();
        if !subagent_path.exists() {
            anyhow::bail!("Claude subagent file should exist after installation");
        }

        let subagent_content = fs::read_to_string(&subagent_path)?;

        // Verify essential content sections
        if !subagent_content.contains("---") {
            anyhow::bail!("Subagent should contain YAML frontmatter");
        }
        if !subagent_content.contains("foundry-mcp-agent") {
            anyhow::bail!("Subagent should contain agent name");
        }
        if !subagent_content.contains("mcp_foundry_") {
            anyhow::bail!("Subagent should reference MCP tools");
        }
        if !subagent_content.contains("Content Agnostic") {
            anyhow::bail!("Subagent should contain core principles");
        }
        if !subagent_content.contains("IMPORTANT: Append only adds to the END") {
            anyhow::bail!("Subagent should contain critical append guidance");
        }
        if !subagent_content.contains("Content Creation Standards") {
            anyhow::bail!("Subagent should contain content formatting guidelines");
        }

        Ok(())
    }
}

impl Default for TestEnvironment {
    fn default() -> Self {
        Self::new().expect("Failed to create test environment")
    }
}