forge-cpp-mcp 0.2.4__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 (127) hide show
  1. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/PKG-INFO +33 -3
  2. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/README.md +32 -2
  3. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/pyproject.toml +1 -1
  4. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/__init__.py +1 -1
  5. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/service.py +18 -5
  6. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/build_tree.py +67 -0
  7. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/file_api.py +229 -0
  8. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/models.py +17 -0
  9. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/profiles.py +21 -0
  10. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/service.py +218 -200
  11. forge_cpp_mcp-0.2.5/src/forgemcp/cmake/workspace.py +122 -0
  12. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/server.py +2 -0
  13. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/.gitignore +0 -0
  14. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/LICENSE +0 -0
  15. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/clangd-result.html +0 -0
  16. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-build.html +0 -0
  17. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-configure.html +0 -0
  18. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-profiles.html +0 -0
  19. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-run-test.html +0 -0
  20. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/cmake-test.html +0 -0
  21. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/package-lock.json +0 -0
  22. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/package.json +0 -0
  23. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/process-details.html +0 -0
  24. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/process-overview.html +0 -0
  25. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-navigation-view.js +0 -0
  26. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-result.js +0 -0
  27. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/clangd-view.js +0 -0
  28. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-build.js +0 -0
  29. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-configure.js +0 -0
  30. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-profiles.js +0 -0
  31. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-run-test.js +0 -0
  32. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-test.js +0 -0
  33. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/cmake-view.js +0 -0
  34. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process-details.js +0 -0
  35. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process-overview.js +0 -0
  36. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process-status.js +0 -0
  37. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/process.css +0 -0
  38. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/app.js +0 -0
  39. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/copy-icon.js +0 -0
  40. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/presentation.js +0 -0
  41. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-resources.js +0 -0
  42. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/result-view.js +0 -0
  43. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/shared/widget.css +0 -0
  44. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/source-view.js +0 -0
  45. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/toolsets.js +0 -0
  46. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-file.js +0 -0
  47. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-result.js +0 -0
  48. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-search.js +0 -0
  49. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-tree.js +0 -0
  50. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/src/workspace-view.js +0 -0
  51. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/tests/result-resources.test.js +0 -0
  52. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/tests/result-view.test.js +0 -0
  53. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/tests/workspace.test.js +0 -0
  54. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/toolsets.html +0 -0
  55. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/vite.config.js +0 -0
  56. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-file.html +0 -0
  57. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-result.html +0 -0
  58. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-search.html +0 -0
  59. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/frontend/workspace-tree.html +0 -0
  60. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/hatch_build.py +0 -0
  61. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/assets.py +0 -0
  62. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/__init__.py +0 -0
  63. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/errors.py +0 -0
  64. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/models.py +0 -0
  65. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/session.py +0 -0
  66. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/clangd/text.py +0 -0
  67. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/__init__.py +0 -0
  68. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/errors.py +0 -0
  69. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/cmake/text.py +0 -0
  70. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/completion.py +0 -0
  71. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.LICENSE +0 -0
  72. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/clangd.svg +0 -0
  73. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/cmake.svg +0 -0
  74. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/process.svg +0 -0
  75. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/toolchain.svg +0 -0
  76. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-edit.svg +0 -0
  77. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-file.svg +0 -0
  78. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace-search.svg +0 -0
  79. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/icons/workspace.svg +0 -0
  80. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/markdown.py +0 -0
  81. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/__init__.py +0 -0
  82. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/errors.py +0 -0
  83. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/models.py +0 -0
  84. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/service.py +0 -0
  85. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/text.py +0 -0
  86. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/process/transcript.py +0 -0
  87. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/progress.py +0 -0
  88. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/text.py +0 -0
  89. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/__init__.py +0 -0
  90. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/discovery.py +0 -0
  91. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/errors.py +0 -0
  92. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/loader.py +0 -0
  93. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/__init__.py +0 -0
  94. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/system.py +0 -0
  95. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/user.py +0 -0
  96. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/providers/visual_studio.py +0 -0
  97. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/service.py +0 -0
  98. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/spec.py +0 -0
  99. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/__init__.py +0 -0
  100. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang.py +0 -0
  101. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clang_cl.py +0 -0
  102. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangd.py +0 -0
  103. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/clangxx.py +0 -0
  104. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cmake.py +0 -0
  105. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/cppvsdbg.py +0 -0
  106. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ctest.py +0 -0
  107. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gcc.py +0 -0
  108. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gdb.py +0 -0
  109. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/git.py +0 -0
  110. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/gxx.py +0 -0
  111. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/link.py +0 -0
  112. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld.py +0 -0
  113. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lld_link.py +0 -0
  114. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/lldb_dap.py +0 -0
  115. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/make.py +0 -0
  116. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msbuild.py +0 -0
  117. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/msvc.py +0 -0
  118. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/toolchain/tools/ninja.py +0 -0
  119. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/__init__.py +0 -0
  120. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/diff.py +0 -0
  121. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/errors.py +0 -0
  122. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/metadata.py +0 -0
  123. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/models.py +0 -0
  124. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/path.py +0 -0
  125. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/providers.py +0 -0
  126. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.2.5}/src/forgemcp/workspace/service.py +0 -0
  127. {forge_cpp_mcp-0.2.4 → 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.4
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
@@ -96,7 +96,7 @@ and install it in a virtual environment:
96
96
 
97
97
  ```powershell
98
98
  python -m venv .venv
99
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.4-py3-none-any.whl
99
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
100
100
  .\.venv\Scripts\forgemcp.exe --help
101
101
  ```
102
102
 
@@ -108,7 +108,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
108
108
  After the package is published to PyPI, the install command can instead be:
109
109
 
110
110
  ```powershell
111
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.4
111
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
112
112
  ```
113
113
 
114
114
  ## Setup from source
@@ -322,6 +322,36 @@ no source preview because explicit external reads require confirmation.
322
322
 
323
323
  ## CMake profiles
324
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
+
325
355
  `cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
326
356
  operate on all profiles unless a nonempty `profiles` list selects a subset.
327
357
  Configuration must precede building, and building must precede testing. Profiles
@@ -71,7 +71,7 @@ and install it in a virtual environment:
71
71
 
72
72
  ```powershell
73
73
  python -m venv .venv
74
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.4-py3-none-any.whl
74
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
75
75
  .\.venv\Scripts\forgemcp.exe --help
76
76
  ```
77
77
 
@@ -83,7 +83,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
83
83
  After the package is published to PyPI, the install command can instead be:
84
84
 
85
85
  ```powershell
86
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.4
86
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
87
87
  ```
88
88
 
89
89
  ## Setup from source
@@ -297,6 +297,36 @@ no source preview because explicit external reads require confirmation.
297
297
 
298
298
  ## CMake profiles
299
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
+
300
330
  `cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
301
331
  operate on all profiles unless a nonempty `profiles` list selects a subset.
302
332
  Configuration must precede building, and building must precede testing. Profiles
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "forge-cpp-mcp"
7
- version = "0.2.4"
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.4"
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,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
@@ -1,6 +1,7 @@
1
1
  """CMake profiles and typed execution results shared with analysis and presentation."""
2
2
 
3
3
  from typing import Literal
4
+ from urllib.parse import quote
4
5
 
5
6
  from pydantic import BaseModel, Field
6
7
 
@@ -22,6 +23,22 @@ class CMakeProfile(BaseModel):
22
23
  generator: str | None = None
23
24
 
24
25
 
26
+ @property
27
+ def compilation_context(self) -> "CompilationContext | None":
28
+ """Derive a candidate identity only for an unambiguous managed build directory."""
29
+ if self.build_directory is None or len(self.configure_presets) > 1:
30
+ return None
31
+ identifier = quote(self.name, safe="")
32
+ if self.configure_presets:
33
+ identifier += "/" + quote(self.configure_presets[0], safe="")
34
+ return CompilationContext(
35
+ id=identifier,
36
+ toolset_id=self.toolset_id,
37
+ build_directory=self.build_directory,
38
+ compilation_database=WorkspacePath(f"{self.build_directory}/compile_commands.json"),
39
+ )
40
+
41
+
25
42
  class CompilationContext(BaseModel):
26
43
  """A successful build configuration with its toolset and compilation database."""
27
44
 
@@ -96,3 +96,24 @@ def parse_profiles(
96
96
  )
97
97
  profiles.append(profile)
98
98
  return tuple(profiles)
99
+
100
+
101
+ def require_configuration(
102
+ cache: dict[str, str],
103
+ configuration: str | None,
104
+ ) -> None:
105
+ """Validate the requested configuration against single- or multi-config cache settings."""
106
+ choices = cache.get("CMAKE_CONFIGURATION_TYPES", "").split(";")
107
+ if choices != [""] and configuration not in choices:
108
+ raise CMakeError(
109
+ "Select an available multi-config configuration in the operator "
110
+ f"profile or preset: {', '.join(choices)}."
111
+ )
112
+ if (
113
+ choices == [""]
114
+ and configuration is not None
115
+ and cache.get("CMAKE_BUILD_TYPE") != configuration
116
+ ):
117
+ raise CMakeError(
118
+ "The build directory has a different configuration; configure this profile first."
119
+ )