# MCP Registry Validator — Check Endpoints Actually Resolve (`quaternary_nepotist_afc/mcp-uptime`) Actor

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.

- **URL**: https://apify.com/quaternary\_nepotist\_afc/mcp-uptime.md
- **Developed by:** [Parker Levin](https://apify.com/quaternary_nepotist_afc) (community)
- **Categories:** Agents, Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 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.

```jsonc
// 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, auth
src/worker/   Cloudflare Worker — HTTP router, MCP server, D1 adapter
src/actor/    Apify Actor entry point
tests/        node:test, no test framework to install
.actor/       actor.json, input schema, Dockerfile, PPE event reference
schema.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

```bash
npm install
npm test          # 37 tests, no network, no framework
npm run typecheck
npm 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](DEPLOY.md). Accounts and go-live: see
[LAUNCH-README.md](LAUNCH-README.md).

# Actor input Schema

## `endpoints` (type: `array`):

Public http(s) URLs of the MCP endpoints to check. Max 100 per run. Private, loopback and link-local targets are rejected.

## `includeTools` (type: `boolean`):

Return every tool descriptor, not just the count and the schema hash.

## `timeoutMs` (type: `integer`):

How long to wait for each endpoint before recording it as down. 1000-30000.

## Actor input object example

```json
{
  "endpoints": [
    "https://mcp.example.com/mcp"
  ],
  "includeTools": false,
  "timeoutMs": 10000
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "endpoints": [
        "https://mcp.example.com/mcp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("quaternary_nepotist_afc/mcp-uptime").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "endpoints": ["https://mcp.example.com/mcp"] }

# Run the Actor and wait for it to finish
run = client.actor("quaternary_nepotist_afc/mcp-uptime").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "endpoints": [
    "https://mcp.example.com/mcp"
  ]
}' |
apify call quaternary_nepotist_afc/mcp-uptime --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=quaternary_nepotist_afc/mcp-uptime",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/3891rIIrVPcxs7kkF/builds/e9aLGEP9I9b2PF4qp/openapi.json
