From b5583c4c1b97e38f8d6e447a387156cfcf2c51df Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 1 Mar 2026 12:05:41 -0600 Subject: [PATCH] Update dependencies and integrate schemars for JSON schema generation - Added `schemars` and related dependencies to `Cargo.toml` and `Cargo.lock` for enhanced JSON schema support. - Updated the `mcp.rs` file to utilize `schemars` for generating JSON schemas for various tool arguments, improving API documentation and validation. - Enhanced the TODO list to reflect new tasks related to schema documentation and potential improvements in input validation. These changes aim to improve the overall functionality and usability of the MCP integration by providing better schema definitions for tool arguments. --- Cargo.lock | 63 +++++++++++ Cargo.toml | 1 + TODO.txt | 1 + src/web/mcp.rs | 296 ++++++++++++++++++++++++------------------------- 4 files changed, 211 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6430aa2..d97cd761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,7 @@ dependencies = [ "rmp-serde", "roblox_install", "rojo-insta-ext", + "schemars", "semver 1.0.27", "serde", "serde_bytes", @@ -966,6 +967,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -3747,6 +3754,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -4052,6 +4079,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.44", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4148,6 +4200,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.117", +] + [[package]] name = "serde_json" version = "1.0.149" diff --git a/Cargo.toml b/Cargo.toml index 378a35c0..b5b6a9e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ tempfile = "3.26.0" ritz = "0.1.0" roblox_install = "1.0.0" serde = { version = "1.0.228", features = ["derive", "rc"] } +schemars = "1" serde_json = "1.0.149" json5 = "1.3.1" lexical-write-float = "1.0.6" diff --git a/TODO.txt b/TODO.txt index 126e1f31..291e11b2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -370,6 +370,7 @@ can we allow the terminal to stay so i can like run some git commands myself wit - Listen to MessageOut to get the log info we are wanting. - Is this actually the one we want? I might be confused, check the docs, this looks new. * The goal with these trace logging is so that when others are using this tool, we can actually debug real issues by having them send me their logs. +* It might be worth actually having a log entry per-sync? [sync] right now sync is running REALLY slow diff --git a/src/web/mcp.rs b/src/web/mcp.rs index 7fee5560..c1142d9f 100644 --- a/src/web/mcp.rs +++ b/src/web/mcp.rs @@ -6,6 +6,7 @@ use std::sync::{ use bytes::Bytes; use http_body_util::{BodyExt, Full}; use hyper::{body::Incoming, header::CONTENT_TYPE, Method, Request, Response, StatusCode}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -26,7 +27,7 @@ pub struct PluginConfig { } /// Agent-specified auto-accept directive for a single instance change. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct SyncOverride { pub id: String, @@ -94,6 +95,101 @@ pub struct PluginToolResult { pub response: Option, } +// --------------------------------------------------------------------------- +// Tool argument schemas (schemars-derived, used only for inputSchema generation) +// --------------------------------------------------------------------------- + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct AtlasSyncArgs { + /// Sync mode. 'standard' (default): auto-accept if all changes are + /// git-resolved, otherwise show Studio UI. 'manual': always show + /// Studio UI. 'fastfail': fail immediately if any changes are + /// unresolved. 'dryrun': return what would change without applying. + mode: Option, + /// Auto-accept directives for specific instance changes, matched + /// by id from a previous sync response. + overrides: Option>, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(rename_all = "snake_case")] +enum SyncMode { + Standard, + Manual, + Fastfail, + Dryrun, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(rename_all = "camelCase")] +struct GetScriptArgs { + /// Server Ref (32-char hex) from a previous atlas_sync response. + /// Preferred over fsPath. + id: Option, + /// Atlas filesystem path relative to project root + /// (e.g. 'src/server/MyScript.server.luau'). Resolved server-side + /// to an instance id. + fs_path: Option, + /// If true, read unsaved editor content instead of the saved Source + /// property. Default: false. + from_draft: Option, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct RunCodeArgs { + /// Luau code to execute in Roblox Studio. + command: String, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct InsertModelArgs { + /// Search query for the model on the Roblox Creator Store. + query: String, +} + +#[derive(JsonSchema)] +struct NoArgs {} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(rename_all = "snake_case")] +enum StartStopPlayMode { + StartPlay, + RunServer, + Stop, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct StartStopPlayArgs { + /// Play mode action. + mode: StartStopPlayMode, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +#[schemars(rename_all = "snake_case")] +enum PlayTestMode { + StartPlay, + RunServer, +} + +#[allow(dead_code)] +#[derive(JsonSchema)] +struct RunScriptInPlayModeArgs { + /// Luau code to run inside a play session. + code: String, + /// Timeout in seconds. Defaults to 100. + timeout: Option, + /// Play mode to start. + mode: PlayTestMode, +} + /// Result for get_script, deserialized from the plugin's Value response. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -294,155 +390,50 @@ fn handle_initialize(id: Option) -> Response> { json_response(&resp, StatusCode::OK) } -fn handle_tools_list(id: Option) -> Response> { - let result = serde_json::json!({ - "tools": [ - { - "name": "atlas_sync", - "description": include_str!("mcp_docs/atlas_sync.md"), - "inputSchema": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["standard", "manual", "fastfail", "dryrun"], - "description": "Sync mode. 'standard' (default): auto-accept if all changes are git-resolved, otherwise show Studio UI. 'manual': always show Studio UI. 'fastfail': fail immediately if any changes are unresolved. 'dryrun': return what would change without applying." - }, - "overrides": { - "type": "array", - "description": "Auto-accept directives for specific instance changes, matched by id from a previous sync response.", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Server Ref (32-char hex) from a previous sync response." - }, - "direction": { - "type": "string", - "enum": ["push", "pull"], - "description": "Direction to accept: 'push' (Atlas to Studio) or 'pull' (Studio to Atlas)." - }, - "studioHash": { - "type": "string", - "description": "For scripts: SHA1 of git blob of current Studio Source. Override is rejected if hash doesn't match (concurrent edit detected)." - }, - "expectedProperties": { - "type": "object", - "description": "Map of property name to expected current Studio value (RbxDom encoded). Override is rejected if any value doesn't match." - } - }, - "required": ["id", "direction"] - } - } - } - } - }, - { - "name": "get_script", - "description": include_str!("mcp_docs/get_script.md"), - "inputSchema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Server Ref (32-char hex) from a previous atlas_sync response. Preferred over fsPath." - }, - "fsPath": { - "type": "string", - "description": "Atlas filesystem path relative to project root (e.g. 'src/server/MyScript.server.luau'). Resolved server-side to an instance id." - }, - "fromDraft": { - "type": "boolean", - "description": "If true, read unsaved editor content instead of the saved Source property. Default: false." - } - } - } - }, - { - "name": "run_code", - "description": include_str!("mcp_docs/run_code.md"), - "inputSchema": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "Luau code to execute in Roblox Studio." - } - }, - "required": ["command"] - } - }, - { - "name": "insert_model", - "description": include_str!("mcp_docs/insert_model.md"), - "inputSchema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query for the model on the Roblox Creator Store." - } - }, - "required": ["query"] - } - }, - { - "name": "get_console_output", - "description": include_str!("mcp_docs/get_console_output.md"), - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "get_studio_mode", - "description": include_str!("mcp_docs/get_studio_mode.md"), - "inputSchema": { - "type": "object", - "properties": {} - } - }, - { - "name": "start_stop_play", - "description": include_str!("mcp_docs/start_stop_play.md"), - "inputSchema": { - "type": "object", - "properties": { - "mode": { - "type": "string", - "enum": ["start_play", "run_server", "stop"], - "description": "Play mode action." - } - }, - "required": ["mode"] - } - }, - { - "name": "run_script_in_play_mode", - "description": include_str!("mcp_docs/run_script_in_play_mode.md"), - "inputSchema": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "Luau code to run inside a play session." - }, - "timeout": { - "type": "integer", - "description": "Timeout in seconds. Defaults to 100." - }, - "mode": { - "type": "string", - "enum": ["start_play", "run_server"], - "description": "Play mode to start." - } - }, - "required": ["code", "mode"] - } - } - ] +fn tool_def(name: &str, description: &str) -> Value { + let settings = schemars::generate::SchemaSettings::draft07().with(|s| { + s.meta_schema = None; + s.inline_subschemas = true; }); + let schema = settings.into_generator().into_root_schema_for::(); + let mut schema_value = serde_json::to_value(schema).unwrap_or_default(); + if let Some(obj) = schema_value.as_object_mut() { + obj.remove("$schema"); + obj.remove("title"); + obj.remove("definitions"); + } + serde_json::json!({ + "name": name, + "description": description, + "inputSchema": schema_value, + }) +} + +fn handle_tools_list(id: Option) -> Response> { + let tools = vec![ + tool_def::("atlas_sync", include_str!("mcp_docs/atlas_sync.md")), + tool_def::("get_script", include_str!("mcp_docs/get_script.md")), + tool_def::("run_code", include_str!("mcp_docs/run_code.md")), + tool_def::("insert_model", include_str!("mcp_docs/insert_model.md")), + tool_def::( + "get_console_output", + include_str!("mcp_docs/get_console_output.md"), + ), + tool_def::( + "get_studio_mode", + include_str!("mcp_docs/get_studio_mode.md"), + ), + tool_def::( + "start_stop_play", + include_str!("mcp_docs/start_stop_play.md"), + ), + tool_def::( + "run_script_in_play_mode", + include_str!("mcp_docs/run_script_in_play_mode.md"), + ), + ]; + + let result = serde_json::json!({ "tools": tools }); let resp = JsonRpcResponse::success(id, result); json_response(&resp, StatusCode::OK) } @@ -2398,7 +2389,12 @@ mod tests { .unwrap(); let values: Vec<&str> = mode_enum.iter().map(|v| v.as_str().unwrap()).collect(); assert_eq!(values, vec!["start_play", "run_server"]); - assert!(tool["inputSchema"]["properties"]["timeout"]["type"] == "integer"); + let timeout = &tool["inputSchema"]["properties"]["timeout"]; + let has_integer = timeout["type"] == "integer" + || timeout["type"] + .as_array() + .map_or(false, |arr| arr.iter().any(|v| v == "integer")); + assert!(has_integer, "timeout should accept integer: {timeout}"); } } }