Files
ombi-mcp/docs/schema/04-results.md
gronod 80e01253a2
Build and publish / Test and build (linux) (pull_request) Canceled after 0s
Build and publish / Test and build (windows) (pull_request) Canceled after 0s
Build and publish / Build and publish Docker image (pull_request) Canceled after 0s
Build and publish / Test and build (darwin) (pull_request) Canceled after 40s
Build and publish / Test and build (darwin) (push) Successful in 2m9s
Build and publish / Test and build (linux) (push) Successful in 2m34s
Build and publish / Test and build (windows) (push) Successful in 3m11s
Build and publish / Build and publish Docker image (push) Successful in 1m55s
Fix #10, #27 and #28 from the read-tools sweep
v2 request lists sent the RAML example sort field requestDate; Ombi
looks up RequestedDate and NullReferenceException'd every non-empty
page. Browse now streams TV popular/most-watched payloads and skips
the hydrated seasonRequests graph that blew the 8 MiB read budget.
provider_summary treats an empty upstream body as an empty group_page.
2026-09-19 20:59:57 +01:00

39 KiB
Raw Permalink Blame History

Output contracts and MCP behaviour

Registration and results

Publish each tool with its name, description, complete inputSchema, its own outputSchema composed per the rules below, and the annotations in the input catalogue. tools/list advertises outputSchema on each Tool object per MCP 2025-11-25; output contracts never appear inside description. operation is a per-tool const carrying the tool name, not an upstream route. Each tool's output schema permits only its allowed result families and embeds exactly the transitive closure of the shared $defs library it references, keeping tool-list size bounded.

Use structuredContent for the result object and serialize the same bounded object into a text content block for compatibility. Set MCP isError to the inverse of ok; isError is a CallToolResult field, not an Ombi business-result field. JSON-RPC errors describe malformed protocol requests or unknown tools; tool argument/domain/upstream failures are actionable tool errors. This design adopts the MCP tools result contract.

The annotations are advisory metadata, never access control. readOnlyHint describes effects, not HTTP verbs; idempotentHint describes repeated side effects, not response equality. Destructive mutations conservatively advertise true even when some branches are additive. All writes advertise idempotent false until effects are verified. See ToolAnnotations.

Declare tools capability; declare listChanged only if tool-list changes are actually notified. MCP tools/list cursor pagination is separate from these tools' media offset pagination. Do not advertise tasks, subscriptions, progress or cancellation guarantees that are not implemented. Cancellation cannot undo a submitted Ombi mutation. A submitted job does not imply a trackable MCP Task or completed media download.

Domain projections

Each per-tool output schema is intentionally a bounded projection, not the recursive upstream entity graph. Missing optional upstream properties stay absent. Never convert missing availability, quota, IDs or counts into false/zero. Integer IDs are emitted only when known and positive; optional enum fields preserve documented numeric values. Unknown provider semantics use provider_unknown and cannot feed a provider-specific write automatically.

