seolint-mcp 0.1.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/README.md +41 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +111 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# seolint-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [SEOLint](https://seolint.dev) — scan any website for SEO issues directly from Claude.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add to your Claude Desktop config (`claude_desktop_config.json`):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"seolint": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "seolint-mcp"],
|
|
15
|
+
"env": {
|
|
16
|
+
"SEOLINT_API_KEY": "sl_your_key"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Get your API key at [seolint.dev/dashboard](https://seolint.dev/dashboard).
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
### scan_website
|
|
28
|
+
|
|
29
|
+
Scan a URL for SEO, performance, accessibility, and AI search issues.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
"Scan https://mysite.com for SEO issues"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### get_scan
|
|
36
|
+
|
|
37
|
+
Retrieve results of a previous scan by ID.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const API_BASE = process.env.SEOLINT_API_URL ?? "https://seolint.dev";
|
|
6
|
+
const API_KEY = process.env.SEOLINT_API_KEY ?? "";
|
|
7
|
+
function headers() {
|
|
8
|
+
const h = { "Content-Type": "application/json" };
|
|
9
|
+
if (API_KEY)
|
|
10
|
+
h["Authorization"] = `Bearer ${API_KEY}`;
|
|
11
|
+
return h;
|
|
12
|
+
}
|
|
13
|
+
async function pollScan(scanId, maxWaitMs = 60_000) {
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
while (Date.now() - start < maxWaitMs) {
|
|
16
|
+
const res = await fetch(`${API_BASE}/api/v1/scan/${scanId}`, { headers: headers() });
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
throw new Error(`Poll failed: ${res.status}`);
|
|
19
|
+
const data = await res.json();
|
|
20
|
+
if (data.status === "complete")
|
|
21
|
+
return data;
|
|
22
|
+
if (data.status === "error")
|
|
23
|
+
throw new Error(data.error_message ?? "Scan failed");
|
|
24
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Scan timed out after 60 seconds");
|
|
27
|
+
}
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: "seolint",
|
|
30
|
+
version: "0.1.0",
|
|
31
|
+
});
|
|
32
|
+
// Tool: scan a website
|
|
33
|
+
server.tool("scan_website", "Scan a website for SEO, performance, accessibility, and AI search issues. Returns structured issues with LLM-ready fix instructions.", { url: z.string().describe("The full URL to scan, e.g. https://example.com") }, async ({ url }) => {
|
|
34
|
+
// Start scan
|
|
35
|
+
const startRes = await fetch(`${API_BASE}/api/v1/scan`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: headers(),
|
|
38
|
+
body: JSON.stringify({ url }),
|
|
39
|
+
});
|
|
40
|
+
if (!startRes.ok) {
|
|
41
|
+
const err = await startRes.json().catch(() => ({}));
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: `Failed to start scan: ${err.error ?? startRes.statusText}` }],
|
|
44
|
+
isError: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const { scanId } = await startRes.json();
|
|
48
|
+
// Poll until complete
|
|
49
|
+
try {
|
|
50
|
+
const result = await pollScan(scanId);
|
|
51
|
+
const issues = (result.issues ?? []);
|
|
52
|
+
if (issues.length === 0) {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: `No issues found for ${url}. The site looks good!` }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// Format as readable text with fix prompts
|
|
58
|
+
const lines = issues.map((issue, i) => {
|
|
59
|
+
return [
|
|
60
|
+
`## ${i + 1}. [${issue.severity.toUpperCase()}] ${issue.title}`,
|
|
61
|
+
`**Category:** ${issue.category}`,
|
|
62
|
+
issue.description,
|
|
63
|
+
`**Fix:** ${issue.fix}`,
|
|
64
|
+
].join("\n");
|
|
65
|
+
});
|
|
66
|
+
const summary = `# SEO Audit: ${url}\n\n${issues.length} issues found.\n\n${lines.join("\n\n---\n\n")}`;
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: summary }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: `Scan failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Tool: get an existing scan result
|
|
79
|
+
server.tool("get_scan", "Get the results of a previous SEOLint scan by its ID.", { scanId: z.string().describe("The scan ID (UUID)") }, async ({ scanId }) => {
|
|
80
|
+
const res = await fetch(`${API_BASE}/api/v1/scan/${scanId}`, { headers: headers() });
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: `Scan not found: ${scanId}` }],
|
|
84
|
+
isError: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const data = await res.json();
|
|
88
|
+
if (data.status === "pending") {
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: `Scan ${scanId} is still running. Try again in a few seconds.` }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (data.status === "error") {
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: `Scan failed: ${data.error_message ?? "Unknown error"}` }],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const markdown = data.markdown ?? JSON.stringify(data.issues, null, 2);
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: markdown }],
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
async function main() {
|
|
105
|
+
const transport = new StdioServerTransport();
|
|
106
|
+
await server.connect(transport);
|
|
107
|
+
}
|
|
108
|
+
main().catch((err) => {
|
|
109
|
+
console.error("SEOLint MCP server failed to start:", err);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "seolint-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for SEOLint — scan any site for SEO issues from Claude Desktop or Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"seolint-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["mcp", "seo", "claude", "seolint", "audit", "ai"],
|
|
15
|
+
"author": "Random Code",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"zod": "^3.23.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.5.0",
|
|
23
|
+
"@types/node": "^22.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|