Files
gronod 6926a91c80
Build and publish / Test and build (darwin) (pull_request) Successful in 1m52s
Build and publish / Test and build (linux) (pull_request) Successful in 2m30s
Build and publish / Test and build (windows) (pull_request) Successful in 3m7s
Build and publish / Test and build (windows) (push) Successful in 3m7s
Build and publish / Test and build (darwin) (push) Successful in 1m45s
Build and publish / Test and build (linux) (push) Successful in 2m12s
Build and publish / Build and publish Docker image (pull_request) Successful in 1m49s
Build and publish / Build and publish Docker image (push) Successful in 2m4s
Fix M7 admin/server wire contracts
2026-09-19 18:36:03 +01:00

186 lines
9.0 KiB
Go

package tools
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
)
//go:generate sh -c "cd ../.. && go run ./tools/schemagen"
//go:embed schemas.json
var schemasJSON []byte
// schemas is the embedded contract extracted from docs/schema by
// tools/schemagen: complete input schemas per tool plus the shared
// output $defs library.
var schemas struct {
Input map[string]json.RawMessage `json:"input"`
Defs map[string]json.RawMessage `json:"defs"`
}
func init() {
if err := json.Unmarshal(schemasJSON, &schemas); err != nil {
panic("tools: embedded schemas.json is invalid: " + err.Error())
}
}
// ToolDef is one registered tool: its MCP name, bundle, advisory
// annotations, allowed result families and handler.
type ToolDef struct {
Name string
Bundle string // core|moderation|administration
Description string
ReadOnly bool
Destructive bool
Idempotent bool
OpenWorld bool
Families []string // allowed `data` families for outputSchema
Handler Handler
}
// registry is the fixed 31-tool catalogue from the routing ledger.
var registry = []ToolDef{
{"read_search", "core", "Search media by text, with explicit movie refinements or a multi-search filter.", true, false, true, true, []string{"media_page"}, handleSearch},
{"read_discover", "core", "Curated lists, similar movies, collection, credits, artist albums, advanced movie filters.", true, false, true, true, []string{"media_page"}, handleDiscover},
{"read_media", "core", "Details by explicit provider or request; ratings; streaming.", true, false, true, true, []string{"media_page"}, handleMedia},
{"read_reference", "core", "Genres, languages, keywords, watch-provider catalogue, countries, issue categories.", true, false, true, true, []string{"reference_page"}, handleReference},
{"read_requests", "core", "List/get/search, TV children, recent requests, privileged retry queue.", true, false, true, true, []string{"request_page", "retry_page"}, handleRequests},
{"read_request_stats", "core", "Counts, totals, per-media quota, user-has-requests.", true, false, true, true, []string{"metrics"}, handleRequestStats},
{"read_issues", "core", "Issues, grouped summaries, comments and counts.", true, false, true, true, []string{"issue_page", "comment_page", "group_page", "metrics"}, handleIssues},
{"read_votes", "core", "Global vote list or votes on a request.", true, false, true, true, []string{"vote_page"}, handleVotes},
{"read_users", "core", "Self, authorized user lookup, claims, online users, preference read.", true, false, true, true, []string{"user_page", "reference_page"}, handleUsers},
{"read_library", "core", "Recent additions, calendar, artwork.", true, false, true, true, []string{"media_page", "calendar_page", "artwork_page"}, handleLibrary},
{"read_server", "core", "Status, version, features, stats, cron validation.", true, false, true, true, []string{"metrics", "reference_page"}, handleServer},
{"read_integration", "core", "Saved ARR options and authorized media-server metadata.", true, false, true, true, []string{"reference_page", "user_page"}, handleIntegration},
{"write_request_create", "core", "One media request or explicit collection request.", false, false, false, true, []string{"mutation"}, handleRequestCreate},
{"write_request_subscribe", "core", "Subscribe/unsubscribe.", false, false, false, true, []string{"mutation"}, handleRequestSubscribe},
{"write_issue_create", "core", "Report an issue.", false, false, false, true, []string{"mutation"}, handleIssueCreate},
{"write_issue_comment", "core", "Add a comment.", false, false, false, true, []string{"mutation"}, handleIssueComment},
{"write_vote", "core", "Up/down vote.", false, false, false, true, []string{"mutation"}, handleVote},
{"write_user_preferences", "core", "Language, streaming country, newsletter opt-out.", false, false, false, true, []string{"mutation"}, handleUserPrefs},
{"write_request_moderate", "moderation", "Approval, denial, availability.", false, true, false, true, []string{"mutation"}, handleRequestModerate},
{"write_request_delete", "moderation", "Explicit single movie/album/TV-parent/TV-child deletion.", false, true, false, true, []string{"mutation"}, handleRequestDelete},
{"write_request_options", "moderation", "Advanced routing overrides and TV root/quality.", false, true, false, true, []string{"mutation"}, handleRequestOptions},
{"write_request_reprocess", "moderation", "Reprocess an existing request.", false, true, false, true, []string{"mutation"}, handleRequestReprocess},
{"write_issue_manage", "moderation", "State, deletes, category management.", false, true, false, true, []string{"mutation"}, handleIssueManage},
{"read_settings", "administration", "Safe configuration projection and revision.", true, false, true, true, []string{"settings"}, handleSettingsRead},
{"write_settings_patch", "administration", "Typed non-secret patch or feature flag.", false, true, false, true, []string{"mutation"}, handleSettingsPatch},
{"write_user_manage", "administration", "Delete user or send welcome email.", false, true, false, true, []string{"mutation"}, handleUserManage},
{"write_integration_test", "administration", "Test a saved profile; can send notifications.", false, false, false, true, []string{"mutation"}, handleIntegrationTest},
{"write_job_run", "administration", "Trigger permitted jobs / watchlist revalidation.", false, true, false, true, []string{"mutation"}, handleJobRun},
{"write_notification_send", "administration", "Email explicit recipients.", false, false, false, true, []string{"mutation"}, handleNotificationSend},
{"write_retry_remove", "administration", "Remove one queue entry.", false, true, false, true, []string{"mutation"}, handleRetryRemove},
{"read_logs", "administration", "Bounded, sanitized diagnostic reads.", true, false, true, true, []string{"logs"}, handleLogs},
}
// Registry returns the full tool catalogue.
func Registry() []ToolDef { return registry }
// Enabled reports whether the tool's bundle is enabled.
func (d ToolDef) Enabled(bundles map[string]bool) bool { return bundles[d.Bundle] }
// InputSchema returns the tool's complete JSON Schema object.
func (d ToolDef) InputSchema() (any, error) {
raw, ok := schemas.Input[d.Name]
if !ok {
return nil, fmt.Errorf("no embedded input schema for %q", d.Name)
}
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return nil, err
}
return v, nil
}
// OutputSchema composes the per-tool envelope: operation const, the
// tool's allowed `data` families, and the transitive $defs closure
// (error plus every def reachable through $ref).
func (d ToolDef) OutputSchema() (any, error) {
oneOf := make([]any, 0, len(d.Families))
roots := append([]string{"error"}, d.Families...)
for _, f := range d.Families {
oneOf = append(oneOf, map[string]any{"$ref": "#/$defs/" + f})
}
defs, err := defsClosure(roots)
if err != nil {
return nil, err
}
return map[string]any{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": map[string]any{
"ok": map[string]any{"type": "boolean"},
"operation": map[string]any{"const": d.Name},
"data": map[string]any{"oneOf": oneOf},
"error": map[string]any{"$ref": "#/$defs/error"},
"warnings": map[string]any{
"type": "array",
"items": map[string]any{"type": "string"},
"maxItems": 100,
},
"truncated": map[string]any{"type": "boolean"},
"correlation_id": map[string]any{"type": "string"},
},
"required": []any{"ok", "operation", "warnings", "truncated", "correlation_id"},
"additionalProperties": false,
"allOf": []any{map[string]any{
"if": map[string]any{"properties": map[string]any{"ok": map[string]any{"const": true}}},
"then": map[string]any{"required": []any{"data"}, "not": map[string]any{"required": []any{"error"}}},
"else": map[string]any{"required": []any{"error"}},
}},
"$defs": defs,
}, nil
}
// defsClosure BFS-expands the given root def names through every
// `#/$defs/<name>` reference found inside their schemas.
func defsClosure(roots []string) (map[string]any, error) {
out := map[string]any{}
queue := append([]string{}, roots...)
for len(queue) > 0 {
name := queue[0]
queue = queue[1:]
if _, seen := out[name]; seen {
continue
}
raw, ok := schemas.Defs[name]
if !ok {
return nil, fmt.Errorf("missing shared def %q", name)
}
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return nil, err
}
out[name] = v
for _, ref := range findRefs(v) {
if _, seen := out[ref]; !seen {
queue = append(queue, ref)
}
}
}
return out, nil
}
// findRefs walks a decoded JSON value collecting `#/$defs/` names.
func findRefs(v any) []string {
var out []string
switch t := v.(type) {
case map[string]any:
for k, e := range t {
if k == "$ref" {
if s, ok := e.(string); ok && strings.HasPrefix(s, "#/$defs/") {
out = append(out, strings.TrimPrefix(s, "#/$defs/"))
}
continue
}
out = append(out, findRefs(e)...)
}
case []any:
for _, e := range t {
out = append(out, findRefs(e)...)
}
}
return out
}