forge-cpp-mcp 0.2.4__tar.gz → 0.3.0__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 (146) hide show
  1. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/PKG-INFO +108 -4
  2. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/README.md +103 -3
  3. forge_cpp_mcp-0.3.0/frontend/admin.html +12 -0
  4. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/package.json +1 -1
  5. forge_cpp_mcp-0.3.0/frontend/sandbox.html +11 -0
  6. forge_cpp_mcp-0.3.0/frontend/src/admin-sandbox.js +43 -0
  7. forge_cpp_mcp-0.3.0/frontend/src/admin-widget.js +133 -0
  8. forge_cpp_mcp-0.3.0/frontend/src/admin.css +82 -0
  9. forge_cpp_mcp-0.3.0/frontend/src/admin.js +793 -0
  10. forge_cpp_mcp-0.3.0/frontend/tests/admin-widget.test.js +170 -0
  11. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/vite.config.js +2 -0
  12. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/hatch_build.py +2 -0
  13. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/pyproject.toml +5 -1
  14. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/__init__.py +1 -1
  15. forge_cpp_mcp-0.3.0/src/forgemcp/admin/__init__.py +1 -0
  16. forge_cpp_mcp-0.3.0/src/forgemcp/admin/errors.py +10 -0
  17. forge_cpp_mcp-0.3.0/src/forgemcp/admin/history.py +157 -0
  18. forge_cpp_mcp-0.3.0/src/forgemcp/admin/instance.py +458 -0
  19. forge_cpp_mcp-0.3.0/src/forgemcp/admin/service.py +549 -0
  20. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/service.py +18 -5
  21. forge_cpp_mcp-0.3.0/src/forgemcp/cmake/build_tree.py +67 -0
  22. forge_cpp_mcp-0.3.0/src/forgemcp/cmake/file_api.py +229 -0
  23. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/models.py +17 -0
  24. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/profiles.py +21 -0
  25. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/service.py +218 -200
  26. forge_cpp_mcp-0.3.0/src/forgemcp/cmake/workspace.py +122 -0
  27. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/__init__.py +23 -0
  28. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/errors.py +21 -0
  29. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/service.py +159 -0
  30. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/unix.py +295 -0
  31. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/windows.py +316 -0
  32. forge_cpp_mcp-0.3.0/src/forgemcp/process/host.py +95 -0
  33. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/service.py +16 -0
  34. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/progress.py +7 -0
  35. forge_cpp_mcp-0.3.0/src/forgemcp/server.py +486 -0
  36. forge_cpp_mcp-0.2.4/src/forgemcp/server.py +0 -198
  37. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/.gitignore +0 -0
  38. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/LICENSE +0 -0
  39. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/clangd-result.html +0 -0
  40. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/cmake-build.html +0 -0
  41. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/cmake-configure.html +0 -0
  42. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/cmake-profiles.html +0 -0
  43. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/cmake-run-test.html +0 -0
  44. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/cmake-test.html +0 -0
  45. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/package-lock.json +0 -0
  46. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/process-details.html +0 -0
  47. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/process-overview.html +0 -0
  48. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/clangd-navigation-view.js +0 -0
  49. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/clangd-result.js +0 -0
  50. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/clangd-view.js +0 -0
  51. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-build.js +0 -0
  52. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-configure.js +0 -0
  53. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-profiles.js +0 -0
  54. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-run-test.js +0 -0
  55. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-test.js +0 -0
  56. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-view.js +0 -0
  57. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/process-details.js +0 -0
  58. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/process-overview.js +0 -0
  59. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/process-status.js +0 -0
  60. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/process.css +0 -0
  61. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/shared/app.js +0 -0
  62. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/shared/copy-icon.js +0 -0
  63. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/shared/presentation.js +0 -0
  64. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/shared/result-resources.js +0 -0
  65. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/shared/result-view.js +0 -0
  66. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/shared/widget.css +0 -0
  67. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/source-view.js +0 -0
  68. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/toolsets.js +0 -0
  69. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-file.js +0 -0
  70. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-result.js +0 -0
  71. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-search.js +0 -0
  72. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-tree.js +0 -0
  73. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-view.js +0 -0
  74. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/tests/result-resources.test.js +0 -0
  75. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/tests/result-view.test.js +0 -0
  76. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/tests/workspace.test.js +0 -0
  77. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/toolsets.html +0 -0
  78. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/workspace-file.html +0 -0
  79. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/workspace-result.html +0 -0
  80. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/workspace-search.html +0 -0
  81. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/frontend/workspace-tree.html +0 -0
  82. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/assets.py +0 -0
  83. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/__init__.py +0 -0
  84. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/errors.py +0 -0
  85. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/models.py +0 -0
  86. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/session.py +0 -0
  87. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/text.py +0 -0
  88. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/__init__.py +0 -0
  89. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/errors.py +0 -0
  90. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/text.py +0 -0
  91. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/completion.py +0 -0
  92. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/clangd.LICENSE +0 -0
  93. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/clangd.svg +0 -0
  94. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/cmake.svg +0 -0
  95. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/process.svg +0 -0
  96. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/toolchain.svg +0 -0
  97. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace-edit.svg +0 -0
  98. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace-file.svg +0 -0
  99. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace-search.svg +0 -0
  100. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace.svg +0 -0
  101. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/markdown.py +0 -0
  102. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/__init__.py +0 -0
  103. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/errors.py +0 -0
  104. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/models.py +0 -0
  105. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/text.py +0 -0
  106. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/transcript.py +0 -0
  107. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/text.py +0 -0
  108. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/__init__.py +0 -0
  109. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/discovery.py +0 -0
  110. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/errors.py +0 -0
  111. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/loader.py +0 -0
  112. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/__init__.py +0 -0
  113. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/system.py +0 -0
  114. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/user.py +0 -0
  115. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/visual_studio.py +0 -0
  116. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/service.py +0 -0
  117. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/spec.py +0 -0
  118. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/__init__.py +0 -0
  119. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clang.py +0 -0
  120. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clang_cl.py +0 -0
  121. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clangd.py +0 -0
  122. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clangxx.py +0 -0
  123. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/cmake.py +0 -0
  124. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/cppvsdbg.py +0 -0
  125. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/ctest.py +0 -0
  126. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/gcc.py +0 -0
  127. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/gdb.py +0 -0
  128. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/git.py +0 -0
  129. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/gxx.py +0 -0
  130. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/link.py +0 -0
  131. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/lld.py +0 -0
  132. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/lld_link.py +0 -0
  133. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/lldb_dap.py +0 -0
  134. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/make.py +0 -0
  135. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/msbuild.py +0 -0
  136. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/msvc.py +0 -0
  137. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/ninja.py +0 -0
  138. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/__init__.py +0 -0
  139. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/diff.py +0 -0
  140. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/errors.py +0 -0
  141. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/metadata.py +0 -0
  142. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/models.py +0 -0
  143. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/path.py +0 -0
  144. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/providers.py +0 -0
  145. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/service.py +0 -0
  146. {forge_cpp_mcp-0.2.4 → forge_cpp_mcp-0.3.0}/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.3.0
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
@@ -12,8 +12,12 @@ License-File: LICENSE
12
12
  Requires-Python: >=3.13
