conform

package
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package conform implements the end-to-end MCP conformance suite. The `mcp-tui conform <url|cmd>` subcommand runs a curated battery of MCP protocol scenarios plus every verify probe, prints a per-scenario PASS/ FAIL summary, and (when `--report-junit <file>` is set) writes a JUnit XML report consumable by CI dashboards.

Scenario authors implement the Scenario interface; the dispatcher in scenarios.go resolves a name to a typed Scenario value and runs it against the supplied Target. JUnit XML marshaling lives in junit.go.

Index

Constants

This section is empty.

Variables

View Source
var AllScenarios = []string{
	"initialize",
	"tools.list",
	"tools.call",
	"tools.call.isError",
	"resources.list",
	"resources.read",
	"resources.templates.list",
	"prompts.list",
	"prompts.get",
	"sampling.createMessage",
	"elicitation.create",
	"notifications",
	"completion.complete",
	"verify.cross-origin",
	"verify.dns-rebind",
	"verify.content-type",
	"verify.origin-header",
	"verify.mcp-method-headers",
	"verify.seterror-content",
}

AllScenarios is the canonical, ordered list of conformance scenarios. The order doubles as the iteration order — text and JUnit reports both emit results in this sequence so dashboards can diff runs over time.

Sections:

  1. Protocol scenarios (initialize through completion/complete) — driven against the connected target via mcp.Service.
  2. Verify probes — six security/behavior probes from internal/cli/verify prefixed with "verify." for namespacing.

Functions

func AllPassed

func AllPassed(results []ScenarioResult) bool

AllPassed returns true when every non-skipped scenario in results passed. An empty slice counts as failure (consistent with verify.AllPassed) so a run that produced zero scenarios exits non-zero. Skipped scenarios do NOT block a green exit — `--scenario foo` against a target without `foo`'s preconditions should be a successful no-op.

func CountResults

func CountResults(results []ScenarioResult) (passed, failed, skipped int)

CountResults tallies (passed, failed, skipped). Passed includes Skipped because Skipped is a sub-state of Pass — but the dedicated counter lets the text report show "5 passed, 1 failed, 2 skipped" instead of glomming them together.

func IsScenarioName

func IsScenarioName(name string) bool

IsScenarioName reports whether name is one of AllScenarios. The CLI uses this to reject typos in --scenario before connecting.

func WriteJUnitReport

func WriteJUnitReport(w io.Writer, suite JUnitTestSuite) error

