Skip to content

feat(remote-control): add MCP server core feature#27

Merged
alandtse merged 17 commits into
devfrom
claude/cpp-mcp-remote-control
May 22, 2026
Merged

feat(remote-control): add MCP server core feature#27
alandtse merged 17 commits into
devfrom
claude/cpp-mcp-remote-control

Conversation

@alandtse
Copy link
Copy Markdown
Owner

@alandtse alandtse commented May 20, 2026

Summary

Adds a new Remote Control CORE feature that hosts an embedded Model Context Protocol (MCP) server inside CommunityShaders.dll, so AI assistants (Claude Code, Cursor, Continue, etc.) can query and mutate runtime state for A/B testing and performance investigation.

  • Off by default, binds to 127.0.0.1 — opt-in from the Settings UI.
  • ImGui panel includes a "Copy MCP client config to clipboard" button that emits a Streamable-HTTP (MCP 2025-03-26) config snippet ready to paste into a host's mcpServers settings.
  • Initial slice ships one bootstrap tool (get_state) to validate the wiring end-to-end. Subsequent commits will add list_features, get/set/toggle/reset_feature, run_abtest, capture_renderdoc, capture_screenshot.

Library choice — hkr04/cpp-mcp

Pinned to a0eb22c9. 280 stars, MIT, 2025-03-26 spec compliant including Streamable HTTP. Compared against gopher-mcp (26× larger, enterprise filter-chain arch) and peppemas/mcp_server (low activity); cpp-mcp is the right fit for an in-process server inside a game DLL.

