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.
This commit is contained in:
Joe 2026-03-01 12:05:41 -06:00
parent fd990b86c6
commit b5583c4c1b
4 changed files with 211 additions and 150 deletions

63
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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<String>,
}
// ---------------------------------------------------------------------------
// 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<SyncMode>,
/// Auto-accept directives for specific instance changes, matched
/// by id from a previous sync response.
overrides: Option<Vec<SyncOverride>>,
}
#[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<String>,
/// Atlas filesystem path relative to project root
/// (e.g. 'src/server/MyScript.server.luau'). Resolved server-side
/// to an instance id.
fs_path: Option<String>,
/// If true, read unsaved editor content instead of the saved Source
/// property. Default: false.
from_draft: Option<bool>,
}
#[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<u32>,
/// 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<Value>) -> Response<Full<Bytes>> {
json_response(&resp, StatusCode::OK)
}
fn handle_tools_list(id: Option<Value>) -> Response<Full<Bytes>> {
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<T: JsonSchema>(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::<T>();
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<Value>) -> Response<Full<Bytes>> {
let tools = vec![
tool_def::<AtlasSyncArgs>("atlas_sync", include_str!("mcp_docs/atlas_sync.md")),
tool_def::<GetScriptArgs>("get_script", include_str!("mcp_docs/get_script.md")),
tool_def::<RunCodeArgs>("run_code", include_str!("mcp_docs/run_code.md")),
tool_def::<InsertModelArgs>("insert_model", include_str!("mcp_docs/insert_model.md")),
tool_def::<NoArgs>(
"get_console_output",
include_str!("mcp_docs/get_console_output.md"),
),
tool_def::<NoArgs>(
"get_studio_mode",
include_str!("mcp_docs/get_studio_mode.md"),
),
tool_def::<StartStopPlayArgs>(
"start_stop_play",
include_str!("mcp_docs/start_stop_play.md"),
),
tool_def::<RunScriptInPlayModeArgs>(
"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}");
}
}
}