{
  "openapi": "3.1.0",
  "info": {
    "title": "Storevisor API",
    "version": "1.0.0",
    "description": "The Storevisor tools over REST: POST /api/tools/<tool> with the tool's arguments as a JSON object. The same tools, wording and rules as the MCP server at https://storevisor.devino.ca/mcp. Every write answers a preview first; commit it by repeating the call with apply: true and the confirmToken from the preview (single use, 10 minutes). Guide: https://storevisor.devino.ca/docs/api. Error codes: https://storevisor.devino.ca/docs/errors."
  },
  "servers": [
    {
      "url": "https://storevisor.devino.ca"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "Read",
      "description": "Reads the workspace's data or a store. Never changes anything."
    },
    {
      "name": "Write",
      "description": "Previewed first; changes a store (or the workspace's own settings) only when confirmed."
    }
  ],
  "externalDocs": {
    "url": "https://storevisor.devino.ca/docs/api",
    "description": "API reference"
  },
  "paths": {
    "/api/tools/apple_listing_get": {
      "post": {
        "operationId": "apple_listing_get",
        "summary": "Apple: read the live App Store listing",
        "description": "Reads the CURRENT App Store listing for an app straight from App Store Connect (not the warehouse cache): name, subtitle, keywords, description, promotional text, what's new, URLs, and a screenshot-set summary per locale. `locales` is the MERGED view — what the NEXT RELEASE will ship: when the app has an editable appInfo / App Store version (PREPARE_FOR_SUBMISSION, *_REJECTED) its values win, which is where an apple_listing_update write lands. `live_locales` lists the values customers see right now, for every locale where the two differ. Apps with several platforms (e.g. iOS + macOS) resolve to the iOS version unless `platform` names another — each platform's version has its own description, keywords and what's new.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_listing_get_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: read the live App Store listing",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_listing_update": {
      "post": {
        "operationId": "apple_listing_update",
        "summary": "Apple: update App Store listing metadata (gated)",
        "description": "Proposes a listing metadata change. ALWAYS snapshots the current listing first and records the change in listing_changes. In preview it returns the diff + validation and sends NOTHING to Apple. name/subtitle/keywords/description/whatsNew can only change on an EDITABLE App Store version; if none exists the call is rejected with an explanation — this tool NEVER creates a version. promotionalText is the one field editable on a live version. Version fields belong to ONE platform's version: pass `platform` (MAC_OS, …) to write a non-iOS listing; app-info fields (name, subtitle) are shared.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_listing_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: update App Store listing metadata (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_cpp_list": {
      "post": {
        "operationId": "apple_cpp_list",
        "summary": "Apple: list custom product pages",
        "description": "Lists App Store custom product pages (CPPs) for an app, with their versions and states.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_cpp_list_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: list custom product pages",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_cpp_create": {
      "post": {
        "operationId": "apple_cpp_create",
        "summary": "Apple: create a custom product page (gated)",
        "description": "Creates a custom product page with one localization (promotional text only — screenshots are out of scope in this version). Follows the same write contract: audit row first, and in preview it returns the exact API plan without calling Apple.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_cpp_create_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: create a custom product page (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_screenshots_get": {
      "post": {
        "operationId": "apple_screenshots_get",
        "summary": "Apple: read App Store screenshot sets",
        "description": "Reads the screenshot sets live from App Store Connect for the EDITABLE App Store version when one exists (that is the only surface a write can touch), otherwise the live version. Per locale: each set's screenshotDisplayType, count, and every screenshot's id, fileName, pixel dimensions, sourceFileChecksum, assetDeliveryState and image_url (the full-size PNG: imageAsset.templateUrl filled with the asset's own width/height). assetDeliveryState is the authoritative 'did this actually land' signal — an asset can sit in AWAITING_UPLOAD or FAILED while the count still looks right.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_screenshots_get_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: read App Store screenshot sets",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_screenshot_delete": {
      "post": {
        "operationId": "apple_screenshot_delete",
        "summary": "Apple: delete an App Store screenshot (gated)",
        "description": "Deletes one screenshot from a display-type set on the EDITABLE App Store version's localization. Same write contract: pre_write snapshot, listing_changes row, and in preview nothing is sent to Apple. Rejected when there is no editable version, no such set, or the screenshot id is not actually in the named set. Get ids from apple_screenshots_get.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_screenshot_delete_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: delete an App Store screenshot (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_screenshots_copy": {
      "post": {
        "operationId": "apple_screenshots_copy",
        "summary": "Apple: copy one localization's screenshots to another (gated)",
        "description": "Copies the screenshot sets of `from_locale` onto `to_locale` of the SAME platform's EDITABLE App Store version (every display type, or only `display_types`). Platform: `platform`, else the display types' own (APP_DESKTOP -> MAC_OS, iPhone/iPad -> IOS; one platform per call), else IOS. Built ON the existing tools: each source asset is downloaded full-size from its imageAsset.templateUrl (PNG; the opaque JPG rendition when the PNG carries alpha) and uploaded in order through the screenshot upload path (validation, snapshots, its own listing_changes row per shot); with replace_existing: true the target's shots of a copied display type are deleted FIRST through apple_screenshot_delete's path (a set holds 10). REFUSED as an error (no confirm token) when there is no editable version, either locale is missing on it (this tool never creates locales), a source shot is not COMPLETE or has an invalid size, or the target already has shots of a copied display type without replace_existing. Dry-run (default) lists per display type what would be copied and what the target holds, records the umbrella listing_changes row, and downloads / writes NOTHING. The preview returns `commit_args` — repeat them on the commit: expect_editable_version_id + expect_source_ids + expect_target_ids pin it to what the preview saw and a drift is refused before anything is deleted or downloaded. Apply stops at the first failure (`failed`, `partial: true` when some steps landed — `steps` names each one) and reads the target back.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_screenshots_copy_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: copy one localization's screenshots to another (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_release_status": {
      "post": {
        "operationId": "apple_release_status",
        "summary": "Apple: release state (live / in review / waiting build)",
        "description": "Reads the app's release pipeline live from App Store Connect: every App Store version (versionString, appVersionState, platform, createdDate, releaseType) with its attached build (version, processingState, uploadedDate), the latest review submissions with their items, the 5 latest TestFlight builds (processingState, expired, TestFlight what's-new when set) and the latest pre-release versions. `summary` is the compact row: live_version, pending_version, pending_state, latest_build, blocked_reason (null when nothing is waiting on you). 4 store requests.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_release_status_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: release state (live / in review / waiting build)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_cloud_workflow_update": {
      "post": {
        "operationId": "apple_cloud_workflow_update",
        "summary": "Apple: make an Xcode Cloud workflow archive a platform for the App Store (gated)",
        "description": "Adds an APP_STORE_ELIGIBLE ARCHIVE action for a platform to an Xcode Cloud workflow (default \"Release (tag)\"), or lifts an existing archive of that platform to APP_STORE_ELIGIBLE — the way to get macOS builds of a Mac Catalyst shell without a Mac. The new action copies the scheme of the workflow's existing ARCHIVE action (macOS: destination ANY_MAC); every other action is sent back unchanged (PATCH /v1/ciWorkflows/{id}). Same write contract as apple_cloud_build_create: refusals listed together, pre_write snapshot, listing_changes row, dry-run returns the plan; commit_args pins the workflow and its action list (expect_actions). Then start a run with apple_cloud_build_create on a release tag. 3+ store requests.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_cloud_workflow_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: make an Xcode Cloud workflow archive a platform for the App Store (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_cloud_build_create": {
      "post": {
        "operationId": "apple_cloud_build_create",
        "summary": "Apple: start an Xcode Cloud release build (gated)",
        "description": "Starts ONE Xcode Cloud build of the app's store-eligible workflow on a branch or tag — the way to get a build App Review accepts without a Mac. Picks the enabled workflow whose ARCHIVE actions distribute APP_STORE_ELIGIBLE builds (usually \"Release (tag)\"; name another with `workflow`); a workflow that archives INTERNAL_ONLY is REFUSED because App Store Connect refuses to attach such a build. Same write contract as apple_version_submit: refusals listed together (no confirm token), pre_write snapshot, listing_changes row, dry-run returns the POST /v1/ciBuildRuns plan; commit_args pins the workflow and the git reference. The build lands in TestFlight 15–40 min after the run starts — poll apple_builds_list, then apple_version_build_attach. 4+ store requests.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_cloud_build_create_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: start an Xcode Cloud release build (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_version_submit": {
      "post": {
        "operationId": "apple_version_submit",
        "summary": "Apple: submit a prepared version for review (gated)",
        "description": "Submits an EXISTING App Store version for App Review — attaches nothing new. The version must be PREPARE_FOR_SUBMISSION with a VALID build attached, every localization needs a description, what's new (for an update) and at least one screenshot, and no other review submission may be open; EVERY missing prerequisite is listed and the call is REFUSED as an error (nothing recorded as planned, no confirm token). Same write contract as apple_listing_update: pre_write snapshot, listing_changes row, and in preview it returns the version, build and localizations it WOULD submit plus the exact 3-call plan (POST reviewSubmissions → POST reviewSubmissionItems → PATCH submitted:true) without calling Apple. The preview returns `commit_args` — repeat them on the commit: `expect_build_id` pins the commit to the build the preview saw and the commit refuses (before any store call) if a different build is attached by then. A preview costs 4 + N(localizations) store requests. with_products: true also sends every READY_TO_SUBMIT subscription / in-app purchase (and its subscription group's preparing version) in the SAME submission — Apple reviews a FIRST product only together with a version, so use it whenever the build sells a product App Review has never approved. A MISSING_METADATA product is a refusal; `expect_products` in commit_args pins the product set the preview saw. Costs 2 + 2 per subscription group + 1 per ready product extra requests. The preview also REFUSES on the first-release fields App Review requires (read-only GETs, +6 requests): no contentRightsDeclaration, no price schedule, an appInfo localization without privacyPolicyUrl, an empty copyright, no appStoreReviewDetail (or one without contact fields) — fix them with apple_app_setup_update — and on missing 13-inch iPad screenshots when another localization of the version has iPad ones. `notes` carries what cannot be checked: App Privacy (web only, no public API) and iPad support when no localization has iPad screenshots (the build's device family is not readable). RESUBMISSION: a REJECTED / METADATA_REJECTED version whose UNRESOLVED_ISSUES submission on its platform carries it is resubmitted on THAT submission — the plan is PATCH reviewSubmissionItems {resolved:true} on its still-REJECTED item (the web's Edit → Add for Review; without it Apple 409s 'Version is not ready to be submitted yet') then PATCH submitted:true, pinned by `expect_submission_id` in commit_args; nothing is created and, on failure, nothing is canceled.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_version_submit_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: submit a prepared version for review (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_version_release": {
      "post": {
        "operationId": "apple_version_release",
        "summary": "Apple: release an approved version that waits on a manual release (gated)",
        "description": "Releases an App Store version that App Review has ALREADY approved and that waits on the developer (appVersionState PENDING_DEVELOPER_RELEASE — a version created with release_type MANUAL). The last step of the release path after apple_version_submit; nothing else can be released: any other state is REFUSED as an error before any store call (no confirm token), naming the versions the platform has. Same write contract as apple_version_submit: pre_write snapshot, listing_changes row, and in preview it returns the pending version it WOULD release plus the exact plan (ONE POST /v1/appStoreVersionReleaseRequests) without calling Apple. The preview returns `commit_args` — repeat them on the commit: `expect_version_id` pins the commit to the pending version the preview saw and the commit refuses, before any store call, if it no longer matches. On apply the version is `applied` the moment ASC returns 201 (release_request_id); the read-back is `readback.seen` once Apple shows a new state and `pending` while it still says PENDING_DEVELOPER_RELEASE (Apple lags a few minutes) — never a failure. 1 store request on preview, +2 on apply.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_version_release_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: release an approved version that waits on a manual release (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_submission_cancel": {
      "post": {
        "operationId": "apple_submission_cancel",
        "summary": "Apple: withdraw a version from App Review / cancel a review submission (gated)",
        "description": "Cancels ONE open review submission of a platform — App Store Connect's \"Remove from Review\" / \"Cancel Submission\" — with PATCH /v1/reviewSubmissions/{id} {canceled:true}. Without submission_id it picks the ONE submission on the platform that is WAITING_FOR_REVIEW / IN_REVIEW / UNRESOLVED_ISSUES / READY_FOR_REVIEW (an empty never-submitted draft is never auto-picked: it is not in review, Apple has refused to cancel one, and apple_version_submit reuses it); none, or more than one, is REFUSED as an error (no confirm token) listing them. Cancelling an UNRESOLVED_ISSUES submission WITHDRAWS the rejected version from review: the version becomes editable again (new build, new metadata, then apple_version_submit opens a new submission) and the Resolution Center thread stays. Same write contract as apple_version_release: pre_write snapshot, listing_changes row, and in preview it returns the submission, the versions it carries and the exact plan without calling Apple. The preview returns `commit_args` — repeat them on the commit: `expect_submission_id` + `expect_state` pin it to what the preview saw and a drift is refused before any store call. On apply it is `applied` the moment Apple accepts the PATCH; a refusal from Apple (409 'not in cancellable state', …) is `failed` with Apple's detail; the read-back is `seen` once the state moved (CANCELING / COMPLETE) and `pending` while Apple still shows the old one. 2 store requests on preview, +1 PATCH and 2 read-back on apply.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_submission_cancel_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: withdraw a version from App Review / cancel a review submission (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_builds_list": {
      "post": {
        "operationId": "apple_builds_list",
        "summary": "Apple: list TestFlight builds (newest first)",
        "description": "Lists the newest TestFlight builds for an app live from App Store Connect: id, build_number (CFBundleVersion), pre_release_version (the marketing version), platform, processing_state, uploaded_at, expired, uses_non_exempt_encryption, build_audience_type (APP_STORE_ELIGIBLE | INTERNAL_ONLY — an INTERNAL_ONLY build 409s on attach and cannot be changed; re-upload it), attached_to_version (the App Store version holding it, or null) and `attachable` (VALID, not expired AND APP_STORE_ELIGIBLE — what apple_version_create / apple_version_build_attach accept). 2 store requests (versions + builds).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_builds_list_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: list TestFlight builds (newest first)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_testflight_overview": {
      "post": {
        "operationId": "apple_testflight_overview",
        "summary": "Apple: TestFlight overview — beta groups, testers, builds in testing, tester feedback",
        "description": "Reads an app's TestFlight state live from App Store Connect — the truthful, citable answer to App Review's \"what beta testing did you do?\": every beta group (name, internal / external, public link enabled + URL + limit, feedback enabled, tester count), the app's total tester count, the newest builds PER PLATFORM with their internal / external beta state (buildBetaDetail), their Beta App Review submission state and the groups each was added to, and the tester feedback (betaFeedbackScreenshotSubmissions / betaFeedbackCrashSubmissions: date, comment, device, OS, build). `summary` counts it (external_states uses Apple's own state names). Feedback the key cannot read (403 / 404) is `{available: false, reason}`, never a failure. Tester e-mails are never returned. Capped: 50 groups (tester counts for the first 20), 25 builds per platform, 50 feedback items each. Read only; `requests` reports the store requests spent (about 5 + groups + platforms).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_testflight_overview_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: TestFlight overview — beta groups, testers, builds in testing, tester feedback",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_version_create": {
      "post": {
        "operationId": "apple_version_create",
        "summary": "Apple: create the next App Store version (gated)",
        "description": "Creates the NEXT App Store version (PREPARE_FOR_SUBMISSION) for a platform and optionally attaches a TestFlight build to it (ids from apple_builds_list). Same write contract as apple_version_submit: pre_write snapshot, listing_changes row, and in preview it returns the version + build it WOULD create plus the exact plan (ONE POST /v1/appStoreVersions that carries the build as `relationships.build` when one is named) without calling Apple. REFUSED as an error (no confirm token) when an editable version already exists for the platform (Apple allows one — use apple_version_build_attach / apple_version_submit), when the version string already exists for the platform, when release_type SCHEDULED has no earliest_release_date (or a date is given without SCHEDULED), or when the build is unknown, another app's, another platform's, not VALID, expired or already attached to a version. The preview returns `commit_args` — repeat them on the commit: `expect_editable_version_id: null` pins the commit to the 'no editable version' state the preview saw. On apply the version is `applied` the moment the POST returns; the read-back then confirms the build, and only if it is missing is a fallback PATCH …/relationships/build sent — a failure there is reported as `build_attach_error` (finish it with apple_version_build_attach), never a rollback. 2–3 store requests (the editable version is read with a state filter, so an app with many versions never misses it).\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_version_create_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: create the next App Store version (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_version_build_attach": {
      "post": {
        "operationId": "apple_version_build_attach",
        "summary": "Apple: attach a TestFlight build to the editable version (gated)",
        "description": "Attaches (or replaces) the TestFlight build on the EDITABLE App Store version of a platform (PREPARE_FOR_SUBMISSION / *_REJECTED) — the step between apple_version_create and apple_version_submit. Same write contract: pre_write snapshot, listing_changes row, and in preview it returns the version, the build attached now and the build it WOULD attach plus the exact plan (PATCH /v1/appStoreVersions/{id}/relationships/build) without calling Apple. REFUSED as an error (no confirm token) when there is no editable version for the platform (create one with apple_version_create), when the build is unknown, another app's, another platform's, not VALID, expired, already attached to another version, or already the attached build. The preview returns `commit_args` — repeat them on the commit: `expect_build_id` pins the commit to the build the preview saw attached (null when none — an explicit null is honoured: a build appearing since is reported as drift, not as a missing pin) and the commit refuses, before any store call, if a different build is attached by then. 3 store requests.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_version_build_attach_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: attach a TestFlight build to the editable version (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_app_setup_update": {
      "post": {
        "operationId": "apple_app_setup_update",
        "summary": "Apple: first-release App Store setup — content rights, free price, privacy URL, copyright, App Review details (gated)",
        "description": "Sets the first-release fields App Review refuses a submission without, through App Store Connect's public API: content_rights_declaration (PATCH /v1/apps), price: \"free\" (POST /v1/appPriceSchedules, base territory USA, the FREE price point — any other price is REFUSED: paid pricing is an owner decision; so is an existing non-free schedule), privacy_policy_url (PATCH every appInfoLocalization of the EDITABLE appInfo, or only privacy_policy_locales), copyright (PATCH the editable version) and review_detail (App Review contact + demo account: POST /v1/appStoreReviewDetails, or PATCH the one that exists). Same write contract as apple_version_create: every requested field is read live first (pre_write), a field already holding the value is listed in `unchanged` and gets no step, a listing_changes row is written, and in preview it returns the diff + the exact plan without calling Apple. The preview returns `commit_args` — repeat your arguments plus commit_args on the commit: `expect_editable_version_id` pins it to the editable version the preview saw and a drifted pin is refused before any store call. On apply each step runs on its own and is reported `applied` or `failed` (`partial: true` when some landed), then every requested field is read back. The demo account password is never returned, snapshotted, audited or logged (`[redacted]`), and a review_detail carrying one is NOT echoed in commit_args. review_detail_from_platform copies this app's other platform's App Review details (password included, never shown); review_detail_from_app copies ONLY the contact (first/last name, phone, e-mail) from another app's newest version — the demo account and notes stay as they are. The three are exclusive, and a copied detail is never echoed in commit_args. Not here: screenshots and App Privacy (no public API — App Store Connect on the web). 1–8 store requests on preview.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_app_setup_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Apple: first-release App Store setup — content rights, free price, privacy URL, copyright, App Review details (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/play_listing_get": {
      "post": {
        "operationId": "play_listing_get",
        "summary": "Play: get store listing",
        "description": "Read a Google Play store listing live from androidpublisher (title, short/full description, video, image counts per type). Opens a throwaway edit and deletes it — nothing is published. Omit `locale` to get every localization. `images` is null (never zeros) when the counts could not be read — that is UNKNOWN, not an empty gallery.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_listing_get_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: get store listing",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/play_listing_locales": {
      "post": {
        "operationId": "play_listing_locales",
        "summary": "Play: list every localized store listing",
        "description": "Every localization of a Google Play store listing, live from androidpublisher edits.listings.list (language, title, shortDescription, fullDescription, video + character counts against Play's limits). The Play twin of apple_listing_get's locale view: opens a throwaway edit, reads, and deletes it in a finally — nothing is published and no image call is made (use play_listing_get for image counts). A language missing here can be CREATED by play_listing_update with that locale and `create_locale: true`. Note: Play custom store listings (per-country / per-keyword / URL variants) are Play-Console-only — the Android Publisher API v3 has no resource for them, so they are not listed here.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_listing_locales_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: list every localized store listing",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/play_listing_update": {
      "post": {
        "operationId": "play_listing_update",
        "summary": "Play: update store listing, or add a new language (gated)",
        "description": "Change Play listing text, or CREATE a localization by passing a `locale` the listing does not have yet together with `create_locale: true` (then title, shortDescription and fullDescription are all required — Play refuses a new language without them). Creation is refused without the flag, when the language exists under a different case (en-us vs en-US), and when listings.list returned no language at all (a failed read, never an empty listing). In dry-run it snapshots the current listing (existing locale only), records a planned change, and returns the diff without touching Google. In apply mode it opens an edit, PUTs the listing (edits.listings.update creates-or-updates), commits it for review, and snapshots the result. `keep_existing_fields` (default true) merges your fields over the live ones; false replaces the localization with exactly the fields you pass (unpassed `video` is cleared; the three required fields must then all be passed). Limits: title 30, shortDescription 80, fullDescription 4000 chars. NOTE: the connected service account may lack production release rights — a commit 403 is recorded as a failed change, not a crash.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_listing_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: update store listing, or add a new language (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/play_app_details_get": {
      "post": {
        "operationId": "play_app_details_get",
        "summary": "Play: contact details + default language",
        "description": "The app's store contact details (contactEmail, contactPhone, contactWebsite) and default language, live from androidpublisher edits.details through a throwaway edit that is always deleted. The Data safety declaration has no read API (see play_data_safety_update); category and countries are Console-only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_app_details_get_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: contact details + default language",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/play_app_details_update": {
      "post": {
        "operationId": "play_app_details_update",
        "summary": "Play: set contact details / default language (gated)",
        "description": "Sets the store contact e-mail, phone, website and/or default language (a Play dashboard setup task) through edits.details. Dry-run (default) reads the live values, records a planned change with them as pre_write and returns the diff; apply opens an edit, PUTs the merged details, commits it and reads them back. A field you do not pass keeps its value; an empty string clears phone or website (the e-mail cannot be cleared). Refused when the live read comes back empty.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_app_details_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: set contact details / default language (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/play_data_safety_update": {
      "post": {
        "operationId": "play_data_safety_update",
        "summary": "Play: replace the Data safety declaration (gated)",
        "description": "Replaces the app's whole Data safety declaration through applications.dataSafety, using the CSV format Play Console exports (App content → Data safety → Export to CSV; columns Question ID / Response ID / Response value / Answer requirement). Pass the CSV text as csv (file_path reads the server's own asset directory, which a workspace cannot fill). The CSV is validated by column name (TRUE/FALSE/blank on choice rows, no blank REQUIRED row) before anything is sent; dry-run (default) stops there and records a planned change. Play has NO read API for Data safety, so there is no pre_write snapshot: an apply archives the exact CSV it sent. The answers must truthfully describe what the app collects and shares — this tool checks the shape, not the truth.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_data_safety_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: replace the Data safety declaration (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/play_release_status": {
      "post": {
        "operationId": "play_release_status",
        "summary": "Play: release state per track",
        "description": "Reads every Play track (internal, alpha, beta, production, custom) live from androidpublisher with its releases: status (draft | inProgress | halted | completed), name, versionCodes, userFraction for staged rollouts, release notes per language. Opens a throwaway edit and deletes it — nothing is published. `summary` is the compact row: live_version (production's completed release), pending_version / pending_state (a staged, halted or draft production release), latest_build (highest versionCode and the track it sits on), blocked_reason (halted rollout, un-rolled draft, or a build waiting on a lower track). 3 store requests.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_release_status_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: release state per track",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/play_release_promote": {
      "post": {
        "operationId": "play_release_promote",
        "summary": "Play: promote a release to another track (gated)",
        "description": "Promotes the latest completed (else in-progress) release of `from_track` onto `to_track` via edits.tracks.update + edits.commit. `user_fraction` (0 < f < 1) makes it a staged rollout (inProgress); omit it or pass 1 for a full (completed) release. `release_notes` replaces the carried-over notes per language. REFUSED as an error (nothing planned, no confirm token) when the codes are already on the target — completed, in progress, or sitting in a draft — when the target has a staged rollout in flight, or when the source track has nothing to promote; every reason is listed. A never-rolled DRAFT on the target with the same codes (the Console's 'save as draft') is the one refusal you can override: `replace_draft: true` discards that draft and puts the promoted release there instead (tracks.update is a full replace, the draft's own notes are lost; the response names it in `replaces_draft`). Write contract: pre_write snapshot of the target track, listing_changes row, and in preview the exact version codes / fraction that WOULD go live plus the tracks.update body, without touching Google. The preview returns `commit_args` — repeat them on the commit: `expect_version_codes` pins the commit to the release the preview saw and the commit refuses (before any store call) if the source track moved. NOTE: the connected service account may lack production release rights — a commit 403 is recorded as a failed change, never as success.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_release_promote_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: promote a release to another track (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/play_rollout_update": {
      "post": {
        "operationId": "play_rollout_update",
        "summary": "Play: change or halt a staged rollout (gated)",
        "description": "Moves a track's staged rollout: `user_fraction` raises it (Play never lets a fraction go down; 1 completes the rollout), `halt: true` halts it. REFUSED as an error (nothing planned, no confirm token) when the track has no inProgress / halted release, when the fraction is not above the current one, or when both arguments are passed. Same write contract as play_release_promote: snapshot, audit row, and in preview the exact tracks.update body without touching Google; repeat the preview's `commit_args` on the commit (`expect_version_codes` pins it to the staged release the preview saw).\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_rollout_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Play: change or halt a staged rollout (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/ms_listing_get": {
      "post": {
        "operationId": "ms_listing_get",
        "summary": "Microsoft Store listing (live)",
        "description": "Live Partner Center listing for an app: description, short description, features, search terms, image slots. Reads the last published submission (falls back to pending). `title` comes from the product name reservation and is not editable through ms_listing_update.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_listing_get_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Microsoft Store listing (live)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/ms_release_status": {
      "post": {
        "operationId": "ms_release_status",
        "summary": "Microsoft Store release state (live)",
        "description": "Live Partner Center release state for an app: the last published submission and the pending one — status, statusDetails (errors, warnings, certification reports), targetPublishMode, and every package with its version, architecture and fileStatus. `summary` is the compact row: live_version, pending_version, pending_state, latest_build, blocked_reason (certification/pre-processing failure, an uncommitted submission, or a manual publish waiting on you). Read-only, 2–3 spaced requests.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_release_status_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Microsoft Store release state (live)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/ms_listing_update": {
      "post": {
        "operationId": "ms_listing_update",
        "summary": "Microsoft Store listing update (gated write)",
        "description": "Propose a Microsoft Store listing change. In preview it snapshots the listing, logs a planned change and returns the diff WITHOUT touching Partner Center. Search-term rules: max 7 terms, 30 chars per term, 21 unique words total. Images are never modified. Advanced-pricing / advanced-listing products reject every submission-API write with InvalidState — those come back as status 'rejected' and must be edited in the Partner Center UI.\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_listing_update_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Microsoft Store listing update (gated write)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/ms_submission_create": {
      "post": {
        "operationId": "ms_submission_create",
        "summary": "Microsoft Store: create a pending submission (gated)",
        "description": "Creates a new pending submission on an app, cloned from the last published one (POST /submissions) — the first step of a release: create, upload the package (package upload is not in the cloud yet), then ms_submission_commit. REFUSED when a pending submission already exists (Partner Center allows one; finish or ms_submission_delete it) or when the product has no published submission to clone. The preview pins `expect_pending_submission_id: null` (the only state a create can succeed in) and the commit requires it. The result reports `has_upload_url` — the submission's fileUploadUrl is a bearer SAS URL and is never returned. Write contract: pre_write snapshot (listing_versions, store microsoft:release), listing_changes row, and in preview the exact plan WITHOUT touching Partner Center. The preview returns `commit_args` — repeat them on the commit: the `expect_*` pin(s) bind the commit to the state the preview saw and the commit refuses, before any store call, when the store moved or the pin is missing. A refusal is an error (nothing planned, no confirm token) listing every reason. On apply, `applied` is recorded the moment Partner Center accepted the call; the read-back is best-effort (`readback_error`).\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_submission_create_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Microsoft Store: create a pending submission (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/ms_submission_commit": {
      "post": {
        "operationId": "ms_submission_commit",
        "summary": "Microsoft Store: commit the pending submission (gated)",
        "description": "Commits the app's pending submission (POST /submissions/{id}/commit): Partner Center then runs pre-processing and certification, and publishes per the submission's targetPublishMode (a Manual mode still needs a human Publish). REFUSED when no submission is pending, when it is not PendingCommit, when any package is still PendingUpload (nothing was uploaded for it), or when statusDetails.errors is non-empty. Pins: `expect_submission_id` and `expect_status` (must be PendingCommit). The read-back polls GET /submissions/{id}/status once. Write contract: pre_write snapshot (listing_versions, store microsoft:release), listing_changes row, and in preview the exact plan WITHOUT touching Partner Center. The preview returns `commit_args` — repeat them on the commit: the `expect_*` pin(s) bind the commit to the state the preview saw and the commit refuses, before any store call, when the store moved or the pin is missing. A refusal is an error (nothing planned, no confirm token) listing every reason. On apply, `applied` is recorded the moment Partner Center accepted the call; the read-back is best-effort (`readback_error`).\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_submission_commit_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Microsoft Store: commit the pending submission (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/ms_submission_delete": {
      "post": {
        "operationId": "ms_submission_delete",
        "summary": "Microsoft Store: delete the pending submission (gated)",
        "description": "Deletes the app's pending submission (DELETE /submissions/{id}) — the last published submission is untouched. REFUSED when nothing is pending or when the pending submission is in flight (CommitStarted, PreProcessing, Certification, Release): Partner Center does not delete those; a failed one (CommitFailed, PreProcessingFailed, CertificationFailed, ReleaseFailed) or an uncommitted one (PendingCommit) can go. `expect_submission_id` pins the commit to the submission the preview saw. The read-back confirms the application shows no pending submission. Write contract: pre_write snapshot (listing_versions, store microsoft:release), listing_changes row, and in preview the exact plan WITHOUT touching Partner Center. The preview returns `commit_args` — repeat them on the commit: the `expect_*` pin(s) bind the commit to the state the preview saw and the commit refuses, before any store call, when the store moved or the pin is missing. A refusal is an error (nothing planned, no confirm token) listing every reason. On apply, `applied` is recorded the moment Partner Center accepted the call; the read-back is best-effort (`readback_error`).\n\nHosted mode: this tool ALWAYS runs as a dry-run preview first and returns a `confirm_token`. To commit, call it again with the SAME arguments plus `apply: true` and `confirmToken: <the token>` (single use, valid 10 minutes, bound to you, this client, this tool and these exact arguments). An apply without a valid token is refused and never mints one — run the preview again. There is no global apply mode on the server.",
        "tags": [
          "Write"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_submission_delete_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The preview (or, with apply and confirmToken, the commit's result).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WritePreview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Microsoft Store: delete the pending submission (gated)",
          "readOnlyHint": false,
          "destructiveHint": true,
          "idempotentHint": false,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/release_status": {
      "post": {
        "operationId": "release_status",
        "summary": "Release state across stores (live)",
        "description": "One compact row per app x store, read live: live_version, pending_version, pending_state, latest_build, blocked_reason (null when nothing waits on you). `app` = one app (all its stores, 4/3/3 requests); omit it for every app in lean mode (2/3/2 requests per store) under a hard ceiling of 60 store requests per call — the budget is reserved in app order, so the pairs past the ceiling are the LAST ones and come back as status 'skipped' with truncated:true; narrow with `stores` or `app` to see them. Every store call is time-bounded, so a dead store is an 'error' row, never a hang and never a missing app. Detail lives in apple_release_status / play_release_status / ms_release_status.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/release_status_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "Release state across stores (live)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/review_watch_status": {
      "post": {
        "operationId": "review_watch_status",
        "summary": "App Review check state (no store call)",
        "description": "What this workspace's App Review check saw on its last run, read from its saved state: NO store call. `lanes` = the last known state per app x store x lane (Apple: the newest version per platform; Play: per track, internal excluded; Microsoft: the pending or last published submission), `recent_transitions` = the newest state changes, `last_check` = when it ran, how many app x store pairs it read, the requests it used against its 60-request ceiling and which stores failed. A rejection's reasons are in App Store Connect. For a live read use release_status.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/review_watch_status_input"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The tool's answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ToolAnswer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Refused"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/UnknownTool"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/Failed"
          },
          "503": {
            "$ref": "#/components/responses/Busy"
          }
        },
        "x-mcp-annotations": {
          "title": "App Review check state (no store call)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "A workspace key (sws_…) from the MCP & API page, or an OAuth access token this host issued for its /mcp resource."
      }
    },
    "schemas": {
      "Denial": {
        "type": "object",
        "description": "A tool refusal (see https://storevisor.devino.ca/docs/errors).",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "SCOPE_MISSING",
              "WRONG_PRODUCT",
              "CONFIRMATION_REQUIRED",
              "CONSENT_REVOKED",
              "TOOL_EXECUTION_FAILED"
            ]
          },
          "message": {
            "type": "string"
          },
          "requiredScope": {
            "type": "string",
            "description": "SCOPE_MISSING: the scope the tool needs"
          },
          "product": {
            "type": "string",
            "description": "WRONG_PRODUCT: the product the tool belongs to"
          },
          "reason": {
            "type": "string",
            "description": "CONFIRMATION_REQUIRED: malformed | bad_signature | expired | mismatch | reused"
          },
          "audit_id": {
            "type": "integer",
            "description": "TOOL_EXECUTION_FAILED: the logged failure's number"
          }
        }
      },
      "RequestError": {
        "type": "object",
        "description": "A request refused before any tool ran (see https://storevisor.devino.ca/docs/errors).",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_token",
              "rate_limited",
              "payload_too_large",
              "unknown_tool",
              "invalid_body"
            ]
          },
          "message": {
            "type": "string"
          },
          "retry_after_s": {
            "type": "integer"
          }
        }
      },
      "JsonRpcError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "jsonrpc": {
            "const": "2.0"
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "ToolAnswer": {
        "type": "object",
        "description": "The tool's own JSON answer.",
        "additionalProperties": true
      },
      "WritePreview": {
        "type": "object",
        "description": "A write's preview: the tool's answer (the diff or plan; nothing reached the store) plus a single-use confirm_token valid 10 minutes. Commit by repeating the call with the same arguments, commit_args merged in when present, apply: true and confirmToken.",
        "additionalProperties": true,
        "properties": {
          "status": {
            "type": "string",
            "description": "planned | rejected (a preview), applied | failed (a commit)"
          },
          "confirm_token": {
            "type": "string"
          },
          "confirm_expires_in_s": {
            "type": "integer"
          },
          "apply_hint": {
            "type": "string"
          },
          "commit_args": {
            "type": "object",
            "additionalProperties": true,
            "description": "The store state the preview saw; the commit must repeat it."
          },
          "commit": {
            "type": "string",
            "enum": [
              "not_available"
            ],
            "description": "Set on a rejected preview: there is nothing to commit."
          }
        }
      },
      "apple_listing_get_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "locale": {
            "description": "e.g. en-US, en-CA. Omit for all locales.",
            "type": "string"
          },
          "platform": {
            "description": "default: iOS preferred",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_listing_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "description": "exact ASC locale, e.g. en-US"
          },
          "fields": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 400
              },
              "subtitle": {
                "type": "string",
                "maxLength": 400
              },
              "keywords": {
                "type": "string",
                "maxLength": 400
              },
              "description": {
                "type": "string",
                "maxLength": 20000
              },
              "promotionalText": {
                "type": "string",
                "maxLength": 2000
              },
              "whatsNew": {
                "type": "string",
                "maxLength": 20000
              }
            },
            "description": "only the fields you want to change"
          },
          "platform": {
            "description": "whose version the version fields land on; default: iOS preferred",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why this change — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "locale",
          "fields",
          "rationale"
        ]
      },
      "apple_cpp_list_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_cpp_create_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "internal CPP name (not customer visible)"
          },
          "locale": {
            "type": "string"
          },
          "promotionalText": {
            "type": "string",
            "minLength": 1,
            "description": "max 170 chars"
          },
          "rationale": {
            "type": "string",
            "minLength": 1
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "name",
          "locale",
          "promotionalText",
          "rationale"
        ]
      },
      "apple_screenshots_get_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "locale": {
            "description": "e.g. en-US. Omit for all locales on the version.",
            "type": "string"
          },
          "platform": {
            "description": "whose version to read (each platform has its own). Omit = every platform, iOS preferred",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_screenshot_delete_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "displayType": {
            "type": "string",
            "enum": [
              "APP_IPHONE_67",
              "APP_IPHONE_65",
              "APP_IPHONE_61",
              "APP_IPHONE_58",
              "APP_IPHONE_55",
              "APP_IPHONE_47",
              "APP_IPHONE_40",
              "APP_IPHONE_35",
              "APP_IPAD_PRO_3GEN_129",
              "APP_IPAD_PRO_3GEN_11",
              "APP_IPAD_PRO_129",
              "APP_IPAD_105",
              "APP_IPAD_97",
              "APP_DESKTOP",
              "APP_APPLE_TV",
              "APP_APPLE_VISION_PRO"
            ]
          },
          "screenshotId": {
            "type": "string",
            "minLength": 1,
            "description": "appScreenshots resource id from apple_screenshots_get"
          },
          "platform": {
            "description": "default = the display type's own; a mismatch is refused",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "rationale": {
            "type": "string",
            "minLength": 1
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "locale",
          "displayType",
          "screenshotId",
          "rationale"
        ]
      },
      "apple_screenshots_copy_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "from_locale": {
            "type": "string",
            "minLength": 2,
            "description": "the localization to copy FROM, e.g. en-US"
          },
          "to_locale": {
            "type": "string",
            "minLength": 2,
            "description": "the localization to copy TO, e.g. en-CA (must already exist on the editable version)"
          },
          "display_types": {
            "description": "only these sets; default every set of from_locale on the platform",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "APP_IPHONE_67",
                "APP_IPHONE_65",
                "APP_IPHONE_61",
                "APP_IPHONE_58",
                "APP_IPHONE_55",
                "APP_IPHONE_47",
                "APP_IPHONE_40",
                "APP_IPHONE_35",
                "APP_IPAD_PRO_3GEN_129",
                "APP_IPAD_PRO_3GEN_11",
                "APP_IPAD_PRO_129",
                "APP_IPAD_105",
                "APP_IPAD_97",
                "APP_DESKTOP",
                "APP_APPLE_TV",
                "APP_APPLE_VISION_PRO"
              ]
            }
          },
          "platform": {
            "description": "default = the display types' own platform, else IOS",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "replace_existing": {
            "description": "delete the target's screenshots of each copied display type first (default: refuse when it has any)",
            "type": "boolean"
          },
          "expect_editable_version_id": {
            "description": "from commit_args; required on the commit, refused on mismatch",
            "type": "string",
            "minLength": 1
          },
          "expect_source_ids": {
            "description": "from commit_args: <displayType>:<id> of every source shot; required on the commit, refused on mismatch",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expect_target_ids": {
            "description": "from commit_args: <displayType>:<id> of every target shot replaced ([] = none); required on the commit, refused on mismatch",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "from_locale",
          "to_locale",
          "rationale"
        ]
      },
      "apple_release_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_cloud_workflow_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "platform": {
            "type": "string",
            "enum": [
              "MACOS",
              "IOS",
              "VISIONOS"
            ],
            "description": "the platform to archive for the App Store"
          },
          "workflow": {
            "description": "workflow id or exact name; default \"Release (tag)\"",
            "type": "string"
          },
          "expect_workflow_id": {
            "description": "from commit_args; required on the commit",
            "type": "string"
          },
          "expect_actions": {
            "description": "from commit_args: the action list the preview saw; required on the commit",
            "type": "string"
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "platform",
          "rationale"
        ]
      },
      "apple_cloud_build_create_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "git_reference": {
            "type": "string",
            "minLength": 1,
            "description": "branch or tag name to build, e.g. main or v1.2.0"
          },
          "workflow": {
            "description": "workflow id or exact name; default = the one store-eligible workflow, preferring \"Release (tag)\"",
            "type": "string"
          },
          "expect_workflow_id": {
            "description": "from commit_args: the workflow the preview resolved; required on the commit",
            "type": "string"
          },
          "expect_git_reference_id": {
            "description": "from commit_args: the git reference the preview resolved; required on the commit",
            "type": "string"
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "git_reference",
          "rationale"
        ]
      },
      "apple_version_submit_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "version_string": {
            "type": "string",
            "minLength": 1,
            "description": "exact versionString of the prepared App Store version, e.g. 3.2.0"
          },
          "expect_build_id": {
            "description": "the build id the preview showed (from commit_args); required on the commit, refused on mismatch",
            "type": "string"
          },
          "with_products": {
            "description": "also submit every READY_TO_SUBMIT subscription / in-app purchase with this version (first products must ride with a version)",
            "type": "boolean"
          },
          "expect_products": {
            "description": "from commit_args (with_products only): the kind:id list of products the preview saw; required on the commit, refused on mismatch",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expect_submission_id": {
            "description": "from commit_args (resubmission only): the UNRESOLVED_ISSUES submission id the preview saw; required on the commit, refused on mismatch",
            "type": "string",
            "minLength": 1
          },
          "platform": {
            "description": "which platform's version to submit when the same versionString exists on several (default IOS)",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "expect_draft_submission_id": {
            "description": "from commit_args: the empty never-submitted draft submission the preview chose to reuse (Apple will not cancel one); refused on mismatch",
            "type": "string",
            "minLength": 1
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "version_string",
          "rationale"
        ]
      },
      "apple_version_release_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "platform": {
            "description": "default IOS — each platform's version is released separately",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "expect_version_id": {
            "description": "from commit_args: the pending version id the preview showed; required on the commit, refused on mismatch",
            "type": "string",
            "minLength": 1
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "rationale"
        ]
      },
      "apple_submission_cancel_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "platform": {
            "description": "default IOS — review submissions are per platform",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "submission_id": {
            "description": "the review submission to cancel (apple_release_status review_submissions); default = the one open on the platform",
            "type": "string",
            "minLength": 1
          },
          "expect_submission_id": {
            "description": "from commit_args: the submission the preview chose; required on the commit, refused on mismatch",
            "type": "string",
            "minLength": 1
          },
          "expect_state": {
            "description": "from commit_args: its state when the preview ran; required on the commit, refused on mismatch",
            "type": "string",
            "minLength": 1
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "rationale"
        ]
      },
      "apple_builds_list_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "limit": {
            "description": "newest N builds, default 10",
            "type": "integer",
            "minimum": 1,
            "maximum": 25
          },
          "platform": {
            "description": "default IOS",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_testflight_overview_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "platform": {
            "description": "only this platform's builds; default = every platform the app has builds for",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "builds_per_platform": {
            "description": "newest N builds per platform, default 5",
            "type": "integer",
            "minimum": 1,
            "maximum": 25
          },
          "feedback_limit": {
            "description": "newest N feedback items of each kind, default 20",
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_version_create_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "version_string": {
            "type": "string",
            "minLength": 1,
            "description": "the marketing version to create, e.g. 1.4.0"
          },
          "platform": {
            "description": "default IOS",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "build_id": {
            "description": "TestFlight build id to attach right after creating (from apple_builds_list)",
            "type": "string",
            "minLength": 1
          },
          "copyright": {
            "description": "e.g. 2026 Example Inc.",
            "type": "string"
          },
          "release_type": {
            "description": "MANUAL | AFTER_APPROVAL | SCHEDULED",
            "type": "string",
            "enum": [
              "MANUAL",
              "AFTER_APPROVAL",
              "SCHEDULED"
            ]
          },
          "earliest_release_date": {
            "description": "ISO 8601; required with (and only with) release_type SCHEDULED",
            "type": "string"
          },
          "expect_editable_version_id": {
            "description": "from commit_args: null = the preview saw no editable version for the platform; required on the commit, refused on mismatch",
            "type": [
              "string",
              "null"
            ]
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "version_string",
          "rationale"
        ]
      },
      "apple_version_build_attach_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "build_id": {
            "type": "string",
            "minLength": 1,
            "description": "TestFlight build id to attach (from apple_builds_list)"
          },
          "platform": {
            "description": "default IOS",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "expect_build_id": {
            "description": "from commit_args: the build id attached when the preview ran (null = none); required on the commit when a build is attached, refused on mismatch",
            "type": [
              "string",
              "null"
            ]
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "build_id",
          "rationale"
        ]
      },
      "apple_app_setup_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "platform": {
            "description": "default IOS — the platform whose editable version copyright / review_detail land on",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "version_string": {
            "description": "the editable version to target; default the platform's editable version",
            "type": "string",
            "minLength": 1
          },
          "content_rights_declaration": {
            "description": "does the app show, include or access third-party content?",
            "type": "string",
            "enum": [
              "DOES_NOT_USE_THIRD_PARTY_CONTENT",
              "USES_THIRD_PARTY_CONTENT"
            ]
          },
          "price": {
            "description": "\"free\" only — anything else is refused (paid pricing is an owner decision)",
            "type": "string"
          },
          "privacy_policy_url": {
            "description": "https URL of the privacy policy",
            "type": "string",
            "format": "uri"
          },
          "privacy_policy_locales": {
            "description": "only these appInfo locales; default every locale of the editable appInfo",
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 2
            }
          },
          "copyright": {
            "description": "e.g. \"© 2026 14930398 Canada Inc.\"",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "review_detail": {
            "description": "App Review contact + sign-in details for the version",
            "type": "object",
            "properties": {
              "contact_first_name": {
                "type": "string",
                "minLength": 1
              },
              "contact_last_name": {
                "type": "string",
                "minLength": 1
              },
              "contact_phone": {
                "type": "string",
                "minLength": 5
              },
              "contact_email": {
                "type": "string",
                "format": "email",
                "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
              },
              "demo_account_required": {
                "type": "boolean"
              },
              "demo_account_name": {
                "type": "string",
                "minLength": 1
              },
              "demo_account_password": {
                "description": "never echoed back, logged or stored — sent once to Apple",
                "type": "string",
                "minLength": 1
              },
              "notes": {
                "type": "string",
                "maxLength": 4000
              }
            },
            "required": [
              "contact_first_name",
              "contact_last_name",
              "contact_phone",
              "contact_email",
              "demo_account_required"
            ]
          },
          "review_detail_from_platform": {
            "description": "instead of review_detail: copy the App Review details (demo password included, never shown) from this other platform's newest version",
            "type": "string",
            "enum": [
              "IOS",
              "MAC_OS",
              "TV_OS",
              "VISION_OS"
            ]
          },
          "review_detail_from_app": {
            "description": "instead of review_detail: copy ONLY the App Review contact (first/last name, phone, e-mail) from this other app's newest version — never its demo account or notes",
            "type": "string",
            "minLength": 1
          },
          "expect_editable_version_id": {
            "description": "from commit_args: the editable version id the preview saw (null = none); required on the commit, refused on mismatch",
            "type": [
              "string",
              "null"
            ]
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "rationale"
        ]
      },
      "play_listing_get_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug or Play package name"
          },
          "locale": {
            "description": "BCP-47 Play language, e.g. en-US. Omit for all locales.",
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "play_listing_locales_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug or Play package name"
          }
        },
        "required": [
          "app"
        ]
      },
      "play_listing_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "description": "Play language, e.g. en-US (case-sensitive). A language the listing does not have yet is CREATED only with create_locale: true."
          },
          "fields": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "shortDescription": {
                "type": "string"
              },
              "fullDescription": {
                "type": "string"
              },
              "video": {
                "description": "YouTube URL, or empty string to clear",
                "type": "string"
              }
            },
            "description": "only the fields you want to change (all three text fields when creating a new locale)"
          },
          "keep_existing_fields": {
            "description": "default true: fields you do not pass keep their live value. false: the localization becomes exactly the fields you pass.",
            "type": "boolean"
          },
          "create_locale": {
            "description": "default false: a locale the listing does not have is REFUSED. true: create it (all three text fields required).",
            "type": "boolean"
          },
          "rationale": {
            "type": "string",
            "description": "why this change is being made — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "locale",
          "fields",
          "rationale"
        ]
      },
      "play_app_details_get_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug or Play package name"
          }
        },
        "required": [
          "app"
        ]
      },
      "play_app_details_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "fields": {
            "type": "object",
            "properties": {
              "contactEmail": {
                "type": "string"
              },
              "contactPhone": {
                "description": "international format, empty string clears",
                "type": "string"
              },
              "contactWebsite": {
                "description": "http(s) URL, empty string clears",
                "type": "string"
              },
              "defaultLanguage": {
                "description": "Play language code, e.g. en-US",
                "type": "string"
              }
            }
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "fields",
          "rationale"
        ]
      },
      "play_data_safety_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "file_path": {
            "description": "not usable in the cloud (a server directory); pass csv instead",
            "type": "string"
          },
          "csv": {
            "description": "the CSV text itself",
            "type": "string",
            "maxLength": 200000
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "rationale"
        ]
      },
      "play_release_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug or Play package name"
          }
        },
        "required": [
          "app"
        ]
      },
      "play_release_promote_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "from_track": {
            "type": "string",
            "minLength": 1,
            "description": "internal | alpha | beta | production | a custom track name"
          },
          "to_track": {
            "type": "string",
            "minLength": 1
          },
          "user_fraction": {
            "description": "staged rollout fraction, e.g. 0.1 for 10%; omit or 1 = full release",
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 1
          },
          "release_notes": {
            "description": "{ 'en-US': 'What changed' } — replaces the source release's notes",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {
              "type": "string"
            }
          },
          "replace_draft": {
            "description": "true = discard a never-rolled DRAFT on to_track that carries the same version codes and put the promoted release there instead (that draft's notes are lost); without it such a draft is a refusal, and the flag with no such draft is refused too",
            "type": "boolean"
          },
          "expect_version_codes": {
            "description": "the version codes the preview showed (from commit_args); required on the commit, refused on mismatch",
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": -9007199254740991,
              "maximum": 9007199254740991
            }
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "from_track",
          "to_track",
          "rationale"
        ]
      },
      "play_rollout_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "track": {
            "type": "string",
            "minLength": 1,
            "description": "the track carrying the staged rollout, usually production"
          },
          "user_fraction": {
            "description": "new fraction, must be above the current one; 1 = complete the rollout",
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 1
          },
          "halt": {
            "description": "true = halt the rollout (mutually exclusive with user_fraction)",
            "type": "boolean"
          },
          "expect_version_codes": {
            "description": "the version codes the preview showed (from commit_args); required on the commit, refused on mismatch",
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": -9007199254740991,
              "maximum": 9007199254740991
            }
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why — stored in the audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "track",
          "rationale"
        ]
      },
      "ms_listing_get_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          },
          "lang": {
            "description": "listing locale, default en-us",
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "ms_release_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          }
        },
        "required": [
          "app"
        ]
      },
      "ms_listing_update_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          },
          "lang": {
            "type": "string",
            "description": "listing locale, e.g. en-us"
          },
          "fields": {
            "type": "object",
            "properties": {
              "description": {
                "type": "string"
              },
              "shortDescription": {
                "type": "string"
              },
              "features": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "searchTerms": {
                "maxItems": 7,
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "description": "only the listing fields you want changed"
          },
          "rationale": {
            "type": "string",
            "description": "why this change — stored in the listing_changes audit log"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app",
          "lang",
          "fields",
          "rationale"
        ]
      },
      "ms_submission_create_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          },
          "rationale": {
            "description": "why — stored in the audit log; defaults to a per-tool line naming the app + submission so commit_args always re-validates",
            "type": "string",
            "minLength": 1
          },
          "expect_pending_submission_id": {
            "description": "from the preview's commit_args: null = no pending submission existed; required on the commit",
            "type": [
              "string",
              "null"
            ]
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "ms_submission_commit_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          },
          "rationale": {
            "description": "why — stored in the audit log; defaults to a per-tool line naming the app + submission so commit_args always re-validates",
            "type": "string",
            "minLength": 1
          },
          "expect_submission_id": {
            "description": "the pending submission id the preview showed (from commit_args); required on the commit, refused on mismatch",
            "type": "string"
          },
          "expect_status": {
            "description": "the submission status the preview showed (PendingCommit); required on the commit, refused on mismatch",
            "type": "string"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "ms_submission_delete_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          },
          "rationale": {
            "description": "why — stored in the audit log; defaults to a per-tool line naming the app + submission so commit_args always re-validates",
            "type": "string",
            "minLength": 1
          },
          "expect_submission_id": {
            "description": "the pending submission id the preview showed (from commit_args); required on the commit, refused on mismatch",
            "type": "string"
          },
          "apply": {
            "description": "true = commit for real. Requires the confirmToken returned by the preceding dry-run of this exact call.",
            "type": "boolean"
          },
          "confirmToken": {
            "description": "The confirm_token from the dry-run preview (single use, 10-minute TTL).",
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "release_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug, app name, or a store id; omit for every app",
            "type": "string"
          },
          "stores": {
            "description": "restrict to these stores (default: every configured store)",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "apple",
                "play",
                "microsoft"
              ]
            }
          }
        }
      },
      "review_watch_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug to narrow to; omit for every app",
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "Refused": {
        "description": "A tool refusal (Denial), invalid arguments (the validation message as a JSON string), a body that is not JSON (RequestError invalid_body) or a JSON-RPC error.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Denial"
                },
                {
                  "$ref": "#/components/schemas/RequestError"
                },
                {
                  "type": "string"
                },
                {
                  "$ref": "#/components/schemas/JsonRpcError"
                }
              ]
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No credential, or one this host does not accept (invalid_token).",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            },
            "description": "Bearer, with resource_metadata for OAuth discovery"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RequestError"
            }
          }
        }
      },
      "UnknownTool": {
        "description": "Not a tool of either product (unknown_tool).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RequestError"
            }
          }
        }
      },
      "TooLarge": {
        "description": "Body over 256 KB (payload_too_large).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RequestError"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over 60 calls a minute for this key or connection (rate_limited).",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RequestError"
            }
          }
        }
      },
      "Failed": {
        "description": "The tool ran out of time or its worker stopped (JSON-RPC -32603).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/JsonRpcError"
            }
          }
        }
      },
      "Busy": {
        "description": "Every worker is in use (JSON-RPC -32000); retry after the Retry-After seconds.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/JsonRpcError"
            }
          }
        }
      }
    }
  }
}