Family Projection rules
media_page Search/discovery/details return zero or more normalized media records. Details usually has one item. Keep every known ID namespace; never emit the same namespace+value twice. On TV the upstream theMovieDbId field name lies: TVMaze-backed v1 routes (Search/tv/{term}, Search/tv/info/{tvdbId}) carry the TVDB id there (emit tvdb, plus seriesId→tvmaze); TMDB-keyed v2 routes carry the TMDB id (emit tmdb) and must not label seriesId as tvmaze — on those routes it echoes the TMDB id. When theMovieDbId is absent, id is labelled with the same origin namespace (v2 browse/collection members and MovieFullInfoViewModel). belongsToCollection.id is the collection's TMDB id, not the movie's. Multi-search mediaType is matched case-insensitively (Artist→artist/musicbrainz). The namespace label is per origin route, never per field name. Credit calls require the caller-supplied person name because Ombi returns only the person ID; TV credit titles are enriched from their TMDB detail records. If requested browse falls back to Ombi's bounded recently-requested feed, mark it truncated and leave total/continuation unknown. Browse streams the upstream array and discards seasonRequests (Ombi hydrates full episode trees on popular/most-watched when hiding available titles). Map cast/crew into credits; title-specific streaming into providers; rating fields into named rating references. Never claim a global provider catalogue is a title's availability. Collections keep their own collection identity and returned members.
request_page Map the Ombi request id to target kind and ID: prefer requestId over id. v2 TV list items are children and parentRequestId is preserved; v1 parent records stay parents; child provider ids live on the embedded parentRequest record. On recent, RecentlyRequestedModel.requestId is the request id for movie/album, but on TV rows it is a child request id (upstream builds recent TV rows from child requests and persists the provider id as the child PK for new-request children). The tv_parent target id is therefore resolved through a bounded v1 parent scan — child-id match first, then tvDbId/externalProviderId fallback — the child id is emitted as an ombi_tv_child identifier, provider values land in identifiers (mediaId, tvDbId, externalProviderId), and unresolvable rows emit target.id 0 with a warning. A provider-shaped value is never the target. Include standard and 4K state separately. Never infer one combined lifecycle status when booleans disagree.
issue_page Project writable/display fields plus IDs/timestamps. Wire resovledDate maps to resolved_date without changing upstream spelling. Omit nested user objects and comments unless requested separately.
group_page v2 issue summaries are provider groups. Count and page units describe groups, not individual issues. Truncate nested issues with a warning. An empty, null, or [] body from provider_summary is an empty page, not a schema mismatch.
comment_page Preserve comment text and authorized author identifier, omit full user graph.
vote_page Preserve numeric VoteType until verified; totals only if supplied or completely computed from an authorized complete set. No inference of the caller's own vote without identity evidence.
user_page Only ID, username/alias and non-secret language/country/online state. Permission-limited user visibility applies even to nested source objects.
reference_page Named ID/value records for genres, language/country lists, categories, claims, integration options, features and safe metadata. Names/IDs need verified upstream adapters. value holds the native-typed identifier (e.g. integer or string) matching the resolved ID, falling back to name string when no ID matched; selection is deterministic across calls. Upstream corrupted labels (all-? mojibake or U+FFFD) are skipped for clean candidate keys; if only corrupted labels exist, the corrupted string is emitted and a warnings[] note is recorded. Unspecified response shapes fail explicitly instead of dumping raw data.
metrics Counts/quota/health/stats become named scalar values with an explicit instance/principal/selected-user scope. RequestQuota fields are has_limit, limit, remaining and next_request. Server request counts are pending, approved, available and denied. Do not equate false hasLimit with remaining=0.
settings Only allowlisted non-secret fields; nested settings are flattened using escaped JSON Pointer names, one scalar leaf per entry. Array indices are display paths only, never mutation identifiers. revision appears only when a safe corresponding patch can be offered. No credentials or destination URLs.
calendar_page Known date/title/episode/ID values, no invented time zone or promised date-range filtering.
artwork_page Validated resource URIs/content types. No credential-bearing URLs, raw HTML or executable content.
retry_page queue_id is distinct from request_id. Queue removal uses only the former.
logs Vetted opaque file identifiers and bounded sanitized lines, never raw download bytes.
mutation Report completed, accepted, partial, rejected or unknown honestly. Include a newly created ID only if returned or authoritatively resolved. HTTP success without a response body is not permission to invent an ID, affected count or per-item collection outcome.

Reference/metric names are adapter allowlists derived from the specific response type in the endpoint ledger. They are not arbitrary raw properties. For example stats can expose its seven numeric totals but not entire mostRequestedUserMovie or mostRequestedUserTv objects. Preference read exposes enabled agent labels/codes, never delivery-token value strings. Enum label gaps remain verification items. When an upstream enum integer has no verified label mapping, the read side degrades gracefully: it emits the raw *_code integer, omits the label, and records the gap in warnings[] rather than failing the call.

Errors and partial effects

An HTTP 200 carrying RequestEngineResult.isError=true, result=false, an IdentityResult failure or a tester Boolean false is a tool failure. Read the endpoint-specific result shape; do not treat a legitimate status Boolean false as an HTTP transport failure. Preserve a sanitized business error code/message when present. Missing/conflicting business-result fields produce a schema/unknown-outcome error as appropriate, not fabricated success.

Only mark timeout/429 errors retryable where replay is safe; a timeout after sending a mutation is UNKNOWN_OUTCOME with retryable false. Reconcile using reads before a user chooses a retry. Sanitize ProblemDetails; never forward extensions, stack traces, HTML error pages, connection strings or credential headers. Partial compound reads or collection writes return ok=false, PARTIAL_FAILURE and available data with per-item outcomes only if actually known. Never claim rollback or transactional behaviour.

