forge-cpp-mcp 0.2.1__tar.gz → 0.2.3__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 (123) hide show
  1. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/PKG-INFO +55 -13
  2. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/README.md +53 -12
  3. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/shared/app.js +2 -1
  4. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/shared/result-resources.js +14 -0
  5. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/shared/result-view.js +1 -1
  6. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/workspace-search.js +1 -1
  7. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/workspace-view.js +12 -7
  8. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/hatch_build.py +1 -0
  9. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/pyproject.toml +2 -1
  10. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/__init__.py +1 -1
  11. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/assets.py +3 -0
  12. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/clangd/errors.py +1 -0
  13. forge_cpp_mcp-0.2.3/src/forgemcp/clangd/models.py +186 -0
  14. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/clangd/service.py +204 -110
  15. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/clangd/session.py +72 -44
  16. forge_cpp_mcp-0.2.3/src/forgemcp/clangd/text.py +244 -0
  17. forge_cpp_mcp-0.2.3/src/forgemcp/cmake/models.py +74 -0
  18. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/cmake/profiles.py +3 -0
  19. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/cmake/service.py +43 -67
  20. forge_cpp_mcp-0.2.3/src/forgemcp/cmake/text.py +98 -0
  21. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/completion.py +4 -0
  22. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/markdown.py +44 -0
  23. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/process/models.py +4 -0
  24. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/process/service.py +36 -18
  25. forge_cpp_mcp-0.2.3/src/forgemcp/process/text.py +68 -0
  26. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/process/transcript.py +2 -0
  27. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/progress.py +3 -0
  28. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/server.py +1 -0
  29. forge_cpp_mcp-0.2.3/src/forgemcp/text.py +37 -0
  30. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/discovery.py +1 -0
  31. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/errors.py +1 -0
  32. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/loader.py +3 -0
  33. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/providers/system.py +2 -0
  34. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/providers/user.py +5 -0
  35. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/providers/visual_studio.py +5 -0
  36. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/service.py +55 -4
  37. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/spec.py +7 -0
  38. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/clang.py +4 -0
  39. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/clang_cl.py +4 -0
  40. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/clangd.py +26 -1
  41. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/clangxx.py +4 -0
  42. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/cmake.py +35 -3
  43. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/cppvsdbg.py +2 -1
  44. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/ctest.py +16 -1
  45. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/gcc.py +4 -0
  46. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/gdb.py +4 -0
  47. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/git.py +4 -0
  48. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/gxx.py +4 -0
  49. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/link.py +2 -1
  50. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/lld.py +4 -0
  51. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/lld_link.py +4 -0
  52. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/lldb_dap.py +2 -1
  53. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/make.py +4 -0
  54. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/msbuild.py +4 -0
  55. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/msvc.py +2 -1
  56. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/ninja.py +4 -0
  57. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/diff.py +14 -0
  58. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/metadata.py +1 -0
  59. forge_cpp_mcp-0.2.3/src/forgemcp/workspace/models.py +103 -0
  60. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/path.py +18 -1
  61. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/providers.py +6 -0
  62. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/service.py +174 -108
  63. forge_cpp_mcp-0.2.3/src/forgemcp/workspace/text.py +113 -0
  64. forge_cpp_mcp-0.2.1/src/forgemcp/clangd/models.py +0 -94
  65. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/.gitignore +0 -0
  66. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/LICENSE +0 -0
  67. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/clangd-result.html +0 -0
  68. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/cmake-build.html +0 -0
  69. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/cmake-configure.html +0 -0
  70. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/cmake-profiles.html +0 -0
  71. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/cmake-test.html +0 -0
  72. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/package-lock.json +0 -0
  73. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/package.json +0 -0
  74. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/process-details.html +0 -0
  75. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/process-overview.html +0 -0
  76. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/clangd-navigation-view.js +0 -0
  77. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/clangd-result.js +0 -0
  78. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/clangd-view.js +0 -0
  79. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/cmake-build.js +0 -0
  80. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/cmake-configure.js +0 -0
  81. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/cmake-profiles.js +0 -0
  82. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/cmake-test.js +0 -0
  83. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/cmake-view.js +0 -0
  84. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/process-details.js +0 -0
  85. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/process-overview.js +0 -0
  86. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/process-status.js +0 -0
  87. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/process.css +0 -0
  88. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/shared/copy-icon.js +0 -0
  89. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/shared/presentation.js +0 -0
  90. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/shared/widget.css +0 -0
  91. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/source-view.js +0 -0
  92. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/toolsets.js +0 -0
  93. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/workspace-file.js +0 -0
  94. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/workspace-result.js +0 -0
  95. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/src/workspace-tree.js +0 -0
  96. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/tests/result-resources.test.js +0 -0
  97. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/tests/result-view.test.js +0 -0
  98. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/tests/workspace.test.js +0 -0
  99. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/toolsets.html +0 -0
  100. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/vite.config.js +0 -0
  101. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/workspace-file.html +0 -0
  102. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/workspace-result.html +0 -0
  103. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/workspace-search.html +0 -0
  104. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/frontend/workspace-tree.html +0 -0
  105. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/clangd/__init__.py +0 -0
  106. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/cmake/__init__.py +0 -0
  107. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/cmake/errors.py +0 -0
  108. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/clangd.LICENSE +0 -0
  109. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/clangd.svg +0 -0
  110. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/cmake.svg +0 -0
  111. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/process.svg +0 -0
  112. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/toolchain.svg +0 -0
  113. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/workspace-edit.svg +0 -0
  114. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/workspace-file.svg +0 -0
  115. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/workspace-search.svg +0 -0
  116. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/icons/workspace.svg +0 -0
  117. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/process/__init__.py +0 -0
  118. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/process/errors.py +0 -0
  119. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/__init__.py +0 -0
  120. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/providers/__init__.py +0 -0
  121. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/toolchain/tools/__init__.py +0 -0
  122. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/__init__.py +0 -0
  123. {forge_cpp_mcp-0.2.1 → forge_cpp_mcp-0.2.3}/src/forgemcp/workspace/errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forge-cpp-mcp
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -10,6 +10,7 @@ Author: ForgeMCP contributors
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
12
12
  Requires-Python: >=3.13
