forge-cpp-mcp 0.2.5__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.5/README.md → forge_cpp_mcp-0.3.0/PKG-INFO +102 -3
  2. forge_cpp_mcp-0.2.5/PKG-INFO → forge_cpp_mcp-0.3.0/README.md +73 -28
  3. forge_cpp_mcp-0.3.0/frontend/admin.html +12 -0
  4. {forge_cpp_mcp-0.2.5 → 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.5 → forge_cpp_mcp-0.3.0}/frontend/vite.config.js +2 -0
  12. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/hatch_build.py +2 -0
  13. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/pyproject.toml +5 -1
  14. {forge_cpp_mcp-0.2.5 → 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.3.0/src/forgemcp/local_ipc/__init__.py +23 -0
  21. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/errors.py +21 -0
  22. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/service.py +159 -0
  23. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/unix.py +295 -0
  24. forge_cpp_mcp-0.3.0/src/forgemcp/local_ipc/windows.py +316 -0
  25. forge_cpp_mcp-0.3.0/src/forgemcp/process/host.py +95 -0
  26. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/service.py +16 -0
  27. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/progress.py +7 -0
  28. forge_cpp_mcp-0.3.0/src/forgemcp/server.py +486 -0
  29. forge_cpp_mcp-0.2.5/src/forgemcp/server.py +0 -200
  30. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/.gitignore +0 -0
  31. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/LICENSE +0 -0
  32. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/clangd-result.html +0 -0
  33. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/cmake-build.html +0 -0
  34. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/cmake-configure.html +0 -0
  35. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/cmake-profiles.html +0 -0
  36. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/cmake-run-test.html +0 -0
  37. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/cmake-test.html +0 -0
  38. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/package-lock.json +0 -0
  39. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/process-details.html +0 -0
  40. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/process-overview.html +0 -0
  41. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/clangd-navigation-view.js +0 -0
  42. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/clangd-result.js +0 -0
  43. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/clangd-view.js +0 -0
  44. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-build.js +0 -0
  45. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-configure.js +0 -0
  46. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-profiles.js +0 -0
  47. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-run-test.js +0 -0
  48. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-test.js +0 -0
  49. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/cmake-view.js +0 -0
  50. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/process-details.js +0 -0
  51. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/process-overview.js +0 -0
  52. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/process-status.js +0 -0
  53. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/process.css +0 -0
  54. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/shared/app.js +0 -0
  55. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/shared/copy-icon.js +0 -0
  56. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/shared/presentation.js +0 -0
  57. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/shared/result-resources.js +0 -0
  58. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/shared/result-view.js +0 -0
  59. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/shared/widget.css +0 -0
  60. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/source-view.js +0 -0
  61. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/toolsets.js +0 -0
  62. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-file.js +0 -0
  63. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-result.js +0 -0
  64. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-search.js +0 -0
  65. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-tree.js +0 -0
  66. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/src/workspace-view.js +0 -0
  67. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/tests/result-resources.test.js +0 -0
  68. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/tests/result-view.test.js +0 -0
  69. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/tests/workspace.test.js +0 -0
  70. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/toolsets.html +0 -0
  71. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/workspace-file.html +0 -0
  72. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/workspace-result.html +0 -0
  73. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/workspace-search.html +0 -0
  74. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/frontend/workspace-tree.html +0 -0
  75. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/assets.py +0 -0
  76. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/__init__.py +0 -0
  77. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/errors.py +0 -0
  78. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/models.py +0 -0
  79. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/service.py +0 -0
  80. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/session.py +0 -0
  81. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/clangd/text.py +0 -0
  82. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/__init__.py +0 -0
  83. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/build_tree.py +0 -0
  84. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/errors.py +0 -0
  85. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/file_api.py +0 -0
  86. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/models.py +0 -0
  87. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/profiles.py +0 -0
  88. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/service.py +0 -0
  89. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/text.py +0 -0
  90. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/cmake/workspace.py +0 -0
  91. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/completion.py +0 -0
  92. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/clangd.LICENSE +0 -0
  93. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/clangd.svg +0 -0
  94. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/cmake.svg +0 -0
  95. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/process.svg +0 -0
  96. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/toolchain.svg +0 -0
  97. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace-edit.svg +0 -0
  98. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace-file.svg +0 -0
  99. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace-search.svg +0 -0
  100. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/icons/workspace.svg +0 -0
  101. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/markdown.py +0 -0
  102. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/__init__.py +0 -0
  103. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/errors.py +0 -0
  104. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/models.py +0 -0
  105. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/text.py +0 -0
  106. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/process/transcript.py +0 -0
  107. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/text.py +0 -0
  108. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/__init__.py +0 -0
  109. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/discovery.py +0 -0
  110. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/errors.py +0 -0
  111. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/loader.py +0 -0
  112. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/__init__.py +0 -0
  113. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/system.py +0 -0
  114. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/user.py +0 -0
  115. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/providers/visual_studio.py +0 -0
  116. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/service.py +0 -0
  117. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/spec.py +0 -0
  118. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/__init__.py +0 -0
  119. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clang.py +0 -0
  120. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clang_cl.py +0 -0
  121. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clangd.py +0 -0
  122. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/clangxx.py +0 -0
  123. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/cmake.py +0 -0
  124. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/cppvsdbg.py +0 -0
  125. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/ctest.py +0 -0
  126. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/gcc.py +0 -0
  127. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/gdb.py +0 -0
  128. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/git.py +0 -0
  129. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/gxx.py +0 -0
  130. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/link.py +0 -0
  131. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/lld.py +0 -0
  132. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/lld_link.py +0 -0
  133. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/lldb_dap.py +0 -0
  134. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/make.py +0 -0
  135. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/msbuild.py +0 -0
  136. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/msvc.py +0 -0
  137. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/toolchain/tools/ninja.py +0 -0
  138. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/__init__.py +0 -0
  139. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/diff.py +0 -0
  140. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/errors.py +0 -0
  141. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/metadata.py +0 -0
  142. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/models.py +0 -0
  143. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/path.py +0 -0
  144. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/providers.py +0 -0
  145. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/service.py +0 -0
  146. {forge_cpp_mcp-0.2.5 → forge_cpp_mcp-0.3.0}/src/forgemcp/workspace/text.py +0 -0
@@ -1,3 +1,32 @@
1
+ Metadata-Version: 2.5
2
+ Name: forge-cpp-mcp
3
+ Version: 0.3.0
4
+ Summary: A safe MCP server for structured C++ project development workflows.
5
+ Project-URL: Homepage, https://github.com/hardened-steel/ForgeMCP
6
+ Project-URL: Repository, https://github.com/hardened-steel/ForgeMCP
7
+ Project-URL: Issues, https://github.com/hardened-steel/ForgeMCP/issues
8
+ Project-URL: Changelog, https://github.com/hardened-steel/ForgeMCP/releases
9
+ Author: ForgeMCP contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Requires-Python: >=3.13
13
+ Requires-Dist: markdown-it-py>=4.0
14
+ Requires-Dist: mcp[cli]==2.1.1
15
+ Requires-Dist: psutil<8,>=7.2
16
+ Requires-Dist: pydantic>=2.13
17
+ Requires-Dist: pywin32>=311; sys_platform == 'win32'
18
+ Requires-Dist: regex>=2024.11.6
19
+ Requires-Dist: starlette>=0.48.0
20
+ Requires-Dist: uvicorn>=0.31.1
21
+ Provides-Extra: build
22
+ Requires-Dist: build>=1.3; extra == 'build'
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.3; extra == 'dev'
25
+ Requires-Dist: pytest>=9.1; extra == 'dev'
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=9.1; extra == 'test'
28
+ Description-Content-Type: text/markdown
29
+
1
30
  # ForgeMCP
2
31
 
3
32
  ForgeMCP is a Python MCP server for structured C and C++ development workflows.
@@ -71,19 +100,20 @@ and install it in a virtual environment:
71
100
 
72
101
  ```powershell
73
102
  python -m venv .venv
74
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
103
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.3.0-py3-none-any.whl
75
104
  .\.venv\Scripts\forgemcp.exe --help
76
105
  ```
77
106
 
78
107
  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
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
80
110
  tools: install the ones needed by your workflows separately.
81
111
  The distribution name is `forge-cpp-mcp`; the Python module and server command are `forgemcp`.
82
112
 
83
113
  After the package is published to PyPI, the install command can instead be:
84
114
 
85
115
  ```powershell
86
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
116
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.3.0
87
117
  ```
88
118
 
89
119
  ## Setup from source
@@ -127,6 +157,75 @@ builds the wheel first and then starts the server for this repository.
127
157
 
128
158
  All MCP traffic uses stdout. Operational logs must go to stderr.
129
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
+
130
229
  ## Connect Codex or Claude Code
131
230
 
132
231
  After [installation](#install-a-release) or [source setup](#setup-from-source), copy the appropriate example into the root of the C/C++
@@ -1,28 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: forge-cpp-mcp
3
- Version: 0.2.5
4
- Summary: A safe MCP server for structured C++ project development workflows.
5
- Project-URL: Homepage, https://github.com/hardened-steel/ForgeMCP
6
- Project-URL: Repository, https://github.com/hardened-steel/ForgeMCP
7
- Project-URL: Issues, https://github.com/hardened-steel/ForgeMCP/issues
8
- Project-URL: Changelog, https://github.com/hardened-steel/ForgeMCP/releases
9
- Author: ForgeMCP contributors
10
- License-Expression: MIT
11
- License-File: LICENSE
12
- Requires-Python: >=3.13
13
- Requires-Dist: markdown-it-py>=4.0
14
- Requires-Dist: mcp[cli]==2.1.1
15
- Requires-Dist: pydantic>=2.13
16
- Requires-Dist: regex>=2024.11.6
17
- Provides-Extra: build
18
- Requires-Dist: build>=1.3; extra == 'build'
19
- Provides-Extra: dev
20
- Requires-Dist: build>=1.3; extra == 'dev'
21
- Requires-Dist: pytest>=9.1; extra == 'dev'
22
- Provides-Extra: test
23
- Requires-Dist: pytest>=9.1; extra == 'test'
24
- Description-Content-Type: text/markdown
25
-
26
1
  # ForgeMCP
27
2
 
28
3
  ForgeMCP is a Python MCP server for structured C and C++ development workflows.
@@ -96,19 +71,20 @@ and install it in a virtual environment:
96
71
 
97
72
  ```powershell
98
73
  python -m venv .venv
99
- .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.2.5-py3-none-any.whl
74
+ .\.venv\Scripts\python.exe -m pip install .\forge_cpp_mcp-0.3.0-py3-none-any.whl
100
75
  .\.venv\Scripts\forgemcp.exe --help
101
76
  ```
102
77
 
103
78
  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
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
105
81
  tools: install the ones needed by your workflows separately.
106
82
  The distribution name is `forge-cpp-mcp`; the Python module and server command are `forgemcp`.
107
83
 
108
84
  After the package is published to PyPI, the install command can instead be:
109
85
 
110
86
  ```powershell
111
- .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.2.5
87
+ .\.venv\Scripts\python.exe -m pip install forge-cpp-mcp==0.3.0
112
88
  ```
113
89
 
114
90
  ## Setup from source
@@ -152,6 +128,75 @@ builds the wheel first and then starts the server for this repository.
152
128
 
153
129
  All MCP traffic uses stdout. Operational logs must go to stderr.
154
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
+
155
200
  ## Connect Codex or Claude Code
156
201
 
157
202
  After [installation](#install-a-release) or [source setup](#setup-from-source), copy the appropriate example into the root of the C/C++
@@ -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
+ }