Skip to content

Commit 79e77ff

Browse files
Grantimclaude
andauthored
MCP: gateway-driven port coordination (#6007)
* MCP: gateway-driven port coordination Gateway now forwards -mcpPort N to spawned MI (configurable via --mcp-port, default 7887). MI's setupMcp parses the flag, forces the server port and auto-start (overriding the mcp.enableByDefault config), and shows the port read-only in Settings. -mcpDumpFile additionally suppresses server start so the gateway's cache-prime spawn never briefly binds the port. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Move CmdLineOverrides + parser from MRMcp to MRMcpSettings Pure code motion — colocates CLI-flag parsing with the settings layer (its only consumer). MRMcp public surface stays focused on the Server. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 221801d commit 79e77ff

10 files changed

Lines changed: 113 additions & 28 deletions

source/MRMCPGateway/MRMCPGateway.cpp

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ void printUsage()
8989
" --launch-arg <value> Default argument forwarded to the backend (repeatable).\n"
9090
" A 'launch' tool call may override these for that call.\n"
9191
" --launch-timeout <secs> How long 'launch' waits for the backend (default 30).\n"
92-
" --target-url <url> Backend MCP server URL (default http://127.0.0.1:7887).\n"
92+
" --mcp-port <port> MCP port the backend should bind (default 7887). Forwarded\n"
93+
" to spawned MI as -mcpPort; if --target-url is omitted, the\n"
94+
" gateway's probe URL is derived from this port.\n"
95+
" --target-url <url> Backend MCP server URL (default http://127.0.0.1:<mcp-port>).\n"
9396
" --sse-path <path> SSE endpoint path (default /sse).\n"
9497
" --messages-path <path> POST endpoint path (default /messages).\n"
9598
" --tools-cache-namespace <name>\n"
@@ -100,6 +103,7 @@ void printUsage()
100103

101104
bool parseArgs( int argc, char** argv, Config& cfg )
102105
{
106+
bool targetUrlGiven = false;
103107
for ( int i = 1; i < argc; ++i )
104108
{
105109
const std::string a = argv[i];
@@ -117,6 +121,12 @@ bool parseArgs( int argc, char** argv, Config& cfg )
117121
{
118122
if ( !needNext( "--target-url" ) ) return false;
119123
cfg.targetUrl = argv[++i];
124+
targetUrlGiven = true;
125+
}
126+
else if ( a == "--mcp-port" )
127+
{
128+
if ( !needNext( "--mcp-port" ) ) return false;
129+
cfg.mcpPort = std::atoi( argv[++i] );
120130
}
121131
else if ( a == "--sse-path" )
122132
{
@@ -167,6 +177,15 @@ bool parseArgs( int argc, char** argv, Config& cfg )
167177
printUsage();
168178
return false;
169179
}
180+
if ( cfg.mcpPort <= 0 )
181+
{
182+
std::cerr << "MRMCPGateway: --mcp-port must be a positive integer\n";
183+
return false;
184+
}
185+
// Keep probe URL in sync with the port we tell MI to bind, unless the user
186+
// pointed --target-url somewhere else explicitly (e.g. a remote backend).
187+
if ( !targetUrlGiven )
188+
cfg.targetUrl = "http://127.0.0.1:" + std::to_string( cfg.mcpPort );
170189
return true;
171190
}
172191

source/MRMCPGateway/MRMCPGatewayBackend.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ void registerLocalTools( fastmcpp::ProxyApp& proxy, const Config& cfg )
112112
args.push_back( a.get<std::string>() );
113113
}
114114
}
115+
// Always tell MI the port the gateway will probe; last-occurrence-wins
116+
// parsing on MI's side means this beats any user-supplied -mcpPort in args.
117+
args.emplace_back( "-mcpPort" );
118+
args.emplace_back( std::to_string( cfg.mcpPort ) );
115119
if ( probeAndTrackBackend( cfg.targetUrl ) )
116120
return std::string( "already running" );
117121
if ( !spawnDetached( cfg.launchCommand, args ) )

source/MRMCPGateway/MRMCPGatewayCache.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ void ensureFreshCache( const Config& cfg )
143143
std::vector<std::string> primeArgs = cfg.launchArgs;
144144
for ( const char* flag : { "-hidden", "-noEventLoop", "-noTelemetry", "-noSplash" } )
145145
primeArgs.emplace_back( flag );
146+
primeArgs.emplace_back( "-mcpPort" );
147+
primeArgs.emplace_back( std::to_string( cfg.mcpPort ) );
146148
primeArgs.emplace_back( "-mcpDumpFile" );
147149
primeArgs.emplace_back( cache.string() );
148150

source/MRMCPGateway/MRMCPGatewayConfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace MR::McpGateway
1313
/// read what they need without each replicating its own subset of CLI parsing.
1414
struct Config
1515
{
16+
int mcpPort = 7887; ///< -mcpPort N forwarded to spawned MI; targetUrl is derived from this if --target-url is omitted.
1617
std::string targetUrl = "http://127.0.0.1:7887";
1718
std::string ssePath = "/sse";
1819
std::string messagesPath = "/messages";

source/MRMcp/MRMcp.cpp

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,6 @@ Expected<void> Server::saveToolsCache( const std::filesystem::path& path ) const
251251
return {};
252252
}
253253

254-
void Server::processCmdArgs( const std::vector<std::string>& commandArgs ) const
255-
{
256-
for ( size_t i = 0; i + 1 < commandArgs.size(); ++i )
257-
{
258-
if ( commandArgs[i] == "-mcpDumpFile" )
259-
{
260-
const std::filesystem::path target = commandArgs[i + 1];
261-
if ( auto res = saveToolsCache( target ); !res )
262-
spdlog::error( "MRMcp: {}", res.error() );
263-
return;
264-
}
265-
}
266-
}
267254
void Server::setToolValidator( ToolValidator validator )
268255
{
269256
if ( !state_ )

source/MRMcp/MRMcp.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,6 @@ class Server
179179
/// directories as needed. Returns an error message on I/O failure.
180180
MRMCP_API Expected<void> saveToolsCache( const std::filesystem::path& path ) const;
181181

182-
/// Processes MCP-related command-line arguments. Currently only `-mcpDumpFile <path>`,
183-
/// which writes the tool cache to that path. Otherwise a no-op. Intended to be called
184-
/// once during MCP setup with the viewer's own launch arguments, after every
185-
/// `MR_ON_INIT` tool registration has run.
186-
MRMCP_API void processCmdArgs( const std::vector<std::string>& commandArgs ) const;
187182
/// Optional predicate consulted before every tool dispatch, given the tool's id.
188183
/// Return {} to allow; return `unexpected("reason")` to block — the reason surfaces
189184
/// to the MCP client as the tool-call error.

source/MRViewer/MRMcpSettings.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#include "MRMcpSettings.h"
22

33
#include "MRMesh/MRConfig.h"
4+
#include "MRMesh/MRStringConvert.h"
45

56
#ifndef MESHLIB_NO_MCP
67
#include "MRMcp/MRMcp.h"
8+
#include "MRViewer/MRViewer.h"
79
#endif
810

911
#include <utility>
@@ -61,4 +63,30 @@ void applyToServer()
6163
#endif
6264
}
6365

66+
bool isPortLockedFromCmdLine()
67+
{
68+
#ifndef MESHLIB_NO_MCP
69+
static const bool locked = []
70+
{
71+
return parseCmdLineOverrides( getViewerInstance().commandArgs ).port > 0;
72+
}();
73+
return locked;
74+
#else
75+
return false;
76+
#endif
77+
}
78+
79+
CmdLineOverrides parseCmdLineOverrides( const std::vector<std::string>& commandArgs )
80+
{
81+
CmdLineOverrides out;
82+
for ( size_t i = 0; i + 1 < commandArgs.size(); ++i )
83+
{
84+
if ( commandArgs[i] == "-mcpPort" )
85+
out.port = std::atoi( commandArgs[i + 1].c_str() );
86+
else if ( commandArgs[i] == "-mcpDumpFile" )
87+
out.dumpFilePath = pathFromUtf8( commandArgs[i + 1] );
88+
}
89+
return out;
90+
}
91+
6492
} // namespace MR::McpSettings

source/MRViewer/MRMcpSettings.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,31 @@
22

33
#include "exports.h"
44

5+
#include <filesystem>
6+
#include <string>
7+
#include <vector>
8+
59
// Those functions act on the default MCP settings in the config file.
610

