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.
105 lines
2.6 KiB
Go
105 lines
2.6 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"ombi-mcp/internal/ombi"
|
|
)
|
|
|
|
// Env carries the dependencies a handler needs at call time.
|
|
type Env struct {
|
|
// Upstream performs authenticated calls against the configured
|
|
// Ombi instance.
|
|
Upstream *ombi.Client
|
|
|
|
// settingsMu serializes settings patch read-modify-write cycles
|
|
// so concurrent local saves cannot interleave between the private
|
|
// GET and the POST (07-settings-types: serialize local saves).
|
|
settingsMu sync.Mutex
|
|
}
|
|
|
|
// Handler executes one tool call. raw is the MCP arguments object.
|
|
// It never returns nil; failures are expressed in the ToolResult
|
|
// envelope, not as Go errors.
|
|
type Handler func(ctx context.Context, env *Env, raw json.RawMessage) *ToolResult
|
|
|
|
// op is the per-call working context: it accumulates warnings and
|
|
// truncation state and stamps the shared envelope fields.
|
|
type op struct {
|
|
ctx context.Context
|
|
env *Env
|
|
tool string
|
|
corr string
|
|
warn []string
|
|
truncated bool
|
|
write bool // mutating call — changes transport-error mapping
|
|
}
|
|
|
|
func newOp(ctx context.Context, env *Env, tool string, write bool) *op {
|
|
return &op{
|
|
ctx: ctx,
|
|
env: env,
|
|
tool: tool,
|
|
corr: newCorrelationID(),
|
|
warn: []string{},
|
|
write: write,
|
|
}
|
|
}
|
|
|
|
func newCorrelationID() string {
|
|
var b [12]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return "corr-unknown"
|
|
}
|
|
return hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// warnf appends a degradation note to warnings[].
|
|
func (o *op) warnf(format string, args ...any) {
|
|
o.warn = append(o.warn, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// ok wraps a successful data family object in the envelope.
|
|
func (o *op) ok(data any) *ToolResult {
|
|
return &ToolResult{
|
|
OK: true,
|
|
Operation: o.tool,
|
|
Data: data,
|
|
Warnings: o.warn,
|
|
Truncated: o.truncated,
|
|
CorrelationID: o.corr,
|
|
}
|
|
}
|
|
|
|
// failErr wraps a ToolError in the envelope (ok=false).
|
|
func (o *op) failErr(e *ToolError) *ToolResult {
|
|
return &ToolResult{
|
|
OK: false,
|
|
Operation: o.tool,
|
|
Error: e,
|
|
Warnings: o.warn,
|
|
Truncated: o.truncated,
|
|
CorrelationID: o.corr,
|
|
}
|
|
}
|
|
|
|
// fail builds and wraps a ToolError.
|
|
func (o *op) fail(code, msg string, retryable bool) *ToolResult {
|
|
return o.failErr(&ToolError{Code: code, Message: msg, Retryable: retryable})
|
|
}
|
|
|
|
// invalid reports a client-argument problem (INVALID_ARGUMENT).
|
|
func (o *op) invalid(field, format string, args ...any) *ToolResult {
|
|
return o.failErr(&ToolError{
|
|
Code: "INVALID_ARGUMENT",
|
|
Message: fmt.Sprintf(format, args...),
|
|
Retryable: false,
|
|
Field: field,
|
|
})
|
|
}
|