mockmcp

package module
v0.0.0-...-dcd475b Latest Latest
Warning

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

Go to latest
Published: May 20, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

mockmcp — MCP test fixture

A minimal MCP server exposing three tools (echo, add_numbers, get_time) over both the streamable-HTTP and SSE transports, optionally with TLS. Modeled on mockllm — usable both as a standalone binary spawned from shell scripts and as an in-process library imported from Go tests.

Typical deployment is on a developer's host while a local container/cluster dials back via host.docker.internal:<port> (Mac) or 172.17.0.1:<port> (Linux).

Layout

File Purpose
server.go Server, Options, NewServer, Start, Stop, Requests
tools.go Tool parameter types and the default echo/add_numbers/get_time handlers (RegisterDefaultTools)
certs.go NewSelfSignedCertsForTest — programmatic self-signed CA + leaf for HTTPS tests
recorder.go RecordedRequest and the request-capture middleware
middleware.go HeaderLoggingMiddleware, ServerIDHeader, and the server-ID middleware
cmd/mockmcp/main.go Standalone binary entry point — flag parsing + signal handling

Endpoints

Path Handler
/mcp streamable-HTTP MCP transport (constant mockmcp.MCPPath)
/sse SSE MCP transport (constant mockmcp.SSEPath)
/healthz 200 OK liveness probe

Both transports are always mounted on the same listener; tests point their client at whichever path matches the protocol they're exercising. Paths are fixed to the MCP convention.

Standalone binary

Plain HTTP on :13443 (default — sufficient for tests that don't exercise the upstream TLS path):

go run github.com/kagent-dev/mockmcp/cmd/mockmcp@latest

HTTPS with a real cert (mint a CA + server cert offline first; see "TLS end-to-end" below):

go run github.com/kagent-dev/mockmcp/cmd/mockmcp@latest \
    --cert /path/to/server.crt --key /path/to/server.key

Flags:

Flag Default Notes
--addr :13443 listen address; :0 picks a free port (logged on startup)
--cert `` PEM cert file; pair with --key to serve HTTPS
--key `` PEM key file; pair with --cert to serve HTTPS
--log-headers false log every inbound request's headers in sorted order, values verbatim; test-only

In-process library

Minimal HTTPS fixture with programmatic certs
import (
    "context"

    "github.com/kagent-dev/mockmcp"
)

func TestSomething(t *testing.T) {
    caPEM, certPEM, keyPEM, err := mockmcp.NewSelfSignedCertsForTest(
        "host.docker.internal", "localhost", "127.0.0.1",
    )
    if err != nil {
        t.Fatal(err)
    }

    server, err := mockmcp.NewServer(mockmcp.Options{
        Addr:    "127.0.0.1:0",
        CertPEM: certPEM,
        KeyPEM:  keyPEM,
    })
    if err != nil {
        t.Fatal(err)
    }
    baseURL, err := server.Start(context.Background())
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() { _ = server.Stop(context.Background()) })

    // baseURL is e.g. "https://127.0.0.1:53210"; clients dial baseURL + "/mcp"
    // for streamable-HTTP or baseURL + "/sse" for SSE. Write `caPEM` into a
    // K8s Secret if a downstream resource (e.g. RemoteMCPServer.spec.tls.caCertSecretRef)
    // needs to verify the server cert.
    _, _ = baseURL, caPEM
}
Options reference
Field Purpose
Addr listen address (:13443 default; :0 picks a free port)
CertPath, KeyPath HTTPS via PEM files on disk; both set together
CertPEM, KeyPEM HTTPS via in-memory PEM bytes; both set together; mutually exclusive with the file pair
LogHeaders log every inbound request's headers to Logger (or stderr if unset)
RecordRequests capture inbound method/path/headers/body into an internal slice readable via Server.Requests()
ServerID when non-empty, echo Mockmcp-Server-Id: <id> on every response (useful when a test runs multiple instances)
Logger *log.Logger for startup messages and middleware logs (defaults to log.Default())
Programmatic self-signed certificates