Use a bounded request deadline and a design response budget of 64 KiB serialized structured data, 100 records, 100 nested records per array and 500 log lines. Reduce records/long text as needed to meet the byte budget and emit warnings/truncated. A complete source string may be longer than its returned projection. Do not duplicate unlimited data in both text and structured content. Oversized upstream data must be bounded while reading, before parsing/normalizing an unbounded entity graph.

For unpaged upstream arrays, total is known only after receiving the complete authorized collection. For bounded/truncated scans use null total, null has_more when uncertain and null next_offset if no reliable continuation exists. Return the requested window when possible; do not scan an unbounded legacy list to guarantee a missing album detail. Upstream pagination has no snapshot guarantee: warn that concurrent changes may shift offsets.

Resources

Optional resources can provide cached reference catalogues and sanitized artwork/log excerpts. Suggested server-owned URIs use ombi://reference/..., ombi://artwork/... and ombi://logs/...; these are design identifiers, not upstream HTTP URLs. Enforce the same authorization when resources are read, apply expiry and size limits, and never encode a secret in the URI. A tool can return a resource_link without implying the link appears in resources/list. Provide a bounded text/structured fallback to clients that do not consume resources. See MCP resources.

Shared $defs library

Every definition shared by the per-tool output schemas. Each tool's outputSchema embeds only the transitive closure of the definitions it references — the error def plus its allowed data families and every def reachable by following $refs inside them. Runtime projection rules above impose semantic requirements beyond JSON types.

