# Feranor Resilience MCP

An MCP server that runs a resilience check on any public domain and returns a scored report: TLS certificate, security headers, DNS redundancy, and response time, with every individual finding it was able to measure.

Built for agents that check domains repeatedly and unattended, not for a single manual look. For one manual check, use the free web version at [feranor.com/check](https://www.feranor.com/check).

---

## At a glance

| | |
|---|---|
| **Endpoint** | `https://www.feranor.com/mcp` |
| **Transport** | Streamable HTTP |
| **Price** | Free triage, $0.01 per full report, from $0.005 per domain in batch |
| **Payment** | x402, USDC on Base (`eip155:8453`) |
| **Signup** | None. No account, no API key, no dashboard. |
| **Free** | `initialize`, `tools/list`, `domain_check_score_v1`, and `domain_check_score_batch_v1` |

You do not create anything to use this server. You point a client at the URL, and the first paid call returns a payment challenge your x402-capable client settles automatically. If your client cannot pay, everything except the paid tools still works.

---

## Try it without paying

`tools/list` is free and needs no client setup. This is the fastest way to confirm the server is reachable and see the current catalogue:

```bash
curl -sS https://www.feranor.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Each tool carries its price in two places: in plain text at the end of its `description`, and machine-readable under `_meta["x-x402"]`. You can therefore budget a task before committing to any call.

---

## Add it to your client

### Claude Code

```bash
claude mcp add --transport http feranor https://www.feranor.com/mcp
```

Scoped to the current project by default. For every project:

```bash
claude mcp add --transport http --scope user feranor https://www.feranor.com/mcp
```

Confirm with `claude mcp list`, then `/mcp` inside a session.

### Cursor

`~/.cursor/mcp.json` for all projects, or `.cursor/mcp.json` in a repository:

```json
{
  "mcpServers": {
    "feranor": {
      "url": "https://www.feranor.com/mcp"
    }
  }
}
```

### VS Code

`.vscode/mcp.json` in the workspace root. Note the root key is `servers`, not `mcpServers`, and `type` must be present:

```json
{
  "servers": {
    "feranor": {
      "type": "http",
      "url": "https://www.feranor.com/mcp"
    }
  }
}
```

MCP tools are only available in Copilot Chat agent mode.

### Shared with your team

`.mcp.json` at the repository root, committed to git:

```json
{
  "mcpServers": {
    "feranor": {
      "type": "http",
      "url": "https://www.feranor.com/mcp"
    }
  }
}
```

### Any other client

Point it at `https://www.feranor.com/mcp` over streamable HTTP. There is no authentication step, no OAuth flow, and no token to provision.

---

## The tools

### `text_scan_v1` — free

Scans a text string for invisible Unicode characters, homoglyphs, and normalization issues. Returns a risk score, a list of flagged characters, and detected homoglyphs. Never modifies the input text.

```json
{ "text": "Hello​World" }
```

Detects zero-width characters, BOM, variation selectors, tag characters, bidi controls, and Cyrillic/Greek homoglyphs. Also checks whether the text is in NFC normalization form. The intended path: run `text_scan_v1` first to see what is wrong, then call `text_purge_v1` to fix it.

### `text_purge_v1` — $0.01 per call

Removes invisible Unicode characters and replaces homoglyphs in a text string, then normalizes the result. Returns the cleaned text plus a before-and-after risk comparison.

```json
{ "text": "Hello​World" }
```

Strips zero-width characters, variation selectors, tag characters, and bidi controls. Replaces Cyrillic and Greek homoglyphs with their Latin equivalent. Normalizes the output to NFC.

### `domain_check_score_v1` — free

Overall score, per-category scores, and how many findings each category raised.

Use it to triage: rank a list of domains, confirm one is healthy, or decide whether the full report is worth pulling. It tells you which categories are weak and how many issues they hold, but not what the issues are.

**Input**

```json
{ "domain": "example.com" }
```

**Output**

```json
{
  "domain": "example.com",
  "score": 52,
  "measuredCategories": 5,
  "evaluatedAt": "2026-08-01T00:00:00Z",
  "detailsAvailableVia": "domain_check_v1",
  "categories": [
    { "name": "Transport",        "score": 40,  "findingCount": 2 },
    { "name": "Performance",      "score": 67,  "findingCount": 1 },
    { "name": "Security headers", "score": 0,   "findingCount": 4 },
    { "name": "Infrastructure",   "score": 100, "findingCount": 0 },
    { "name": "Hygiene",          "score": 100, "findingCount": 0 }
  ]
}
```

### `domain_check_score_batch_v1` — free

The same triage scores for up to 100 domains in a single call. One score summary per distinct domain: overall score, per-category scores, and how many findings each category raised. Never the findings themselves.

**Input**

```json
{ "domains": ["example.com", "example.org", "www.example.net"] }
```

Between 1 and 100 entries. Duplicates are removed. A domain that cannot be reached is returned with its own error rather than failing the batch.

**Output**

```json
{
  "requested": 3,
  "evaluated": 2,
  "succeeded": 2,
  "evaluatedAt": "2026-08-10T09:14:22Z",
  "results": [
    {
      "domain": "example.com",
      "summary": {
        "domain": "example.com",
        "score": 52,
        "measuredCategories": 5,
        "evaluatedAt": "2026-08-10T09:14:22Z",
        "detailsAvailableVia": "domain_check_batch_v1",
        "categories": [
          { "name": "Transport",        "score": 40,  "findingCount": 2 },
          { "name": "Performance",      "score": 67,  "findingCount": 1 },
          { "name": "Security headers", "score": 0,   "findingCount": 4 },
          { "name": "Infrastructure",   "score": 100, "findingCount": 0 },
          { "name": "Hygiene",          "score": 100, "findingCount": 0 }
        ]
      }
    },
    {
      "domain": "example.org",
      "summary": {
        "domain": "example.org",
        "score": 91,
        "measuredCategories": 5,
        "evaluatedAt": "2026-08-10T09:14:22Z",
        "detailsAvailableVia": "domain_check_batch_v1",
        "categories": [
          { "name": "Transport",        "score": 100, "findingCount": 0 },
          { "name": "Performance",      "score": 100, "findingCount": 0 },
          { "name": "Security headers", "score": 70,  "findingCount": 1 },
          { "name": "Infrastructure",   "score": 100, "findingCount": 0 },
          { "name": "Hygiene",          "score": 80,  "findingCount": 1 }
        ]
      }
    },
    {
      "domain": "unreachable.invalid",
      "error": "Host could not be resolved."
    }
  ]
}
```

The intended path: rank a portfolio here for free, then pull `domain_check_batch_v1` on the domains that need attention.

### `domain_check_v1` — $0.01 per call

The full report for one domain: the same score plus every individual finding, grouped by category, with a prioritised remediation order.

**Input**

```json
{ "domain": "example.com" }
```

A bare domain. Scheme and path are ignored if present.

**Output**

```json
{
  "domain": "example.com",
  "requestedDomain": "example.com",
  "score": 52,
  "evaluatedAt": "2026-08-01T00:00:00Z",
  "checks": [
    {
      "id": "tls.certificate",
      "category": "Transport",
      "label": "TLS Certificate",
      "weight": 12,
      "earned": 12,
      "status": "Ok",
      "message": "Certificate valid for another 84 days, protocol negotiated Tls13."
    },
    {
      "id": "headers.hsts",
      "category": "Transport",
      "label": "HSTS",
      "weight": 10,
      "earned": 0,
      "status": "Critical",
      "message": "No Strict-Transport-Security: the initial visit remains interceptable."
    },
    {
      "id": "perf.response-time",
      "category": "Performance",
      "label": "Response time",
      "weight": 10,
      "earned": 10,
      "status": "Ok",
      "message": "Root reached in 159 ms."
    },
    {
      "id": "headers.nosniff",
      "category": "Security headers",
      "label": "X-Content-Type-Options",
      "weight": 5,
      "earned": 0,
      "status": "Warning",
      "message": "Missing X-Content-Type-Options."
    },
    {
      "id": "infra.cdn",
      "category": "Infrastructure",
      "label": "Front-end CDN",
      "weight": 6,
      "earned": 6,
      "status": "Ok",
      "message": "CDN detected (hint: cf-ray)."
    },
    {
      "id": "hygiene.directory-listing",
      "category": "Hygiene",
      "label": "Directory listing",
      "weight": 5,
      "earned": 0,
      "status": "Unknown",
      "message": "No folder derivable from the root: not evaluated."
    }
  ]
}
```

### `domain_check_batch_v1` — from $0.005 per domain

The same full report for up to 100 domains in a single call, with **one payment for the whole batch** instead of one per domain. Both cheaper per domain and much faster than calling `domain_check_v1` in a loop.

**Volume tiers**

| Domains | Price per domain | Example total |
|---|---|---|
| 1 | $0.010 | $0.010 |
| 2 to 9 | $0.008 | 5 domains: $0.040 |
| 10 to 49 | $0.007 | 10 domains: $0.070 |
| 50 to 99 | $0.006 | 50 domains: $0.300 |
| 100 | $0.005 | 100 domains: $0.500 |

The unit price is the lowest tier the quantity qualifies for. Duplicates are removed before billing. For a single domain, `domain_check_v1` costs the same and is simpler.

**Input**

```json
{ "domains": ["example.com", "example.org", "www.example.net"] }
```

Between 1 and 100 entries. A request above 100 is rejected rather than truncated, so you never receive a partial report without knowing it.

**Output**

One entry per distinct domain. **A domain that cannot be reached is returned with its own error rather than failing the batch, and it is still billed**, because the crawl was attempted. This is deliberate and differs from the single-domain tool, where a failed check is not charged.

```json
{
  "requested": 3,
  "evaluated": 3,
  "succeeded": 2,
  "evaluatedAt": "2026-08-01T00:00:00Z",
  "results": [
    {
      "domain": "example.com",
      "report": {
        "domain": "example.com",
        "score": 52,
        "checks": [
          {
            "id": "tls.certificate",
            "category": "Transport",
            "label": "TLS Certificate",
            "weight": 12,
            "earned": 12,
            "status": "Ok",
            "message": "Certificate valid for another 84 days, protocol negotiated Tls13."
          }
        ]
      }
    },
    {
      "domain": "example.org",
      "report": {
        "domain": "example.org",
        "score": 91,
        "checks": []
      }
    },
    {
      "domain": "unreachable.invalid",
      "error": "Host could not be resolved."
    }
  ]
}
```

### How the score works

The score is measured only on what could actually be read from outside. A category that could not be observed is excluded from the denominator rather than counted as a failure, so a domain behind a WAF that blocks the probe gets a partial score over fewer categories, not a bad score. Compare two domains only when the number of measured categories matches.

Everything is read from the public internet. Nothing is authenticated, no ports are scanned, and no credentials are needed. The crawler identifies itself as FeranorBot and honours robots.txt. See [the FeranorBot page](https://www.feranor.com/bot) to identify or block it.

---

## Paying

The server uses [x402](https://github.com/coinbase/x402): payment happens in the HTTP exchange itself, with no account in between.

The sequence, which an x402-capable client performs on its own:

1. Your client calls the tool with no payment attached.
2. The server answers `402` with the exact amount, asset, network, and recipient.
   For a batch, the amount reflects the number of distinct domains and the tier applied.
3. Your client signs a USDC transfer authorization and repeats the call.
4. The server verifies, runs the check, settles, and returns the report.

**Header conventions.** Both are accepted on the way in, `PAYMENT-SIGNATURE` and `X-PAYMENT`, and both are emitted on the way out. If your x402 client library predates the v2 transport naming, it will still work.

**Receipt.** The settlement receipt comes back two ways: base64 in the `PAYMENT-RESPONSE` header for x402 clients, and in decoded short form under the result's `_meta["x-x402-receipt"]` for MCP clients that never see response headers. It carries the transaction hash, so you can reconcile spend on-chain.

**What you are charged for.** Only successful checks, with one deliberate exception: in a batch, a domain that could not be reached is still billed, because the crawl was attempted. If the tool itself fails, or if settlement does not complete, nothing is charged and no report is returned. Retrying a call with the same payment returns the report you already paid for rather than charging twice.

**Batch amounts are recomputed identically.** The challenge and the paid retry carry the same arguments, so the total is the same. If you clean your list of duplicates between the two, the amount still matches, because duplicates were never billed.

---

## Errors

The status code tells you whose problem it is.

| Status | Meaning | What to do |
|---|---|---|
| `402` | Payment missing, malformed, or not matching the offer | Read the `error` field. It names the exact mismatch. |
| `405` | Method not allowed, use POST only | Nothing was charged. Fix and retry shortly. |
| `406` | Not acceptable, body may be unreadable | Nothing was charged. Fix and retry shortly. |
| `429` | Rate limit | Back off for the delay in `Retry-After`. |
| `502` | Our payment infrastructure is unavailable | Nothing was charged. Retry shortly. |

A request that is **invalid** rather than unpaid, such as an empty list or more than 100 domains, comes back as a tool error, not a `402`. Paying would not fix it.

Common causes of a repeated `402`:

- **Wrong network.** The offer is Base mainnet, `eip155:8453`. A payload signed for another chain, including Base Sepolia `eip155:84532`, is rejected.
- **Amount below the offer.** A signature over a smaller amount is a valid signature over the wrong amount. The `error` field states the expected atomic value.
- **Wrong asset or recipient.** Both are published in the challenge and in the server card.
- **Authorization window expired.** Sign a fresh one.

If you loop on `402` without a useful `error`, that is a bug on our side. Tell us at [contact@feranor.com](mailto:contact@feranor.com).

---

## Discovery

For registries, crawlers, and agents that resolve servers from a domain:

- Server card: `https://www.feranor.com/.well-known/mcp.json`
- Enumeration: `https://www.feranor.com/.well-known/mcp`
- Site catalogue: `https://www.feranor.com/llms.txt`
- MCP Registry: `com.feranor/resilience`

---

## Contact

Questions, bug reports, or a check you wish existed: [contact@feranor.com](mailto:contact@feranor.com).

Feranor is a distributed systems architecture and governance practice based in Lyon, France. [About the practice](https://www.feranor.com/).