mcpfed

package
v0.167.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package mcpfed federates external Model Context Protocol (MCP) servers as native PromptZero tools. It is the outbound counterpart to internal/mcp, which exposes PromptZero's own tool surface as an MCP server.

Why this exists

Many high-leverage capabilities (Proxmark3, Hashcat, Burp, BloodHound, Ghidra, Metasploit, the FuzzingLabs security hub) are already published as MCP servers. Rather than re-implementing each one, mcpfed connects to them at startup, lists their tools, and registers each remote tool as a internal/tools.Spec under a prefixed name (e.g. secsec__nmap_scan). The agent's normal dispatch path then invokes the federated tool exactly like a native one — risk gating, audit logging, and confirm callbacks all apply uniformly.

Tool name layout

Federated tools are namespaced as `<prefix>__<remoteName>` (double underscore separator). The prefix is operator-chosen at config time, must match `^[a-z][a-z0-9-]*$`, and is reserved per-Federation. Anthropic's tool-name rule caps total length at 64 chars; mcpfed validates this before registration and rejects servers with names that would overflow.

Risk classification

MCP advertises optional behaviour hints on each tool via `mcp.ToolAnnotation` (ReadOnlyHint, DestructiveHint, IdempotentHint, OpenWorldHint). mcpfed maps these to risk.Level before registration:

  • DestructiveHint=true → Critical
  • ReadOnlyHint=true → Low
  • OpenWorldHint=true (and not read-only) → +1 tier vs. baseline
  • no annotations → ClientConfig.RiskDefault (or High if unset)

The mapping is conservative: every federated tool gets at least High unless the server explicitly declares it read-only.

Sandboxing

STDIO transports launch arbitrary child processes. mcpfed wires a configurable sandbox via mcp-go's `transport.WithCommandFunc` hook so the command's `*exec.Cmd` is wrapped before spawn. Supported profiles:

  • "none" — bare exec, intended for trusted local tools.
  • "docker" — `docker run --rm -i --network=none <image>`. The ClientConfig.Command is rewritten so the original command becomes the docker image; original args become the containerised process args.
  • "bwrap" — bubblewrap with read-only rootfs, tmpfs /tmp, no net.
  • "firejail" — firejail with --net=none --private (WSL-friendly fallback).

http and sse transports do not spawn processes; their sandbox value must be "none" and is otherwise rejected at validation.

Lifecycle

One Federation per process. Created with New, populated by Federation.Start (reads config, spawns each ClientConfig in parallel, registers tools), torn down with Federation.Close. Closed federations cannot be restarted — create a new one. The federation owns each managedClient for its lifetime; clients are long-lived (subprocess initialisation overhead is too high to spawn-per-call).

Reconnect

mcp-go does not auto-reconnect. mcpfed wraps each Handler with one retry: if CallTool returns an error matching transport-closed semantics, the client is closed, respawned, re-Initialized, and the call retried once. A second failure surfaces to the caller. Failed health-pings (every 30s by default) mark a client unhealthy and trigger a respawn on the next call.

Index

Constants

View Source
const MaxNameLen = 64

MaxNameLen is Anthropic's tool-name length cap. Anthropic enforces `^[a-zA-Z0-9_-]{1,64}$`; mcpfed pre-filters longer names rather than letting the API reject them at first use.

View Source
const NameSeparator = "__"

NameSeparator joins prefix and remote name, e.g. `secsec__nmap_scan`.

Variables

This section is empty.

Functions

This section is empty.

Types

type ClientBuilder

type ClientBuilder func(cfg ClientConfig) (*mcpclient.Client, error)

ClientBuilder is the constructor signature for a managed client's underlying connection. The default implementation routes by ClientConfig.Transport (stdio/http/sse). Tests inject custom builders to bypass subprocess spawning — see Options.ClientBuilder.

type ClientConfig

type ClientConfig struct {
	// Prefix is the tool-name namespace. Lower-case alphanumeric +
	// hyphens, must start with a letter. Required.
	Prefix string `yaml:"prefix"`

	// Transport is "stdio" | "http" | "sse". Required.
	Transport string `yaml:"transport"`

	// Command is the stdio command (e.g. "docker", "python", "uvx").
	Command string `yaml:"command,omitempty"`

	// Args are stdio command-line arguments.
	Args []string `yaml:"args,omitempty"`

	// Env is the stdio child process environment. Values may contain
	// `$VAR` to copy from the parent's env at startup.
	Env map[string]string `yaml:"env,omitempty"`

	// URL is the http/sse base URL.
	URL string `yaml:"url,omitempty"`

	// Headers are static HTTP headers injected by the http/sse client.
	Headers map[string]string `yaml:"headers,omitempty"`

	// Sandbox picks an exec wrapper for stdio transports. Empty defaults
	// to "none". Must be "none" for http/sse.
	Sandbox string `yaml:"sandbox,omitempty"`

	// RiskDefault is the per-tool risk level used when a federated tool
	// carries no MCP annotations to derive from. Empty defaults to
	// "high". One of "low" | "medium" | "high" | "critical".
	RiskDefault string `yaml:"risk_default,omitempty"`

	// InitTimeout caps the time spent on Initialize + ListTools at
	// startup. Zero defaults to 30s.
	InitTimeout time.Duration `yaml:"init_timeout,omitempty"`

	// HealthInterval sets the Ping cadence. Zero defaults to 30s.
	// Negative disables health checks entirely (rely on call-path
	// failure detection only).
	HealthInterval time.Duration `yaml:"health_interval,omitempty"`

	// Disabled skips this entry without removing it from config.
	Disabled bool `yaml:"disabled,omitempty"`
}