13
+ Requires-Dist: markdown-it-py>=4.0
13
14
  Requires-Dist: mcp[cli]==2.1.1
14
15
  Requires-Dist: pydantic>=2.13
15
16
  Requires-Dist: regex>=2024.11.6
@@ -56,8 +57,12 @@ debugger modules are planned.
56
57
  toolsets as markdown, querying available versions for details, with completion
57
58
  for retained toolset IDs.
58
59
 
59
- The tools remain useful in clients without MCP Apps support because the Python SDK
60
- serializes their typed results into both text `content` and `structuredContent`.
60
+ Every tool returns readable English plain text in `content` alongside its typed
61
+ `structuredContent`. Trees use branch characters, source excerpts use line numbers,
62
+ and diagnostics use compiler-style messages. Text does not include Markdown
63
+ formatting or Workspace provider resource links; the structured data retains those
64
+ links for widgets. Clients decide how to display the text and may prefer structured
65
+ data when supplying results to a model.
61
66
 
62
67
  All widgets share a compact console style with a fixed 420px height and adapt to the
63
68
  host width. The move confirmation uses only two rows, source and destination.
@@ -88,7 +93,7 @@ and install it in a virtual environment:
88
93
 
89
94
  ```powershell
90
95
  python -m venv .venv
91
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.1-py3-none-any.whl
96
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.3-py3-none-any.whl
92
97
  .\.venv\Scripts\forgemcp.exe --help
93
98
  ```
94
99
 
@@ -100,7 +105,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
100
105
  After the package is published to PyPI, the install command can instead be:
101
106
 
102
107
  ```powershell
103
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.1
108
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.3
104
109
  ```
105
110
 
106
111
  ## Setup from source
@@ -163,13 +168,27 @@ add the `forgemcp` entry to it rather than replacing the file. Restart the clien
163
168
  after editing its configuration. Check the connection with `codex mcp list` or
164
169
  `claude mcp get forgemcp`, respectively.
165
170
 
171
+ To require agents to use ForgeMCP for supported operations, append the
172
+ [example agent instructions](examples/mcp-clients/agent-instructions.md) to your
173
+ project's `AGENTS.md` (Codex) or `CLAUDE.md` (Claude Code). The same block works
174
+ for both clients and requires evidence of an unsupported operation, tool failure,
175
+ or unavailability before using alternatives. Preserve existing project instructions.
176
+
166
177
  ## Workspace files and storage
167
178
 
168
179
  All file tools use string paths such as `project/src/main.cpp` and
169
180
  `storage/build/debug`. Directory tools default to `project/`; the separate tool
170
181
  argument `root` has been removed. Python consumers use the shared `WorkspacePath`
171
- type, which serializes as the same string. Storage defaults to `.<project-name>.forgemcp` beside the
172
- project. Set its location explicitly when needed:
182
+ type, which serializes as the same string. Tool path arguments also accept ordinary
183
+ relative paths: `src/main.cpp` and `./src/main.cpp` both mean `project/src/main.cpp`,
184
+ and `./` means `project/`. One trailing `/` is accepted for managed paths:
185
+ `project/src/` becomes `project/src`. Results always use canonical qualified paths.
186
+ The prefixes `project/`, `storage/`, and `root/` are reserved; use `./storage/...`
187
+ to access a project directory named `storage`. Absolute paths, `..`, and repeated
188
+ separators remain invalid; external locations require an explicit `root/` prefix.
189
+
190
+ Storage defaults to `.<project-name>.forgemcp` beside the project. Set its location
191
+ explicitly when needed:
173
192
 