mockmcp.NewSelfSignedCertsForTest(hosts ...string) (caPEM, certPEM, keyPEM []byte, err error) mints a self-signed CA, a leaf certificate signed by that CA, and the leaf's private key — all PEM-encoded — using only the Go standard library (crypto/ecdsa, crypto/x509). Hosts that parse as IP literals land in IPAddresses; the rest land in DNSNames.

The helper is deliberately parameter-free beyond hosts and uses fixed cryptographic choices (ECDSA P-256, SHA-256, ~1y validity, 1h backdating to absorb verifier clock skew). Callers that need different parameters should mint their own with crypto/x509.

Test-only — do not embed in production code paths.

Request recorder
server, _ := mockmcp.NewServer(mockmcp.Options{
    RecordRequests: true,
})
server.Start(ctx)
// ... client makes requests ...
for _, r := range server.Requests() {
    if r.Path == "/mcp" {
        // r.Method, r.Headers, r.Body, r.ReceivedAt
    }
}

Server.Requests() returns a snapshot copy. Opt-in because body capture has a per-request memory cost; long-running fixtures that don't need it should leave RecordRequests false.

Server-ID response header
upA, _ := mockmcp.NewServer(mockmcp.Options{ServerID: "A", Addr: ":13443"})
upB, _ := mockmcp.NewServer(mockmcp.Options{ServerID: "B", Addr: ":13444"})

Every response from upA carries Mockmcp-Server-Id: A (constant mockmcp.ServerIDHeader); from upB, Mockmcp-Server-Id: B. Empty ServerID leaves the header off entirely.

Default tool set on a custom server

mockmcp.RegisterDefaultTools(server *mcp.Server) is exported for callers that own their own *mcp.Server and want the default tool set attached to it.

TLS end-to-end (file-based cert recipe)

For the standalone binary or for tests that prefer offline-minted certs, the openssl recipe is:

# CA private key + self-signed CA cert
openssl genrsa -out .tls/ca.key 4096
openssl req -x509 -new -nodes -key .tls/ca.key -sha256 -days 3650 \
    -out .tls/ca.crt -subj "/CN=mockmcp-ca"

# Server key + CSR + cert signed by the CA, with the SANs cluster pods
# will dial via host.docker.internal.
cat > .tls/server.ext <<'EOF'
subjectAltName = DNS:host.docker.internal, DNS:localhost, IP:127.0.0.1, IP:172.17.0.1
EOF
openssl genrsa -out .tls/server.key 2048
openssl req -new -key .tls/server.key -out .tls/server.csr \
    -subj "/CN=host.docker.internal"
openssl x509 -req -in .tls/server.csr -CA .tls/ca.crt -CAkey .tls/ca.key \
    -CAcreateserial -out .tls/server.crt -days 365 -sha256 -extfile .tls/server.ext

Run mockmcp with --cert .tls/server.crt --key .tls/server.key, create a Kubernetes Secret in the consumer's namespace from ca.crt, and reference that Secret from the consumer's CA bundle field.

For in-process Go tests, prefer NewSelfSignedCertsForTest — it avoids the openssl dependency and per-developer cert files.

Verifying header propagation

Set Options.LogHeaders: true (or pass --log-headers to the binary) to print one log block per inbound request showing the full header set as seen at the MCP server. Useful for verifying static headers baked into client config, runtime header pass-through, or any token-rewriting layer in front of the server.

Example log line:

==> POST /mcp from 10.244.0.7:43210
    Accept: application/json
    Authorization: Bearer ghs_...
    Content-Type: application/json
    User-Agent: example-client/0.1.0
    X-Tenant: acme

Values are logged verbatim — keep LogHeaders off in any setup that could see real credentials. For programmatic assertions on traffic, prefer RecordRequests + Server.Requests() over grepping stderr.

