Skip to content

Add MRMCPGateway: stdio MCP proxy for the HTTP MCP server#5988

Draft
Grantim wants to merge 4 commits intomasterfrom
mrmcp-gateway
Draft

Add MRMCPGateway: stdio MCP proxy for the HTTP MCP server#5988
Grantim wants to merge 4 commits intomasterfrom
mrmcp-gateway

Conversation

@Grantim
Copy link
Copy Markdown
Contributor

@Grantim Grantim commented Apr 25, 2026

Summary

  • Adds MRMCPGateway, a small console exe under source/MRMCPGateway/ that runs an MCP stdio server and forwards tools/* to MRMcp's existing HTTP+SSE server.
  • Two local control tools — launch (spawns the configured backend exe) and status (probes its liveness) — let a single MCP client connection survive the backend being killed and restarted underneath.
  • Pure wireup over fastmcpp::ProxyApp + SseClientTransport + StdioServerWrapper; no new protocol code. Initialize is hand-crafted to advertise tools.listChanged: true and 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 so tools/list never blocks on a dead backend.

Test plan

  • Build via MeshLib.sln (Debug|x64). MRMCPGateway.exe lands in the standard install bin dir.
  • Register in any MCP client: command = MRMCPGateway.exe, args = ["--launch-cmd", "<backend exe>"].
  • Backend offline → connect; only launch+status visible. statusnot started. launchstarted, backend starts, forwarded calls work after.
  • Backend online at session start → full proxied tool surface (ui.* etc.) appears immediately; forwarded calls return live backend responses.

🤖 Generated with Claude Code

…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>
@Grantim Grantim marked this pull request as draft April 25, 2026 17:29
…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>
Comment thread source/MRMCPGateway/MRMCPGateway.cpp Outdated
@@ -0,0 +1,513 @@
// fastmcpp must be included before any standard headers per MRMcp's pattern.

#if defined( __GNUC__ )
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Move this to some common header to reuse in MRMcp.cpp. In MRPch project perhaps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in e2f7d1b — extracted to MRPch/MRFastmcpp.h (mirrors the MRSpdlog.h wrapping pattern), and MRMcp.cpp now also uses it.

Comment thread source/MRMCPGateway/MRMCPGateway.cpp Outdated
// 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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>
Comment thread source/MRMcp/MRFastmcpp.h
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Move it to MRMcp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed

Comment thread source/MRMcp/MRMcp.h Outdated
/// 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why isn't it resolved in MRViewer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread source/MRMcp/MRMcp.cpp
Comment on lines +177 to +179
// 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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a good approach that might be used in other places, consider moving it to MRMesh or MRViewer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

Comment thread source/MRMcp/MRMcp.cpp Outdated
Comment on lines +172 to +173
spdlog::error( "MRMcp: cannot create directory {}: {}", utf8string( path.parent_path() ), ec.message() );
return false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should be replaced with Expected<void>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should probably be moved to MRMesh or MRViewer as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the functions should be accessable from MRMCPGateway project (which is minimal proxy having no dependences)

Comment on lines +76 to +82
std::wstring cmdLine;
appendQuotedArg( cmdLine, exe.wstring() );
for ( const auto& a : args )
{
cmdLine.push_back( L' ' );
appendQuotedArg( cmdLine, utf8ToWide( a ) );
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't it be generalized to a separate helper function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed

}
const DWORD waitMs = static_cast<DWORD>(
std::chrono::duration_cast<std::chrono::milliseconds>( timeout ).count() );
const DWORD wait = WaitForSingleObject( pi.hProcess, waitMs );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed

Comment on lines +197 to +203
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 );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Create a helper function for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this file be decomposed to several ones?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ok

…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>
@Grantim
Copy link
Copy Markdown
Contributor Author

Grantim commented Apr 27, 2026

Round 2 (8c1db970) addresses the inline review:

  • MRFastmcpp.h moved to MRMcp/.
  • MRMCPGateway.cpp decomposed into MRMCPGatewayConfig.h + MRMCPGatewayCache.{h,cpp} + MRMCPGatewayBackend.{h,cpp}; MRMCPGateway.cpp shrinks to just main + arg parsing.
  • MRMCPGatewaySpawn.cpp now shares buildCmdLine / createProcessRaw (Win) and forkExec / waitForProcessWithTimeout (POSIX) between the detached and attached-with-timeout paths.
  • Server::saveToolsCache returns Expected<void>; renamed dumpToolCacheIfNeededprocessCmdArgs.

Not in this PR per the threads: moving the atomic-write helper to MRMesh; moving the spawn helpers to MRMesh.

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.

4 participants