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
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.
1387 lines
39 KiB
Markdown
1387 lines
39 KiB
Markdown
# 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](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
|
||
|
||
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](https://modelcontextprotocol.io/specification/2025-11-25/schema#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](https://modelcontextprotocol.io/specification/2025-11-25/server/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 `$ref`s inside them. Runtime projection rules above impose semantic requirements beyond JSON types.
|
||
|
||
```json
|
||
{
|
||
"$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.
|
||
|
||
```json
|
||
{
|
||
"$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 `$ref`s 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`.
|