Limitations

  • Three baked-in tools only; embedders that need a custom tool set should compose their own *mcp.Server and call RegisterDefaultTools (or skip it).
  • DNS-rebinding protection in the MCP SDK is disabled at the streamable-HTTP handler level so cluster-pod-to-host dials via host.docker.internal succeed. Acceptable for a test fixture; not appropriate for a production MCP server.
  • Paths are fixed: streamable-HTTP at /mcp, SSE at /sse, health at /healthz. Not configurable.

License

Apache 2.0. See LICENSE.

Documentation

Overview

Package mockmcp is a minimal MCP test fixture. It exposes a small set of tools (echo, add_numbers, get_time) over the streamable-HTTP transport, optionally with TLS, so end-to-end tests have a non-trivial MCP server to point at without depending on a real upstream.

The library is designed for two usage shapes:

  • As an in-process fixture from Go tests: construct via NewServer, call Start to begin serving and obtain the base URL, defer Stop.
  • As a standalone binary spawned as a subprocess: see the cmd/mockmcp entrypoint, which wires the same Options from flags.

Transport is plaintext HTTP unless both Options.CertPath and Options.KeyPath are set, in which case the server serves HTTPS with the operator-supplied cert. Asymmetric configuration (one set, the other empty) is rejected by NewServer.

Index

Constants

View Source
const (
	// DefaultAddr is the listen address used when Options.Addr is empty.
	DefaultAddr = ":13443"
	// MCPPath is the fixed URL path the streamable-HTTP handler binds
	// under. Not configurable — /mcp is the MCP convention and no real-
	// world test needs a different path.
	MCPPath = "/mcp"
	// SSEPath is the fixed URL path the SSE handler binds under. Not
	// configurable for the same reason as MCPPath.
	SSEPath = "/sse"
)
View Source
const ServerIDHeader = "Mockmcp-Server-Id"

ServerIDHeader is the response header populated when Options.ServerID is non-empty. Tests running multiple fixture instances disambiguate which one handled a given request by reading this header.

Variables

View Source
var ErrAlreadyStarted = errors.New("mockmcp: server already started")

ErrAlreadyStarted is returned by Server.Start when called more than once.

Functions

func HeaderLoggingMiddleware

func HeaderLoggingMiddleware(next http.Handler, logger *log.Logger) http.Handler

HeaderLoggingMiddleware wraps the given handler with a logger that prints every inbound request's headers in sorted order with values verbatim. Each request produces one multi-line log entry so test runs have deterministic, grep-able output.

Test-only. Values are logged unredacted; do not enable in any setup that could see real credentials.

func NewSelfSignedCertsForTest

func NewSelfSignedCertsForTest(hosts ...string) (caPEM, certPEM, keyPEM []byte, err error)

NewSelfSignedCertsForTest mints a self-signed CA, a leaf certificate signed by that CA, and the leaf's private key — all PEM-encoded — using only the Go standard library. The leaf is suitable for an HTTPS server whose clients verify against the returned CA.

hosts are SANs embedded in the leaf certificate. Inputs that parse as IP literals (e.g. "127.0.0.1", "::1") land in IPAddresses; the rest land in DNSNames. At least one host is required.

The helper is deliberately parameter-free beyond hosts and uses fixed cryptographic choices (ECDSA P-256, SHA-256, ~1y validity, 1h backdating to tolerate small verifier clock skew). Callers that need different parameters should mint their own certificates with crypto/x509 directly.

Test-only. Do not embed the output into production code paths.

func RegisterDefaultTools

func RegisterDefaultTools(server *mcp.Server)

RegisterDefaultTools attaches mockmcp's default tool set (echo, add_numbers, get_time) to the supplied MCP server. Exposed so embedders can compose the default set with their own tools on a server they own.

Types

type AddNumbersParams

type AddNumbersParams struct {
	A float64 `json:"a" jsonschema:"the first number"`
	B float64 `json:"b" jsonschema:"the second number"`
}

AddNumbersParams is the input schema for the "add_numbers" tool.

type EchoParams

type EchoParams struct {
	Message string `json:"message" jsonschema:"the message to echo back"`
}

EchoParams is the input schema for the "echo" tool.

type GetTimeParams

