feat(mcp): 增加游戏内方块调试桥接
新增 stdio MCP server 和 mcp 子命令,通过调试会话状态文件连接内置 TCP bridge,并提供 place_block 工具。 Bridge 将请求投递到服务端 System 的游戏线程执行,限制请求大小与队列长度,并按会话端口安全清理状态文件和连接。
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.claude/
|
.claude/
|
||||||
.qoder/
|
.qoder/
|
||||||
.mcdev.json
|
.mcdev.json
|
||||||
|
.emod-cli/
|
||||||
518
Cargo.lock
generated
518
Cargo.lock
generated
@@ -28,6 +28,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@@ -86,6 +95,23 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -113,6 +139,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bzip2"
|
name = "bzip2"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -151,6 +183,20 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cipher"
|
name = "cipher"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -213,6 +259,12 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -262,6 +314,40 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||||
|
dependencies = [
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deflate64"
|
name = "deflate64"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -310,6 +396,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "emod-cli"
|
name = "emod-cli"
|
||||||
version = "0.1.0-dev"
|
version = "0.1.0-dev"
|
||||||
@@ -318,8 +410,10 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rmcp",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -343,6 +437,94 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-executor"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-macro"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@@ -385,6 +567,36 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@@ -425,6 +637,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.169"
|
version = "0.2.169"
|
||||||
@@ -474,12 +697,27 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.20.2"
|
version = "1.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pastey"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
@@ -490,6 +728,12 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
@@ -559,6 +803,26 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.2"
|
||||||
@@ -588,6 +852,47 @@ version = "0.8.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmcp"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"pastey",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rmcp-macros",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmcp-macros"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90c23c8f26cae4da838fbc3eadfaecf2d549d97c04b558e7bd90526a9c28b42a"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_json",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -603,6 +908,32 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"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",
|
||||||
|
"quote",
|
||||||
|
"serde_derive_internals",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.217"
|
version = "1.0.217"
|
||||||
@@ -623,6 +954,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive_internals"
|
||||||
|
version = "0.29.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.134"
|
version = "1.0.134"
|
||||||
@@ -667,6 +1009,12 @@ version = "0.3.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -729,6 +1077,41 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.23"
|
version = "0.8.23"
|
||||||
@@ -770,6 +1153,37 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -832,6 +1246,51 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.123"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -841,6 +1300,65 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ clap = { version = "4.5.32", features = ["derive"] }
|
|||||||
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
rmcp = { version = "0.16.0", features = ["server", "macros", "transport-io"] }
|
||||||
|
tokio = { version = "1", features = ["macros", "rt", "io-std", "time"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
zip = "2.2.2"
|
zip = "2.2.2"
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -54,7 +54,25 @@ emod-cli debug --new # 创建全新调试存档
|
|||||||
2. 清理运行时旧包链接
|
2. 清理运行时旧包链接
|
||||||
3. 注册调试 MOD 并链接用户 MOD 目录
|
3. 注册调试 MOD 并链接用户 MOD 目录
|
||||||
4. 准备开发世界(含自动加入游戏配置)
|
4. 准备开发世界(含自动加入游戏配置)
|
||||||
5. 启动游戏进程,挂载 IPC 日志与热重载
|
5. 启动游戏进程;启用内置调试 MOD 时挂载 IPC 日志、热重载与 debug MCP bridge
|
||||||
|
|
||||||
|
### mcp
|
||||||
|
|
||||||
|
启动标准 stdio MCP server,让 AI 连接正在运行的 `debug` 会话并在游戏内放置方块。`mcp` 命令不会启动游戏;必须先为同一项目启动启用了内置调试 MOD 的 `debug`。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 终端 1:启动游戏并注入 debug MCP bridge
|
||||||
|
emod-cli debug --path <项目路径>
|
||||||
|
|
||||||
|
# MCP 客户端配置:启动 stdio MCP server
|
||||||
|
emod-cli mcp --path <项目路径>
|
||||||
|
```
|
||||||
|
|
||||||
|
工具示例:
|
||||||
|
|
||||||
|
```text
|
||||||
|
place_block {"x":0,"y":5,"z":0,"name":"minecraft:stone"}
|
||||||
|
```
|
||||||
|
|
||||||
### release
|
### release
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,11 @@ def GET_DEBUG_IPC_PORT():
|
|||||||
port = os.getenv("MCDEV_DEBUG_IPC_PORT")
|
port = os.getenv("MCDEV_DEBUG_IPC_PORT")
|
||||||
if port is None:
|
if port is None:
|
||||||
return None
|
return None
|
||||||
return int(port)
|
return int(port)
|
||||||
|
|
||||||
|
def GET_MCP_BRIDGE_STATE_PATH():
|
||||||
|
import os
|
||||||
|
path = os.getenv("MCDEV_MCP_BRIDGE_STATE")
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
return path
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import mod.server.extraServerApi as serverApi
|
import mod.server.extraServerApi as serverApi
|
||||||
|
|
||||||
from . import IPCSystem, Lifecycle
|
from . import IPCSystem, Lifecycle, MCPBridge
|
||||||
|
|
||||||
|
|
||||||
ServerSystem = serverApi.GetServerSystemCls()
|
ServerSystem = serverApi.GetServerSystemCls()
|
||||||
@@ -16,13 +16,18 @@ class DebugServerSystem(ServerSystem):
|
|||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
Lifecycle.activate(self)
|
Lifecycle.activate(self)
|
||||||
IPCSystem.ON_SERVER_INIT()
|
IPCSystem.ON_SERVER_INIT()
|
||||||
|
MCPBridge.ON_SERVER_INIT()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
IPCSystem.UPDATE_SERVER()
|
IPCSystem.UPDATE_SERVER()
|
||||||
|
MCPBridge.UPDATE()
|
||||||
return ServerSystem.Update(self)
|
return ServerSystem.Update(self)
|
||||||
|
|
||||||
def Destroy(self):
|
def Destroy(self):
|
||||||
try:
|
try:
|
||||||
IPCSystem.ON_SERVER_EXIT()
|
MCPBridge.ON_SERVER_EXIT()
|
||||||
finally:
|
finally:
|
||||||
self._initialized = False
|
try:
|
||||||
Lifecycle.deactivate(self)
|
IPCSystem.ON_SERVER_EXIT()
|
||||||
|
finally:
|
||||||
|
self._initialized = False
|
||||||
|
Lifecycle.deactivate(self)
|
||||||
|
|||||||
438
debug_mod/DEBUG_ENV_SCRIPT/MCPBridge.py
Normal file
438
debug_mod/DEBUG_ENV_SCRIPT/MCPBridge.py
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import mod.server.extraServerApi as serverApi
|
||||||
|
|
||||||
|
from .Config import GET_MCP_BRIDGE_STATE_PATH
|
||||||
|
|
||||||
|
try:
|
||||||
|
basestring
|
||||||
|
except NameError:
|
||||||
|
basestring = str
|
||||||
|
|
||||||
|
try:
|
||||||
|
integer_types = (int, long)
|
||||||
|
except NameError:
|
||||||
|
integer_types = (int,)
|
||||||
|
|
||||||
|
_BRIDGE = None
|
||||||
|
_SERVER_ACTIVE = False
|
||||||
|
_SERVER_TASKS = []
|
||||||
|
_SERVER_TASKS_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
MAX_PENDING_TASKS = 32
|
||||||
|
MAX_REQUEST_BYTES = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
class BridgeError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _is_int(value):
|
||||||
|
return isinstance(value, integer_types) and not isinstance(value, bool)
|
||||||
|
|
||||||
|
|
||||||
|
def _event_is_set(event):
|
||||||
|
if hasattr(event, "is_set"):
|
||||||
|
return event.is_set()
|
||||||
|
return event.isSet()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_daemon_thread(target, args=()):
|
||||||
|
thread = threading.Thread(target=target, args=args)
|
||||||
|
try:
|
||||||
|
thread.daemon = True
|
||||||
|
except Exception:
|
||||||
|
thread.setDaemon(True)
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
def _to_text(data):
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
return data.decode("utf-8")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _send_json_line(sock, payload):
|
||||||
|
data = json.dumps(payload, separators=(",", ":")) + "\n"
|
||||||
|
if not isinstance(data, bytes):
|
||||||
|
data = data.encode("utf-8")
|
||||||
|
sock.sendall(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_params(params):
|
||||||
|
if not isinstance(params, dict):
|
||||||
|
raise BridgeError("place_block params must be an object")
|
||||||
|
|
||||||
|
for key in ("x", "y", "z"):
|
||||||
|
if key not in params or not _is_int(params.get(key)):
|
||||||
|
raise BridgeError("place_block.%s must be an integer" % key)
|
||||||
|
|
||||||
|
name = params.get("name")
|
||||||
|
if not isinstance(name, basestring) or not name.strip() or ":" not in name:
|
||||||
|
raise BridgeError("place_block.name must be a non-empty namespaced block id")
|
||||||
|
|
||||||
|
dimension = params.get("dimension", 0)
|
||||||
|
if not _is_int(dimension):
|
||||||
|
raise BridgeError("place_block.dimension must be an integer")
|
||||||
|
|
||||||
|
old_block_handling = params.get("old_block_handling", 0)
|
||||||
|
if not _is_int(old_block_handling) or old_block_handling not in (0, 1, 2):
|
||||||
|
raise BridgeError("place_block.old_block_handling must be 0, 1, or 2")
|
||||||
|
|
||||||
|
aux = params.get("aux", None)
|
||||||
|
states = params.get("states", None)
|
||||||
|
if aux is not None and states is not None:
|
||||||
|
raise BridgeError("place_block.aux and place_block.states cannot both be provided")
|
||||||
|
if aux is not None and not _is_int(aux):
|
||||||
|
raise BridgeError("place_block.aux must be an integer")
|
||||||
|
if states is not None and not isinstance(states, dict):
|
||||||
|
raise BridgeError("place_block.states must be an object")
|
||||||
|
|
||||||
|
is_legacy = params.get("is_legacy", True)
|
||||||
|
if not isinstance(is_legacy, bool):
|
||||||
|
raise BridgeError("place_block.is_legacy must be a boolean")
|
||||||
|
|
||||||
|
update_neighbors = params.get("update_neighbors", True)
|
||||||
|
if not isinstance(update_neighbors, bool):
|
||||||
|
raise BridgeError("place_block.update_neighbors must be a boolean")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"x": params["x"],
|
||||||
|
"y": params["y"],
|
||||||
|
"z": params["z"],
|
||||||
|
"name": name,
|
||||||
|
"dimension": dimension,
|
||||||
|
"aux": aux,
|
||||||
|
"states": states,
|
||||||
|
"old_block_handling": old_block_handling,
|
||||||
|
"is_legacy": is_legacy,
|
||||||
|
"update_neighbors": update_neighbors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _place_block_on_server(request):
|
||||||
|
name = request["name"]
|
||||||
|
aux = request["aux"]
|
||||||
|
states = request["states"]
|
||||||
|
if states is not None:
|
||||||
|
block_state = serverApi.GetEngineCompFactory().CreateBlockState(serverApi.GetLevelId())
|
||||||
|
aux = block_state.GetBlockAuxValueFromStates(name, states)
|
||||||
|
if aux == -1:
|
||||||
|
raise BridgeError("invalid block states for %s" % name)
|
||||||
|
elif aux is None:
|
||||||
|
aux = 0
|
||||||
|
|
||||||
|
x = request["x"]
|
||||||
|
y = request["y"]
|
||||||
|
z = request["z"]
|
||||||
|
dimension = request["dimension"]
|
||||||
|
old_block_handling = request["old_block_handling"]
|
||||||
|
is_legacy = request["is_legacy"]
|
||||||
|
update_neighbors = request["update_neighbors"]
|
||||||
|
|
||||||
|
block_info = serverApi.GetEngineCompFactory().CreateBlockInfo(serverApi.GetLevelId())
|
||||||
|
changed = block_info.SetBlockNew(
|
||||||
|
(x, y, z),
|
||||||
|
{"name": name, "aux": aux},
|
||||||
|
old_block_handling,
|
||||||
|
dimension,
|
||||||
|
is_legacy,
|
||||||
|
update_neighbors,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"changed": bool(changed),
|
||||||
|
"position": [x, y, z],
|
||||||
|
"dimension": dimension,
|
||||||
|
"block": {"name": name, "aux": aux},
|
||||||
|
"old_block_handling": old_block_handling,
|
||||||
|
"is_legacy": is_legacy,
|
||||||
|
"update_neighbors": update_neighbors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _place_block(params):
|
||||||
|
request = _require_params(params)
|
||||||
|
task = {
|
||||||
|
"request": request,
|
||||||
|
"done": threading.Event(),
|
||||||
|
"cancelled": False,
|
||||||
|
"started": False,
|
||||||
|
}
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
if not _SERVER_ACTIVE:
|
||||||
|
raise BridgeError("server system is not ready")
|
||||||
|
if len(_SERVER_TASKS) >= MAX_PENDING_TASKS:
|
||||||
|
raise BridgeError("server request queue is full")
|
||||||
|
_SERVER_TASKS.append(task)
|
||||||
|
|
||||||
|
task["done"].wait(5.0)
|
||||||
|
if not _event_is_set(task["done"]):
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
if task["started"]:
|
||||||
|
started = True
|
||||||
|
else:
|
||||||
|
started = False
|
||||||
|
task["cancelled"] = True
|
||||||
|
if task in _SERVER_TASKS:
|
||||||
|
_SERVER_TASKS.remove(task)
|
||||||
|
if started:
|
||||||
|
task["done"].wait()
|
||||||
|
else:
|
||||||
|
raise BridgeError("place_block timed out waiting for server thread")
|
||||||
|
if "error" in task:
|
||||||
|
raise BridgeError(task["error"])
|
||||||
|
return task["result"]
|
||||||
|
|
||||||
|
|
||||||
|
def UPDATE():
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
tasks = list(_SERVER_TASKS)
|
||||||
|
del _SERVER_TASKS[:]
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
if task["cancelled"]:
|
||||||
|
task["done"].set()
|
||||||
|
continue
|
||||||
|
task["started"] = True
|
||||||
|
try:
|
||||||
|
task["result"] = _place_block_on_server(task["request"])
|
||||||
|
except BridgeError as err:
|
||||||
|
task["error"] = str(err)
|
||||||
|
except Exception as err:
|
||||||
|
traceback.print_exc()
|
||||||
|
task["error"] = str(err)
|
||||||
|
finally:
|
||||||
|
task["done"].set()
|
||||||
|
|
||||||
|
|
||||||
|
def _cancel_server_tasks():
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
tasks = list(_SERVER_TASKS)
|
||||||
|
del _SERVER_TASKS[:]
|
||||||
|
for task in tasks:
|
||||||
|
task["cancelled"] = True
|
||||||
|
task["error"] = "server stopped before executing request"
|
||||||
|
task["done"].set()
|
||||||
|
|
||||||
|
|
||||||
|
class MCPBridge(object):
|
||||||
|
def __init__(self, state_path):
|
||||||
|
self.state_path = state_path
|
||||||
|
self.sock = None
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.running = False
|
||||||
|
self.connections = set()
|
||||||
|
self.accept_thread = None
|
||||||
|
self.client_threads = set()
|
||||||
|
self.port = None
|
||||||
|
self.tmp_path = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
with self.lock:
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(("127.0.0.1", 0))
|
||||||
|
sock.listen(4)
|
||||||
|
sock.settimeout(0.2)
|
||||||
|
self.sock = sock
|
||||||
|
self.running = True
|
||||||
|
port = sock.getsockname()[1]
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
self._write_state(port)
|
||||||
|
print("[MCPBridge] Listening on 127.0.0.1:%d" % port)
|
||||||
|
thread = _create_daemon_thread(self._accept_loop)
|
||||||
|
with self.lock:
|
||||||
|
self.accept_thread = thread
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
with self.lock:
|
||||||
|
sock = self.sock
|
||||||
|
self.sock = None
|
||||||
|
self.running = False
|
||||||
|
connections = list(self.connections)
|
||||||
|
self.connections.clear()
|
||||||
|
accept_thread = self.accept_thread
|
||||||
|
self.accept_thread = None
|
||||||
|
client_threads = list(self.client_threads)
|
||||||
|
self.client_threads.clear()
|
||||||
|
if sock:
|
||||||
|
try:
|
||||||
|
sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for conn in connections:
|
||||||
|
try:
|
||||||
|
conn.shutdown(socket.SHUT_RDWR)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if accept_thread and accept_thread.is_alive():
|
||||||
|
accept_thread.join(1.0)
|
||||||
|
for thread in client_threads:
|
||||||
|
if thread.is_alive():
|
||||||
|
thread.join(1.0)
|
||||||
|
self._delete_state()
|
||||||
|
|
||||||
|
def _write_state(self, port):
|
||||||
|
state_dir = os.path.dirname(self.state_path)
|
||||||
|
if state_dir and not os.path.isdir(state_dir):
|
||||||
|
os.makedirs(state_dir)
|
||||||
|
tmp_path = "%s.%d.tmp" % (self.state_path, port)
|
||||||
|
self.tmp_path = tmp_path
|
||||||
|
data = json.dumps(
|
||||||
|
{"version": 1, "host": "127.0.0.1", "port": port},
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
with open(tmp_path, "w") as fp:
|
||||||
|
fp.write(data)
|
||||||
|
os.rename(tmp_path, self.state_path)
|
||||||
|
self.tmp_path = None
|
||||||
|
|
||||||
|
def _delete_state(self):
|
||||||
|
try:
|
||||||
|
if self.tmp_path and os.path.exists(self.tmp_path):
|
||||||
|
os.remove(self.tmp_path)
|
||||||
|
self.tmp_path = None
|
||||||
|
if not os.path.exists(self.state_path) or self.port is None:
|
||||||
|
return
|
||||||
|
with open(self.state_path, "r") as fp:
|
||||||
|
state = json.load(fp)
|
||||||
|
if state.get("host") == "127.0.0.1" and state.get("port") == self.port:
|
||||||
|
os.remove(self.state_path)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
self.port = None
|
||||||
|
|
||||||
|
def _accept_loop(self):
|
||||||
|
while True:
|
||||||
|
with self.lock:
|
||||||
|
sock = self.sock
|
||||||
|
running = self.running
|
||||||
|
if not running or sock is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
conn, _addr = sock.accept()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except socket.error:
|
||||||
|
with self.lock:
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
with self.lock:
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
with self.lock:
|
||||||
|
if not self.running:
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
self.connections.add(conn)
|
||||||
|
thread = _create_daemon_thread(self._handle_client, (conn,))
|
||||||
|
self.client_threads.add(thread)
|
||||||
|
conn.settimeout(0.2)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _handle_client(self, conn):
|
||||||
|
try:
|
||||||
|
pending = b""
|
||||||
|
while True:
|
||||||
|
with self.lock:
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
chunk = conn.recv(4096)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
if not chunk:
|
||||||
|
return
|
||||||
|
pending += chunk
|
||||||
|
if len(pending) > MAX_REQUEST_BYTES:
|
||||||
|
raise BridgeError("request line exceeds maximum size")
|
||||||
|
while b"\n" in pending:
|
||||||
|
raw_line, pending = pending.split(b"\n", 1)
|
||||||
|
line = _to_text(raw_line).strip()
|
||||||
|
if line:
|
||||||
|
self._handle_line(conn, line)
|
||||||
|
except Exception:
|
||||||
|
with self.lock:
|
||||||
|
if self.running:
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
with self.lock:
|
||||||
|
self.connections.discard(conn)
|
||||||
|
self.client_threads.discard(threading.currentThread())
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_line(self, conn, line):
|
||||||
|
request_id = None
|
||||||
|
try:
|
||||||
|
request = json.loads(line)
|
||||||
|
if not isinstance(request, dict):
|
||||||
|
raise BridgeError("request must be an object")
|
||||||
|
request_id = request.get("id")
|
||||||
|
method = request.get("method")
|
||||||
|
if method != "place_block":
|
||||||
|
raise BridgeError("unknown MCP bridge method: %s" % method)
|
||||||
|
result = _place_block(request.get("params", {}))
|
||||||
|
_send_json_line(conn, {"id": request_id, "ok": True, "result": result})
|
||||||
|
except BridgeError as err:
|
||||||
|
_send_json_line(conn, {"id": request_id, "ok": False, "error": str(err)})
|
||||||
|
except Exception as err:
|
||||||
|
traceback.print_exc()
|
||||||
|
_send_json_line(conn, {"id": request_id, "ok": False, "error": str(err)})
|
||||||
|
|
||||||
|
|
||||||
|
def ON_SERVER_INIT():
|
||||||
|
global _BRIDGE, _SERVER_ACTIVE
|
||||||
|
state_path = GET_MCP_BRIDGE_STATE_PATH()
|
||||||
|
if not state_path:
|
||||||
|
return
|
||||||
|
if _BRIDGE:
|
||||||
|
return
|
||||||
|
bridge = MCPBridge(state_path)
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
_SERVER_ACTIVE = True
|
||||||
|
try:
|
||||||
|
bridge.start()
|
||||||
|
except Exception:
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
_SERVER_ACTIVE = False
|
||||||
|
bridge.close()
|
||||||
|
raise
|
||||||
|
_BRIDGE = bridge
|
||||||
|
|
||||||
|
|
||||||
|
def ON_SERVER_EXIT():
|
||||||
|
global _BRIDGE, _SERVER_ACTIVE
|
||||||
|
bridge = _BRIDGE
|
||||||
|
_BRIDGE = None
|
||||||
|
with _SERVER_TASKS_LOCK:
|
||||||
|
_SERVER_ACTIVE = False
|
||||||
|
_cancel_server_tasks()
|
||||||
|
if bridge:
|
||||||
|
bridge.close()
|
||||||
3
examples/default/.gitignore
vendored
3
examples/default/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.idea/
|
.idea/
|
||||||
studio.json
|
studio.json
|
||||||
.mcs/editorSave.json
|
.mcs/editorSave.json
|
||||||
.mcs/images/
|
.mcs/images/
|
||||||
|
.emod-cli/
|
||||||
15
src/commands/mcp.rs
Normal file
15
src/commands/mcp.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::commands::McpArgs;
|
||||||
|
|
||||||
|
pub fn execute(args: &McpArgs) {
|
||||||
|
let project_dir = args
|
||||||
|
.path
|
||||||
|
.as_deref()
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
|
||||||
|
if let Err(err) = crate::mcp::run_blocking(project_dir) {
|
||||||
|
eprintln!("Error: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ pub mod create;
|
|||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod fork_boss_engine;
|
pub mod fork_boss_engine;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
|
pub mod mcp;
|
||||||
pub mod release;
|
pub mod release;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -34,6 +35,8 @@ pub enum Commands {
|
|||||||
Components(ComponentsArgs),
|
Components(ComponentsArgs),
|
||||||
/// Launch NetEase Minecraft with debug MOD, IPC logging, and hot reload
|
/// Launch NetEase Minecraft with debug MOD, IPC logging, and hot reload
|
||||||
Debug(DebugArgs),
|
Debug(DebugArgs),
|
||||||
|
/// Run stdio MCP server for the current debug session
|
||||||
|
Mcp(McpArgs),
|
||||||
/// Convert Blockbench .bbmodel to NetEase block geometry
|
/// Convert Blockbench .bbmodel to NetEase block geometry
|
||||||
Bbmodel(BbmodelArgs),
|
Bbmodel(BbmodelArgs),
|
||||||
/// Fork AbyssBossEngine into a new boss mod project
|
/// Fork AbyssBossEngine into a new boss mod project
|
||||||
@@ -122,6 +125,13 @@ pub struct DebugArgs {
|
|||||||
pub new_world: bool,
|
pub new_world: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct McpArgs {
|
||||||
|
/// The path of the project (default: current directory)
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct ForkBossEngineArgs {
|
pub struct ForkBossEngineArgs {
|
||||||
/// Path to the AbyssBossEngine source project.
|
/// Path to the AbyssBossEngine source project.
|
||||||
|
|||||||
56
src/debug/mcp.rs
Normal file
56
src/debug/mcp.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::{CliError, Result};
|
||||||
|
|
||||||
|
pub const MCP_STATE_DIR: &str = ".emod-cli";
|
||||||
|
pub const MCP_STATE_FILE: &str = "debug-mcp.json";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct BridgeState {
|
||||||
|
pub version: u8,
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_file(project_dir: &Path) -> PathBuf {
|
||||||
|
project_dir.join(MCP_STATE_DIR).join(MCP_STATE_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_state_file(project_dir: &Path) -> Result<PathBuf> {
|
||||||
|
let state_dir = project_dir.join(MCP_STATE_DIR);
|
||||||
|
fs::create_dir_all(&state_dir)?;
|
||||||
|
|
||||||
|
let path = state_dir.join(MCP_STATE_FILE);
|
||||||
|
match fs::remove_file(&path) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => return Err(CliError::Io(err)),
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_state(project_dir: &Path) -> Result<BridgeState> {
|
||||||
|
let path = state_file(project_dir);
|
||||||
|
let data = fs::read_to_string(path)?;
|
||||||
|
let state: BridgeState = serde_json::from_str(&data)?;
|
||||||
|
if state.version != 1 {
|
||||||
|
return Err(CliError::InvalidData(format!(
|
||||||
|
"unsupported debug MCP bridge state version {}",
|
||||||
|
state.version
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if state.host.trim().is_empty() {
|
||||||
|
return Err(CliError::InvalidData(
|
||||||
|
"debug MCP bridge state host is empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if state.port == 0 {
|
||||||
|
return Err(CliError::InvalidData(
|
||||||
|
"debug MCP bridge state port is zero".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ pub mod hotreload;
|
|||||||
pub mod ipc;
|
pub mod ipc;
|
||||||
pub mod level;
|
pub mod level;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
pub mod mcp;
|
||||||
pub mod nbt;
|
pub mod nbt;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod win;
|
pub mod win;
|
||||||
@@ -24,6 +25,11 @@ pub fn run(project_dir: &Path, new_world: bool) -> Result<()> {
|
|||||||
config::ensure_game_executable(project_dir, &mut config)?;
|
config::ensure_game_executable(project_dir, &mut config)?;
|
||||||
|
|
||||||
let mod_dirs = config.included_mod_dirs(project_dir)?;
|
let mod_dirs = config.included_mod_dirs(project_dir)?;
|
||||||
|
let mcp_state_path = if config.include_debug_mod && !config::env_is_subprocess_mode() {
|
||||||
|
Some(mcp::prepare_state_file(project_dir)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let mut linked_packs = Vec::new();
|
let mut linked_packs = Vec::new();
|
||||||
|
|
||||||
if !config::env_is_subprocess_mode() {
|
if !config::env_is_subprocess_mode() {
|
||||||
@@ -45,5 +51,10 @@ pub fn run(project_dir: &Path, new_world: bool) -> Result<()> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
process::launch_game(&config, config_arg.as_deref(), &mod_dirs)
|
process::launch_game(
|
||||||
|
&config,
|
||||||
|
config_arg.as_deref(),
|
||||||
|
&mod_dirs,
|
||||||
|
mcp_state_path.as_deref(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub fn launch_game(
|
|||||||
config: &DebugConfig,
|
config: &DebugConfig,
|
||||||
config_arg: Option<&Path>,
|
config_arg: Option<&Path>,
|
||||||
mod_dirs: &[ResolvedModDir],
|
mod_dirs: &[ResolvedModDir],
|
||||||
|
mcp_state_path: Option<&Path>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let enable_ipc = config.auto_hot_reload_mods;
|
let enable_ipc = config.auto_hot_reload_mods;
|
||||||
let ipc = if enable_ipc {
|
let ipc = if enable_ipc {
|
||||||
@@ -52,9 +53,10 @@ pub fn launch_game(
|
|||||||
|
|
||||||
let command = build_command(config, config_arg);
|
let command = build_command(config, config_arg);
|
||||||
let mut command_w = wide_null(OsStr::new(&command));
|
let mut command_w = wide_null(OsStr::new(&command));
|
||||||
let env_block = if ipc.is_some() || config.auto_hot_reload_ui {
|
let env_block = if ipc.is_some() || mcp_state_path.is_some() || config.auto_hot_reload_ui {
|
||||||
Some(build_environment_block(
|
Some(build_environment_block(
|
||||||
ipc.as_ref().map(|server| server.port()),
|
ipc.as_ref().map(|server| server.port()),
|
||||||
|
mcp_state_path,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -154,6 +156,7 @@ pub fn launch_game(
|
|||||||
_config: &DebugConfig,
|
_config: &DebugConfig,
|
||||||
_config_arg: Option<&Path>,
|
_config_arg: Option<&Path>,
|
||||||
_mod_dirs: &[ResolvedModDir],
|
_mod_dirs: &[ResolvedModDir],
|
||||||
|
_mcp_state_path: Option<&Path>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
Err(CliError::InvalidInput(
|
Err(CliError::InvalidInput(
|
||||||
"debug launch is only supported on Windows".to_string(),
|
"debug launch is only supported on Windows".to_string(),
|
||||||
@@ -230,11 +233,12 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn build_environment_block(ipc_port: Option<u16>) -> Vec<u16> {
|
fn build_environment_block(ipc_port: Option<u16>, mcp_state_path: Option<&Path>) -> Vec<u16> {
|
||||||
let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = std::env::vars_os()
|
let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = std::env::vars_os()
|
||||||
.filter(|(key, _)| {
|
.filter(|(key, _)| {
|
||||||
let key = key.to_string_lossy();
|
let key = key.to_string_lossy();
|
||||||
!key.eq_ignore_ascii_case("MCDEV_DEBUG_IPC_PORT")
|
!key.eq_ignore_ascii_case("MCDEV_DEBUG_IPC_PORT")
|
||||||
|
&& !key.eq_ignore_ascii_case("MCDEV_MCP_BRIDGE_STATE")
|
||||||
})
|
})
|
||||||
.map(|(key, value)| (key.encode_wide().collect(), value.encode_wide().collect()))
|
.map(|(key, value)| (key.encode_wide().collect(), value.encode_wide().collect()))
|
||||||
.collect();
|
.collect();
|
||||||
@@ -245,6 +249,13 @@ fn build_environment_block(ipc_port: Option<u16>) -> Vec<u16> {
|
|||||||
OsStr::new(&port.to_string()).encode_wide().collect(),
|
OsStr::new(&port.to_string()).encode_wide().collect(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let Some(path) = mcp_state_path {
|
||||||
|
let state_path = path.to_string_lossy().replace('\\', "/");
|
||||||
|
pairs.push((
|
||||||
|
OsStr::new("MCDEV_MCP_BRIDGE_STATE").encode_wide().collect(),
|
||||||
|
OsStr::new(&state_path).encode_wide().collect(),
|
||||||
|
));
|
||||||
|
}
|
||||||
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
let mut block = Vec::new();
|
let mut block = Vec::new();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mod commands;
|
|||||||
mod debug;
|
mod debug;
|
||||||
mod entity;
|
mod entity;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod mcp;
|
||||||
mod template;
|
mod template;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ fn main() {
|
|||||||
Commands::Init(args) => commands::init::execute(args),
|
Commands::Init(args) => commands::init::execute(args),
|
||||||
Commands::Components(args) => commands::components::execute(args),
|
Commands::Components(args) => commands::components::execute(args),
|
||||||
Commands::Debug(args) => commands::debug::execute(args),
|
Commands::Debug(args) => commands::debug::execute(args),
|
||||||
|
Commands::Mcp(args) => commands::mcp::execute(args),
|
||||||
Commands::Bbmodel(args) => commands::bbmodel::execute(args),
|
Commands::Bbmodel(args) => commands::bbmodel::execute(args),
|
||||||
Commands::ForkBossEngine(args) => commands::fork_boss_engine::execute(args),
|
Commands::ForkBossEngine(args) => commands::fork_boss_engine::execute(args),
|
||||||
}
|
}
|
||||||
|
|||||||
269
src/mcp.rs
Normal file
269
src/mcp.rs
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader, ErrorKind, Write},
|
||||||
|
net::{TcpStream, ToSocketAddrs},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rmcp::{
|
||||||
|
ErrorData as McpError, ServerHandler, ServiceExt,
|
||||||
|
handler::server::{tool::ToolRouter, wrapper::Parameters},
|
||||||
|
model::{CallToolResult, Content, ServerCapabilities, ServerInfo},
|
||||||
|
schemars::{self, JsonSchema},
|
||||||
|
tool, tool_handler, tool_router,
|
||||||
|
transport::stdio,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value, json};
|
||||||
|
|
||||||
|
use crate::{debug::mcp as debug_mcp, error::CliError};
|
||||||
|
|
||||||
|
const BRIDGE_NOT_READY: &str = "debug MCP bridge is not ready; start emod-cli debug --path <project> and wait for [MCPBridge] Listening";
|
||||||
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
const IO_TIMEOUT: Duration = Duration::from_secs(6);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DebugMcpServer {
|
||||||
|
project_dir: PathBuf,
|
||||||
|
tool_router: ToolRouter<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub struct PlaceBlockParams {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub z: i32,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dimension: Option<i32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub aux: Option<i32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub states: Option<Map<String, Value>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub old_block_handling: Option<i32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_legacy: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub update_neighbors: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum BridgeCallError {
|
||||||
|
NotReady,
|
||||||
|
Message(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct BridgeRequest {
|
||||||
|
id: u64,
|
||||||
|
method: &'static str,
|
||||||
|
params: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BridgeResponse {
|
||||||
|
ok: bool,
|
||||||
|
result: Option<Value>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_router]
|
||||||
|
impl DebugMcpServer {
|
||||||
|
fn new(project_dir: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
project_dir,
|
||||||
|
tool_router: Self::tool_router(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool(
|
||||||
|
description = "Place a block in the currently running emod-cli debug world through the injected debug MCP bridge"
|
||||||
|
)]
|
||||||
|
async fn place_block(
|
||||||
|
&self,
|
||||||
|
Parameters(params): Parameters<PlaceBlockParams>,
|
||||||
|
) -> Result<CallToolResult, McpError> {
|
||||||
|
validate_place_block_params(¶ms)?;
|
||||||
|
|
||||||
|
match call_bridge(&self.project_dir, ¶ms) {
|
||||||
|
Ok(result) => {
|
||||||
|
let text = serde_json::to_string_pretty(&result).map_err(|err| {
|
||||||
|
McpError::internal_error(format!("failed to encode bridge result: {err}"), None)
|
||||||
|
})?;
|
||||||
|
Ok(CallToolResult::success(vec![Content::text(text)]))
|
||||||
|
}
|
||||||
|
Err(BridgeCallError::NotReady) => Ok(tool_error(BRIDGE_NOT_READY)),
|
||||||
|
Err(BridgeCallError::Message(message)) => Ok(tool_error(message)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tool_handler]
|
||||||
|
impl ServerHandler for DebugMcpServer {
|
||||||
|
fn get_info(&self) -> ServerInfo {
|
||||||
|
ServerInfo {
|
||||||
|
instructions: Some(
|
||||||
|
"Place blocks in a running emod-cli debug world through the injected debug MCP bridge."
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_blocking(project_dir: PathBuf) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
runtime.block_on(async move {
|
||||||
|
let service = DebugMcpServer::new(project_dir).serve(stdio()).await?;
|
||||||
|
service.waiting().await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_place_block_params(params: &PlaceBlockParams) -> Result<(), McpError> {
|
||||||
|
if params.name.trim().is_empty() || !params.name.contains(':') {
|
||||||
|
return Err(McpError::invalid_params(
|
||||||
|
"place_block.name must be a non-empty namespaced block id",
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if params.aux.is_some() && params.states.is_some() {
|
||||||
|
return Err(McpError::invalid_params(
|
||||||
|
"place_block.aux and place_block.states cannot both be provided",
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(old_block_handling) = params.old_block_handling {
|
||||||
|
if !(0..=2).contains(&old_block_handling) {
|
||||||
|
return Err(McpError::invalid_params(
|
||||||
|
"place_block.old_block_handling must be 0, 1, or 2",
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool_error(message: impl Into<String>) -> CallToolResult {
|
||||||
|
CallToolResult::error(vec![Content::text(message.into())])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn call_bridge(project_dir: &Path, params: &PlaceBlockParams) -> Result<Value, BridgeCallError> {
|
||||||
|
let state = read_bridge_state(project_dir)?;
|
||||||
|
if state.host != "127.0.0.1" && state.host != "localhost" {
|
||||||
|
return Err(BridgeCallError::Message(
|
||||||
|
CliError::InvalidData(format!(
|
||||||
|
"debug MCP bridge host is not local: {}",
|
||||||
|
state.host
|
||||||
|
))
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut addrs = (state.host.as_str(), state.port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to resolve debug MCP bridge: {err}"))
|
||||||
|
})?;
|
||||||
|
let addr = addrs.next().ok_or_else(|| {
|
||||||
|
BridgeCallError::Message("debug MCP bridge address did not resolve".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut stream = TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT).map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to connect debug MCP bridge: {err}"))
|
||||||
|
})?;
|
||||||
|
stream.set_read_timeout(Some(IO_TIMEOUT)).map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to set bridge read timeout: {err}"))
|
||||||
|
})?;
|
||||||
|
stream.set_write_timeout(Some(IO_TIMEOUT)).map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to set bridge write timeout: {err}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let request = BridgeRequest {
|
||||||
|
id: 1,
|
||||||
|
method: "place_block",
|
||||||
|
params: bridge_params(params),
|
||||||
|
};
|
||||||
|
serde_json::to_writer(&mut stream, &request).map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to encode bridge request: {err}"))
|
||||||
|
})?;
|
||||||
|
stream.write_all(b"\n").map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to write bridge request: {err}"))
|
||||||
|
})?;
|
||||||
|
stream.flush().map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to flush bridge request: {err}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(stream);
|
||||||
|
let mut line = String::new();
|
||||||
|
let bytes = reader.read_line(&mut line).map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to read bridge response: {err}"))
|
||||||
|
})?;
|
||||||
|
if bytes == 0 {
|
||||||
|
return Err(BridgeCallError::Message(
|
||||||
|
"debug MCP bridge closed connection without response".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: BridgeResponse = serde_json::from_str(line.trim()).map_err(|err| {
|
||||||
|
BridgeCallError::Message(format!("failed to decode bridge response: {err}"))
|
||||||
|
})?;
|
||||||
|
if !response.ok {
|
||||||
|
return Err(BridgeCallError::Message(response.error.unwrap_or_else(
|
||||||
|
|| "debug MCP bridge returned an error".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
response.result.ok_or_else(|| {
|
||||||
|
BridgeCallError::Message("debug MCP bridge response is missing result".to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bridge_state(project_dir: &Path) -> Result<debug_mcp::BridgeState, BridgeCallError> {
|
||||||
|
match debug_mcp::read_state(project_dir) {
|
||||||
|
Ok(state) => Ok(state),
|
||||||
|
Err(CliError::Io(err)) if err.kind() == ErrorKind::NotFound => {
|
||||||
|
Err(BridgeCallError::NotReady)
|
||||||
|
}
|
||||||
|
Err(err) => Err(BridgeCallError::Message(err.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bridge_params(params: &PlaceBlockParams) -> Value {
|
||||||
|
let mut map = Map::new();
|
||||||
|
map.insert("x".to_string(), json!(params.x));
|
||||||
|
map.insert("y".to_string(), json!(params.y));
|
||||||
|
map.insert("z".to_string(), json!(params.z));
|
||||||
|
map.insert(
|
||||||
|
"dimension".to_string(),
|
||||||
|
json!(params.dimension.unwrap_or(0)),
|
||||||
|
);
|
||||||
|
map.insert("name".to_string(), json!(params.name));
|
||||||
|
match ¶ms.states {
|
||||||
|
Some(states) => {
|
||||||
|
map.insert("aux".to_string(), Value::Null);
|
||||||
|
map.insert("states".to_string(), Value::Object(states.clone()));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
map.insert("aux".to_string(), json!(params.aux.unwrap_or(0)));
|
||||||
|
map.insert("states".to_string(), Value::Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map.insert(
|
||||||
|
"old_block_handling".to_string(),
|
||||||
|
json!(params.old_block_handling.unwrap_or(0)),
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"is_legacy".to_string(),
|
||||||
|
json!(params.is_legacy.unwrap_or(true)),
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"update_neighbors".to_string(),
|
||||||
|
json!(params.update_neighbors.unwrap_or(true)),
|
||||||
|
);
|
||||||
|
Value::Object(map)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user