ClientConfig describes one external MCP server to federate.

Required fields per transport:

  • stdio: Prefix, Transport="stdio", Command [, Args, Env]
  • http: Prefix, Transport="http", URL [, Headers]
  • sse: Prefix, Transport="sse", URL [, Headers]

Env values prefixed with `$` are resolved from the parent process's environment at federation startup. A missing `$VAR` is left empty (the child server may treat unset as a hard error — that is the server's concern).

func ParseClientConfigs

func ParseClientConfigs(nodes []yaml.Node) ([]ClientConfig, error)

ParseClientConfigs decodes the raw yaml.Node entries from config.Config.MCPClients into typed ClientConfig values, validates each one, and returns the result. Invalid entries return an error joined with all collected validation problems so the operator sees every misconfiguration at once.

Lives in mcpfed (not config) so config has no dependency on mcpfed — the package boundary lets future federations be optional at the config layer without forcing every consumer to pull in the federation runtime.

func (ClientConfig) Validate

func (c ClientConfig) Validate() error

Validate returns an error if the config would not bring up cleanly. Run at startup so misconfigurations fail loud before any client spawns.

type Federation

type Federation struct {
	// contains filtered or unexported fields
}

Federation owns one or more managed external MCP servers and surfaces their tools as native PromptZero Specs.

func New

func New(opts Options) *Federation

New returns an empty Federation. Call Start to bring it up.

func (*Federation) Close

func (f *Federation) Close() error

Close tears down every managed client and stops health loops. Safe to call multiple times. After Close the federation cannot be reused.

func (*Federation) Healthy

func (f *Federation) Healthy(prefix string) bool

Healthy returns whether the most recent ping for prefix succeeded. False for unknown prefixes.

func (*Federation) Prefixes

func (f *Federation) Prefixes() []string

Prefixes returns the registered prefixes in undefined order.

func (*Federation) Start

func (f *Federation) Start(ctx context.Context, cfg FederationConfig) error

Start dials every non-disabled client in cfg, registers their remote tools as Specs, and starts background health probes. Failure on a single client is non-fatal — the error returned is a multi-error joining every client that failed; clients that succeeded are kept and registered.

Start is idempotent in the sense that calling it twice with disjoint configs adds the second batch. Calling it twice with overlapping prefixes returns an error for the conflicting entries.

type FederationConfig

type FederationConfig struct {
	Clients []ClientConfig `yaml:"mcp_clients,omitempty"`
}

FederationConfig groups multiple federated server entries. Mirrors the shape of `mcp_clients:` in the operator's config.yaml.

type Options

type Options struct {
	// SpecRegistrar overrides the function used to register a Spec.
	// Nil means use tools.Register.
	SpecRegistrar SpecRegistrar

	// RiskRegistrar overrides the function used to publish a tool's
	// risk level. Nil means use risk.Register.
	RiskRegistrar RiskRegistrar

	// Logger is called with informational messages. Nil silences them.
	Logger func(format string, args ...any)

	// ClientBuilder overrides the default transport-routed constructor.
	// Tests inject this to attach an in-process client (mcptest server)
	// without spawning a subprocess. Production wiring leaves it nil.
	ClientBuilder ClientBuilder
}

Options configures a Federation. Zero value is valid: production wiring (tools.Register + risk.Register) is selected when fields are nil.

type RiskRegistrar

type RiskRegistrar func(toolName string, level risk.Level)

RiskRegistrar mirrors risk.Register; the same indirection rationale applies — keep the package free of init-order side effects on the global risk map during tests.

type Sandbox

type Sandbox int

Sandbox identifies an exec-wrapping profile for stdio transports.

const (
	// SandboxNone runs the configured command directly. Suitable only
	// for trusted local tools (e.g. operator-installed CLIs).
	SandboxNone Sandbox = iota

	// SandboxDocker wraps the command as
	// `docker run --rm -i --network=none --read-only <image> [args...]`.
	// The original ClientConfig.Command becomes the image name; args
	// become the container's process args. Env is passed via -e flags.
	SandboxDocker

	// SandboxBwrap uses bubblewrap to isolate filesystem and namespaces:
	// `bwrap --ro-bind / / --tmpfs /tmp --unshare-all --share-net <cmd>`.
	// Network is shared (federated MCP servers typically need it).
	SandboxBwrap

	// SandboxFirejail uses firejail's lightweight sandbox:
	// `firejail --net=none --private <cmd>`. Works inside WSL where
	// bubblewrap and full Docker may be unavailable.
	SandboxFirejail
)

func (Sandbox) String

func (s Sandbox) String() string

type SpecRegistrar

type SpecRegistrar func(tools.Spec)

SpecRegistrar is the surface mcpfed needs from the tools registry. The production implementation is tools.Register — a function value type lets tests inject a recording fake without resetting the global registry.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL