pub const MCP_DEFAULT_HOST: &str = "127.0.0.1";
pub const MCP_DEFAULT_PORT: u16 = 3333;
use rmcp::{
ServerHandler,
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::*,
tool, tool_handler, tool_router,
transport::streamable_http_server::{
StreamableHttpService, session::local::LocalSessionManager,
},
};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ListEntitiesRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct QueryEntityRequest {
#[schemars(description = "Name of the entity to query")]
pub name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnEntityRequest {
#[schemars(description = "Unique name for the entity")]
pub name: String,
#[schemars(description = "Mesh type: Cube, Sphere, Cylinder, Cone, Plane, Torus")]
pub mesh: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(description = "Scale as [x, y, z], defaults to [1, 1, 1]")]
pub scale: Option<[f32; 3]>,
#[schemars(description = "Base color as [r, g, b, a] where each component is 0.0-1.0")]
pub color: Option<[f32; 4]>,
#[schemars(
description = "Emissive color as [r, g, b]. Values > 1.0 create HDR bloom/glow effects"
)]
pub emissive: Option<[f32; 3]>,
#[schemars(
description = "Parent entity name. The spawned entity will be a child of this entity"
)]
pub parent: Option<String>,
#[schemars(description = "Alpha mode: opaque (default), mask, or blend (for transparency)")]
pub alpha: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct DespawnEntityRequest {
#[schemars(description = "Name of the entity to despawn")]
pub name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetPositionRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "New position as [x, y, z]")]
pub position: [f32; 3],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetRotationRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Rotation as euler angles [pitch, yaw, roll] in radians")]
pub rotation: [f32; 3],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetScaleRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "New scale as [x, y, z]")]
pub scale: [f32; 3],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetMaterialColorRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Base color as [r, g, b, a] where each component is 0.0-1.0")]
pub color: [f32; 4],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetEmissiveRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(
description = "Emissive color as [r, g, b]. Values > 1.0 create HDR bloom/glow effects"
)]
pub emissive: [f32; 3],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetParentRequest {
#[schemars(description = "Name of the child entity")]
pub name: String,
#[schemars(description = "Name of the parent entity, or null/empty to unparent")]
pub parent: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct LoadAssetRequest {
#[schemars(description = "Absolute file path to .glb, .gltf, or .fbx file")]
pub path: String,
#[schemars(description = "Unique name to reference this asset")]
pub asset_name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnPrefabRequest {
#[schemars(description = "Name of a previously loaded asset")]
pub asset_name: String,
#[schemars(description = "Unique name for the spawned entity")]
pub entity_name: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(description = "Scale as [x, y, z], defaults to [1, 1, 1]")]
pub scale: Option<[f32; 3]>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ListLoadedAssetsRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ClearSceneRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetAtmosphereRequest {
#[schemars(description = "Atmosphere type: none, sky, cloudy_sky, space, nebula, sunset, hdr")]
pub atmosphere: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct LoadHdrRequest {
#[schemars(description = "Absolute file path to .hdr file")]
pub path: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnLightRequest {
#[schemars(description = "Unique name for the light")]
pub name: String,
#[schemars(description = "Light type: point, spot, or directional")]
pub light_type: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(description = "Light color as [r, g, b] (linear RGB, 0.0-1.0+)")]
pub color: Option<[f32; 3]>,
#[schemars(description = "Light intensity (lumens for point/spot, lux for directional)")]
pub intensity: Option<f32>,
#[schemars(description = "Maximum range for point/spot lights")]
pub range: Option<f32>,
#[schemars(description = "Inner cone angle in degrees for spot lights")]
pub inner_cone_angle: Option<f32>,
#[schemars(description = "Outer cone angle in degrees for spot lights")]
pub outer_cone_angle: Option<f32>,
#[schemars(description = "Whether this light casts shadows")]
pub cast_shadows: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct AddLineRequest {
#[schemars(description = "Start point as [x, y, z]")]
pub start: [f32; 3],
#[schemars(description = "End point as [x, y, z]")]
pub end: [f32; 3],
#[schemars(description = "Line color as [r, g, b, a]")]
pub color: Option<[f32; 4]>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct AddLinesRequest {
#[schemars(description = "Array of lines, each with start, end, and optional color")]
pub lines: Vec<AddLineRequest>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ClearLinesRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetGraphicsRequest {
#[schemars(description = "Enable bloom post-processing")]
pub bloom_enabled: Option<bool>,
#[schemars(description = "Bloom intensity (0.0-1.0+)")]
pub bloom_intensity: Option<f32>,
#[schemars(
description = "Bloom brightness threshold — only pixels above this luminance contribute to bloom (default 1.0)"
)]
pub bloom_threshold: Option<f32>,
#[schemars(description = "Enable SSAO (screen-space ambient occlusion)")]
pub ssao_enabled: Option<bool>,
#[schemars(description = "SSAO radius in world units")]
pub ssao_radius: Option<f32>,
#[schemars(description = "SSAO intensity")]
pub ssao_intensity: Option<f32>,
#[schemars(description = "SSAO bias to reduce self-occlusion")]
pub ssao_bias: Option<f32>,
#[schemars(description = "Enable fog")]
pub fog_enabled: Option<bool>,
#[schemars(description = "Fog color as [r, g, b]")]
pub fog_color: Option<[f32; 3]>,
#[schemars(description = "Fog start distance")]
pub fog_start: Option<f32>,
#[schemars(description = "Fog end distance")]
pub fog_end: Option<f32>,
#[schemars(
description = "Tonemapping algorithm: aces, reinhard, reinhard_extended, uncharted2, agx, neutral, none"
)]
pub tonemap: Option<String>,
#[schemars(description = "Exposure/gamma (typically 2.2)")]
pub gamma: Option<f32>,
#[schemars(description = "Color saturation (1.0 = neutral)")]
pub saturation: Option<f32>,
#[schemars(description = "Brightness offset (-1 to 1)")]
pub brightness: Option<f32>,
#[schemars(description = "Contrast multiplier (1.0 = neutral)")]
pub contrast: Option<f32>,
#[schemars(description = "Show debug grid")]
pub show_grid: Option<bool>,
#[schemars(description = "Background clear color as [r, g, b, a]")]
pub clear_color: Option<[f32; 4]>,
#[schemars(description = "Enable depth of field")]
pub dof_enabled: Option<bool>,
#[schemars(description = "Depth of field focus distance")]
pub dof_focus_distance: Option<f32>,
#[schemars(description = "Depth of field focus range (sharp region)")]
pub dof_focus_range: Option<f32>,
#[schemars(description = "Depth of field max blur radius")]
pub dof_max_blur_radius: Option<f32>,
#[schemars(description = "Enable unlit mode (disable all lighting)")]
pub unlit_mode: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnWaterRequest {
#[schemars(description = "Unique name for the water plane")]
pub name: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(description = "Scale as [x, y, z]")]
pub scale: Option<[f32; 3]>,
#[schemars(description = "Base color (deep water) as [r, g, b, a]")]
pub base_color: Option<[f32; 4]>,
#[schemars(description = "Water color (surface) as [r, g, b, a]")]
pub water_color: Option<[f32; 4]>,
#[schemars(description = "Wave height")]
pub wave_height: Option<f32>,
#[schemars(description = "Choppiness of waves")]
pub choppy: Option<f32>,
#[schemars(description = "Wave animation speed")]
pub speed: Option<f32>,
#[schemars(description = "Wave frequency")]
pub frequency: Option<f32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetLightRequest {
#[schemars(description = "Name of the light entity to modify")]
pub name: String,
#[schemars(description = "Light color as [r, g, b] (linear RGB, 0.0-1.0+)")]
pub color: Option<[f32; 3]>,
#[schemars(description = "Light intensity (lumens for point/spot, lux for directional)")]
pub intensity: Option<f32>,
#[schemars(description = "Maximum range for point/spot lights")]
pub range: Option<f32>,
#[schemars(description = "Inner cone angle in degrees for spot lights")]
pub inner_cone_angle: Option<f32>,
#[schemars(description = "Outer cone angle in degrees for spot lights")]
pub outer_cone_angle: Option<f32>,
#[schemars(description = "Whether this light casts shadows")]
pub cast_shadows: Option<bool>,
#[schemars(description = "Shadow depth bias")]
pub shadow_bias: Option<f32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetCameraRequest {
#[schemars(description = "Camera position as [x, y, z]")]
pub position: Option<[f32; 3]>,
#[schemars(description = "Camera target/look-at point as [x, y, z]")]
pub target: Option<[f32; 3]>,
#[schemars(description = "Field of view in degrees (for perspective cameras)")]
pub fov: Option<f32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetScriptRequest {
#[schemars(description = "Name of the entity to add/update script on")]
pub name: String,
#[schemars(
description = "Rhai script source code. Variables: pos_x/y/z, rot_x/y/z, scale_x/y/z (own transform), dt/time, mouse_x/y, pressed_keys/just_pressed_keys, state (shared map), entities (map: name->{x,y,z,scale_x,scale_y,scale_z}), entity_names (array of all names for iteration), do_despawn (despawn self), despawn_names (push names to despawn other entities), do_spawn_cube/sphere + spawn_cube/sphere_x/y/z"
)]
pub source: String,
#[schemars(description = "Whether the script is enabled (default: true)")]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct RemoveScriptRequest {
#[schemars(description = "Name of the entity to remove script from")]
pub name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetGameStateRequest {
#[schemars(
description = "Key-value pairs to set in the shared game state (scripts access via state[\"key\"])"
)]
pub values: std::collections::HashMap<String, f64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct GetGameStateRequest {
#[schemars(description = "Keys to retrieve from game state (empty = get all)")]
pub keys: Option<Vec<String>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetVisibilityRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Whether the entity is visible")]
pub visible: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct PlayAnimationRequest {
#[schemars(description = "Name of the entity with AnimationPlayer")]
pub name: String,
#[schemars(description = "Animation clip name or index")]
pub clip: String,
#[schemars(description = "Whether to loop the animation (default: true)")]
pub looping: Option<bool>,
#[schemars(description = "Playback speed multiplier (default: 1.0)")]
pub speed: Option<f32>,
#[schemars(description = "Blend duration in seconds for crossfade (0 = instant switch)")]
pub blend_duration: Option<f32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct StopAnimationRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ListAnimationsRequest {
#[schemars(description = "Name of the entity with AnimationPlayer")]
pub name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnParticlesRequest {
#[schemars(description = "Unique name for the particle emitter")]
pub name: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(
description = "Preset type: fire, smoke, sparks, firework_explosion, firework_willow, firework_chrysanthemum"
)]
pub preset: String,
#[schemars(description = "Color as [r, g, b] for applicable presets")]
pub color: Option<[f32; 3]>,
#[schemars(description = "Particle count for burst effects")]
pub particle_count: Option<u32>,
#[schemars(description = "Whether emitter is enabled (default: true)")]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetParticlesRequest {
#[schemars(description = "Name of the particle emitter")]
pub name: String,
#[schemars(description = "Whether emitter is enabled")]
pub enabled: Option<bool>,
#[schemars(description = "Spawn rate (particles per second)")]
pub spawn_rate: Option<f32>,
#[schemars(description = "Emissive strength for glow")]
pub emissive_strength: Option<f32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct AddRigidBodyRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Body type: dynamic, kinematic, or static")]
pub body_type: String,
#[schemars(description = "Mass in kg (for dynamic bodies)")]
pub mass: Option<f32>,
#[schemars(description = "Initial linear velocity as [x, y, z]")]
pub linear_velocity: Option<[f32; 3]>,
#[schemars(description = "Initial angular velocity as [x, y, z]")]
pub angular_velocity: Option<[f32; 3]>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct AddColliderRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Collider shape: ball, cuboid, capsule, cylinder")]
pub shape: String,
#[schemars(
description = "Size params: [radius] for ball, [hx,hy,hz] for cuboid, [half_height,radius] for capsule/cylinder"
)]
pub size: Vec<f32>,
#[schemars(description = "Friction coefficient (0.0-1.0)")]
pub friction: Option<f32>,
#[schemars(description = "Restitution/bounciness (0.0-1.0)")]
pub restitution: Option<f32>,
#[schemars(description = "Whether this is a sensor/trigger (no collision response)")]
pub is_sensor: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ApplyImpulseRequest {
#[schemars(description = "Name of the entity with rigid body")]
pub name: String,
#[schemars(description = "Impulse vector as [x, y, z]")]
pub impulse: [f32; 3],
#[schemars(description = "Whether this is a torque impulse (angular) instead of linear")]
pub torque: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ApplyForceRequest {
#[schemars(description = "Name of the entity with rigid body")]
pub name: String,
#[schemars(description = "Force vector as [x, y, z]")]
pub force: [f32; 3],
#[schemars(description = "Whether this is a torque (angular) instead of linear force")]
pub torque: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetVelocityRequest {
#[schemars(description = "Name of the entity with rigid body")]
pub name: String,
#[schemars(description = "Linear velocity as [x, y, z]")]
pub linear: Option<[f32; 3]>,
#[schemars(description = "Angular velocity as [x, y, z]")]
pub angular: Option<[f32; 3]>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct Spawn3dTextRequest {
#[schemars(description = "Unique name for the text")]
pub name: String,
#[schemars(description = "Text content")]
pub text: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(description = "Font size")]
pub font_size: Option<f32>,
#[schemars(description = "Text color as [r, g, b, a]")]
pub color: Option<[f32; 4]>,
#[schemars(description = "Whether text always faces camera (billboard)")]
pub billboard: Option<bool>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SpawnDecalRequest {
#[schemars(description = "Unique name for the decal")]
pub name: String,
#[schemars(description = "Path to decal texture")]
pub texture: String,
#[schemars(description = "Position as [x, y, z]")]
pub position: [f32; 3],
#[schemars(description = "Size as [width, height, depth]")]
pub size: Option<[f32; 3]>,
#[schemars(description = "Rotation as euler angles [pitch, yaw, roll]")]
pub rotation: Option<[f32; 3]>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetMaterialRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Base color as [r, g, b, a]")]
pub base_color: Option<[f32; 4]>,
#[schemars(description = "Emissive color as [r, g, b]")]
pub emissive: Option<[f32; 3]>,
#[schemars(description = "Roughness (0.0 = mirror, 1.0 = diffuse)")]
pub roughness: Option<f32>,
#[schemars(description = "Metallic factor (0.0 = dielectric, 1.0 = metal)")]
pub metallic: Option<f32>,
#[schemars(description = "Alpha mode: opaque, mask, blend")]
pub alpha_mode: Option<String>,
#[schemars(description = "Alpha cutoff for mask mode")]
pub alpha_cutoff: Option<f32>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetCastsShadowRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Whether the entity casts shadows")]
pub casts_shadow: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetEntityTagsRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Tags to set on the entity (replaces existing tags)")]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct SetEntityMetadataRequest {
#[schemars(description = "Name of the entity")]
pub name: String,
#[schemars(description = "Key-value metadata pairs to set (replaces existing metadata)")]
pub metadata: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct RenameEntityRequest {
#[schemars(description = "Current name of the entity")]
pub name: String,
#[schemars(description = "New name for the entity")]
pub new_name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct GetInputRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct GetTimeRequest {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct BatchOperation {
#[schemars(
description = "Operation type: spawn_entity, despawn_entity, set_position, set_rotation, set_scale, set_material_color, set_emissive, set_parent, query_entity, list_entities, clear_scene, set_atmosphere"
)]
pub operation: String,
#[schemars(description = "Entity name for the operation")]
pub name: Option<String>,
#[schemars(
description = "Mesh type for spawn_entity: Cube, Sphere, Cylinder, Cone, Plane, Torus"
)]
pub mesh: Option<String>,
#[schemars(description = "Position as [x, y, z]")]
pub position: Option<[f32; 3]>,
#[schemars(description = "Scale as [x, y, z]")]
pub scale: Option<[f32; 3]>,
#[schemars(description = "Rotation as euler angles [pitch, yaw, roll] in radians")]
pub rotation: Option<[f32; 3]>,
#[schemars(description = "Color as [r, g, b, a] where each component is 0.0-1.0")]
pub color: Option<[f32; 4]>,
#[schemars(
description = "Emissive color as [r, g, b]. Values > 1.0 create HDR bloom/glow effects"
)]
pub emissive: Option<[f32; 3]>,
#[schemars(description = "Parent entity name for spawn_entity or set_parent")]
pub parent: Option<String>,
#[schemars(description = "Alpha mode: opaque (default), mask, or blend (for transparency)")]
pub alpha: Option<String>,
#[schemars(
description = "Atmosphere type for set_atmosphere: none, sky, cloudy_sky, space, nebula, sunset, hdr"
)]
pub atmosphere: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct BatchRequest {
#[schemars(description = "Array of operations to execute atomically in a single frame")]
pub operations: Vec<BatchOperation>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct RunCommandsRequest {
#[schemars(description = "Newline-separated commands. Format:\n\
spawn <name> <mesh> <x>,<y>,<z> [scale:<s>] [color:<r>,<g>,<b>,<a>] [emissive:<r>,<g>,<b>] [parent:<name>] [alpha:<mode>]\n\
despawn <name>\n\
pos <name> <x>,<y>,<z>\n\
rot <name> <pitch>,<yaw>,<roll>\n\
scale <name> <x>,<y>,<z>\n\
color <name> <r>,<g>,<b>,<a>\n\
emissive <name> <r>,<g>,<b>\n\
parent <child> <parent>\n\
atmosphere <type> (none, sky, cloudy_sky, space, nebula, sunset, hdr)\n\
hdr <path> (load .hdr file)\n\
clear\n\
Mesh types: Cube, Sphere, Cylinder, Cone, Plane, Torus\n\
Alpha modes: opaque (default), mask, blend")]
pub commands: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum McpCommand {
ListEntities(ListEntitiesRequest),
QueryEntity(QueryEntityRequest),
SpawnEntity(SpawnEntityRequest),
DespawnEntity(DespawnEntityRequest),
SetPosition(SetPositionRequest),
SetRotation(SetRotationRequest),
SetScale(SetScaleRequest),
SetMaterialColor(SetMaterialColorRequest),
SetEmissive(SetEmissiveRequest),
SetParent(SetParentRequest),
LoadAsset(LoadAssetRequest),
SpawnPrefab(SpawnPrefabRequest),
ListLoadedAssets(ListLoadedAssetsRequest),
ClearScene(ClearSceneRequest),
SetAtmosphere(SetAtmosphereRequest),
LoadHdr(LoadHdrRequest),
SpawnLight(SpawnLightRequest),
SetLight(SetLightRequest),
AddLine(AddLineRequest),
AddLines(AddLinesRequest),
ClearLines(ClearLinesRequest),
SetGraphics(SetGraphicsRequest),
SpawnWater(SpawnWaterRequest),
SetCamera(SetCameraRequest),
SetScript(SetScriptRequest),
RemoveScript(RemoveScriptRequest),
SetGameState(SetGameStateRequest),
GetGameState(GetGameStateRequest),
SetVisibility(SetVisibilityRequest),
PlayAnimation(PlayAnimationRequest),
StopAnimation(StopAnimationRequest),
ListAnimations(ListAnimationsRequest),
SpawnParticles(SpawnParticlesRequest),
SetParticles(SetParticlesRequest),
AddRigidBody(AddRigidBodyRequest),
AddCollider(AddColliderRequest),
ApplyImpulse(ApplyImpulseRequest),
ApplyForce(ApplyForceRequest),
SetVelocity(SetVelocityRequest),
Spawn3dText(Spawn3dTextRequest),
SpawnDecal(SpawnDecalRequest),
SetMaterial(SetMaterialRequest),
SetCastsShadow(SetCastsShadowRequest),
GetInput(GetInputRequest),
GetTime(GetTimeRequest),
SetEntityTags(SetEntityTagsRequest),
SetEntityMetadata(SetEntityMetadataRequest),
RenameEntity(RenameEntityRequest),
Batch(BatchRequest),
RunCommands(RunCommandsRequest),
}
impl std::fmt::Display for McpCommand {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
McpCommand::ListEntities(_) => write!(formatter, "Listing entities"),
McpCommand::QueryEntity(request) => write!(formatter, "Querying '{}'", request.name),
McpCommand::SpawnEntity(request) => {
write!(formatter, "Spawning {} '{}'", request.mesh, request.name)
}
McpCommand::DespawnEntity(request) => write!(formatter, "Removing '{}'", request.name),
McpCommand::SetPosition(request) => {
write!(
formatter,
"Moving '{}' to ({:.1}, {:.1}, {:.1})",
request.name, request.position[0], request.position[1], request.position[2]
)
}
McpCommand::SetRotation(request) => write!(formatter, "Rotating '{}'", request.name),
McpCommand::SetScale(request) => write!(formatter, "Scaling '{}'", request.name),
McpCommand::SetMaterialColor(request) => {
write!(formatter, "Changing color of '{}'", request.name)
}
McpCommand::SetEmissive(request) => {
write!(formatter, "Setting emissive on '{}'", request.name)
}
McpCommand::SetMaterial(request) => {
write!(formatter, "Setting material on '{}'", request.name)
}
McpCommand::SetParent(request) => write!(formatter, "Reparenting '{}'", request.name),
McpCommand::SetVisibility(request) => {
let action = if request.visible { "Showing" } else { "Hiding" };
write!(formatter, "{action} '{}'", request.name)
}
McpCommand::LoadAsset(request) => write!(formatter, "Loading asset '{}'", request.path),
McpCommand::SpawnPrefab(request) => {
write!(formatter, "Spawning prefab '{}'", request.entity_name)
}
McpCommand::ListLoadedAssets(_) => write!(formatter, "Listing loaded assets"),
McpCommand::ClearScene(_) => write!(formatter, "Clearing scene"),
McpCommand::SetAtmosphere(request) => {
write!(formatter, "Setting atmosphere to '{}'", request.atmosphere)
}
McpCommand::LoadHdr(request) => write!(formatter, "Loading HDR '{}'", request.path),
McpCommand::SpawnLight(request) => {
write!(
formatter,
"Spawning {} light '{}'",
request.light_type, request.name
)
}
McpCommand::SetLight(request) => {
write!(formatter, "Adjusting light '{}'", request.name)
}
McpCommand::SetCamera(_) => write!(formatter, "Adjusting camera"),
McpCommand::SpawnWater(request) => {
write!(formatter, "Spawning water '{}'", request.name)
}
McpCommand::Spawn3dText(request) => {
write!(formatter, "Spawning 3D text '{}'", request.name)
}
McpCommand::SpawnParticles(request) => {
write!(
formatter,
"Spawning {} particles '{}'",
request.preset, request.name
)
}
McpCommand::SetParticles(request) => {
write!(formatter, "Updating particles '{}'", request.name)
}
McpCommand::SpawnDecal(request) => {
write!(formatter, "Spawning decal '{}'", request.name)
}
McpCommand::SetGraphics(_) => write!(formatter, "Updating graphics settings"),
McpCommand::AddLine(_) => write!(formatter, "Drawing line"),
McpCommand::AddLines(_) => write!(formatter, "Drawing lines"),
McpCommand::ClearLines(_) => write!(formatter, "Clearing lines"),
McpCommand::AddRigidBody(request) => {
write!(formatter, "Adding rigid body to '{}'", request.name)
}
McpCommand::AddCollider(request) => {
write!(formatter, "Adding collider to '{}'", request.name)
}
McpCommand::ApplyImpulse(request) => {
write!(formatter, "Applying impulse to '{}'", request.name)
}
McpCommand::ApplyForce(request) => {
write!(formatter, "Applying force to '{}'", request.name)
}
McpCommand::SetVelocity(request) => {
write!(formatter, "Setting velocity on '{}'", request.name)
}
McpCommand::SetScript(request) => {
write!(formatter, "Setting script '{}'", request.name)
}
McpCommand::RemoveScript(request) => {
write!(formatter, "Removing script '{}'", request.name)
}
McpCommand::SetGameState(_) => write!(formatter, "Setting game state"),
McpCommand::GetGameState(_) => write!(formatter, "Getting game state"),
McpCommand::PlayAnimation(request) => {
write!(
formatter,
"Playing animation '{}' on '{}'",
request.clip, request.name
)
}
McpCommand::StopAnimation(request) => {
write!(formatter, "Stopping animation on '{}'", request.name)
}
McpCommand::ListAnimations(request) => {
write!(formatter, "Listing animations for '{}'", request.name)
}
McpCommand::SetCastsShadow(request) => {
write!(formatter, "Setting shadow casting on '{}'", request.name)
}
McpCommand::GetInput(_) => write!(formatter, "Getting input state"),
McpCommand::GetTime(_) => write!(formatter, "Getting time info"),
McpCommand::SetEntityTags(request) => {
write!(formatter, "Setting tags on '{}'", request.name)
}
McpCommand::SetEntityMetadata(request) => {
write!(formatter, "Setting metadata on '{}'", request.name)
}
McpCommand::RenameEntity(request) => {
write!(
formatter,
"Renaming '{}' to '{}'",
request.name, request.new_name
)
}
McpCommand::Batch(_) => write!(formatter, "Executing batch commands"),
McpCommand::RunCommands(_) => write!(formatter, "Running text commands"),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EntityInfoData {
pub name: String,
pub position: [f32; 3],
pub rotation: [f32; 4],
pub scale: [f32; 3],
pub tags: Vec<String>,
pub metadata: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum McpResponse {
EntityList(Vec<String>),
EntityInfo(Box<EntityInfoData>),
AssetList(Vec<String>),
AnimationList(Vec<String>),
GameState(std::collections::HashMap<String, f64>),
InputState {
pressed_keys: Vec<String>,
mouse_position: [f32; 2],
mouse_buttons: [bool; 3],
},
TimeInfo {
delta_time: f32,
elapsed_time: f32,
},
Success(String),
Error(String),
BatchResult(Vec<McpResponse>),
}
pub type PendingCommand = (McpCommand, tokio::sync::oneshot::Sender<McpResponse>);
pub type CommandQueue = Arc<Mutex<Vec<PendingCommand>>>;
#[derive(Clone)]
pub struct McpServer {
tool_router: ToolRouter<Self>,
command_queue: CommandQueue,
}
#[tool_router]
impl McpServer {
pub fn new(command_queue: CommandQueue) -> Self {
Self {
tool_router: Self::tool_router(),
command_queue,
}
}
async fn send_command_and_wait(&self, command: McpCommand) -> String {
let (sender, receiver) = tokio::sync::oneshot::channel();
{
let mut queue = self.command_queue.lock().unwrap();
queue.push((command, sender));
}
match tokio::time::timeout(std::time::Duration::from_secs(5), receiver).await {
Ok(Ok(response)) => serde_json::to_string_pretty(&response)
.unwrap_or_else(|_| format!("{:?}", response)),
Ok(Err(_)) => "Command processing cancelled".to_string(),
Err(_) => "Timeout waiting for response".to_string(),
}
}
#[tool(description = "List all named entities in the scene")]
async fn list_entities(&self, Parameters(request): Parameters<ListEntitiesRequest>) -> String {
self.send_command_and_wait(McpCommand::ListEntities(request))
.await
}
#[tool(description = "Query information about a specific entity by name")]
async fn query_entity(&self, Parameters(request): Parameters<QueryEntityRequest>) -> String {
self.send_command_and_wait(McpCommand::QueryEntity(request))
.await
}
#[tool(
description = "Spawn a new entity with a mesh at a position, optionally with color, emissive, parent, and alpha mode"
)]
async fn spawn_entity(&self, Parameters(request): Parameters<SpawnEntityRequest>) -> String {
self.send_command_and_wait(McpCommand::SpawnEntity(request))
.await
}
#[tool(description = "Despawn an entity by name")]
async fn despawn_entity(
&self,
Parameters(request): Parameters<DespawnEntityRequest>,
) -> String {
self.send_command_and_wait(McpCommand::DespawnEntity(request))
.await
}
#[tool(description = "Set the position of an entity")]
async fn set_position(&self, Parameters(request): Parameters<SetPositionRequest>) -> String {
self.send_command_and_wait(McpCommand::SetPosition(request))
.await
}
#[tool(description = "Set the rotation of an entity using euler angles in radians")]
async fn set_rotation(&self, Parameters(request): Parameters<SetRotationRequest>) -> String {
self.send_command_and_wait(McpCommand::SetRotation(request))
.await
}
#[tool(description = "Set the scale of an entity")]
async fn set_scale(&self, Parameters(request): Parameters<SetScaleRequest>) -> String {
self.send_command_and_wait(McpCommand::SetScale(request))
.await
}
#[tool(description = "Set the material base color of an entity")]
async fn set_material_color(
&self,
Parameters(request): Parameters<SetMaterialColorRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SetMaterialColor(request))
.await
}
#[tool(
description = "Set the emissive color of an entity. Values > 1.0 create HDR bloom/glow effects"
)]
async fn set_emissive(&self, Parameters(request): Parameters<SetEmissiveRequest>) -> String {
self.send_command_and_wait(McpCommand::SetEmissive(request))
.await
}
#[tool(
description = "Set or clear the parent of an entity. Pass null/empty parent to unparent"
)]
async fn set_parent(&self, Parameters(request): Parameters<SetParentRequest>) -> String {
self.send_command_and_wait(McpCommand::SetParent(request))
.await
}
#[tool(description = "Load a 3D asset from file (.glb, .gltf, or .fbx)")]
async fn load_asset(&self, Parameters(request): Parameters<LoadAssetRequest>) -> String {
self.send_command_and_wait(McpCommand::LoadAsset(request))
.await
}
#[tool(description = "Spawn a prefab from a previously loaded asset")]
async fn spawn_prefab(&self, Parameters(request): Parameters<SpawnPrefabRequest>) -> String {
self.send_command_and_wait(McpCommand::SpawnPrefab(request))
.await
}
#[tool(description = "List all loaded assets available for spawning")]
async fn list_loaded_assets(
&self,
Parameters(request): Parameters<ListLoadedAssetsRequest>,
) -> String {
self.send_command_and_wait(McpCommand::ListLoadedAssets(request))
.await
}
#[tool(description = "Clear all named entities from the scene")]
async fn clear_scene(&self, Parameters(request): Parameters<ClearSceneRequest>) -> String {
self.send_command_and_wait(McpCommand::ClearScene(request))
.await
}
#[tool(
description = "Set the atmosphere/skybox type: none, sky, cloudy_sky, space, nebula, sunset, hdr"
)]
async fn set_atmosphere(
&self,
Parameters(request): Parameters<SetAtmosphereRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SetAtmosphere(request))
.await
}
#[tool(
description = "Load an HDR skybox from file path. Sets atmosphere to 'hdr' automatically"
)]
async fn load_hdr(&self, Parameters(request): Parameters<LoadHdrRequest>) -> String {
self.send_command_and_wait(McpCommand::LoadHdr(request))
.await
}
#[tool(
description = "Execute multiple operations atomically in a single frame. Supported operations: spawn_entity, despawn_entity, set_position, set_rotation, set_scale, set_material_color, set_emissive, set_parent, query_entity, list_entities, clear_scene, set_atmosphere. Each operation requires 'name' (except list_entities and clear_scene) and operation-specific fields."
)]
async fn batch(&self, Parameters(request): Parameters<BatchRequest>) -> String {
self.send_command_and_wait(McpCommand::Batch(request)).await
}
#[tool(
description = "Execute concise text commands. Much shorter than batch for common operations. Example:\nspawn sun Sphere 0,0,0 scale:2 emissive:5,4,0\nspawn planet Sphere 5,0,0 scale:0.5 parent:sun\nparent child_name parent_name\nclear"
)]
async fn run(&self, Parameters(request): Parameters<RunCommandsRequest>) -> String {
self.send_command_and_wait(McpCommand::RunCommands(request))
.await
}
#[tool(description = "Spawn a light (point, spot, or directional)")]
async fn spawn_light(&self, Parameters(request): Parameters<SpawnLightRequest>) -> String {
self.send_command_and_wait(McpCommand::SpawnLight(request))
.await
}
#[tool(description = "Modify an existing light's properties")]
async fn set_light(&self, Parameters(request): Parameters<SetLightRequest>) -> String {
self.send_command_and_wait(McpCommand::SetLight(request))
.await
}
#[tool(description = "Add a debug line")]
async fn add_line(&self, Parameters(request): Parameters<AddLineRequest>) -> String {
self.send_command_and_wait(McpCommand::AddLine(request))
.await
}
#[tool(description = "Add multiple debug lines")]
async fn add_lines(&self, Parameters(request): Parameters<AddLinesRequest>) -> String {
self.send_command_and_wait(McpCommand::AddLines(request))
.await
}
#[tool(description = "Clear all debug lines")]
async fn clear_lines(&self, Parameters(request): Parameters<ClearLinesRequest>) -> String {
self.send_command_and_wait(McpCommand::ClearLines(request))
.await
}
#[tool(description = "Configure graphics settings: bloom, SSAO, fog, tonemapping, DOF, etc.")]
async fn set_graphics(&self, Parameters(request): Parameters<SetGraphicsRequest>) -> String {
self.send_command_and_wait(McpCommand::SetGraphics(request))
.await
}
#[tool(description = "Spawn a water plane with wave effects")]
async fn spawn_water(&self, Parameters(request): Parameters<SpawnWaterRequest>) -> String {
self.send_command_and_wait(McpCommand::SpawnWater(request))
.await
}
#[tool(description = "Set the main camera's position, target, and field of view")]
async fn set_camera(&self, Parameters(request): Parameters<SetCameraRequest>) -> String {
self.send_command_and_wait(McpCommand::SetCamera(request))
.await
}
#[tool(
description = "Add or update a Rhai script on an entity. Scripts run each frame. Modify pos_x/y/z, rot_x/y/z, scale_x/y/z for transforms. Use state[\"key\"] for shared game state. Query entities via entities[\"name\"].x/y/z. Iterate with entity_names array (use starts_with to filter by prefix). Despawn others via despawn_names.push(\"name\"). Check input via pressed_keys.contains(\"W\"). Set do_despawn=true to remove self."
)]
async fn set_script(&self, Parameters(request): Parameters<SetScriptRequest>) -> String {
self.send_command_and_wait(McpCommand::SetScript(request))
.await
}
#[tool(description = "Remove script from an entity")]
async fn remove_script(&self, Parameters(request): Parameters<RemoveScriptRequest>) -> String {
self.send_command_and_wait(McpCommand::RemoveScript(request))
.await
}
#[tool(
description = "Set values in the shared game state that scripts can read/write via state[\"key\"]"
)]
async fn set_game_state(&self, Parameters(request): Parameters<SetGameStateRequest>) -> String {
self.send_command_and_wait(McpCommand::SetGameState(request))
.await
}
#[tool(
description = "Get values from the shared game state. Pass specific keys or omit to get all."
)]
async fn get_game_state(&self, Parameters(request): Parameters<GetGameStateRequest>) -> String {
self.send_command_and_wait(McpCommand::GetGameState(request))
.await
}
#[tool(description = "Set visibility of an entity (show/hide)")]
async fn set_visibility(
&self,
Parameters(request): Parameters<SetVisibilityRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SetVisibility(request))
.await
}
#[tool(description = "Play an animation on an entity. Clip can be name or index.")]
async fn play_animation(
&self,
Parameters(request): Parameters<PlayAnimationRequest>,
) -> String {
self.send_command_and_wait(McpCommand::PlayAnimation(request))
.await
}
#[tool(description = "Stop animation playback on an entity")]
async fn stop_animation(
&self,
Parameters(request): Parameters<StopAnimationRequest>,
) -> String {
self.send_command_and_wait(McpCommand::StopAnimation(request))
.await
}
#[tool(description = "List available animation clips on an entity")]
async fn list_animations(
&self,
Parameters(request): Parameters<ListAnimationsRequest>,
) -> String {
self.send_command_and_wait(McpCommand::ListAnimations(request))
.await
}
#[tool(
description = "Spawn a particle emitter with a preset (fire, smoke, sparks, firework_explosion, firework_willow, firework_chrysanthemum)"
)]
async fn spawn_particles(
&self,
Parameters(request): Parameters<SpawnParticlesRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SpawnParticles(request))
.await
}
#[tool(description = "Modify particle emitter settings")]
async fn set_particles(&self, Parameters(request): Parameters<SetParticlesRequest>) -> String {
self.send_command_and_wait(McpCommand::SetParticles(request))
.await
}
#[tool(description = "Add a rigid body for physics simulation (dynamic, kinematic, or static)")]
async fn add_rigid_body(&self, Parameters(request): Parameters<AddRigidBodyRequest>) -> String {
self.send_command_and_wait(McpCommand::AddRigidBody(request))
.await
}
#[tool(
description = "Add a collider shape for collision detection (ball, cuboid, capsule, cylinder)"
)]
async fn add_collider(&self, Parameters(request): Parameters<AddColliderRequest>) -> String {
self.send_command_and_wait(McpCommand::AddCollider(request))
.await
}
#[tool(description = "Apply an instant impulse to a rigid body")]
async fn apply_impulse(&self, Parameters(request): Parameters<ApplyImpulseRequest>) -> String {
self.send_command_and_wait(McpCommand::ApplyImpulse(request))
.await
}
#[tool(description = "Apply a continuous force to a rigid body")]
async fn apply_force(&self, Parameters(request): Parameters<ApplyForceRequest>) -> String {
self.send_command_and_wait(McpCommand::ApplyForce(request))
.await
}
#[tool(description = "Set linear and/or angular velocity of a rigid body")]
async fn set_velocity(&self, Parameters(request): Parameters<SetVelocityRequest>) -> String {
self.send_command_and_wait(McpCommand::SetVelocity(request))
.await
}
#[tool(description = "Spawn 3D text in world space")]
async fn spawn_3d_text(&self, Parameters(request): Parameters<Spawn3dTextRequest>) -> String {
self.send_command_and_wait(McpCommand::Spawn3dText(request))
.await
}
#[tool(description = "Spawn a decal (projected texture onto surfaces)")]
async fn spawn_decal(&self, Parameters(request): Parameters<SpawnDecalRequest>) -> String {
self.send_command_and_wait(McpCommand::SpawnDecal(request))
.await
}
#[tool(description = "Set material properties (roughness, metallic, colors, alpha)")]
async fn set_material(&self, Parameters(request): Parameters<SetMaterialRequest>) -> String {
self.send_command_and_wait(McpCommand::SetMaterial(request))
.await
}
#[tool(description = "Set whether an entity casts shadows")]
async fn set_casts_shadow(
&self,
Parameters(request): Parameters<SetCastsShadowRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SetCastsShadow(request))
.await
}
#[tool(description = "Get current input state (pressed keys, mouse position)")]
async fn get_input(&self, Parameters(request): Parameters<GetInputRequest>) -> String {
self.send_command_and_wait(McpCommand::GetInput(request))
.await
}
#[tool(description = "Get time information (delta time, elapsed time)")]
async fn get_time(&self, Parameters(request): Parameters<GetTimeRequest>) -> String {
self.send_command_and_wait(McpCommand::GetTime(request))
.await
}
#[tool(
description = "Set tags on an entity. Tags are string labels used by game logic (e.g. 'locked', 'exit_door', 'note'). Replaces existing tags."
)]
async fn set_entity_tags(
&self,
Parameters(request): Parameters<SetEntityTagsRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SetEntityTags(request))
.await
}
#[tool(
description = "Set metadata key-value pairs on an entity. Values can be strings, numbers, booleans, arrays, or objects. Used by game logic for properties like note content, lever actions, etc. Replaces existing metadata."
)]
async fn set_entity_metadata(
&self,
Parameters(request): Parameters<SetEntityMetadataRequest>,
) -> String {
self.send_command_and_wait(McpCommand::SetEntityMetadata(request))
.await
}
#[tool(description = "Rename an entity")]
async fn rename_entity(&self, Parameters(request): Parameters<RenameEntityRequest>) -> String {
self.send_command_and_wait(McpCommand::RenameEntity(request))
.await
}
}
#[tool_handler]
impl ServerHandler for McpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some(
"Nightshade Engine MCP Server - Control entities and scene elements".into(),
),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
pub fn start_mcp_server(command_queue: CommandQueue) -> tokio::sync::oneshot::Sender<()> {
let (shutdown_sender, shutdown_receiver) = tokio::sync::oneshot::channel();
std::thread::spawn(move || {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let command_queue_clone = command_queue.clone();
let service = StreamableHttpService::new(
move || Ok(McpServer::new(command_queue_clone.clone())),
LocalSessionManager::default().into(),
Default::default(),
);
let bind_address = format!("{}:{}", MCP_DEFAULT_HOST, MCP_DEFAULT_PORT);
let router = axum::Router::new().nest_service("/mcp", service);
let tcp_listener = match tokio::net::TcpListener::bind(&bind_address).await {
Ok(listener) => listener,
Err(error) => {
tracing::error!("Failed to bind MCP server to {}: {}", bind_address, error);
return;
}
};
tracing::info!("MCP server listening on http://{}/mcp", bind_address);
tracing::info!(
"Add to Claude Code: claude mcp add --transport http nightshade http://{}/mcp",
bind_address
);
axum::serve(tcp_listener, router)
.with_graceful_shutdown(async {
tokio::select! {
_ = shutdown_receiver => {}
_ = tokio::signal::ctrl_c() => {}
}
})
.await
.ok();
});
});
shutdown_sender
}
pub fn create_mcp_queues() -> CommandQueue {
Arc::new(Mutex::new(Vec::new()))
}