Skip to main content
Glama
devops-gm88

MCP Connector Lint

by devops-gm88
README.md
# MCP Connector Lint

A remote [Model Context Protocol](https://modelcontextprotocol.io) server with three read-only tools that check an MCP connector against the requirements of the Anthropic Claude Connectors Directory, before it is submitted.

![test](https://github.com/devops-gm88/mcp-connector-lint/actions/workflows/test.yml/badge.svg)

Live endpoint: **https://mcp-lint.superintelligencesystems.ca/mcp** (Streamable HTTP, no authentication)

- **No account, no OAuth, no storage.** Every call is a JSON argument in, a JSON report out.
- **Deterministic.** 58 rules, sorted and de-duplicated findings, byte-identical output for identical input.
- **First-party.** The tools and the REST API are two surfaces over the same rule engine in this repository; no third-party service is called.
- **Static by design.** The checker never connects to the server being reviewed, never calls its tools and never fetches the docs, privacy or icon URLs. It checks text and structure, and says so.

## Tools

| Tool | Title | Annotations | What it returns |
| --- | --- | --- | --- |
| `validate_manifest` | Validate MCP connector manifest | `readOnlyHint: true`, `destructiveHint: false` | A report on the connector manifest: remote HTTPS transport, accepted auth mode, declared first-party service domain, and every tool's annotations. |
| `check_tool_annotations` | Check MCP tool annotations | `readOnlyHint: true`, `destructiveHint: false` | A report on a `tools/list` array: titles, hints, name length and format, unique names, object input schemas, description hygiene. |
| `lint_listing_text` | Lint connector listing text | `readOnlyHint: true`, `destructiveHint: false` | A report on a listing draft: the published 100/200/2,000-character limits, 1-5 categories, HTTPS docs/privacy/icon URLs, support contact, reviewer note, copy rules. |

All three return the same envelope:

```json
{
  "ok": false,
  "verdict": "fail",
  "ruleset": "2026-11-03.1",
  "summary": { "errors": 1, "warnings": 2, "infos": 0 },
  "findings": [
    {
      "rule": "MNF-008",
      "severity": "error",
      "path": "manifest.url",
      "message": "host \"localhost\" is a loopback or private address",
      "fix": "Publish the server on a public HTTPS hostname."
    }
  ],
  "guidance": "Fix the 1 blocking error before submitting. …"
}
```

`ok` is false only when there is a blocking **error**. Warnings are advisory. A validation failure is a **result, not an error**: the call succeeds and returns the report. The only protocol errors are malformed calls (for example a manifest that is not an object), and their message names the argument.

## Quick start

```sh
npm install
npm test          # 81 deterministic tests, no network
npm run typecheck
npm run dev       # wrangler dev on http://localhost:8787
```

Exercise it locally:

```sh
curl -s http://localhost:8787/api/v1/validate-manifest \
  -H 'content-type: application/json' \
  -d '{"manifest":{"name":"Example","url":"https://api.example.com/mcp","transport":"streamable-http","authentication":"none","service_domain":"example.com","tools":[{"name":"get_weather","title":"Get weather","description":"Returns the current weather for one city.","readOnlyHint":true,"inputSchema":{"type":"object","properties":{}}}]}}'
```

Run the live smoke test (transport, all three tools, one successful and one failing call each):

```sh
node scripts/smoke.mjs https://mcp-lint.superintelligencesystems.ca
```

## Connect it

Any Streamable HTTP MCP client can use the endpoint. In Claude, add a **custom connector** with URL `https://mcp-lint.superintelligencesystems.ca/mcp` and no authentication; three tools appear.

The MCP endpoint is stateless: each POST is a complete JSON-RPC exchange and there is no session to expire. JSON responses are enabled (`application/json`), GET streaming is not supported (405 with the reason).

## REST API

The MCP tools are thin adapters over the same engine; the REST API is the second surface, useful for CI:

| Method | Path | Body |
| --- | --- | --- |
| POST | `/api/v1/validate-manifest` | `{"manifest": {...}, "expect": "read-only"}` |
| POST | `/api/v1/check-tool-annotations` | `{"tools": [...], "expect": "read-only"}` |
| POST | `/api/v1/lint-listing-text` | `{"listing": {...}}` |
| GET | `/health` | service identity, ruleset, tool list, limits |
| GET | `/rules` | the machine-readable rule catalogue |
| GET | `/docs` | full documentation and rule rationale |
| GET | `/privacy` | privacy statement |

A REST failure report is still HTTP 200, because the check succeeded and the answer is "this fails". Malformed requests are 400 with `{"error":{"code":"…","message":"…"}}`.

## Rule catalogue

58 rules across the three checks, each with a stable id, a severity and a rationale tied to a published requirement. The full table with rationale is at `/docs` and the machine-readable copy at `/rules`; `src/lib/catalog.ts` is the source of truth and a test fails if any tool emits an id that is not documented there.

- `MNF-001` … `MNF-014` — manifest: transport, HTTPS, loopback hosts, auth mode, first-party domain, tool presence, embedded listing.
- `ANN-001` … `ANN-018` — annotations: title, hints, names, uniqueness, schemas, description hygiene, read/write contradictions.
- `LST-001` … `LST-026` — listing text: limits, categories, URLs, support contact, reviewer note, superlatives, contact details, instructions.

Two rules worth calling out because they catch the mistakes that most often bounce a submission:

- **`MNF-009`** — a static bearer token blocks listing. Directory auth is OAuth 2.0 for account-scoped tools, or none for public data.
- **`ANN-009`** — every tool must set `readOnlyHint` or `destructiveHint`; the portal flags tools that set neither.

## Architecture

```
src/
  index.ts          Worker routes: /mcp, /api/v1/*, /health, /rules, /docs, /privacy, /icon.svg
  mcp.ts            three MCP tools (registerTool with title + annotations)
  api.ts            REST handlers over the same engine
  lib/              report envelope, rule catalogue, URL/pattern helpers
  rules/            manifest.ts, annotations.ts, listing.ts — pure functions
  pages/            server-rendered HTML (no client JavaScript, no external assets)
test/               81 tests: every rule, report determinism, REST, MCP transport and tools
scripts/            smoke.mjs (live endpoint), make-icon.mjs (PNG asset, no dependencies)
```

The rule engines are pure functions over untrusted JSON. They are the only place findings are produced, which is why the MCP surface, the REST API and the tests cannot disagree. Findings are sorted by path then rule id and de-duplicated, so determinism is a property of the code, not of the test harness.

The Worker uses the official `@modelcontextprotocol/sdk` web-standard Streamable HTTP transport in stateless mode. No Durable Objects, no KV, no D1, no secrets.

## Limits

- Request body: 256 KB (413 above that).
- Tools per call: 200 (rejected at the schema).
- No rate-limit state is stored; Cloudflare's edge protects the Worker.

## Deploy

The live service is a Cloudflare Worker:

```sh
export CLOUDFLARE_API_TOKEN=...   # Workers Scripts: Edit for the account
export CLOUDFLARE_ACCOUNT_ID=...
npx wrangler deploy
```

`wrangler.jsonc` binds the custom domain `mcp-lint.superintelligencesystems.ca`. Change the `routes` entry to deploy under a different hostname.

## Privacy

No accounts, cookies, analytics or stored payloads. The JSON you send is parsed in memory, checked and discarded with the response; the application does not log request bodies or call third-party APIs. Hosting metadata (IP, URL, status) is processed by Cloudflare as the hosting provider. Full statement at `/privacy`.

## License

MIT. Copyright 2026 Moid Chowdhury (Super Intelligence Systems).