mcpvet

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 12 Imported by: 0

README

mcpvet 🔍

go vet for MCP servers. A static Go binary for CI that snapshots an MCP server's tool surface into a lockfile, fails the build when it silently changes, and fuzzes every tool with inputs generated from that tool's own JSON Schema.

Your agent trusts whatever tool descriptions and schemas a server hands it, at every startup. Nothing pins them. A server that quietly rewrites a tool description tomorrow — the MCP "rug pull" — reprograms your agent, and no test you own will notice.

$ mcpvet lock -- npx -y some-mcp-server
mcpvet: locked 3 tools from some-mcp-server v1.0.0 → mcp.lock.json

# next day, the server updates itself:
$ mcpvet check -- npx -y some-mcp-server
mcpvet: some-mcp-server v1.0.0 — 3 tools

DRIFT vs lockfile:
 ! greet    description_changed  description rewritten (23 → 105 chars) — review for injected instructions
exit 1

And the contract side — inputs generated from each tool's schema, then checked against what the server actually does with them:

$ mcpvet fuzz -timeout 3s -- ./my-mcp-server
mcpvet: testserver v1.0.0 — 3 tools, 28 generated cases

FINDINGS:
 ! lookup_city   accepted_invalid   server accepted input its own schema forbids
     case: missing:city
 ! lookup_city   accepted_invalid   server accepted input its own schema forbids
     case: oversize:city
 ! slow          hang               no response within 3s — an agent would stall here
     case: valid
exit 1

Install

go install github.com/doxuta/mcpvet/cmd/mcpvet@latest

Use in CI

- run: mcpvet check -- npx -y @your/mcp-server   # gate on drift
- run: mcpvet fuzz  -- ./your-server -skip delete_everything

check exits non-zero on breaking drift (tool removed, schema shape changed, new required field, description rewritten); cosmetic changes are reported but don't fail. fuzz additionally exits non-zero on findings.

What it generates

From each tool's own input schema, per field:

Category Cases Expected
valid mid-range values, format-aware (date, email, uri), first enum member accepted
boundary minimum/maximum, one past each, empty string vs minLength edges accepted, past-edge rejected
hostile missing required, type confusion, oversized strings, out-of-enum rejected
payloads SQL/path-traversal/JNDI/template/NUL/emoji/"ignore previous instructions" treated as data, never a crash

A server that accepts what its own schema forbids is a finding: the agent's model reads that schema and will eventually send exactly what it promises is invalid.

Design choices

  • Schemas are handled as decoded JSON, not a typed model, so mcpvet vets any server's schema rather than only the drafts one SDK understands.
  • Shape fingerprint, not byte hash, for the diff: key order and description edits inside a schema don't cause false drift, but a changed type, bound, or required field does. Both are stored — the byte hash still reports "docs-only change".
  • Descriptions are locked too. A description rewrite is treated as breaking: descriptions are the part of the surface that steers the model.
  • Read-only by default. lock and check never call a tool. Fuzzing is opt-in and -skip excludes destructive tools.

Tiếng Việt

mcpvet là "package-lock cho tool của AI agent": chụp lại toàn bộ bề mặt tool của một MCP server vào lockfile, CI sẽ fail khi server âm thầm đổi schema hay đổi mô tả tool (kiểu rug-pull chèn lệnh vào agent), đồng thời sinh input từ chính JSON Schema của tool — hợp lệ, biên, và độc hại — để bắt handler nào nhận cả thứ mà schema của nó cấm, hoặc treo không trả lời.

MIT © Xuan Tai Doan — built with an AI coding agent under human review. Uses the official modelcontextprotocol/go-sdk.

Documentation

Overview

Package mcpvet is a contract-testing and drift-detection tool for Model Context Protocol (MCP) servers: it snapshots a server's tool surface into a lockfile so CI fails when tools, descriptions, or input schemas silently change, and it fuzzes each tool with inputs generated from that tool's own JSON Schema — valid, boundary, and hostile — to find handlers that panic, hang, or accept what their schema forbids.

Schemas are handled as decoded JSON (`map[string]any`) rather than a typed model, so mcpvet works against any server's schema, not only the drafts a particular SDK understands.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WriteLock

func WriteLock(path string, l Lock) error

WriteLock saves a lock as indented JSON.

Types

type Case

type Case struct {
	Name  string         // e.g. "valid", "missing:city", "hostile:oversize:name"
	Args  map[string]any // the tool arguments
	Valid bool           // true if the schema should accept these args
}