13
13
  Requires-Dist: markdown-it-py>=4.0
14
14
  Requires-Dist: mcp[cli]==2.1.1
15
+ Requires-Dist: psutil<8,>=7.2
15
16
  Requires-Dist: pydantic>=2.13
17
+ Requires-Dist: pywin32>=311; sys_platform == 'win32'
16
18
  Requires-Dist: regex>=2024.11.6
19
+ Requires-Dist: starlette>=0.48.0
20
+ Requires-Dist: uvicorn>=0.31.1
17
21
  Provides-Extra: build
18
22
  Requires-Dist: build>=1.3; extra == 'build'
19
23
  Provides-Extra: dev
@@ -96,19 +100,20 @@ and install it in a virtual environment:
96
100
 
97
101
  ```powershell
98
102
  python -m venv .venv
99
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.4-py3-none-any.whl
103
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.3.0-py3-none-any.whl
100
104
  .\.venv\Scripts\forgemcp.exe --help
101
105
  ```
102
106
 
103
107
  The wheel includes the compiled HTML widgets and icons. No npm commands or separate
104
- frontend deployment are needed. CMake, Ninja, compilers, and clangd are external
108
+ frontend deployment are needed. It also packages the local admin panel and its
109
+ isolated widget host. CMake, Ninja, compilers, and clangd are external
105
110
  tools: install the ones needed by your workflows separately.
106
111
  The distribution name is `forge-cpp-mcp`; the Python module and server command are `forgemcp`.
107
112
 
108
113
  After the package is published to PyPI, the install command can instead be:
109
114
 
110
115
  ```powershell
111
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.4
116
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.3.0
112
117
  ```
113
118
 
114
119
  ## Setup from source
@@ -152,6 +157,75 @@ builds the wheel first and then starts the server for this repository.
152
157
 
153
158
  All MCP traffic uses stdout. Operational logs must go to stderr.
154
159
 
