MCP testing framework
Test MCP Apps in CI.
No accounts, no keys.
Point moonpeak at any MCP server, written in TypeScript, Python, Go, Rust or Java, and get schema checks, simulations, browser tests in ChatGPT and Claude style hosts, and evals that ask real models which tool they'd pick.
Why use an MCP testing framework?
MCP Apps fail in ways unit tests don't catch: a schema the host rejects, a View that never receives its data, a tool the model never picks. Testing in the real hosts means accounts, tunnels and AI credits on every run. moonpeak replicates the hosts locally so the same checks run on every push.
- Simulations: JSON fixtures with input, mocked output, expectations and host settings.
- Schema and View checks: every tool schema validated, every View built and rendered.
- E2E: Playwright drives the inspector in ChatGPT and Claude style hosts.
- Visual regression: screenshot tests tagged
@visual. - Evals: do Claude and GPT models pick the right tool with the right arguments?
$ moonpeak test moonpeak test my-app ✓ schema moon-calendar ✓ schema plan-night ✓ view ui://my-app/calendar.html ✓ view ui://my-app/review.html ✓ sim plan-night#dark-new-moon-night ✓ sim plan-night#bad-date all passed · 21 checks
[
{
"name": "dark new moon night",
"prompt": "Stargazing at Bromo on Dec 9 2026?",
"input": { "date": "2026-12-09", "place": "Bromo" },
"expect": { "output": { "phase": "New Moon", "darkness": "excellent" } }
},
{
"name": "bad date",
"input": { "date": "next friday" },
"expect": { "isError": true, "text": "not a date" }
}
]Simulations are just JSON.
Partial matching on output, "re:..." for regexes, "*" for any value, $length and $minLength for arrays. Mocked output lets you design a View before the backend exists. moonpeak test init --server writes a first fixture for every tool of an existing server.
E2E with real host behaviour.
Tests get an inspector fixture that opens your View in a ChatGPT or Claude style host, with theme, device and display mode, and an mcp fixture that calls tools directly. Sent messages and model context are captured so you can assert on them.
import { test, expect } from "moonpeak/test";
test("confirm tells the model", async ({ inspector }) => {
const view = await inspector.open({
tool: "plan-night",
sim: "plan-night#dark-new-moon-night",
host: "claude",
});
await view.getByRole("button", { name: "Confirm plan" }).click();
await expect(inspector.modelContext())
.toContainText("confirmed the stargazing plan");
});{
"models": ["claude-opus-5-5", "gpt-5"],
"runs": 3,
"cases": [
{
"prompt": "plan stargazing at Bromo on 2026-12-09",
"expect": { "tool": "plan-night",
"args": { "date": "2026-12-09" } }
},
{ "prompt": "write me a haiku", "expect": { "tool": null } }
]
}Evals against real models.
moonpeak test --eval sends your tool list to Claude and OpenAI models and checks which tool they call, with which arguments, over several runs. App-only tools are left out, exactly as hosts do. Bring your own ANTHROPIC_API_KEY or OPENAI_API_KEY.
Any server, any language.
Everything above works against a server moonpeak didn't build: --server speaks Streamable HTTP to it, --header adds auth. Run it in GitHub Actions next to the server's own tests.
- run: npx playwright install --with-deps chromium - run: npx moonpeak test --server http://localhost:8000/mcp - run: npx moonpeak test --e2e