174
193
  ```powershell
175
194
  forgemcp --workspace C:\Projects\Example --workspace-storage D:\ForgeMCP\Example
@@ -192,7 +211,7 @@ a working-directory check, not an operating-system sandbox.
192
211
  | `workspace_find_files(pattern="*", path="project/")` | Recursive filename/path glob search |
193
212
  | `workspace_file_info(path)` | Creation/modification times, byte size, owner; unavailable metadata is null |
194
213
  | `workspace_read_file(path, start_line=1, end_line=null)` | UTF-8 text, with an optional inclusive line range |
195
- | `workspace_search(query, path="project/", regex=false, extensions=null, case_sensitive=true)` | Matching lines and skipped binary/non-UTF-8 files |
214
+ | `workspace_text_search(query, path="project/", regex=false, extensions=null, case_sensitive=true, max_matches=100)` | Bounded matching lines, exact counts, and a linked skipped-file list |
196
215
  | `workspace_write_file(path, text)` | Create or overwrite; return removed/added line counts |
197
216
  | `workspace_edit_file(path, old_text, new_text, replace_all=false)` | Exact replacement; zero or ambiguous matches fail without modifying the file |
198
217
  | `workspace_move(source, destination)` | Move a file or directory; destination must not exist |
@@ -209,8 +228,15 @@ dot-prefixed files remain visible. Searches skip directories beginning with a do
209
228
  access hidden directories and in-root links. Mutations cannot traverse links;
210
229
  moving/removing a whole tree containing links is rejected. Dependent modules may
211
230
  register protected paths, which remain readable but cannot be changed through the
212
- workspace API. Searches intentionally have no application-level timeout, result
213
- limit, or pagination in this iteration.
231
+ workspace API. Searches have no application-level timeout or pagination. Text search
232
+ returns at most `max_matches` matching lines (positive integer, default 100), with
233
+ all spans on each returned line. It continues scanning to return exact
234
+ `matches_count` and `skipped_files_count`; `matches_truncated` indicates omitted
235
+ matching lines. The complete binary/non-UTF-8 file list is an immutable JSON resource
236
+ linked as `resources.skipped_files` when nonempty, rather than inline output.
237
+ The widget loads that snapshot for its skipped-files tab. Narrow the path/extensions
238
+ or increase `max_matches` to retrieve more matching lines. Individual lines and their
239
+ span arrays have no byte limit.
214
240
 
215
241
  File resource templates use a single path including its project/ or storage/ prefix:
216
242
 
@@ -220,7 +246,7 @@ forgemcp://workspace/raw{/path*}
220
246
  forgemcp://workspace/list{?path,depth,include_hidden}
221
247
  forgemcp://workspace/find-files{?pattern,path}
222
248
  forgemcp://workspace/file-info{?path}
223
- forgemcp://workspace/search{?query,path,regex,extensions,case_sensitive}
249
+ forgemcp://workspace/search{?query,path,regex,extensions,case_sensitive,max_matches}
224
250
  forgemcp://workspace/results/{result_id}/{name}.json
225
251
  forgemcp://workspace/results/{result_id}/{name}.md
226
252
  ```
@@ -254,10 +280,26 @@ A resource-loading failure does not change the successful file-operation outcome
254
280
  When a CMake configuration has a compilation database and clangd in its toolset,
255
281
  Workspace reads, writes, and edits of C/C++ files also return
256
282
  `resources.clangd`. This immutable JSON contains diagnostics and semantic
257
- highlighting grouped by configuration. Workspace mutations synchronize retained
258
- clangd sessions; changing a header refreshes already opened dependent files.
283
+ highlighting grouped by configuration, always across all available configurations.
284
+ Clangd analyzes the complete file even for a partial Workspace read, but the saved
285
+ diagnostics and semantic spans include only those intersecting the lines actually
286
+ returned. Their coordinates remain absolute file positions; an empty partial-read
287
+ excerpt (for example, beyond EOF) has no annotations.
288
+
259
289
  Clangd starts sessions from CMake's configuration subscription and keeps them until
260
290
  the configuration disappears, its database changes, or the server stops.
291
+ Each process receives `-j=2`; this worker setting does not impose a memory limit.
292
+ File analysis opens the complete current file, collects the requested result, and
293
+ closes the document before returning. Documents are not retained between calls, so
294
+ the next analysis incorporates saved header changes without reopening other files
295
+ after each Workspace mutation. Repeated calls pay the cost of a fresh file parse;
296
+ closing a document does not release clangd's whole index. Workspace mutations still
297
+ notify sessions of filesystem changes and reconcile changed compilation databases.
298
+ Explicit clangd tools use their existing `configurations` argument to select a
299
+ subset, for example
300
+ `clangd_diagnostics(path="project/src/main.cpp", configurations=["debug"])`.
301
+ Omitting that argument or passing `[]` selects all available configurations.
302
+
261
303
  Workspace widgets load both resources independently. The source view lets you
