mirror of
https://github.com/anand34577/ferrum.git
synced 2026-09-12 05:48:58 +00:00
22c1dd382c
- User-scoped API keys (Profile > API Keys) for 3rd-party REST API access
and MCP clients, each locked to one scope at creation, with expiry,
revocation, and last-used tracking.
- A hand-rolled MCP (Model Context Protocol) server exposing the fleet
(connections, nodes, guests, storage, pools, alerts, cluster status) as
read tools plus one admin-gated power-action tool, so Claude Code/Desktop
or any other MCP client can query and operate the fleet directly.
- Both the REST API and MCP are off by default and toggleable instance-wide
from Settings > API & MCP, enforced live on every request.
- Admin-managed AI providers (any OpenAI-chat-completions-compatible
endpoint) backing the AI Assistant's tool-calling loop, replacing the
single hardcoded provider.
- A built-in, zero-config, no-API-key local provider backed by Needle 2
(internal/needle) for fully offline tool-calling, wired in as a one-click
preset. Requires the operator to separately download the Needle 2 binary
and point FERRUM_NEEDLE_BIN at it -- Ferrum never fetches executable
content from the network itself; see README "Built-in LLM (Needle 2)".
- System settings (CORS allow-list, instance-wide toggles) moved to the
admin Settings UI; environment variables are now scoped to true
bootstrap-level config only (listen address, TLS, DB connection, secret,
optional Needle binary path).
- Fixed: node Journal tab 502'ing with "unexpected end of JSON input" on an
empty response, and separately with a decode error on PVE versions that
return a bare-string journal line instead of the documented {n,t} object.
- Fixed: bottom content padding disappearing on every page except the AI
Assistant (an unconditional h-full on the content wrapper let overflowing
content bleed through where the padding should render).
- Fixed: Profile page felt cramped despite a wide viewport (stray max-w-2xl
cap not present on the equivalent Settings page).
- Test coverage added for the previously-untested MCP package and the new
Needle adapter (20 new Go tests), plus a regression test for the journal
decode fix.
60 lines
2.4 KiB
Go
60 lines
2.4 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// mcpAuth is the auth+authorization gate for the /mcp endpoint. It enforces,
|
|
// in order: (1) an admin has enabled MCP at all — off by default, since this
|
|
// endpoint lets external agents act on live infrastructure; (2) the caller
|
|
// presents an MCP-scoped API key (auth.ScopeMCP) — a general "api"-scoped
|
|
// key never works here, so connecting an MCP client always requires a token
|
|
// the user explicitly minted for that purpose (see apikeys.go). MCP clients
|
|
// (Claude Desktop, Claude Code) authenticate with a header, never a browser
|
|
// cookie, so this deliberately doesn't reuse requireAuth's cookie-or-key
|
|
// fallback — a bare bearer-token check with the 401/403 response shapes MCP
|
|
// clients expect.
|
|
func (s *Server) mcpAuth(next func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
agentSettings, err := s.loadAgentSettings(r.Context())
|
|
if err != nil {
|
|
s.writeError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
if !agentSettings.mcpEnabled {
|
|
writeErrorCode(w, http.StatusForbidden, "mcp_disabled", "MCP is disabled for this Ferrum instance — an admin must enable it in Settings")
|
|
return
|
|
}
|
|
|
|
key := bearerAPIKey(r)
|
|
if key == "" {
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="ferrum-mcp"`)
|
|
writeErrorMsg(w, http.StatusUnauthorized, "an MCP API key is required — create one under Profile > API Keys")
|
|
return
|
|
}
|
|
user, err := s.auth.AuthenticateMCPKey(r.Context(), key)
|
|
if err != nil {
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="ferrum-mcp", error="invalid_token"`)
|
|
writeErrorMsg(w, http.StatusUnauthorized, "invalid, revoked, or non-MCP-scoped API key")
|
|
return
|
|
}
|
|
|
|
// A slow upstream Proxmox host (or a chain of tool calls a client
|
|
// makes in sequence) can exceed the 30s global request timeout
|
|
// applied in Router(); detach from that inherited deadline the same
|
|
// way /ai/chat does, and apply a generous one of our own.
|
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 2*time.Minute)
|
|
defer cancel()
|
|
ctx = context.WithValue(ctx, userCtxKey, user)
|
|
next(w, r.WithContext(ctx))
|
|
}
|
|
}
|
|
|
|
// mcpHandler delegates the already-authenticated request to the MCP JSON-RPC
|
|
// dispatcher (internal/mcp) — see mcpAuth for how the user in context got there.
|
|
func (s *Server) mcpHandler(w http.ResponseWriter, r *http.Request) {
|
|
s.mcp.ServeHTTP(w, r, userFromContext(r))
|
|
}
|