Documentation
¶
Overview ¶
Package client provides MCP protocol client implementation for communicating with backend servers.
This package implements the BackendClient interface defined in the vmcp package, using the mark3labs/mcp-go SDK for protocol communication.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewHTTPBackendClient ¶
func NewHTTPBackendClient(registry vmcpauth.OutgoingAuthRegistry, opts ...Option) (vmcp.BackendClient, error)
NewHTTPBackendClient creates a new HTTP-based backend client. This client supports streamable-HTTP and SSE transports.
The registry parameter manages authentication strategies for outgoing requests to backend MCP servers. It must not be nil. To disable authentication, use a registry configured with the "unauthenticated" strategy.
Options are additive: nil or absent options reproduce the default behavior exactly. See WithDialControl to install a per-connection dial hook for SSRF / DNS-rebinding defense.
Returns an error if registry is nil.
Types ¶
type Option ¶ added in v0.30.1
type Option func(*httpBackendClient)
Option configures an httpBackendClient.
func WithDialControl ¶ added in v0.30.1
WithDialControl installs a per-connection Control hook on the dialer used to reach every backend. The hook fires after DNS resolution and before the TCP handshake, receiving the resolved peer IP in address — which is why it defeats DNS-rebinding attacks that a host-name–based allow/deny check cannot: a hostname can legitimately resolve to a blocked IP after the name-based check passes.
The hook composes with per-backend CA-bundle handling: it augments the internally built *http.Transport rather than replacing it. Supplying it implies the standard dial timeouts (30 s Timeout, 30 s KeepAlive) used throughout this package.
The signature matches net.Dialer.Control exactly.
Security limitations embedders must understand:
Per-TCP-dial, not per-request: the hook fires once per TCP connection. A pooled connection is reused without re-invoking the hook until it is recycled. Because each backend gets its own isolated transport and connection pool, a reused connection is always one this hook already approved on its first dial — reuse cannot reach an unclassified peer. This client does not offer per-request re-classification.
Proxy transparency: when http.ProxyFromEnvironment selects a proxy (HTTP_PROXY/HTTPS_PROXY set), the dial target is the proxy server, so the hook receives the proxy's IP, not the backend's. Embedders relying on this hook for SSRF or IP allow-listing must either unset the proxy env vars or additionally validate the request URL's host before dialing.
Both IP families: the address argument may be an IPv4 or IPv6 literal (host:port form); embedders must handle both families — including IPv4-mapped IPv6 such as ::ffff:127.0.0.1 — in their check. See the OWASP SSRF Prevention Cheat Sheet for the full set of ranges to deny (loopback, RFC 1918, link-local 169.254/16, CGNAT 100.64/10, IPv6 ULA).
Example ¶
ExampleWithDialControl shows how an embedder installs a dial-control hook that rejects connections to non-public IP ranges — the building block of an SSRF / DNS-rebinding defense. The hook receives the resolved peer IP, so it classifies the address the kernel is about to connect to rather than the (re-resolvable) hostname. A production check should additionally cover the ranges listed in the WithDialControl doc comment (CGNAT, IPv6 ULA, etc.).
denyNonPublic := func(_, address string, _ syscall.RawConn) error {
host, _, err := net.SplitHostPort(address)
if err != nil {
return err
}
ip := net.ParseIP(host)
if ip == nil {
return fmt.Errorf("unresolved dial address %q", address)
}
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
return fmt.Errorf("blocked connection to non-public IP %s", ip)
}
return nil
}
registry := auth.NewDefaultOutgoingAuthRegistry()
if err := registry.RegisterStrategy(
authtypes.StrategyTypeUnauthenticated, &strategies.UnauthenticatedStrategy{},
); err != nil {
panic(err)
}
backendClient, err := NewHTTPBackendClient(registry, WithDialControl(denyNonPublic))
if err != nil {
panic(err)
}
_ = backendClient