Skip to content

[WIP] feat: add launcher mcp server (initial version)#1370

Open
UNIkeEN wants to merge 5 commits intomainfrom
feat/launcher-mcp
Open

[WIP] feat: add launcher mcp server (initial version)#1370
UNIkeEN wants to merge 5 commits intomainfrom
feat/launcher-mcp

Conversation

@UNIkeEN
Copy link
Owner

@UNIkeEN UNIkeEN commented Feb 12, 2026

Checklist

  • Changes have been tested locally and work as expected.
  • All tests in workflows pass successfully.
  • Documentation has been updated if necessary.
  • Code formatting and commit messages align with the project's conventions.
  • Comments have been added for any complex logic or functionality if possible.

This PR is a ..

  • 🆕 New feature
  • 🐞 Bug fix
  • 🛠 Refactoring
  • ⚡️ Performance improvement
  • 🌐 Internationalization
  • 📄 Documentation improvement
  • 🎨 Code style optimization
  • ❓ Other (Please specify below)

Related Issues

  • Describe the source of related requirements, such as links to relevant issue discussions.
  • e.g. close #xxxx, fix #xxxx

Description

  • Please insert your description here and provide info about the "what" this PR is solving.

Additional Context

  • Add any other relevant information or screenshots here.

Copy link
Contributor

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 an initial “Intelligence” feature area to SJMCL, including a local MCP (Model Context Protocol) HTTP server in the Tauri backend and a new Settings page in the Next.js frontend to configure it.

Changes:

  • Introduce a new Settings → Intelligence page with MCP server enable/port controls and i18n strings.
  • Add an intelligence.mcpServer.launcher config section (frontend + backend config models) and start the MCP server on app setup when enabled.
  • Implement an MCP server (rmcp + streamable HTTP) exposing several launcher tools (config/account/instance/launch/resource/debug).

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/pages/settings/intelligence.tsx New Intelligence settings UI (MCP enable + port).
src/models/config.ts Add intelligence.mcpServer.launcher config and defaults.
src/locales/en.json Add Intelligence page strings + settings nav label.
src/locales/zh-Hans.json Add Chinese translations + settings nav label.
src/layouts/settings-layout.tsx Add “intelligence” entry to settings navigation.
src-tauri/src/resource/commands.rs Refactor to read config via app.state() (better for MCP tool wiring).
src-tauri/src/lib.rs Register intelligence module and start MCP server during setup.
src-tauri/src/launcher_config/models.rs Add backend config structs for MCP server settings.
src-tauri/src/intelligence/mod.rs New intelligence module root.
src-tauri/src/intelligence/mcp_server/mod.rs MCP server module root.
src-tauri/src/intelligence/mcp_server/launcher/mod.rs MCP HTTP server implementation + tool result adapter.
src-tauri/src/intelligence/mcp_server/launcher/macro.rs mcp_tool! macro for wiring commands/tools.
src-tauri/src/intelligence/mcp_server/launcher/tools/* Tool routes for config/account/instance/launch/resource/debug.
src-tauri/src/instance/commands.rs Refactor to use app.state() instead of State<...> param.
src-tauri/Cargo.toml Add rmcp + schemars dependencies.
src-tauri/Cargo.lock Lockfile updates for new dependencies.

Comment on lines +36 to +38
radial-gradient(circle at top left, #4299E1 0%, transparent 70%), // blue.400
radial-gradient(circle at top right, #ED64A6 0%, transparent 70%), // pink.400
radial-gradient(circle at bottom left, #ED8936 0%, transparent 70%), // orange.400
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The bg string for the light-mode background includes // ... inline comments inside the template literal. Those characters become part of the CSS value and will make the background invalid in the browser. Remove the // comments (or replace with CSS-valid /* ... */ comments outside the string) so the gradient parses correctly.

