{
  "openapi": "3.1.0",
  "info": {
    "title": "StoreScope Analytics API",
    "version": "1.0.0",
    "description": "The StoreScope Analytics 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://storescope.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://storescope.devino.ca/docs/api. Error codes: https://storescope.devino.ca/docs/errors."
  },
  "servers": [
    {
      "url": "https://storescope.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://storescope.devino.ca/docs/api",
    "description": "API reference"
  },
  "paths": {
    "/api/tools/data_freshness": {
      "post": {
        "operationId": "data_freshness",
        "summary": "Data freshness",
        "description": "START HERE. Per-adapter last sync status/time/rows, latest fact date per store, apps with no data, stale (>3d over the store's own lag) warnings, and the count of pending Apple analytics report requests. Tells you what you are allowed to conclude before you read any numbers.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/data_freshness_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": "Data freshness",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/fleet_overview": {
      "post": {
        "operationId": "fleet_overview",
        "summary": "Overview of every app",
        "description": "Every app x store for the last N days: downloads/installs/acquisitions totals, impressions -> page_views -> downloads funnel where the store provides it, conversion, rating average and review count, plus a one-line data-derived signal per row. AI marketplaces (claude/chatgpt/copilot/registry) report tool_calls / tool_call_users instead. Each row is anchored on THAT app x store's own latest date (stores lag differently) and says so; rows with no volume metric and no reviews are omitted unless includeEmpty. When more app x store combos exist than fit, `truncated`/`total_combos` say so instead of dropping them silently, and `unmapped_metrics` lists any raw metric name that has no canonical family (those rows are kept visible, never aggregated).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/fleet_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": "Overview of every app",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/app_funnel": {
      "post": {
        "operationId": "app_funnel",
        "summary": "App funnel",
        "description": "Daily impressions/page_views/downloads time series for one app on one store plus a grouped funnel with conversion computed per group. Use groupBy=source_type on Apple to compare search vs browse conversion. On Play the grouped block is read from the `_by_source` traffic-source re-cut (the canonical rows are the country cut and carry no source_type); `group_family_suffix` says which families the groups came from - never add the two cuts together.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/app_funnel_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 funnel",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/compare_periods": {
      "post": {
        "operationId": "compare_periods",
        "summary": "Compare periods",
        "description": "Current vs previous period for one metric, broken down per group (defaults: group by store when store is omitted, by source_type when it is given). When grouping by store each store is anchored on its OWN latest date (a lagging store is never compared against zero). Returns delta and delta_pct and flags material changes (>=20% and >=5 units) so privacy noise is not reported as a trend.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/compare_periods_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": "Compare periods",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/listing_change_impact": {
      "post": {
        "operationId": "listing_change_impact",
        "summary": "Listing change impact",
        "description": "For each listing change (listing_changes audit rows and detected listing_versions snapshot diffs), the 14d-before vs 14d-after impressions / page_views / downloads / conversion. Each event states honestly whether its after-window is complete given the store's own lag (Apple T+2..T+5, Play 3-7d).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/listing_change_impact_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": "Listing change impact",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/listing_history": {
      "post": {
        "operationId": "listing_history",
        "summary": "Listing history",
        "description": "The listing_versions snapshot chain for an app/store (optionally one locale) with field-level diffs between consecutive snapshots, plus the listing_changes audit rows (what this MCP planned or applied, in which mode).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/listing_history_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": "Listing history",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/reviews_digest": {
      "post": {
        "operationId": "reviews_digest",
        "summary": "Reviews digest",
        "description": "Review counts by rating, average, unanswered count, top recurring words and two-word phrases (simple stopword tokenizer), and the 10 most recent low-rating (<=3) reviews. Scope with app/store/rating bounds. The window ends on the newest review IN SCOPE, not today, so a lagging store still reports its latest reviews; `total` is the real match count and `truncated`/`scanned` say when the aggregates cover only the newest reviews.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/reviews_digest_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": "Reviews digest",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/reviews_unanswered": {
      "post": {
        "operationId": "reviews_unanswered",
        "summary": "Reviews awaiting a developer reply",
        "description": "Lists the archived reviews that have NO developer response yet, newest first, with the ids the *_review_reply tools take (apple_review_reply / play_review_reply / ms_review_reply). Scope with app / store / rating bounds; `days` is anchored on the newest review IN SCOPE, not today, so a lagging store still surfaces its latest reviews. Max 200 rows (`limit`); `total` is the real match count and `truncated` says when the list was cut. Warehouse only — the reply tools work on exactly these rows.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/reviews_unanswered_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": "Reviews awaiting a developer reply",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/reviews_inbox": {
      "post": {
        "operationId": "reviews_inbox",
        "summary": "Reviews inbox, per app",
        "description": "Every app's reviews in one place: per app, a per-store summary (count, average rating, unanswered, unanswered <= 3 stars, newest review date) and the newest reviews, unanswered low ratings first, each with the reply tool that answers it (apple_review_reply / play_review_reply / ms_review_reply) and that store's character limit. `days` is anchored per (app, store) on that pair's newest review, not today. only_unanswered / max_rating narrow the listed reviews; the summary always covers the whole window. `limit` reviews per app (max 50), 200 rows across the response; `matching` is the real count. Warehouse only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/reviews_inbox_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": "Reviews inbox, per app",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/developer_channels": {
      "post": {
        "operationId": "developer_channels",
        "summary": "Developer channels (GitHub, npm, PyPI)",
        "description": "Where developers find each app: GitHub traffic on its PUBLIC SDK repos (views, unique visitors, clones per day, stars/forks, and the top REFERRERS and paths that sent the visits) plus npm and PyPI downloads of every package it ships, each over `days` anchored on that series' own newest day, with the window before. `fleet_referrers` sums every app's GitHub referrers by host. GitHub counts are exact; package downloads are proxies (CI and mirrors download too). Warehouse only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/developer_channels_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": "Developer channels (GitHub, npm, PyPI)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/search_performance": {
      "post": {
        "operationId": "search_performance",
        "summary": "Web search performance (Search Console, Bing)",
        "description": "Per app and engine (gsc = Google Search Console, bing = Bing Webmaster Tools): clicks, impressions, CTR and impression-weighted average position on the app's PUBLIC WEBSITE over `days`, anchored on that engine's own newest day, the same totals for the window before, and the top queries (and, for Google, pages) of the latest 28-day window. Web search, not the store listing: never add these to store impressions or downloads. Per-query Google rows omit anonymized queries (privacy_noised). Warehouse only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/search_performance_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": "Web search performance (Search Console, Bing)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/web_traffic": {
      "post": {
        "operationId": "web_traffic",
        "summary": "Website traffic (Google Analytics 4)",
        "description": "Per app: sessions, engaged sessions (and the engagement rate), new users, page views and key events on the app's PUBLIC WEBSITE from Google Analytics 4, summed over `days` anchored on that app's own newest GA4 day, the same sums for the window before, active users of the latest day (a daily unique count, never summed), and sessions per default channel group over the latest 28 days. Website traffic, not the store listing: never add these to store page views or downloads. Warehouse only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/web_traffic_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": "Website traffic (Google Analytics 4)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/product_usage": {
      "post": {
        "operationId": "product_usage",
        "summary": "Product usage (PostHog)",
        "description": "Per app: what people do IN the app, from PostHog: events, pageviews and sessions summed over `days` anchored on that app's own newest PostHog day, the same sums for the window before, active users of the latest day and the window's daily average (a daily unique count, never summed), the daily users for a trend, and the app's store downloads over the same length beside it so installs and usage read together. Warehouse only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/product_usage_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": "Product usage (PostHog)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/app_stability": {
      "post": {
        "operationId": "app_stability",
        "summary": "App stability (Sentry)",
        "description": "Per app: Sentry errors and release-health sessions summed over `days` anchored on that app's own newest Sentry day, the window before, the crash-free session rate computed from the session sums (never an average of daily rates), and errors per day. An app's Sentry projects are the ones named like it or `<app>-web`, `<app>-ios`... Warehouse only.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/app_stability_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 stability (Sentry)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/top_movers": {
      "post": {
        "operationId": "top_movers",
        "summary": "Top movers",
        "description": "Workspace-wide biggest ups and downs across app x store x metric: last N days vs the N days before, each series anchored on its own latest date. Series that stopped publishing for >10d are returned in stale_series rather than as -100% drops, and tiny cells are suppressed because Apple's privacy noise makes them meaningless.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/top_movers_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": "Top movers",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/app_health": {
      "post": {
        "operationId": "app_health",
        "summary": "App health",
        "description": "Sessions, crashes (+ computed crashes per 1k sessions with its basis), purchases (count only) and pre-orders per app x store over the last N days, from Apple's App Sessions / App Crashes / App Store Purchases / Pre-Orders reports. EVERY series is anchored on its own latest date and flagged stale on its own grain (daily >10d, weekly >17d) rather than read as a drop to zero. `_weekly` series are Apple's Monday-stamped weekly instances - for small apps the ONLY ones Apple emits. Retention is NOT available: Apple publishes no retention report, and the response says so.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/app_health_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 health",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/listing_proposals": {
      "post": {
        "operationId": "listing_proposals",
        "summary": "Listing proposals (data-guided)",
        "description": "THE optimization tool: deterministic, rule-based, evidence-backed changes to an app's store text and images, derived only from warehouse data. Rules: (1) Apple keyword-field waste — rebuild the 100-char field as a word bag with no spaces, no word repeated and nothing already in the name/subtitle; (2) unused title/subtitle/short-description/search-term budget; (3) rank gaps — terms the app ranks top-10 for on one storefront while another's scan finds it nowhere; (4) search traffic converting far below browse/referral traffic (creative problem); (5) thin screenshot/feature-graphic coverage; (6) recurring themes in low-star reviews the listing never answers, plus unanswered reviews; (7) a listing unchanged for 120+ days while downloads fell 20%+. Every proposal carries `evidence` (the rows it came from, each with its quality flag), a stable `id`, `current` vs `proposed`, and the EXACT `write_args` for its `write_tool` (apple_listing_update / play_listing_update / ms_listing_update / play_screenshot_upload (not in the cloud) / apple_cpp_create / manual) so it can be passed straight through. `needs_asset: true` means an image must be produced first. A rule whose data is absent is skipped and listed in `data_gaps`. Max 50 proposals per app.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/listing_proposals_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": "Listing proposals (data-guided)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/aso_audit": {
      "post": {
        "operationId": "aso_audit",
        "summary": "ASO audit (every app)",
        "description": "Workspace-wide triage over listing_proposals: one row per app x store x locale with its high/medium/low proposal counts, its downloads in the window, and the top 3 proposals as one-liners. Sorted by high-priority count then by download volume, so the first row is where a listing fix is both most needed and worth the most. Rows are capped at 200 (`truncated` says when there were more); totals always cover every audited listing. Call listing_proposals for the full evidence and write_args of any row.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/aso_audit_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": "ASO audit (every app)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/revenue_overview": {
      "post": {
        "operationId": "revenue_overview",
        "summary": "Revenue overview",
        "description": "Money for the last N days from the stores' own sales reports (warehouse only): proceeds, paid units, free units and refunds per app | store | country | currency. proceeds is the developer's net in the STORE'S PAYOUT CURRENCY and comes back as ONE LINE PER CURRENCY - it is never converted or totalled across currencies; units and refunds are counts. Every (store, app) money series is windowed on its OWN latest money date (series[]; quiet >10d = stale_series): Apple's daily Sales & Trends land ~1 day late, Play's earnings ~5 days after month end. `basis` names the exact report behind each store's numbers and every row carries its quality flags. Microsoft publishes no revenue through its analytics API and is absent.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/revenue_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": "Revenue overview",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/subscription_metrics": {
      "post": {
        "operationId": "subscription_metrics",
        "summary": "Subscription metrics",
        "description": "Apple auto-renewable subscriptions per app (warehouse only): active_subscriptions and active_free_trials as the LATEST daily snapshot (as_of; a stock summed over countries and plans, never over days), plus subscribers_new / subscribers_cancelled / subscription_renewals / subscription_refunds summed over the last N days on the series' own anchor, and the active stock per plan. Source: Sales & Trends SUBSCRIPTION (v1_4) and SUBSCRIPTION_EVENT (v1_4) daily reports. Play's reports bucket and Microsoft's API publish no subscription counts, so those stores are listed in stores_without_data.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/subscription_metrics_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": "Subscription metrics",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/warehouse_schema": {
      "post": {
        "operationId": "warehouse_schema",
        "summary": "Warehouse schema",
        "description": "DDL of every warehouse table plus the raw-metric -> canonical-family map and the known store ids. Read this before writing sql_query.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/warehouse_schema_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": "Warehouse schema",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/sql_query": {
      "post": {
        "operationId": "sql_query",
        "summary": "SQL query (read-only)",
        "description": "Escape hatch: run ONE read-only SELECT/WITH statement over the warehouse on a readonly connection (PRAGMA query_only). Max 500 rows, with a row-streaming time budget. Rejection is based on the statement's LEADING keyword (looking past any WITH ... AS (...) prefix), so a literal like '%update%' is fine while DELETE / PRAGMA / a CTE-hidden UPDATE is not. Only the warehouse tables listed by warehouse_schema are readable; any other table in the file (server internals), sqlite_master and PRAGMA are refused. Call warehouse_schema first.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/sql_query_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": "SQL query (read-only)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_reviews_recent": {
      "post": {
        "operationId": "apple_reviews_recent",
        "summary": "Apple: recent App Store reviews (warehouse)",
        "description": "Reads the most recent App Store reviews for an app from the warehouse (populated by the apple sync).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_reviews_recent_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: recent App Store reviews (warehouse)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/apple_review_reply": {
      "post": {
        "operationId": "apple_review_reply",
        "summary": "Apple: reply to an App Store review (gated)",
        "description": "Posts (or replaces) the developer response under one App Store review. Same write contract as apple_listing_update: the review and its current response are read from the warehouse (reviews table — sync the workspace first, find ids with reviews_unanswered / apple_reviews_recent), the change is recorded in listing_changes as `review_response:<review_id>`, and in preview the diff + API plan are returned and NOTHING is sent to Apple. In apply mode it POSTs /v1/customerReviewResponses (App Store Connect creates or REPLACES the one response a review can hold), re-reads it, and writes the confirmed text back to the warehouse row. Limit 5970 characters. Rejected when the review is not archived, the text is empty / over the limit, or it equals the response already posted.\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_review_reply_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: reply to an App Store review (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/apple_report_status": {
      "post": {
        "operationId": "apple_report_status",
        "summary": "Apple: analytics report pipeline status",
        "description": "Shows the app's analyticsReportRequests (including the stopped_due_to_inactivity flag — Apple stops an ONGOING request that is never downloaded), the report ids for the four ASO reports, and the latest DAILY/WEEKLY instance dates for each.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/apple_report_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: analytics report pipeline status",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/play_reviews_recent": {
      "post": {
        "operationId": "play_reviews_recent",
        "summary": "Play: recent reviews",
        "description": "Most recent Google Play reviews for an app from the warehouse (synced from the bucket's reviews/ exports). Play only exports reviews that carry text or a reply, so silent star ratings never appear here.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_reviews_recent_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: recent reviews",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/play_review_reply": {
      "post": {
        "operationId": "play_review_reply",
        "summary": "Play: reply to a Google Play review (gated)",
        "description": "Posts (or edits) the developer reply under one Google Play review. The review and its current reply are read from the warehouse (reviews table, synced from the bucket's reviews/ exports — find ids with reviews_unanswered / play_reviews_recent); the change is recorded in listing_changes as `review_response:<review_id>`; in dry-run the diff + API plan come back and NOTHING is sent to Google. In apply mode it calls reviews.reply (POST .../reviews/{reviewId}:reply — creates or replaces the one reply a review holds; HTML is stripped by Play) and writes the confirmed text back to the warehouse row. Limit 350 characters. Rejected when the review is not archived, its id is a synthetic `submit-…` placeholder from an export without review_link, the text is empty / over the limit, or it equals the reply already posted.\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_review_reply_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: reply to a Google Play review (gated)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/play_store_performance": {
      "post": {
        "operationId": "play_store_performance",
        "summary": "Play: store listing performance",
        "description": "Store-listing visitors, acquisitions and conversion rate by month × traffic source (and by territory) from the warehouse. Play acquisition data is MONTHLY and lags 3–7 days — every row is flagged quality='lagged'.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/play_store_performance_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: store listing performance",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/ms_analytics": {
      "post": {
        "operationId": "ms_analytics",
        "summary": "Microsoft Store analytics (warehouse)",
        "description": "Acquisitions and installs by day for an app from the warehouse, plus the legacy funnel totals and data freshness. Funnel rows are quality=legacy_untrusted: they come from the legacy Dev Center funnel and can disagree with the Partner Center portal — say so when you quote them.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_analytics_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 analytics (warehouse)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/ms_review_reply": {
      "post": {
        "operationId": "ms_review_reply",
        "summary": "Microsoft Store review response (gated write)",
        "description": "Posts the developer response under one Microsoft Store review through the Partner Center 'respond to reviews' API. The review and its current response are read from the warehouse (reviews table, synced from the Partner Center reviews endpoint — find ids with reviews_unanswered / ms_reviews_recent); the change is recorded in listing_changes as `review_response:<review_id>`; in dry-run the diff + API plan come back and NOTHING is sent to Partner Center. In apply mode it GETs .../responses/info (CanRespond + the listing's support e-mail), POSTs /v1.0/my/reviews/responses with IsPublic=true, and writes the text back to the warehouse row. Limit 1000 characters. Partner Center does NOT allow editing a posted response, so a review that already has one is rejected — as is a review that is not archived, an empty / over-limit text, or a missing support e-mail (the listing has none and support_email was not given).\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_review_reply_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 review response (gated write)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": true
        }
      }
    },
    "/api/tools/ms_reviews_recent": {
      "post": {
        "operationId": "ms_reviews_recent",
        "summary": "Microsoft Store recent reviews",
        "description": "Most recent Microsoft Store reviews for an app from the warehouse (synced from the Partner Center reviews endpoint).",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ms_reviews_recent_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 recent reviews",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/registry_status": {
      "post": {
        "operationId": "registry_status",
        "summary": "Public-registry listing + download proxies",
        "description": "Per app: whether the official MCP Registry lists it (and under which server name@version), plus the latest public download/usage proxies — npm, PyPI, Docker Hub, Smithery — from the warehouse. ALL numbers here are quality='proxy': they are package downloads and listing flags, NOT MCP tool calls. The workspace sync refreshes it.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/registry_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": "Public-registry listing + download proxies",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/rank_check": {
      "post": {
        "operationId": "rank_check",
        "summary": "Live keyword-rank check (calls the storefronts now)",
        "description": "LIVE one-off check: queries Apple (iTunes Search), Google Play (search page) and the Microsoft Store search endpoint RIGHT NOW and returns this app's 1-based position per keyword x country x store, plus the top-5 competitors ahead of it. 0 = not in the scanned top N (N is returned per row). Unlike the warehouse reads, this one calls the public storefronts now. Results are archived and upserted into the warehouse, so a check also feeds rank_history and saves the next sync the same request. Ceilings per call: 10 keywords, 5 countries, and 60 total store requests (stores x keywords x countries) - a call over budget is refused, not trimmed. Omit keywords to use the app's tracked keywords. quality=proxy: one unauthenticated, un-personalized vantage point, not what a given user sees.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/rank_check_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": "Live keyword-rank check (calls the storefronts now)",
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/store_search": {
      "post": {
        "operationId": "store_search",
        "summary": "Live storefront search (top results for a keyword, as the public store paints them)",
        "description": "LIVE: runs one keyword through the PUBLIC Apple App Store (iTunes Search) and/or Google Play search for one country and returns the top results IN ORDER with rank, app_id (Apple trackId / Play package), name, developer, rating, ratings_count, price, icon_url and is_fleet_app (the app slug when it is one of this workspace's apps). This is the same result list rank_check scores against, so the raw list is archived and a second call today (or the next sync) re-reads the archive instead of spending another request. Cost: exactly ONE storefront request per store leg per call, throttled (Apple 3 s); limit is at most 25 and over-limit is refused, not trimmed. Play paints no price and hides the rating on low-volume apps — absent = unknown. quality=proxy: one unauthenticated, un-personalized vantage point on the public storefront at one moment — not what any given user sees, and never summable. Absent values are UNKNOWN (the store did not paint them), not zero.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/store_search_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": "Live storefront search (top results for a keyword, as the public store paints them)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/store_app_lookup": {
      "post": {
        "operationId": "store_app_lookup",
        "summary": "Public listing facts for ONE app (competitor listing audit)",
        "description": "LIVE: reads one app's PUBLIC product listing — Apple via the iTunes Lookup API (app id or bundle id), Google Play via the product page — for one country and returns name, developer, rating, ratings_count, price, category, content rating, release/updated dates, version, the first 500 chars of the description (+ its full length), screenshot count, icon and store URL. Any app, not just yours (is_fleet_app names the slug when it is one of yours). ONE request, throttled, archived so a repeat today reads the archive. Play paints no version or first-release date and shows the rating only above a volume threshold — those come back null, never invented. quality=proxy: one unauthenticated, un-personalized vantage point on the public storefront at one moment — not what any given user sees, and never summable. Absent values are UNKNOWN (the store did not paint them), not zero.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/store_app_lookup_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": "Public listing facts for ONE app (competitor listing audit)",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/rank_history": {
      "post": {
        "operationId": "rank_history",
        "summary": "Keyword-rank history from the warehouse",
        "description": "Warehouse-only (no network): tracked keyword positions over the last N days per store leg x keyword x country, with latest / best / worst / change. rank 0 means the app was outside the scanned top N that day - `top_n` on the same row says how deep the scan went, so 0 is a real observation, not a gap. Lower is better, so `change` is (previous - latest): positive = moved UP the list. The workspace sync refreshes it; rank_check gives a live spot check.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/rank_history_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": "Keyword-rank history from the warehouse",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/rank_keywords_get": {
      "post": {
        "operationId": "rank_keywords_get",
        "summary": "Read the tracked keyword sets",
        "description": "The keyword sets the ranks adapter tracks, per app (the Keywords page). Also reports the request cost of the current configuration, since a sync is apps x keywords x countries x stores.",
        "tags": [
          "Read"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/rank_keywords_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": "Read the tracked keyword sets",
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true,
          "openWorldHint": false
        }
      }
    },
    "/api/tools/rank_keywords_set": {
      "post": {
        "operationId": "rank_keywords_set",
        "summary": "Replace one app's tracked keyword set",
        "description": "Replaces (not merges) the keywords and countries tracked for one app and saves the set. Keywords are lowercased, whitespace-collapsed and de-duped; countries must be ISO-3166 alpha-2. This edits the workspace's own keyword list only: it never touches a store listing. Passing an empty keywords[] stops tracking the app. A set that would take the sync past its query ceiling (max_queries_per_sync in rank_keywords_get) is refused, nothing written.\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/rank_keywords_set_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": "Replace one app's tracked keyword set",
          "readOnlyHint": false,
          "destructiveHint": true,
          "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://storescope.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://storescope.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."
          }
        }
      },
      "data_freshness_input": {
        "type": "object",
        "properties": {}
      },
      "fleet_overview_input": {
        "type": "object",
        "properties": {
          "days": {
            "default": 28,
            "description": "lookback window in days (default 28)",
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "includeEmpty": {
            "default": false,
            "description": "keep app x store rows that have nothing actionable in the window",
            "type": "boolean"
          }
        }
      },
      "app_funnel_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "minLength": 1,
            "description": "app slug (as on the Apps page)"
          },
          "store": {
            "type": "string",
            "minLength": 1,
            "description": "apple | play | microsoft | claude | chatgpt | copilot | registry"
          },
          "days": {
            "default": 28,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "groupBy": {
            "default": "source_type",
            "type": "string",
            "enum": [
              "source_type",
              "page_type",
              "territory",
              "device",
              "source_info"
            ]
          }
        },
        "required": [
          "app",
          "store"
        ]
      },
      "compare_periods_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "minLength": 1,
            "description": "app slug (as on the Apps page)"
          },
          "store": {
            "type": "string",
            "minLength": 1,
            "description": "apple | play | microsoft | claude | chatgpt | copilot | registry"
          },
          "metric": {
            "type": "string",
            "minLength": 1,
            "description": "raw or canonical metric, e.g. downloads_first_time | impressions | page_views | tool_calls"
          },
          "periodDays": {
            "default": 14,
            "type": "integer",
            "minimum": 1,
            "maximum": 180
          },
          "groupBy": {
            "type": "string",
            "enum": [
              "store",
              "source_type",
              "page_type",
              "territory",
              "device",
              "source_info"
            ]
          }
        },
        "required": [
          "app",
          "metric"
        ]
      },
      "listing_change_impact_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "minLength": 1,
            "description": "app slug (as on the Apps page)"
          },
          "store": {
            "type": "string",
            "minLength": 1,
            "description": "apple | play | microsoft | claude | chatgpt | copilot | registry"
          },
          "changeId": {
            "description": "a listing_changes.id; when given, only that change is analysed",
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991
          },
          "since": {
            "description": "ISO date/timestamp lower bound for events",
            "type": "string"
          },
          "windowDays": {
            "default": 14,
            "type": "integer",
            "minimum": 1,
            "maximum": 90
          }
        },
        "required": [
          "app",
          "store"
        ]
      },
      "listing_history_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "minLength": 1,
            "description": "app slug (as on the Apps page)"
          },
          "store": {
            "type": "string",
            "minLength": 1,
            "description": "apple | play | microsoft | claude | chatgpt | copilot | registry"
          },
          "locale": {
            "type": "string"
          }
        },
        "required": [
          "app",
          "store"
        ]
      },
      "reviews_digest_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "store": {
            "type": "string",
            "minLength": 1,
            "description": "apple | play | microsoft | claude | chatgpt | copilot | registry"
          },
          "days": {
            "default": 30,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "minRating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "maxRating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          }
        }
      },
      "reviews_unanswered_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "store": {
            "description": "apple | play | microsoft",
            "type": "string",
            "minLength": 1
          },
          "min_rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "max_rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "days": {
            "default": 30,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          }
        }
      },
      "reviews_inbox_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "store": {
            "description": "apple | play | microsoft",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 30,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "only_unanswered": {
            "default": false,
            "type": "boolean"
          },
          "max_rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "limit": {
            "default": 10,
            "description": "reviews listed per app",
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          }
        }
      },
      "developer_channels_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 28,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "top": {
            "default": 10,
            "description": "referrers / paths listed per app",
            "type": "integer",
            "minimum": 0,
            "maximum": 25
          }
        }
      },
      "search_performance_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "engine": {
            "type": "string",
            "enum": [
              "gsc",
              "bing"
            ]
          },
          "days": {
            "default": 28,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "top": {
            "default": 10,
            "description": "queries / pages listed per app and engine",
            "type": "integer",
            "minimum": 0,
            "maximum": 50
          }
        }
      },
      "web_traffic_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 28,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        }
      },
      "product_usage_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 28,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        }
      },
      "app_stability_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 28,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        }
      },
      "top_movers_input": {
        "type": "object",
        "properties": {
          "days": {
            "default": 7,
            "type": "integer",
            "minimum": 1,
            "maximum": 90
          },
          "limit": {
            "default": 10,
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          }
        }
      },
      "app_health_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 30,
            "description": "lookback window in days, per series (default 30)",
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        }
      },
      "listing_proposals_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "minLength": 1,
            "description": "app slug (as on the Apps page)"
          },
          "store": {
            "description": "apple | play | microsoft. Omit for every store this app is listed on.",
            "type": "string",
            "enum": [
              "apple",
              "play",
              "microsoft"
            ]
          },
          "locale": {
            "description": "exact listing locale, e.g. en-US (Apple/Play) or en-us (Microsoft). Omit for all locales.",
            "type": "string"
          },
          "days": {
            "default": 28,
            "description": "window for the conversion and download-trend rules (default 28)",
            "type": "integer",
            "minimum": 7,
            "maximum": 180
          }
        },
        "required": [
          "app"
        ]
      },
      "aso_audit_input": {
        "type": "object",
        "properties": {
          "days": {
            "default": 28,
            "description": "window for downloads and the trend rules (default 28)",
            "type": "integer",
            "minimum": 7,
            "maximum": 180
          }
        }
      },
      "revenue_overview_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 30,
            "description": "window in days on each series' own anchor (default 30)",
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "group_by": {
            "default": "app",
            "description": "app | store | country | currency",
            "type": "string",
            "enum": [
              "app",
              "store",
              "country",
              "currency"
            ]
          }
        }
      },
      "subscription_metrics_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug (as on the Apps page)",
            "type": "string",
            "minLength": 1
          },
          "days": {
            "default": 30,
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        }
      },
      "warehouse_schema_input": {
        "type": "object",
        "properties": {}
      },
      "sql_query_input": {
        "type": "object",
        "properties": {
          "sql": {
            "type": "string",
            "minLength": 1,
            "description": "a single SELECT or WITH statement, no trailing semicolon needed"
          }
        },
        "required": [
          "sql"
        ]
      },
      "apple_reviews_recent_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "limit": {
            "description": "default 20",
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          }
        },
        "required": [
          "app"
        ]
      },
      "apple_review_reply_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, Apple app id, or app name"
          },
          "review_id": {
            "type": "string",
            "minLength": 1,
            "description": "customerReviews resource id, as returned by reviews_unanswered / apple_reviews_recent"
          },
          "response_text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 5970,
            "description": "the public developer response, max 5970 chars"
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why this reply — 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",
          "review_id",
          "response_text",
          "rationale"
        ]
      },
      "apple_report_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          }
        },
        "required": [
          "app"
        ]
      },
      "play_reviews_recent_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "limit": {
            "description": "default 20",
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          }
        },
        "required": [
          "app"
        ]
      },
      "play_review_reply_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug or Play package name"
          },
          "review_id": {
            "type": "string",
            "minLength": 1,
            "description": "Play review id (gp:…), as returned by reviews_unanswered / play_reviews_recent"
          },
          "response_text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 350,
            "description": "the public developer reply, max 350 chars"
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why this reply — 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",
          "review_id",
          "response_text",
          "rationale"
        ]
      },
      "play_store_performance_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "months": {
            "description": "how many recent months to include, default 3",
            "type": "integer",
            "minimum": 1,
            "maximum": 24
          }
        },
        "required": [
          "app"
        ]
      },
      "ms_analytics_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "days": {
            "description": "lookback window, default 30",
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        },
        "required": [
          "app"
        ]
      },
      "ms_review_reply_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug, app name, or Microsoft product id"
          },
          "review_id": {
            "type": "string",
            "minLength": 1,
            "description": "Partner Center review id (a GUID), as returned by reviews_unanswered / ms_reviews_recent"
          },
          "response_text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "the public developer response, max 1000 chars"
          },
          "rationale": {
            "type": "string",
            "minLength": 1,
            "description": "why this response — stored in the listing_changes audit log"
          },
          "support_email": {
            "description": "support address Partner Center requires on every response; defaults to the listing's DefaultSupportEmail",
            "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,}$"
          },
          "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",
          "review_id",
          "response_text",
          "rationale"
        ]
      },
      "ms_reviews_recent_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string"
          },
          "limit": {
            "description": "default 20",
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          }
        },
        "required": [
          "app"
        ]
      },
      "registry_status_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug. Omit for every app.",
            "type": "string"
          }
        }
      },
      "rank_check_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug"
          },
          "keywords": {
            "description": "up to 10 search terms. Default: this app's tracked keywords.",
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "countries": {
            "description": "ISO-3166 alpha-2, e.g. ['us','ca']. Default: this app's configured countries.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "stores": {
            "description": "which store legs to query. Default: every store the app is actually listed on.",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "apple",
                "play",
                "ms"
              ]
            }
          }
        },
        "required": [
          "app"
        ]
      },
      "store_search_input": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "the search term, exactly as a user would type it"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "description": "ISO-3166 alpha-2 storefront, e.g. 'us'"
          },
          "store": {
            "description": "apple | play | both (default both)",
            "type": "string",
            "enum": [
              "apple",
              "play",
              "both"
            ]
          },
          "limit": {
            "description": "top N per store, default 10, max 25",
            "type": "integer",
            "minimum": 1,
            "maximum": 25
          }
        },
        "required": [
          "keyword",
          "country"
        ]
      },
      "store_app_lookup_input": {
        "type": "object",
        "properties": {
          "store": {
            "type": "string",
            "enum": [
              "apple",
              "play"
            ],
            "description": "apple | play"
          },
          "id_or_package": {
            "type": "string",
            "minLength": 1,
            "description": "Apple numeric app id (or bundle id), or the Play package name"
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "description": "ISO-3166 alpha-2 storefront, e.g. 'us'"
          }
        },
        "required": [
          "store",
          "id_or_package",
          "country"
        ]
      },
      "rank_history_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug"
          },
          "keyword": {
            "description": "single keyword; omit for all tracked keywords",
            "type": "string"
          },
          "store": {
            "description": "apple | play | ms; omit for all",
            "type": "string",
            "enum": [
              "apple",
              "play",
              "ms"
            ]
          },
          "days": {
            "description": "lookback window in days (default 30)",
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          }
        },
        "required": [
          "app"
        ]
      },
      "rank_keywords_get_input": {
        "type": "object",
        "properties": {
          "app": {
            "description": "app slug; omit for every app",
            "type": "string"
          }
        }
      },
      "rank_keywords_set_input": {
        "type": "object",
        "properties": {
          "app": {
            "type": "string",
            "description": "app slug"
          },
          "keywords": {
            "maxItems": 25,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 100
            },
            "description": "full replacement set, at most 25; [] removes the app from tracking"
          },
          "countries": {
            "description": "ISO-3166 alpha-2, at most 10; defaults to us/ca/gb",
            "maxItems": 10,
            "type": "array",
            "items": {
              "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",
          "keywords"
        ]
      }
    },
    "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"
            }
          }
        }
      }
    }
  }
}