Lands the executable half of the Phase 01-06 contracts: stdio MCP wiring with bundle policy enforced at call time, JWT/api_key upstream auth with single-flight renewal and 401 retry, strict argument decoding, allowlisted result projections with enum label twins, TV season expansion, settings read-modify-write under a revision lock, and a sanitized ToolError envelope that never forwards raw upstream bodies.
115 lines
2.8 KiB
Go
115 lines
2.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"ombi-mcp/internal/ombi"
|
|
)
|
|
|
|
// read_votes — global vote list or per-request votes.
|
|
// Family: vote_page.
|
|
func handleVotes(ctx context.Context, env *Env, raw json.RawMessage) *ToolResult {
|
|
o := newOp(ctx, env, "read_votes", false)
|
|
var a VotesArgs
|
|
if fail := o.args(raw, &a); fail != nil {
|
|
return fail
|
|
}
|
|
switch a.Action {
|
|
case "list":
|
|
return o.votesList(&a)
|
|
case "get":
|
|
return o.votesGet(&a)
|
|
default:
|
|
return o.invalid("action", "unsupported action %q", a.Action)
|
|
}
|
|
}
|
|
|
|
func (o *op) votesList(a *VotesArgs) *ToolResult {
|
|
raw, fail := o.call("GET", "/api/v1/Vote", nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
arr, fail := o.decodeArray(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
items := []Vote{}
|
|
for _, m := range arr {
|
|
items = append(items, o.projectVoteSummary(m))
|
|
}
|
|
win, pg := localWindow(o, items, a.Page, "votes")
|
|
return o.ok(&VotePage{Kind: "vote_page", Items: win, Page: pg})
|
|
}
|
|
|
|
func (o *op) votesGet(a *VotesArgs) *ToolResult {
|
|
var segName string
|
|
switch a.Media {
|
|
case "movie":
|
|
segName = "movie"
|
|
case "tv":
|
|
segName = "tv"
|
|
case "album":
|
|
segName = "music" // album votes read via the music segment
|
|
default:
|
|
return o.invalid("media", "media must be movie|tv|album")
|
|
}
|
|
if a.RequestID == nil || *a.RequestID < 1 {
|
|
return o.invalid("request_id", "request_id must be a positive integer")
|
|
}
|
|
raw, fail := o.call("GET",
|
|
fmt.Sprintf("/api/v1/Vote/%s/%d", segName, *a.RequestID), nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
arr, fail := o.decodeArray(raw)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
items := []Vote{}
|
|
for _, m := range arr {
|
|
items = append(items, o.projectVoteRecord(m))
|
|
}
|
|
win, pg := localWindow(o, items, a.Page, "votes")
|
|
return o.ok(&VotePage{Kind: "vote_page", Items: win, Page: pg})
|
|
}
|
|
|
|
// write_vote — POST Vote/{up|down}/{movie|tv|album}/{requestId};
|
|
// album uses the literal album segment on writes.
|
|
func handleVote(ctx context.Context, env *Env, raw json.RawMessage) *ToolResult {
|
|
o := newOp(ctx, env, "write_vote", true)
|
|
var a VoteArgs
|
|
if fail := o.args(raw, &a); fail != nil {
|
|
return fail
|
|
}
|
|
var segName string
|
|
switch a.Media {
|
|
case "movie", "tv", "album":
|
|
segName = a.Media
|
|
default:
|
|
return o.invalid("media", "media must be movie|tv|album")
|
|
}
|
|
if a.RequestID < 1 {
|
|
return o.invalid("request_id", "request_id must be a positive integer")
|
|
}
|
|
if a.Direction != "up" && a.Direction != "down" {
|
|
return o.invalid("direction", "direction must be up or down")
|
|
}
|
|
raw, fail := o.call("POST",
|
|
fmt.Sprintf("/api/v1/Vote/%s/%s/%d", a.Direction, segName, a.RequestID),
|
|
nil, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
var er ombi.EngineResult
|
|
if fail := o.decodeJSON(raw, &er); fail != nil {
|
|
return fail
|
|
}
|
|
m, fail := o.engineMutation(&er, nil)
|
|
if fail != nil {
|
|
return fail
|
|
}
|
|
m.RequestID = &a.RequestID
|
|
return o.ok(m)
|
|
}
|