262
304
  select a configuration for semantic highlighting and inline diagnostic markers;
263
305
  lexical C/C++ colors also cover keywords, comments, literals, directives, and nested
@@ -32,8 +32,12 @@ debugger modules are planned.
32
32
  toolsets as markdown, querying available versions for details, with completion
33
33
  for retained toolset IDs.
34
34
 
35
- The tools remain useful in clients without MCP Apps support because the Python SDK
36
- serializes their typed results into both text `content` and `structuredContent`.
35
+ Every tool returns readable English plain text in `content` alongside its typed
36
+ `structuredContent`. Trees use branch characters, source excerpts use line numbers,
37
+ and diagnostics use compiler-style messages. Text does not include Markdown
38
+ formatting or Workspace provider resource links; the structured data retains those
39
+ links for widgets. Clients decide how to display the text and may prefer structured
40
+ data when supplying results to a model.
37
41
 
38
42
  All widgets share a compact console style with a fixed 420px height and adapt to the
39
43
  host width. The move confirmation uses only two rows, source and destination.
@@ -64,7 +68,7 @@ and install it in a virtual environment:
64
68
 
65
69
  ```powershell
66
70
  python -m venv .venv
67
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.1-py3-none-any.whl
71
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.3-py3-none-any.whl
68
72
  .\.venv\Scripts\forgemcp.exe --help
69
73
  ```
70
74
 
@@ -76,7 +80,7 @@ The distribution name is `forge-cpp-mcp`; the Python module and server command a
76
80
  After the package is published to PyPI, the install command can instead be:
77
81
 
78
82
  ```powershell
79
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.1
83
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.3
80
84
  ```
81
85
 
82
86
  ## Setup from source
@@ -139,13 +143,27 @@ add the `forgemcp` entry to it rather than replacing the file. Restart the clien
139
143
  after editing its configuration. Check the connection with `codex mcp list` or
140
144
  `claude mcp get forgemcp`, respectively.
141
145
 
146
+ To require agents to use ForgeMCP for supported operations, append the
147
+ [example agent instructions](examples/mcp-clients/agent-instructions.md) to your
148
+ project's `AGENTS.md` (Codex) or `CLAUDE.md` (Claude Code). The same block works
149
+ for both clients and requires evidence of an unsupported operation, tool failure,
150
+ or unavailability before using alternatives. Preserve existing project instructions.
151
+
142
152
  ## Workspace files and storage
143
153
 
144
154
  All file tools use string paths such as `project/src/main.cpp` and
145
155
  `storage/build/debug`. Directory tools default to `project/`; the separate tool
146
156
  argument `root` has been removed. Python consumers use the shared `WorkspacePath`
147
- type, which serializes as the same string. Storage defaults to `.<project-name>.forgemcp` beside the
148
- project. Set its location explicitly when needed:
157
+ type, which serializes as the same string. Tool path arguments also accept ordinary
158
+ relative paths: `src/main.cpp` and `./src/main.cpp` both mean `project/src/main.cpp`,
159
+ and `./` means `project/`. One trailing `/` is accepted for managed paths:
160
+ `project/src/` becomes `project/src`. Results always use canonical qualified paths.
161
+ The prefixes `project/`, `storage/`, and `root/` are reserved; use `./storage/...`
162
+ to access a project directory named `storage`. Absolute paths, `..`, and repeated
163
+ separators remain invalid; external locations require an explicit `root/` prefix.
164
+
165
+ Storage defaults to `.<project-name>.forgemcp` beside the project. Set its location
166
+ explicitly when needed:
149
167
 
