periscope

module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Apr 20, 2026 License: GPL-3.0

README

periscope

A four-dimensional quality framework for testing MCP servers.

periscope goes beyond "does the tool return a response" — it tests whether your tools work correctly, whether an LLM actually reaches for them when it should, whether their descriptions are clear enough to be understood unambiguously, and whether they hold up under load.

Four testing dimensions:

  • Functional — correctness across happy-path, boundary, invalid, and adversarial inputs
  • Routing — does the LLM prefer your tools over answering from parametric knowledge?
  • Clarity — are tool names, descriptions, and parameter schemas unambiguous to an LLM?
  • Performance — latency percentiles and throughput under smoke, ramp, spike, and soak profiles

Works with any MCP server over both stdio and Streamable HTTP transports. Single binary, zero infrastructure — results persist to a local SQLite file.

Quick Start

From zero to your first periscope run in under 10 minutes.

  1. Install the periscope binary with Go:

    go install github.com/agenthands/periscope/cmd/periscope@latest
    
  2. Build the example MCP server (a tiny echo server used by the bundled test cases):

    go build -o echo-server ./testdata/integration/echo_server/
    
  3. Create a minimal periscope.yaml in the current directory. Replace /path/to/echo-server with the absolute path to the binary you just built:

    version: "1.0"
    transport:
      type: stdio
      stdio:
        command: "/path/to/echo-server"  # Must be an absolute path — relative paths are rejected at config-load time.
    judge:
      provider: anthropic
      model: "claude-sonnet-4-20250514"
      apiKeyEnv: "ANTHROPIC_API_KEY"
      temperature: 0.0
      maxTokens: 2048
    embeddings:
      provider: voyage
      model: "voyage-3-lite"
      apiKeyEnv: "VOYAGE_API_KEY"
    tests:
      paths:
        - docs/examples/testcases/functional-echo.yaml
    thresholds:
      functional:
        passRate: 0.95
    reporting:
      formats: [markdown]
      outputDir: "./test-results"
      baselineDb: "./test-results/baseline.sqlite"
    
  4. Run the functional dimension against the echo server:

    periscope run --config periscope.yaml --dimension functional --tags smoke
    

    You should see output shaped like this:

    === functional	[PASS] score=1.0000
    
    Overall: PASS
    Total: 1  Passed: 1  Failed: 0  Errored: 0  Skipped: 0
    

    What this means: A line reading Overall: PASS means every gate for the selected dimension cleared its threshold, and the binary exits with code 0 so CI keeps moving. Open test-results/<RunID>.md for the full Markdown report including per-case detail and the exact threshold each gate evaluated.

The Four Dimensions

Each dimension targets a different failure mode. You can run one at a time (with --dimension), or let periscope run execute every dimension your config enables.

  • Functional — Validates that each tool returns the correct output across four input categories: happy-path, boundary, invalid, and adversarial. The gate metric is pass rate (per-dimension fraction of cases that passed), and the default threshold is 0.95. Run this dimension on every PR — it is the fastest to execute and the most likely to catch regressions in your tool's own logic. Assertion modes range from exact-string matching to LLM-judge semantic comparison, so you pay only for the judgement you need.

  • Routing — Measures whether an LLM actually chooses your tool when a user prompt should trigger it, as opposed to answering from its own parametric knowledge. The gate metrics are correct selection rate (did the LLM call the right tool?), parametric fallback rate (did it skip tools entirely?), parameter accuracy, and position-bias total-variation distance (does swapping tool order change the answer?). Run this dimension when you change a tool's name, description, or parameter schema, and whenever you add a tool that might compete with an existing one.

  • Clarity — Evaluates whether tool names, descriptions, and parameter schemas communicate unambiguously to an LLM. Three evaluation methods are available: self-reflection (the judge rates understanding of the description), adversarial-paraphrase (judges a misleading rewrite side-by-side), and hallucination-detection (checks whether the LLM invents parameters that do not exist). The gate metrics are overall score, disambiguation rate, and hallucination rate. Run this dimension after any description rewrite and before shipping a new tool.

  • Performance — Exercises your server under one of four load profiles — smoke, ramp, spike, or soak — and records latency with an HDR histogram so percentiles are accurate at p50, p95, and p99. The gate metrics are the configured SLA latencies (per-case or global), error rate, throughput (rps), and regression against the stored baseline. Run this dimension on a schedule (for example nightly) to catch performance drift before users feel it. The stdio transport is clamped to 10 virtual users so you cannot accidentally fork-bomb your local subprocess.

