Files
ombi-mcp/internal/tools/server.go
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

247 lines
6.4 KiB
Go

package tools
import (
"context"
"encoding/json"
"time"
"ombi-mcp/internal/ombi"
)
// read_server — status, version, features, stats and the
// administrator-gated cron validation. Families: metrics,
// reference_page.
func handleServer(ctx context.Context, env *Env, raw json.RawMessage) *ToolResult {
o := newOp(ctx, env, "read_server", false)
var a ServerArgs
if fail := o.args(raw, &a); fail != nil {
return fail
}
switch a.Action {
case "status":
return o.serverStatus()
case "status_info":
return o.serverStatusInfo()
case "about":
return o.serverAbout()
case "update_info":
return o.serverUpdateInfo()
case "update_check":
return o.serverUpdateCheck()
case "landing":
return o.serverLanding()
case "features":
return o.serverFeatures()
case "stats":
return o.serverStats(&a)
case "cron_validate":
return o.serverCronValidate(&a)
default:
return o.invalid("action", "unsupported action %q", a.Action)
}
}
func (o *op) serverStatus() *ToolResult {
raw, fail := o.call("GET", "/api/v1/Status", nil, nil)
if fail != nil {
return fail
}
v, fail := o.decodeScalar(raw)
if fail != nil {
return fail
}
val, ok := toInt(v)
if !ok {
if s, ok2 := v.(string); ok2 {
// Status may serialize as a named status string.
return o.ok(&Metrics{Kind: "metrics",
Values: []Metric{metric("status", s, "instance", "")}})
}
return o.fail("UPSTREAM_SCHEMA_MISMATCH", "upstream status was not a scalar", false)
}
return o.ok(&Metrics{Kind: "metrics",
Values: []Metric{metric("status", val, "instance", "")}})
}
func (o *op) serverStatusInfo() *ToolResult {
raw, fail := o.call("GET", "/api/v1/Status/info", nil, nil)
if fail != nil {
return fail
}
v, fail := o.decodeScalar(raw)
if fail != nil {
return fail
}
return o.ok(&Metrics{Kind: "metrics",
Values: []Metric{metric("status_info", v, "instance", "")}})
}
// serverAbout projects AboutViewModel excluding connection strings.
func (o *op) serverAbout() *ToolResult {
raw, fail := o.call("GET", "/api/v1/Settings/about", nil, nil)
if fail != nil {
return fail
}
m, fail := o.decodeObject(raw)
if fail != nil {
return fail
}
vals := []Metric{}
// Connection-string fields are never projected.
for _, k := range []string{"version", "branch", "frameworkDescription",
"osArchitecture", "osDescription", "processArchitecture",
"ombiDatabaseType", "externalDatabaseType", "settingsDatabaseType",
"notSupported"} {
if v, ok := m[k]; ok {
switch t := v.(type) {
case string, bool, float64:
vals = append(vals, metric(k, t, "instance", ""))
}
}
}
return o.ok(&Metrics{Kind: "metrics", Values: vals})
}
func (o *op) serverUpdateInfo() *ToolResult {
raw, fail := o.call("GET", "/api/v1/Update", nil, nil)
if fail != nil {
return fail
}
m, fail := o.decodeObject(raw)
if fail != nil {
return fail
}
vals := []Metric{}
for k, v := range m {
switch t := v.(type) {
case string, bool, float64:
vals = append(vals, metric(k, t, "instance", ""))
}
}
vals = capItems(o, vals)
return o.ok(&Metrics{Kind: "metrics", Values: vals})
}
func (o *op) serverUpdateCheck() *ToolResult {
raw, fail := o.call("GET", "/api/v1/Job/update", nil, nil)
if fail != nil {
return fail
}
v, fail := o.decodeScalar(raw)
if fail != nil {
return fail
}
return o.ok(&Metrics{Kind: "metrics",
Values: []Metric{metric("update_available", v, "instance", "")}})
}
func (o *op) serverLanding() *ToolResult {
raw, fail := o.call("GET", "/api/v1/LandingPage", nil, nil)
if fail != nil {
return fail
}
m, fail := o.decodeObject(raw)
if fail != nil {
return fail
}
vals := []Metric{}
for k, v := range m {
switch t := v.(type) {
case string, bool, float64:
vals = append(vals, metric(k, t, "instance", ""))
}
}
vals = capItems(o, vals)
return o.ok(&Metrics{Kind: "metrics", Values: vals})
}
func (o *op) serverFeatures() *ToolResult {
raw, fail := o.call("GET", "/api/v2/Features", nil, nil)
if fail != nil {
return fail
}
items, fail := o.refArray(raw,
[]string{"name"}, []string{"name"}, "feature")
if fail != nil {
return fail
}
// Feature records carry enabled as their value.
var arr []map[string]any
_ = json.Unmarshal(raw, &arr)
for i, m := range arr {
if i < len(items) {
if v, ok := m["enabled"]; ok {
items[i].Value = v
}
}
}
return o.ok(&ReferencePage{Kind: "reference_page",
Items: items, Page: singlePage(len(items), "references")})
}
func (o *op) serverStats(a *ServerArgs) *ToolResult {
if !nonempty(a.From) || !nonempty(a.To) {
field := "from"
if nonempty(a.From) {
field = "to"
}
return o.invalid(field, "stats requires both from and to as RFC3339 date-time values")
}
// from <= to is enforced before the upstream call.
f, ferr := time.Parse(time.RFC3339, a.From)
t, terr := time.Parse(time.RFC3339, a.To)
if ferr != nil || terr != nil {
return o.invalid("from", "from/to must be RFC3339 date-time values")
}
if f.After(t) {
return o.invalid("from", "from must be on or before to")
}
q := map[string]string{"from": a.From, "to": a.To}
raw, fail := o.call("GET", "/api/v1/Stats", q, nil)
if fail != nil {
return fail
}
m, fail := o.decodeObject(raw)
if fail != nil {
return fail
}
// Only the seven numeric totals; nested user objects are dropped.
vals := []Metric{}
for _, k := range []string{"totalRequests", "totalMovieRequests",
"totalTvRequests", "totalIssues", "completedRequestsMovies",
"completedRequestsTv", "completedRequests"} {
if i, ok := toInt(m[k]); ok {
vals = append(vals, metric(k, i, "instance", "requests"))
}
}
return o.ok(&Metrics{Kind: "metrics", Values: vals})
}
func (o *op) serverCronValidate(a *ServerArgs) *ToolResult {
if !nonempty(a.Expression) {
return o.invalid("expression", "expression must contain non-whitespace text")
}
raw, fail := o.call("POST", "/api/v1/Settings/testcron", nil,
ombi.CronViewModelBody{Expression: a.Expression})
if fail != nil {
return fail
}
m, fail := o.decodeObject(raw)
if fail != nil {
return fail
}
vals := []Metric{}
if v, ok := m["success"]; ok {
if b, ok2 := v.(bool); ok2 {
vals = append(vals, metric("valid", b, "instance", ""))
}
}
if s := jstr(m, "message"); s != "" {
vals = append(vals, metric("message", s, "instance", ""))
}
if valid, ok := m["success"].(bool); ok && !valid {
o.warnf("Ombi validates Quartz cron syntax (6-7 fields: seconds minutes hours day-of-month month day-of-week [year]); use ? for the unused day-of-month/day-of-week field")
}
return o.ok(&Metrics{Kind: "metrics", Values: vals})
}