type GetTimeParams struct {
	Timezone string `json:"timezone,omitempty" jsonschema:"IANA timezone (e.g. UTC, America/New_York); defaults to UTC"`
}

GetTimeParams is the input schema for the "get_time" tool.

type Options

type Options struct {
	// Addr is the TCP address to listen on (e.g. ":13443" or "127.0.0.1:0").
	// Empty defaults to DefaultAddr. Use ":0" to bind a random free port;
	// the actual address is reflected in the base URL returned by Start.
	Addr string

	// CertPath and KeyPath enable HTTPS when both are set. Asymmetric
	// configuration is rejected by NewServer so an operator who omits one
	// by mistake gets a fast failure instead of a silent fall-through to
	// plaintext.
	CertPath string
	KeyPath  string

	// CertPEM and KeyPEM enable HTTPS via in-memory PEM-encoded certificate
	// material. Like CertPath/KeyPath they must be set together. CertPEM
	// may contain a leaf followed by intermediates; tls.X509KeyPair
	// validates the chain. Mutually exclusive with CertPath/KeyPath —
	// NewServer rejects a configuration that mixes file-based and
	// in-memory cert input.
	CertPEM []byte
	KeyPEM  []byte

	// LogHeaders enables a middleware that logs every inbound request's
	// headers in sorted order with values verbatim. Test-only — leave off
	// in any setup that could see real credentials.
	LogHeaders bool

	// RecordRequests, when true, installs a middleware that captures the
	// method, path, headers, and body of every inbound request into an
	// internal slice. Test code reads the snapshot via Server.Requests().
	// Opt-in because body capture has a per-request memory cost.
	RecordRequests bool

	// ServerID, when non-empty, is echoed on every response as the
	// `Mockmcp-Server-Id` header (see ServerIDHeader). Useful when a test
	// runs multiple fixture instances and needs to distinguish them. Empty
	// leaves the header off.
	ServerID string

	// Logger receives header logs and startup messages. Nil falls back to
	// the standard logger.
	Logger *log.Logger
}

Options configures a Server.

type RecordedRequest

type RecordedRequest struct {
	Method     string
	Path       string
	Headers    http.Header
	Body       []byte
	ReceivedAt time.Time
}

RecordedRequest is a snapshot of an inbound HTTP request captured by the request recorder. Tests use these snapshots to make positive assertions about traffic the fixture served — "did the controller's ListTools call arrive?", "did this request carry the expected header?" — without grepping log output.

type Server

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

Server is the running fixture. Zero value is not usable; construct with NewServer.

func NewServer

func NewServer(opts Options) (*Server, error)

NewServer validates options and constructs a Server. Returns an error when cert/key fields are set asymmetrically, when file-based and in-memory cert input are mixed, or when Path and SSEPath collide.

func (*Server) Addr

func (s *Server) Addr() net.Addr

Addr returns the actual listener address. Useful when Options.Addr was ":0" and the caller needs to discover the chosen port. Returns nil if Start has not yet succeeded.

func (*Server) Requests

func (s *Server) Requests() []RecordedRequest

Requests returns a snapshot of the requests captured since the server started. Returns nil when Options.RecordRequests was false.

func (*Server) Start

func (s *Server) Start(_ context.Context) (string, error)

Start binds the listener, registers the default tool set, and begins serving in a background goroutine. Returns the base URL clients should dial (e.g. "http://127.0.0.1:13443"). Subsequent calls return ErrAlreadyStarted.

The ctx parameter is accepted for API symmetry with related fixtures (notably mockllm); the server itself runs until Stop is called.

func (*Server) Stop

func (s *Server) Stop(ctx context.Context) error

Stop gracefully shuts down the HTTP server, draining in-flight requests up to the ctx deadline. Safe to call multiple times; subsequent calls are no-ops.

Directories

Path Synopsis
cmd
mockmcp command
Command mockmcp runs the mockmcp MCP test fixture as a standalone binary.
Command mockmcp runs the mockmcp MCP test fixture as a standalone binary.

Jump to

Keyboard shortcuts

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