feat(mcp): 增加游戏内方块调试桥接

新增 stdio MCP server 和 mcp 子命令,通过调试会话状态文件连接内置 TCP bridge,并提供 place_block 工具。

Bridge 将请求投递到服务端 System 的游戏线程执行,限制请求大小与队列长度,并按会话端口安全清理状态文件和连接。
This commit is contained in:
2026-07-21 21:04:45 +08:00
parent 94b1b9a1f5
commit 1a7bc6aca1
15 changed files with 1375 additions and 11 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
.claude/
.qoder/
.mcdev.json
.emod-cli/

518
Cargo.lock generated
View File

@@ -28,6 +28,15 @@ dependencies = [
"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]]
name = "anstream"
version = "0.6.18"
@@ -86,6 +95,23 @@ dependencies = [
"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]]
name = "base64"
version = "0.22.1"
@@ -113,6 +139,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "bzip2"
version = "0.4.4"
@@ -151,6 +183,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "cipher"
version = "0.4.4"
@@ -213,6 +259,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.16"
@@ -262,6 +314,40 @@ dependencies = [
"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]]
name = "deflate64"
version = "0.1.9"
@@ -310,6 +396,12 @@ dependencies = [
"syn",
]
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "emod-cli"
version = "0.1.0-dev"
@@ -318,8 +410,10 @@ dependencies = [
"clap",
"rand",
"regex",
"rmcp",
"serde",
"serde_json",
"tokio",
"toml",
"uuid",
"walkdir",
@@ -343,6 +437,94 @@ dependencies = [
"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]]
name = "generic-array"
version = "0.14.7"
@@ -385,6 +567,36 @@ dependencies = [
"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]]
name = "indexmap"
version = "2.7.0"
@@ -425,6 +637,17 @@ dependencies = [
"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]]
name = "libc"
version = "0.2.169"
@@ -474,12 +697,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "pastey"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@@ -490,6 +728,12 @@ dependencies = [
"hmac",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.31"
@@ -559,6 +803,26 @@ dependencies = [
"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]]
name = "regex"
version = "1.12.2"
@@ -588,6 +852,47 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "ryu"
version = "1.0.18"
@@ -603,6 +908,32 @@ dependencies = [
"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]]
name = "serde"
version = "1.0.217"
@@ -623,6 +954,17 @@ dependencies = [
"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]]
name = "serde_json"
version = "1.0.134"
@@ -667,6 +1009,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -729,6 +1077,41 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "toml"
version = "0.8.23"
@@ -770,6 +1153,37 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "typenum"
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"
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]]
name = "winapi-util"
version = "0.1.9"
@@ -841,6 +1300,65 @@ dependencies = [
"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]]
name = "windows-sys"
version = "0.59.0"

View File

@@ -12,6 +12,8 @@ clap = { version = "4.5.32", features = ["derive"] }
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
serde = { version = "1.0", features = ["derive"] }
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"
toml = "0.8"
zip = "2.2.2"

View File

@@ -54,7 +54,25 @@ emod-cli debug --new # 创建全新调试存档
2. 清理运行时旧包链接
3. 注册调试 MOD 并链接用户 MOD 目录
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

View File

@@ -19,3 +19,10 @@ def GET_DEBUG_IPC_PORT():
if port is None:
return None
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

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
from . import IPCSystem, Lifecycle
from . import IPCSystem, Lifecycle, MCPBridge
ServerSystem = serverApi.GetServerSystemCls()
@@ -16,11 +16,16 @@ class DebugServerSystem(ServerSystem):
if not self._initialized:
Lifecycle.activate(self)
IPCSystem.ON_SERVER_INIT()
MCPBridge.ON_SERVER_INIT()
self._initialized = True
IPCSystem.UPDATE_SERVER()
MCPBridge.UPDATE()
return ServerSystem.Update(self)
def Destroy(self):
try:
MCPBridge.ON_SERVER_EXIT()
finally:
try:
IPCSystem.ON_SERVER_EXIT()
finally:

View 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()

View File

@@ -2,3 +2,4 @@
studio.json
.mcs/editorSave.json
.mcs/images/
.emod-cli/

15
src/commands/mcp.rs Normal file
View 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);
}
}

View File

@@ -6,6 +6,7 @@ pub mod create;
pub mod debug;
pub mod fork_boss_engine;
pub mod init;
pub mod mcp;
pub mod release;
#[derive(Parser)]
@@ -34,6 +35,8 @@ pub enum Commands {
Components(ComponentsArgs),
/// Launch NetEase Minecraft with debug MOD, IPC logging, and hot reload
Debug(DebugArgs),
/// Run stdio MCP server for the current debug session
Mcp(McpArgs),
/// Convert Blockbench .bbmodel to NetEase block geometry
Bbmodel(BbmodelArgs),
/// Fork AbyssBossEngine into a new boss mod project
@@ -122,6 +125,13 @@ pub struct DebugArgs {
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)]
pub struct ForkBossEngineArgs {
/// Path to the AbyssBossEngine source project.

56
src/debug/mcp.rs Normal file
View 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)
}

View File

@@ -5,6 +5,7 @@ pub mod hotreload;
pub mod ipc;
pub mod level;
pub mod log;
pub mod mcp;
pub mod nbt;
pub mod process;
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)?;
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();
if !config::env_is_subprocess_mode() {
@@ -45,5 +51,10 @@ pub fn run(project_dir: &Path, new_world: bool) -> Result<()> {
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(),
)
}

View File

@@ -40,6 +40,7 @@ pub fn launch_game(
config: &DebugConfig,
config_arg: Option<&Path>,
mod_dirs: &[ResolvedModDir],
mcp_state_path: Option<&Path>,
) -> Result<()> {
let enable_ipc = config.auto_hot_reload_mods;
let ipc = if enable_ipc {
@@ -52,9 +53,10 @@ pub fn launch_game(
let command = build_command(config, config_arg);
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(
ipc.as_ref().map(|server| server.port()),
mcp_state_path,
))
} else {
None
@@ -154,6 +156,7 @@ pub fn launch_game(
_config: &DebugConfig,
_config_arg: Option<&Path>,
_mod_dirs: &[ResolvedModDir],
_mcp_state_path: Option<&Path>,
) -> Result<()> {
Err(CliError::InvalidInput(
"debug launch is only supported on Windows".to_string(),
@@ -230,11 +233,12 @@ where
}
#[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()
.filter(|(key, _)| {
let key = key.to_string_lossy();
!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()))
.collect();
@@ -245,6 +249,13 @@ fn build_environment_block(ipc_port: Option<u16>) -> Vec<u16> {
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));
let mut block = Vec::new();

View File

@@ -3,6 +3,7 @@ mod commands;
mod debug;
mod entity;
mod error;
mod mcp;
mod template;
mod utils;
@@ -17,6 +18,7 @@ fn main() {
Commands::Init(args) => commands::init::execute(args),
Commands::Components(args) => commands::components::execute(args),
Commands::Debug(args) => commands::debug::execute(args),
Commands::Mcp(args) => commands::mcp::execute(args),
Commands::Bbmodel(args) => commands::bbmodel::execute(args),
Commands::ForkBossEngine(args) => commands::fork_boss_engine::execute(args),
}

269
src/mcp.rs Normal file
View 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(&params)?;
match call_bridge(&self.project_dir, &params) {
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 &params.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)
}