Schema docs (docs/schema) cover tool mapping, input/output contracts and endpoint coverage for all 31 MCP tools; megaplan M1 phases 01-03 applied (namespace rename to read_*/write_*, input schema rectification). Go module provides config loading, Ombi client, and tool registry skeleton for implementation.
18 KiB
18 KiB
Phase 06 — Specification Harmonisation & Go Interface Baseline
Objective
Synchronise all remaining documents to the new names, auth model, and schemas; publish the MCP→wire name-mapping appendix; land the Go baseline.
Prerequisites
- Phases 01–05 complete: names realigned (01), auth rewritten (02), input schemas rectified (03), enum maps published (04), output schemas modularised (05).
- Locked decision D3: snake_case MCP names kept; the Go adapter translates to wire names — this phase mandates the explicit documented mapping layer (Appendix A in
02-tool-mapping.md).
Specification
6.1 02-tool-mapping.md edits
- Rename all
ombi_*references (Phase 01) including inline mentions like "ombi_search.text", "ombi_media.details", "ombi_request_create". - Update prose: shared routing rule #4 — replace "Numeric-code inputs are deliberately retained… Publish a verified label map later" with the Phase 04 label maps (now published; remaining numeric enums listed).
- Update request-create routing table:
is_4know optional-default-false; addseasonselection row (season_numbers[]→ adapter episode expansion →seasons[].episodes). - Update moderation paragraph: movie
is_4koptional. - Update
read_requestsparagraph:statusdefaultsall;sort_directionmaps to{sortOrder}. - Append new "Appendix A — Parameter name translation" table (the mandated mapping layer):
| MCP field | Context | Wire form |
|---|---|---|
query |
search/requests/issues keywords | {searchTerm} path or searchTerm body/query |
person_id |
read_discover.credits |
{actorId} path |
artist_id |
read_discover.artist_albums |
{foreignArtistId} path |
collection_id |
read_discover.collection, write_request_create.collection |
{collectionId} path |
tmdb_id |
read_media, write_request_create.movie |
theMovieDbId body / {movieDbId}/{theMovieDbId} path |
tvdb_id |
read_library.tv_images |
{tvdbid} path |
musicbrainz_id |
write_request_create.album, read_library.album_art |
foreignAlbumId body / {musicBrainzId} path |
request_id |
many tools | {requestId}/{id} path or requestId/id body (per ledger) |
parent_request_id |
read_requests.children, write_request_options.tv_* |
{parentRequestId}/{requestId} path (verify per route) |
issue_id |
read_issues.comments, write_issue_manage, write_issue_comment |
{id} path / issueId body |
comment_id |
write_issue_manage.delete_comment |
{id} path |
category_id |
write_issue_manage.delete_category, write_issue_create |
{catId} path / issueCategoryId body |
keyword_id |
read_reference.keyword |
{keywordId} path |
user_id |
read_users.get, write_user_preferences, write_user_manage, write_notification_send |
{id}/{userId} path / users body list |
queue_id |
write_retry_remove |
retry-queue {requestId} path (queue namespace only) |
file_id |
read_logs.read |
{logFileName} path |
machine_id |
read_integration.plex_libraries |
saved Plex server resolution (no passthrough) |
server_id |
read_integration.media_server |
saved Emby/Jellyfin server resolution (no passthrough) |
profile_id |
write_integration_test |
saved-settings resolution (no passthrough) |
root_folder_id |
create/options | rootFolderOverride body / {rootFolderId} path |
quality_profile_id |
create/options | qualityPathOverride body / {qualityId} path |
language_profile_id |
create/options | languageProfile body |
is_4k |
create/moderate/reprocess | is4kRequest body / is4K body / {is4K} path |
request_type |
write_issue_create, write_request_reprocess |
requestType body int / {type} path int (T1 map) |
status |
read_issues, write_issue_manage |
{status} path int / status body int (T2 map) |
sort_direction |
read_requests.list |
{sortOrder} path asc/desc (sort=requestDate) |
season_numbers |
write_request_create tv season mode |
adapter expansion → seasons[].episodes |
on_behalf_user_id |
write_request_create |
requestOnBehalf body (Verify: id vs username) |
requested_by_alias |
write_request_create.album |
requestedByAlias body |
from,to |
read_server.stats |
from,to query |
expression |
read_server.cron_validate |
{expression} body |
section,revision,changes |
read_settings,write_settings_patch |
private load/merge → section POST body |
name,enabled |
write_settings_patch feature |
FeatureEnablement body → enable/disable POST |
subject,body,bcc,user_ids |
write_notification_send |
NotificationMessage fields; ids → users entity list |
job |
write_job_run.run |
literal /api/v1/Job/{job} path segment |
media |
everywhere | route segment or body discriminator per ledger |
6.2 05-endpoint-coverage.md edits
Owner / branchcolumn: rename everyombi_*token (e.g.ombi_media / by_request→read_media / by_request;ombi_settings_write / patch:*→write_settings_patch / patch:*).- Token rows 361–366: update Notes from "optional auth flow must be verified" → "JWT-primary auth adapter (
OMBI_AUTH_MODE=jwt); login/refresh semantics per01-authentication.md; Bearer-on-all-routes remains Verify." - Row for
Search/artist/request/{requestId}: owner becomesread_media / by_request; note gains "MCPmedia=albummaps to this literalartistroute segment." - Intro paragraph gains a line: ledger tool names reflect the
read_*/write_*namespace. - All 377 rows, dispositions, links, and counts unchanged.
6.3 06-verification.md edits
- "Enum labels" gap row: mark RequestType/IssueStatus/NotificationAgent/NotificationType label maps published (Phase 04); retain
VoteType/RequestSource/RequestLimitType/legacy filters as gaps. - "Whole-season semantics" row: update —
seasonmode now exists; adapter expansion is the default construction;episodes:[]remains Verify. - Acceptance criteria additions: "Omitted
is_4k/status/sort_directionresolve to documented defaults without triggering conditional requirements"; "season_numbersrequests produce explicitseasons[].episodeswire bodies"; "renamed catalogue matches 31 names; noombi_-prefixed tool advertised"; "JWT startup path exercised;api_keymode sendsUserNameonly when configured". - Update all examples to new tool names (
read_search,read_media,write_request_create,write_request_moderate,write_issue_comment,write_settings_patch,write_integration_test); add aseasonmode example and aread_requestsminimal example. - "Authentication model" row → JWT-primary per
01-authentication.md.
6.4 07-settings-types.md edits
- Tool names →
write_settings_patch/read_settings. - Add note:
notificationTemplatespatch elements usenotification_type/agentstring labels; the merge layer translates label→int and key→notificationTypebefore merging; reads emit labels with*_codefallback per Phase 04.
6.5 README.md edits
- Reading order gains
8. [Go interface baseline](08-go-baseline.md). - Final paragraph: replace "
OMBI_API_KEYremains the standard upstream credential" with "Upstream authentication is selected explicitly viaOMBI_AUTH_MODE(jwtprimary,api_keysecondary); see01-authentication.md." - Add naming sentence (Phase 01 rule 6).
6.6 AGENTS.md edits
- Conventions: replace
ombi_*naming line withread_*/write_*rule. - Environment variables: add
OMBI_AUTH_MODE,OMBI_USERNAME,OMBI_PASSWORD,OMBI_USER_NAMEto the list. - Commands: fill
TBDs with Go toolchain — installgo mod download, buildgo build ./..., testgo test ./..., lint/typecheckgo vet ./....
6.7 New file docs/schema/08-go-baseline.md (content specified in full)
Sections: package layout; config; auth (Phase 02 skeleton); upstream client interface; arg/result structs for the changed tools; envelope structs.
Package layout:
cmd/ombi-mcp/main.go — entrypoint, config load, server start
internal/config/config.go — env loading + validation (env-var matrix)
internal/ombi/client.go — HTTP client: base-URL join, encoding, retries
internal/ombi/auth.go — AuthManager (Phase 02 skeleton)
internal/ombi/wiretypes.go — upstream body/response types per ledger
internal/mcpserver/server.go — MCP transport, tools/list, tools/call
internal/tools/registry.go — tool defs, bundle filters, annotations
internal/tools/args.go — input arg structs (below)
internal/tools/results.go — result envelope + family structs (below)
internal/translate/enums.go — Phase 04 package (printed in full there)
Config (full):
package config
import (
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"
)
type AuthMode string
const (
AuthModeJWT AuthMode = "jwt"
AuthModeAPIKey AuthMode = "api_key"
)
type Config struct {
BaseURL *url.URL
AuthMode AuthMode
Username string
Password string
APIKey string
UserName string
EnabledBundles map[string]bool // keys: core, moderation, administration
HTTPTimeout time.Duration
}
func Load() (*Config, error) {
c := &Config{HTTPTimeout: 30 * time.Second, EnabledBundles: map[string]bool{"core": true}}
raw := os.Getenv("OMBI_URL")
if raw == "" {
return nil, errors.New("OMBI_URL is required")
}
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return nil, fmt.Errorf("OMBI_URL invalid: %q", raw)
}
if u.User != nil || u.RawQuery != "" || u.Fragment != "" {
return nil, errors.New("OMBI_URL must not contain credentials, query or fragment")
}
c.BaseURL = u
switch AuthMode(os.Getenv("OMBI_AUTH_MODE")) {
case AuthModeJWT:
c.AuthMode = AuthModeJWT
case AuthModeAPIKey:
c.AuthMode = AuthModeAPIKey
default:
return nil, errors.New("OMBI_AUTH_MODE must be \"jwt\" or \"api_key\"")
}
c.Username, c.Password = os.Getenv("OMBI_USERNAME"), os.Getenv("OMBI_PASSWORD")
c.APIKey, c.UserName = os.Getenv("OMBI_API_KEY"), os.Getenv("OMBI_USER_NAME")
if c.AuthMode == AuthModeJWT && (c.Username == "" || c.Password == "") {
return nil, errors.New("jwt mode requires OMBI_USERNAME and OMBI_PASSWORD")
}
if c.AuthMode == AuthModeAPIKey && c.APIKey == "" {
return nil, errors.New("api_key mode requires OMBI_API_KEY")
}
if b := os.Getenv("OMBI_BUNDLES"); b != "" {
c.EnabledBundles = map[string]bool{}
for _, s := range strings.Split(b, ",") {
s = strings.TrimSpace(s)
if s != "core" && s != "moderation" && s != "administration" {
return nil, fmt.Errorf("unknown bundle %q", s)
}
c.EnabledBundles[s] = true
}
}
return c, nil
}
Upstream client interface (full):
package ombi
import (
"context"
"net/http"
)
// Client performs one authenticated upstream call. It applies auth,
// encodes segments, sends, and returns the raw response for the
// adapter to interpret. It never decides business outcomes.
type Client struct {
base string
auth *AuthManager
http *http.Client
}
// Do issues a single authenticated request. Callers pass method, the
// API path already joined onto the configured base (prefix preserved),
// optional query values, and an optional JSON body. Do returns
// ErrUnauthorized after one failed post-renewal retry so the adapter
// can decide its own retry policy.
func (c *Client) Do(ctx context.Context, method, path string,
query map[string]string, body any) (*http.Response, error)
Changed-tool arg structs (full):
package tools
// write_request_create
type RequestCreateArgs struct {
Action string `json:"action"` // movie|tv|album|collection
TmdbID *int `json:"tmdb_id,omitempty"`
Is4K bool `json:"is_4k"` // defaults false
Language string `json:"language,omitempty"`
Provider string `json:"provider,omitempty"` // tmdb|tvdb for tv
ID *int `json:"id,omitempty"`
Selection *TVSelect `json:"selection,omitempty"`
LanguageProfileID *int `json:"language_profile_id,omitempty"`
OnBehalfUserID string `json:"on_behalf_user_id,omitempty"`
Overrides *Overrides `json:"overrides,omitempty"`
MusicBrainzID string `json:"musicbrainz_id,omitempty"`
RequestedByAlias string `json:"requested_by_alias,omitempty"`
CollectionID *int `json:"collection_id,omitempty"`
}
type TVSelect struct {
Mode string `json:"mode"` // all|first_season|latest_season|season|episodes
SeasonNumbers []int `json:"season_numbers,omitempty"`
Seasons []Season `json:"seasons,omitempty"`
}
type Season struct {
SeasonNumber int `json:"season_number"`
Episodes []int `json:"episodes"`
}
type Overrides struct {
RootFolderID *int `json:"root_folder_id,omitempty"`
QualityProfileID *int `json:"quality_profile_id,omitempty"`
}
// write_request_moderate
type ModerateArgs struct {
Action string `json:"action"` // approve|deny|mark_available|mark_unavailable
Media string `json:"media"` // movie|tv|album
RequestID int `json:"request_id"`
Is4K bool `json:"is_4k"`
Reason string `json:"reason,omitempty"`
}
// write_request_options
type OptionsArgs struct {
Action string `json:"action"` // advanced|tv_root|tv_quality
Media string `json:"media,omitempty"`
RequestID *int `json:"request_id,omitempty"`
Options *AdvOptions `json:"options,omitempty"`
ParentRequestID *int `json:"parent_request_id,omitempty"`
RootFolderID *int `json:"root_folder_id,omitempty"`
QualityProfileID *int `json:"quality_profile_id,omitempty"`
}
type AdvOptions struct {
RootFolderID *int `json:"root_folder_id,omitempty"`
QualityProfileID *int `json:"quality_profile_id,omitempty"`
LanguageProfileID *int `json:"language_profile_id,omitempty"`
}
// write_request_reprocess
type ReprocessArgs struct {
RequestType string `json:"request_type"` // movie|tv|album
RequestID int `json:"request_id"`
Is4K bool `json:"is_4k"`
}
// write_issue_create
type IssueCreateArgs struct {
Title string `json:"title"`
Subject string `json:"subject,omitempty"`
Description string `json:"description"`
CategoryID int `json:"category_id"`
RequestType string `json:"request_type"` // movie|tv|album
RequestID *int `json:"request_id,omitempty"`
ProviderID string `json:"provider_id,omitempty"`
}
// write_issue_manage / read_issues shared status label
type IssueStatus = string // "pending"|"in_progress"|"resolved"|"closed"
// read_requests
type RequestsListArgs struct {
Action string `json:"action"`
Media string `json:"media,omitempty"`
Status string `json:"status"` // adapter default "all"
SortDirection string `json:"sort_direction"` // adapter default "desc"
ParentRequestID *int `json:"parent_request_id,omitempty"`
Query string `json:"query,omitempty"`
Page *Page `json:"page,omitempty"`
Target *Target `json:"target,omitempty"`
}
type Page struct{ Offset, Limit int }
type Target struct{ Kind string; ID int }
// read_media
type MediaArgs struct {
Action string `json:"action"`
Target *Target `json:"target,omitempty"`
Media string `json:"media,omitempty"`
RequestID *int `json:"request_id,omitempty"`
TmdbID *int `json:"tmdb_id,omitempty"`
Language string `json:"language,omitempty"`
Name string `json:"name,omitempty"`
Year *int `json:"year,omitempty"`
}
Result envelope (full):
package tools
// ToolResult is the structuredContent payload for every tool.
type ToolResult struct {
OK bool `json:"ok"`
Operation string `json:"operation"` // tool name
Data any `json:"data,omitempty"` // one family object
Error *ToolError `json:"error,omitempty"`
Warnings []string `json:"warnings"`
Truncated bool `json:"truncated"`
CorrelationID string `json:"correlation_id"`
}
type ToolError struct {
Code string `json:"code"` // INVALID_ARGUMENT … INTERNAL_ERROR
Message string `json:"message"`
Retryable bool `json:"retryable"`
HTTPStatus *int `json:"http_status,omitempty"`
Field string `json:"field,omitempty"`
RetryAfterSeconds *int `json:"retry_after_seconds,omitempty"`
}
Target artefacts
docs/schema/02-tool-mapping.md— §6.1 edits plus Appendix A table appended.docs/schema/05-endpoint-coverage.md— §6.2 edits; counts unchanged (377/321; D257/A37/P59/I12/X12).docs/schema/06-verification.md— §6.3 edits.docs/schema/07-settings-types.md— §6.4 edits.docs/schema/README.md— §6.5 edits.docs/schema/08-go-baseline.md— new file with all §6.7 content.AGENTS.md— §6.6 edits.
Verification gates
grep -rn "ombi_" docs/returns no tool-name occurrences (namespace enum values excepted).- Appendix A table present in
02-tool-mapping.mdwith every row above. 05-endpoint-coverage.mdowner column contains onlyread_*/write_*names; counts intact (377/321; D257/A37/P59/I12/X12).06-verification.mdexamples use new names only.docs/schema/08-go-baseline.mdexists with all sections above.AGENTS.mdenv-var list and commands updated; noombi_*convention remains.- README references
OMBI_AUTH_MODEand links08-go-baseline.md.
Evidence labels
- Documented — Appendix A wire forms per the endpoint ledger; Go toolchain commands for
AGENTS.md. - Design — the explicit MCP→wire mapping layer (D3), package layout, config validation order, arg/envelope struct shapes.
- Verify —
on_behalf_user_id→requestOnBehalf(id vs username);parent_request_idper-route segment verification; Bearer-on-all-routes note carried into ledger rows 361–366.