Add MRMCPGateway: stdio MCP proxy for the HTTP MCP server#5988
Add MRMCPGateway: stdio MCP proxy for the HTTP MCP server#5988
Conversation
…P server. Lets a single MCP client connection (e.g. Claude Code) outlive MeshInspector restarts: gateway exposes local launch/status tools, forwards everything else to the running app via fastmcpp::ProxyApp + SSE. Backend probe in the client factory short-circuits the proxy to local-only when the app is offline so tools/list never hangs. Initialize is hand-crafted to advertise tools.listChanged and skip the slow proxy resource/prompt aggregation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…isible at session start. MeshInspector now exposes Server::dumpToolsAsJson / saveToolsCache plus a dumpToolCacheIfNeeded(commandArgs) wrapper that writes the registered tools to a JSON file when invoked with `-mcpDumpFile <path>`. The gateway adds an ensureFreshCache step at startup: if its on-disk cache is missing or its embedded build stamp doesn't match the gateway's compile-time __TIMESTAMP__, it spawns the backend hidden + dump-and-exit, waits for it to finish, then embeds the stamp into the just-written cache. tools/list responses splice the cached entries in when the backend is offline (deduped by name) so MCP clients see the full surface even before the user has called launch — works around Claude Code's stdio client not honouring mid-session notifications/tools/list_changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,513 @@ | |||
| // fastmcpp must be included before any standard headers per MRMcp's pattern. | |||
|
|
|||
| #if defined( __GNUC__ ) | |||
There was a problem hiding this comment.
Move this to some common header to reuse in MRMcp.cpp. In MRPch project perhaps?
There was a problem hiding this comment.
Done in e2f7d1b — extracted to MRPch/MRFastmcpp.h (mirrors the MRSpdlog.h wrapping pattern), and MRMcp.cpp now also uses it.
| // Per-user app-data dir for the gateway. Mirrors the leaf-folder convention of | ||
| // MR::getUserConfigDir() (in MRSystem.cpp:168-202) but uses "MRMCPGateway" so | ||
| // the cache is namespaced to the gateway, not to MeshInspector. | ||
| std::filesystem::path gatewayUserConfigDir() |
There was a problem hiding this comment.
Perhaps paste the existing function getUserConfigDir() from MRSystem.cpp here? With a comment that it's pasted, if you don't want a dependency on MRMesh.
There was a problem hiding this comment.
Done in e2f7d1b — replaced gatewayUserConfigDir with a paste of MR::getUserConfigDir()'s body, with a PASTED from … attribution comment. Adapted only to hardcode the leaf folder and use std::cerr instead of spdlog so the gateway keeps its zero-MRMesh dependency footprint.
…astmcpp.h, paste getUserConfigDir() body from MRSystem.cpp. The shared header de-duplicates the warning-suppression dance both MCP TUs need; gatewayUserConfigDir() now mirrors MeshInspector's existing helper instead of a bespoke implementation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| /// the headless dump invocation doesn't provide). Otherwise a no-op. Intended to be | ||
| /// called once during MCP setup with the viewer's own launch arguments, after every | ||
| /// `MR_ON_INIT` tool registration has run. | ||
| MRMCP_API void dumpToolCacheIfNeeded( const std::vector<std::string>& commandArgs ) const; |
There was a problem hiding this comment.
Why isn't it resolved in MRViewer?
There was a problem hiding this comment.
could be resolved, but i think that is better to rename it to processCmdArgs and leave it here, so all MCP related logic lives in one place? mb call it directly from setRunning(not sure)?
There was a problem hiding this comment.
Processing command line arguments is more like app related logic. Now for some reason most flags are declared and processed in MRViewer and a single one in MRMcp.
| // Write to a sibling .tmp first then rename: this is atomic on the filesystem, | ||
| // so a concurrent reader (e.g. the gateway polling for the cache) never sees a | ||
| // partial write or an empty file mid-flush. |
There was a problem hiding this comment.
This is a good approach that might be used in other places, consider moving it to MRMesh or MRViewer.
There was a problem hiding this comment.
not sure that we should do in this PR, not to overcomlicate it (I suggest moving when we want to reuse it in other place, or otherwise introduce this function first (with usage in other place) and then update this PR with existing function it)
| spdlog::error( "MRMcp: cannot create directory {}: {}", utf8string( path.parent_path() ), ec.message() ); | ||
| return false; |
There was a problem hiding this comment.
Should be replaced with Expected<void>.
There was a problem hiding this comment.
This should probably be moved to MRMesh or MRViewer as well.
There was a problem hiding this comment.
the functions should be accessable from MRMCPGateway project (which is minimal proxy having no dependences)
| std::wstring cmdLine; | ||
| appendQuotedArg( cmdLine, exe.wstring() ); | ||
| for ( const auto& a : args ) | ||
| { | ||
| cmdLine.push_back( L' ' ); | ||
| appendQuotedArg( cmdLine, utf8ToWide( a ) ); | ||
| } |
There was a problem hiding this comment.
Shouldn't it be generalized to a separate helper function?
| } | ||
| const DWORD waitMs = static_cast<DWORD>( | ||
| std::chrono::duration_cast<std::chrono::milliseconds>( timeout ).count() ); | ||
| const DWORD wait = WaitForSingleObject( pi.hProcess, waitMs ); |
There was a problem hiding this comment.
Essentially the only differences between spawnDetached and spawnAndWait are the DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP flag and this call. Can we join these functions or generalize them to a helper class?
| std::vector<std::string> argsCopy = args; | ||
| std::vector<char*> argv; | ||
| argv.reserve( argsCopy.size() + 2 ); | ||
| argv.push_back( exeStr.data() ); | ||
| for ( auto& a : argsCopy ) | ||
| argv.push_back( a.data() ); | ||
| argv.push_back( nullptr ); |
There was a problem hiding this comment.
Create a helper function for it.
There was a problem hiding this comment.
Can this file be decomposed to several ones?
…ache/Backend, share spawn helpers, relocate fastmcpp wrapper, switch saveToolsCache to Expected<void>. Rename dumpToolCacheIfNeeded -> processCmdArgs to keep MCP-arg processing in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Round 2 (
Not in this PR per the threads: moving the atomic-write helper to MRMesh; moving the spawn helpers to MRMesh. |
Summary
MRMCPGateway, a small console exe undersource/MRMCPGateway/that runs an MCP stdio server and forwardstools/*toMRMcp's existing HTTP+SSE server.launch(spawns the configured backend exe) andstatus(probes its liveness) — let a single MCP client connection survive the backend being killed and restarted underneath.fastmcpp::ProxyApp+SseClientTransport+StdioServerWrapper; no new protocol code. Initialize is hand-crafted to advertisetools.listChanged: trueand to skip the slow proxy resource/prompt aggregation. The client factory probes the backend and short-circuits to local-only listings when it's down sotools/listnever blocks on a dead backend.Test plan
MeshLib.sln(Debug|x64).MRMCPGateway.exelands in the standard install bin dir.command = MRMCPGateway.exe,args = ["--launch-cmd", "<backend exe>"].launch+statusvisible.status→not started.launch→started, backend starts, forwarded calls work after.ui.*etc.) appears immediately; forwarded calls return live backend responses.🤖 Generated with Claude Code