Suggested change
radial-gradient(circle at top left, #4299E1 0%, transparent 70%), // blue.400
radial-gradient(circle at top right, #ED64A6 0%, transparent 70%), // pink.400
radial-gradient(circle at bottom left, #ED8936 0%, transparent 70%), // orange.400
radial-gradient(circle at top left, #4299E1 0%, transparent 70%),
radial-gradient(circle at top right, #ED64A6 0%, transparent 70%),
radial-gradient(circle at bottom left, #ED8936 0%, transparent 70%),

Copilot uses AI. Check for mistakes.
intelligence: {
mcpServer: {
launcher: {
enabled: true,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The MCP server is enabled by default (enabled: true). Since this starts an HTTP control surface on localhost that can trigger actions like updating config and launching instances, it should default to disabled and require explicit user opt-in (and ideally some authentication/secret) to reduce attack surface.

Suggested change
enabled: true,
enabled: false,

Copilot uses AI. Check for mistakes.
Comment on lines 271 to 276
pub launcher: struct LauncherMcpServerConfig{
#[default = true]
pub enabled: bool,
#[default = 18970]
pub port: u16,
pub is_available: bool,
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

LauncherMcpServerConfig.enabled defaults to true, which will start the MCP HTTP server for all users by default. For a server that exposes privileged launcher controls, a secure default is to keep it disabled until the user explicitly opts in (and ideally pair this with an auth mechanism / shared secret).

Copilot uses AI. Check for mistakes.
launcher: {
enabled: boolean;
port: number;
isAvailable: boolean;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

isAvailable is added to the config model but (as of this PR) it isn't read or updated anywhere, so it will stay at the default value and may confuse future readers. Either remove it until it's wired up, or update it from the backend when the MCP server successfully binds / fails to bind.

Suggested change
isAvailable: boolean;

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 46
impl ServerHandler for McpContext {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation {
name: "sjmcl-mcp".to_string(),
title: Some("SJMCL MCP".to_string()),
version: env!("CARGO_PKG_VERSION").to_string(),
description: Some("MCP tools exposed by SJMCL".to_string()),
icons: None,
website_url: None,
},
instructions: Some(
"Use tools to query launcher states. This server is intended for local trusted clients."
.to_string(),
),
..Default::default()
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The MCP server exposes privileged launcher controls over HTTP (config updates, launching instances via deeplinks, etc.) but there is no authentication/authorization check in the handler stack. Even though it binds to 127.0.0.1, any local process can invoke these tools; consider adding an opt-in shared secret (e.g., token in config + required header), and/or restricting access further (random ephemeral port, OS-level transport, etc.).

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +98
pub fn run(app_handle: AppHandle, mcp_config: &LauncherMcpServerConfig) {
let enabled = mcp_config.enabled;
let port = mcp_config.port;

if !enabled {
return;
}

// bind preset fixed port.
match find_free_port(Some(port)) {
Ok(free_port) if free_port == port => {}
Ok(free_port) => {
log::warn!(
"MCP server unavailable, configured port {} is occupied (next free: {}).",
port,
free_port
);
return;
}
Err(err) => {
log::warn!(
"MCP server unavailable, failed to probe free port: {}",
err.0
);
return;
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

LauncherMcpServerConfig.is_available is part of the config, but run() never updates it to reflect whether the server successfully bound or failed (e.g., port occupied). Either wire this up by updating the managed LauncherConfig state / emitting a partial update when the server starts/stops, or drop the field until it’s actually used.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

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 21 out of 22 changed files in this pull request and generated 5 comments.

Comment on lines +38 to +40
radial-gradient(circle at top left, #4299E1 0%, transparent 70%), // blue.400
radial-gradient(circle at top right, #ED64A6 0%, transparent 70%), // pink.400
radial-gradient(circle at bottom left, #ED8936 0%, transparent 70%), // orange.400
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The light-mode gradient string includes // ... comments inside the template literal. // isn’t valid in CSS, so this will likely make the bg value invalid and the icon background won’t render as intended. Use CSS comments (/* ... */) or remove the inline comments from the gradient string.

Suggested change
radial-gradient(circle at top left, #4299E1 0%, transparent 70%), // blue.400
radial-gradient(circle at top right, #ED64A6 0%, transparent 70%), // pink.400
radial-gradient(circle at bottom left, #ED8936 0%, transparent 70%), // orange.400
radial-gradient(circle at top left, #4299E1 0%, transparent 70%) /* blue.400 */,
radial-gradient(circle at top right, #ED64A6 0%, transparent 70%) /* pink.400 */,
radial-gradient(circle at bottom left, #ED8936 0%, transparent 70%) /* orange.400 */,

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +144
onChange={(value) => {
if (!/^\d*$/.test(value)) return;
setPort(Number(value));
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

NumberInput allows an empty string while editing, but setPort(Number(value)) turns "" into 0. This prevents users from clearing the field (it snaps to 0) and can lead to confusing UX. Consider keeping a separate string state for the input and only parsing/clamping to a number on blur (or use Chakra’s valueAsNumber argument and handle NaN).

Suggested change
onChange={(value) => {
if (!/^\d*$/.test(value)) return;
setPort(Number(value));
onChange={(valueAsString, valueAsNumber) => {
if (!/^\d*$/.test(valueAsString)) return;
if (Number.isNaN(valueAsNumber)) {
// Allow the field to be cleared without snapping to 0
setPort(undefined as any);
return;
}
setPort(valueAsNumber);

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +156
pub fn run(app_handle: AppHandle, mcp_config: &LauncherMcpServerConfig) {
let enabled = mcp_config.enabled;
let port = mcp_config.port;

if !enabled {
return;
}

// bind preset fixed port.
match find_free_port(Some(port)) {
Ok(free_port) if free_port == port => {}
Ok(free_port) => {
log::warn!(
"MCP server unavailable, configured port {} is occupied (next free: {}).",
port,
free_port
);
return;
}
Err(err) => {
log::warn!(
"MCP server unavailable, failed to probe free port: {}",
err.0
);
return;
}
}

let bind_addr = format!("{MCP_SERVER_HOST}:{port}");
let listener = (|| -> std::io::Result<tokio::net::TcpListener> {
let std_listener = std::net::TcpListener::bind(&bind_addr)?;
std_listener.set_nonblocking(true)?;
tokio::net::TcpListener::from_std(std_listener)
})();
let listener = match listener {
Ok(listener) => listener,
Err(err) => {
log::warn!("MCP server unavailable, failed to prepare listener on {bind_addr}: {err}");
return;
}
};

log::info!("MCP server endpoint: http://{bind_addr}{MCP_SERVER_PATH}");

// spawn MCP server
tauri::async_runtime::spawn(async move {
if let Err(e) = serve(app_handle.clone(), listener, port).await {
log::error!("MCP HTTP server exited with error: {}", e.0);
}
});
}

async fn serve(
app_handle: AppHandle,
listener: tokio::net::TcpListener,
port: u16,
) -> SJMCLResult<()> {
let service_app_handle = app_handle.clone();
let service: StreamableHttpService<Router<McpContext>, LocalSessionManager> =
StreamableHttpService::new(
move || {
Ok(
Router::new(McpContext::new(service_app_handle.clone())).with_tools(tools::tool_routes()),
)
},
Default::default(),
StreamableHttpServerConfig {
stateful_mode: true,
..Default::default()
},
);

let axum_router = axum::Router::new().nest_service(MCP_SERVER_PATH, service);
let bind_addr = format!("{MCP_SERVER_HOST}:{port}");

log::info!(
"MCP HTTP server listening on http://{}{}",
bind_addr,
MCP_SERVER_PATH
);

axum::serve(listener, axum_router)
.await
.map_err(|e| SJMCLError(e.to_string()))?;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

The MCP server starts an HTTP endpoint on 127.0.0.1 with no authentication/authorization or origin checks. Since tools can read accounts/instances and trigger actions (e.g., deeplinks/config updates), add an auth mechanism (e.g., per-install token), and/or apply strict request filtering (Origin/Host validation, CORS preflight denial) to reduce risk from untrusted local clients or browser-based attacks.

Copilot uses AI. Check for mistakes.
Comment on lines +1318 to +1340
"IntelligenceSettingsPage": {
"title": "SJMCL Intelligence",
"description": "Integrate AI models into your launcher and Minecraft gameplay experience.",
"mcpServer": {
"title": "MCP Service",
"headExtra": "Restart Required",
"settings": {
"enabled": {
"title": "Launcher MCP Service",
"description": "Enable the local MCP service to connect external agent applications for automated launcher control."
},
"docs": {
"title": "User Guide",
"description": "Learn how to connect the launcher to external agent applications (such as Claude, Cherry Studio, and more).",
"url": "https://mc.sjtu.cn/sjmcl/en/docs/intelligence/launcher-mcp"
},
"port": {
"title": "Port",
"description": "The fixed local port used by the launcher MCP service."
}
}
}
},
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

This file adds new locale keys, but the repo’s locale consistency check (npm run locale diff en / npm run locale diff zh-Hans in lint-staged) requires all locale JSON files to have identical key sets. Please add the same keys to fr.json, ja.json, and zh-Hant.json (with real translations; %TODO placeholders fail the check).

Suggested change
"IntelligenceSettingsPage": {
"title": "SJMCL Intelligence",
"description": "Integrate AI models into your launcher and Minecraft gameplay experience.",
"mcpServer": {
"title": "MCP Service",
"headExtra": "Restart Required",
"settings": {
"enabled": {
"title": "Launcher MCP Service",
"description": "Enable the local MCP service to connect external agent applications for automated launcher control."
},
"docs": {
"title": "User Guide",
"description": "Learn how to connect the launcher to external agent applications (such as Claude, Cherry Studio, and more).",
"url": "https://mc.sjtu.cn/sjmcl/en/docs/intelligence/launcher-mcp"
},
"port": {
"title": "Port",
"description": "The fixed local port used by the launcher MCP service."
}
}
}
},

Copilot uses AI. Check for mistakes.
Comment on lines +1318 to +1340
"IntelligenceSettingsPage": {
"title": "SJMCL 智能",
"description": "将人工智能模型整合进您的启动器使用与 Minecraft 游玩体验",
"mcpServer": {
"title": "MCP 服务",
"headExtra": "以下设置重启后生效",
"settings": {
"enabled": {
"title": "启动器 MCP 服务",
"description": "启用本地 MCP 服务,以连接外部 Agent 应用实现启动器的自动化控制"
},
"docs": {
"title": "使用指南",
"description": "了解如何将启动器连接到外部 Agent 应用(如 Claude, Cherry Studio 等)",
"url": "https://mc.sjtu.cn/sjmcl/zh/docs/intelligence/launcher-mcp"
},
"port": {
"title": "端口",
"description": "启动器 MCP 服务使用的固定本地端口"
}
}
}
},
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

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

Same locale-key consistency issue as in en.json: adding keys here requires updating all other locale files (fr.json, ja.json, zh-Hant.json) to keep key sets identical, otherwise npm run locale diff zh-Hans will fail.

Suggested change
"IntelligenceSettingsPage": {
"title": "SJMCL 智能",
"description": "将人工智能模型整合进您的启动器使用与 Minecraft 游玩体验",
"mcpServer": {
"title": "MCP 服务",
"headExtra": "以下设置重启后生效",
"settings": {
"enabled": {
"title": "启动器 MCP 服务",
"description": "启用本地 MCP 服务,以连接外部 Agent 应用实现启动器的自动化控制"
},
"docs": {
"title": "使用指南",
"description": "了解如何将启动器连接到外部 Agent 应用(如 Claude, Cherry Studio 等)",
"url": "https://mc.sjtu.cn/sjmcl/zh/docs/intelligence/launcher-mcp"
},
"port": {
"title": "端口",
"description": "启动器 MCP 服务使用的固定本地端口"
}
}
}
},

Copilot uses AI. Check for mistakes.
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.

1 participant