MCP Registry Validator — Check Endpoints Actually Resolve
Pricing
Pay per usage
MCP Registry Validator — Check Endpoints Actually Resolve
Validate that MCP endpoints actually answer before you depend on them. Checks liveness, lists the tools a server really exposes right now, and detects schema drift since the last run. Free, no auth.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Parker Levin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 hours ago
Last modified
Categories
Share
mcp-uptime
Is that MCP endpoint alive, what does it actually expose right now, and has its tool schema changed since you last looked?
52% of remote MCP endpoints are dead. Schema drift is worse than downtime, because
downtime is loud and a quietly-changed inputSchema fails mid-agent-run and looks
like a model error. This service answers four questions about any public MCP
endpoint: liveness, tool inventory, schema drift, and 30-day uptime.
It ships twice from one codebase:
| Surface | What it is | Why |
|---|---|---|
| Cloudflare Worker | An MCP server (/mcp) plus a plain HTTP API (/v1/*) | Reach — agents discover it through the MCP registry |
| Apify Actor | Same check logic, pay-per-event | Metering + payout — Apify counts runs and users natively |
Free tier: 20 checks/day, on-demand. Paid tier ($9/mo, not wired up yet): scheduled monitoring, drift history, webhook alerts.
MCP tools
| Tool | Probes? | Returns |
|---|---|---|
check_mcp_endpoint | yes | status, transport, latency, tool count (or the full list), schema hash, drift |
check_schema_drift | yes | tools added / removed / modified since the last snapshot |
get_uptime | no | uptime % and incident windows over the last N days (max 30) |
get_known_tools | no | last observed tool list + when the schema last changed |
Point any MCP client at POST https://<your-worker>.workers.dev/mcp. Streamable
HTTP; no auth needed for the free tier.
// tools/call → check_mcp_endpoint{"endpoint": "https://mcp.example.com/mcp","status": "up", // up | degraded | down"transport": "streamable-http","latencyMs": 214,"protocolVersion": "2025-06-18","server": { "name": "example-mcp", "version": "1.4.0" },"toolCount": 12,"toolsHash": "9f2c…","drift": { "changed": true, "added": [], "removed": ["search_v1"],"modified": [{ "name": "search", "fields": ["inputSchema"] }] },"firstSeen": false,"error": null}
degraded is its own status on purpose: the server answered initialize but its
tools/list failed. Collapsing that into down would hide exactly the failure
this product exists to surface.
HTTP API
| Method | Path | Auth | Notes |
|---|---|---|---|
POST | /v1/check | none | { "url": "...", "include_tools": false } |
GET | /v1/status?url=…&days=30 | none | stored history, no probe |
GET/POST | /v1/monitors | key | list / create (scheduled checks + webhook) |
GET/DELETE | /v1/monitors/:id | key | own monitors only |
POST | /mcp | none | MCP Streamable HTTP |
GET | /health | none | liveness |
GET | /admin/stats | operator secret | probe call counts; 404s without the secret |
Security model
The whole job of this service is to fetch a URL a stranger hands it, so the
guard rails are the product, not decoration. Implemented against the
access-control matrix in ../PROBE-SPEC.md §SECURITY:
- SSRF guard (
src/core/ssrf.ts) — http/https only; loopback, RFC1918, CGNAT, link-local (incl.169.254.169.254), NAT64, multicast, reserved, and IPv4-mapped IPv6 all rejected; internal TLDs and bare hostnames rejected; embedded credentials rejected; internal service ports blocked. Re-applied to every redirect hop — redirects are followed manually because a public host is allowed to bounce you at metadata. - Rate limit before auth — a coarse per-IP limiter runs before any key lookup, so an unauthenticated flood never reaches the tenants table. Plan quotas apply afterwards, on the resolved principal.
- Fail closed — if the counter store errors, requests are denied (503), not admitted.
- No cross-tenant reads — monitor queries are tenant-scoped in SQL. Another tenant's monitor returns 404, not 403, so ids are not an existence oracle.
- No secrets or caller data in logs — callers are stored as SHA-256 hashes, never raw IPs; API keys are stored only as hashes; error bodies are fixed codes, never remote content or internals; URL query strings (which routinely carry tokens) are dropped before an endpoint is persisted.
- Untrusted remote content — tool names and descriptions come from the endpoint being probed and are handed to agents, so they are count-capped (200) and length-capped before they reach storage.
- Dependencies — exact-pinned, with a lockfile. The Worker has zero runtime dependencies: the MCP server is hand-rolled JSON-RPC.
Verified by tests/ssrf.test.ts, tests/ratelimit.test.ts, and
tests/tenant.test.ts — the three tests the spec names.
Layout
src/core/ runtime-agnostic: SSRF guard, prober, drift, uptime, quotas, authsrc/worker/ Cloudflare Worker — HTTP router, MCP server, D1 adaptersrc/actor/ Apify Actor entry pointtests/ node:test, no test framework to install.actor/ actor.json, input schema, Dockerfile, PPE event referenceschema.sql D1 schema
src/core imports nothing but the Web platform (fetch, URL, crypto.subtle),
which is why both wrappers run identical logic and the tests exercise the real
thing rather than a stand-in.
Development
npm installnpm test # 37 tests, no network, no frameworknpm run typechecknpm run dev # wrangler dev on http://localhost:8787
Tests run the TypeScript sources directly under Node's strip-only mode (Node 22+), so there is no build step and no test runner to install. That constrains the source: no enums, no namespaces, no constructor parameter properties.
Deployment: see DEPLOY.md. Accounts and go-live: see LAUNCH-README.md.