{
  "$defs": {
    "target": {
      "type": "object",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "movie",
            "tv_parent",
            "tv_child",
            "album"
          ]
        },
        "id": {
          "type": "integer",
          "minimum": 1
        }
      },
      "required": [
        "kind",
        "id"
      ],
      "additionalProperties": false
    },
    "identifier": {
      "type": "object",
      "properties": {
        "namespace": {
          "type": "string",
          "enum": [
            "tmdb",
            "tvdb",
            "imdb",
            "musicbrainz",
            "tvmaze",
            "ombi_movie",
            "ombi_tv_parent",
            "ombi_tv_child",
            "ombi_album",
            "provider_unknown"
          ]
        },
        "value": {
          "type": "string"
        }
      },
      "required": [
        "namespace",
        "value"
      ],
      "additionalProperties": false
    },
    "reference": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "value": {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ]
        },
        "category": {
          "type": "string"
        }
      },
      "required": [
        "name"
      ],
      "additionalProperties": false
    },
    "credit": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "person_id": {
          "type": "integer",
          "minimum": 1
        },
        "role": {
          "type": "string"
        },
        "department": {
          "type": "string"
        }
      },
      "required": [
        "name"
      ],
      "additionalProperties": false
    },
    "provider": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "country": {
          "type": "string"
        },
        "access": {
          "type": "string"
        },
        "url": {
          "type": "string"
        }
      },
      "required": [
        "name"
      ],
      "additionalProperties": false
    },
    "episode": {
      "type": "object",
      "properties": {
        "episode_number": {
          "type": "integer",
          "minimum": 0
        },
        "title": {
          "type": "string"
        },
        "requested": {
          "type": "boolean"
        },
        "available": {
          "type": "boolean"
        }
      },
      "required": [
        "episode_number"
      ],
      "additionalProperties": false
    },
    "season": {
      "type": "object",
      "properties": {
        "season_number": {
          "type": "integer",
          "minimum": 0
        },
        "episodes": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/episode"
          },
          "maxItems": 100
        },
        "truncated": {
          "type": "boolean"
        }
      },
      "required": [
        "season_number",
        "episodes",
        "truncated"
      ],
      "additionalProperties": false
    },
    "media": {
      "type": "object",
      "properties": {
        "media": {
          "type": "string",
          "enum": [
            "movie",
            "tv",
            "artist",
            "album",
            "person",
            "collection",
            "unknown"
          ]
        },
        "identifiers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/identifier"
          },
          "maxItems": 100
        },
        "title": {
          "type": "string"
        },
        "overview": {
          "type": "string"
        },
        "year": {
          "type": "integer"
        },
        "available": {
          "type": "boolean"
        },
        "requested": {
          "type": "boolean"
        },
        "request_targets": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/target"
          },
          "maxItems": 100
        },
        "genres": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "maxItems": 100
        },
        "credits": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/credit"
          },
          "maxItems": 100
        },
        "providers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/provider"
          },
          "maxItems": 100
        },
        "ratings": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/reference"
          },
          "maxItems": 100
        },
        "seasons": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/season"
          },
          "maxItems": 100
        },
        "artwork_uris": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "maxItems": 100
        }
      },
      "required": [
        "media",
        "identifiers"
      ],
      "additionalProperties": false
    },
    "request": {
      "type": "object",
      "properties": {
        "target": {
          "$ref": "#/$defs/target"
        },
        "parent_request_id": {
          "type": "integer",
          "minimum": 1
        },
        "title": {
          "type": "string"
        },
        "identifiers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/identifier"
          },
          "maxItems": 100
        },
        "approved": {
          "type": "boolean"
        },
        "available": {
          "type": "boolean"
        },
        "denied": {
          "type": "boolean"
        },
        "status_text": {
          "type": "string"
        },
        "requested_date": {
          "type": "string"
        },
        "requested_user_id": {
          "type": "string"
        },
        "denied_reason": {
          "type": "string"
        },
        "is_4k": {
          "type": "boolean"
        },
        "approved_4k": {
          "type": "boolean"
        },
        "available_4k": {
          "type": "boolean"
        },
        "denied_4k": {
          "type": "boolean"
        },
        "subscribed": {
          "type": "boolean"
        },
        "can_approve": {
          "type": "boolean"
        },
        "seasons": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/season"
          },
          "maxItems": 100
        }
      },
      "required": [
        "target"
      ],
      "additionalProperties": false
    },
    "issue": {
      "type": "object",
      "properties": {
        "id": {
          "type": "integer",
          "minimum": 1
        },
        "title": {
          "type": "string"
        },
        "subject": {
          "type": "string"
        },
        "description": {
          "type": "string"
        },
        "category_id": {
          "type": "integer",
          "minimum": 1
        },
        "status": {
          "type": "string",
          "enum": [
            "pending",
            "in_progress",
            "resolved",
            "closed"
          ]
        },
        "status_code": {
          "type": "integer",
          "enum": [
            0,
            1,
            2,
            3
          ]
        },
        "request_type": {
          "type": "string",
          "enum": [
            "movie",
            "tv",
            "album"
          ]
        },
        "request_type_code": {
          "type": "integer",
          "enum": [
            0,
            1,
            2
          ]
        },
        "request_id": {
          "type": "integer",
          "minimum": 1
        },
        "provider_id": {
          "type": "string"
        },
        "created_date": {
          "type": "string"
        },
        "resolved_date": {
          "type": "string"
        },
        "reported_by_user_id": {
          "type": "string"
        }
      },
      "required": [
        "id"
      ],
      "additionalProperties": false
    },
    "comment": {
      "type": "object",
      "properties": {
        "id": {
          "type": "integer",
          "minimum": 1
        },
        "issue_id": {
          "type": "integer",
          "minimum": 1
        },
        "comment": {
          "type": "string"
        },
        "author_id": {
          "type": "string"
        },
        "created_date": {
          "type": "string"
        }
      },
      "required": [
        "comment"
      ],
      "additionalProperties": false
    },
    "group": {
      "type": "object",
      "properties": {
        "provider_id": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "count": {
          "type": "integer",
          "minimum": 0
        },
        "issues": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/issue"
          },
          "maxItems": 100
        }
      },
      "required": [],
      "additionalProperties": false
    },
    "vote": {
      "type": "object",
      "properties": {
        "request_id": {
          "type": "integer",
          "minimum": 1
        },
        "media": {
          "type": "string",
          "enum": [
            "movie",
            "tv",
            "album"
          ]
        },
        "user_id": {
          "type": "string"
        },
        "vote_code": {
          "type": "integer",
          "enum": [
            0,
            1
          ]
        },
        "up": {
          "type": "integer",
          "minimum": 0
        },
        "down": {
          "type": "integer",
          "minimum": 0
        }
      },
      "required": [],
      "additionalProperties": false
    },
    "user": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        },
        "user_name": {
          "type": "string"
        },
        "alias": {
          "type": "string"
        },
        "language": {
          "type": "string"
        },
        "streaming_country": {
          "type": "string"
        },
        "online": {
          "type": "boolean"
        }
      },
      "required": [
        "id"
      ],
      "additionalProperties": false
    },
    "calendar": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string"
        },
        "date": {
          "type": "string"
        },
        "media": {
          "type": "string",
          "enum": [
            "movie",
            "tv",
            "unknown"
          ]
        },
        "identifiers": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/identifier"
          },
          "maxItems": 100
        },
        "season_number": {
          "type": "integer",
          "minimum": 0
        },
        "episode_number": {
          "type": "integer",
          "minimum": 0
        },
        "available": {
          "type": "boolean"
        }
      },
      "required": [],
      "additionalProperties": false
    },
    "artwork": {
      "type": "object",
      "properties": {
        "uri": {
          "type": "string"
        },
        "mime_type": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "kind": {
          "type": "string"
        }
      },
      "required": [
        "uri",
        "name"
      ],
      "additionalProperties": false
    },
    "retry": {
      "type": "object",
      "properties": {
        "queue_id": {
          "type": "integer",
          "minimum": 1
        },
        "request_id": {
          "type": "integer",
          "minimum": 1
        },
        "request_type": {
          "type": "string",
          "enum": [
            "movie",
            "tv",
            "album"
          ]
        },
        "request_type_code": {
          "type": "integer",
          "enum": [
            0,
            1,
            2
          ]
        },
        "title": {
          "type": "string"
        },
        "reason": {
          "type": "string"
        }
      },
      "required": [
        "queue_id"
      ],
      "additionalProperties": false
    },
    "paging": {
      "type": "object",
      "properties": {
        "offset": {
          "type": "integer",
          "minimum": 0
        },
        "limit": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        },
        "returned": {
          "type": "integer",
          "minimum": 0
        },
        "total": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "has_more": {
          "type": [
            "boolean",
            "null"
          ]
        },
        "next_offset": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "mode": {
          "type": "string",
          "enum": [
            "upstream",
            "local",
            "none"
          ]
        },
        "unit": {
          "type": "string",
          "enum": [
            "media",
            "requests",
            "issues",
            "provider_groups",
            "comments",
            "users",
            "references",
            "votes",
            "calendar_entries",
            "artwork",
            "queue_entries"
          ]
        }
      },
      "required": [
        "offset",
        "limit",
        "returned",
        "total",
        "has_more",
        "next_offset",
        "mode",
        "unit"
      ],
      "additionalProperties": false
    },
    "media_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "media_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/media"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "request_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "request_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/request"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "issue_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "issue_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/issue"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "comment_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "comment_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/comment"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "group_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "group_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/group"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "vote_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "vote_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/vote"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "user_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "user_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/user"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "calendar_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "calendar_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/calendar"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "artwork_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "artwork_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/artwork"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "retry_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "retry_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/retry"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "reference_page": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "reference_page"
        },
        "items": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/reference"
          },
          "maxItems": 100
        },
        "page": {
          "$ref": "#/$defs/paging"
        }
      },
      "required": [
        "kind",
        "items",
        "page"
      ],
      "additionalProperties": false
    },
    "metric": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "value": {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ]
        },
        "scope": {
          "type": "string",
          "enum": [
            "instance",
            "principal",
            "selected_user",
            "unknown"
          ]
        },
        "unit": {
          "type": "string"
        }
      },
      "required": [
        "name",
        "value",
        "scope"
      ],
      "additionalProperties": false
    },
    "metrics": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "metrics"
        },
        "values": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/metric"
          },
          "maxItems": 100
        }
      },
      "required": [
        "kind",
        "values"
      ],
      "additionalProperties": false
    },
    "change": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "value": {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ]
        }
      },
      "required": [
        "name",
        "value"
      ],
      "additionalProperties": false
    },
    "settings": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "settings"
        },
        "section": {
          "type": "string"
        },
        "revision": {
          "type": "string"
        },
        "values": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/change"
          },
          "maxItems": 100
        },
        "omitted_fields": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "maxItems": 100
        }
      },
      "required": [
        "kind",
        "section",
        "values",
        "omitted_fields"
      ],
      "additionalProperties": false
    },
    "mutation": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "mutation"
        },
        "outcome": {
          "type": "string",
          "enum": [
            "completed",
            "accepted",
            "partial",
            "unknown",
            "rejected"
          ]
        },
        "target": {
          "$ref": "#/$defs/target"
        },
        "request_id": {
          "type": "integer",
          "minimum": 1
        },
        "issue_id": {
          "type": "integer",
          "minimum": 1
        },
        "comment_id": {
          "type": "integer",
          "minimum": 1
        },
        "category_id": {
          "type": "integer",
          "minimum": 1
        },
        "user_id": {
          "type": "string"
        },
        "queue_id": {
          "type": "integer",
          "minimum": 1
        },
        "message": {
          "type": "string"
        },
        "upstream_result": {
          "type": "boolean"
        },
        "upstream_is_error": {
          "type": "boolean"
        },
        "upstream_error_code": {
          "type": "integer"
        },
        "affected_count": {
          "type": "integer",
          "minimum": 0
        },
        "item_results": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "identifier": {
                "type": "string"
              },
              "outcome": {
                "type": "string",
                "enum": [
                  "completed",
                  "accepted",
                  "unknown",
                  "rejected"
                ]
              },
              "message": {
                "type": "string"
              }
            },
            "required": [
              "identifier",
              "outcome"
            ],
            "additionalProperties": false
          },
          "maxItems": 100
        }
      },
      "required": [
        "kind",
        "outcome"
      ],
      "additionalProperties": false
    },
    "log_file": {
      "type": "object",
      "properties": {
        "file_id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        }
      },
      "required": [
        "file_id",
        "name"
      ],
      "additionalProperties": false
    },
    "logs": {
      "type": "object",
      "properties": {
        "kind": {
          "const": "logs"
        },
        "files": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/log_file"
          },
          "maxItems": 100
        },
        "lines": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "maxItems": 500
        },
        "offset": {
          "type": "integer",
          "minimum": 0
        },
        "next_offset": {
          "type": [
            "integer",
            "null"
          ],
          "minimum": 0
        },
        "truncated": {
          "type": "boolean"
        }
      },
      "required": [
        "kind",
        "truncated"
      ],
      "additionalProperties": false
    },
    "error": {
      "type": "object",
      "properties": {
        "code": {
          "type": "string",
          "enum": [
            "INVALID_ARGUMENT",
            "AUTHENTICATION_FAILED",
            "FORBIDDEN",
            "NOT_FOUND",
            "UNSUPPORTED_CAPABILITY",
            "UPSTREAM_REJECTED",
            "RATE_LIMITED",
            "TIMEOUT",
            "UNKNOWN_OUTCOME",
            "CONFLICT",
            "PARTIAL_FAILURE",
            "UPSTREAM_SCHEMA_MISMATCH",
            "INTERNAL_ERROR"
          ]
        },
        "message": {
          "type": "string"
        },
        "retryable": {
          "type": "boolean"
        },
        "http_status": {
          "type": "integer",
          "minimum": 100,
          "maximum": 599
        },
        "field": {
          "type": "string"
        },
        "retry_after_seconds": {
          "type": "integer",
          "minimum": 0
        }
      },
      "required": [
        "code",
        "message",
        "retryable"
      ],
      "additionalProperties": false
    }
  }
}

