isagentready-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bart Waardenburg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # isagentready-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/isagentready-mcp.svg)](https://www.npmjs.com/package/isagentready-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org/)
6
+ [![CI](https://github.com/bartwaardenburg/isagentready-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/bartwaardenburg/isagentready-mcp/actions/workflows/ci.yml)
7
+ [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/BartWaardenburg/dc467336d6825cc861b82bee7a38397c/raw/isagentready-mcp-coverage.json)](https://bartwaardenburg.github.io/isagentready-mcp/)
8
+ [![MCP](https://img.shields.io/badge/MCP-compatible-purple.svg)](https://modelcontextprotocol.io)
9
+
10
+ A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for the [IsAgentReady](https://isagentready.com) API. Scan any website for AI agent readiness and get detailed reports with scores, letter grades, and actionable recommendations across 5 categories: Discovery, Structured Data, Semantics, Agent Protocols, and Security.
11
+
12
+ ## Features
13
+
14
+ - **3 MCP tools** for scanning, retrieving results, and browsing rankings
15
+ - **No API key required** — public API, zero configuration needed
16
+ - **Built-in caching** with configurable TTL and automatic invalidation on scans
17
+ - **Retry logic** with exponential backoff and `Retry-After` header support
18
+ - **Structured content** returned alongside human-readable text
19
+ - **Toolset filtering** to expose only the tool categories you need
20
+ - **Actionable error messages** with context-aware recovery suggestions
21
+ - **Docker support** for containerized deployment
22
+ - **97 unit tests** across 7 test files
23
+
24
+ ## Supported Clients
25
+
26
+ This MCP server works with any client that supports the Model Context Protocol, including:
27
+
28
+ | Client | Easiest install |
29
+ |---|---|
30
+ | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | One-liner: `claude mcp add` |
31
+ | [Codex CLI](https://github.com/openai/codex) (OpenAI) | One-liner: `codex mcp add` |
32
+ | [Gemini CLI](https://github.com/google-gemini/gemini-cli) (Google) | One-liner: `gemini mcp add` |
33
+ | [VS Code](https://code.visualstudio.com/) (Copilot) | Command Palette: `MCP: Add Server` |
34
+ | [Claude Desktop](https://claude.ai/download) | JSON config file |
35
+ | [Cursor](https://cursor.com) | JSON config file |
36
+ | [Windsurf](https://codeium.com/windsurf) | JSON config file |
37
+ | [Cline](https://github.com/cline/cline) | UI settings |
38
+ | [Zed](https://zed.dev) | JSON settings file |
39
+
40
+ ## Installation
41
+
42
+ ### Claude Code
43
+
44
+ ```bash
45
+ claude mcp add isagentready-mcp -- npx -y isagentready-mcp
46
+ ```
47
+
48
+ ### Codex CLI (OpenAI)
49
+
50
+ ```bash
51
+ codex mcp add isagentready-mcp -- npx -y isagentready-mcp
52
+ ```
53
+
54
+ ### Gemini CLI (Google)
55
+
56
+ ```bash
57
+ gemini mcp add isagentready-mcp -- npx -y isagentready-mcp
58
+ ```
59
+
60
+ ### VS Code (Copilot)
61
+
62
+ Open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) > `MCP: Add Server` > select **Command (stdio)**.
63
+
64
+ Or add to `.vscode/mcp.json` in your project directory:
65
+
66
+ ```json
67
+ {
68
+ "servers": {
69
+ "isagentready-mcp": {
70
+ "type": "stdio",
71
+ "command": "npx",
72
+ "args": ["-y", "isagentready-mcp"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ### Claude Desktop / Cursor / Windsurf / Cline
79
+
80
+ These clients share the same JSON format. Add the config below to the appropriate file:
81
+
82
+ | Client | Config file |
83
+ |---|---|
84
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
85
+ | Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
86
+ | Cursor (project) | `.cursor/mcp.json` |
87
+ | Cursor (global) | `~/.cursor/mcp.json` |
88
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
89
+ | Cline | Settings > MCP Servers > Edit |
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "isagentready-mcp": {
95
+ "command": "npx",
96
+ "args": ["-y", "isagentready-mcp"]
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Zed
103
+
104
+ Add to your Zed settings (`~/.zed/settings.json` on macOS, `~/.config/zed/settings.json` on Linux):
105
+
106
+ ```json
107
+ {
108
+ "context_servers": {
109
+ "isagentready-mcp": {
110
+ "command": "npx",
111
+ "args": ["-y", "isagentready-mcp"]
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Docker
118
+
119
+ ```bash
120
+ docker run -i --rm ghcr.io/bartwaardenburg/isagentready-mcp
121
+ ```
122
+
123
+ ### Codex CLI (TOML config alternative)
124
+
125
+ If you prefer editing `~/.codex/config.toml` directly:
126
+
127
+ ```toml
128
+ [mcp_servers.isagentready-mcp]
129
+ command = "npx"
130
+ args = ["-y", "isagentready-mcp"]
131
+ ```
132
+
133
+ ### Other MCP Clients
134
+
135
+ For any MCP-compatible client, use this server configuration:
136
+
137
+ - **Command:** `npx`
138
+ - **Args:** `["-y", "isagentready-mcp"]`
139
+ - **No environment variables required**
140
+
141
+ ## Configuration
142
+
143
+ All configuration is optional via environment variables:
144
+
145
+ | Variable | Description | Default |
146
+ |---|---|---|
147
+ | `ISAGENTREADY_CACHE_TTL` | Response cache lifetime in seconds. Set to `0` to disable caching. | `120` |
148
+ | `ISAGENTREADY_MAX_RETRIES` | Maximum retry attempts for rate-limited (429) requests with exponential backoff. | `3` |
149
+ | `ISAGENTREADY_BASE_URL` | API base URL. | `https://isagentready.com` |
150
+ | `ISAGENTREADY_TOOLSETS` | Comma-separated list of tool categories to enable (see [Toolset Filtering](#toolset-filtering)). | All toolsets |
151
+
152
+ ### Example with configuration
153
+
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "isagentready-mcp": {
158
+ "command": "npx",
159
+ "args": ["-y", "isagentready-mcp"],
160
+ "env": {
161
+ "ISAGENTREADY_CACHE_TTL": "300",
162
+ "ISAGENTREADY_MAX_RETRIES": "5"
163
+ }
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Available Tools
170
+
171
+ ### Scans
172
+
173
+ | Tool | Description |
174
+ |---|---|
175
+ | `scan_website` | Trigger a new scan of a website for AI agent readiness. Returns cached results if scanned within the last hour. |
176
+ | `get_scan_results` | Get the latest scan results for a domain with scores, grades, and recommendations. |
177
+
178
+ ### Rankings
179
+
180
+ | Tool | Description |
181
+ |---|---|
182
+ | `get_rankings` | Browse paginated AI readiness rankings with filtering by grade range, search, and sorting. |
183
+
184
+ ## Toolset Filtering
185
+
186
+ Reduce context window usage by enabling only the tool categories you need. Set the `ISAGENTREADY_TOOLSETS` environment variable to a comma-separated list:
187
+
188
+ ```bash
189
+ ISAGENTREADY_TOOLSETS=scans
190
+ ```
191
+
192
+ | Toolset | Tools included |
193
+ |---|---|
194
+ | `scans` | `scan_website`, `get_scan_results` |
195
+ | `rankings` | `get_rankings` |
196
+
197
+ When not set, all toolsets are enabled. Invalid names are ignored; if all names are invalid, all toolsets are enabled as a fallback.
198
+
199
+ ## Example Usage
200
+
201
+ Once connected, you can interact with the IsAgentReady API using natural language:
202
+
203
+ - "Scan example.com for AI agent readiness"
204
+ - "Check if the scan results for example.com are ready"
205
+ - "Show me the top AI-ready websites"
206
+ - "Search the rankings for sites in the high grade range"
207
+ - "How does my site compare to others?"
208
+
209
+ ### Scan + fix workflow
210
+
211
+ Combine with the [isagentready-skills](https://github.com/bartwaardenburg/isagentready-skills) agent skills for a full scan, fix, and re-verify workflow:
212
+
213
+ ```
214
+ > Scan example.com for AI agent readiness
215
+
216
+ Scan enqueued for example.com
217
+ Status: pending
218
+
219
+ > Check the results for example.com
220
+
221
+ Domain: example.com
222
+ Grade: B (72/100)
223
+ Status: completed
224
+
225
+ Discovery — 15/20 (75%, weight: 30%)
226
+ [PASS] 1.1 robots.txt (5/5)
227
+ [FAIL] 1.2 sitemap.xml (0/5)
228
+ Recommendation: Add a sitemap.xml file...
229
+
230
+ > Fix the failing checkpoints
231
+ ```
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ # Install dependencies
237
+ pnpm install
238
+
239
+ # Run in development mode
240
+ pnpm dev
241
+
242
+ # Build for production
243
+ pnpm build
244
+
245
+ # Run tests
246
+ pnpm test
247
+
248
+ # Type check
249
+ pnpm typecheck
250
+ ```
251
+
252
+ ### Project Structure
253
+
254
+ ```
255
+ src/
256
+ index.ts # Entry point (stdio transport)
257
+ server.ts # MCP server setup, toolset filtering
258
+ client.ts # IsAgentReady API HTTP client with caching and retry
259
+ cache.ts # TTL-based in-memory response cache
260
+ types.ts # TypeScript interfaces
261
+ format.ts # Output formatting helpers
262
+ tool-result.ts # Error formatting with recovery suggestions
263
+ update-checker.ts # NPM update notifications
264
+ tools/
265
+ scans.ts # scan_website, get_scan_results
266
+ rankings.ts # get_rankings
267
+ ```
268
+
269
+ ## Requirements
270
+
271
+ - Node.js >= 20
272
+
273
+ ## Related
274
+
275
+ - [isagentready-skills](https://github.com/bartwaardenburg/isagentready-skills) — Agent skills for fixing AI readiness issues identified by scans
276
+ - [IsAgentReady.com](https://isagentready.com) — The web scanner
277
+
278
+ ## License
279
+
280
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,10 @@
1
+ export declare class TtlCache {
2
+ private readonly store;
3
+ private readonly defaultTtlMs;
4
+ constructor(defaultTtlMs?: number);
5
+ get<T>(key: string): T | undefined;
6
+ set<T>(key: string, value: T, ttlMs?: number): void;
7
+ invalidate(pattern: string): void;
8
+ clear(): void;
9
+ get size(): number;
10
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,36 @@
1
+ export class TtlCache {
2
+ store = new Map();
3
+ defaultTtlMs;
4
+ constructor(defaultTtlMs = 120_000) {
5
+ this.defaultTtlMs = defaultTtlMs;
6
+ }
7
+ get(key) {
8
+ const entry = this.store.get(key);
9
+ if (!entry)
10
+ return undefined;
11
+ if (Date.now() > entry.expiresAt) {
12
+ this.store.delete(key);
13
+ return undefined;
14
+ }
15
+ return entry.value;
16
+ }
17
+ set(key, value, ttlMs) {
18
+ this.store.set(key, {
19
+ value,
20
+ expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
21
+ });
22
+ }
23
+ invalidate(pattern) {
24
+ for (const key of this.store.keys()) {
25
+ if (key.includes(pattern)) {
26
+ this.store.delete(key);
27
+ }
28
+ }
29
+ }
30
+ clear() {
31
+ this.store.clear();
32
+ }
33
+ get size() {
34
+ return this.store.size;
35
+ }
36
+ }
@@ -0,0 +1,27 @@
1
+ import type { ScanResult, ScanPending, ScanEnqueued, RankingsResponse } from "./types.js";
2
+ export declare class IsAgentReadyApiError extends Error {
3
+ readonly status: number;
4
+ readonly details?: unknown | undefined;
5
+ constructor(message: string, status: number, details?: unknown | undefined);
6
+ }
7
+ interface ClientOptions {
8
+ maxRetries?: number;
9
+ cacheTtlMs?: number;
10
+ }
11
+ export declare class IsAgentReadyClient {
12
+ private readonly baseUrl;
13
+ private readonly cache;
14
+ private readonly maxRetries;
15
+ constructor(baseUrl?: string, options?: ClientOptions);
16
+ private request;
17
+ getScanResults(domain: string): Promise<ScanResult | ScanPending>;
18
+ createScan(url: string): Promise<ScanEnqueued | ScanResult>;
19
+ getRankings(params?: {
20
+ page?: number;
21
+ per_page?: number;
22
+ grade_range?: "high" | "mid" | "low";
23
+ search?: string;
24
+ sort?: "score_desc" | "score_asc" | "domain" | "newest";
25
+ }): Promise<RankingsResponse>;
26
+ }
27
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,79 @@
1
+ import { TtlCache } from "./cache.js";
2
+ export class IsAgentReadyApiError extends Error {
3
+ status;
4
+ details;
5
+ constructor(message, status, details) {
6
+ super(message);
7
+ this.status = status;
8
+ this.details = details;
9
+ }
10
+ }
11
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+ export class IsAgentReadyClient {
13
+ baseUrl;
14
+ cache;
15
+ maxRetries;
16
+ constructor(baseUrl = "https://isagentready.com", options = {}) {
17
+ this.baseUrl = baseUrl.replace(/\/$/, "");
18
+ this.cache = new TtlCache(options.cacheTtlMs ?? 120_000);
19
+ this.maxRetries = options.maxRetries ?? 3;
20
+ }
21
+ async request(method, path, body, cacheKey) {
22
+ if (cacheKey) {
23
+ const cached = this.cache.get(cacheKey);
24
+ if (cached !== undefined)
25
+ return cached;
26
+ }
27
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
28
+ const url = `${this.baseUrl}${path}`;
29
+ const headers = new Headers({ "Content-Type": "application/json" });
30
+ const init = { method, headers };
31
+ if (body) {
32
+ init.body = JSON.stringify(body);
33
+ }
34
+ const response = await fetch(url, init);
35
+ if (response.status === 429 && attempt < this.maxRetries) {
36
+ const retryAfter = response.headers.get("retry-after");
37
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.pow(2, attempt) * 1000;
38
+ await sleep(delayMs);
39
+ continue;
40
+ }
41
+ if (!response.ok) {
42
+ const errorBody = await response.json().catch(() => null);
43
+ throw new IsAgentReadyApiError(errorBody?.error ?? `HTTP ${response.status}`, response.status, errorBody);
44
+ }
45
+ const data = (await response.json());
46
+ if (cacheKey) {
47
+ this.cache.set(cacheKey, data);
48
+ }
49
+ return data;
50
+ }
51
+ throw new Error("Request failed after retries");
52
+ }
53
+ async getScanResults(domain) {
54
+ const encoded = encodeURIComponent(domain);
55
+ return this.request("GET", `/api/v1/scan/${encoded}`, undefined, `scan:${domain}`);
56
+ }
57
+ async createScan(url) {
58
+ this.cache.invalidate("scan:");
59
+ this.cache.invalidate("rankings:");
60
+ return this.request("POST", "/api/v1/scan", { url });
61
+ }
62
+ async getRankings(params = {}) {
63
+ const searchParams = new URLSearchParams();
64
+ if (params.page !== undefined)
65
+ searchParams.set("page", String(params.page));
66
+ if (params.per_page !== undefined)
67
+ searchParams.set("per_page", String(params.per_page));
68
+ if (params.grade_range)
69
+ searchParams.set("grade_range", params.grade_range);
70
+ if (params.search)
71
+ searchParams.set("search", params.search);
72
+ if (params.sort)
73
+ searchParams.set("sort", params.sort);
74
+ const query = searchParams.toString();
75
+ const path = `/api/v1/rankings${query ? `?${query}` : ""}`;
76
+ const cacheKey = `rankings:${query}`;
77
+ return this.request("GET", path, undefined, cacheKey);
78
+ }
79
+ }
@@ -0,0 +1,5 @@
1
+ import type { ScanResult, Category, Checkpoint } from "./types.js";
2
+ export declare const formatCheckpoint: (cp: Checkpoint) => string;
3
+ export declare const formatCategory: (cat: Category) => string;
4
+ export declare const formatScanResult: (scan: ScanResult) => string;
5
+ export declare const formatScanSummary: (scan: ScanResult) => string;
package/dist/format.js ADDED
@@ -0,0 +1,37 @@
1
+ const statusIcon = {
2
+ pass: "[PASS]",
3
+ partial: "[PARTIAL]",
4
+ fail: "[FAIL]",
5
+ skip: "[SKIP]",
6
+ };
7
+ export const formatCheckpoint = (cp) => {
8
+ const lines = [
9
+ ` ${statusIcon[cp.status] ?? cp.status} ${cp.id} ${cp.name} (${cp.score}/${cp.max_score})`,
10
+ ];
11
+ if (cp.details)
12
+ lines.push(` Details: ${cp.details}`);
13
+ if (cp.status !== "pass" && cp.recommendation)
14
+ lines.push(` Recommendation: ${cp.recommendation}`);
15
+ return lines.join("\n");
16
+ };
17
+ export const formatCategory = (cat) => {
18
+ const pct = cat.max_score > 0 ? Math.round((cat.score / cat.max_score) * 100) : 0;
19
+ const lines = [
20
+ `${cat.label} — ${cat.score}/${cat.max_score} (${pct}%, weight: ${cat.weight}%)`,
21
+ ...cat.checkpoints.map(formatCheckpoint),
22
+ ];
23
+ return lines.join("\n");
24
+ };
25
+ export const formatScanResult = (scan) => {
26
+ const lines = [
27
+ `Domain: ${scan.domain}`,
28
+ `Grade: ${scan.letter_grade} (${scan.overall_score}/100)`,
29
+ `Status: ${scan.status}`,
30
+ scan.scan_duration_ms ? `Scan duration: ${scan.scan_duration_ms}ms` : "",
31
+ scan.completed_at ? `Completed: ${scan.completed_at}` : "",
32
+ "",
33
+ ...(scan.categories ?? []).map(formatCategory),
34
+ ];
35
+ return lines.filter(Boolean).join("\n");
36
+ };
37
+ export const formatScanSummary = (scan) => `${scan.domain} — Grade: ${scan.letter_grade} (${scan.overall_score}/100)`;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { IsAgentReadyClient } from "./client.js";
5
+ import { createServer, parseToolsets } from "./server.js";
6
+ import { checkForUpdate } from "./update-checker.js";
7
+ const require = createRequire(import.meta.url);
8
+ const { name, version } = require("../package.json");
9
+ const cacheTtl = process.env.ISAGENTREADY_CACHE_TTL !== undefined
10
+ ? parseInt(process.env.ISAGENTREADY_CACHE_TTL, 10) * 1000
11
+ : undefined;
12
+ const maxRetries = process.env.ISAGENTREADY_MAX_RETRIES !== undefined
13
+ ? parseInt(process.env.ISAGENTREADY_MAX_RETRIES, 10)
14
+ : 3;
15
+ const baseUrl = process.env.ISAGENTREADY_BASE_URL ?? "https://isagentready.com";
16
+ const client = new IsAgentReadyClient(baseUrl, { maxRetries, cacheTtlMs: cacheTtl });
17
+ const toolsets = parseToolsets(process.env.ISAGENTREADY_TOOLSETS);
18
+ const server = createServer(client, toolsets);
19
+ const main = async () => {
20
+ const transport = new StdioServerTransport();
21
+ await server.connect(transport);
22
+ // Fire-and-forget — don't block server startup
23
+ void checkForUpdate(name, version);
24
+ };
25
+ main().catch((error) => {
26
+ console.error("IsAgentReady MCP server failed:", error);
27
+ process.exit(1);
28
+ });
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { IsAgentReadyClient } from "./client.js";
3
+ export type Toolset = "scans" | "rankings";
4
+ export declare const parseToolsets: (env?: string) => Set<Toolset>;
5
+ export declare const createServer: (client: IsAgentReadyClient, toolsets?: Set<Toolset>) => McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,41 @@
1
+ import { createRequire } from "node:module";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerScanTools } from "./tools/scans.js";
4
+ import { registerRankingTools } from "./tools/rankings.js";
5
+ const require = createRequire(import.meta.url);
6
+ const { version } = require("../package.json");
7
+ const ALL_TOOLSETS = ["scans", "rankings"];
8
+ export const parseToolsets = (env) => {
9
+ if (!env)
10
+ return new Set(ALL_TOOLSETS);
11
+ const requested = env.split(",").map((s) => s.trim().toLowerCase());
12
+ const valid = new Set();
13
+ for (const name of requested) {
14
+ if (ALL_TOOLSETS.includes(name)) {
15
+ valid.add(name);
16
+ }
17
+ }
18
+ return valid.size > 0 ? valid : new Set(ALL_TOOLSETS);
19
+ };
20
+ const toolsetRegistry = {
21
+ scans: [registerScanTools],
22
+ rankings: [registerRankingTools],
23
+ };
24
+ export const createServer = (client, toolsets) => {
25
+ const server = new McpServer({
26
+ name: "isagentready-mcp",
27
+ version,
28
+ });
29
+ const enabled = toolsets ?? new Set(ALL_TOOLSETS);
30
+ const registered = new Set();
31
+ for (const toolset of enabled) {
32
+ const registerers = toolsetRegistry[toolset];
33
+ for (const register of registerers) {
34
+ if (!registered.has(register)) {
35
+ registered.add(register);
36
+ register(server, client);
37
+ }
38
+ }
39
+ }
40
+ return server;
41
+ };
@@ -0,0 +1,14 @@
1
+ export declare const toTextResult: (text: string, structuredContent?: Record<string, unknown>) => {
2
+ structuredContent?: Record<string, unknown> | undefined;
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ };
8
+ export declare const toErrorResult: (error: unknown) => {
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ isError: boolean;
14
+ };
@@ -0,0 +1,61 @@
1
+ import { IsAgentReadyApiError } from "./client.js";
2
+ export const toTextResult = (text, structuredContent) => ({
3
+ content: [{ type: "text", text }],
4
+ ...(structuredContent ? { structuredContent } : {}),
5
+ });
6
+ const getRecoverySuggestion = (status, message, details) => {
7
+ if (status === 429) {
8
+ return "Rate limit exceeded. Wait a moment and retry. If scanning, note that recently scanned domains have a 1-hour cooldown.";
9
+ }
10
+ if (status === 404) {
11
+ return "No scan found for this domain. Use scan_website to trigger a new scan first.";
12
+ }
13
+ if (status === 400) {
14
+ const detailStr = typeof details === "string" ? details : JSON.stringify(details ?? "");
15
+ const lower = detailStr.toLowerCase();
16
+ if (lower.includes("url")) {
17
+ return "Invalid or missing URL. Ensure the URL starts with http:// or https://.";
18
+ }
19
+ return "Missing required parameter. Ensure the URL or domain is provided.";
20
+ }
21
+ if (status === 409) {
22
+ return "Conflict — a scan for this domain is already in progress. Wait for it to complete, then use get_scan_results to retrieve the results.";
23
+ }
24
+ if (status === 422) {
25
+ return "Invalid URL. The URL must start with http:// or https:// and be a valid website address.";
26
+ }
27
+ if (status >= 500) {
28
+ return "IsAgentReady API server error. This is a temporary issue. Wait a moment and retry.";
29
+ }
30
+ return null;
31
+ };
32
+ export const toErrorResult = (error) => {
33
+ if (error instanceof IsAgentReadyApiError) {
34
+ const suggestion = getRecoverySuggestion(error.status, error.message, error.details);
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: [
40
+ `IsAgentReady API error: ${error.message}`,
41
+ `Status: ${error.status}`,
42
+ error.details ? `Details: ${JSON.stringify(error.details, null, 2)}` : "",
43
+ suggestion ? `\nRecovery: ${suggestion}` : "",
44
+ ]
45
+ .filter(Boolean)
46
+ .join("\n"),
47
+ },
48
+ ],
49
+ isError: true,
50
+ };
51
+ }
52
+ return {
53
+ content: [
54
+ {
55
+ type: "text",
56
+ text: error instanceof Error ? error.message : String(error),
57
+ },
58
+ ],
59
+ isError: true,
60
+ };
61
+ };
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { IsAgentReadyClient } from "../client.js";
3
+ export declare const registerRankingTools: (server: McpServer, client: IsAgentReadyClient) => void;
@@ -0,0 +1,52 @@
1
+ import * as z from "zod/v4";
2
+ import { formatScanSummary } from "../format.js";
3
+ import { toTextResult, toErrorResult } from "../tool-result.js";
4
+ export const registerRankingTools = (server, client) => {
5
+ server.registerTool("get_rankings", {
6
+ title: "Get AI Readiness Rankings",
7
+ description: "Get a paginated, sorted list of website AI readiness scores. Supports filtering by grade range (high/mid/low), search by domain name, and sorting by score, domain, or newest.",
8
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
9
+ inputSchema: z.object({
10
+ page: z.number().int().min(1).default(1).describe("Page number (default: 1)"),
11
+ per_page: z
12
+ .number()
13
+ .int()
14
+ .min(1)
15
+ .max(100)
16
+ .default(25)
17
+ .describe("Results per page (1-100, default: 25)"),
18
+ grade_range: z
19
+ .enum(["high", "mid", "low"])
20
+ .optional()
21
+ .describe("Filter by grade range: high (A/A+), mid (B/C), low (D/F)"),
22
+ search: z.string().optional().describe("Search by domain name"),
23
+ sort: z
24
+ .enum(["score_desc", "score_asc", "domain", "newest"])
25
+ .default("score_desc")
26
+ .describe("Sort order (default: score_desc)"),
27
+ }),
28
+ }, async ({ page, per_page, grade_range, search, sort }) => {
29
+ try {
30
+ const result = await client.getRankings({ page, per_page, grade_range, search, sort });
31
+ const lines = [
32
+ `AI Readiness Rankings — Page ${result.page}/${result.total_pages} (${result.total} total)`,
33
+ "",
34
+ ...result.entries.map((entry, i) => `${(result.page - 1) * result.per_page + i + 1}. ${formatScanSummary(entry)}`),
35
+ ];
36
+ if (result.entries.length === 0) {
37
+ lines.push("No results found.");
38
+ }
39
+ return toTextResult(lines.join("\n"), {
40
+ type: "rankings",
41
+ page: result.page,
42
+ per_page: result.per_page,
43
+ total: result.total,
44
+ total_pages: result.total_pages,
45
+ entries: result.entries,
46
+ });
47
+ }
48
+ catch (error) {
49
+ return toErrorResult(error);
50
+ }
51
+ });
52
+ };
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { IsAgentReadyClient } from "../client.js";
3
+ export declare const registerScanTools: (server: McpServer, client: IsAgentReadyClient) => void;
@@ -0,0 +1,67 @@
1
+ import * as z from "zod/v4";
2
+ import { formatScanResult } from "../format.js";
3
+ import { toTextResult, toErrorResult } from "../tool-result.js";
4
+ const isScanCompleted = (result) => result.status === "completed" || result.status === "failed";
5
+ export const registerScanTools = (server, client) => {
6
+ server.registerTool("scan_website", {
7
+ title: "Scan Website for AI Agent Readiness",
8
+ description: "Trigger a new scan of a website for AI agent readiness. The scan runs asynchronously and typically takes 15-30 seconds. If a recent scan exists (within 1 hour), returns cached results. Use get_scan_results to poll for completed results.",
9
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
10
+ inputSchema: z.object({
11
+ url: z
12
+ .string()
13
+ .min(1)
14
+ .describe("The full URL to scan (must be http:// or https://)"),
15
+ }),
16
+ }, async ({ url }) => {
17
+ try {
18
+ const result = await client.createScan(url);
19
+ if (isScanCompleted(result)) {
20
+ return toTextResult(`Recent scan found (cached):\n\n${formatScanResult(result)}`, { type: "cached_result", ...result });
21
+ }
22
+ return toTextResult([
23
+ `Scan enqueued for ${result.domain}`,
24
+ `Status: ${result.status}`,
25
+ `ID: ${result.id}`,
26
+ "message" in result ? `Message: ${result.message}` : "",
27
+ "",
28
+ `Poll with get_scan_results for domain "${result.domain}" to check when results are ready.`,
29
+ ]
30
+ .filter(Boolean)
31
+ .join("\n"), { type: "enqueued", id: result.id, domain: result.domain, status: result.status });
32
+ }
33
+ catch (error) {
34
+ return toErrorResult(error);
35
+ }
36
+ });
37
+ server.registerTool("get_scan_results", {
38
+ title: "Get Scan Results",
39
+ description: "Get the latest scan results for a domain, including overall score, letter grade, per-category breakdowns, and individual checkpoint results with actionable recommendations. Returns a pending status if the scan is still in progress.",
40
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
41
+ inputSchema: z.object({
42
+ domain: z
43
+ .string()
44
+ .min(1)
45
+ .describe("The domain to get scan results for (e.g., example.com)"),
46
+ }),
47
+ }, async ({ domain }) => {
48
+ try {
49
+ const result = await client.getScanResults(domain);
50
+ if (!isScanCompleted(result)) {
51
+ const message = "message" in result ? String(result.message) : "";
52
+ return toTextResult([
53
+ `Scan for ${result.domain} is ${result.status}.`,
54
+ message,
55
+ "",
56
+ "Poll again in a few seconds to check for completion.",
57
+ ]
58
+ .filter(Boolean)
59
+ .join("\n"), { type: "pending", id: result.id, domain: result.domain, status: result.status });
60
+ }
61
+ return toTextResult(formatScanResult(result), { type: "completed", ...result });
62
+ }
63
+ catch (error) {
64
+ return toErrorResult(error);
65
+ }
66
+ });
67
+ };
@@ -0,0 +1,49 @@
1
+ export interface Checkpoint {
2
+ id: string;
3
+ name: string;
4
+ status: "pass" | "partial" | "fail" | "skip";
5
+ score: number;
6
+ max_score: number;
7
+ details: string;
8
+ recommendation: string;
9
+ why: string;
10
+ code_example?: string;
11
+ }
12
+ export interface Category {
13
+ category: "discovery" | "structured_data" | "semantics" | "agent_protocols" | "security";
14
+ label: string;
15
+ score: number;
16
+ max_score: number;
17
+ weight: number;
18
+ checkpoints: Checkpoint[];
19
+ }
20
+ export interface ScanResult {
21
+ id: string;
22
+ domain: string;
23
+ status: "completed" | "failed";
24
+ overall_score: number;
25
+ letter_grade: "F" | "D" | "C" | "B" | "A" | "A+";
26
+ scan_duration_ms: number;
27
+ completed_at: string;
28
+ categories: Category[];
29
+ }
30
+ export interface ScanPending {
31
+ id: string;
32
+ domain: string;
33
+ status: "pending" | "running";
34
+ message: string;
35
+ }
36
+ export interface ScanEnqueued {
37
+ id: string;
38
+ domain: string;
39
+ status: "pending" | "running";
40
+ poll_url: string;
41
+ message: string;
42
+ }
43
+ export interface RankingsResponse {
44
+ entries: ScanResult[];
45
+ total: number;
46
+ page: number;
47
+ per_page: number;
48
+ total_pages: number;
49
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const isNewerVersion: (latest: string, current: string) => boolean;
2
+ export declare const checkForUpdate: (packageName: string, currentVersion: string) => Promise<void>;
@@ -0,0 +1,26 @@
1
+ export const isNewerVersion = (latest, current) => {
2
+ const l = latest.split(".").map(Number);
3
+ const c = current.split(".").map(Number);
4
+ for (let i = 0; i < 3; i++) {
5
+ if ((l[i] ?? 0) > (c[i] ?? 0))
6
+ return true;
7
+ if ((l[i] ?? 0) < (c[i] ?? 0))
8
+ return false;
9
+ }
10
+ return false;
11
+ };
12
+ export const checkForUpdate = async (packageName, currentVersion) => {
13
+ try {
14
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
15
+ if (!response.ok)
16
+ return;
17
+ const data = (await response.json());
18
+ if (isNewerVersion(data.version, currentVersion)) {
19
+ process.stderr.write(`\n Update available: ${currentVersion} → ${data.version}\n` +
20
+ ` Run: npm install -g ${packageName}@latest\n\n`);
21
+ }
22
+ }
23
+ catch {
24
+ // Best-effort — silently ignore network errors
25
+ }
26
+ };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "isagentready-mcp",
3
+ "version": "0.2.0",
4
+ "packageManager": "pnpm@10.28.1",
5
+ "description": "MCP server for scanning websites for AI agent readiness via the IsAgentReady API",
6
+ "type": "module",
7
+ "bin": {
8
+ "isagentready-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "dev": "tsx src/index.ts",
15
+ "start": "node --enable-source-maps dist/index.js",
16
+ "build": "tsc -p tsconfig.json",
17
+ "typecheck": "tsc -p tsconfig.json --noEmit",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "pnpm build",
20
+ "release": "GITHUB_TOKEN=$(gh auth token) release-it"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/bartwaardenburg/isagentready-mcp.git"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "mcp-server",
29
+ "isagentready",
30
+ "ai-readiness",
31
+ "website-scanner",
32
+ "agent-readiness",
33
+ "model-context-protocol",
34
+ "automation",
35
+ "ai",
36
+ "claude",
37
+ "llm"
38
+ ],
39
+ "author": "Bart Waardenburg",
40
+ "license": "MIT",
41
+ "mcpName": "io.github.BartWaardenburg/isagentready-mcp",
42
+ "bugs": {
43
+ "url": "https://github.com/bartwaardenburg/isagentready-mcp/issues"
44
+ },
45
+ "homepage": "https://github.com/bartwaardenburg/isagentready-mcp#readme",
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.26.0",
48
+ "zod": "^4.3.6"
49
+ },
50
+ "devDependencies": {
51
+ "@release-it/conventional-changelog": "^10.0.5",
52
+ "@types/node": "^20.19.30",
53
+ "@vitest/coverage-v8": "^4.0.18",
54
+ "release-it": "^19.2.4",
55
+ "tsx": "^4.21.0",
56
+ "typescript": "^5.9.2",
57
+ "vitest": "^4.0.18"
58
+ },
59
+ "engines": {
60
+ "node": ">=20"
61
+ }
62
+ }