forge-cpp-mcp 0.2.3__tar.gz → 0.2.5__tar.gz

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.
Files changed (128) hide show
  1. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/PKG-INFO +62 -5
  2. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/README.md +61 -4
  3. forge_cpp_mcp-0.2.5/frontend/cmake-run-test.html +12 -0
  4. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/package.json +1 -1
  5. forge_cpp_mcp-0.2.5/frontend/src/cmake-run-test.js +8 -0
  6. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/vite.config.js +1 -0
  7. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/hatch_build.py +1 -1
  8. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/pyproject.toml +1 -1
  9. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/__init__.py +1 -1
  10. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/service.py +18 -5
  11. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/build_tree.py +67 -0
  12. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/errors.py +5 -0
  13. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/file_api.py +229 -0
  14. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/models.py +30 -0
  15. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/profiles.py +21 -0
  16. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/service.py +295 -184
  17. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/text.py +19 -0
  18. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/workspace.py +122 -0
  19. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/service.py +2 -1
  20. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/server.py +3 -0
  21. forge_cpp_mcp-0.2.3/src/forgemcp/cmake/errors.py +0 -5
  22. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/.gitignore +0 -0
  23. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/LICENSE +0 -0
  24. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/clangd-result.html +0 -0
  25. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-build.html +0 -0
  26. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-configure.html +0 -0
  27. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-profiles.html +0 -0
  28. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/cmake-test.html +0 -0
  29. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/package-lock.json +0 -0
  30. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/process-details.html +0 -0
  31. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/process-overview.html +0 -0
  32. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-navigation-view.js +0 -0
  33. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-result.js +0 -0
  34. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-view.js +0 -0
  35. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-build.js +0 -0
  36. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-configure.js +0 -0
  37. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-profiles.js +0 -0
  38. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-test.js +0 -0
  39. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-view.js +0 -0
  40. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process-details.js +0 -0
  41. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process-overview.js +0 -0
  42. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process-status.js +0 -0
  43. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/process.css +0 -0
  44. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/app.js +0 -0
  45. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/copy-icon.js +0 -0
  46. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/presentation.js +0 -0
  47. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-resources.js +0 -0
  48. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-view.js +0 -0
  49. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/shared/widget.css +0 -0
  50. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/source-view.js +0 -0
  51. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/toolsets.js +0 -0
  52. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-file.js +0 -0
  53. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-result.js +0 -0
  54. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-search.js +0 -0
  55. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-tree.js +0 -0
  56. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-view.js +0 -0
  57. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/tests/result-resources.test.js +0 -0
  58. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/tests/result-view.test.js +0 -0
  59. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/tests/workspace.test.js +0 -0
  60. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/toolsets.html +0 -0
  61. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-file.html +0 -0
  62. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-result.html +0 -0
  63. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-search.html +0 -0
  64. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/frontend/workspace-tree.html +0 -0
  65. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/assets.py +0 -0
  66. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/__init__.py +0 -0
  67. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/errors.py +0 -0
  68. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/models.py +0 -0
  69. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/session.py +0 -0
  70. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/text.py +0 -0
  71. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/__init__.py +0 -0
  72. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/completion.py +0 -0
  73. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.LICENSE +0 -0
  74. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.svg +0 -0
  75. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/cmake.svg +0 -0
  76. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/process.svg +0 -0
  77. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/toolchain.svg +0 -0
  78. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-edit.svg +0 -0
  79. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-file.svg +0 -0
  80. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-search.svg +0 -0
  81. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace.svg +0 -0
  82. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/markdown.py +0 -0
  83. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/__init__.py +0 -0
  84. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/errors.py +0 -0
  85. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/models.py +0 -0
  86. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/text.py +0 -0
  87. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/transcript.py +0 -0
  88. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/progress.py +0 -0
  89. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/text.py +0 -0
  90. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/__init__.py +0 -0
  91. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/discovery.py +0 -0
  92. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/errors.py +0 -0
  93. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/loader.py +0 -0
  94. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/__init__.py +0 -0
  95. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/system.py +0 -0
  96. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/user.py +0 -0
  97. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/visual_studio.py +0 -0
  98. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/service.py +0 -0
  99. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/spec.py +0 -0
  100. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/__init__.py +0 -0
  101. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang.py +0 -0
  102. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang_cl.py +0 -0
  103. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangd.py +0 -0
  104. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangxx.py +0 -0
  105. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cmake.py +0 -0
  106. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cppvsdbg.py +0 -0
  107. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ctest.py +0 -0
  108. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gcc.py +0 -0
  109. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gdb.py +0 -0
  110. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/git.py +0 -0
  111. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gxx.py +0 -0
  112. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/link.py +0 -0
  113. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld.py +0 -0
  114. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld_link.py +0 -0
  115. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lldb_dap.py +0 -0
  116. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/make.py +0 -0
  117. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msbuild.py +0 -0
  118. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msvc.py +0 -0
  119. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ninja.py +0 -0
  120. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/__init__.py +0 -0
  121. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/diff.py +0 -0
  122. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/errors.py +0 -0
  123. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/metadata.py +0 -0
  124. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/models.py +0 -0
  125. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/path.py +0 -0
  126. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/providers.py +0 -0
  127. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/service.py +0 -0
  128. {forge_cpp_mcp-0.2.3 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forge-cpp-mcp
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: A safe MCP server for structured C++ project development workflows.
5
5
  Project-URL: Homepage, https://github.com/hardened-steel/ForgeMCP
6
6
  Project-URL: Repository, https://github.com/hardened-steel/ForgeMCP
@@ -44,7 +44,8 @@ debugger modules are planned.
44
44
  configuration selections use all available contexts.
45
45
  - No prompts are currently registered.
46
46
  - CMake tools list operator profiles, configure projects, build targets, and run
47
- CTest. Each tool has its own widget with profile filters, Fields/JSON views,
47
+ CTest. `cmake_run_test` directly runs a project/storage test executable with
48
+ literal command-line arguments. Each tool has its own widget with Fields/JSON views,
48
49
  copying and per-test results. Command logs are available through `process_get`. CMake syntax highlighting remains deferred.
49
50
  - Tool `processes_overview` shows running and completed external development tools,
50
51
  including exit codes and timeout interruptions. `process_get(process_id)` returns
@@ -75,7 +76,9 @@ without making additional tool calls. Process times are readable to seconds; JSO
75
76
  copying preserve the original precision. A toolsets list shows summaries, while a
76
77
  `toolset_get` result shows the details returned by that call.
77
78
 
78
- ForgeMCP does not expose arbitrary command execution over MCP. Feature services use
79
+ ForgeMCP can directly run an explicitly selected test executable inside the
80
+ project or storage root; it does not expose shell commands or external executable
81
+ paths over MCP. Feature services use
79
82
  the shared process service internally; its public MCP surface is read-only.
80
83
 
81
84
  ## Requirements
@@ -93,7 +96,7 @@ and install it in a virtual environment:
93
96
 
94
97
  ```powershell
95
98
  python -m venv .venv
96
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.3-py3-none-any.whl
99
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
97
100
  .\.venv\Scripts\forgemcp.exe --help
98
101
  ```
99
102
 
@@ -105,7 +108,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
105
108
  After the package is published to PyPI, the install command can instead be:
106
109
 
107
110
  ```powershell
108
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.3
111
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
109
112
  ```
110
113
 
111
114
  ## Setup from source
@@ -319,6 +322,36 @@ no source preview because explicit external reads require confirmation.
319
322
 
320
323
  ## CMake profiles
321
324
 
325
+ CMake owns which configurations can supply clangd compilation contexts. After a
326
+ successful configure/generate, ForgeMCP reads `cmakeFiles-v1` from a new CMake
327
+ File API reply and records SHA-256 hashes of that build directory's configuration
328
+ inputs in `project/` and `storage/`. Hashes and dependency lists live in memory;
329
+ restart the server and run `cmake_configure` to confirm contexts again. Existing
330
+ File API replies and `compile_commands.json` alone do not confirm current inputs.
331
+
332
+ Workspace writes, edits, moves, deletions, and directory creation check known
333
+ inputs at the operation boundary. Actual input changes withdraw affected contexts
334
+ and notify clangd through the CMake subscription; identical content and ordinary
335
+ source/header edits keep contexts available unless those files are CMake inputs.
336
+ Known preset files are treated conservatively without resolving preset inheritance
337
+ or includes. CONFIGURE_DEPENDS globs conservatively invalidate on structural
338
+ changes; older File API replies without glob information use this policy for all
339
+ configurations. No project-wide dependency scan or CMake/glob parser is used.
340
+
341
+ Run `cmake_configure` to restore outdated or unknown contexts. `cmake_build` is
342
+ also allowed for outdated configurations: a new successful File API generation
343
+ can restore them even when later compilation fails. A successful build without
344
+ regeneration does not refresh the input baseline. Failed configure/generate cannot
345
+ reactivate old artifacts, and invalidation leaves build trees and databases intact.
346
+ Unchanged contexts and database contents retain their clangd sessions.
347
+
348
+ Checks occur only during Workspace mutations, configure, and build. There are no
349
+ filesystem watchers, polling, or background scans, and no guarantee of detecting
350
+ external editor/process changes. External `root/` dependencies are omitted from
351
+ hash tracking without elicitation or reads; changes to those inputs require an
352
+ explicit configure. Native preset contexts still require a known managed build
353
+ directory and a compilation database.
354
+
322
355
  `cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
323
356
  operate on all profiles unless a nonempty `profiles` list selects a subset.
324
357
  Configuration must precede building, and building must precede testing. Profiles
@@ -332,6 +365,30 @@ Full process transcripts remain in the process module; CMake results contain no
332
365
  transcript links. The default command timeout is 600 seconds per execution and
333
366
  can be overridden with the existing `ProcessTimeout` shape (`total`, `idle`).
334
367
 
368
+ `cmake_run_test(executable, arguments=null, cwd="project/", timeout={"total":600})`
369
+ runs one already-built test executable directly, without a profile or CTest
370
+ registration. Executable and working-directory paths use `project/...` or
371
+ `storage/...` and must resolve inside their respective roots. The working directory
372
+ must exist; storage is not created automatically. On Windows, batch files are not
373
+ supported. Arguments are a list of literal strings, preserving spaces and empty
374
+ strings; NUL is rejected. For example:
375
+
376
+ ```json
377
+ {
378
+ "executable": "project/build/ninja-debug/fixture_tests.exe",
379
+ "arguments": ["--fail"],
380
+ "cwd": "project/build/ninja-debug",
381
+ "timeout": {"total": 30}
382
+ }
383
+ ```
384
+
385
+ The process inherits the server environment, closes stdin, and waits for completion.
386
+ It does not replay toolset/preset environments or CTest properties such as
387
+ `WILL_FAIL`, fixtures, or per-test timeouts. Exit zero means success; a nonzero
388
+ exit or process failure is reported in `error`. The single structured result
389
+ contains `executable`, `arguments`, `cwd`, `error`, and `process_id`; logs are read
390
+ through `process_get`. No JUnit or framework case report is inferred from output.
391
+
335
392
  With no explicit profiles:
336
393
 
337
394
  - If `CMakePresets.json` or `CMakeUserPresets.json` exists, the automatic `presets`
@@ -19,7 +19,8 @@ debugger modules are planned.
19
19
  configuration selections use all available contexts.
20
20
  - No prompts are currently registered.
21
21
  - CMake tools list operator profiles, configure projects, build targets, and run
22
- CTest. Each tool has its own widget with profile filters, Fields/JSON views,
22
+ CTest. `cmake_run_test` directly runs a project/storage test executable with
23
+ literal command-line arguments. Each tool has its own widget with Fields/JSON views,
23
24
  copying and per-test results. Command logs are available through `process_get`. CMake syntax highlighting remains deferred.
24
25
  - Tool `processes_overview` shows running and completed external development tools,
25
26
  including exit codes and timeout interruptions. `process_get(process_id)` returns
@@ -50,7 +51,9 @@ without making additional tool calls. Process times are readable to seconds; JSO
50
51
  copying preserve the original precision. A toolsets list shows summaries, while a
51
52
  `toolset_get` result shows the details returned by that call.
52
53
 
53
- ForgeMCP does not expose arbitrary command execution over MCP. Feature services use
54
+ ForgeMCP can directly run an explicitly selected test executable inside the
55
+ project or storage root; it does not expose shell commands or external executable
56
+ paths over MCP. Feature services use
54
57
  the shared process service internally; its public MCP surface is read-only.
55
58
 
56
59
  ## Requirements
@@ -68,7 +71,7 @@ and install it in a virtual environment:
68
71
 
69
72
  ```powershell
70
73
  python -m venv .venv
71
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.3-py3-none-any.whl
74
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
72
75
  .\.venv\Scripts\forgemcp.exe --help
73
76
  ```
74
77
 
@@ -80,7 +83,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
80
83
  After the package is published to PyPI, the install command can instead be:
81
84
 
82
85
  ```powershell
83
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.3
86
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
84
87
  ```
85
88
 
86
89
  ## Setup from source
@@ -294,6 +297,36 @@ no source preview because explicit external reads require confirmation.
294
297
 
295
298
  ## CMake profiles
296
299
 
300
+ CMake owns which configurations can supply clangd compilation contexts. After a
301
+ successful configure/generate, ForgeMCP reads `cmakeFiles-v1` from a new CMake
302
+ File API reply and records SHA-256 hashes of that build directory's configuration
303
+ inputs in `project/` and `storage/`. Hashes and dependency lists live in memory;
304
+ restart the server and run `cmake_configure` to confirm contexts again. Existing
305
+ File API replies and `compile_commands.json` alone do not confirm current inputs.
306
+
307
+ Workspace writes, edits, moves, deletions, and directory creation check known
308
+ inputs at the operation boundary. Actual input changes withdraw affected contexts
309
+ and notify clangd through the CMake subscription; identical content and ordinary
310
+ source/header edits keep contexts available unless those files are CMake inputs.
311
+ Known preset files are treated conservatively without resolving preset inheritance
312
+ or includes. CONFIGURE_DEPENDS globs conservatively invalidate on structural
313
+ changes; older File API replies without glob information use this policy for all
314
+ configurations. No project-wide dependency scan or CMake/glob parser is used.
315
+
316
+ Run `cmake_configure` to restore outdated or unknown contexts. `cmake_build` is
317
+ also allowed for outdated configurations: a new successful File API generation
318
+ can restore them even when later compilation fails. A successful build without
319
+ regeneration does not refresh the input baseline. Failed configure/generate cannot
320
+ reactivate old artifacts, and invalidation leaves build trees and databases intact.
321
+ Unchanged contexts and database contents retain their clangd sessions.
322
+
323
+ Checks occur only during Workspace mutations, configure, and build. There are no
324
+ filesystem watchers, polling, or background scans, and no guarantee of detecting
325
+ external editor/process changes. External `root/` dependencies are omitted from
326
+ hash tracking without elicitation or reads; changes to those inputs require an
327
+ explicit configure. Native preset contexts still require a known managed build
328
+ directory and a compilation database.
329
+
297
330
  `cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
298
331
  operate on all profiles unless a nonempty `profiles` list selects a subset.
299
332
  Configuration must precede building, and building must precede testing. Profiles
@@ -307,6 +340,30 @@ Full process transcripts remain in the process module; CMake results contain no
307
340
  transcript links. The default command timeout is 600 seconds per execution and
308
341
  can be overridden with the existing `ProcessTimeout` shape (`total`, `idle`).
309
342
 
343
+ `cmake_run_test(executable, arguments=null, cwd="project/", timeout={"total":600})`
344
+ runs one already-built test executable directly, without a profile or CTest
345
+ registration. Executable and working-directory paths use `project/...` or
346
+ `storage/...` and must resolve inside their respective roots. The working directory
347
+ must exist; storage is not created automatically. On Windows, batch files are not
348
+ supported. Arguments are a list of literal strings, preserving spaces and empty
349
+ strings; NUL is rejected. For example:
350
+
351
+ ```json
352
+ {
353
+ "executable": "project/build/ninja-debug/fixture_tests.exe",
354
+ "arguments": ["--fail"],
355
+ "cwd": "project/build/ninja-debug",
356
+ "timeout": {"total": 30}
357
+ }
358
+ ```
359
+
360
+ The process inherits the server environment, closes stdin, and waits for completion.
361
+ It does not replay toolset/preset environments or CTest properties such as
362
+ `WILL_FAIL`, fixtures, or per-test timeouts. Exit zero means success; a nonzero
363
+ exit or process failure is reported in `error`. The single structured result
364
+ contains `executable`, `arguments`, `cwd`, `error`, and `process_id`; logs are read
365
+ through `process_get`. No JUnit or framework case report is inferred from output.
366
+
310
367
  With no explicit profiles:
311
368
 
312
369
  - If `CMakePresets.json` or `CMakeUserPresets.json` exists, the automatic `presets`
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Test executable result</title>
7
+ </head>
8
+ <body>
9
+ <main id="widget" aria-label="Test executable result"></main>
10
+ <script type="module" src="/src/cmake-run-test.js"></script>
11
+ </body>
12
+ </html>
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "scripts": {
6
- "build": "vite build --mode workspace && vite build --mode file && vite build --mode search && vite build --mode result && vite build --mode process && vite build --mode process-details && vite build --mode toolsets && vite build --mode cmake-profiles && vite build --mode cmake-configure && vite build --mode cmake-build && vite build --mode cmake-test && vite build --mode clangd",
6
+ "build": "vite build --mode workspace && vite build --mode file && vite build --mode search && vite build --mode result && vite build --mode process && vite build --mode process-details && vite build --mode toolsets && vite build --mode cmake-profiles && vite build --mode cmake-configure && vite build --mode cmake-build && vite build --mode cmake-test && vite build --mode cmake-run-test && vite build --mode clangd",
7
7
  "test": "node --test tests/*.test.js"
8
8
  },
9
9
  "dependencies": {
@@ -0,0 +1,8 @@
1
+ import { connectWidget } from "./shared/app.js";
2
+ import { cmakePresentation, renderCMakeValue } from "./cmake-view.js";
3
+
4
+ await connectWidget({
5
+ toolName: "cmake_run_test",
6
+ describe: (data) => cmakePresentation(data, "cmake_run_test"),
7
+ renderValue: renderCMakeValue,
8
+ });
@@ -15,6 +15,7 @@ const widgets = {
15
15
  "cmake-configure": "cmake-configure",
16
16
  "cmake-build": "cmake-build",
17
17
  "cmake-test": "cmake-test",
18
+ "cmake-run-test": "cmake-run-test",
18
19
  clangd: "clangd-result",
19
20
  };
20
21
 
@@ -31,7 +31,7 @@ class CustomBuildHook(BuildHookInterface):
31
31
  Path(self.root) / "src" / "forgemcp" / "assets" / "clangd-result.html",
32
32
  *(
33
33
  Path(self.root) / "src" / "forgemcp" / "assets" / f"cmake-{kind}.html"
34
- for kind in ("profiles", "configure", "build", "test")
34
+ for kind in ("profiles", "configure", "build", "test", "run-test")
35
35
  ),
36
36
  ]
37
37
  artifacts = build_data.setdefault("artifacts", [])
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "forge-cpp-mcp"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "A safe MCP server for structured C++ project development workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.3"
5
+ __version__ = "0.2.5"
@@ -273,21 +273,32 @@ class ClangdService(ResultProvider[WorkspaceContext]):
273
273
  async def configurations(self) -> list[CompilationContext]:
274
274
  """Return independent copies of currently usable compilation contexts."""
275
275
  self.check_ready()
276
- return [context.model_copy(deep=True) for context in self.contexts.values()]
276
+ current = {context.id: context for context in await self.cmake.compilation_contexts()}
277
+ return [
278
+ context.model_copy(deep=True)
279
+ for context in self.contexts.values()
280
+ if current.get(context.id) == context
281
+ ]
277
282
 
278
283
  async def selection(self, configurations: Sequence[str]) -> list[CompilationContext]:
279
284
  """Validate requested configuration IDs, using all contexts for an empty selection."""
280
285
  contexts = await self.configurations()
281
- unknown = set(configurations) - self.contexts.keys()
286
+ unknown = set(configurations) - {context.id for context in contexts}
282
287
  if unknown:
283
- raise ClangdError(f"Unavailable clangd configurations: {', '.join(sorted(unknown))}.")
288
+ raise ClangdError(
289
+ f"Unavailable clangd configurations: {', '.join(sorted(unknown))}. "
290
+ "CMake inputs may be outdated or unconfirmed; run cmake_configure."
291
+ )
284
292
  selected = [
285
293
  context
286
294
  for context in contexts
287
295
  if not configurations or context.id in configurations
288
296
  ]
289
297
  if not selected:
290
- raise ClangdError("No CMake configuration has both a compilation database and clangd.")
298
+ raise ClangdError(
299
+ "No confirmed CMake configuration has both a compilation database and clangd. "
300
+ "CMake inputs may be outdated or unconfirmed; run cmake_configure."
301
+ )
291
302
  return selected
292
303
 
293
304
  @asynccontextmanager
@@ -867,7 +878,9 @@ class ClangdService(ResultProvider[WorkspaceContext]):
867
878
  databases = {context.compilation_database for context in self.contexts.values()}
868
879
  if any(change.path in databases for change in changes):
869
880
  await self.cmake.publish_configurations(refresh=True)
870
- await self.update_configurations(await self.cmake.compilation_contexts())
881
+ # CMake's provider may still be checking this mutation. It never waits
882
+ # for analysis while holding its operation lock, so this order is safe.
883
+ await self.update_configurations(await self.cmake.compilation_contexts())
871
884
  notifications: list[JsonValue] = [
872
885
  {
873
886
  "uri": self.workspace.resolve_workspace_path(change.path).as_uri(),
@@ -0,0 +1,67 @@
1
+ """Managed CMake build trees and direct test execution paths."""
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ from forgemcp.workspace.path import WorkspacePath
8
+
9
+ from .errors import CMakeError
10
+
11
+
12
+ def clear_build_directory(
13
+ directory: Path,
14
+ root: Path,
15
+ storage: Path,
16
+ protected: set[Path],
17
+ ) -> None:
18
+ """Remove an owned tree only after root, protected subtree, and link checks succeed."""
19
+ if (
20
+ directory == root
21
+ or not directory.resolve().is_relative_to(root)
22
+ or storage.is_relative_to(directory)
23
+ or any(path.is_relative_to(directory) for path in protected)
24
+ ):
25
+ raise CMakeError(
26
+ "Build directory contains a protected path or escaped its root."
27
+ )
28
+
29
+ def raise_walk_error(error: OSError) -> None:
30
+ """Reject incomplete pre-removal inspection rather than ignoring traversal failures."""
31
+ raise error
32
+
33
+ try:
34
+ if not directory.exists():
35
+ return
36
+ for parent, directories, files in os.walk(
37
+ directory,
38
+ followlinks=False,
39
+ onerror=raise_walk_error,
40
+ ):
41
+ for name in (*directories, *files):
42
+ item = Path(parent) / name
43
+ if item.is_symlink() or item.is_junction():
44
+ raise CMakeError("Build directory contains a link or junction.")
45
+ if not directory.resolve().is_relative_to(root):
46
+ raise CMakeError("Build directory escaped its root.")
47
+ shutil.rmtree(directory)
48
+ except OSError as error:
49
+ raise CMakeError("Cannot clear build directory.") from error
50
+
51
+
52
+ def execution_path(reference: WorkspacePath, project: Path, storage: Path) -> Path:
53
+ """Resolve a test executable or working directory without escaping its managed root."""
54
+ if reference.area == "root":
55
+ raise CMakeError("Test execution paths must be inside project/ or storage/.")
56
+ base = project if reference.area == "project" else storage
57
+ try:
58
+ if os.name == "nt" and any(
59
+ os.path.isreserved(part) for part in Path(reference.relative).parts
60
+ ):
61
+ raise CMakeError(f"Test execution path contains a reserved name: {reference}.")
62
+ path = (base / reference.relative).resolve()
63
+ if not path.is_relative_to(base):
64
+ raise CMakeError(f"Test execution path resolves outside its root: {reference}.")
65
+ return path
66
+ except OSError as error:
67
+ raise CMakeError(f"Cannot resolve test execution path: {reference}.") from error
@@ -0,0 +1,5 @@
1
+ """Expected CMake configuration and operation errors."""
2
+
3
+
4
+ class CMakeError(Exception):
5
+ """Invalid profiles, execution paths, unavailable tools, or unusable build metadata."""
@@ -0,0 +1,229 @@
1
+ """Read bounded CMake-owned replies and fingerprint managed configuration inputs."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from forgemcp.workspace.path import WorkspacePath
11
+
12
+ from .errors import CMakeError
13
+
14
+ MAX_REPLY_BYTES = 16 * 1024 * 1024
15
+ MAX_INPUTS = 100_000
16
+
17
+
18
+ @dataclass
19
+ class WorkspaceChange:
20
+ """Known input hashes and path kinds needed to verify one controlled mutation."""
21
+
22
+ inputs: dict[WorkspacePath, str | None]
23
+ kinds: dict[WorkspacePath, str]
24
+
25
+
26
+ @dataclass
27
+ class ConfigurationInputs:
28
+ """One generation's input hashes and conservative structural invalidation policy."""
29
+
30
+ checksums: dict[WorkspacePath, str | None] = field(default_factory=dict)
31
+ structural: bool = False
32
+ current: bool = False
33
+
34
+ def verify(self, project: Path, storage: Path) -> None:
35
+ """Withdraw a confirmed baseline when an input changes or cannot safely be read."""
36
+ if not self.current:
37
+ return
38
+ try:
39
+ self.current = self.checksums == input_checksums(
40
+ self.checksums,
41
+ project,
42
+ storage,
43
+ )
44
+ except (CMakeError, OSError):
45
+ self.current = False
46
+
47
+
48
+ def managed_path(path: Path, project: Path, storage: Path) -> WorkspacePath | None:
49
+ """Qualify a path only when both its lexical and resolved targets stay managed."""
50
+ for area, base in (("storage", storage), ("project", project)):
51
+ if path.is_relative_to(base) and path.resolve().is_relative_to(base):
52
+ relative = path.relative_to(base).as_posix()
53
+ return WorkspacePath(f"{area}/{'' if relative == '.' else relative}")
54
+ return None
55
+
56
+
57
+ def checksum(path: Path) -> str | None:
58
+ """Hash a file incrementally, representing a missing input without retaining contents."""
59
+ try:
60
+ with path.open("rb") as stream:
61
+ return hashlib.file_digest(stream, "sha256").hexdigest()
62
+ except FileNotFoundError:
63
+ return None
64
+
65
+
66
+ def read_reply(path: Path, project: Path, storage: Path) -> dict:
67
+ """Read a size-bounded managed JSON reply without following references outside the roots."""
68
+ if managed_path(path, project, storage) is None:
69
+ raise CMakeError("CMake File API reply is outside managed roots.")
70
+ with path.open("rb") as stream:
71
+ data = stream.read(MAX_REPLY_BYTES + 1)
72
+ if len(data) > MAX_REPLY_BYTES:
73
+ raise CMakeError("CMake File API reply exceeds the size limit.")
74
+ value = json.loads(data)
75
+ if not isinstance(value, dict):
76
+ raise CMakeError("Expected a CMake File API object.")
77
+ return value
78
+
79
+
80
+ def generation(directory: Path) -> str | None:
81
+ """Identify the latest successful index, rejecting a newer CMake 4.1 error index."""
82
+ try:
83
+ reply = directory / ".cmake/api/v1/reply"
84
+ current = reply
85
+ while current != directory:
86
+ if current.is_symlink() or current.is_junction():
87
+ raise CMakeError("CMake File API cannot traverse links or junctions.")
88
+ current = current.parent
89
+ if not reply.exists():
90
+ return None
91
+ indexes = []
92
+ errors = []
93
+ for count, path in enumerate(reply.iterdir()):
94
+ if count >= MAX_INPUTS:
95
+ raise CMakeError("Too many CMake File API reply files.")
96
+ if path.name.startswith("index-") and path.suffix == ".json":
97
+ indexes.append(path.name)
98
+ elif path.name.startswith("error-") and path.suffix == ".json":
99
+ errors.append(path.name)
100
+ latest = max(indexes, default=None)
101
+ if latest is None or (errors and max(errors)[6:] >= latest[6:]):
102
+ return None
103
+ return latest
104
+ except OSError as error:
105
+ raise CMakeError("Cannot inspect CMake File API generation.") from error
106
+
107
+
108
+ def read_inputs(
109
+ directory: Path,
110
+ index: str,
111
+ project: Path,
112
+ storage: Path,
113
+ ) -> ConfigurationInputs:
114
+ """Follow cmakeFiles-v1 from this generation and hash only project/storage inputs."""
115
+ try:
116
+ reply = directory / ".cmake/api/v1/reply"
117
+ reference = read_reply(reply / index, project, storage)["reply"][
118
+ "client-forgemcp"
119
+ ]["cmakeFiles-v1"]
120
+ filename = reference["jsonFile"]
121
+ if not isinstance(filename, str) or Path(filename).name != filename:
122
+ raise CMakeError("Invalid CMake File API reply reference.")
123
+ files = read_reply(reply / filename, project, storage)
124
+ if files["kind"] != "cmakeFiles" or files["version"]["major"] != 1:
125
+ raise CMakeError("Unsupported CMake configuration inputs reply.")
126
+ if (
127
+ Path(files["paths"]["source"]).resolve() != project
128
+ or Path(files["paths"]["build"]).resolve() != directory.resolve()
129
+ ):
130
+ raise CMakeError(
131
+ "CMake File API reply belongs to a different source/build tree."
132
+ )
133
+ inputs = files["inputs"]
134
+ if not isinstance(inputs, list) or not inputs or len(inputs) > MAX_INPUTS:
135
+ raise CMakeError("Invalid or excessive CMake configuration inputs.")
136
+ checksums = {}
137
+ for item in inputs:
138
+ native = Path(item["path"])
139
+ if not native.is_absolute():
140
+ native = project / native
141
+ native = Path(os.path.abspath(native))
142
+ qualified = managed_path(native, project, storage)
143
+ if qualified is not None:
144
+ checksums[qualified] = checksum(native)
145
+ for filename in ("CMakePresets.json", "CMakeUserPresets.json"):
146
+ native = project / filename
147
+ qualified = managed_path(native, project, storage)
148
+ if qualified is not None:
149
+ checksums[qualified] = checksum(native)
150
+ # Older replies cannot tell us whether CONFIGURE_DEPENDS globs exist.
151
+ return ConfigurationInputs(
152
+ checksums,
153
+ structural=files["version"].get("minor", 0) < 1
154
+ or bool(files.get("globsDependent")),
155
+ current=True,
156
+ )
157
+ except (OSError, ValueError, KeyError, TypeError) as error:
158
+ raise CMakeError(
159
+ "Cannot confirm CMake configuration inputs; run cmake_configure."
160
+ ) from error
161
+
162
+
163
+ def input_path(reference: WorkspacePath, project: Path, storage: Path) -> Path:
164
+ """Resolve an input within its selected managed root without accessing external targets."""
165
+ if reference.area == "root":
166
+ raise CMakeError("Configuration inputs must be managed paths.")
167
+ base = project if reference.area == "project" else storage
168
+ path = base / reference.relative
169
+ if not path.resolve().is_relative_to(base):
170
+ raise CMakeError(f"Configuration input escaped its root: {reference}.")
171
+ return path
172
+
173
+
174
+ def input_checksums(
175
+ paths: Iterable[WorkspacePath],
176
+ project: Path,
177
+ storage: Path,
178
+ ) -> dict[WorkspacePath, str | None]:
179
+ """Capture only hashes for the supplied managed input names."""
180
+ return {path: checksum(input_path(path, project, storage)) for path in paths}
181
+
182
+
183
+ def path_kind(path: Path) -> str:
184
+ """Capture a mutation target's existence and kind without traversing its subtree."""
185
+ if path.is_dir():
186
+ return "directory"
187
+ return "file" if path.exists() else "missing"
188
+
189
+
190
+ def prepare_queries(query: Path) -> None:
191
+ """Create owned File API queries in a validated build-tree directory."""
192
+ try:
193
+ query.mkdir(parents=True, exist_ok=True)
194
+ for name in ("codemodel-v2", "cache-v2", "toolchains-v1", "cmakeFiles-v1"):
195
+ target = query / name
196
+ if target.is_symlink() or target.is_junction():
197
+ raise CMakeError("CMake File API queries cannot be links.")
198
+ target.write_text("", encoding="utf-8")
199
+ except OSError as error:
200
+ raise CMakeError("Cannot prepare CMake File API queries.") from error
201
+
202
+
203
+ def compilation_database(
204
+ directory: WorkspacePath,
205
+ native: Path,
206
+ ) -> WorkspacePath | None:
207
+ """Return the qualified compilation database name when that build tree contains one."""
208
+ path = native / "compile_commands.json"
209
+ return (
210
+ WorkspacePath(f"{directory}/compile_commands.json") if path.is_file() else None
211
+ )
212
+
213
+
214
+ def read_generation(
215
+ directory: Path,
216
+ previous: str | None,
217
+ project: Path,
218
+ storage: Path,
219
+ ) -> ConfigurationInputs | None:
220
+ """Read inputs only when a command produced a new successful File API index."""
221
+ try:
222
+ latest = generation(directory)
223
+ if latest is None or latest == previous:
224
+ return None
225
+ return read_inputs(directory, latest, project, storage)
226
+ except OSError as error:
227
+ raise CMakeError(
228
+ "Cannot inspect CMake generation; run cmake_configure."
229
+ ) from error