Envelope template (per-tool outputSchema)

Each Tool object declares a distinct outputSchema of this form. operation is the tool name as a const; data.oneOf lists only that tool's allowed families; $defs is the transitive closure of the shared library for that tool.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "ok": { "type": "boolean" },
    "operation": { "const": "<tool_name>" },
    "data": { "oneOf": [ "<$ref to each allowed family>" ] },
    "error": { "$ref": "#/$defs/error" },
    "warnings": {
      "type": "array",
      "items": { "type": "string" },
      "maxItems": 100
    },
    "truncated": { "type": "boolean" },
    "correlation_id": { "type": "string" }
  },
  "required": ["ok", "operation", "warnings", "truncated", "correlation_id"],
  "additionalProperties": false,
  "allOf": [
    {
      "if": { "properties": { "ok": { "const": true } } },
      "then": { "required": ["data"], "not": { "required": ["error"] } },
      "else": { "required": ["error"] }
    }
  ],
  "$defs": { "<transitive closure of the shared library for this tool>" }
}

Tool → family matrix

Tool Allowed data families
read_search media_page
read_discover media_page
read_media media_page
read_reference reference_page
read_requests request_page, retry_page
read_request_stats metrics
read_issues issue_page, comment_page, group_page, metrics
read_votes vote_page
read_users user_page, reference_page
read_library media_page, calendar_page, artwork_page
read_server metrics, reference_page
read_integration reference_page, user_page
read_settings settings
read_logs logs
write_request_create … all 17 write_* mutation

Composition rule

For each tool: set operation.const to the tool name; set data.oneOf to its allowed families per the matrix; compute $defs = {error} ∪ the allowed families ∪ every def reachable by following $refs inside them (e.g. media_page → media → identifier, target, credit, provider, reference, season, episode; request_page → request → target, identifier, season, episode, paging). tools/list advertises outputSchema on each Tool object per MCP 2025-11-25; output contracts never appear inside description.