150
168
  ```powershell
151
169
  forgemcp --workspace C:\Projects\Example --workspace-storage D:\ForgeMCP\Example
@@ -168,7 +186,7 @@ a working-directory check, not an operating-system sandbox.
168
186
  | `workspace_find_files(pattern="*", path="project/")` | Recursive filename/path glob search |
169
187
  | `workspace_file_info(path)` | Creation/modification times, byte size, owner; unavailable metadata is null |
170
188
  | `workspace_read_file(path, start_line=1, end_line=null)` | UTF-8 text, with an optional inclusive line range |
171
- | `workspace_search(query, path="project/", regex=false, extensions=null, case_sensitive=true)` | Matching lines and skipped binary/non-UTF-8 files |
189
+ | `workspace_text_search(query, path="project/", regex=false, extensions=null, case_sensitive=true, max_matches=100)` | Bounded matching lines, exact counts, and a linked skipped-file list |
172
190
  | `workspace_write_file(path, text)` | Create or overwrite; return removed/added line counts |
173
191
  | `workspace_edit_file(path, old_text, new_text, replace_all=false)` | Exact replacement; zero or ambiguous matches fail without modifying the file |
174
192
  | `workspace_move(source, destination)` | Move a file or directory; destination must not exist |
@@ -185,8 +203,15 @@ dot-prefixed files remain visible. Searches skip directories beginning with a do
185
203
  access hidden directories and in-root links. Mutations cannot traverse links;
186
204
  moving/removing a whole tree containing links is rejected. Dependent modules may
187
205
  register protected paths, which remain readable but cannot be changed through the
188
- workspace API. Searches intentionally have no application-level timeout, result
189
- limit, or pagination in this iteration.
206
+ workspace API. Searches have no application-level timeout or pagination. Text search
207
+ returns at most `max_matches` matching lines (positive integer, default 100), with
208
+ all spans on each returned line. It continues scanning to return exact
209
+ `matches_count` and `skipped_files_count`; `matches_truncated` indicates omitted
210
+ matching lines. The complete binary/non-UTF-8 file list is an immutable JSON resource
211
+ linked as `resources.skipped_files` when nonempty, rather than inline output.
212
+ The widget loads that snapshot for its skipped-files tab. Narrow the path/extensions
213
+ or increase `max_matches` to retrieve more matching lines. Individual lines and their
214
+ span arrays have no byte limit.
190
215
 
191
216
  File resource templates use a single path including its project/ or storage/ prefix:
192
217
 
@@ -196,7 +221,7 @@ forgemcp://workspace/raw{/path*}
196
221
  forgemcp://workspace/list{?path,depth,include_hidden}
197
222
  forgemcp://workspace/find-files{?pattern,path}
198
223
  forgemcp://workspace/file-info{?path}
199
- forgemcp://workspace/search{?query,path,regex,extensions,case_sensitive}
224
+ forgemcp://workspace/search{?query,path,regex,extensions,case_sensitive,max_matches}
200
225
  forgemcp://workspace/results/{result_id}/{name}.json
201
226
  forgemcp://workspace/results/{result_id}/{name}.md
202
227
  ```
@@ -230,10 +255,26 @@ A resource-loading failure does not change the successful file-operation outcome
230
255
  When a CMake configuration has a compilation database and clangd in its toolset,
231
256
  Workspace reads, writes, and edits of C/C++ files also return
232
257
  `resources.clangd`. This immutable JSON contains diagnostics and semantic
233
- highlighting grouped by configuration. Workspace mutations synchronize retained
234
- clangd sessions; changing a header refreshes already opened dependent files.
258
+ highlighting grouped by configuration, always across all available configurations.
259
+ Clangd analyzes the complete file even for a partial Workspace read, but the saved
260
+ diagnostics and semantic spans include only those intersecting the lines actually
261
+ returned. Their coordinates remain absolute file positions; an empty partial-read
262
+ excerpt (for example, beyond EOF) has no annotations.
263
+
235
264
  Clangd starts sessions from CMake's configuration subscription and keeps them until
236
265
  the configuration disappears, its database changes, or the server stops.
266
+ Each process receives `-j=2`; this worker setting does not impose a memory limit.
267
+ File analysis opens the complete current file, collects the requested result, and
268
+ closes the document before returning. Documents are not retained between calls, so
269
+ the next analysis incorporates saved header changes without reopening other files
270
+ after each Workspace mutation. Repeated calls pay the cost of a fresh file parse;
271
+ closing a document does not release clangd's whole index. Workspace mutations still
272
+ notify sessions of filesystem changes and reconcile changed compilation databases.
273
+ Explicit clangd tools use their existing `configurations` argument to select a
274
+ subset, for example
275
+ `clangd_diagnostics(path="project/src/main.cpp", configurations=["debug"])`.
276
+ Omitting that argument or passing `[]` selects all available configurations.
277
+
237
278
  Workspace widgets load both resources independently. The source view lets you
238
279
  select a configuration for semantic highlighting and inline diagnostic markers;
239
280
  lexical C/C++ colors also cover keywords, comments, literals, directives, and nested
@@ -1,6 +1,6 @@
1
1
  import { App, PostMessageTransport, applyDocumentTheme, applyHostFonts, applyHostStyleVariables } from "@modelcontextprotocol/ext-apps";
2
2
  import { createResultView } from "./result-view.js";
3
- import { loadResultClangd, loadResultDiff } from "./result-resources.js";
3
+ import { loadResultClangd, loadResultDiff, loadResultSkippedFiles } from "./result-resources.js";
4
4
  import "./widget.css";
5
5
 