Vendored as a git submodule under extern/cpp-mcp — same pattern we use for FidelityFX-SDK and Streamline — because cpp-mcp has no install rules (upstream PR #12 still open). Only server-side translation units are compiled; the bundled stdio/SSE client implementations are intentionally omitted.

Two non-obvious integration fixes (captured in cmake/cpp-mcp.cmake)

  1. nlohmann_json ABI mismatch. cpp-mcp bundles 3.11.3, vcpkg ships 3.12.0. Both wrap their public API in an ABI-versioned inline namespace (json_abi_v3_11_3 vs json_abi_v3_12_0), so even sharing an include guard wasn't enough — cpp-mcp's TUs and our consumer TUs would emit different mangled symbols → LNK2001 on set_capabilities / register_tool. Fix: patch mcp_message.h at configure time to use <nlohmann/json.hpp> from vcpkg, writing the patched header to a build-tree mirror so the submodule stays clean.
  2. winsock1/winsock2 conflict. cpp-mcp's vendored cpp-httplib pulls in <winsock2.h>; Skyrim's PCH transitively brings legacy <winsock.h> via <Windows.h>. Define _WINSOCKAPI_ on the cpp-mcp target (PUBLIC) so consumers and the PCH skip the legacy header.

Files

.gitmodules                                               +3
CMakeLists.txt                                            +2
cmake/cpp-mcp.cmake                                      +96
extern/cpp-mcp                                       (submodule pin)
features/Remote Control/CORE                              (marker)
features/Remote Control/Shaders/Features/RemoteControl.ini +2
src/Features/RemoteControl.{h,cpp}                      +302
src/Feature.cpp                                           +2  (register)
src/Globals.{h,cpp}                                       +4  (singleton wiring)

11 files, +412 / -0.

Test plan

  • BuildRelease.bat ALL-WITH-AUTO-DEPLOYMENT succeeds with the feature wired in
  • CommunityShaders.dll grows ~500 KB (cpp-mcp + bundled httplib code), deploys to SE + VR Data folders
  • Feature appears under Core Features → Utility in the in-game menu
  • Clipboard config preview renders correctly ({"mcpServers": {"community-shaders": {"type": "http", "url": "http://127.0.0.1:8910/mcp"}}})
  • Connect Claude Code to the running game, confirm get_state returns frame counter
  • VR build (compile-time VR preset)
  • Confirm no firewall popup on first localhost bind

Follow-ups (separate PRs)

  • list_features / get_feature / set_feature / toggle_feature / reset_feature tools
  • run_abtest wrapping ABTestingManager
  • capture_renderdoc(frames=N) and capture_screenshot() wrapping the existing triggers
  • Optional: command-queue drain on the render thread for settings mutations (current implementation applies on the listener thread; safe for the bootstrap tool, but settings RPC will need synchronization)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Remote Control: local server for monitoring/control (disabled by default). Settings to enable, choose port and bind address. Shows running status, last error, active listen address/port, copyable client config, and a connected-clients table with session info.
    • Remote tools: inspect (runtime metrics), feature (list/get/set/toggle), console (enqueue commands), capture (render/screenshot), and abtest (start/stop/status/diff).

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Warning

Rate limit exceeded

@alandtse has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 26 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f8ad0843-4ef8-490a-8fc7-9c9f1a7100ba

📥 Commits

Reviewing files that changed from the base of the PR and between abc58ce and 65b7545.

📒 Files selected for processing (13)
  • .gitmodules
  • CMakeLists.txt
  • cmake/cpp-mcp.cmake
  • extern/cpp-mcp
  • features/Remote Control/CORE
  • features/Remote Control/Shaders/Features/RemoteControl.ini
  • src/Feature.cpp
  • src/Features/RemoteControl.cpp
  • src/Features/RemoteControl.h
  • src/Globals.cpp
  • src/Globals.h
  • src/State.cpp
  • src/State.h
📝 Walkthrough

Walkthrough

Adds cpp-mcp as a vendored submodule and CMake integration, declares and registers a new RemoteControl feature, and implements an in-process MCP JSON-RPC server with ImGui settings, session tracking, client config generation, and multiple MCP tools.

Changes

Remote Control MCP Server Feature

Layer / File(s) Summary
Build infrastructure and cpp-mcp integration
.gitmodules, CMakeLists.txt, cmake/cpp-mcp.cmake, extern/cpp-mcp
cpp-mcp added as a Git submodule and built as a static library via CMake. The build copies/patches headers to use vcpkg's nlohmann/json.hpp, sets C++17 and MCP tuning defines, and links the library into the main target.
Feature type declaration and global registration
src/Features/RemoteControl.h, src/Globals.h, src/Globals.cpp, src/Feature.cpp, features/Remote Control/Shaders/Features/RemoteControl.ini
Adds RemoteControl Feature subclass with settings and session types, forward-declares and exposes a global instance, instantiates it, registers it in Feature::GetFeatureList(), and provides INI metadata.
Server lifecycle, settings UI, and MCP tool implementations
src/Features/RemoteControl.cpp
Implements singleton, ctor/dtor, Load/Reset, settings serialization, ImGui settings UI (enable, port, bind address, error/display, copy client config), BuildClientConfig(), guarded StartServer()/StopServer(), session bookkeeping, and registers MCP tools: inspect, feature, console, capture, and abtest.

Sequence Diagram

sequenceDiagram
  participant ImGui as ImGui (User)
  participant RemoteControl
  participant Server as mcp::server
  participant Client as MCP Client
  participant Runtime as Runtime State

  ImGui->>RemoteControl: Toggle enable / change settings
  RemoteControl->>Server: StartServer() / configure (host,port,tools)
  RemoteControl->>Server: RegisterTools()
  Server->>Server: run non-blocking

  Client->>Server: HTTP MCP request (tool call)
  Server->>RemoteControl: invoke tool handler (e.g., inspect)
  RemoteControl->>Runtime: query frame_count, VR status, features
  Runtime-->>RemoteControl: return state
  RemoteControl-->>Server: tool response JSON
  Server-->>Client: MCP response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I stitched a little server into code so neat,
Buttons to start, and ports to greet,
Tools that peek at frame and VR,
Clients connect from near and far —
The rabbit hops, the MCP hums, and bytes go tweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a new Remote Control feature that implements an MCP server core functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/cpp-mcp-remote-control

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

No actionable suggestions for changed features.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmake/cpp-mcp.cmake (1)

49-53: ⚡ Quick win

Add validation before include pattern replacement to prevent silent configuration failures.

The string(REPLACE) at lines 49-53 silently no-ops if the upstream include pattern changes (e.g., whitespace variations), which could silently reintroduce the ABI mismatch issue later. Since the submodule has no pinned commit in .gitmodules, a defensive pre-check is warranted.

💡 Suggested validation
 foreach(_hdr IN LISTS _cpp_mcp_headers)
     get_filename_component(_name "${_hdr}" NAME)
     file(READ "${_hdr}" _content)
     if(_name STREQUAL "mcp_message.h")
+        if(NOT _content MATCHES "`#include`[ \t]+\"json\\.hpp\"")
+            message(FATAL_ERROR
+                "cpp-mcp include pattern mismatch: mcp_message.h does not contain expected '`#include` \"json.hpp\"'. "
+                "Update cmake/cpp-mcp.cmake patch logic.")
+        endif()
-        string(REPLACE
-            "`#include` \"json.hpp\""
-            "`#include` <nlohmann/json.hpp>"
-            _content "${_content}")
+        string(REGEX REPLACE
+            "`#include`[ \t]+\"json\\.hpp\""
+            "`#include` <nlohmann/json.hpp>"
+            _content "${_content}")
     endif()
     file(WRITE "${CPP_MCP_PATCHED_INC}/${_name}" "${_content}")
 endforeach()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/cpp-mcp.cmake` around lines 49 - 53, Before calling string(REPLACE) on
_content to swap "`#include` \"json.hpp\"" with "`#include` <nlohmann/json.hpp>",
add a validation step that searches _content for the exact include pattern
(e.g., match the literal "`#include` \"json.hpp\"" or a narrowly defined regex
accounting for common whitespace) and error-out with a clear cmake_fatal_error
(or message(FATAL_ERROR)) if the pattern is not found; this ensures the
subsequent string(REPLACE) in cmake/cpp-mcp.cmake actually modified something
and prevents silent no-ops if the upstream include format changed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Features/RemoteControl.cpp`:
- Around line 50-55: RemoteControl::LoadSettings currently accepts arbitrary
settings.bindAddress and settings.port from persisted JSON; enforce
loopback-only binding and clamp ports here and again in the server start path
(e.g., RemoteControl::Start or whatever launches the listener). Specifically,
when loading, validate settings.bindAddress is a loopback address (accept
"127.0.0.1" and "::1" or resolve/normalize and check isLoopback) and if not,
replace it with the loopback address; also validate/clamp settings.port into the
allowed range (e.g., 1–65535 or your app's allowed port range) and fall back to
the default (8910) on invalid values. Repeat the same checks right before
binding in the start/bind routine to avoid any runtime override from malicious
or stale config.

---

Nitpick comments:
In `@cmake/cpp-mcp.cmake`:
- Around line 49-53: Before calling string(REPLACE) on _content to swap
"`#include` \"json.hpp\"" with "`#include` <nlohmann/json.hpp>", add a validation
step that searches _content for the exact include pattern (e.g., match the
literal "`#include` \"json.hpp\"" or a narrowly defined regex accounting for
common whitespace) and error-out with a clear cmake_fatal_error (or
message(FATAL_ERROR)) if the pattern is not found; this ensures the subsequent
string(REPLACE) in cmake/cpp-mcp.cmake actually modified something and prevents
silent no-ops if the upstream include format changed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 628edac9-aafb-4f5e-af00-d43cca0c2d8d

📥 Commits

Reviewing files that changed from the base of the PR and between fd20066 and ff55a22.

📒 Files selected for processing (11)
  • .gitmodules
  • CMakeLists.txt
  • cmake/cpp-mcp.cmake
  • extern/cpp-mcp
  • features/Remote Control/CORE
  • features/Remote Control/Shaders/Features/RemoteControl.ini
  • src/Feature.cpp
  • src/Features/RemoteControl.cpp
  • src/Features/RemoteControl.h
  • src/Globals.cpp
  • src/Globals.h

Comment thread src/Features/RemoteControl.cpp
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 20, 2026

✅ A pre-release build is available for this PR:
Download

alandtse added a commit that referenced this pull request May 20, 2026
Addresses CodeRabbit Major finding on PR #27: persisted bindAddress
and port were applied verbatim, so a config edit could expose the
control endpoint off-host (the surface advertises loopback-only).

- Centralize validation in IsLoopbackAddress / NormalizeBindAddress
  / ClampPort helpers in an anonymous namespace.
- Apply in LoadSettings so stale/malicious config is corrected at
  load with a warning.
- Re-apply in StartServer immediately before the bind so UI edits
  or hot-reload can't slip a non-loopback host through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 20, 2026 07:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new CORE “Remote Control” feature that embeds an in-process MCP (Model Context Protocol) HTTP server inside CommunityShaders.dll, intended to let external AI/dev tools inspect and manipulate runtime state for investigation and A/B testing.

Changes:

  • Adds a vendored cpp-mcp submodule and CMake integration to build/link it as a static library (including an ABI-alignment patch for nlohmann_json).
  • Implements the RemoteControl feature with an ImGui settings panel, server lifecycle, session tracking, and initial MCP tool wiring.
  • Wires RemoteControl into the global feature registry/singletons so it participates in the normal feature lifecycle.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
.gitmodules Adds extern/cpp-mcp submodule entry.
CMakeLists.txt Includes and links the new cpp-mcp target.
cmake/cpp-mcp.cmake Builds cpp-mcp as a static library and patches headers to align JSON ABI / avoid winsock conflicts.
features/Remote Control/CORE Marks the feature as CORE for packaging/core detection.
features/Remote Control/Shaders/Features/RemoteControl.ini Adds feature version metadata for auto-detected versioning.
src/Feature.cpp Registers RemoteControl in Feature::GetFeatureList().
src/Globals.h Declares the globals::features::remoteControl singleton.
src/Globals.cpp Defines the RemoteControl singleton instance.
src/Features/RemoteControl.h Declares the new RemoteControl feature class and settings/session state.
src/Features/RemoteControl.cpp Implements server start/stop, UI, session bookkeeping, and MCP tool registration/handlers.
Comments suppressed due to low confidence (6)

src/Features/RemoteControl.cpp:905

  • The console tool description tells callers to poll get_state, but there is no get_state tool in this PR (the equivalent is inspect(kind='state')). Update the description so clients don’t call a nonexistent tool.
							  "To verify a state change, poll get_state until "
							  "frame_count > enqueued_at_frame (at least one tick "
							  "elapsed), then observe via side channels: tracy "
							  "captures for perf-affecting changes, "

src/Features/RemoteControl.cpp:800

  • The capture tool description references list_features, but the tool exposed here is feature(action='list'). Update the text to match the actual tool name/action so clients can follow the instructions.
							  "be attached or the in-app DLL loaded; check "
							  "list_features for RenderDoc loaded=true. Output "
							  "lands in RenderDoc's configured captures dir.\n"

src/Features/RemoteControl.cpp:292

  • BuildClientConfig() produces an invalid URL when bindAddress is an IPv6 literal like ::1 (needs brackets: http://[::1]:PORT/mcp). Either bracket IPv6 literals when formatting or restrict bindAddress to IPv4/hostname values to avoid generating unusable client configs.
					{ "type", "http" },
					{ "url", std::format("http://{}:{}/mcp",
								 settings.bindAddress, settings.port) },
				} } } }