Case is one generated tool input plus what mcpvet expects the server to do with it.

func GenerateCases

func GenerateCases(input any) []Case

GenerateCases produces valid, boundary, and hostile inputs for a tool from its input schema. Valid cases exercise the happy path; boundary cases probe min/max edges; hostile cases (type confusion, oversized strings, missing required fields, unexpected extra fields) probe input handling. Every hostile/missing case that the server *accepts* is a finding.

type Drift

type Drift struct {
	Tool   string `json:"tool"`
	Kind   string `json:"kind"` // added, removed, description_changed, schema_changed, required_changed
	Detail string `json:"detail"`
}

Drift is one difference between a stored lock and the live server.

func Diff

func Diff(old, new Lock) []Drift

Diff compares a stored lock against a freshly built one.

func (Drift) Breaking

func (d Drift) Breaking() bool

Breaking reports whether a drift breaks callers (as opposed to a cosmetic change): removals, schema shape changes, and new required fields.

type Finding

type Finding struct {
	Tool     string `json:"tool"`
	Case     string `json:"case"`
	Kind     string `json:"kind"` // crash, hang, accepted_invalid, protocol_error
	Detail   string `json:"detail"`
	Breaking bool   `json:"breaking"`
}

Finding is one problem mcpvet found while exercising a server.

type Lock

type Lock struct {
	Version int        `json:"version"`
	Server  ServerInfo `json:"server"`
	Tools   []ToolLock `json:"tools"`
}

Lock is the snapshot of a server's tool surface — the "package-lock" for an agent's tools. Descriptions are hashed as well as schemas: a silently rewritten description is a prompt-injection vector even when the schema is unchanged.

func BuildLock

func BuildLock(server ServerInfo, tools []ToolSurface) Lock

BuildLock snapshots the given tools.

func ReadLock

func ReadLock(path string) (Lock, error)

ReadLock loads a lock from disk.

type Options

type Options struct {
	// Command and Args launch a stdio MCP server (e.g. "npx", "-y", "some-mcp").
	Command string
	Args    []string
	// Timeout bounds each individual tool call; a call that exceeds it is a
	// "hang" finding.
	Timeout time.Duration
	// Fuzz enables generated-input testing. Without it, mcpvet only snapshots
	// and diffs the tool surface (safe against servers with side effects).
	Fuzz bool
	// SkipTools are tool names never to call (destructive tools).
	SkipTools []string
}

Options configure a vet run.

type Report

type Report struct {
	Server    ServerInfo `json:"server"`
	ToolCount int        `json:"tool_count"`
	CaseCount int        `json:"case_count"`
	Findings  []Finding  `json:"findings"`
	Drifts    []Drift    `json:"drifts,omitempty"`
	Lock      Lock       `json:"-"`
}

Report is the outcome of a vet run.

func Vet

func Vet(ctx context.Context, opts Options) (*Report, error)

Vet connects to the server, snapshots its tools, and optionally fuzzes them.

func (Report) Failed

func (r Report) Failed() bool

Failed reports whether the run should fail CI.

type ServerInfo

type ServerInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

ServerInfo identifies the server that produced the lock.

type ToolLock

type ToolLock struct {
	Name            string `json:"name"`
	DescriptionHash string `json:"description_sha256"`
	DescriptionLen  int    `json:"description_len"`
	SchemaHash      string `json:"input_schema_sha256"`
	SchemaShape     string `json:"input_schema_shape"`
	Required        string `json:"required,omitempty"`
}

ToolLock is one tool's locked surface.

type ToolSurface

type ToolSurface struct {
	Name        string
	Description string
	InputSchema any
}

ToolSurface is the part of an MCP tool mcpvet inspects.

Directories

Path Synopsis
cmd
mcpvet command
Command mcpvet is contract testing and drift detection for MCP servers.
Command mcpvet is contract testing and drift detection for MCP servers.
internal
testserver command
Command testserver is a deliberately flawed MCP server used by mcpvet's own tests and demo: one well-behaved tool, one that accepts input its schema forbids, one that hangs, and one whose description changes when the MCPVET_DEMO_DRIFT environment variable is set.
Command testserver is a deliberately flawed MCP server used by mcpvet's own tests and demo: one well-behaved tool, one that accepts input its schema forbids, one that hangs, and one whose description changes when the MCPVET_DEMO_DRIFT environment variable is set.

Jump to

Keyboard shortcuts

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