Installation

The simplest path is go install github.com/agenthands/periscope/cmd/periscope@latest, which drops a periscope binary into $(go env GOBIN) (or $GOPATH/bin). Prebuilt binaries for the release tags are attached to each GitHub release if you prefer not to compile. For local development, git clone the repo and run make build to produce ./periscope in the repo root.

See docs/INSTALL.md for full prereqs, verification steps, and API-key setup.

Usage

Periscope exposes 6 subcommands: run (execute tests), calibrate (tune the LLM judge against human-labeled fixtures), baseline (freeze the current run as the regression baseline), report (re-render a past run from SQLite), discover (probe an MCP server and list its tools), and history prune (retention maintenance). The common workflow:

periscope run --config periscope.yaml
periscope calibrate run --rubric clarity-self-reflection
periscope baseline --update
periscope report --format markdown

See docs/USAGE.md for every flag, expected output, YAML test case schema, and config reference.

Examples

Runnable test case YAMLs — one per dimension — live under docs/examples/testcases/: functional-echo.yaml, routing-echo.yaml, clarity-echo.yaml, and performance-echo.yaml. Each targets the bundled echo server at testdata/integration/echo_server/ so you can execute them end-to-end without pointing at a real production MCP server.

See docs/examples/testcases/ for runnable test cases and docs/examples/ci/ for GitHub Actions and GitLab CI snippets.

CI/CD Integration

periscope run --ci is threshold-gated: the binary exits 0 when every configured dimension clears its gates, and exits 1 the moment any gate fails — so your pipeline fails the same run your PR broke. JUnit XML output (reporting.formats: [junit]) populates the native Tests tab in GitHub Actions and GitLab CI, and the Markdown report doubles as a ready-to-post PR comment via $GITHUB_STEP_SUMMARY or a GitLab comment bot.

See the CI Integration section of docs/USAGE.md for full details and copy-pasteable snippets in docs/examples/ci/.

Contributing

Contributing guide (coming soon) — will land as docs/CONTRIBUTING.md in a later milestone. For now, see open issues and open a PR against main.

Built in Go. Designed for CI/CD.

Directories

Path Synopsis
cmd
periscope command
Package main is the entry point for the periscope binary.
Package main is the entry point for the periscope binary.
internal
assert
Package assert provides the assertion cascade engine for evaluating MCP tool outputs across multiple comparison levels: schema, exact, structural, semantic, and LLM judge.
Package assert provides the assertion cascade engine for evaluating MCP tool outputs across multiple comparison levels: schema, exact, structural, semantic, and LLM judge.
dimensions
Package dimensions defines the DimensionRunner interface and acts as the parent package for individual dimension implementations (functional, routing, clarity, performance).
Package dimensions defines the DimensionRunner interface and acts as the parent package for individual dimension implementations (functional, routing, clarity, performance).
dimensions/clarity
Package clarity implements the clarity dimension runner.
Package clarity implements the clarity dimension runner.
dimensions/functional
Package functional implements the functional dimension runner.
Package functional implements the functional dimension runner.
dimensions/performance
Package performance implements the performance dimension runner.
Package performance implements the performance dimension runner.
dimensions/routing
Package routing implements the routing dimension runner.
Package routing implements the routing dimension runner.
judge
Package judge provides LLM-based evaluation using rubric templates.
Package judge provides LLM-based evaluation using rubric templates.
registry
Package registry handles test case loading, validation, discovery, and grouping.
Package registry handles test case loading, validation, discovery, and grouping.
runner
Package runner provides the TestRunner orchestration engine and RetryController for resilient test execution across all dimensions.
Package runner provides the TestRunner orchestration engine and RetryController for resilient test execution across all dimensions.
store
Package store provides the persistence interface for periscope test results, rolling baselines, calibration data, and history management.
Package store provides the persistence interface for periscope test results, rolling baselines, calibration data, and history management.
types
Package types defines shared primitive types used across all internal packages.
Package types defines shared primitive types used across all internal packages.

Jump to

Keyboard shortcuts

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