6
6
  /** Lifecycle bridge and immutable result-resource loading; never calls tools. */
@@ -34,6 +34,7 @@ export async function connectWidget({ toolName, describe, renderValue }) {
34
34
  if (result?.isError || data?.action === "moved") return;
35
35
  await Promise.all([
36
36
  ["diff", loadResultDiff], ["clangd", loadResultClangd],
37
+ ["skipped_files", loadResultSkippedFiles],
37
38
  ].filter(([name]) => data?.resources?.[name]).map(async ([name, load]) => {
38
39
  try {
39
40
  const resource = await load(result, (params) => app.readServerResource(params), () => current === generation);
@@ -47,6 +47,20 @@ function resultResourceUri(link, provider) {
47
47
  return link.uri;
48
48
  }
49
49
 
50
+ export async function loadResultSkippedFiles(result, read, current = () => true) {
51
+ const link = result?.structuredContent?.resources?.skipped_files;
52
+ if (!link || result.isError) return null;
53
+ const uri = resultResourceUri(link, "skipped_files");
54
+ if (!current()) return null;
55
+ const data = decode(await read({ uri }), uri);
56
+ if (!Array.isArray(data?.skipped_files) || !data.skipped_files.every((path) =>
57
+ typeof path === "string" && /^(project|storage)\//.test(path))
58
+ || data.skipped_files.length !== result.structuredContent.skipped_files_count) {
59
+ throw new Error("Invalid skipped-file resource");
60
+ }
61
+ return current() ? data : null;
62
+ }
63
+
50
64
  export async function loadResultClangd(result, read, current = () => true) {
51
65
  const link = result?.structuredContent?.resources?.clangd;
52
66
  if (!link || result.isError) return null;
@@ -272,7 +272,7 @@ export function createResultView(root, { toolName, describe, renderValue }) {
272
272
  copyResource.addEventListener("click", () => { void copy(JSON.stringify(resource, null, 2)); });
273
273
  heading.append(copyResource);
274
274
  content.append(heading, json);
275
- } else if (["diff", "clangd"].includes(name)) {
275
+ } else if (["diff", "clangd", "skipped_files"].includes(name)) {
276
276
  content.append(element("p", "fm-empty", `${name}: ${resourceData[`${name}State`] === "error" ? "Resource could not be loaded." : "Loading resource…"}`));
277
277
  }
278
278
  }
@@ -1,4 +1,4 @@
1
1
  import { connectWidget } from "./shared/app.js";
2
2
  import { workspacePresentation, workspaceValue } from "./workspace-view.js";
3
3
 
4
- await connectWidget({ toolName: "workspace_search", describe: workspacePresentation, renderValue: workspaceValue });
4
+ await connectWidget({ toolName: "workspace_text_search", describe: workspacePresentation, renderValue: workspaceValue });
@@ -3,7 +3,7 @@ import { isObject } from "./shared/presentation.js";
3
3
  import { clangdValue } from "./clangd-view.js";
4
4
  import { appendSourceText, cppSyntax, sourceConfiguration } from "./source-view.js";
5
5
 
6
- function searchView(doc, data, query, width) {
6
+ function searchView(doc, data, query, width, loaded) {
7
7
  const node = (tag, className, text) => {
8
8
  const result = doc.createElement(tag);
9
9
  result.className = className;
@@ -17,7 +17,7 @@ function searchView(doc, data, query, width) {
17
17
  const matches = node("div", "fm-search-matches");
18
18
  const skipped = node("div", "fm-search-skipped");
19
19
  const panels = [matches, skipped];
20
- const buttons = ["Matches", `Skipped files (${data.skipped_files?.length ?? 0})`].map((title, index) => {
20
+ const buttons = ["Matches", `Skipped files (${data.skipped_files_count})`].map((title, index) => {
21
21
  const button = node("button", "", title);
22
22
  button.type = "button";
23
23
  button.id = `workspace-search-tab-${index}`;
@@ -127,9 +127,14 @@ function searchView(doc, data, query, width) {
127
127
  matches.append(file);
128
128
  }
129
129
  if (!groups.size) matches.append(node("p", "fm-empty", "No matching lines."));
130
- const paths = (data.skipped_files ?? []).filter((path) => path.toLocaleLowerCase().includes(query));
130
+ const paths = (loaded.skipped_files?.skipped_files ?? []).filter((path) => path.toLocaleLowerCase().includes(query));
131
131
  for (const path of paths) skipped.append(node("div", "fm-skipped-path", path));
132
- if (!paths.length) skipped.append(node("p", "fm-empty", "No skipped files."));
132
+ if (!paths.length) skipped.append(node("p", "fm-empty", data.skipped_files_count === 0
133
+ ? "No skipped files." : loaded.skipped_filesState === "error"
134
+ ? "Skipped-file resource could not be loaded." : loaded.skipped_filesState === "ready"
135
+ ? "No skipped files match the filter." : "Loading skipped files…"));
136
+ if (data.matches_truncated) matches.prepend(node("p", "fm-empty",
137
+ `Showing ${data.matches.length} of ${data.matches_count} matching lines (matches_truncated=true). Narrow the search or increase max_matches.`));
133
138
  view.append(tabs, matches, skipped);
134
139
  return view;
135
140
  }
@@ -212,10 +217,10 @@ export function workspacePresentation(data, loaded = {}) {
212
217
  }
213
218
  if (Array.isArray(data.matches)) {
214
219
  return {
215
- toolName: "workspace_search", summary: data, records: null,
220
+ toolName: "workspace_text_search", summary: data, records: null,
216
221
  resourceFields: resources(data), filterPlaceholder: "Search results",
217
- count: `${data.matches.length} matches · ${new Set(data.matches.map((match) => match.path)).size} files`,
218
- render: (doc, query, width) => searchView(doc, data, query, width),
222
+ count: `${data.matches.length}/${data.matches_count} matching lines · ${data.skipped_files_count} skipped files`,
223
+ render: (doc, query, width) => searchView(doc, data, query, width, loaded),
219
224
  };
220
225
  }
221
226
  const toolName = Array.isArray(data.entries) ? "workspace_list"
@@ -14,6 +14,7 @@ class CustomBuildHook(BuildHookInterface):
14
14
  """Produce frontend assets as part of the wheel build."""
15
15
 
16
16
  def initialize(self, version: str, build_data: dict[str, object]) -> None:
17
+ """Build missing frontend assets and include them in the wheel artifacts."""
17
18
  frontend = Path(self.root) / "frontend"
18
19
  outputs = [
19
20
  *(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "forge-cpp-mcp"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "A safe MCP server for structured C++ project development workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "mcp[cli]==2.1.1",
16
16
  "pydantic>=2.13",
17
17
  "regex>=2024.11.6",
18
+ "markdown-it-py>=4.0",
18
19
  ]
19
20
 
20
21
  [project.optional-dependencies]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.1"
5
+ __version__ = "0.2.3"
@@ -36,10 +36,12 @@ class Widget:
36
36
 
37
37
  @property
38
38
  def uri(self) -> str:
39
+ """Return the stable UI resource URI derived from the packaged filename."""
39
40
  return f"ui://forgemcp/{PurePosixPath(self.path).name}"
40
41
 
41
42
  @property
42
43
  def content(self) -> str:
44
+ """Read the packaged widget as UTF-8 HTML."""
43
45
  return package_file(self.path).read_text(encoding="utf-8")
44
46
 
45
47
 
@@ -52,6 +54,7 @@ class IconFile:
52
54
 
53
55
  @property
54
56
  def icon(self) -> Icon:
57
+ """Encode the packaged icon as portable MCP metadata with its MIME type and sizes."""
55
58
  resource = package_file(self.path)
56
59
  mime_type = mimetypes.guess_type(self.path)[0] or "application/octet-stream"
57
60
  encoded = base64.b64encode(resource.read_bytes()).decode("ascii")
@@ -21,6 +21,7 @@ class ClangdRequestError(ClangdError):
21
21
  """The language server rejected an operation."""
22
22
 
23
23
  def __init__(self, method: str, code: int, message: str) -> None:
24
+ """Retain the server error code and describe the failed LSP method."""
24
25
  super().__init__(f"{method} failed ({code}): {message}")
25
26
  self.code = code
26
27
 
@@ -0,0 +1,186 @@
1
+ """Language values shared by session decoding and service results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, Field, model_validator
8
+
9
+ from forgemcp.workspace.path import WorkspacePath
10
+
11
+
12
+ class Position(BaseModel):
13
+ """A one-based source line and zero-based Unicode character offset."""
14
+
15
+ line: int = Field(ge=1)
16
+ character: int = Field(ge=0, description="Zero-based Unicode code point offset.")
17
+
18
+
19
+ class SourceRange(BaseModel):
20
+ """An ordered pair of source positions delimiting a range."""
21
+
22
+ start: Position
23
+ end: Position
24
+
25
+ @model_validator(mode="after")
26
+ def ordered(self) -> SourceRange:
27
+ """Reject ranges whose end precedes the start."""
28
+ if (self.end.line, self.end.character) < (self.start.line, self.start.character):
29
+ raise ValueError("Source range end precedes its start.")
30
+ return self
31
+
32
+
33
+ class Location(BaseModel):
34
+ """A qualified file path and source range."""
35
+
36
+ path: WorkspacePath
37
+ range: SourceRange
38
+
39
+
40
+ class SourceExcerpt(BaseModel):
41
+ """Immutable source context captured with a navigation answer."""
42
+
43
+ start_line: int = Field(ge=1)
44
+ text: str
45
+
46
+
47
+ class NavigationLocation(Location):
48
+ """A navigation target with optional saved source context."""
49
+
50
+ preview: SourceExcerpt | None = Field(
51
+ default=None,
52
+ description="Up to seven saved source lines near the target. External or unreadable files have no preview.",
53
+ )
54
+
55
+
56
+ class RelatedDiagnostic(BaseModel):
57
+ """A diagnostic note attached to another source location."""
58
+
59
+ location: Location
60
+ message: str
61
+
62
+
63
+ class Diagnostic(BaseModel):
64
+ """A source diagnostic with severity, provenance, tags, and related notes."""
65
+
66
+ range: SourceRange
67
+ severity: Literal["error", "warning", "information", "hint"] | None = None
68
+ message: str
69
+ code: int | str | None = None
70
+ source: str | None = None
71
+ tags: list[int] = Field(default_factory=list)
72
+ related: list[RelatedDiagnostic] = Field(default_factory=list)
73
+
74
+
75
+ class HoverText(BaseModel):
76
+ """One hover fragment with its format and optional code language."""
77
+
78
+ kind: Literal["plaintext", "markdown", "code"]
79
+ text: str
80
+ language: str | None = None
81
+
82
+
83
+ class Hover(BaseModel):
84
+ """Hover fragments and the optional source range they describe."""
85
+
86
+ contents: list[HoverText]
87
+ range: SourceRange | None = None
88
+
89
+
90
+ class DocumentSymbol(BaseModel):
91
+ """A hierarchical symbol with its full range and name selection range."""
92
+
93
+ name: str
94
+ kind: int
95
+ range: SourceRange
96
+ selection_range: SourceRange
97
+ detail: str | None = None
98
+ tags: list[int] = Field(default_factory=list)
99
+ children: list[DocumentSymbol] = Field(default_factory=list)
100
+
101
+
102
+ class WorkspaceSymbol(BaseModel):
103
+ """A workspace symbol with a navigation target and optional containing scope."""
104
+
105
+ name: str
106
+ kind: int
107
+ location: NavigationLocation
108
+ container_name: str | None = None
109
+ tags: list[int] = Field(default_factory=list)
110
+
111
+
112
+ class HighlightSpan(BaseModel):
113
+ """A semantic source range with its token kind and modifiers."""
114
+
115
+ range: SourceRange
116
+ kind: str
117
+ modifiers: list[str] = Field(default_factory=list)
118
+
119
+
120
+ class DiagnosticsResult(BaseModel):
121
+ """Diagnostics for one file with the configurations that produced them."""
122
+
123
+ configurations: list[str]
124
+ path: WorkspacePath
125
+ diagnostics: list[Diagnostic]
126
+
127
+
128
+ class HoverResult(BaseModel):
129
+ """A hover answer at a source position with configuration provenance."""
130
+
131
+ configurations: list[str]
132
+ path: WorkspacePath
133
+ position: Position
134
+ hover: Hover | None
135
+
136
+
137
+ class DefinitionResult(BaseModel):
138
+ """Definition targets shared by the listed configurations."""
139
+
140
+ configurations: list[str]
141
+ locations: list[NavigationLocation]
142
+
143
+
144
+ class ReferencesResult(BaseModel):
145
+ """Reference targets shared by the listed configurations."""
146
+
147
+ configurations: list[str]
148
+ locations: list[NavigationLocation]
149
+
150
+
151
+ class DocumentSymbolsResult(BaseModel):
152
+ """A file's symbol tree with configuration provenance."""
153
+
154
+ configurations: list[str]
155
+ path: WorkspacePath
156
+ symbols: list[DocumentSymbol]
157
+
158
+
159
+ class WorkspaceSymbolsResult(BaseModel):
160
+ """Matching workspace symbols with configuration provenance."""
161
+
162
+ configurations: list[str]
163
+ symbols: list[WorkspaceSymbol]
164
+
165
+
166
+ class HighlightingResult(BaseModel):
167
+ """Semantic spans for a file with configuration provenance."""
168
+
169
+ configurations: list[str]
170
+ path: WorkspacePath
171
+ spans: list[HighlightSpan]
172
+
173
+
174
+ class FileAnalysis(BaseModel):
175
+ """Saved diagnostic and highlighting answers for one file."""
176
+
177
+ path: WorkspacePath
178
+ diagnostics: list[DiagnosticsResult]
179
+ highlighting: list[HighlightingResult]
180
+
181
+
182
+ class ClangdResource(BaseModel):
183
+ """The versioned payload of an immutable workspace analysis resource."""
184
+
185
+ version: Literal[1] = 1
186
+ files: list[FileAnalysis]