config: MCPServerConfig and its mappers move to mcp.go (V-410)
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>
This commit is contained in:
+1
-113
@@ -23,7 +23,6 @@ import (
|
|||||||
|
|
||||||
"github.com/kami/maven/internal/delivery/ntfysink"
|
"github.com/kami/maven/internal/delivery/ntfysink"
|
||||||
"github.com/kami/maven/internal/delivery/telegramsink"
|
"github.com/kami/maven/internal/delivery/telegramsink"
|
||||||
"github.com/kami/maven/internal/mcp"
|
|
||||||
"github.com/kami/maven/internal/morning"
|
"github.com/kami/maven/internal/morning"
|
||||||
"github.com/kami/maven/internal/netscan"
|
"github.com/kami/maven/internal/netscan"
|
||||||
"github.com/kami/maven/internal/smarthome"
|
"github.com/kami/maven/internal/smarthome"
|
||||||
@@ -432,114 +431,6 @@ func (c *Config) NetScanner() (netscan.Config, bool) {
|
|||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// PraxisConfig — maven's connection to the Praxis attention service.
|
// PraxisConfig — maven's connection to the Praxis attention service.
|
||||||
type PraxisConfig struct {
|
type PraxisConfig struct {
|
||||||
// URL — the Praxis HTTP API base URL (e.g. "http://localhost:9742").
|
// URL — the Praxis HTTP API base URL (e.g. "http://localhost:9742").
|
||||||
@@ -1680,10 +1571,7 @@ func (c *Config) validate() error {
|
|||||||
return fmt.Errorf("routine %q: bad cron %q: %w", r.Name, r.Cron, err)
|
return fmt.Errorf("routine %q: bad cron %q: %w", r.Name, r.Cron, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// An MCP block with a typo (no name, both command and url, a bare hostname
|
if err := c.validateMCP(); err != nil {
|
||||||
// as the url) fails here, at startup, rather than at the first turn that
|
|
||||||
// needed the tool.
|
|
||||||
if err := mcp.Validate(c.allMCPServers()); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Same for the house: a missing token or a bare hostname fails at startup,
|
// Same for the house: a missing token or a bare hostname fails at startup,
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user