160
+ ## Local admin panel
161
+
162
+ Normal CLI launches automatically join one admin Hub for the current OS user.
163
+ Each server remains an independent stdio process with its own workspace,
164
+ environment, toolsets, processes, and clangd sessions. The Hub presents them as
165
+ separate browser tabs; it does not move their services into a shared process.
166
+
167
+ Open the panel while at least one server is running:
168
+
169
+ ```powershell
170
+ forgemcp --open-admin
171
+ ```
172
+
173
+ This command retrieves the private browser URL through same-user local IPC and
174
+ opens the system browser, then exits. The startup message contains the public
175
+ address and this command; the authentication token is never printed. Opening the
176
+ public address in a new browser session alone does not authenticate that session.
177
+
178
+ Each instance tab offers:
179
+
180
+ - tool-call history from both MCP clients and the browser, with original arguments,
181
+ results, progress, timestamps, and errors;
182
+ - the original MCP App widget, text fallback, and complete JSON result;
183
+ - tool schemas, static resources, resource templates, and resource reading;
184
+ - startup settings, process status, discovered toolsets, CMake configurations,
185
+ and retained clangd sessions;
186
+ - browser responses to SDK elicitation requests, including external-file read
187
+ confirmation for commands initiated from the panel.
188
+
189
+ Settings are read-only. Command controls start hidden; select **Enable command
190
+ controls** to call an existing tool with literal JSON arguments; the catalog shows
191
+ its input schema. These
192
+ calls pass through the same SDK handlers, validation, and feature locks as stdio
193
+ calls. There is no panel-wide execution queue. Existing modules decide which
194
+ operations may run concurrently.
195
+ The panel can cancel its own running commands; calls initiated by an MCP client
196
+ remain owned by that client.
197
+
198
+ The panel listens on `127.0.0.1`, preferring port 8765 and choosing a free port when
199
+ that port is unavailable. Widgets use a second loopback port for their isolated
200
+ host. The Hub stops shortly after the last server session ends; browser tabs do
201
+ not keep it running. A temporary IPC disconnect is shown as reconnecting while
202
+ the underlying server process remains alive. Lost tool calls are never replayed
203
+ automatically; read the instance history after reconnecting before repeating one.
204
+
205
+ History and linked widget result resources live only in the server's memory.
206
+ Stopping that server loses its history; there is no database or disk archive.
207
+ Process transcripts retain their existing in-memory lifecycle and are not copied
208
+ into a separate admin log.
209
+
210
+ Use these operator options when needed:
211
+
212
+ ```powershell
213
+ forgemcp --no-admin
214
+ forgemcp --admin-port 9000
215
+ forgemcp --admin-ipc forgemcp-other
216
+ forgemcp --open-admin --admin-ipc forgemcp-other
217
+ ```
218
+
219
+ `--no-admin` opts that instance out of the panel. `--admin-port` is a preference
220
+ when starting a new Hub; it does not change an already running Hub's port.
221
+ Instances using the same `--admin-ipc` name share a Hub; a different name creates
222
+ a separate group for the same OS user.
223
+
224
+ The normal wheel build includes `admin.html` and `sandbox.html` alongside the MCP
225
+ widgets. After changing frontend source, use the documented wheel build or
226
+ `npm run build --prefix frontend`. An unbuilt source checkout returns an explicit
227
+ missing-assets page until those files are generated.
228
+
155
229
  ## Connect Codex or Claude Code
156
230
 
