Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,16 @@ await Promise.all([
outdir: "dist/src/server",
external: PEER_EXTERNALS,
}),
// The validator runs in Node (CLI + library), not the browser; playwright
// stays external because it is an optional requirement for behavioral checks.
buildJs("src/validator/index.ts", {
outdir: "dist/src/validator",
target: "node",
external: [...PEER_EXTERNALS, "playwright"],
}),
buildJs("src/validator/cli.ts", {
outdir: "dist/src/validator",
target: "node",
external: [...PEER_EXTERNALS, "playwright"],
}),
]);
18 changes: 18 additions & 0 deletions docs/testing-mcp-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ description: Test MCP Apps locally with the basic-host reference implementation

This guide covers two approaches for testing your MCP App: using the `basic-host` reference implementation for local development, or using an MCP Apps-compatible host like Claude\.ai or VS Code.

## Validate against the specification

The SDK ships a validator that checks your server and app against the app-side requirements of the MCP Apps specification — resource format, tool metadata, CSP declarations, and (with [Playwright](https://playwright.dev) installed) the app's observable protocol behavior under a mock host:

```bash
# Validate a running server (streamable HTTP)
npx mcp-app-validator http://localhost:3001/mcp

# Validate a server over stdio, or a built HTML document directly
npx mcp-app-validator --stdio node dist/server.js
npx mcp-app-validator --html dist/mcp-app.html

# CI usage: JSON report, exit code 1 on any MUST-level violation
npx mcp-app-validator http://localhost:3001/mcp --json
```

Every finding cites the rule it violates and the spec section the rule derives from. See the {@link validator! validator} API documentation to run it programmatically.

## Test with basic-host

The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example in this repository is a reference host implementation that lets you select a tool, call it, and see your App UI rendered in a sandboxed iframe.
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,15 @@
"types": "./dist/src/server/index.d.ts",
"default": "./dist/src/server/index.js"
},
"./validator": {
"types": "./dist/src/validator/index.d.ts",
"default": "./dist/src/validator/index.js"
},
"./schema.json": "./dist/src/generated/schema.json"
},
"bin": {
"mcp-app-validator": "./dist/src/validator/cli.js"
},
"files": [
"dist"
],
Expand Down
73 changes: 73 additions & 0 deletions src/validator/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env node
/**
* CLI for the MCP App validator.
*
* Usage:
* mcp-app-validator <http(s) server url> [flags]
* mcp-app-validator --stdio <command> [args...] [flags]
* mcp-app-validator --html <file.html> [flags]
*
* Flags:
* --json Emit the report as JSON
* --no-behavioral Skip behavioral (headless browser) checks
*
* Exit codes: 0 = no errors (warnings allowed), 1 = errors found,
* 2 = usage or connection failure.
*
* @module validator
*/

import { readFile } from "node:fs/promises";

import {
errorCount,
formatJson,
formatPretty,
validateApp,
type ValidationTarget,
} from "./index.js";

function usage(): never {
console.error(
[
"Usage:",
" mcp-app-validator <http(s) server url> [--json] [--no-behavioral]",
" mcp-app-validator --stdio <command> [args...] [--json] [--no-behavioral]",
" mcp-app-validator --html <file.html> [--json]",
].join("\n"),
);
process.exit(2);
}

async function main(): Promise<void> {
const args = process.argv.slice(2);
const json = args.includes("--json");
const behavioral = !args.includes("--no-behavioral");
const positional = args.filter((a) => !a.startsWith("--"));

let target: ValidationTarget;
if (args.includes("--html")) {
const path = args[args.indexOf("--html") + 1];
if (!path) usage();
target = { html: await readFile(path, "utf-8"), label: path };
} else if (args.includes("--stdio")) {
const command = args
.slice(args.indexOf("--stdio") + 1)
.filter((a) => !a.startsWith("--"));
if (command.length === 0) usage();
target = { command };
} else if (positional.length === 1 && /^https?:\/\//.test(positional[0])) {
target = { url: positional[0] };
} else {
usage();
}

const report = await validateApp(target, { behavioral });
console.log(json ? formatJson(report) : formatPretty(report));
process.exit(errorCount(report) > 0 ? 1 : 0);
}

main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(2);
});
Loading