MCP Release validates MCP servers. It verifies the protocol handshake, discovers tools, validates their schemas, and checks for stdio protocol violations. Supports both HTTP/SSE remote servers and local stdio servers (spawned processes). It does not execute tools or accept credentials.
Web app: https://mcprelease.dev Demo endpoint: https://mcp-release-fixture.vercel.app/mcp Repository: https://github.com/daranium2020/mcp-release
The web checker at https://mcprelease.dev supports public HTTPS endpoints only. It does not accept credentials and cannot reach private networks or localhost.
- Open https://mcprelease.dev
- Enter a public HTTPS MCP endpoint URL and click Run Release Check
- Review the report. Download it as JSON or Markdown if needed.
To try without a real server, paste https://mcp-release-fixture.vercel.app/mcp directly.
The CLI runs on your machine and supports public, private, localhost, and authenticated MCP endpoints. Credentials stay in your environment and are never stored.
The web checker at https://mcprelease.dev only accepts public HTTPS endpoints without credentials. Use the CLI or GitHub Action for private networks, localhost, staging servers, or any endpoint that requires authentication.
# Install globally
npm install -g @mcp-release/cli
# Or run directly without installing
npx -y @mcp-release/cli check https://your-mcp-server.example.com/mcpHTTP/SSE endpoints:
# Public endpoint
mcp-release check https://your-mcp-server.example.com/mcp
# JSON output
mcp-release check https://example.com/mcp --json
# Markdown output
mcp-release check https://example.com/mcp --markdown
# Write report to file
mcp-release check https://example.com/mcp --json --out report.json
# Bearer token from environment variable (recommended for secrets)
MCP_TOKEN=your-token mcp-release check https://staging.example.com/mcp --bearer-token-env MCP_TOKEN
# Literal header (for non-secret values only)
mcp-release check https://staging.example.com/mcp --header "X-Tenant-Id: acme"
# Header value from environment variable
MY_KEY=secret mcp-release check https://staging.example.com/mcp --header-env "X-API-Key=MY_KEY"
# Localhost or private network endpoint
mcp-release check http://localhost:4000/mcp --allow-http
# Exit 4 on WARNING (e.g., AUTH_REQUIRED)
MCP_TOKEN=your-token mcp-release check https://staging.example.com/mcp \
--bearer-token-env MCP_TOKEN --fail-on-warningLocal stdio servers (new in v0.2.0):
# Spawn a local server by command string; validate over stdin/stdout
mcp-release check --stdio --command "npx -y my-mcp-server"
# With a working directory
mcp-release check --stdio --command "node dist/server.js" --cwd /path/to/project
# JSON output
mcp-release check --stdio --command "node dist/server.js" --json
# Write report to file
mcp-release check --stdio --command "npx -y my-mcp-server" --json --out report.jsonStdio validation runs entirely locally. No data is sent to the web app or any remote service.
From this repo (without installing):
pnpm build
node packages/cli/dist/index.js check https://mcp-release-fixture.vercel.app/mcpCLI exit codes:
| Code | Meaning |
|---|---|
0 |
PASS |
1 |
FAIL (validation found a blocking issue) |
2 |
Invalid CLI usage (bad arguments, missing env var) |
3 |
Transport error, timeout, or unexpected runtime error |
4 |
WARNING and --fail-on-warning was set |
All CLI options for check:
| Option | Description |
|---|---|
--stdio |
Validate a local stdio server (spawned process). Requires --command. |
--command <cmd> |
Command string to spawn the stdio server (e.g. "npx -y my-mcp-server"). |
--cwd <dir> |
Working directory for the spawned process (stdio mode only). |
--header "Name: value" |
Add a request header (HTTP mode). Repeatable. |
--header-env "Name=VAR" |
Read a header value from an env var (HTTP mode). Repeatable. |
--bearer-token-env VAR |
Read a bearer token from an env var; sends Authorization: Bearer <token> (HTTP mode). |
--timeout-ms <ms> |
Request/startup timeout in milliseconds (default: 10000). |
--max-redirects <n> |
Maximum redirects to follow (HTTP mode, default: 3). |
--allow-http |
Allow HTTP connections for localhost/development (HTTP mode). |
--json |
Print JSON report to stdout. |
--markdown |
Print Markdown report to stdout. |
--out <path> |
Write report to file (JSON by default; Markdown if --markdown). |
--config <path> |
Path to a mcp-release.config.yml file. Runs all scenarios defined in the file. |
--fail-on-warning |
Exit 4 when overall status is WARNING. |
The --config flag runs a sequence of named scenarios against a single MCP endpoint, each with independent headers, expected outcomes, retry settings, and timeouts. This is the recommended approach for authenticated endpoints and regression testing.
Config file format (mcp-release.config.yml):
version: 1
endpoint: https://api.example.com/mcp
# Base headers sent with all requests. ${VAR} is resolved from environment at runtime.
headers:
Authorization: Bearer ${API_TOKEN}
timeouts:
connectMs: 5000 # TCP connection timeout (default: 10000)
responseMs: 10000 # HTTP response timeout, separate from connect (default: timeoutMs)
retries:
maxAttempts: 3 # Total attempts including first (default: 1 = no retry)
backoffMs: 500 # Fixed wait between retries (default: 1000)
retryOn: # Categories to retry — all disabled by default
- rate-limit # HTTP 429
- server-error # 5xx responses
- connection-failure # Connect failures and connect timeouts
- response-timeout # Response timeouts
scenarios:
# Expected positive: server responds correctly with provided token
- name: healthy
expect:
result: pass
# Expected negative: calling without token should return 401
- name: missing-auth
removeHeaders:
- Authorization
expect:
httpStatus: 401
# Custom token override: per-scenario header replacement
- name: custom-token
headers:
Authorization: Bearer ${READONLY_TOKEN}
expect:
result: passEnvironment variable substitution:
Header values use ${VAR_NAME} syntax. Variables are resolved at runtime from the process environment, never at parse time. Unresolved variables throw an error before any requests are made. Resolved values are never printed or logged.
API_TOKEN=your-token READONLY_TOKEN=read-only-token \
mcp-release check --config mcp-release.config.ymlReport formats for config runs:
mcp-release check --config mcp-release.config.yml # terminal (default)
mcp-release check --config mcp-release.config.yml --json # JSON
mcp-release check --config mcp-release.config.yml --markdown # Markdown
mcp-release check --config mcp-release.config.yml --markdown --out report.mdGitHub Actions with config file:
- name: Validate MCP server
run: |
mcp-release check --config mcp-release.config.yml --markdown >> $GITHUB_STEP_SUMMARY
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
READONLY_TOKEN: ${{ secrets.READONLY_TOKEN }}The GitHub Action supports public, staging, and authenticated MCP endpoints. Secrets stay in GitHub Actions secrets and are masked in logs.
Reference by a tagged release or by commit SHA for pinned usage:
HTTP/SSE endpoint:
- name: Validate MCP server
uses: daranium2020/mcp-release@v0.3.0
with:
endpoint: https://staging.example.com/mcp
bearer-token-env: MCP_TOKEN # reads token from env; never put secrets inline
fail-on: fail # optional: fail (default) | warning
timeout-ms: 10000 # optional
format: markdown # optional: json | markdown | both
env:
MCP_TOKEN: ${{ secrets.MCP_TOKEN }}Local stdio server (new in v0.2.0):
- name: Validate local MCP server
uses: daranium2020/mcp-release@v0.3.0
with:
transport: stdio
command: npx -y my-mcp-server
working-directory: ./packages/my-server # optional
fail-on: fail
format: markdownPass all secrets through the env block using GitHub Actions secrets (${{ secrets.YOUR_SECRET }}). Never put secret values directly in with: inputs or workflow YAML.
All inputs:
| Input | Description |
|---|---|
transport |
http (default) or stdio. |
endpoint |
MCP endpoint URL (required when transport: http). |
command |
Command to spawn the stdio server (required when transport: stdio). |
working-directory |
Working directory for the spawned process (stdio mode). |
bearer-token-env |
Name of an env var containing a bearer token. Sends Authorization: Bearer <token> (HTTP mode). |
header |
Newline-separated Name: value pairs added to every request (HTTP mode). |
header-env |
Newline-separated Name=ENV_VAR pairs. Values read from environment (HTTP mode). |
fail-on |
fail (default) or warning. |
timeout-ms |
Request/startup timeout in milliseconds. |
format |
json, markdown, or both. |
Outputs: status, failures, warnings, tools, report-path, pass-count, warning-count, fail-count, report-json, report-markdown
The action annotates the workflow job with findings and writes a summary to the GitHub Actions job summary.
| Area | Details |
|---|---|
| Protocol | MCP initialization handshake, protocol version negotiation, transport response codes |
| Tool schemas | Tool names (non-empty, valid characters), descriptions, inputSchema (valid JSON Schema, Ajv-compilable), outputSchema (if present), duplicate names |
| Network safety (HTTP) | SSRF protection, DNS pinning, redirect chain validation (up to 3 hops), HTTPS enforcement across all redirects |
| Stdio transport (stdio) | Non-JSON lines on stdout (STDIO_UNEXPECTED_OUTPUT), malformed MCP messages (STDIO_FRAMING_ERROR), response size limit (STDIO_RESPONSE_SIZE_EXCEEDED), unclean shutdown (STDIO_SHUTDOWN_TIMEOUT) |
| Auth & resilience (v0.3.0) | 401/403 classification, expired vs invalid credential detection, 429 rate-limit with Retry-After, connect/response timeout types, configurable retries, scenario-based expected-negative testing |
| Reports | Findings exportable as JSON or Markdown |
What is not checked: tools are never invoked, runtime correctness of tool responses is not assessed.
| Result | Meaning |
|---|---|
| PASS | All checks completed without a blocking or incomplete condition. A PASS does not guarantee universal security or correctness. It reflects the checks MCP Release ran. |
| WARNING | One or more checks could not complete or found a non-blocking issue. Example: AUTH_REQUIRED (server returned 401, no credentials provided). |
| FAIL | One or more checks found a blocking condition. Examples: AUTH_INVALID, AUTH_EXPIRED, AUTH_FORBIDDEN, RATE_LIMITED, CONNECT_TIMEOUT, RESPONSE_TIMEOUT. |
| Code | Severity | Condition |
|---|---|---|
AUTH_REQUIRED |
WARNING | 401 response without credentials — server requires auth |
AUTH_INVALID |
FAIL | 401 response with credentials (including RFC 6750 error="invalid_token") |
AUTH_EXPIRED |
FAIL | 401 response with credentials + explicit unambiguous expiry code: error="expired" or error="token_expired" |
AUTH_FORBIDDEN |
FAIL | 403 response (insufficient permissions) |
SCENARIO_MISMATCH |
FAIL | Actual outcome did not match the declared expect block |
Limitation: AUTH_EXPIRED is only produced when the server returns an unambiguous, expiry-specific WWW-Authenticate: Bearer error= value. The RFC 6750 standard code error="invalid_token" is classified as AUTH_INVALID because it covers expired, revoked, and malformed tokens — it is too broad to unambiguously signal expiry. Most OAuth 2.0 servers use invalid_token and will produce AUTH_INVALID. Response bodies and error_description fields are never read or included in reports.
| Code | Severity | Condition |
|---|---|---|
RATE_LIMITED |
FAIL | 429 Too Many Requests |
RETRY_AFTER_INVALID |
WARNING | 429 with an unparseable Retry-After header |
RETRY_EXHAUSTED |
FAIL | All configured attempts failed; or Retry-After exceeds the 60s cap |
CONNECT_TIMEOUT |
FAIL | TCP connection timed out before establishment |
RESPONSE_TIMEOUT |
FAIL | Connection established but server did not send a response in time |
SCENARIO_TIMEOUT |
FAIL | Scenario exceeded its per-scenario time budget (never retried) |
Retries are off by default. Each failure category must be explicitly listed in retries.retryOn. A retry only happens when retries.maxAttempts > 1 AND the relevant category is enabled.
| Condition | retryOn category |
Default |
|---|---|---|
| HTTP 429 (RATE_LIMITED) | rate-limit |
off |
| 5xx responses | server-error |
off |
| Connection failures and connect timeouts | connection-failure |
off |
| Response timeouts | response-timeout |
off |
| 401, 403 | — | Never retried |
| 400, schema errors, malformed MCP | — | Never retried |
| Scenario timeout | — | Never retried |
Rate-limit retries honour the server's Retry-After header (integer seconds or HTTP date), capped at 60 seconds. Values above the cap produce RETRY_EXHAUSTED immediately without waiting.
- Tools are discovered via
tools/listbut never invoked. No arguments are constructed or sent. - Only public HTTPS endpoints are accepted by the web checker. HTTP is rejected before any connection.
- Private, loopback, link-local (
169.254.0.0/16), and cloud-metadata (169.254.169.254) destinations are blocked at the DNS level (web). CLI/GitHub Action allow private networks for local testing. - DNS pinning closes the TOCTOU gap. The resolved IP is pinned at connection time.
- Redirects are re-validated at each hop. HTTPS applies across all redirects.
- Remote response bodies are never read or included in findings. Only structured response headers (e.g.
WWW-Authenticate error=,Retry-After) are inspected, never body content. - The web checker accepts no credentials. The CLI and GitHub Action send credentials only to the configured MCP endpoint — never to MCP Release infrastructure.
- TLS verification is enforced (
rejectUnauthorized: true). - Error messages are redacted. Token patterns and embedded URL credentials are stripped before being returned.
- Sensitive headers (
Authorization,x-api-key,cookie, etc.) are dropped on cross-origin redirects. ${VAR_NAME}env var values resolved from config headers are never logged or printed.
Human-readable summary suitable for pull request descriptions and release notes. Generated by packages/reporter.
After a check completes, the results area displays overall status, findings grouped by severity, discovered tools, and export buttons (Copy JSON, Download JSON, Download Markdown).
| Package / App | Purpose |
|---|---|
packages/core |
Validation engine: SSRF guard, DNS pinning, transport adapter, MCP validator, report model |
packages/cli |
Command-line interface |
packages/reporter |
JSON, Markdown, and terminal report formatters |
packages/github-action |
GitHub Action (action.yml) wrapping the core validator |
apps/web |
Next.js 15 web interface (https://mcprelease.dev) |
apps/public-mcp-fixture |
Public MCP fixture server (https://mcp-release-fixture.vercel.app/mcp) |
fixtures/servers |
Localhost fixture MCP servers used in tests |
Two independent Vercel projects:
| Project | Root directory | URL |
|---|---|---|
mcp-release |
apps/web |
https://mcprelease.dev |
mcp-release-fixture |
apps/public-mcp-fixture |
https://mcp-release-fixture.vercel.app |
apps/web (mcprelease.dev) deploys automatically on every push to main. apps/public-mcp-fixture has automatic deployments disabled and is deployed manually.
apps/web depends on packages/core and packages/reporter. Its build command builds those packages first:
pnpm --filter @mcp-release/core build && pnpm --filter @mcp-release/reporter build && pnpm --filter @mcp-release/web build
apps/public-mcp-fixture has no sibling package dependencies and builds with next build directly.
Requirements: Node.js ≥ 22.13.0, pnpm ≥ 10.28.0
pnpm install # install all workspace dependencies
pnpm typecheck # TypeScript type check (all packages + apps)
pnpm lint # ESLint across the workspace
pnpm build # build all packages and apps
pnpm test # run all tests (500+ automated tests)pnpm --filter @mcp-release/web dev
# → http://localhost:3000The development server shows fixture buttons (PASS / WARNING / FAIL) that load sample reports without making network requests. These buttons are removed in production builds.
apps/web/src/
├── app/
│ ├── layout.tsx HTML shell, metadata, OG tags
│ ├── page.tsx Landing page
│ ├── docs/
│ │ └── page.tsx Documentation page
│ └── api/check/
│ ├── handler.ts Testable request handler
│ └── route.ts Next.js route entry point
├── components/
│ ├── Header.tsx / .module.css
│ ├── Footer.tsx / .module.css
│ ├── CheckClient.tsx / .module.css Client component (form, state)
│ └── Results.tsx / .module.css Report display
└── lib/
├── constants.ts SITE_URL, GITHUB_URL, DEMO_ENDPOINT, timeout bounds
├── rate-limit.ts In-memory sliding-window rate limiter
└── concurrency.ts In-memory concurrency guard
POST /api/check accepts application/json:
{ "endpoint": "https://example.com/mcp", "timeoutMs": 10000 }endpoint: required, HTTPS only, no embedded credentialstimeoutMs: optional, 1000-30000 ms (default 10000)- Unexpected fields are rejected with
400 UNEXPECTED_FIELD
Returns 200 { "report": CheckReport } or a JSON error body with error and message.
- Rate limit: 10 requests per IP per minute (sliding window)
- Concurrency: max 5 simultaneous outbound checks
Both controls are per-process. See Known Limitations below.
- Public HTTPS endpoints only. HTTP and private network endpoints are rejected.
- No credential input. Authenticated checks are not performed. Servers requiring authorization return
AUTH_REQUIRED(WARNING). - Tools are not invoked. Runtime correctness of tool responses is not validated.
- A PASS is not a security guarantee. It reflects the checks MCP Release ran. Runtime behavior may differ in other environments.
- In-memory rate limiting. Per-process only, not shared across horizontally-scaled instances.
- Reports are not stored server-side. Export before closing the tab.
- x-forwarded-for is used for rate limiting. Accurate behind a trusted proxy, not verified otherwise.
pnpm install
pnpm typecheck && pnpm lint && pnpm testAll PRs run the full suite via .github/workflows/ci.yml. The workflow uses SHA-pinned actions and pnpm 10.28.0.
Core validation behavior (transport, SSRF, DNS pinning, error classification, report format) is covered by tests in packages/core/tests/. Web UI behavior is covered by tests in apps/web/tests/.