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

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