Expose the runtime block/item registry through compact material search and single-material descriptions. Preserve private block-entity data through checked edits and durable undo without sending payloads to model context. Include the completed terrain tools, isolated world/map plugin, station lift, ACP streaming and guidance fixes, local camera auto-connect, construction scripts, and their public documentation, references and verification records. Active station decoration and private runtime data remain outside this commit. Validation: 145 Maven tests, 47 Bridge tests, successful camera Gradle build, and isolated Paper verification of all 1,196 block defaults plus 5,392 independent property cases for placement, same-material edits and restoration.
96 lines
16 KiB
Markdown
96 lines
16 KiB
Markdown
# Prototype protocol
|
||
|
||
Version: 1. The exact implemented tool catalog is available through MCP `tools/list`, and the capabilities of the installed Paper plugin through `project_context`.
|
||
|
||
## Paper connection
|
||
|
||
The service listens on loopback only, at `127.0.0.1:8765` by default. `GET /health` returns status and version information without private data. `POST /v1/rpc` accepts JSON and an `Authorization: Bearer <token>` header.
|
||
|
||
Request: `{ "method": "project_context", "params": { "player_id": "UUID", "project_id": "default" }, "requestId": "correlation-id" }`.
|
||
|
||
Success: `{ "ok": true, "result": { ... } }`. Error: `{ "ok": false, "error": { "code": "...", "message": "..." } }`. An error may come with HTTP 400/403; clients must read the structured body. Tokens are never returned in results.
|
||
|
||
The administrator key is required for `chat_poll`, `chat_reply`, `recovery_review`, and `recovery_abandon`. The agent key permits world tools within the bound owner's project, but not administrative recovery. The MCP process injects `player_id/project_id` from its configuration; the model is not asked to supply them. The plugin rechecks the owner and their current permissions. An isolated test world can explicitly enable `allow-local-automation` with the `console` principal; administrative recovery operations also pass through this scope check.
|
||
|
||
## Material discovery
|
||
|
||
`project_context.material_catalog` contains `{version, materials, blocks, items, search_default_limit, search_max_limit}`. Block and item counts overlap for materials that have both forms. This replaces the expanded `supported_materials` list; no material registry or property list is sent in project context.
|
||
|
||
`material_search` and `material_describe` are read-only, idempotent MCP tools available within the same authorized project scope. `material_search` accepts optional `query` (at most 96 printable characters), `kind: "block" | "item" | "all"` (default `block`), `limit: 1..32` (default 16), and an opaque `cursor` of at most 100 characters. Search matches all whitespace-separated query tokens against material IDs, case-insensitively. It returns `{catalog_version, query, kind, total, results: [{id, block, item}], next_cursor?}`. A cursor belongs to its catalog version and normalized query/kind; changed filters or stale catalogs require a new search. Empty queries remain paginated.
|
||
|
||
`material_describe` accepts `{id: "minecraft:material_name"}` with a maximum ID length of 128 characters. It returns `{catalog_version, id, block, item, placeable, default_state?, properties?, behavior?}`. Block entries include the full default state and `properties: {name: [allowed_value, ...]}`; the response never enumerates the Cartesian product of all property combinations. Optional behavior hints identify features such as gravity, fluids, attachment, multiple parts or block-entity data. Item-only entries return `placeable: false` without block states. Direct RPC also accepts an ID without the `minecraft:` prefix; MCP requires the explicit namespace. Unknown IDs or unavailable property introspection fail explicitly.
|
||
|
||
Callers should search a focused family and describe only 1–3 selected materials, reusing those results while the catalog version remains unchanged. See [MATERIALS.md](MATERIALS.md) for the building workflow and behavior limits.
|
||
|
||
## Reading and writing
|
||
|
||
`region_inspect` requires `min/max` as `{x,y,z}` and `detail: "summary" | "blocks"`. Coordinates are inclusive integers. The prototype limit is 4096 positions. The response contains a palette with counts and, for `blocks`, exact `{pos, state, snapshot_id?}` entries. Block entities include a lowercase 64-character SHA-256 `snapshot_id` covering their captured data; NBT and inventory contents are not returned to the model. Data comes from loaded chunks without implicit world generation.
|
||
|
||
`build_prepare` accepts `recipe: {version: 1, operations: [...]}`, an optional `dependencies` array, and an optional `part_id` for an exact part mask. Geometry: `box(min,max,block,hollow?)`, `line(from,to,block)`, `cylinder(center,radius,height,block,hollow?)`, `repeat(count,offset,operations)`. This is a JSON description, not executable JavaScript.
|
||
|
||
Ordinary recipes accept registered vanilla block IDs and their valid properties as `minecraft:block[property=value,...]`, at most 1024 characters. Omitted properties use the runtime defaults; the server validates and canonicalizes the result. Item-only materials, unknown IDs/properties/values and raw NBT are rejected. Same-material state edits preserve existing block-entity data; a newly created block entity uses defaults. There is no inventory, sign-text or entity-data editor.
|
||
|
||
Callers planning from an earlier survey can supply `expected_blocks: [{pos: {x,y,z}, state: "minecraft:grass_block", snapshot_id?: "..."}, ...]`. When present, the array must cover every compiled desired position exactly once (maximum 4096). Copy each inspected `snapshot_id` unchanged: it is required for existing block entities, including those with default or empty data. The server checks these original states and digests in the same main-thread task that prepares the plan, returning `stale_snapshot` if any have changed; a required digest that was omitted is an invalid request. Without `expected_blocks`, preparation captures the current full state itself. `project_context.checked_expected_blocks` advertises support. This closes the caller-inspection-to-preparation gap; the usual comparison before applying still protects the prepared plan.
|
||
|
||
Preparation returns `plan_id`, `plan_hash`, `changed_blocks`, `region`, and `expires_at`. The full plan and original blocks stay in the journal. `build_apply` accepts this ID, hash, and an `idempotency_key` that remains constant for that logical call. Repeating the same call returns the same operation. A different plan requires a different key.
|
||
|
||
`operation_status(operation_id)` returns status, counters, and a bounded sample of conflicts. `operation_cancel` stops subsequent slices. `operation_undo_prepare` creates a reverse plan; it goes through the normal `build_apply` and checks. Losing the response to an apply request does not justify creating a different key and repeating the write: first check `project_context` or repeat the original call with the same key.
|
||
|
||
`part_define(name,operation_id)` registers only blocks actually written by a completed operation. `part_get(part_id)` returns the name, bounds, count, and protection settings. Bounds are not the mask: `build_prepare(part_id)` checks every block against the exact mask. Extensions are created as separate parts.
|
||
|
||
Before a write, the actual states of target blocks and declared dependencies are checked; the check runs again after the intent is persisted. A mismatch is never overwritten automatically. Undo also accounts for known subsequent writes by our operations, even when a block's value again matches the earlier result.
|
||
|
||
Multi-block structures require explicit states for every part: placing a door's lower half does not construct its upper half. Support and attachment constraints remain the caller's design responsibility. Fluids, gravity, random ticks, plant growth and redstone may cause later game changes. The journal and checked undo cover the direct operation's recorded writes, not every resulting simulation effect.
|
||
|
||
The plugin observes uncancelled `BlockPlaceEvent` and `BlockBreakEvent` events in the configured world. Such an event revokes the earlier operation's right to undo that block, including an edit followed by a change back to the original state (ABA). These notifications are held only in the current process's memory. After a restart, and for external paths without an observed event, current-content checks remain in place; a complete history of other plugins' actions is not promised.
|
||
|
||
A full server-side journal of external changes is not implemented yet. `region_changes` returns `resync_required`; the agent uses fresh, bounded reads. This is an explicit prototype limitation.
|
||
|
||
## Crash recovery
|
||
|
||
After a restart, an unfinished operation receives `recovery_required` and prevents new writes from starting. Unfinished slices are not replayed automatically. The administrative RPCs below are not exposed as agent MCP tools and do not roll back the world.
|
||
|
||
`recovery_review` accepts `operation_id`. Its result uses the Java record's field names: `operationId`, `planId`, `positions`, `matchesBefore`, `matchesAfter`, `foreignStates`, `currentDigest`, `sampledAtMillis`. The reviewed mask combines confirmed writes from earlier slices with potential writes from the unfinished slice; skipped positions that were not written are excluded. The counters describe how current values match `before/after`, while `currentDigest` is the SHA-256 digest of this snapshot. Matching content does not prove who made a change.
|
||
|
||
`recovery_abandon` accepts `operation_id` and `expected_digest`, taken from the latest review's `currentDigest`. The server rereads the mask and rejects the request if its contents have changed. If they match, it leaves all world blocks in place, changes the operation to `failed`, and persists the decision before returning success. The response has the usual `operation_status` shape.
|
||
|
||
Abandoning uncertain history permanently disables undo for that operation and preserves the invalidation of earlier undo ownership for blocks in the affected mask. New writes are permitted only after the decisions for all unfinished operations have been durably saved. A failure before persistence leaves recovery required; a lost response requires checking `operation_status`, not assuming that a rollback occurred. A stale or incorrect digest is returned as an `invalid_request` RPC error with a reason.
|
||
|
||
The `applied` status confirms the write and verification result observed by the running server. Chunk files and the journal do not form a shared transaction; automatic verification that all previously completed operations survived a crash is not implemented yet.
|
||
|
||
## Local schematics
|
||
|
||
The implemented RPC names are `asset_list`, `schematic_export`, and `schematic_import_prepare`. There are no separate `schematic_list` or immediate `schematic_import` routes. Check `project_context` for these capabilities.
|
||
|
||
`asset_list` accepts an optional `query` for case-insensitive name search and returns `{ "assets": [...] }`. Each entry contains `assetId`, `name`, `width`, `height`, `length`, `blockCount`, `dataVersion`, `offset`, `sha256`, and `bytes`. The catalog holds up to 64 files; every file is validated when read, so a corrupt schematic may cause the entire list request to fail.
|
||
|
||
`schematic_export` accepts `name`, inclusive `min/max`, and an optional `origin`. The name contains 1–64 printable characters. `origin` defines the schematic's anchor point and defaults to `min`. The server reads a dense rectangular region, including air, within the current project area and the `max_plan_blocks` limit (at most 4096). Registered block states are accepted. Block entities, including empty ones, and entities other than players cause rejection; their data is never silently discarded, and players are not written to the schematic. The result is one object with the same metadata fields as `asset_list`. The file remains in the `schematics` subdirectory of the plugin's data directory; the RPC returns neither its contents nor an arbitrary path.
|
||
|
||
`schematic_import_prepare` accepts `asset_id`, `target: {x,y,z}`, and an optional `rotation: 0 | 90 | 180 | 270` (default 0). Rotation is clockwise when viewed from above, around the `target` anchor; the saved `offset` is taken into account. Block-state orientation uses the runtime rotation behavior, including directional and axis properties. The result is the usual `plan_id/plan_hash/changed_blocks/region/expires_at`; writing requires a separate `build_apply`. Air in the schematic is part of the plan and can remove existing blocks. The area, original contents, surroundings, block policy, and conflicts are checked through the normal preparation and application path. State-only palette entries for block-entity block types use the same new-default or same-material-preservation rules as ordinary recipes.
|
||
|
||
A limited subset of Sponge Schematic v2 is supported: gzip and NBT with a palette of registered vanilla block states. Limits are 4096 positions, a 1 MiB compressed file, and 4 MiB of decompressed NBT. Entity and block-entity NBT payloads, biomes, unknown top-level fields, required mods, and other format versions are rejected. A `DataVersion` newer than the current server is not accepted; there is no DataFixer conversion. Full compatibility with every WorldEdit schematic is not claimed.
|
||
|
||
An external `.schem` can be placed in the local schematic directory in advance with a name matching `[A-Za-z0-9][A-Za-z0-9_-]{0,63}.schem`; its filename stem then serves as `asset_id`. Arbitrary paths, network URLs, and symbolic links are not accepted. The prototype does not support file uploads through RPC.
|
||
|
||
## Chat
|
||
|
||
`chat_poll` accepts a `client_id` that remains stable throughout the Bridge process's lifetime. The response is `messages` with `id`, `playerId`, `projectId`, `text`, `type`, and position and targeted-block information when available. `type` is either `prompt` or `cancel`. The administrator key is required.
|
||
|
||
`chat_reply` accepts `id`, `playerId`, `text`, `done`, and an optional `error`. The response is routed only to the initiator of the original request. When the Bridge process changes, unfinished requests are not replayed automatically: the plugin stops changes and asks the user to inspect the world. `/ai stop` pauses new writes regardless of whether the ACP turn has finished.
|
||
|
||
## Camera
|
||
|
||
`camera_capture` accepts `camera_id` or `pose: {x,y,z,yaw,pitch,fov?,width?,height?}`. The position refers to the observer's feet; the Worker returns eye coordinates separately. `after_operation_id` checks that the server operation has completed, but does not guarantee that the client has received all of its packets.
|
||
|
||
Paper serializes requests, moves the configured spectator observer, and sends a request to the local Camera Worker at `127.0.0.1:8766` with a separate key. A pending response contains `captureId`; poll by calling `camera_capture` with `capture_id`. A completed result contains `imageBase64` and `mimeType`, which the Bridge converts into MCP image content rather than a textual base64 string.
|
||
|
||
Captures include a heuristic assessment of chunk/frame readiness. `serverRevisionVerified: false` remains in place until strict client acknowledgement is implemented. A missing camera, a timeout, or failure to obtain a fresh frame never counts as visual success.
|
||
|
||
Worker details: [camera-mod/README.md](../camera-mod/README.md). Threading and journal: [world-core/README.md](../world-core/README.md). ACP and isolation: [bridge/README.md](../bridge/README.md).
|
||
|
||
## Terrain
|
||
|
||
`terrain_preview(recipe,resolution?)` renders and caches a bounded deterministic height-field recipe without reading or writing world blocks. It returns a native PNG through the Bridge, a content-addressed `terrain_id`, tile layout and sampled statistics. The cache holds 32 recipes until restart. `terrain_prepare(terrain_id,tile_index)` prepares one bounded tile against live natural terrain and returns either the usual immutable plan summary plus `tile_bounds`, or `status: empty` with zero changes. Existing `build_apply`, cancellation, conflict checks and checked undo apply unchanged. The terrain language, limits, preserve masks and local batch workflow are specified in [TERRAFORMING.md](TERRAFORMING.md). Preview output is explicitly marked `world_verified: false`.
|
||
|
||
`terrain_brush_prepare(brush)` prepares a relative edit of an existing live surface. Its bounded scan is stored as read dependencies (up to 4096), and the response combines a native before/after/delta image with a normal plan summary. `plan_state: empty` has no plan ID. Actions are raise, lower, flatten and smooth. Input, scan-window semantics and limitations are documented in [TERRAFORMING.md](TERRAFORMING.md#relative-brushes-on-existing-terrain).
|