package mcp import ( "context" "fmt" "github.com/kami/maven/internal/webfetch" ) // WebfetchDoor builds the PosterFactory used in production: one guarded // webfetch.Fetcher per url server, with that server's allow_private and the // shared host lists and limits. // // One fetcher PER server is the point. allow_private is a hole in the // private-address guard, and a hole punched for the Vikunja server on loopback // 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 } return fetcherPoster{webfetch.New(c)}, nil } } // fetcherPoster adapts webfetch.Fetcher to Poster. It exists so this package // does not have to know webfetch's Response type, and so a test can substitute // a fake without a listener. type fetcherPoster struct{ f *webfetch.Fetcher } func (p fetcherPoster) Post(ctx context.Context, rawURL, contentType string, body []byte, hdr map[string]string) (*PostResponse, error) { resp, err := p.f.Post(ctx, rawURL, contentType, body, hdr) if err != nil { return nil, fmt.Errorf("mcp: post %s: %w", rawURL, err) } return &PostResponse{ Status: resp.Status, ContentType: resp.ContentType, Body: resp.Body, Header: resp.Header, }, nil }