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
186 lines
9.0 KiB
Go
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
|
|
}
|