{
  "openapi": "3.1.0",
  "info": {
    "title": "Phroller Companion API",
    "version": "1.0.0",
    "description": "A local loopback REST and WebSocket API that lets external software on the same computer read the open map and build on it in real time: stream overlays, Elgato Stream Deck macro pads, procedural map generators, custom scripts, and automation tools.\n\n### Key Concepts\n- **Desktop Only**: macOS and Windows.\n- **Off by Default**: Enabled in Phroller under **Settings → System → Companion API**.\n- **Loopback Only**: Listens strictly on `127.0.0.1` (default port `47321`).\n- **Live Changes**: Every edit is an ordinary VTT edit—visible immediately, synchronized to multiplayer peers, and registered in undo history.\n- **Coordinates**: Cell coordinates `[x, y]` indicate where tokens stand (1 cell = 60px). Point coordinates `[x, y]` indicate cell top-left corners for walls, doors, and decals.",
    "contact": {
      "name": "Phroller Support",
      "url": "https://phroller.com"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "http://127.0.0.1:47321",
      "description": "Default local loopback server"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    },
    {
      "TokenQuery": []
    }
  ],
  "tags": [
    { "name": "System", "description": "Server discovery, status, and health" },
    { "name": "Board", "description": "Read live map scene, levels, objects, and visual snapshots" },
    { "name": "Assets", "description": "Search online asset library, upload files, and import tokens/decals" },
    { "name": "Building", "description": "Tokens, decals, walls, doors, rooms, levels, and fog of war" },
    { "name": "Environment & Camera", "description": "Grid, lighting, background, scene control, and camera views" },
    { "name": "Model Context Protocol", "description": "Streamable HTTP MCP endpoint for AI agents" },
    { "name": "Events", "description": "Real-time WebSocket event subscription" }
  ],
  "paths": {
    "/v1": {
      "get": {
        "tags": ["System"],
        "summary": "Server Info",
        "description": "Returns basic information about the running Phroller Companion API server and available endpoints.",
        "responses": {
          "200": {
            "description": "Server information",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ServerInfoResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/commands": {
      "get": {
        "tags": ["System"],
        "summary": "Command Discovery & Schemas",
        "description": "Returns every available board command along with its full JSON Schema and write-permission requirements. Also used by MCP `tools/list`.",
        "responses": {
          "200": {
            "description": "List of command specifications",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CommandDiscoveryResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/board": {
      "get": {
        "tags": ["Board"],
        "summary": "Get Board Snapshot (GET)",
        "description": "Reads the open map: grid type, levels, active level, and per level every token, prop, decal, wall, and door with its id and cell/point coordinates.",
        "parameters": [
          {
            "name": "level",
            "in": "query",
            "description": "Optional level id or level name to filter by. Defaults to all or active level.",
            "required": false,
            "schema": { "type": "string" }
          },
          {
            "name": "region",
            "in": "query",
            "description": "Optional bounding rectangle `[x, y, width, height]` in points to filter objects.",
            "required": false,
            "schema": {
              "type": "array",
              "items": { "type": "number" },
              "minItems": 4,
              "maxItems": 4
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Board snapshot",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GetBoardResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" },
          "409": { "$ref": "#/components/responses/ConflictError" }
        }
      }
    },
    "/v1/screenshot": {
      "get": {
        "tags": ["Board"],
        "summary": "Capture Board Screenshot (GET)",
        "description": "Captures a live PNG image of the board exactly as displayed on the user's screen.",
        "parameters": [
          {
            "name": "maxSize",
            "in": "query",
            "description": "Longest edge in pixels (clamped between 256 and 2048, default 1024).",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 256,
              "maximum": 2048,
              "default": 1024
            }
          }
        ],
        "responses": {
          "200": {
            "description": "PNG image of the board",
            "content": {
              "image/png": {
                "schema": { "type": "string", "format": "binary" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" },
          "409": { "$ref": "#/components/responses/ConflictError" }
        }
      },
      "post": {
        "tags": ["Board"],
        "summary": "screenshot Command (POST)",
        "description": "Captures a PNG screenshot of the board.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "maxSize": {
                    "type": "integer",
                    "description": "Longest edge in pixels, 256–2048. Default 1024.",
                    "default": 1024
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PNG image",
            "content": {
              "image/png": {
                "schema": { "type": "string", "format": "binary" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/upload": {
      "post": {
        "tags": ["Assets"],
        "summary": "Upload Asset File",
        "description": "Upload raw file bytes directly to Phroller's local store. Returns an `uploadId` that can be passed to `import_asset`. Especially useful in sandboxed environments (such as Mac App Store builds).",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "description": "The file name including extension (e.g. `goblin.png`, `tavern.glb`).",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Raw binary payload of the asset (up to 256 MB).",
          "content": {
            "application/octet-stream": {
              "schema": { "type": "string", "format": "binary" }
            },
            "image/png": {
              "schema": { "type": "string", "format": "binary" }
            },
            "image/jpeg": {
              "schema": { "type": "string", "format": "binary" }
            },
            "model/gltf-binary": {
              "schema": { "type": "string", "format": "binary" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload received",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": { "type": "string", "example": "1" },
                    "ok": { "type": "boolean", "example": true },
                    "result": {
                      "type": "object",
                      "properties": {
                        "uploadId": { "type": "string", "example": "up_8f93a1c0d2" },
                        "bytes": { "type": "integer", "example": 245100 }
                      },
                      "required": ["uploadId", "bytes"]
                    }
                  },
                  "required": ["apiVersion", "ok", "result"]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequestError" },
          "401": { "$ref": "#/components/responses/UnauthorizedError" },
          "413": { "$ref": "#/components/responses/PayloadTooLargeError" }
        }
      }
    },
    "/v1/events": {
      "get": {
        "tags": ["Events"],
        "summary": "Real-time Event Stream (WebSocket)",
        "description": "Connect a WebSocket to `/v1/events` to subscribe to live board events. Send `?token=<key>` in the query string if the client cannot set an Authorization header.\n\nOn connection, the server sends `{\"type\": \"hello\", \"apiVersion\": \"1\"}`. Subsequent events include `dice.rolled`, `tokens.moved`, `tokens.changed`, `decals.changed`, `strokes.changed`, `fog.changed`, `levels.changed`, and `scene.switched`.",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": false,
            "description": "API authorization bearer key.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "101": {
            "description": "Switching Protocols to WebSocket"
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["Model Context Protocol"],
        "summary": "MCP Streamable HTTP Endpoint",
        "description": "Model Context Protocol (MCP) server endpoint for AI assistants (e.g. Gemini CLI, Claude Code CLI, Cursor, Claude Desktop). Supports JSON-RPC 2.0 requests including `tools/list` and `tools/call`. All board commands are directly exposed as MCP tools.",
        "requestBody": {
          "required": true,
          "description": "MCP JSON-RPC 2.0 message object.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "jsonrpc": { "type": "string", "example": "2.0" },
                  "id": { "oneOf": [{ "type": "string" }, { "type": "number" }] },
                  "method": { "type": "string", "example": "tools/call" },
                  "params": { "type": "object" }
                },
                "required": ["jsonrpc", "method"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "MCP JSON-RPC response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jsonrpc": { "type": "string", "example": "2.0" },
                    "id": { "oneOf": [{ "type": "string" }, { "type": "number" }] },
                    "result": { "type": "object" },
                    "error": { "type": "object" }
                  }
                }
              }
            }
          },
          "202": { "description": "Accepted (notifications with no reply required)" },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/get_board": {
      "post": {
        "tags": ["Board"],
        "summary": "get_board Command",
        "description": "Reads the scene, levels, and every token, prop, decal, wall, and door with its id and position.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": { "type": "string", "description": "Only this level (id or name)." },
                  "region": {
                    "type": "array",
                    "items": { "type": "number" },
                    "minItems": 4,
                    "maxItems": 4,
                    "description": "Only objects inside this rectangle `[x, y, width, height]` in points."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Board data",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/GetBoardResponse" } }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/list_assets": {
      "post": {
        "tags": ["Assets"],
        "summary": "list_assets Command",
        "description": "Lists local token, prop, and decal asset collections and their asset IDs.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "type": "object" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Asset collections",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StandardResponse"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/search_library": {
      "post": {
        "tags": ["Assets"],
        "summary": "search_library Command",
        "description": "Searches the free online library of 3D models and map images by query. Returns items with `url`, `author`, and `artistUrl`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": { "type": "string", "description": "Words to match." },
                  "kind": { "type": "string", "enum": ["model", "image", "any"], "default": "any" },
                  "limit": { "type": "integer", "default": 20, "maximum": 100 }
                },
                "required": ["query"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search results",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/import_asset": {
      "post": {
        "tags": ["Assets"],
        "summary": "import_asset Command",
        "description": "Imports an image (PNG, JPEG, WebP, GIF), 3D model (GLB, GLTF), or PDF into the app. Returns an `assetId`. Provide exactly one of `url`, `path`, or `uploadId`. Always preserve creator attribution via `artist` and `artistUrl`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": { "type": "string", "description": "HTTP(S) URL to download." },
                  "path": { "type": "string", "description": "Local absolute file path (not available in sandboxed builds)." },
                  "uploadId": { "type": "string", "description": "ID returned by POST /v1/upload." },
                  "kind": { "type": "string", "enum": ["image", "model", "pdf"], "description": "Inferred from file if omitted." },
                  "name": { "type": "string", "description": "Display name." },
                  "artist": { "type": "string", "description": "Creator name for Credits attribution." },
                  "artistUrl": { "type": "string", "description": "Creator link or portfolio URL." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": { "type": "string", "example": "1" },
                    "ok": { "type": "boolean", "example": true },
                    "result": {
                      "type": "object",
                      "properties": {
                        "assetId": { "type": "string", "example": "ast_9823412" },
                        "kind": { "type": "string", "example": "model" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequestError" },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/place_tokens": {
      "post": {
        "tags": ["Building"],
        "summary": "place_tokens Command",
        "description": "Places tokens and props on the board in grid cells. 3D models stand as full models; images become standing paper minis (`form: token`) or standing cards (`form: prop`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tokens": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "asset": { "type": "string", "description": "Asset ID." },
                        "cell": { "$ref": "#/components/schemas/Cell" },
                        "level": { "type": "string", "description": "Level id or name. Default: active level." },
                        "facing": { "type": "number", "description": "Degrees clockwise from north. Default 0.", "default": 0 },
                        "form": { "type": "string", "enum": ["token", "prop"], "default": "token" },
                        "label": { "type": "string", "description": "Label displayed on token." },
                        "size": { "type": "number", "description": "Width in cells for an image. Default 1.", "default": 1 }
                      },
                      "required": ["asset", "cell"]
                    }
                  }
                },
                "required": ["tokens"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens placed successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": { "type": "string", "example": "1" },
                    "ok": { "type": "boolean", "example": true },
                    "result": {
                      "type": "object",
                      "properties": {
                        "tokenIds": {
                          "type": "array",
                          "items": { "type": "string" },
                          "example": ["tok_d8174f", "tok_c9281a"]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/place_decals": {
      "post": {
        "tags": ["Building"],
        "summary": "place_decals Command",
        "description": "Lays images, PDF pages, or text flat on the board—such as battlemap art, rugs, traps, signs, or handouts.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decals": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "asset": { "type": "string", "description": "Asset ID of image or PDF." },
                        "text": { "type": "string", "description": "Text to render instead of an asset." },
                        "rect": { "$ref": "#/components/schemas/Rect", "description": "Where it lies in points [x, y, width, height]." },
                        "rotation": { "type": "number", "description": "Degrees clockwise.", "default": 0 },
                        "level": { "type": "string", "description": "Level id or name. Default: active level." },
                        "aboveGrid": { "type": "boolean", "description": "Draw over grid lines (default true). Set false for map tiles where grid lines should overlay.", "default": true },
                        "locked": { "type": "boolean", "description": "Prevent accidental dragging.", "default": false },
                        "opacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 },
                        "page": { "type": "integer", "description": "PDF page number (starts at 1).", "default": 1 },
                        "textColor": { "$ref": "#/components/schemas/Color" },
                        "backgroundColor": { "$ref": "#/components/schemas/Color" },
                        "fontSize": { "type": "number", "default": 64 },
                        "bold": { "type": "boolean", "default": false },
                        "italic": { "type": "boolean", "default": false },
                        "label": { "type": "string", "description": "Name or tag for the decal." }
                      },
                      "required": ["rect"]
                    }
                  }
                },
                "required": ["decals"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Decals placed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/draw_walls": {
      "post": {
        "tags": ["Building"],
        "summary": "draw_walls Command",
        "description": "Draws walls, doors, and rooms along cell corner points. Height is in cells (0 = flat line on floor, 2 = standard wall).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": { "type": "string", "description": "Level id or name. Default: active level." },
                  "walls": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "points": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/Point" },
                          "minItems": 2,
                          "description": "Two or more point coordinates."
                        },
                        "closed": { "type": "boolean", "description": "Connect last point back to first.", "default": false },
                        "height": { "type": "number", "description": "Height in cells (0-10, default 2).", "default": 2 },
                        "color": { "$ref": "#/components/schemas/Color" },
                        "width": { "type": "number", "description": "Line thickness in pixels (default 6).", "default": 6 }
                      },
                      "required": ["points"]
                    }
                  },
                  "doors": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "from": { "$ref": "#/components/schemas/Point", "description": "Hinge point." },
                        "to": { "$ref": "#/components/schemas/Point", "description": "Latch point." },
                        "height": { "type": "number", "default": 2 },
                        "color": { "$ref": "#/components/schemas/Color" },
                        "open": { "type": "number", "description": "Degrees swung open (0 = shut).", "default": 0 }
                      },
                      "required": ["from", "to"]
                    }
                  },
                  "rooms": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "rect": { "$ref": "#/components/schemas/Rect", "description": "Room bounds in points." },
                        "height": { "type": "number", "default": 2 },
                        "color": { "$ref": "#/components/schemas/Color" },
                        "doors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "side": { "type": "string", "enum": ["north", "east", "south", "west"] },
                              "at": { "type": "number", "description": "Offset along side from north/west end in cells." },
                              "width": { "type": "number", "default": 1 }
                            },
                            "required": ["side", "at"]
                          }
                        }
                      },
                      "required": ["rect"]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Walls and doors drawn", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/update_objects": {
      "post": {
        "tags": ["Building"],
        "summary": "update_objects Command",
        "description": "Updates or deletes existing tokens, decals, and doors on the board by their ID.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "updates": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "description": "Token instance ID, decal ID, or wall/door ID." },
                        "delete": { "type": "boolean", "description": "Remove the object if true.", "default": false },
                        "cell": { "$ref": "#/components/schemas/Cell" },
                        "facing": { "type": "number", "description": "Degrees clockwise from north." },
                        "label": { "type": "string" },
                        "level": { "type": "string", "description": "Move object to target level ID or name." },
                        "rect": { "$ref": "#/components/schemas/Rect" },
                        "rotation": { "type": "number", "description": "Degrees rotation." },
                        "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
                        "locked": { "type": "boolean" },
                        "open": { "type": "number", "description": "Door swing angle in degrees." }
                      },
                      "required": ["id"]
                    }
                  }
                },
                "required": ["updates"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Objects updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/set_levels": {
      "post": {
        "tags": ["Building"],
        "summary": "set_levels Command",
        "description": "Creates, updates, or selects active vertical levels (floors). Elevation is measured in cells above ground level.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "add": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": { "type": "string" },
                        "elevation": { "type": "number", "description": "Cells above ground (default: 3 above highest level)." },
                        "gmOnly": { "type": "boolean", "description": "Hide floor from players.", "default": false }
                      },
                      "required": ["name"]
                    }
                  },
                  "update": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "level": { "type": "string", "description": "Level id or name." },
                        "name": { "type": "string" },
                        "elevation": { "type": "number" },
                        "gmOnly": { "type": "boolean" }
                      },
                      "required": ["level"]
                    }
                  },
                  "active": { "type": "string", "description": "Level ID or name to make active." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Levels configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/set_fog": {
      "post": {
        "tags": ["Building"],
        "summary": "set_fog Command",
        "description": "Manages Fog of War coverage on a level. Completely covers or clears the map, then reveals or hides specific rectangles in points.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": { "type": "string", "description": "Level id or name (default: active level)." },
                  "covered": { "type": "boolean", "description": "true covers entire level; false clears it completely." },
                  "reveal": {
                    "type": "array",
                    "items": { "$ref": "#/components/schemas/Rect" },
                    "description": "Rectangles in points to uncover."
                  },
                  "hide": {
                    "type": "array",
                    "items": { "$ref": "#/components/schemas/Rect" },
                    "description": "Rectangles in points to obscure."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Fog state updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/set_environment": {
      "post": {
        "tags": ["Environment & Camera"],
        "summary": "set_environment Command",
        "description": "Configures the map's grid type, grid lines, background styling, horizon appearance, and sunlight angle.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "gridType": { "type": "string", "enum": ["square", "hex", "none"] },
                  "gridStyle": { "type": "string", "enum": ["line", "point"] },
                  "lineStyle": { "type": "string", "enum": ["solid", "dashed", "dotted"] },
                  "gridColor": { "$ref": "#/components/schemas/Color" },
                  "background": { "type": "string", "enum": ["solid", "radial_gradient", "starry"] },
                  "backgroundColor": { "$ref": "#/components/schemas/Color" },
                  "horizon": { "type": "string", "enum": ["match_board", "solid", "radial_gradient", "starry"] },
                  "horizonColor": { "$ref": "#/components/schemas/Color" },
                  "sunAzimuth": { "type": "number", "description": "Sun compass bearing in degrees (0–360)." },
                  "sunElevation": { "type": "number", "description": "Sun height angle in degrees (7–90)." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Environment updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/set_camera": {
      "post": {
        "tags": ["Environment & Camera"],
        "summary": "set_camera Command",
        "description": "Positions the camera on the local screen. Does not affect multiplayer peers' cameras.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "center": { "$ref": "#/components/schemas/Cell", "description": "Cell coordinate to center camera on." },
                  "zoom": { "type": "number", "minimum": 0.2, "maximum": 5.0, "description": "Zoom factor (0.2–5.0)." },
                  "yaw": { "type": "number", "description": "Camera orbit rotation in degrees." },
                  "pitch": { "type": "number", "minimum": 0, "maximum": 85, "description": "Tilt angle in degrees (0 = top-down straight down, up to 85)." }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Camera updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/new_scene": {
      "post": {
        "tags": ["Environment & Camera"],
        "summary": "new_scene Command",
        "description": "Opens a new empty map scene in its own tab and activates it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string", "description": "Name for the new scene." }
                },
                "required": ["name"]
              }
            }
          }
        },
        "responses": {
          "200": { "description": "New scene created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    },
    "/v1/undo_last": {
      "post": {
        "tags": ["Building"],
        "summary": "undo_last Command",
        "description": "Reverts the most recent change initiated through the Companion API without disturbing user manual edits.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "type": "object" }
            }
          }
        },
        "responses": {
          "200": { "description": "Action reverted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StandardResponse" } } } },
          "401": { "$ref": "#/components/responses/UnauthorizedError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Bearer key obtained from Phroller **Settings → System → Companion API**."
      },
      "TokenQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "token",
        "description": "Alternative query parameter token for browser sources (e.g. `?token=<key>`)."
      }
    },
    "schemas": {
      "Cell": {
        "type": "array",
        "items": { "type": "number" },
        "minItems": 2,
        "maxItems": 2,
        "description": "Grid cell coordinate `[x, y]`. 1 cell = 60 world pixels.",
        "example": [12, 8]
      },
      "Point": {
        "type": "array",
        "items": { "type": "number" },
        "minItems": 2,
        "maxItems": 2,
        "description": "Cell corner point `[x, y]` for walls, doors, and bounding rectangles.",
        "example": [12, 8]
      },
      "Rect": {
        "type": "array",
        "items": { "type": "number" },
        "minItems": 4,
        "maxItems": 4,
        "description": "Rectangle `[x, y, width, height]` in point coordinates.",
        "example": [0, 0, 10, 8]
      },
      "Color": {
        "type": "string",
        "description": "Hex color string `#RRGGBB` or `#AARRGGBB`.",
        "example": "#7C3AED"
      },
      "StandardResponse": {
        "type": "object",
        "properties": {
          "apiVersion": { "type": "string", "example": "1" },
          "ok": { "type": "boolean", "example": true },
          "result": { "type": "object" }
        },
        "required": ["apiVersion", "ok", "result"]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "apiVersion": { "type": "string", "example": "1" },
          "ok": { "type": "boolean", "example": false },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_arguments",
                  "unauthorized",
                  "read_only",
                  "not_gm",
                  "limit_reached",
                  "not_found",
                  "unknown_command",
                  "no_board",
                  "busy",
                  "too_large",
                  "forbidden_host",
                  "forbidden_origin"
                ],
                "example": "unauthorized"
              },
              "message": { "type": "string", "example": "Send the key from Settings as 'Authorization: Bearer <key>'." }
            },
            "required": ["code", "message"]
          }
        },
        "required": ["apiVersion", "ok", "error"]
      },
      "ServerInfoResponse": {
        "type": "object",
        "properties": {
          "app": { "type": "string", "example": "Phroller" },
          "version": { "type": "string", "example": "1.2.0" },
          "mcp": { "type": "string", "example": "/mcp" },
          "events": { "type": "string", "example": "/v1/events" },
          "commands": { "type": "string", "example": "/v1/commands" }
        },
        "required": ["app", "version", "mcp", "events", "commands"]
      },
      "CommandDiscoveryResponse": {
        "type": "object",
        "properties": {
          "commands": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": { "type": "string", "example": "draw_walls" },
                "description": { "type": "string" },
                "writes": { "type": "boolean", "example": true },
                "inputSchema": { "type": "object" }
              },
              "required": ["name", "description", "writes", "inputSchema"]
            }
          }
        },
        "required": ["commands"]
      },
      "GetBoardResponse": {
        "type": "object",
        "properties": {
          "apiVersion": { "type": "string", "example": "1" },
          "ok": { "type": "boolean", "example": true },
          "result": {
            "type": "object",
            "properties": {
              "scene": { "type": "string", "example": "Dungeon Entrance" },
              "grid": { "type": "string", "example": "square" },
              "activeLevel": { "type": "string", "example": "base" },
              "levels": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "example": "base" },
                    "name": { "type": "string", "example": "Ground Floor" },
                    "elevation": { "type": "number", "example": 0 },
                    "gmOnly": { "type": "boolean", "example": false }
                  }
                }
              },
              "tokens": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "example": "tok_123" },
                    "kind": { "type": "string", "enum": ["token", "prop"], "example": "token" },
                    "label": { "type": "string", "example": "Valeros" },
                    "cell": { "$ref": "#/components/schemas/Cell" },
                    "level": { "type": "string", "example": "base" },
                    "art": { "type": "string", "example": "fighter.glb" },
                    "locked": { "type": "boolean" }
                  }
                }
              },
              "decals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "example": "dec_456" },
                    "kind": { "type": "string", "enum": ["image", "text", "pdf"], "example": "image" },
                    "rect": { "$ref": "#/components/schemas/Rect" },
                    "level": { "type": "string", "example": "base" },
                    "art": { "type": "string", "example": "stone_tiles.png" },
                    "aboveGrid": { "type": "boolean", "example": false }
                  }
                }
              },
              "walls": { "type": "array", "items": { "type": "object" } },
              "doors": { "type": "array", "items": { "type": "object" } }
            },
            "required": ["scene", "grid", "activeLevel", "levels", "tokens", "decals"]
          }
        },
        "required": ["apiVersion", "ok", "result"]
      }
    },
    "responses": {
      "UnauthorizedError": {
        "description": "Missing or invalid authorization token",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "BadRequestError": {
        "description": "Malformed arguments or missing required fields",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "ConflictError": {
        "description": "Board is busy, loading, or no map is currently open",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      },
      "PayloadTooLargeError": {
        "description": "Uploaded file or payload exceeds the size limit (256 MB)",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ErrorResponse" }
          }
        }
      }
    }
  }
}
