1c20df70f8
Pure move. Nothing changes but the file a reader opens. validateMCP is the one new name: config.go's validate arm becomes a method next to the block it checks, which is the shape the rest of this sweep follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
4.6 KiB
Go
123 lines
4.6 KiB
Go
package config
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/mcp"
|
|
)
|
|
|
|
// MCPServerConfig — one MCP server.
|
|
type MCPServerConfig struct {
|
|
// Name — the local handle. It prefixes every tool this server contributes
|
|
// ("vikunja" + "list_tasks" ⇒ the allowlist row "vikunja_list_tasks") and
|
|
// becomes the store scope "mcp:<name>", so its provenance is readable on
|
|
// /tools without opening the config.
|
|
Name string `json:"name"`
|
|
|
|
// Command / Args / Env / Dir — a stdio server: a child process of mavend,
|
|
// on this box, under this user. argv, never a shell string.
|
|
Command string `json:"command,omitempty"`
|
|
Args []string `json:"args,omitempty"`
|
|
Env []string `json:"env,omitempty"`
|
|
Dir string `json:"dir,omitempty"`
|
|
|
|
// URL — a streamable-HTTP endpoint. It is fetched through
|
|
// internal/webfetch, so the SSRF guard, the redirect cap, the size cap and
|
|
// the one-request-per-host-per-second limit all apply.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// AllowPrivate — let THIS server be a loopback or LAN address. The Vikunja
|
|
// server on homesrv is "http://localhost:9100/mcp", which is refused
|
|
// without this flag. Understand what it means before setting it: a local
|
|
// server is a DIFFERENT trust level from a public one. It is inside the
|
|
// network, it usually needs no credential, and it can change things that
|
|
// matter — so an argument the router got wrong lands somewhere real. Set it
|
|
// only for a server you run yourself, and prefer allow_tools with it.
|
|
AllowPrivate bool `json:"allow_private,omitempty"`
|
|
|
|
// AllowTools — when set, the ONLY remote tool names taken from this server.
|
|
// This is the knob that keeps the catalogue deliberate: the resident model
|
|
// is a 1.7B with a 4096-token context, and a tool name it half-remembers is
|
|
// a wrong act, so fewer and better-chosen beats complete.
|
|
AllowTools []string `json:"allow_tools,omitempty"`
|
|
|
|
// MaxTools — cap on this server's contribution. 0 ⇒ mcp.DefaultMaxTools (12).
|
|
MaxTools int `json:"max_tools,omitempty"`
|
|
|
|
// Timeout — per-call budget for this server. 0 ⇒ MCPConfig.Timeout.
|
|
Timeout Duration `json:"timeout,omitempty"`
|
|
|
|
// Headers — sent verbatim on every request to a url server. This is how a
|
|
// bearer token reaches a real remote MCP server: {"Authorization": "Bearer
|
|
// ${MCP_TOKEN}"}, with the value in the gitignored env file like the
|
|
// telegram credentials. The Vikunja server on homesrv needs none only
|
|
// because it is unauthenticated on loopback.
|
|
Headers map[string]string `json:"headers,omitempty"`
|
|
|
|
// Enabled — false (the default) keeps a configured server described but
|
|
// dark, so a block can be written and reviewed before it is switched on.
|
|
Enabled bool `json:"enabled,omitempty"`
|
|
}
|
|
|
|
// MCPServers maps the config blocks onto the mcp package's own type. It lives
|
|
// here so config validation and daemon wiring cannot drift on the mapping.
|
|
// Returns nil when nothing is configured or nothing is enabled.
|
|
//
|
|
// Disabled servers are dropped here, which is why validation does NOT use this
|
|
// list — see allMCPServers.
|
|
func (c *Config) MCPServers() []mcp.ServerConfig {
|
|
return c.mcpServers(true)
|
|
}
|
|
|
|
// allMCPServers is every configured server, enabled or not, for validation.
|
|
//
|
|
// Validating only the enabled ones meant a block with both command and url, or
|
|
// a bare hostname as the url, passed startup validation while it was dark. The
|
|
// doc on Enabled says a block can be written and reviewed before it is switched
|
|
// on; the review the config layer could give was the one thing skipped. Enabled
|
|
// gates the dialing, not the shape check.
|
|
func (c *Config) allMCPServers() []mcp.ServerConfig {
|
|
return c.mcpServers(false)
|
|
}
|
|
|
|
func (c *Config) mcpServers(onlyEnabled bool) []mcp.ServerConfig {
|
|
if c.MCP == nil {
|
|
return nil
|
|
}
|
|
out := make([]mcp.ServerConfig, 0, len(c.MCP.Servers))
|
|
for _, s := range c.MCP.Servers {
|
|
if onlyEnabled && !s.Enabled {
|
|
continue
|
|
}
|
|
timeout := time.Duration(s.Timeout)
|
|
if timeout <= 0 {
|
|
timeout = time.Duration(c.MCP.Timeout)
|
|
}
|
|
out = append(out, mcp.ServerConfig{
|
|
Name: s.Name,
|
|
Command: s.Command,
|
|
Args: s.Args,
|
|
Env: s.Env,
|
|
Dir: s.Dir,
|
|
URL: s.URL,
|
|
AllowPrivate: s.AllowPrivate,
|
|
AllowTools: s.AllowTools,
|
|
MaxTools: s.MaxTools,
|
|
Headers: s.Headers,
|
|
Timeout: timeout,
|
|
Enabled: s.Enabled,
|
|
})
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// validateMCP fails a block with a typo (no name, both command and url, a bare
|
|
// hostname as the url) at startup, rather than at the first turn that needed
|
|
// the tool.
|
|
func (c *Config) validateMCP() error {
|
|
return mcp.Validate(c.allMCPServers())
|
|
}
|