Files
gronod eab16991ac Implement the runnable MCP server: 31 tools, auth, projections (Phase 07)
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.
2026-09-18 19:14:10 +01:00

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)
}