52 KiB
QML SceneGraph MCP Bridge — Plan
Goal
Build an MCP server that exposes QML SceneGraph introspection data (from GammaRay's probe) to LLMs for assisted debugging. The bridge is a GammaRay client: it connects to a probe injected into the target Qt/QML app, reads the same models the GammaRay GUI uses, and translates them into MCP JSON-RPC tool calls.
Architecture
Target Qt/QML App
│ GammaRay probe injected (preload/attach), loads quickinspector plugin
│
│ GammaRay binary protocol over TCP/local socket
│ (common/protocol.h — QDataStream-based, NOT JSON-RPC)
▼
QML SceneGraph MCP Bridge (new project, GPL-2.0-or-later)
│ Links gammaray_client + gammaray_common (installed from source)
│ Uses ClientConnectionManager to connect, ObjectBroker to get models
│ Connection is lazy: bridge starts without a probe, MCP client calls
│ connectProbe(host, port) to establish; auto-reconnects on probe restart.
│
│ MCP JSON-RPC over stdio (or HTTP+SSE)
▼
LLM / AI Agent
The bridge is a GammaRay client peer to gammaray-client (GUI client) and gammaray (launcher+client GUI). It does NOT touch the probe side; it reuses GammaRay's existing probe injection + protocol + model sync.
License
- GammaRay libraries are GPL-2.0-or-later. Linking
gammaray_clientmakes the bridge a derivative work → bridge must be GPL-compatible. - Accepted by user. No commercial license needed.
GammaRay source location
The GammaRay source used for investigation: /home/blumia/Sources/GammaRay (version 3.4.0 per version.txt).
Step 1: Build and install GammaRay from source [DONE]
System packages are too old (system has 2.11.3); protocol version must match between probe and client (common/protocol.h:141 Protocol::version()). Built both probe and client libs from the 3.4.0 source.
Verified environment: Qt 6.8.0 (system), qt6-base-private-dev / qt6-declarative-private-dev installed, CMake 3.31, Ninja 1.12, GCC 12.3.
# Reused existing build/ dir; only reconfigured install prefix.
cmake -S /home/blumia/Sources/GammaRay -B /home/blumia/Sources/GammaRay/build \
-DCMAKE_INSTALL_PREFIX=$(pwd)/install-prefix
cmake --build /home/blumia/Sources/GammaRay/build # already built
cmake --install /home/blumia/Sources/GammaRay/build
Installed to install-prefix/ in this project dir (NOT $HOME/.local as originally drafted — keeps the toolchain self-contained). Contents:
install-prefix/lib/libgammaray_{client,common,core,ui,...}-qt6_8-x86_64.so.3.4.0install-prefix/lib/cmake/GammaRay/(GammaRayConfig.cmake + GammaRayTarget.cmake + GammaRayMacros.cmake)install-prefix/bin/gammaray(launcher+client)install-prefix/include/gammaray/{client,common,core,launcher,ui}/
Verified at runtime: Protocol::version() = 38, Endpoint::defaultPort() = 11732. gammaray_client.so runtime-links libgammaray_ui-qt6_8-x86_64.so (PRIVATE link, as the plan predicted) → install-prefix/lib must be on LD_LIBRARY_PATH at runtime.
VERIFIED — exported target namespace (gotcha #8): CMakeLists.txt:931-932 has NAMESPACE GammaRay:: commented out. Confirmed in installed GammaRayTarget.cmake:22: targets are unnamespaced — gammaray_client, gammaray_common, gammaray_core, gammaray_ui, gammaray_kitemmodels, gammaray_launcher, gammaray_launcher_ui, gammaray_kuserfeedback, gammaray_probe. Use gammaray_client / gammaray_common directly (no GammaRay:: prefix).
Step 2: Create the bridge project
New working directory, separate from GammaRay source tree.
Dependencies
- GammaRay — installed from source (Step 1), provides
gammaray_client,gammaray_common - qtmcp (https://github.com/signal-slot/qtmcp) — Qt-based MCP protocol implementation, eliminates hand-writing JSON-RPC/stdio/handshake
Why qtmcp
qtmcp provides a complete MCP server framework (QMcpServer) with:
- Built-in
stdioandssebackends — no manual stdin/stdout handling - Protocol handshake + version negotiation (supports
2024-11-05and2025-03-26) registerToolSet()+Q_INVOKABLEauto-exposes QObject methods as MCP tools- Supports
QFuture<Result>for async tool results (matches GammaRay's async APIs likegetShader()) - Supports
QImagereturn values (matches GammaRay texture grab / screenshot) - License:
LGPL-3.0 OR GPL-2.0 OR GPL-3.0— choose GPL-2.0 to be compatible with GammaRay's GPL-2.0-or-later
qtmcp — consumed via FetchContent (no separate clone/install needed)
VERIFIED: qtmcp works with FetchContent_MakeAvailable() despite using Qt BuildInternals (qt_internal_project_setup() + qt_build_repo()). Tested empirically: configure + build + link all succeed. The resulting targets are Qt6::McpServer, Qt6::McpCommon, Qt6::McpClient (with Qt:: alias). Headers are generated into the consumer's build tree (${CMAKE_BINARY_DIR}/include/QtMcpServer/).
Skip examples/tests with the Qt-prefixed vars (not BUILD_EXAMPLES): -DQT_BUILD_EXAMPLES=OFF -DQT_BUILD_TESTS=OFF — verified to suppress the examples/ build dir.
Caveats of the FetchContent approach:
- qtmcp builds shared libs (
libQt6McpServer.so.6.10.2) into the consumer's build tree. The bridge exe needs RPATH orLD_LIBRARY_PATHpointing at${CMAKE_BINARY_DIR}/lib/x86_64-linux-gnu/at runtime. For a distributable bridge, either install qtmcp or switch to static (-DBUILD_SHARED_LIBS=OFF, verify). - MCP backend plugins (stdio, sse) are built as Qt plugins under
${CMAKE_BINARY_DIR}/lib/x86_64-linux-gnu/qt6/plugins/mcpserverbackend/. The bridge process must find them via the Qt plugin path /QT_PLUGIN_PATHat runtime, otherwiseQMcpServer("stdio")can't load the stdio backend. - qtmcp version at time of verification:
6.10.2(alpha1 prerelease, from.cmake.conf). PinGIT_TAGto a commit/tag for reproducibility rather than trackingmain.
qtmcp requirements
- Qt 6.8+ (check your Qt version; GammaRay needs 6.5+, qtmcp needs 6.8+)
- C++20 (GammaRay is C++17; bridge project compiled as C++20 can link C++17 libs fine)
- Uses Qt BuildInternals CMake style (
qt_build_repo())
CMakeLists.txt skeleton
cmake_minimum_required(VERSION 3.16)
project(QmlSceneGraphMcpBridge LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20) # qtmcp requires C++20
set(CMAKE_AUTOMOC ON)
# GammaRay installed from Step 1; pass -DCMAKE_PREFIX_PATH=$(pwd)/install-prefix
find_package(GammaRay REQUIRED)
find_package(Qt6 COMPONENTS Core Gui Widgets Network REQUIRED)
# qtmcp via FetchContent (no separate install)
include(FetchContent)
FetchContent_Declare(
qtmcp
GIT_REPOSITORY https://github.com/signal-slot/qtmcp.git
GIT_TAG main # pin to a tag/commit for reproducibility
GIT_SHALLOW TRUE
)
set(QT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(QT_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(qtmcp)
add_executable(qml-sg-mcp-bridge
src/main.cpp
src/gammaray_session.cpp # wraps ClientConnectionManager + ObjectBroker
src/gammaray_session.h
src/scenegraph_tools.cpp # Q_INVOKABLE methods exposed as MCP tools
src/scenegraph_tools.h
)
target_link_libraries(qml-sg-mcp-bridge PRIVATE
gammaray_client # VERIFIED: no GammaRay:: namespace
gammaray_common
Qt6::Core
Qt6::Gui
Qt6::Widgets # REQUIRED: see QApplication note below
Qt6::Network
Qt6::McpServer # qtmcp, provides Qt::McpServer alias
Qt6::McpCommon
)
# qtmcp shared libs + stdio plugin live in build tree; make runtime find them:
set_target_properties(qml-sg-mcp-bridge PROPERTIES
BUILD_RPATH "$<TARGET_FILE_DIR:Qt6::McpServer>;$<TARGET_FILE_DIR:Qt6::McpCommon>"
)
Build
cmake -S . -B build -DCMAKE_PREFIX_PATH=$(pwd)/install-prefix -G Ninja
cmake --build build
# Run with probe libs + qtmcp runtime libs on the path:
LD_LIBRARY_PATH=$(pwd)/install-prefix/lib QT_QPA_PLATFORM=offscreen \
QT_PLUGIN_PATH=$PWD/build/lib/x86_64-linux-gnu/qt6/plugins \
./build/qml-sg-mcp-bridge --connect tcp://127.0.0.1:11732
❗ MUST use QApplication, not QCoreApplication
VERIFIED the hard way: ObjectBroker::model() on the client side goes through ClientConnectionManager's model factory (client/clientconnectionmanager.cpp:44) which constructs a RemoteModel. RemoteModel's constructor (client/remotemodel.cpp:97) calls QApplication::style()->sizeFromContents(...). With a plain QCoreApplication there is no style → segfault the moment ready() fires and the first model is requested.
Fix: use QApplication (which pulls in Qt6::Widgets). This is consistent with client/main.cpp:38 (the standalone GammaRay client uses QApplication), and gammaray_client.so already runtime-links libQt6Widgets.so.6. Run the bridge headless with QT_QPA_PLATFORM=offscreen.
MCP server skeleton (using qtmcp)
#include <QApplication> // NOT QCoreApplication — see note above
#include <QtMcpServer/QMcpServer>
#include "scenegraph_tools.h"
int main(int argc, char **argv) {
QApplication app(argc, argv); // RemoteModel needs QApplication::style()
// ... parse --connect <url> ...
QMcpServer server(QStringLiteral("stdio")); // backend name in ctor, no default
server.setInstructions("QML SceneGraph introspection via GammaRay");
auto *tools = new SceneGraphTools(&server); // holds GammaRay client connection
server.registerToolSet(tools, {
{"listScenegraphNodes", "List all QSGNode tree nodes"},
{"getNodeVertices", "Get vertex data of a SG node"},
{"getShaderSource", "Get shader source for a material"},
{"setRenderMode", "Set SceneGraph render visualization mode"},
// ...
});
QObject::connect(&server, &QMcpServer::finished, &app, &QCoreApplication::quit);
// Connect to GammaRay probe, then start MCP server
tools->connectToProbe(url); // async, uses ClientConnectionManager
server.start();
return app.exec();
}
Tool implementation as Q_INVOKABLE methods (auto-registered via registerToolSet):
class SceneGraphTools : public QObject {
Q_OBJECT
public:
// NOTE: qtmcp's callTool() only supports void/bool/QString/QStringList/QImage
// returns (and QFuture<QList<QMcpCallToolResultContent>> for async). A
// QJsonObject return would hit qFatal() and crash. So structured results
// are returned as JSON-serialized QString:
Q_INVOKABLE QString listScenegraphNodes() const; // returns compact JSON
Q_INVOKABLE QString listQuickWindows() const;
// Async tool returns QFuture<QList<QMcpCallToolResultContent>> (the ONLY
// async return type qtmcp recognizes). NOT QFuture<QString>.
Q_INVOKABLE QFuture<QList<QMcpCallToolResultContent>> getShaderSource(int row) const;
// QImage return supported by qtmcp (for texture/screenshot)
Q_INVOKABLE QImage grabTexture(const QString &nodeId) const;
private:
GammaRaySession *m_session = nullptr;
};
VERIFIED against src/mcpserver/qmcpserversession.cpp (callTool() switch on mm.returnMetaType().id()): only Void/Bool/QString/QStringList/QImage (sync) + QFuture<QList<QMcpCallToolResultContent>> (async) are handled; the default: branch calls qFatal(). The plan's earlier QJsonObject / QFuture<QString> sketches were wrong and would have crashed.
qtmcp target names — VERIFIED
FetchContent produces targets Qt6::McpServer, Qt6::McpCommon, Qt6::McpClient (with Qt:: alias). Headers generated at ${CMAKE_BINARY_DIR}/include/QtMcpServer/ etc. The include #include <QtMcpServer/QMcpServer> works once Qt6::McpServer is linked. Note QMcpServer ctor takes the backend name explicitly: QMcpServer server(QStringLiteral("stdio")); (no default ctor).
Step 3: Client-side connection flow
Reference: client/clientconnectionmanager.h:43, client/client.h:27, common/objectbroker.h:28.
#include <QApplication> // QApplication, not QCoreApplication
#include <client/clientconnectionmanager.h>
#include <common/objectbroker.h>
// One-time init (registers stream operators, factory callbacks)
ClientConnectionManager::init();
QApplication app(argc, argv); // needed for RemoteModel's QApplication::style()
auto *conMan = new ClientConnectionManager(&app, false /*no splash*/);
QObject::connect(conMan, &ClientConnectionManager::ready, [&]() {
// Connection established, object map received, tool model populated.
// NOW safe to call ObjectBroker::model(...) etc.
auto *sgModel = ObjectBroker::model(
QStringLiteral("com.kdab.GammaRay.QuickSceneGraphModel"));
// ...
});
QObject::connect(conMan, &ClientConnectionManager::disconnected,
&app, &QCoreApplication::quit);
conMan->connectToHost(QUrl("tcp://127.0.0.1:11732"));
ClientConnectionManager::ready() is emitted after protocol handshake + object map sync — models are only available after this. Do NOT access models before ready(). VERIFIED: against a running 3.4.0 probe (qml target), ready() fires and ObjectBroker::model("com.kdab.GammaRay.QuickSceneGraphModel") returns a non-null RemoteModel* — full handshake works with matching protocol version 38.
Step 4: Available data and model registration names
All names verified from plugins/quickinspector/quickinspector.cpp and extension .cpp files. These are the strings to pass to ObjectBroker::model(QString).
Models (tree/tabular data via RemoteModel)
| Model name | Source | Description |
|---|---|---|
com.kdab.GammaRay.QuickWindowModel |
quickinspector.cpp:347 | List of QQuickWindows |
com.kdab.GammaRay.QuickItemModel |
quickinspector.cpp:352 | QQuickItem tree |
com.kdab.GammaRay.QuickSceneGraphModel |
quickinspector.cpp:371 | QSGNode tree (primary target) |
com.kdab.GammaRay.QuickSceneGraph.sgGeometryVertexModel |
sggeometryextension.cpp:30 | Vertex data of selected SG node |
com.kdab.GammaRay.QuickSceneGraph.sgGeometryAdjacencyModel |
sggeometryextension.cpp:31 | Adjacency/draw mode of selected SG node |
com.kdab.GammaRay.QuickSceneGraph.shaderModel |
materialextension.cpp:39 | Shader list for selected material |
com.kdab.GammaRay.QuickSceneGraph.materialPropertyModel |
materialextension.cpp:38 | Material properties |
com.kdab.GammaRay.QuickItem.<...> |
property controller extensions | Item properties (suffix varies) |
Model name pattern for property-controller models: <objectBaseName>.<nameSuffix> where objectBaseName is com.kdab.GammaRay.QuickSceneGraph or com.kdab.GammaRay.QuickItem (quickinspector.cpp:327-329). The suffix is registered via PropertyController::registerModel(model, nameSuffix) (core/propertycontroller.cpp:62-64).
Remote objects (interfaces via ObjectBroker::object())
| Interface | Object name pattern | Key methods |
|---|---|---|
QuickInspectorInterface |
com.kdab.GammaRay.QuickInspectorInterface (IID-based, use ObjectBroker::object<QuickInspectorInterface*>()) |
selectWindow(int), setCustomRenderMode(RenderMode), setSlowMode(bool), analyzePainting() |
MaterialExtensionInterface |
<baseName>.material (e.g. com.kdab.GammaRay.QuickSceneGraph.material) |
getShader(int row) → emits gotShader(QString) |
Client-side factory registration
The GUI client registers factories via ObjectBroker::registerClientObjectFactoryCallback<T>() (quickinspectorwidget.cpp:82, 268). The bridge must do the same for interfaces it wants to use:
ObjectBroker::registerClientObjectFactoryCallback<QuickInspectorInterface*>(
[](const QString &, QObject *parent) -> QObject* {
return new QuickInspectorClient(parent); // or own impl
});
Check whether QuickInspectorClient (quickinspectorclient.h) is exported/installed. If not, may need a thin reimplementation calling Endpoint::invokeObject() directly. Verify after install by checking installed headers in $INSTALL_PREFIX/include/gammaray/.
RESOLVED (open question #1): QuickInspectorClient, QuickInspectorInterface, MaterialExtensionInterface and all other plugins/quickinspector/* headers are NOT in install-prefix/include/gammaray/. Only client/ (3 headers: clientconnectionmanager.h, processtracker.h, gammaray_client_export.h), common/, core/, launcher/, ui/ are installed. The plugin client-side classes live only in the GammaRay source tree (plugins/quickinspector/) and are compiled into the per-tool gammaray_quickinspector client plugin .so that the GUI client loads dynamically. The bridge does NOT load these plugins. Good news (verified): Endpoint::invokeObject(name, method, args) is PUBLIC in the installed endpoint.h, and it only needs the name→address mapping (synced at handshake) — NOT a local stub object. So the bridge can call probe-side interface methods directly by IID + bare slot name, with zero stub code. See the "Key implementation findings" in the Verification status section for the exact name (com.kdab.GammaRay.QuickInspectorInterface/1.0) and method ("selectWindow") conventions. Only for receiving signals (e.g. MaterialExtensionInterface::gotShader) would a real client stub be needed — handled case-by-case when implementing those tools.
Step 5: SceneGraph model roles and structure
QuickSceneGraphModel (plugins/quickinspector/quickscenegraphmodel.h:34) inherits ObjectModelBase<QAbstractItemModel> (core/objectmodelbase.h:40).
Columns
- Column 0: node address string (
Util::addressToString(node)) - Column 1: node type string ("Node", "Geometry Node", "Transform Node", "Clip Node", "Opacity Node", "Root Node", "Render Node")
Roles (from ObjectModelBase + QuickSceneGraphModel)
Qt::DisplayRole— address / type (see columns above)ObjectModel::ObjectRole—QVariant::fromValue(node)(QSGNode* as void*)ObjectModel::ObjectIdRole—ObjectIdwrapperObjectModel::DecorationIdRole— icon idObjectModel::CreationLocationRole/DeclarationLocationRole— source locations
ObjectModel roles are defined in common/objectmodel.h (read it for exact enum values).
Tree structure
- Root: the topmost QSGNode (walks up from contentItem's itemNode to find true root, quickscenegraphmodel.cpp:79-90)
- Children:
QSGNode::firstChild()/nextSibling() - Updates: refreshed on
QQuickWindow::afterRenderingsignal - Node deletion:
QuickSceneGraphModel::nodeDeleted(QSGNode*)signal
Caveat: QSGNode* values in the model are raw pointers in the probe's process. They cannot be dereferenced from the client. Use them only as opaque IDs (via ObjectId) for selection and model navigation. Actual geometry/material/texture data must be fetched through the corresponding models/extensions.
Step 6: Suggested MCP tools
Map GammaRay client capabilities to MCP tool calls.
Connection management tools (NEW — implemented)
connectProbe(host?, port?)→GammaRaySession::connectToHost(tcp://host:port), block up to 6s for handshake, return{connected, state, url, error?}. Defaults127.0.0.1:11732. URL remembered for auto-reconnect.connectProbeDefault()→ convenience wrapper forconnectProbe(127.0.0.1, 11732)(qtmcp doesn't support arg-overloading, so two tool entries).probeStatus()→ return{state, ready, url, error?}. No side effects.disconnectProbe()→ drop connection, clear remembered URL (stops auto-reconnect).
All introspection tools below call session->ensureConnected(timeoutMs) first; if a URL is known and the session is Disconnected/Failed, they transparently reconnect. If no URL is known, they return a JSON error pointing the client at connectProbe().
Navigation tools
listQuickWindows()→ readcom.kdab.GammaRay.QuickWindowModel, return window addressesselectQuickWindow(index)→QuickInspectorInterface::selectWindow(int)listQuickItems()→ traversecom.kdab.GammaRay.QuickItemModel, return item tree (id, type, geometry) — not yet implementedlistScenegraphNodes()→ traversecom.kdab.GammaRay.QuickSceneGraphModel, return node tree (id, type, address). UsesprimeAndWait()for async fill.selectScenegraphNode(address)→ select node via selection model (currently blocked — see Known issues)
Geometry tools
getNodeVertices(address)→ select SG node, readsgGeometryVertexModel(vertices: x, y, z, w + render role) — blockedgetNodeAdjacency(address)→ readsgGeometryAdjacencyModel(drawing mode, vertex indices) — blocked
Material/Shader tools
getMaterialShaders(address)→ readshaderModelfor selected node — blockedgetShaderSource(row)→MaterialExtensionInterface::getShader(int)(async: listen forgotShadersignal) — implemented, untested end-to-end (blocked upstream)getMaterialProperties(address)→ readmaterialPropertyModel— blocked
Rendering visualization tools
setRenderMode(mode)→QuickInspectorInterface::setCustomRenderMode(mode)where mode ∈ {NormalRendering, VisualizeClipping, VisualizeOverdraw, VisualizeBatches, VisualizeChanges, VisualizeTraces} (quickinspectorinterface.h:52-66). NOTE: takes enum NAME string, not int. Values map 1:1 to GammaRay'sRenderModeenum.setSlowMode(enabled)→QuickInspectorInterface::setSlowMode(bool)
Texture tools
grabTexture(objectId)→TextureExtension(async image grab; image arrives viaRemoteViewInterfaceasTransferImage) — not yet implemented
Implementation status (updated — see "Known issues / Blocked" section for details on the sub-model population blocker)
Working end-to-end (verified via stdio MCP smoke test against a live probe):
- ✅
connectProbe(host?, port?)/connectProbeDefault()/disconnectProbe()/probeStatus()— lazy connect + auto-reconnect state machine (Disconnected → Connecting → Ready, with→ Failedon persistent error).disconnectProbe()clears remembered URL and stops auto-reconnect. - ✅
listQuickWindows()— readsQuickWindowModel, returns[{address, type}]. - ✅
selectQuickWindow(index)— forwards viaEndpoint::invokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectWindow", {index}). - ✅
listScenegraphNodes()— returns real QSGNode tree (verified: returns 11 GeometryNodes on the test QML). Uses signal-drivenprimeAndWait()(see Implementation findings) — first call may show "Loading..." for deep nodes, second call returns the full tree. - ✅
setRenderMode(mode)— accepts render-mode enum name string (NOT int):NormalRendering/VisualizeClipping/VisualizeOverdraw/VisualizeBatches/VisualizeChanges/VisualizeTraces. CallsinvokeObject(..., "setCustomRenderMode", {QVariant::fromValue<RenderMode>(mode)}). Cross-process QVariant serialization works thanks toquickinspector_types.hRenderModeenum +Q_DECLARE_METATYPEwith matching fully-qualified nameGammaRay::QuickInspectorInterface::RenderMode. - ✅
setSlowMode(enabled)— callsinvokeObject(..., "setSlowMode", {bool}). Verified by reading back state.
Implemented but BLOCKED by selection sync issue — DISABLED (wrapped in #if 0 with TODO markers; registration removed from main.cpp). Re-enable when the bug is fixed:
- ⏳
selectScenegraphNode(address)(disabled) — finds node in SG tree, selects viaObjectBroker::selectionModel(sgModel)+ directMessagesend viaProtocol::fromQModelIndex. Cachesm_selectedAddressto avoid redundant reselection. Selection IS sent and IS received by the probe (sgSelectionChanged()fires), but the probe-side selection maps to aQSGTransformNode(row 0) instead of the targetQSGGeometryNode. Sub-models stay empty. - ⏳
getNodeVertices(address)(disabled) — readssgGeometryVertexModel(display + headers +kIsCoordinateRole=257). ReturnsrowCount=0because the model is never populated (selection wrong on probe side). - ⏳
getNodeAdjacency(address)(disabled) — readssgGeometryAdjacencyModel(drawing mode + indices,kDrawingModeRole=257,kRenderRole=258). Same blocker. - ⏳
getMaterialShaders(address)(disabled) — readsshaderModel(shader stage names). Same blocker. - ⏳
getShaderSource(row)(disabled) — async path implemented:MaterialExtensionInterfaceproxy (material_interface.{h,cpp}) with matching IIDcom.kdab.GammaRay.MaterialExtensionInterface, self-registers viaObjectBroker::registerObject, factory registered ininitOnce(). CallsinvokeObject(..., "getShader", {row})and waits forgotShader(QString)signal. Untested end-to-end because the upstreamshaderModelis never populated. - ⏳
getMaterialProperties(address)(disabled) — readsmaterialPropertyModel. Same blocker.
Not yet implemented:
- ❌
grabTexture(objectId)—TextureExtensionasync image grab viaRemoteViewInterfaceTransferImage. Not started (depends on selection working first to populate the texture list).
Known issues / Blocked — sub-models not populating after SG node selection
This is the single biggest open issue. Extensive debugging has narrowed it down but not yet fixed it.
Symptom: After selectScenegraphNode(address) selects a QSGGeometryNode, all four sub-models (sgGeometryVertexModel / sgGeometryAdjacencyModel / shaderModel / materialPropertyModel) stay at rowCount=0 / columnCount=0. Tested against all 11 GeometryNodes in the test QML — none populate.
What's been verified working:
- All four sub-model addresses are valid on the probe (vertex=161, adjacency=162, shader=154, materialProp=153, selectionModel=142).
- The client-side selection model name is correct:
"com.kdab.GammaRay.QuickSceneGraphModel.selection"(NOT".selection"—_diag_dotSelAddr=0confirms the dotted form is wrong). sourceModelForProxy(sgFilterProxy)returnssgFilterProxyitself (it's registered), soSelectionModelServeris created directly for it with the name matching the client'sSelectionModelClient.- Client-side selection IS correct:
hasSelection()=true,selectedRows()returns the expected index with the full path matching the target address. Endpoint::isConnected()=true— selection messages ARE being sent.- Probe-side
QuickInspector::sgSelectionChanged()IS being called (confirmed viaqWarning()debug logging in the journal — added to/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:737).
Root cause identified (still being narrowed down):
The server's sendSelection() (triggered by SelectionModelStateRequest from SelectionModelClient's 125ms timer, OR by a model-change timer) selects the DEFAULT item (row 0 at root = QSGTransformNode) and sends SelectionModelSelect back to the client, overriding our selection. Even with: a 2000ms settle before selecting, re-sending the selection twice, manually constructing SelectionModelSelect messages via Protocol::fromQModelIndex, and turning off slow mode — the sub-models still return rowCount=0.
The second sgSelectionChanged call shows parentValid=true parentRow=0 — selection IS reaching a child node, but it's a QSGTransformNode, NOT our QSGGeometryNode. The path (0,0)→(0,0)→...→(0,0) is either:
(a) being truncated on the wire,
(b) mapping to the wrong node on the server side via translateSelection() / toQModelIndex(), OR
(c) the server's default-selection response is arriving AFTER our selection and overriding it.
Things tried that did NOT fix it:
- Waiting longer (2s settle before + after selection, re-sending selection twice).
- Disabling slow mode before selection.
- Manually constructing
SelectionModelSelectmessages viaProtocol::fromQModelIndex(bypassingSelectionModelClient). - Turning off slow mode.
Things NOT yet tried (see Next steps):
- Add diagnostic logging to the probe-side
NetworkSelectionModel::translateSelection()andtoQModelIndex()to trace exactly what path arrives and how it's walked. - Check whether the server's
sendSelection()response is arriving AFTER our selection and overriding it (could disable theSelectionModelClient'srequestSelection125ms one-shot timer, or callsendSelection()on the server after our selection to force-sync). - Patch
QuickInspector::selectSGNode()(line 474, currently NOT Q_INVOKABLE) to make it Q_INVOKABLE so the bridge can call it directly viainvokeObject, bypassing the whole selection-sync mechanism. - Run the GammaRay GUI client (
xvfb-runorQT_QPA_PLATFORM=offscreen) against the same probe to verify the selection mechanism works at all with this probe setup — if the GUI also fails to populate sub-models, the bug is in the probe plugin, not the bridge.
Half-finished artifacts to clean up once the blocker is resolved
- Debug
qWarning()logging in probe plugin:/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:737(SGDBG:lines). Must rebuild+install the probe plugin after any change:cmake --build /home/blumia/Sources/GammaRay/build -j --target gammaray_quickinspector && cmake --install /home/blumia/Sources/GammaRay/build --prefix $(pwd)/install-prefix. m_lastSelectPathdiagnostic field inSceneGraphTools: added toscenegraph_tools.{h,cpp}to log the serialized selection path. Pure diagnostic — should be removed (or kept behind a#ifndef QT_NO_DEBUG) once the issue is fixed.- Disabled tool registrations in
main.cpp+scenegraph_tools.{h,cpp}: 6 broken tools (selectScenegraphNode/getNodeVertices/getNodeAdjacency/getMaterialShaders/getShaderSource/getMaterialProperties) wrapped in#if 0or commented out withTODOmarkers. Remove the#if 0/ uncomment when the blocker is fixed. listScenegraphNodesfirst-call fill stability — first call still shows "Loading..." for deep nodes; second call returns the full tree. TheprimeAndWait()loop already handles this, but a single-call fix would be nicer.
Step 7: Launch and run
Connection model (decided)
Phase 1 (now): lazy connect + explicit connectProbe tool + auto-reconnect. The bridge does NOT hard-require a probe URL at startup. A --connect <url> (or GAMMARAY_PROBE_URL env) is treated as a default URL — the bridge tries it best-effort on startup but won't fail if the probe isn't up yet. The MCP client drives connection lifecycle via three tools:
connectProbe(host?, port?)— establish a connection to a probe (defaults127.0.0.1:11732). Blocks up to ~6s for the handshake. Returns JSON{connected, state, url, error?}. The URL is remembered for subsequent auto-reconnect.probeStatus()— report{state, ready, url, error?}without side effects.disconnectProbe()— drop the current connection and forget the URL (auto-reconnect stops untilconnectProbeis called again).
Every introspection tool (listQuickWindows, selectQuickWindow, listScenegraphNodes, …) calls session->ensureConnected(timeoutMs) first: if a URL is known and the session is Disconnected/Failed, it transparently re-issues connectToHost(url) and waits for ready(). If no URL is known, the tool returns a JSON error pointing the user at connectProbe(). So in practice a client only needs to call connectProbe once after starting the probe; subsequent tool calls keep working across probe restarts.
Rationale: opencode (and other MCP hosts) launch the bridge subprocess at host startup, typically BEFORE the developer has started the target app + probe. A hard --connect either succeeds by luck or leaves the bridge in a permanently-broken state (the original scaffold's failure mode). The lazy-connect model lets the bridge come up clean and wait for the client to point it at a probe — and recover automatically when the probe is restarted mid-session (common during iterative QML debugging).
Why not auto-discovery (mDNS)? GammaRay's probe does broadcast via mDNS, which would let the bridge find probes with zero configuration. But (a) multiple probes on a dev machine are common, and silently picking one is wrong; (b) mDNS on Linux is flaky across sandboxes/containers; (c) the explicit connectProbe is one cheap MCP call and gives the client full control. mDNS can be added later as an optional discoverProbes() tool if needed.
Auto-reconnect details: ClientConnectionManager already retries transientConnectionError (host not yet up) every 1s for 60s before emitting persistentConnectionError — so the INITIAL handshake has built-in resilience. But a disconnected signal (probe killed / target crashed MID-session) is NOT auto-retried by the manager. The bridge adds its own 1s-backoff reconnect on disconnected (guarded against re-entrancy and disabled after explicit disconnectProbe()). On persistentConnectionError the bridge does NOT auto-retry (GammaRay already did for 60s); the user calls connectProbe() again once the probe is back. State machine: Disconnected → Connecting → Ready, with → Failed on persistent error (recoverable only via connectProbe()).
Phase 2 (future, not in scope now): hybrid mode. Bridge could also expose attach(pid) / launch(path) MCP tools for LLM-driven dynamic switching using launcher/core/launcher.h and launcher/core/clientlauncher.h. Out of scope for now.
Phase 1 startup sequence
# 1. Developer starts target app with probe (manual)
install-prefix/bin/gammaray --inject-only --listen tcp://127.0.0.1:11732 \
/usr/lib/qt6/bin/qml /path/to/app.qml -platform offscreen
# 2. MCP host launches bridge (stdio). --connect is OPTIONAL: if given, it's
# used as a default URL and tried best-effort at startup; if omitted, the
# client must call connectProbe() before any introspection tool returns data.
# Either way, tools auto-reconnect across probe restarts.
./run.sh # no --connect required
# or: ./run.sh --connect tcp://127.0.0.1:11732
# 3. (MCP client) call connectProbe(host, port) once the probe is up — or just
# call listQuickWindows/etc. and the bridge will return a JSON error hinting
# at connectProbe if no URL is configured.
For headless/CI launching of the probe, systemd-run --user --no-block (with StandardOutput=file:... + Environment=... properties) is a reliable way to fully detach the probe so the launching shell returns immediately — plain &/disown backgrounding tends to hang the parent shell's stdout pipe.
Bridge accepts an optional default probe URL via --connect <url> (or GAMMARAY_PROBE_URL env). At runtime the connection is driven by connectProbe/disconnectProbe MCP tools; the URL is remembered so tools can auto-reconnect after a probe restart. The bridge's GammaRaySession state machine (Disconnected → Connecting → Ready, with → Failed on persistent error) is the single source of truth; probeStatus() reports it to the client.
The bridge acts as an MCP server on stdio. The LLM host (e.g. Claude Desktop, opencode) launches the bridge as a subprocess and communicates via JSON-RPC over stdio.
Key gotchas to remember
- Protocol version lock: probe and client must be same GammaRay build. System package too old → build from 3.4.0 source. (
common/protocol.h:141) - GPL-2.0-or-later: bridge linking gammaray_client must be GPL-compatible. Accepted. qtmcp offers GPL-2.0-only option — compatible.
- Qt private headers: GammaRay build needs
qt6-base-private-dev,qt6-declarative-private-dev. The bridge itself only needs public Qt + installed GammaRay headers. qtmcp requires Qt 6.8+ (newer than GammaRay's 6.5+ floor); ensure build environment Qt >= 6.8. - C++20: qtmcp requires C++20. Bridge project set to C++20; GammaRay C++17 libs link fine.
- Models only available after
ClientConnectionManager::ready()— do not callObjectBroker::model()before handshake completes. MCP tool calls arriving before probe connection should return an error or wait. - QSGNode pointers are opaque IDs on client side — cannot dereference. Use ObjectId for selection/navigation; fetch real data via property/extension models.
- Async interfaces:
MaterialExtensionInterface::getShader()returns via signalgotShader(QString), not synchronously. Texture grabs return viaRemoteViewInterface. Bridge must handle async and correlate to MCP requests. qtmcp supportsQFuture<Result>return types for async tool handlers. - Target namespace: exported CMake targets may lack
GammaRay::prefix (NAMESPACE commented out at CMakeLists.txt:931). Verify in installedGammaRayTarget.cmake. Same applies to qtmcp installed targets — verify after building qtmcp. - OpenGL dependency: SG geometry/material/texture extensions only build when
QT_NO_OPENGLis not set (quickinspector/CMakeLists.txt:47-66). If headless/no-GL build, these models won't exist. Ensure GammaRay is built WITH OpenGL (default). - In-process mode exists (
gammaray --inprocess): probe + client in same process viagammaray_inprocessuimodule. Not useful for bridge (bridge IS the client). Use TCP/local socket mode. - Probe ABI: probes are versioned by Qt ABI.
GAMMARAY_MULTI_BUILD=ON(default) builds multiple. The launcher picks the right one. Bridge doesn't need to worry about probe ABI, only client ABI matching the installedgammaray_client. - QApplication required (NEW, verified): the client-side
RemoteModelctor callsQApplication::style()->sizeFromContents(). AQCoreApplication-only bridge segfaults on the firstObjectBroker::model()call afterready(). UseQApplication+QT_QPA_PLATFORM=offscreenfor the headless bridge. - qtmcp shared libs + plugin path (NEW, verified): under FetchContent,
libQt6Mcp*.soand themcpserverbackend/libqmcpserverstdio.soplugin land in the consumer's build tree. SetLD_LIBRARY_PATHandQT_PLUGIN_PATH(or RPATH) so the bridge finds them at runtime, elseQMcpServer("stdio")can't load the stdio backend.
Files worth re-reading in GammaRay source
When in doubt, these files have the ground truth:
plugins/quickinspector/quickinspector.cpp:322-402— model + object registration namesplugins/quickinspector/quickscenegraphmodel.{h,cpp}— SG node tree modelplugins/quickinspector/quickinspectorinterface.h:38-66— RenderMode enum, Feature flagsplugins/quickinspector/materialextension/materialextensioninterface.h— shader APIplugins/quickinspector/geometryextension/sggeometrymodel.h— vertex/adjacency modelscommon/objectbroker.h— model/object retrieval APIcommon/objectmodel.h— ObjectRole enum valuesclient/clientconnectionmanager.h:43— connection lifecycleclient/client.h:27— low-level client endpointclient/remotemodel.h— client-side model wrappercore/objectmodelbase.h:40— base model roles/columnsGammaRayConfig.cmake.in— installed CMake package contents
Open questions to resolve during implementation
AreRESOLVED: No. See Step 4 note above — bridge needs own thin client stubs callingQuickInspectorClientandMaterialExtensionClientclasses exported in installed headers?Endpoint::invokeObject().- Exact
ObjectModelrole enum values — readcommon/objectmodel.hwhen implementing. (Installed atinstall-prefix/include/gammaray/common/objectmodel.h.) - How does
RemoteModel(client/remotemodel.h) expose data? It's a QAbstractItemModel proxying the server model. Standard model API applies. VERIFIED:ObjectBroker::model()returns a non-nullQAbstractItemModel*that is aRemoteModel— standardQAbstractItemModelAPI works. - Texture image transfer:
TransferImage(common/transferimage.h) over the wire, arrives viaRemoteViewInterface. Need to trace the exact signal path for texture grabs. Still open.
Verification status (as of this revision)
- ✅ Step 1 done: GammaRay 3.4.0 built + installed to
install-prefix/. Protocol version 38. - ✅ Minimal client built & run:
minimal-client/linksgammaray_client+gammaray_common, connects to a live probe, receivesready(), fetchesQuickSceneGraphModel/QuickWindowModelviaObjectBroker— clean exit. Proves the client ABI + protocol path is viable for the bridge. - ✅ qtmcp FetchContent viable:
Qt6::McpServer/Qt6::McpCommonbuild & link from aFetchContent_MakeAvailable()call. No separate clone/install. - ✅ Bridge scaffold built & working end-to-end (
bridge/):qml-sg-mcp-bridgelinks GammaRay client + qtmcp, runs as a stdio MCP server. Smoke test (/tmp/opencode/smoke_test.py) against a live probe confirms:initialize→ serverInfo + capabilities + protocolVersion2024-11-05✅tools/list→ 15 tools registered:connectProbe,connectProbeDefault,disconnectProbe,getMaterialProperties,getMaterialShaders,getNodeAdjacency,getNodeVertices,getShaderSource,listQuickWindows,listScenegraphNodes,probeStatus,selectQuickWindow,selectScenegraphNode,setRenderMode,setSlowMode✅probeStatus(before connect) →{"ready":false,"state":"disconnected","url":""}✅connectProbeDefault()→{"connected":true,"state":"ready","url":"tcp://127.0.0.1:11732"}✅probeStatus(after connect) →{"ready":true,"state":"ready","url":"tcp://127.0.0.1:11732"}✅listQuickWindows→[{"address":"0x... (ApplicationWindow_QMLTYPE_0)","type":""}]✅selectQuickWindow(0)→{"index":0,"status":"selected"}✅setSlowMode(true)→{"slowMode":true}✅setRenderMode("VisualizeOverdraw")→{"renderMode":"VisualizeOverdraw"}✅ (NOTE: takes enum name string, NOT int — see "Known issues")setRenderMode("NormalRendering")→{"renderMode":"NormalRendering"}✅disconnectProbe()→{"ready":false,"state":"disconnected","url":""}✅listScenegraphNodes(round 2, after 1s sleep) → full QSGNode tree with 11 GeometryNodes ✅
- ✅ Connection model revised: bridge no longer hard-requires
--connectat startup. Lazy connect +connectProbeMCP tool + auto-reconnect on dropped session. See "Connection model (decided)" in Step 7. State machine (Disconnected → Connecting → Ready, with→ Failedon persistent error) exposed viaprobeStatus().
Key implementation findings (new, verified during scaffold work)
Endpoint::invokeObject()needs the object name == interface IID, NOT the bare name.QuickInspectorInterfaceis registered viaObjectBroker::registerObject<QuickInspectorInterface*>(this)(quickinspectorinterface.cpp:59), which derives the name fromqobject_interface_iid="com.kdab.GammaRay.QuickInspectorInterface/1.0". The bare"com.kdab.GammaRay.QuickInspectorInterface"is NOT in the client's name map →Q_ASSERT(obj)crash in debug builds. Also the method arg is the bare slot name"selectWindow"(no(int)signature), matchingQuickInspectorClient::selectWindow.- No client-side stub object needed for
invokeObject: it only uses the name→address mapping (synced at handshakeready()), not a local object. SoselectQuickWindowworks without registering any factory/stub —Endpoint::invokeObjectis public in the installedendpoint.h. (This resolves open question #1's "approach (b)" more cleanly than expected.) - RemoteModel is lazily fetched (client/remotemodel.cpp):
data(),rowCount(),hasChildren()return placeholders ("Loading..." / 0 / false) and queue server fetches whose results arrive async viadataChanged/rowsInserted. So a tool that reads a model right afterready()gets stale/empty data. The scaffold handles this with:waitForReady()(block until handshake),waitForData()(poll column 0 until non-"Loading..."), and for the SG tree aprimeAndWait()(see below) before snapshotting withwalkChildren(). primeAndWait()— signal-driven tree loading (new): replaces the old fixed-timeprimeTree()+settle()loop. TracksdataChanged/rowsInsertedsignals on the model, settles until quiet, then a final long settle. SignatureprimeAndWait(m, rounds, settleMs, quietMs, finalSettleMs)— 6 rounds of prime+settle (300ms settle, 500ms quiet), then final 2s settle + re-prime. Fixes "Loading..." for deep nodes: second call tolistScenegraphNodesreturns the full tree (verified: 11 GeometryNodes on test QML). First call may still show "Loading..." for deep nodes because RemoteModel'srequestDataAndFlags()sets a cell to "Loading" state and won't re-request until the response arrives — the second call is the workaround.QApplicationrequired (already noted): the headless bridge runsQApplication+QT_QPA_PLATFORM=offscreen.- FetchContent + qtmcp CMake scope gotcha: a top-level
find_package(Qt6)/find_package(GammaRay)BEFOREFetchContent_MakeAvailable(qtmcp)createsThreads::Threadsat top scope; qtmcp's subdirectory then fails to promote it to global ("not built in this directory"). Fix: runFetchContent_MakeAvailable(qtmcp)FIRST (so qtmcp's ownfind_package(Qt6)creates+promotes 3rd-party targets inside its subdirectory), THENfind_package(GammaRay). - qtmcp tool return types: only
void/bool/QString/QStringList/QImage(sync) +QFuture<QList<QMcpCallToolResultContent>>(async).QJsonObjecthitsqFatal. Structured results → JSON-serializedQString(see Step 2 tool section). - Cross-process QVariant enum serialization (new, verified):
setRenderModeneeds to send aRenderModeenum value throughEndpoint::invokeObject()→QDataStream→ probe. The bridge defines a minimalRenderModeenum inquickinspector_types.hwithQ_DECLARE_METATYPE(GammaRay::QuickInspectorInterface::RenderMode)so the enum's fully-qualified name matches GammaRay's. Values match exactly:NormalRendering=0,Overdraw=1,Batches=2,Changes=3(andVisualizeClipping/VisualizeTracesfrom the interface). Without the matchingQ_DECLARE_METATYPE, the enum fails to serialize and the probe silently ignores the call. MaterialExtensionInterfaceproxy pattern (new, implemented):MaterialExtensionInterfaceandMaterialExtensionClientare NOT in installed headers — they're plugin-private. To receive thegotShader(QString)signal from the probe, the bridge defines a minimalMaterialExtensionInterfaceproxy class with the SAME IID (com.kdab.GammaRay.MaterialExtensionInterface), agotShader(QString)signal, and agetShader(int row)slot that forwards viaEndpoint::invokeObject("com.kdab.GammaRay.QuickSceneGraph.material", "getShader", {row}). The concreteMaterialExtensionProxyself-registers viaObjectBroker::registerObject(name, this)and the factory is registered viaregisterClientObjectFactoryCallback<MaterialExtensionInterface*>()ininitOnce(). Thematerial_interface.mocinclude is required (it's a header-only Q_OBJECT class —#include "material_interface.moc"at the bottom of the .cpp).- Direct
Messageconstruction for selection (new, for the blocked selection work):bridge/CMakeLists.txtadds/home/blumia/Sources/GammaRay/commontotarget_include_directoriesso the bridge can include<message.h>and<protocol.h>directly from the GammaRay source tree. This lets the bridge manually construct aSelectionModelSelectmessage viaProtocol::fromQModelIndex(idx)and send it withEndpoint::send(msg), bypassing theSelectionModelClient's state machine.Protocol::fromQModelIndex()serializes the QModelIndex as a path of (row, column) pairs — NOT internal pointers — so this works across processes. (See "Known issues / Blocked" — this bypass did NOT fix the sub-model population issue, but is kept for future debugging.) - Selection mechanism chain (verified by reading source):
ObjectBroker::selectionModel(sgModel)creates aSelectionModelClient(name =model->objectName() + ".selection"), which syncs to the probe'sSelectionModelServer. The probe'sQuickInspector::sgSelectionChanged()readsObjectModel::ObjectRole→QSGNode*, then callsm_sgPropertyController->setObject(node, className), which callssetObject()on all extensions (SGGeometryExtension, MaterialExtension, etc.) which populate the sub-models.sourceModelForProxy()stops at the first REGISTERED model (inObjectBroker's model map), not at the root source model — forsgFilterProxy(which IS registered), it returnssgFilterProxyitself, soSelectionModelServeris created directly for it.
Running the bridge
cd bridge
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH=$(pwd)/../install-prefix
cmake --build build
# Probe (manual, Phase 1): start a Qt/QML app with the GammaRay probe
../install-prefix/bin/gammaray --inject-only --listen tcp://127.0.0.1:11732 \
--injector preload /usr/lib/qt6/bin/qml /path/to/app.qml
# Bridge (stdio MCP server) — --connect is OPTIONAL now:
./run.sh
# or: ./run.sh --connect tcp://127.0.0.1:11732
# Then (MCP client) call connectProbe(127.0.0.1, 11732) once the probe is up.
run.sh sets LD_LIBRARY_PATH (GammaRay + qtmcp libs) and QT_PLUGIN_PATH (qtmcp mcpserverbackend/libqmcpserverstdio.so) and QT_QPA_PLATFORM=offscreen.
Next steps
- Unblock sub-model population — the single highest-priority issue. Try (in order of likelihood):
- Run the GammaRay GUI client (
xvfb-run install-prefix/bin/gammaray-clientorQT_QPA_PLATFORM=offscreen) against the same probe and click a GeometryNode. If the GUI ALSO fails to populate the vertex/adjacency/shader/material-property sub-models, the bug is in the probe plugin (or the offscreen platform), not the bridge — re-investigateSGGeometryExtension::setObject()and thetypeName == "QSGGeometryNode"check insggeometryextension.cpp. If the GUI WORKS, the bug is in our selection-sync path. - Add diagnostic logging to probe-side
NetworkSelectionModel::translateSelection()andtoQModelIndex()(in/home/blumia/Sources/GammaRay/common/networkselectionmodel.cppandcommon/protocol.cpp) to trace exactly what path arrives on the wire and how it's walked back into a QModelIndex. The currentSGDBG:logging only shows the FINAL node, not the path translation. - Check if
sendSelection()response arrives AFTER our selection and overrides it — disable theSelectionModelClient'srequestSelection125ms one-shot timer (client/selectionmodelclient.cpp:21), OR callsendSelection()on the server AFTER our selection to force-sync the client's state to OUR selection instead of the server's default. - Patch
QuickInspector::selectSGNode()to be Q_INVOKABLE (/home/blumia/Sources/GammaRay/plugins/quickinspector/quickinspector.cpp:474). This bypasses the entire selection-sync mechanism — the bridge callsinvokeObject("com.kdab.GammaRay.QuickInspectorInterface/1.0", "selectSGNode", {QVariant::fromValue<void*>(nodePtr)})directly. Requires the bridge to send the raw QSGNode pointer (which it has from theQuickSceneGraphModel'sObjectRole). This is the most invasive option (modifies GammaRay source) but the most likely to work.
- Run the GammaRay GUI client (
- Clean up debug artifacts once the blocker is resolved — remove the
SGDBG:qWarning()logging inquickinspector.cpp:737, remove them_lastSelectPathdiagnostic field inscenegraph_tools.{h,cpp}. - Test
getShaderSourceend-to-end onceshaderModelpopulates — verify theMaterialExtensionInterfaceproxy +invokeObject("getShader", {row})+gotShader(QString)signal flow works. - Implement
grabTexture(objectId)—TextureExtensionasync image grab viaRemoteViewInterfaceTransferImage. Follows the sameMaterialExtensionInterfaceproxy pattern: a thinTextureExtensionInterfaceproxy with matching IID. Depends on selection working first to populate the texture list. - Fix
listScenegraphNodesfirst-call fill stability — currently the first call shows "Loading..." for deep nodes and the second call returns the full tree. Either: (a) makeprimeAndWait()smarter (detect when a "Loading..." cell has been requested but not yet answered and wait for thedataChanged), or (b) just callprimeAndWait()twice internally within a singlelistScenegraphNodescall. - Pin qtmcp
GIT_TAGto a specific commit/tag inbridge/CMakeLists.txtfor reproducibility (currently tracksmain).