WriteJUnitReport marshals suite to w as indented JUnit XML with the canonical XML 1.0 prolog. Returns an error if the underlying writer fails or marshaling fails (which only happens when the struct contains an un-marshalable field, never in our case — but we surface it anyway so callers don't have to wrap).

The output is deterministic for a given suite value: encoding/xml preserves struct field order and slice element order.

Types

type JUnitFailure

type JUnitFailure struct {
	XMLName xml.Name `xml:"failure"`
	Message string   `xml:"message,attr"`
	Type    string   `xml:"type,attr"`
	Body    string   `xml:",chardata"`
}

JUnitFailure holds the failure metadata. Message is the short summary shown in CI dashboards; Type categorises the failure ("Failure" vs "Error" — we always emit "Failure" because conformance is binary). The chardata body carries the detail (stack-trace equivalent).

type JUnitTestCase

type JUnitTestCase struct {
	XMLName   xml.Name      `xml:"testcase"`
	ClassName string        `xml:"classname,attr"`
	Name      string        `xml:"name,attr"`
	Time      string        `xml:"time,attr"`
	Failure   *JUnitFailure `xml:"failure,omitempty"`
}

JUnitTestCase represents a single <testcase> element. ClassName is the suite name (we use "mcp-tui.conform"), Name is the scenario identifier. Time is elapsed-seconds in fixed-point form. Failure is non-nil when the scenario failed; SystemOut carries any captured diagnostic output (skipped for now to keep payloads compact).

type JUnitTestSuite

type JUnitTestSuite struct {
	XMLName  xml.Name        `xml:"testsuite"`
	Name     string          `xml:"name,attr"`
	Tests    int             `xml:"tests,attr"`
	Failures int             `xml:"failures,attr"`
	Errors   int             `xml:"errors,attr"`
	Time     string          `xml:"time,attr"` // seconds, fixed-point — Surefire format
	Cases    []JUnitTestCase `xml:"testcase"`
}

JUnitTestSuite is the top-level <testsuite> element of a JUnit-XML report.

We follow the de-facto Maven Surefire schema (the most widely consumed shape): a testsuite element with attributes for counts and elapsed time, containing testcase elements. Each testcase has classname (the suite), name (the scenario), and time. A failing testcase contains a <failure> element with the failure message and stack/text body.

The struct tags use `xml:"...,attr"` for attributes and `xml:",chardata"` for element bodies. Field order in the struct dictates emission order because encoding/xml marshals fields top-to-bottom.

func BuildJUnitReport

func BuildJUnitReport(suiteName string, results []ScenarioResult) JUnitTestSuite

BuildJUnitReport converts the per-scenario ScenarioResult slice into a JUnit-shaped testsuite. The conversion is pure: same input always produces the same output, scenarios are emitted in the slice's order so the conform-run report mirrors the runner's iteration order.

suiteName populates the testsuite name attribute and is also used as the classname on every testcase so CI dashboards group them under one suite. Pass "mcp-tui.conform" for the canonical run.

type Runner

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

Runner executes scenarios against a Target. It owns a single mcp.Service that is connected lazily on first protocol-level scenario and reused for the remainder of the run, so a full conform pass against an stdio target pays the spawn-and-handshake cost exactly once.

Verify probes never share Runner.svc — they drive their own per-probe connections so a hung verify doesn't stall the whole suite.

Runner is not safe for concurrent use; the conform suite runs scenarios sequentially by design (so failures are easy to read in a CI log).

func NewRunner

func NewRunner(target Target) *Runner

NewRunner builds a Runner for the supplied target without connecting. The first protocol-level scenario triggers connection.

func (*Runner) Close

func (r *Runner) Close()

Close releases the runner's MCP session if one was established. Safe to call multiple times. Idempotent.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, name string) ScenarioResult

Run executes the named scenario and returns its result. Unknown names produce a failed ScenarioResult rather than an error so callers can keep a single result-shaped output channel.

type ScenarioResult

type ScenarioResult struct {
	Name    string        `json:"name"`
	Pass    bool          `json:"pass"`
	Skipped bool          `json:"skipped,omitempty"`
	Error   string        `json:"error,omitempty"`
	Detail  string        `json:"detail,omitempty"`
	Elapsed time.Duration `json:"elapsed"`
}

ScenarioResult is the typed outcome of a single conformance scenario.

Name    — scenario identifier (matches the --scenario flag value)
Pass    — true if the scenario satisfied its acceptance criteria
Skipped — true if the scenario was skipped because the target lacks
          the required capability (counts as Pass for exit-code purposes)
Error   — short, single-line summary shown in the text report
Detail  — multi-line diagnostic body (goes into JUnit <failure>)
Elapsed — wall time spent running the scenario

type Target

type Target struct {
	// URL is the streamable-HTTP endpoint for HTTP-class targets.
	URL string

	// Command + Args are the stdio command for stdio targets.
	Command string
	Args    []string

	// SamplingStub, when non-empty, is installed via
	// sampling.NewTextStubHandler before connect so the sampling scenario
	// (which calls a tool that triggers sampling/createMessage) gets a
	// canned reply.
	SamplingStub string

	// ElicitStub, when non-empty, is installed via
	// elicitation.NewJSONStubHandler before connect so the elicitation
	// scenario gets a canned reply.
	ElicitStub string

	// SamplingTriggerTool overrides the default "sampleLLM" tool name used
	// by the sampling scenario. The scenario calls this tool to provoke a
	// server-initiated sampling/createMessage request.
	SamplingTriggerTool string

	// ElicitTriggerTool overrides the default "startElicitation" tool name
	// used by the elicitation scenario.
	ElicitTriggerTool string

	// CompletionPromptName is the prompt name (or resource template URI when
	// CompletionRefIsResource=true) used to drive completion/complete. When
	// empty the scenario falls back to the first prompt with arguments
	// returned by prompts/list.
	CompletionPromptName    string
	CompletionRefIsResource bool
	CompletionArgumentName  string
	CompletionArgumentValue string
}

Target describes what the conformance suite drives. URL/Command/Args have the same meaning as verify.Target — when both are set, HTTP-class scenarios use URL and stdio-class scenarios use the command (mirroring `verify`).

SamplingStub and ElicitStub are non-empty when the user wants the suite to auto-reply to server-initiated sampling/elicitation requests; the conform runner installs the appropriate stub handler on the connected service before the scenario fires its trigger tool.

Jump to

Keyboard shortcuts

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