711
namespace MR::McpSettings
812
{
913

14+
// Overrides parsed from the application's command-line arguments. Sentinel values
15+
// (`port <= 0`, empty `dumpFilePath`) mean "no override".
16+
struct CmdLineOverrides
17+
{
18+
int port = 0; ///< `-mcpPort N`. <= 0 means no override.
19+
std::filesystem::path dumpFilePath; ///< `-mcpDumpFile <path>`. Empty means no dump.
20+
};
21+
22+
// Pure parse: scans @p commandArgs for MCP-related flags and returns the resolved
23+
// overrides. Last occurrence of each flag wins (matches shell convention).
24+
// `-mcpPort N` forces the server port to N (overriding the config).
25+
// `-mcpDumpFile <path>` requests writing the tool cache to that path; the caller
26+
// (typically `ViewerSetup::setupMcp`) is expected to skip starting the live server
27+
// in that case so a prime spawn does not collide with a real backend on the port.
28+
[[nodiscard]] MRVIEWER_API CmdLineOverrides parseCmdLineOverrides( const std::vector<std::string>& commandArgs );
29+
1030
// Returns the MCP port from the config file, or the default value.
1131
// Note that this acts on the config file and not on the actual MCP server that might be running.
1232
[[nodiscard]] MRVIEWER_API int getPort();
@@ -25,4 +45,8 @@ MRVIEWER_API void setEnableByDefault( bool enable );
2545
// This ignores `getEnableByDefault()`.
2646
MRVIEWER_API void applyToServer();
2747

48+
// True iff `-mcpPort N` was passed on the command line. The config-backed port is then
49+
// ignored for this session, and the GUI shows the port read-only.
50+
[[nodiscard]] MRVIEWER_API bool isPortLockedFromCmdLine();
51+
2852
} // namespace MR::McpSettings

source/MRViewer/MRSetupViewer.cpp

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,27 @@ void ViewerSetup::unloadExtendedLibraries() const
197197
bool ViewerSetup::setupMcp() const
198198
{
199199
#ifndef MESHLIB_NO_MCP
200-
McpSettings::applyToServer();
201-
if ( McpSettings::getEnableByDefault() )
202-
Mcp::getDefaultServer().setRunning( true );
203-
Mcp::getDefaultServer().processCmdArgs( getViewerInstance().commandArgs );
200+
auto& server = Mcp::getDefaultServer();
201+
const auto overrides = McpSettings::parseCmdLineOverrides( getViewerInstance().commandArgs );
202+
203+
// Push the effective port (CLI override beats config).
204+
Mcp::Server::Params params = server.getParams();
205+
params.port = overrides.port > 0 ? overrides.port : McpSettings::getPort();
206+
server.setParams( std::move( params ) );
207+
208+
if ( !overrides.dumpFilePath.empty() )
209+
{
210+
// Dump-and-exit: write the tool cache and skip server start so a prime spawn
211+
// does not collide with a real backend on the same port.
212+
if ( auto res = server.saveToolsCache( overrides.dumpFilePath ); !res )
213+
spdlog::error( "MRMcp: {}", res.error() );
214+
return true;
215+
}
216+
217+
// `-mcpPort N` forces auto-start (gateway's launch path needs MCP up regardless of
218+
// the user's `mcp.enableByDefault` config). Without the flag, honor the config.
219+
if ( overrides.port > 0 || McpSettings::getEnableByDefault() )
220+
server.setRunning( true );
204221
return true;
205222
#else
206223
return false;

source/MRViewer/MRViewerSettingsPlugin.cpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,12 +1307,20 @@ void ViewerSettingsPlugin::drawMcpSettings_()
13071307
if ( UI::checkbox( _tr( "Enable by Default" ), &enableByDefault ) )
13081308
McpSettings::setEnableByDefault( enableByDefault );
13091309

1310-
int port = McpSettings::getPort();
13111310
ImGui::SetNextItemWidth( ImGui::GetFrameHeight() * 3 );
1312-
if ( UI::input<NoUnit>( _tr( "Port" ), port, 1, 65535, {}, UI::defaultSliderFlags, 0, 0 ) )
1313-
McpSettings::setPort( port );
1314-
if ( ImGui::IsItemDeactivatedAfterEdit() )
1315-
McpSettings::applyToServer();
1311+
if ( McpSettings::isPortLockedFromCmdLine() )
1312+
{
1313+
UI::readOnlyValue<NoUnit>( _tr( "Port" ), server.getParams().port );
1314+
UI::setTooltipIfHovered( _tr( "Port forced by -mcpPort command-line flag" ) );
1315+
}
1316+
else
1317+
{
1318+
int port = McpSettings::getPort();
1319+
if ( UI::input<NoUnit>( _tr( "Port" ), port, 1, 65535, {}, UI::defaultSliderFlags, 0, 0 ) )
1320+
McpSettings::setPort( port );
1321+
if ( ImGui::IsItemDeactivatedAfterEdit() )
1322+
McpSettings::applyToServer();
1323+
}
13161324
#endif
13171325
}
13181326

0 commit comments

Comments
 (0)