src/Features/RemoteControl.cpp:417

  • The PR description says the initial slice ships only one bootstrap tool (get_state), but this implementation registers multiple tools and documents feature mutation/capture workflows. Either update the PR description to match what’s shipping, or trim this PR to just the bootstrap tool so scope matches the stated plan.
void RemoteControl::RegisterTools()
{
	// Five tools, each semantically rich. Reads vs writes vs lifecycles are
	// separated by tool; within each tool, kind/action discriminates the
	// specific operation. See agentic-renderdoc's "Why this design" notes —

src/Features/RemoteControl.cpp:567

  • These tool handlers run on cpp-mcp’s listener thread, but they read/mutate global engine state and Feature instances (including Feature::loaded, settings, ABTestingManager, etc.). That introduces C++ data races with the render/game thread and can lead to crashes/undefined behavior. Marshal tool actions onto the main thread (e.g., via SKSE TaskInterface + promise/future) and only touch game/feature state from that thread.
	server->register_tool(tool,
		[this](const mcp::json& params, const std::string& session_id) -> mcp::json {
			RecordToolCall(session_id, "feature");
			const std::string action = params.value("action", std::string{});
			if (action.empty()) {

src/Features/RemoteControl.cpp:606

  • feature(action='toggle') directly flips Feature::loaded. This bypasses the normal boot-time enable/disable flow and can enable features after State::SetupResources() has already run, leaving required GPU resources uninitialized. Consider restricting toggle to already-loaded features (disable-only), or implement a safe runtime enable/disable path that performs proper setup/teardown on the correct thread.
				const bool previous = target->loaded;
				target->loaded = desired;
				logger::info("Remote Control: feature(toggle, {}, {}) (was {})",
					shortName, desired, previous);

Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread cmake/cpp-mcp.cmake
@alandtse alandtse force-pushed the claude/cpp-mcp-remote-control branch from 86994ef to 150cd0f Compare May 20, 2026 08:37
alandtse added a commit that referenced this pull request May 20, 2026
Addresses CodeRabbit Major finding on PR #27: persisted bindAddress
and port were applied verbatim, so a config edit could expose the
control endpoint off-host (the surface advertises loopback-only).

- Centralize validation in IsLoopbackAddress / NormalizeBindAddress
  / ClampPort helpers in an anonymous namespace.
- Apply in LoadSettings so stale/malicious config is corrected at
  load with a warning.
- Re-apply in StartServer immediately before the bind so UI edits
  or hot-reload can't slip a non-loopback host through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
alandtse added a commit that referenced this pull request May 20, 2026
Addresses CodeRabbit Major finding on PR #27: persisted bindAddress
and port were applied verbatim, so a config edit could expose the
control endpoint off-host (the surface advertises loopback-only).

- Centralize validation in IsLoopbackAddress / NormalizeBindAddress
  / ClampPort helpers in an anonymous namespace.
- Apply in LoadSettings so stale/malicious config is corrected at
  load with a warning.
- Re-apply in StartServer immediately before the bind so UI edits
  or hot-reload can't slip a non-loopback host through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alandtse alandtse force-pushed the claude/cpp-mcp-remote-control branch from 150cd0f to 05922e7 Compare May 20, 2026 08:39
alandtse added a commit that referenced this pull request May 20, 2026
Copilot findings on PR #27:

- DrawSettings panel said the server was bound to 127.0.0.1
  "unless explicitly changed", which is no longer true now that
  NormalizeBindAddress forces non-loopback values back. Reword to
  "loopback-only — any non-loopback bind address is rejected at
  load and bind time" so the UI matches the actual policy.
- cmake/cpp-mcp.cmake's mcp_message.h patch silently no-op'd if
  the expected `#include "json.hpp"` ever disappeared upstream,
  letting the nlohmann_json ABI mismatch silently regress.
  Verify the substring exists before the REPLACE and emit a
  FATAL_ERROR pointing at the rationale in the header comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 20, 2026 08:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (3)

src/Features/RemoteControl.cpp:802

  • The capture tool description tells clients to check list_features for RenderDoc status, but no list_features tool is registered (feature enumeration is via feature with action='list'). This will mislead MCP clients/agents consuming the tool description; update the text to reference the actual tool/API shape.
							  "be attached or the in-app DLL loaded; check "
							  "list_features for RenderDoc loaded=true. Output "
							  "lands in RenderDoc's configured captures dir.\n"
							  "  screenshot — Lossless screenshot via the "

src/Features/RemoteControl.cpp:606

  • feature(action='toggle') writes to Feature::loaded from the MCP listener thread. loaded is a plain bool that is read frequently on the render thread (e.g., Feature::ForEachLoadedFeature), so this introduces a data race/UB. This should be marshalled onto the main/render thread (e.g., via a command queue drained in State::Reset) or protected with proper synchronization (atomics + well-defined gating).
				const bool previous = target->loaded;
				target->loaded = desired;
				logger::info("Remote Control: feature(toggle, {}, {}) (was {})",

src/Features/RemoteControl.cpp:659

  • feature(action='set'|'reset') calls Feature::LoadSettings / RestoreDefaultSettings directly on the MCP listener thread. Feature settings are consumed on the main/render thread, and many features touch D3D/game state during these operations, so this is a high risk for races/undefined behavior. Queue these mutations onto the main/render thread and return an acknowledgement token/status instead of mutating immediately on the listener thread.
					feature->LoadSettings(blob);
				} catch (const std::exception& e) {
					return ErrorResult("LoadSettings threw",
						{ { "shortName", shortName }, { "detail", e.what() } });
				}
				logger::info("Remote Control: feature(set, {})", shortName);
				return TextResult(mcp::json({
												{ "action", "set" },
												{ "shortName", shortName },
												{ "applied", true },
											})
						.dump());
			}
			if (action == "reset") {
				try {
					feature->RestoreDefaultSettings();
				} catch (const std::exception& e) {

Comment thread src/Features/RemoteControl.cpp
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp
Comment thread src/Features/RemoteControl.cpp Outdated
alandtse added a commit that referenced this pull request May 21, 2026
Three Copilot findings on PR #27:

- State::frameCount is non-atomic and incremented on the render
  thread, but RemoteControl reads it from the MCP listener thread
  (inspect kind=state, capture queue, console tick). That's a data
  race / UB. Add State::frameCountAtomic, a thread-safe mirror
  written immediately after the existing `frameCount++` on the
  render thread; route the off-thread reads in RemoteControl
  through it. The hot path (ScreenSpaceGI, Streamline, Upscaling)
  keeps using the non-atomic field — no behavior change there.
- BuildClientConfig formatted the URL as `http://{host}:{port}/mcp`,
  which is invalid for the IPv6 loopback "::1" because the URL
  authority requires bracketed IPv6 literals (RFC 3986 §3.2.2).
  Detect ':' in bindAddress and wrap it.
- The console tool description told agents to "poll get_state",
  but the tool is `inspect(kind='state')`. Update the description
  to match what's actually registered.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
alandtse added a commit that referenced this pull request May 21, 2026
Addresses CodeRabbit Major finding on PR #27: persisted bindAddress
and port were applied verbatim, so a config edit could expose the
control endpoint off-host (the surface advertises loopback-only).

- Centralize validation in IsLoopbackAddress / NormalizeBindAddress
  / ClampPort helpers in an anonymous namespace.
- Apply in LoadSettings so stale/malicious config is corrected at
  load with a warning.
- Re-apply in StartServer immediately before the bind so UI edits
  or hot-reload can't slip a non-loopback host through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
alandtse added a commit that referenced this pull request May 21, 2026
Copilot findings on PR #27:

- DrawSettings panel said the server was bound to 127.0.0.1
  "unless explicitly changed", which is no longer true now that
  NormalizeBindAddress forces non-loopback values back. Reword to
  "loopback-only — any non-loopback bind address is rejected at
  load and bind time" so the UI matches the actual policy.
- cmake/cpp-mcp.cmake's mcp_message.h patch silently no-op'd if
  the expected `#include "json.hpp"` ever disappeared upstream,
  letting the nlohmann_json ABI mismatch silently regress.
  Verify the substring exists before the REPLACE and emit a
  FATAL_ERROR pointing at the rationale in the header comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 21, 2026 07:19
alandtse added a commit that referenced this pull request May 21, 2026
Three Copilot findings on PR #27:

- State::frameCount is non-atomic and incremented on the render
  thread, but RemoteControl reads it from the MCP listener thread
  (inspect kind=state, capture queue, console tick). That's a data
  race / UB. Add State::frameCountAtomic, a thread-safe mirror
  written immediately after the existing `frameCount++` on the
  render thread; route the off-thread reads in RemoteControl
  through it. The hot path (ScreenSpaceGI, Streamline, Upscaling)
  keeps using the non-atomic field — no behavior change there.
- BuildClientConfig formatted the URL as `http://{host}:{port}/mcp`,
  which is invalid for the IPv6 loopback "::1" because the URL
  authority requires bracketed IPv6 literals (RFC 3986 §3.2.2).
  Detect ':' in bindAddress and wrap it.
- The console tool description told agents to "poll get_state",
  but the tool is `inspect(kind='state')`. Update the description
  to match what's actually registered.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alandtse alandtse force-pushed the claude/cpp-mcp-remote-control branch from 0deb63c to 8efcecc Compare May 21, 2026 07:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (4)

src/Features/RemoteControl.cpp:612

  • feature(action='toggle') writes Feature::loaded from the MCP listener thread (target->loaded = desired). Feature::loaded is a plain bool that’s read each frame by Feature::ForEachLoadedFeature, so this introduces a cross-thread data race/UB. Queue toggles onto the main/game thread (e.g., via SKSE::GetTaskInterface()->AddTask or a render-thread command queue) and/or make the loaded flag changes synchronized (atomic or mutex-protected).
			if (action == "toggle") {
				if (!params.contains("enabled") || !params["enabled"].is_boolean()) {
					return ErrorResult("missing required boolean parameter 'enabled'");
				}
				const bool desired = params["enabled"].get<bool>();
				// FindFeatureByShortName filters on loaded==true so it can't
				// help re-enable; walk the full list ourselves.
				Feature* target = nullptr;
				for (auto* f : Feature::GetFeatureList()) {
					if (f->GetShortName() == shortName) {
						target = f;
						break;
					}
				}
				if (!target) {
					return ErrorResult("feature not found",
						{ { "shortName", shortName } });
				}
				const bool previous = target->loaded;
				target->loaded = desired;
				logger::info("Remote Control: feature(toggle, {}, {}) (was {})",
					shortName, desired, previous);
				return TextResult(mcp::json({

src/Features/RemoteControl.cpp:652

  • feature(action='set') calls Feature::LoadSettings directly from the MCP listener thread. Many features’ LoadSettings paths are assumed to run on the main thread during config load/UI interaction and may touch shared state; invoking them off-thread risks races with the render loop. Consider marshaling settings mutations onto the main/game thread (similar to the console tool) and returning an acknowledgment once enqueued/applied.
			if (action == "set") {
				if (!params.contains("settings") || !params["settings"].is_object()) {
					return ErrorResult("missing required object parameter 'settings'");
				}
				::json blob;
				try {
					blob = ::json::parse(params["settings"].dump());
				} catch (const std::exception& e) {
					return ErrorResult("settings is not valid JSON",
						{ { "detail", e.what() } });
				}
				try {
					feature->LoadSettings(blob);
				} catch (const std::exception& e) {
					return ErrorResult("LoadSettings threw",
						{ { "shortName", shortName }, { "detail", e.what() } });
				}
				logger::info("Remote Control: feature(set, {})", shortName);

src/Features/RemoteControl.cpp:668

  • feature(action='reset') calls Feature::RestoreDefaultSettings() from the MCP listener thread. Restore paths can recreate resources / mutate global state and are typically not thread-safe relative to the render loop. Please enqueue this work onto the main/game thread (or a dedicated render-thread command queue) rather than running it on the listener thread.
			if (action == "reset") {
				try {
					feature->RestoreDefaultSettings();
				} catch (const std::exception& e) {
					return ErrorResult("RestoreDefaultSettings threw",
						{ { "shortName", shortName }, { "detail", e.what() } });
				}
				logger::info("Remote Control: feature(reset, {})", shortName);
				return TextResult(mcp::json({

src/Features/RemoteControl.cpp:763

  • The abtest tool calls ABTestingManager::Enable/Disable/SetTestInterval/ClearCachedSnapshots from the MCP listener thread. Enable()/Disable() call into globals::state->Load/LoadFromJson/SaveToJson and flip overlay settings, while ABTestingManager::Update() runs each frame; doing this off-thread risks races and inconsistent state. Queue these operations onto the main/game thread and consider returning the pre/post state once applied.
			if (action == "status") {
				return TextResult(statusBlob().dump());
			}
			if (action == "start") {
				if (params.contains("interval") && params["interval"].is_number()) {
					const auto secs = params["interval"].get<int>();
					if (secs > 0) {
						mgr->SetTestInterval(static_cast<uint32_t>(secs));
					}
				}
				mgr->Enable();
				logger::info("Remote Control: abtest(start)");
				return TextResult(statusBlob().dump());
			}
			if (action == "stop") {
				mgr->Disable();
				logger::info("Remote Control: abtest(stop)");
				return TextResult(statusBlob().dump());
			}
			if (action == "clear") {
				mgr->ClearCachedSnapshots();
				logger::info("Remote Control: abtest(clear)");
				return TextResult(statusBlob().dump());

Comment thread src/Features/RemoteControl.cpp
Comment thread cmake/cpp-mcp.cmake Outdated
Comment thread src/Features/RemoteControl.cpp
alandtse and others added 7 commits May 21, 2026 07:52
Add a new "Remote Control" CORE feature that hosts an embedded
Model Context Protocol (MCP) server inside CommunityShaders.dll so
AI assistants (Claude Code, Cursor, Continue, etc.) can query and
mutate runtime state for A/B testing and performance investigation.

Server is off by default and binds to 127.0.0.1 — opt-in via the
Settings UI. The ImGui panel exposes a "Copy MCP client config to
clipboard" button that emits a Streamable-HTTP (MCP 2025-03-26)
config snippet ready to paste into a host's mcpServers settings.

Initial slice ships a single bootstrap tool (get_state) so the
end-to-end wiring can be validated; subsequent commits will add
list_features, get/set/toggle/reset_feature, run_abtest,
capture_renderdoc, and capture_screenshot.

Library choice: hkr04/cpp-mcp pinned to a0eb22c9 (280 stars, MIT,
2025-03-26 spec compliant including Streamable HTTP). Vendored as
a submodule under extern/cpp-mcp — same pattern as FidelityFX-SDK
and Streamline — because cpp-mcp has no install rules (upstream
PR #12 still open). Only the server-side translation units are
compiled; the bundled stdio/SSE *client* implementations are
intentionally omitted.

The library vendors its own nlohmann_json (3.11.3) which has a
different ABI namespace tag than vcpkg's (3.12.0) and would cause
LNK2001 on mcp::server::set_capabilities / register_tool if cpp-mcp
linked against the vendored copy and consumers against vcpkg.
cmake/cpp-mcp.cmake patches mcp_message.h at configure time to use
<nlohmann/json.hpp> from vcpkg, writing the patched header to a
build-tree mirror so the submodule stays clean.

cpp-mcp's vendored cpp-httplib pulls in <winsock2.h>, but Skyrim's
PCH transitively brings the legacy <winsock.h> via <Windows.h>.
Define _WINSOCKAPI_ on the cpp-mcp target (PUBLIC) so consumers and
the PCH skip the legacy header.

Wiring:
  * extern/cpp-mcp submodule pinned to hkr04/cpp-mcp@a0eb22c9
  * cmake/cpp-mcp.cmake builds the static library target
  * features/Remote Control/CORE marker + versioned ini
  * src/Features/RemoteControl.{h,cpp} (Feature subclass)
  * registered in globals::features and Feature::GetFeatureList

Verified end-to-end: CommunityShaders.dll builds clean, grows
~500 KB, deploys to SE+VR. Off-by-default toggle exposed in the
Core Features → Utility group of the in-game menu.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Enumerate all graphics features over MCP. Returns a JSON array
with one object per feature including: name, shortName, loaded,
version, category, isCore, supportsVR, inMenu.

Read-only — iterates Feature::GetFeatureList() without mutation.
No render-thread synchronization required.

Refactored RegisterTools() into per-tool helpers
(RegisterGetStateTool, RegisterListFeaturesTool) so subsequent
commits can drop in additional tools without ballooning a single
function. Pulled the MCP content envelope into a TextResult()
helper shared across tools.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Return the JSON settings blob for a single feature by shortName.
Round-trips through Feature::SaveSettings(), so what comes out is
exactly what the on-disk config encodes — the same shape that
set_feature_settings (next commit) will accept back.

Read-only. No mutation, no render-thread sync required.

Also introduces ErrorResult() — a small helper that emits a
single-text-item content envelope with {"error": "...", ...}.
Conventions for all future tools:
  * success → {"...": ...} (tool-specific shape)
  * failure → {"error": "<reason>", "<context>": ...}
Callers can always JSON.parse() the text field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Enable or disable a graphics feature at runtime by flipping
Feature::loaded. ForEachLoadedFeature skips features whose loaded
flag is false, so every per-frame entrypoint (Prepass, Reflections,
Reset, etc.) becomes a no-op for that feature on the next frame.

GPU resources allocated during SetupResources() are NOT freed — the
goal is fast A/B perf/quality comparisons, not memory reclaim.
Re-toggling restores rendering with whatever state the feature had
mid-flight.

Atomic single-bool write — no command queue required. The listener
thread sets `loaded`, the render thread reads it on its next pass.
A torn read isn't possible (single byte), and the worst case from a
race is "one extra frame of new state" which is the desired outcome
anyway.

Walks GetFeatureList() directly instead of FindFeatureByShortName()
because the latter filters on loaded==true (built for "find an
active feature") — we need to be able to re-enable disabled ones.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace a feature's settings with a supplied JSON blob via the
existing Feature::LoadSettings(json&) path. Round-trips through
dump/parse to bridge cpp-mcp's ordered_json to plain nlohmann::json
expected by the feature interface.

Handler runs on the cpp-mcp listener thread. Most features only
deserialize into member variables here (same pattern the ImGui
menu uses), and a few set a recompileFlag that the render loop
consumes on the next frame (e.g. ScreenSpaceGI, DynamicCubemaps).
Both patterns are safe at this thread boundary. Settings whose
LoadSettings synchronously reallocates GPU resources would race
the renderer; none in-tree currently do, but documented in the
tool description as a follow-up to tighten with a command queue
if a future feature lands one.

LightLimitFix is a known no-override case: set_feature_settings
will return applied=true but the feature's runtime state won't
change because LightLimitFix doesn't implement LoadSettings.
Tracked separately as a feature-side fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Restore a feature to its built-in defaults via
Feature::RestoreDefaultSettings(). Semantically distinct from
set_feature_settings({}) — RestoreDefaultSettings is per-feature
reset logic (may release or recreate state that an empty-blob
LoadSettings would not touch).

Useful as the "B" side of A/B comparisons: snapshot with
get_feature_settings, reset, observe, then restore with
set_feature_settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Singular MCP tool for the entire console concern. Queues the
command onto the main game thread via SKSE::GetTaskInterface()->
AddTask so RE::Console::ExecuteCommand runs on the next tick,
honoring Skyrim's script-VM main-thread requirement.

Fire-and-forget by design: RE::Console::ExecuteCommand is void
and RE::ConsoleLog is a shared sink with no command-to-output
correlation; many useful commands (tcl, tfc, tg, tm…) are silent.
Scraping the log would produce misleading positives often enough
to be worse than not exposing it. To verify state changes,
callers should poll get_state.frame_count past the returned
enqueued_at_frame and observe via tracy/renderdoc/screenshot
side channels.

Response shape:
  { "queued": true,
    "command": "…",
    "enqueued_at_frame": N }

Following the agentic-renderdoc philosophy of fewer, semantically
rich tools, this is one tool rather than e.g. execute_console,
read_console_log, get_console_history. Future console-related
capabilities get added as optional parameters on this same tool,
not as new top-level tools.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
alandtse and others added 8 commits May 21, 2026 07:52
Single MCP tool for all frame-capture flavors, kind-dispatched.
Per the agentic-renderdoc design philosophy, one rich tool
beats two parallel tools (capture_renderdoc, capture_screenshot)
that would each need their own description and increase agent
decision cost. Future kinds (tracy snapshot, video clip) extend
the same tool.

Supported kinds:
  - renderdoc:  wraps existing RenderDoc::TriggerCapture /
                TriggerMultiFrameCapture. Honors `frames` (1-120,
                default 1). Returns 400-equivalent if RenderDoc
                feature isn't loaded or in-app DLL isn't attached.
  - screenshot: signals ScreenshotFeature::captureRequested
                atomic (existing lossless non-blocking path).
                `frames` ignored.

Fire-and-forget; no artifact path is returned. RenderDoc captures
land in its configured dir; screenshots in Screenshots/. Callers
must observe via the filesystem (or, for renderdoc, attach the UI).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
In-game visibility for who's currently driving the MCP server.
Sortable ImGui table showing session id, connect time, idle time,
request count, and last tool invoked. Tracked entirely in-process
— no MCP tool exposes this; it's a human-debugging surface.

Bookkeeping is updated on every tool handler invocation (lambda
captures `this` and calls RecordToolCall(session_id, toolName)
before the existing body) and on cpp-mcp's session-cleanup
callback when a client disconnects. A std::mutex protects the
session map across the listener and main threads.

Per-session force-disconnect is intentionally not surfaced —
cpp-mcp's public API doesn't expose it cleanly. The settings UI
documents that toggling "Enable MCP server" off/on serves as the
panic-button "kick everyone".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Single MCP tool driving the full A/B testing lifecycle in
features/Performance Overlay/ABTesting. Action-dispatched per the
agentic-renderdoc design: one rich tool instead of separate
start_abtest / stop_abtest / get_abtest_results / clear_snapshots /
set_interval tools.

Actions:
  status  — return enabled, usingTestConfig, interval,
            hasCachedSnapshots
  start   — Enable() the rotation. Optional `interval` parameter
            (seconds) applied first if provided.
  stop    — Disable(). Snapshots retained.
  clear   — ClearCachedSnapshots().
  diff    — return the per-setting USER vs TEST diff list so
            callers know exactly what the rotation is toggling.

Setup of the TEST config itself remains in the Performance Overlay
UI — this tool drives lifecycle only, not test-config authoring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The capture tool dereferences globals::features::renderDoc and
::screenshotFeature, which were only forward-declared via
Globals.h. The previous commit (118e2e5) compiled because
nothing yet touched those pointers — adding the tool exposed the
missing includes. Pull RenderDoc.h and ScreenshotFeature.h so the
member calls (IsAvailable, TriggerCapture, TriggerMultiFrameCapture,
captureRequested.store) resolve.

Folding this into the capture commit would require an interactive
rebase (against project guidance); leaving as a separate fix so
each commit stays self-contained and bisect-correct from this
point forward.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per the agentic-renderdoc design (https://github.com/alandtse/
agentic-renderdoc#why-this-design), "fewer, semantically rich
tools outperform expansive tool suites" — the tool description
IS the prompt engineering and each tool's surface should encode
operational expertise rather than expose one-method-per-call.

We weren't shipped, so collapse the 9-tool surface to 5:

  inspect(kind)                 — non-feature engine reads
                                  (currently kind='state'; extensible)
  feature(action, ...)          — all feature ops:
                                  list / get / set / reset / toggle
  console(command)              — unchanged
  capture(kind, frames?)        — unchanged
  abtest(action, interval?)     — unchanged

Removed tools (folded into the new two):
  get_state              → inspect(kind='state')
  list_features          → feature(action='list')
  get_feature_settings   → feature(action='get', shortName)
  set_feature_settings   → feature(action='set', shortName, settings)
  reset_feature_settings → feature(action='reset', shortName)
  toggle_feature         → feature(action='toggle', shortName, enabled)

The two new tool descriptions are deliberately rich (per the
philosophy): each documents every action's params, the timing /
threading semantics, the A/B testing flow as a worked example,
and the known gotchas (LightLimitFix's missing SaveSettings/
LoadSettings override means get returns null and set is a silent
no-op; toggle keeps GPU resources alive).

Shared helpers (EngineStateBlob, FeatureEntry) are factored as
static-file-locals so the same blobs can be reused if we add
future tools that surface the same data in different contexts.

The ImGui session-tracking table continues to work — handlers
still call RecordToolCall(session_id, "<name>") inline; only the
recorded names change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses CodeRabbit Major finding on PR #27: persisted bindAddress
and port were applied verbatim, so a config edit could expose the
control endpoint off-host (the surface advertises loopback-only).

- Centralize validation in IsLoopbackAddress / NormalizeBindAddress
  / ClampPort helpers in an anonymous namespace.
- Apply in LoadSettings so stale/malicious config is corrected at
  load with a warning.
- Re-apply in StartServer immediately before the bind so UI edits
  or hot-reload can't slip a non-loopback host through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot findings on PR #27:

- DrawSettings panel said the server was bound to 127.0.0.1
  "unless explicitly changed", which is no longer true now that
  NormalizeBindAddress forces non-loopback values back. Reword to
  "loopback-only — any non-loopback bind address is rejected at
  load and bind time" so the UI matches the actual policy.
- cmake/cpp-mcp.cmake's mcp_message.h patch silently no-op'd if
  the expected `#include "json.hpp"` ever disappeared upstream,
  letting the nlohmann_json ABI mismatch silently regress.
  Verify the substring exists before the REPLACE and emit a
  FATAL_ERROR pointing at the rationale in the header comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three Copilot findings on PR #27:

- State::frameCount is non-atomic and incremented on the render
  thread, but RemoteControl reads it from the MCP listener thread
  (inspect kind=state, capture queue, console tick). That's a data
  race / UB. Add State::frameCountAtomic, a thread-safe mirror
  written immediately after the existing `frameCount++` on the
  render thread; route the off-thread reads in RemoteControl
  through it. The hot path (ScreenSpaceGI, Streamline, Upscaling)
  keeps using the non-atomic field — no behavior change there.
- BuildClientConfig formatted the URL as `http://{host}:{port}/mcp`,
  which is invalid for the IPv6 loopback "::1" because the URL
  authority requires bracketed IPv6 literals (RFC 3986 §3.2.2).
  Detect ':' in bindAddress and wrap it.
- The console tool description told agents to "poll get_state",
  but the tool is `inspect(kind='state')`. Update the description
  to match what's actually registered.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alandtse alandtse force-pushed the claude/cpp-mcp-remote-control branch from 8efcecc to a8a2d96 Compare May 21, 2026 07:52
…eader GLOB

Address remaining Copilot review comments on PR #27:
- RemoteControl.cpp uses std::clamp/std::sort (algorithm) and
  std::vector (vector) directly. Include the headers explicitly
  instead of relying on transitive includes from imgui/cpp-mcp,
  which could break on PCH/toolchain changes.
- cmake/cpp-mcp.cmake's file(GLOB) for cpp-mcp headers now uses
  CONFIGURE_DEPENDS so adding/removing a header in the submodule
  triggers a CMake reconfigure on the next build. Otherwise the
  patched header mirror could go stale across submodule updates
  with no obvious failure.
Copilot AI review requested due to automatic review settings May 22, 2026 05:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 9 comments.

Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.h Outdated
Address follow-up review comments on PR #27:

Thread safety — route Feature/ABTesting mutations onto the main
thread via SKSE::GetTaskInterface(), mirroring the existing console
tool. Direct writes from the MCP listener thread race with the
render loop reading the same state:
- feature(action='toggle') now AddTask()s the Feature::loaded write;
  response includes 'previous', 'requested', and 'enqueued_at_frame'.
- feature(action='set') and feature(action='reset') now AddTask()
  the LoadSettings / RestoreDefaultSettings call. LoadSettings can
  touch UI/render-thread-visible state inside features; doing it
  off-thread was a real race.
- abtest(action='start'/'stop'/'clear') now AddTask() the
  Enable/Disable/Clear calls. ABTestingManager::Enable swaps configs
  via State::Load and Menu::Load, which expect main-thread access.
- abtest(action='status') remains synchronous — it only reads.
Tool return contract changes: mutations now respond with
{ queued: true, enqueued_at_frame: N } instead of synchronous
applied/reset flags. Callers wanting confirmation should re-query
with the corresponding read action.

Security — IsLoopbackAddress() no longer treats "localhost" as
loopback. On Windows the hosts file or a hijacked resolver can map
"localhost" to a non-loopback address, which would silently expose
the control endpoint off-host. Only literal 127.0.0.1 / ::1 pass.

Docs — capture tool hints now point at feature(action='list')
instead of the non-existent list_features tool. The
RemoteControl.h forward-declare comment no longer references a
"tool-tracking layer" the class never had; it now explains that
mcp::json (ordered_json alias) is what blocks moving registration
into the header.
@alandtse alandtse merged commit 62ff479 into dev May 22, 2026
18 checks passed
@alandtse alandtse deleted the claude/cpp-mcp-remote-control branch May 22, 2026 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants