mcp: guard the connection, not just the first dial

Refresh called alive() with the manager lock held, so a slow health
check blocked every other server. It now snapshots the candidates and
asks outside the lock.

A server that cannot be dialled was retried every minute forever, which
for a misconfigured stdio block means re-exec'ing a process 1440 times a
day. Dials now back off from one minute to thirty.

An allow_private fetcher followed redirects. A LAN MCP endpoint could
answer a POST with a redirect to 169.254.169.254 and the guard would go
there, because allow_private is what turns the address check off.
Redirects are refused outright on that door.

The tool catalogue was trimmed by taking the first max_tools entries of
whatever order the server sent, so the server chose which of its tools
Maven proposed. Over the cap without allow_tools now contributes
nothing: refusing is honest, silently keeping the server's pick is not.
Descriptions are server-written text that lands in the router prompt and
on /tools, so they are capped too.

A server block with enabled false was skipped by validation, so a typo
in a block written dark surfaced only on the day it was switched on. All
blocks are shape-checked now. Configured static headers carry the bearer
token a real remote server needs, and host_interval bounds how fast one
endpoint is polled.

Found in review of #70.
This commit is contained in:
kami
2026-08-01 14:11:39 +04:00
parent 87d03cf8c6
commit 5e0417306b
5 changed files with 337 additions and 44 deletions
+10
View File
@@ -16,10 +16,20 @@ import (
// must not become a hole for some public endpoint that happens to redirect at
// the LAN. Rate limiting is per fetcher too, which is the right shape here:
// separate servers are separate hosts.
//
// A server WITH allow_private also gets redirects switched off. Across servers
// the per-fetcher split holds the line; within the one server that has the
// flag it did not, because allow_private disables the dialer guard on every
// hop: http://localhost:9100/mcp answering 302 to
// http://169.254.169.254/latest/meta-data/ was followed, up to MaxRedirects. A
// local MCP endpoint has no business redirecting, so refusing costs nothing.
func WebfetchDoor(limits webfetch.Config) PosterFactory {
return func(cfg ServerConfig) (Poster, error) {
c := limits
c.AllowPrivate = cfg.AllowPrivate
if cfg.AllowPrivate {
c.MaxRedirects = -1 // negative ⇒ no redirects followed
}
if c.Timeout <= 0 && cfg.Timeout > 0 {
c.Timeout = cfg.Timeout
}