157
231
  After [installation](#install-a-release) or [source setup](#setup-from-source), copy the appropriate example into the root of the C/C++
@@ -322,6 +396,36 @@ no source preview because explicit external reads require confirmation.
322
396
 
323
397
  ## CMake profiles
324
398
 
399
+ CMake owns which configurations can supply clangd compilation contexts. After a
400
+ successful configure/generate, ForgeMCP reads `cmakeFiles-v1` from a new CMake
401
+ File API reply and records SHA-256 hashes of that build directory's configuration
402
+ inputs in `project/` and `storage/`. Hashes and dependency lists live in memory;
403
+ restart the server and run `cmake_configure` to confirm contexts again. Existing
404
+ File API replies and `compile_commands.json` alone do not confirm current inputs.
405
+
406
+ Workspace writes, edits, moves, deletions, and directory creation check known
407
+ inputs at the operation boundary. Actual input changes withdraw affected contexts
408
+ and notify clangd through the CMake subscription; identical content and ordinary
409
+ source/header edits keep contexts available unless those files are CMake inputs.
410
+ Known preset files are treated conservatively without resolving preset inheritance
411
+ or includes. CONFIGURE_DEPENDS globs conservatively invalidate on structural
412
+ changes; older File API replies without glob information use this policy for all
413
+ configurations. No project-wide dependency scan or CMake/glob parser is used.
414
+
415
+ Run `cmake_configure` to restore outdated or unknown contexts. `cmake_build` is
416
+ also allowed for outdated configurations: a new successful File API generation
417
+ can restore them even when later compilation fails. A successful build without
418
+ regeneration does not refresh the input baseline. Failed configure/generate cannot
419
+ reactivate old artifacts, and invalidation leaves build trees and databases intact.
420
+ Unchanged contexts and database contents retain their clangd sessions.
421
+
422
+ Checks occur only during Workspace mutations, configure, and build. There are no
423
+ filesystem watchers, polling, or background scans, and no guarantee of detecting
424
+ external editor/process changes. External `root/` dependencies are omitted from
425
+ hash tracking without elicitation or reads; changes to those inputs require an
426
+ explicit configure. Native preset contexts still require a known managed build
427
+ directory and a compilation database.
428
+
325
429
  `cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
326
430
  operate on all profiles unless a nonempty `profiles` list selects a subset.
327
431
  Configuration must precede building, and building must precede testing. Profiles
@@ -71,19 +71,20 @@ 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.3.0-py3-none-any.whl
75
75
  .\.venv\Scripts\forgemcp.exe --help
76
76
  ```
77
77
 
78
78
  The wheel includes the compiled HTML widgets and icons. No npm commands or separate
79
- frontend deployment are needed. CMake, Ninja, compilers, and clangd are external
79
+ frontend deployment are needed. It also packages the local admin panel and its
80
+ isolated widget host. CMake, Ninja, compilers, and clangd are external
80
81
  tools: install the ones needed by your workflows separately.
81
82
  The distribution name is `forge-cpp-mcp`; the Python module and server command are `forgemcp`.
82
83
 
83
84
  After the package is published to PyPI, the install command can instead be:
84
85
 
85
86
  ```powershell
86
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.4
87
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.3.0
87
88
  ```
88
89
 
89
90
  ## Setup from source
@@ -127,6 +128,75 @@ builds the wheel first and then starts the server for this repository.
127
128
 
128
129
  All MCP traffic uses stdout. Operational logs must go to stderr.
129
130
 
131
+ ## Local admin panel
132
+
133
+ Normal CLI launches automatically join one admin Hub for the current OS user.
134
+ Each server remains an independent stdio process with its own workspace,
135
+ environment, toolsets, processes, and clangd sessions. The Hub presents them as
136
+ separate browser tabs; it does not move their services into a shared process.
137
+
138
+ Open the panel while at least one server is running:
139
+
140
+ ```powershell
141
+ forgemcp --open-admin
142
+ ```
143
+
144
+ This command retrieves the private browser URL through same-user local IPC and
145
+ opens the system browser, then exits. The startup message contains the public
146
+ address and this command; the authentication token is never printed. Opening the
147
+ public address in a new browser session alone does not authenticate that session.
148
+
149
+ Each instance tab offers:
150
+
151
+ - tool-call history from both MCP clients and the browser, with original arguments,
152
+ results, progress, timestamps, and errors;
153
+ - the original MCP App widget, text fallback, and complete JSON result;
154
+ - tool schemas, static resources, resource templates, and resource reading;
155
+ - startup settings, process status, discovered toolsets, CMake configurations,
156
+ and retained clangd sessions;
157
+ - browser responses to SDK elicitation requests, including external-file read
158
+ confirmation for commands initiated from the panel.
159
+
160
+ Settings are read-only. Command controls start hidden; select **Enable command
161
+ controls** to call an existing tool with literal JSON arguments; the catalog shows
162
+ its input schema. These
163
+ calls pass through the same SDK handlers, validation, and feature locks as stdio
164
+ calls. There is no panel-wide execution queue. Existing modules decide which
165
+ operations may run concurrently.
166
+ The panel can cancel its own running commands; calls initiated by an MCP client
167
+ remain owned by that client.
168
+
169
+ The panel listens on `127.0.0.1`, preferring port 8765 and choosing a free port when
170
+ that port is unavailable. Widgets use a second loopback port for their isolated
171
+ host. The Hub stops shortly after the last server session ends; browser tabs do
172
+ not keep it running. A temporary IPC disconnect is shown as reconnecting while
173
+ the underlying server process remains alive. Lost tool calls are never replayed
174
+ automatically; read the instance history after reconnecting before repeating one.
175
+
176
+ History and linked widget result resources live only in the server's memory.
177
+ Stopping that server loses its history; there is no database or disk archive.
178
+ Process transcripts retain their existing in-memory lifecycle and are not copied
179
+ into a separate admin log.
180
+
181
+ Use these operator options when needed:
182
+
183
+ ```powershell
184
+ forgemcp --no-admin
185
+ forgemcp --admin-port 9000
186
+ forgemcp --admin-ipc forgemcp-other
187
+ forgemcp --open-admin --admin-ipc forgemcp-other
188
+ ```
189
+
190
+ `--no-admin` opts that instance out of the panel. `--admin-port` is a preference
191
+ when starting a new Hub; it does not change an already running Hub's port.
192
+ Instances using the same `--admin-ipc` name share a Hub; a different name creates
193
+ a separate group for the same OS user.
194
+
195
+ The normal wheel build includes `admin.html` and `sandbox.html` alongside the MCP
196
+ widgets. After changing frontend source, use the documented wheel build or
197
+ `npm run build --prefix frontend`. An unbuilt source checkout returns an explicit
198
+ missing-assets page until those files are generated.
199
+
130
200
  ## Connect Codex or Claude Code
131
201
 
132
202
  After [installation](#install-a-release) or [source setup](#setup-from-source), copy the appropriate example into the root of the C/C++
@@ -297,6 +367,36 @@ no source preview because explicit external reads require confirmation.
297
367
 
298
368
  ## CMake profiles
299
369
 
370
+ CMake owns which configurations can supply clangd compilation contexts. After a
371
+ successful configure/generate, ForgeMCP reads `cmakeFiles-v1` from a new CMake
372
+ File API reply and records SHA-256 hashes of that build directory's configuration
373
+ inputs in `project/` and `storage/`. Hashes and dependency lists live in memory;
374
+ restart the server and run `cmake_configure` to confirm contexts again. Existing
375
+ File API replies and `compile_commands.json` alone do not confirm current inputs.
376
+
377
+ Workspace writes, edits, moves, deletions, and directory creation check known
378
+ inputs at the operation boundary. Actual input changes withdraw affected contexts
379
+ and notify clangd through the CMake subscription; identical content and ordinary
380
+ source/header edits keep contexts available unless those files are CMake inputs.
381
+ Known preset files are treated conservatively without resolving preset inheritance
382
+ or includes. CONFIGURE_DEPENDS globs conservatively invalidate on structural
383
+ changes; older File API replies without glob information use this policy for all
384
+ configurations. No project-wide dependency scan or CMake/glob parser is used.
385
+
386
+ Run `cmake_configure` to restore outdated or unknown contexts. `cmake_build` is
387
+ also allowed for outdated configurations: a new successful File API generation
388
+ can restore them even when later compilation fails. A successful build without
389
+ regeneration does not refresh the input baseline. Failed configure/generate cannot
390
+ reactivate old artifacts, and invalidation leaves build trees and databases intact.
391
+ Unchanged contexts and database contents retain their clangd sessions.
392
+
393
+ Checks occur only during Workspace mutations, configure, and build. There are no
394
+ filesystem watchers, polling, or background scans, and no guarantee of detecting
395
+ external editor/process changes. External `root/` dependencies are omitted from
396
+ hash tracking without elicitation or reads; changes to those inputs require an
397
+ explicit configure. Native preset contexts still require a known managed build
398
+ directory and a compilation database.
399
+
300
400
  `cmake_profiles` lists profiles. `cmake_configure`, `cmake_build`, and `cmake_test`
301
401
  operate on all profiles unless a nonempty `profiles` list selects a subset.
302
402
  Configuration must precede building, and building must precede testing. Profiles
@@ -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>ForgeMCP admin</title>
7
+ </head>
8
+ <body>
9
+ <main id="admin" class="fm-widget fm-admin" aria-label="ForgeMCP administration"></main>
10
+ <script type="module" src="/src/admin.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 cmake-run-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 && vite build --mode admin && vite build --mode sandbox",
7
7
  "test": "node --test tests/*.test.js"
8
8
  },
9
9
  "dependencies": {
@@ -0,0 +1,11 @@
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>ForgeMCP widget sandbox</title>
7
+ </head>
8
+ <body>
9
+ <script type="module" src="/src/admin-sandbox.js"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,43 @@
1
+ /** Relay one packaged App through a separate-origin, network-restricted sandbox. */
2
+ const parentOrigin = new URLSearchParams(location.hash.slice(1)).get("parent_origin");
3
+ let parentUrl;
4
+ try { parentUrl = new URL(parentOrigin); } catch { /* An invalid parent never receives messages. */ }
5
+ const loopback = (url) => url?.protocol === "http:"
6
+ && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname);
7
+
8
+ if (window.parent !== window && loopback(parentUrl) && parentUrl.origin !== location.origin) {
9
+ let view = null;
10
+ const internal = (message) => typeof message?.method === "string"
11
+ && message.method.startsWith("ui/notifications/sandbox-");
12
+ window.addEventListener("message", (event) => {
13
+ const message = event.data;
14
+ if (!message || message.jsonrpc !== "2.0") return;
15
+ if (event.source === window.parent && event.origin === parentUrl.origin) {
16
+ if (message.method === "ui/notifications/sandbox-resource-ready") {
17
+ if (view || typeof message.params?.html !== "string") return;
18
+ const parsed = new DOMParser().parseFromString(message.params.html, "text/html");
19
+ const csp = parsed.createElement("meta");
20
+ csp.httpEquiv = "Content-Security-Policy";
21
+ csp.content = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; "
22
+ + "img-src data: blob:; font-src data:; connect-src 'none'; frame-src 'none'; "
23
+ + "object-src 'none'; base-uri 'none'; form-action 'none'; navigate-to 'none'";
24
+ parsed.head.prepend(csp);
25
+ view = document.createElement("iframe");
26
+ view.title = "Tool result";
27
+ view.setAttribute("sandbox", "allow-scripts");
28
+ view.style.cssText = "display:block;width:100%;height:100%;border:0";
29
+ view.srcdoc = `<!doctype html>${parsed.documentElement.outerHTML}`;
30
+ document.body.replaceChildren(view);
31
+ } else if (!internal(message) && view) {
32
+ view.contentWindow.postMessage(message, "*");
33
+ }
34
+ } else if (view && event.source === view.contentWindow && !internal(message)) {
35
+ window.parent.postMessage(message, parentUrl.origin);
36
+ }
37
+ });
38
+ document.documentElement.style.cssText = "height:100%;margin:0;background:transparent";
39
+ document.body.style.cssText = "height:100%;margin:0;background:transparent";
40
+ window.parent.postMessage({
41
+ jsonrpc: "2.0", method: "ui/notifications/sandbox-proxy-ready", params: {},
42
+ }, parentUrl.origin);
43
+ }
@@ -0,0 +1,133 @@
1
+ import { AppBridge, PostMessageTransport, getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
2
+
3
+ /** Identify only immutable result resources linked by the original invocation. */
4
+ function linkedUris(result) {
5
+ const uris = new Set();
6
+ const resources = result?.structuredContent?.resources;
7
+ if (!resources || typeof resources !== "object") return uris;
8
+ for (const resource of Object.values(resources)) {
9
+ if (typeof resource?.uri === "string"
10
+ && /^forgemcp:\/\/workspace\/results\/[0-9a-f]{32}\/[a-zA-Z0-9_-]+\.(json|md)$/.test(resource.uri)) {
11
+ uris.add(resource.uri);
12
+ }
13
+ }
14
+ return uris;
15
+ }
16
+
17
+ /** Mount one original tool invocation; the frame receives no execution capability. */
18
+ export async function mountInvocationWidget({ container, tool, call, sandboxOrigin, readResource, theme, signal }) {
19
+ const uri = getToolUiResourceUri(tool);
20
+ if (!uri) throw new Error("This tool has no widget resource.");
21
+ const sandbox = new URL(sandboxOrigin);
22
+ if (sandbox.origin === location.origin || sandbox.protocol !== "http:"
23
+ || !["127.0.0.1", "localhost", "[::1]"].includes(sandbox.hostname)) {
24
+ throw new Error("The widget sandbox requires a separate local origin.");
25
+ }
26
+ const resource = await readResource(uri);
27
+ if (signal?.aborted) throw new DOMException("Widget loading cancelled.", "AbortError");
28
+ const html = resource?.contents?.find((item) => item.uri === uri && typeof item.text === "string");
29
+ if (!html || !String(html.mimeType ?? "").startsWith("text/html")) {
30
+ throw new Error("The widget HTML resource is unavailable.");
31
+ }
32
+ const allowed = linkedUris(call.result);
33
+ const frame = document.createElement("iframe");
34
+ frame.title = `${call.tool_name} result widget`;
35
+ frame.className = "admin-widget-frame";
36
+ frame.setAttribute("sandbox", "allow-scripts allow-same-origin");
37
+ let disposed = false;
38
+ let initialized = false;
39
+ let timeout;
40
+ let resize;
41
+ const context = () => ({
42
+ toolInfo: { id: call.id, tool },
43
+ theme: theme(),
44
+ displayMode: "inline",
45
+ availableDisplayModes: ["inline"],
46
+ locale: navigator.language,
47
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
48
+ platform: "web",
49
+ containerDimensions: { width: Math.max(1, container.clientWidth), maxHeight: 420 },
50
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
51
+ styles: { variables: { "--font-mono": '"Cascadia Code", Consolas, monospace' } },
52
+ });
53
+ const bridge = new AppBridge(
54
+ null,
55
+ { name: "ForgeMCP admin", version: "1.0.0" },
56
+ { serverResources: {} },
57
+ { hostContext: context() },
58
+ );
59
+ bridge.onreadresource = async ({ uri: requested }) => {
60
+ if (disposed || !allowed.has(requested)) throw new Error("Only resources linked by this invocation are available.");
61
+ return readResource(requested);
62
+ };
63
+ bridge.oncalltool = async () => { throw new Error("Result widgets cannot execute tools."); };
64
+ bridge.onlistresources = async () => ({ resources: [] });
65
+ bridge.onlistresourcetemplates = async () => ({ resourceTemplates: [] });
66
+ bridge.onlistprompts = async () => ({ prompts: [] });
67
+ bridge.onopenlink = async () => { throw new Error("External links are unavailable in result widgets."); };
68
+ bridge.onsizechange = ({ height }) => {
69
+ if (!disposed && Number.isFinite(height) && height > 0) frame.style.height = `${Math.min(420, height)}px`;
70
+ };
71
+ let resolveReady;
72
+ let rejectReady;
73
+ const ready = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
74
+ ready.catch(() => {});
75
+ bridge.onsandboxready = async () => {
76
+ try {
77
+ if (!disposed) await bridge.sendSandboxResourceReady({
78
+ html: html.text,
79
+ sandbox: "allow-scripts",
80
+ csp: { connectDomains: [], resourceDomains: [], frameDomains: [] },
81
+ });
82
+ } catch (error) { rejectReady(error); }
83
+ };
84
+ bridge.oninitialized = async () => {
85
+ if (disposed) return;
86
+ try {
87
+ initialized = true;
88
+ await bridge.sendToolInput({ arguments: call.arguments ?? {} });
89
+ if (call.result) await bridge.sendToolResult(call.result);
90
+ else if (["cancelled", "interrupted"].includes(call.status)) {
91
+ await bridge.sendToolCancelled({ reason: call.error ?? call.status });
92
+ }
93
+ clearTimeout(timeout);
94
+ resolveReady();
95
+ } catch (error) { rejectReady(error); }
96
+ };
97
+ const dispose = async () => {
98
+ if (disposed) return;
99
+ disposed = true;
100
+ clearTimeout(timeout);
101
+ resize?.disconnect();
102
+ if (initialized) {
103
+ try { await bridge.teardownResource({}, { timeout: 1000 }); } catch { /* A closed frame may no longer answer. */ }
104
+ }
105
+ try { await bridge.close(); } catch { /* Closing a disconnected bridge is harmless. */ }
106
+ frame.remove();
107
+ };
108
+ const aborted = () => {
109
+ rejectReady(new DOMException("Widget loading cancelled.", "AbortError"));
110
+ void dispose();
111
+ };
112
+ signal?.addEventListener("abort", aborted, { once: true });
113
+ container.append(frame);
114
+ try {
115
+ await bridge.connect(new PostMessageTransport(frame.contentWindow, frame.contentWindow));
116
+ if (disposed || signal?.aborted) throw new DOMException("Widget loading cancelled.", "AbortError");
117
+ timeout = setTimeout(() => rejectReady(new Error("The widget did not complete its connection handshake.")), 12000);
118
+ const url = new URL("/sandbox.html", sandbox);
119
+ url.hash = new URLSearchParams({ parent_origin: location.origin }).toString();
120
+ frame.src = url.href;
121
+ await ready;
122
+ resize = new ResizeObserver(() => { if (!disposed) bridge.setHostContext(context()); });
123
+ resize.observe(container);
124
+ return {
125
+ dispose: async () => { signal?.removeEventListener("abort", aborted); await dispose(); },
126
+ refreshContext: () => { if (!disposed) bridge.setHostContext(context()); },
127
+ };
128
+ } catch (error) {
129
+ signal?.removeEventListener("abort", aborted);
130
+ await dispose();
131
+ throw error;
132
+ }
133
+ }
@@ -0,0 +1,82 @@
1
+ /* The admin shell uses the same palette, fonts and compact controls as result widgets. */
2
+ .fm-admin { height: 100dvh; min-height: 380px; display: flex; flex-direction: column; background: var(--fm-bg); }
3
+ .fm-admin button, .fm-admin input, .fm-admin select, .fm-admin textarea { font: inherit; color: inherit; border: 1px solid var(--fm-border); background: var(--fm-bar); border-radius: 0; }
4
+ .fm-admin button { padding: 5px 9px; cursor: pointer; }
5
+ .fm-admin button:disabled { cursor: default; opacity: .55; }
6
+ .fm-admin :is(button, input, select, textarea):focus-visible { outline: 2px solid var(--fm-accent); outline-offset: 2px; }
7
+ .fm-admin button[aria-selected="true"], .fm-admin button[aria-pressed="true"] { color: var(--fm-accent); border-color: var(--fm-accent); }
8
+ .fm-admin h1, .fm-admin h2, .fm-admin h3 { margin: 0; font: inherit; font-weight: 600; overflow-wrap: anywhere; }
9
+ .admin-header { display: flex; align-items: center; gap: 12px; padding: 9px 14px; background: var(--fm-bar); border-bottom: 1px solid var(--fm-border); flex-wrap: wrap; }
10
+ .admin-header h1 { color: var(--fm-warning); margin-right: auto; }
11
+ .admin-header label { display: flex; align-items: center; gap: 6px; }
12
+ .admin-connection { color: var(--fm-muted); }
13
+ .admin-connection[data-live="true"] { color: var(--fm-success); }
14
+ .admin-instances { display: flex; gap: 5px; padding: 7px 10px; flex: none; border-bottom: 1px solid var(--fm-border); overflow-x: auto; }
15
+ .admin-instance-tab { display: flex; align-items: center; gap: 8px; max-width: 48ch; flex: none; text-align: left; white-space: normal; overflow-wrap: anywhere; }
16
+ .admin-dot { height: 7px; width: 7px; min-width: 7px; background: var(--fm-muted); }
17
+ .admin-dot[data-connected="true"] { background: var(--fm-success); }
18
+ .admin-statusbar { padding: 7px 14px; display: flex; gap: 6px 20px; flex-wrap: wrap; color: var(--fm-muted); border-bottom: 1px solid var(--fm-border); }
19
+ .admin-statusbar span { overflow-wrap: anywhere; }
20
+ .admin-pages { display: flex; gap: 5px; padding: 7px 10px; border-bottom: 1px solid var(--fm-border); flex-wrap: wrap; }
21
+ .admin-pages .admin-refresh { margin-left: auto; }
22
+ .admin-body { flex: 1; min-height: 0; overflow: auto; }
23
+ .admin-elicitations { flex: none; max-height: 50dvh; overflow: auto; border-bottom: 1px solid var(--fm-border); }
24
+ .admin-elicitations:empty { display: none; }
25
+ .admin-confirmation-link { color: var(--fm-accent); overflow-wrap: anywhere; }
26
+ .admin-notice { margin: 0; padding: 8px 14px; color: var(--fm-warning); background: var(--fm-bar); border-bottom: 1px solid var(--fm-border); overflow-wrap: anywhere; }
27
+ .admin-notice[hidden] { display: none; }
28
+ .admin-empty { padding: 28px 18px; color: var(--fm-muted); }
29
+ .admin-history { display: grid; grid-template-columns: minmax(230px, 29%) minmax(0, 1fr); height: 100%; }
30
+ .admin-history-sidebar { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--fm-border); }
31
+ .admin-history-filter { display: flex; gap: 7px; flex-wrap: wrap; padding: 10px; border-bottom: 1px solid var(--fm-border); }
32
+ .admin-history-filter input { width: 100%; padding: 6px 8px; }
33
+ .admin-history-count { color: var(--fm-muted); }
34
+ .admin-history-list { overflow-y: auto; flex: 1; }
35
+ .admin-call { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 8px; text-align: left; border-width: 0 0 1px !important; padding: 9px 10px !important; background: transparent !important; overflow-wrap: anywhere; }
36
+ .admin-call[aria-selected="true"] { background: var(--fm-hover) !important; box-shadow: inset 3px 0 var(--fm-accent); }
37
+ .admin-call-name { color: var(--fm-text); grid-column: 1 / -1; }
38
+ .admin-call-time { color: var(--fm-muted); font-size: 11px; }
39
+ .admin-state { color: var(--fm-muted); overflow-wrap: anywhere; }
40
+ .admin-state[data-state="running"], .admin-state[data-state="queued"] { color: var(--fm-warning); }
41
+ .admin-state[data-state="failed"], .admin-state[data-state="error"], .admin-state[data-state="interrupted"] { color: var(--fm-error); }
42
+ .admin-state[data-state="cancelled"] { color: var(--fm-warning); }
43
+ .admin-state[data-state="completed"], .admin-state[data-state="succeeded"] { color: var(--fm-success); }
44
+ .admin-detail { min-height: 0; overflow: auto; padding: 14px; }
45
+ .admin-detail-heading { display: flex; gap: 8px 16px; align-items: baseline; flex-wrap: wrap; margin-bottom: 10px; }
46
+ .admin-detail-heading h2 { color: var(--fm-accent); }
47
+ .admin-detail-meta { display: grid; grid-template-columns: 14ch minmax(0, 1fr); gap: 4px 10px; margin: 10px 0; }
48
+ .admin-detail-meta dt { color: var(--fm-muted); overflow-wrap: anywhere; }
49
+ .admin-detail-meta dd { margin: 0; overflow-wrap: anywhere; }
50
+ .admin-detail details { margin: 10px 0; }
51
+ .admin-detail summary, .admin-card summary { cursor: pointer; color: var(--fm-muted); }
52
+ .admin-result-tabs { display: flex; gap: 5px; padding: 7px 0; }
53
+ .admin-result { min-height: 70px; }
54
+ .admin-widget-frame { display: block; width: 100%; height: 420px; border: 0; }
55
+ .admin-pre { margin: 7px 0; padding: 10px; border: 1px solid var(--fm-border); background: var(--fm-bar); white-space: pre-wrap; overflow-wrap: anywhere; tab-size: 4; user-select: text; }
56
+ .admin-error { color: var(--fm-error); white-space: pre-wrap; overflow-wrap: anywhere; }
57
+ .admin-section { padding: 14px; }
58
+ .admin-section > h2 { margin-bottom: 12px; color: var(--fm-accent); }
59
+ .admin-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 310px), 1fr)); gap: 12px; }
60
+ .admin-card { border: 1px solid var(--fm-border); padding: 12px; min-width: 0; }
61
+ .admin-card h3 { margin-bottom: 8px; }
62
+ .admin-card p { white-space: pre-wrap; overflow-wrap: anywhere; margin: 7px 0; }
63
+ .admin-card .admin-pre { max-height: 500px; overflow: auto; }
64
+ .admin-table-scroll { overflow: auto; margin: 10px 0; }
65
+ .admin-table { width: 100%; border-collapse: collapse; text-align: left; }
66
+ .admin-table :is(th, td) { padding: 7px 9px; vertical-align: top; border: 1px solid var(--fm-border); overflow-wrap: anywhere; }
67
+ .admin-table th { color: var(--fm-muted); background: var(--fm-bar); font-weight: 400; }
68
+ .admin-catalog-group { margin-top: 18px; }
69
+ .admin-command-form { display: grid; gap: 9px; margin-bottom: 15px; border: 1px solid var(--fm-accent); padding: 12px; }
70
+ .admin-command-form label { display: grid; gap: 5px; }
71
+ .admin-command-form textarea { min-height: 140px; width: 100%; padding: 8px; resize: vertical; }
72
+ .admin-command-form select { width: 100%; padding: 6px; }
73
+ .admin-command-form button { justify-self: start; }
74
+ .admin-muted { color: var(--fm-muted); }
75
+ .admin-resource-title { overflow-wrap: anywhere; }
76
+ @media (max-width: 760px) {
77
+ .admin-history { grid-template-columns: 1fr; height: auto; min-height: 100%; }
78
+ .admin-history-sidebar { max-height: 40dvh; min-height: 160px; border-right: 0; border-bottom: 1px solid var(--fm-border); }
79
+ .admin-detail { overflow: visible; }
80
+ .admin-header { gap: 8px; }
81
+ .admin-header label { font-size: 11px; }
82
+ }