seo-intel 1.5.25 → 1.5.27
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/CHANGELOG.md +25 -0
- package/mcp/server.js +246 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.27 (2026-05-16)
|
|
4
|
+
|
|
5
|
+
### MCP — three new free-tier read tools
|
|
6
|
+
The MCP server (`seo-intel-mcp`) now exposes individual records, not just summaries. AI agents can drill from inventory into actual pages, keywords, and heading structures without leaving the agent chat.
|
|
7
|
+
|
|
8
|
+
- **`get_pages(project, role?, limit?, offset?)`** — paginated page list with url, title, word count, status, click depth, and domain role. Filterable by role (target / owned / competitor). Returns total count for pagination math.
|
|
9
|
+
- **`list_keywords(project, domain?, limit?)`** — top extracted keywords grouped by domain + location (title / h1 / h2 / meta / body). Use to surface what each site is targeting before running gap analysis.
|
|
10
|
+
- **`get_headings(project, url, limit?)`** — heading structure (H1–H6) for a specific page. Returns ordered `{ level, text }` list. Useful for content-architecture comparisons between target and competitor pages.
|
|
11
|
+
|
|
12
|
+
All three are **free tier** — no license required. Pairs naturally with the existing `list_projects` and `get_intel(raw)` to give AI agents a complete free-tier read surface: list projects → inspect inventory → drill into pages → read headings → analyze with the agent's own flagship LLM.
|
|
13
|
+
|
|
14
|
+
Errors are returned as proper MCP `isError: true` responses with helpful guidance (e.g. `get_headings` on an unknown URL points the agent at `get_pages`).
|
|
15
|
+
|
|
16
|
+
## 1.5.26 (2026-05-16)
|
|
17
|
+
|
|
18
|
+
### New — MCP server (`seo-intel-mcp`)
|
|
19
|
+
- SEO Intel now ships a Model Context Protocol server. Any MCP-capable AI host (Claude Code, Cursor, Cline, Continue, Zed) can call seo-intel's local SQLite intelligence as native tools — no API keys to manage, no remote servers to host, all data stays on your machine.
|
|
20
|
+
- Install for Claude Code: `claude mcp add seo-intel "npx seo-intel-mcp"`
|
|
21
|
+
- Stdio transport — the host spawns the server as a subprocess; zero infrastructure.
|
|
22
|
+
- Tools shipped in this release:
|
|
23
|
+
- `list_projects` (**free**) — every configured project on this machine + crawled page count
|
|
24
|
+
- `get_intel(project, for?)` — wraps `seo-intel intel`. `for=raw` is free; `for=audit|blog|competitor` require an SEO Intel Solo license. When unlicensed, returns a clean MCP error with the upgrade message instead of silent failure.
|
|
25
|
+
- Both tools return structured JSON the agent's LLM can chain — e.g. an agent can call `list_projects` then `get_intel(project=X, for=raw)` and analyse the raw inventory with its own flagship model, no extra prompting needed.
|
|
26
|
+
- New dependency: `@modelcontextprotocol/sdk ^1.29.0`.
|
|
27
|
+
|
|
3
28
|
## 1.5.25 (2026-05-16)
|
|
4
29
|
|
|
5
30
|
### New — `seo-intel intel <project>` — canonical agent-facing entry point
|
package/mcp/server.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* seo-intel MCP server — stdio transport.
|
|
4
|
+
*
|
|
5
|
+
* Run as a subprocess by an MCP-capable host (Claude Code, Cursor, Cline,
|
|
6
|
+
* Continue, Zed, etc.). Exposes seo-intel's local SQLite intelligence to
|
|
7
|
+
* the host's LLM as native tools.
|
|
8
|
+
*
|
|
9
|
+
* Install for Claude Code:
|
|
10
|
+
* claude mcp add seo-intel "npx seo-intel-mcp"
|
|
11
|
+
*
|
|
12
|
+
* Tools (v1.5.26):
|
|
13
|
+
* list_projects — free — projects on this machine + page counts
|
|
14
|
+
* get_intel — free `raw` slice / paid `audit|blog|competitor` slices
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT: stdout is reserved for JSON-RPC messages. All logging here goes
|
|
17
|
+
* to stderr. Never use console.log in this file.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
21
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
22
|
+
import * as z from 'zod/v4';
|
|
23
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
24
|
+
import { dirname, join } from 'path';
|
|
25
|
+
import { fileURLToPath } from 'url';
|
|
26
|
+
|
|
27
|
+
import { getDb } from '../db/db.js';
|
|
28
|
+
import { getIntel, INTEL_SLICES, FREE_SLICES } from '../lib/intel.js';
|
|
29
|
+
import { isPro } from '../lib/license.js';
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const ROOT = join(__dirname, '..');
|
|
33
|
+
const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
34
|
+
const CONFIG_DIR = join(ROOT, 'config');
|
|
35
|
+
|
|
36
|
+
const server = new McpServer({ name: 'seo-intel', version: VERSION });
|
|
37
|
+
|
|
38
|
+
function listConfigProjects() {
|
|
39
|
+
if (!existsSync(CONFIG_DIR)) return [];
|
|
40
|
+
return readdirSync(CONFIG_DIR)
|
|
41
|
+
.filter(f => f.endsWith('.json') && f !== 'example.json' && !f.startsWith('setup'))
|
|
42
|
+
.map(f => {
|
|
43
|
+
try {
|
|
44
|
+
const c = JSON.parse(readFileSync(join(CONFIG_DIR, f), 'utf8'));
|
|
45
|
+
return { project: c.project || f.replace('.json', ''), target: c.target?.domain || null };
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
})
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Tool: list_projects (free) ────────────────────────────────────────────
|
|
52
|
+
server.registerTool(
|
|
53
|
+
'list_projects',
|
|
54
|
+
{
|
|
55
|
+
description: 'List all SEO Intel projects configured on this machine, each with its target domain and crawled page count. Use this first to discover which projects are available before calling get_intel. Free tier — no license required.',
|
|
56
|
+
},
|
|
57
|
+
async () => {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
const configs = listConfigProjects();
|
|
60
|
+
const out = configs.map(c => {
|
|
61
|
+
const row = db.prepare(
|
|
62
|
+
'SELECT COUNT(*) AS n FROM pages p JOIN domains d ON d.id=p.domain_id WHERE d.project=?'
|
|
63
|
+
).get(c.project);
|
|
64
|
+
return { ...c, pages: row?.n || 0 };
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
68
|
+
structuredContent: { projects: out },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// ── Tool: get_intel (free raw / paid others) ──────────────────────────────
|
|
74
|
+
server.registerTool(
|
|
75
|
+
'get_intel',
|
|
76
|
+
{
|
|
77
|
+
description: [
|
|
78
|
+
'Get structured project intelligence as a JSON envelope ready for AI agent consumption.',
|
|
79
|
+
'',
|
|
80
|
+
'Slices:',
|
|
81
|
+
' raw (FREE) page/keyword/heading/schema/sitemap inventory per domain',
|
|
82
|
+
' audit (paid) citability scores + active insights ledger',
|
|
83
|
+
' blog (paid) keyword gaps + long tails + drafting hints',
|
|
84
|
+
' competitor (paid) competitor summary + keyword matrix + positioning',
|
|
85
|
+
'',
|
|
86
|
+
'Paid slices require an SEO Intel Solo license (set SEO_INTEL_LICENSE in env, or activate via the CLI). When unlicensed, the tool returns a clear upgrade message — no silent failure.',
|
|
87
|
+
'',
|
|
88
|
+
'Output envelope: { project, for, tier, generated_at, seo_intel_version, data }.',
|
|
89
|
+
].join('\n'),
|
|
90
|
+
inputSchema: {
|
|
91
|
+
project: z.string().describe('Project slug. Call list_projects first to discover available projects.'),
|
|
92
|
+
for: z.enum(INTEL_SLICES).optional().describe('Slice — defaults to "raw" (free).'),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
async ({ project, for: slice = 'raw' }) => {
|
|
96
|
+
if (!FREE_SLICES.includes(slice) && !isPro()) {
|
|
97
|
+
const msg = `The "${slice}" slice requires SEO Intel Solo (€19.99/mo). Free tier supports: ${FREE_SLICES.join(', ')}. Activate at https://ukkometa.fi/en/seo-intel/ — set SEO_INTEL_LICENSE=SI-xxxx-xxxx-xxxx-xxxx in your env.`;
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: msg }],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
const envelope = getIntel(db, project, { for: slice });
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
|
|
108
|
+
structuredContent: envelope,
|
|
109
|
+
};
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: 'text', text: `seo-intel error: ${err.message}` }],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// ── Tool: get_pages (free) ────────────────────────────────────────────────
|
|
120
|
+
server.registerTool(
|
|
121
|
+
'get_pages',
|
|
122
|
+
{
|
|
123
|
+
description: 'Paginated list of crawled pages for a project, with url, title, word count, status, and domain role. Use this to drill into individual pages after seeing the inventory summary from get_intel. Free tier.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
project: z.string().describe('Project slug'),
|
|
126
|
+
role: z.enum(['target', 'owned', 'competitor']).optional().describe('Filter by domain role'),
|
|
127
|
+
limit: z.number().int().positive().max(500).optional().describe('Max pages to return (default 50, max 500)'),
|
|
128
|
+
offset: z.number().int().nonnegative().optional().describe('Offset for pagination (default 0)'),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
async ({ project, role, limit = 50, offset = 0 }) => {
|
|
132
|
+
try {
|
|
133
|
+
const db = getDb();
|
|
134
|
+
const whereParams = role ? [project, role] : [project];
|
|
135
|
+
const where = role ? 'd.project = ? AND d.role = ?' : 'd.project = ?';
|
|
136
|
+
const rows = db.prepare(
|
|
137
|
+
`SELECT p.url, p.title, p.word_count, p.status_code, p.click_depth,
|
|
138
|
+
d.domain, d.role
|
|
139
|
+
FROM pages p JOIN domains d ON d.id = p.domain_id
|
|
140
|
+
WHERE ${where}
|
|
141
|
+
ORDER BY d.role, d.domain, p.url
|
|
142
|
+
LIMIT ? OFFSET ?`
|
|
143
|
+
).all(...whereParams, limit, offset);
|
|
144
|
+
const total = db.prepare(
|
|
145
|
+
`SELECT COUNT(*) AS n FROM pages p JOIN domains d ON d.id = p.domain_id WHERE ${where}`
|
|
146
|
+
).get(...whereParams)?.n || 0;
|
|
147
|
+
const out = { project, role: role || 'any', total, returned: rows.length, offset, pages: rows };
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
150
|
+
structuredContent: out,
|
|
151
|
+
};
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// ── Tool: list_keywords (free) ────────────────────────────────────────────
|
|
159
|
+
server.registerTool(
|
|
160
|
+
'list_keywords',
|
|
161
|
+
{
|
|
162
|
+
description: 'Top extracted keywords for a project, grouped by domain. Each keyword has frequency, location (title/h1/h2/meta/body), and source domain. Use this to surface what each site is targeting before running gap analysis. Free tier.',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
project: z.string().describe('Project slug'),
|
|
165
|
+
domain: z.string().optional().describe('Optional: filter to a single domain'),
|
|
166
|
+
limit: z.number().int().positive().max(1000).optional().describe('Max keywords to return (default 100, max 1000)'),
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
async ({ project, domain, limit = 100 }) => {
|
|
170
|
+
try {
|
|
171
|
+
const db = getDb();
|
|
172
|
+
const params = [project];
|
|
173
|
+
let where = 'd.project = ?';
|
|
174
|
+
if (domain) { where += ' AND d.domain = ?'; params.push(domain); }
|
|
175
|
+
params.push(limit);
|
|
176
|
+
const rows = db.prepare(
|
|
177
|
+
`SELECT k.keyword, k.location, d.domain, d.role, COUNT(*) AS freq
|
|
178
|
+
FROM keywords k
|
|
179
|
+
JOIN pages p ON p.id = k.page_id
|
|
180
|
+
JOIN domains d ON d.id = p.domain_id
|
|
181
|
+
WHERE ${where}
|
|
182
|
+
GROUP BY k.keyword, k.location, d.domain
|
|
183
|
+
ORDER BY freq DESC
|
|
184
|
+
LIMIT ?`
|
|
185
|
+
).all(...params);
|
|
186
|
+
const out = { project, domain: domain || 'all', returned: rows.length, keywords: rows };
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
189
|
+
structuredContent: out,
|
|
190
|
+
};
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// ── Tool: get_headings (free) ─────────────────────────────────────────────
|
|
198
|
+
server.registerTool(
|
|
199
|
+
'get_headings',
|
|
200
|
+
{
|
|
201
|
+
description: 'Heading structure (H1–H6) for a specific page. Returns ordered list of { level, text }. Useful for content architecture comparisons between target and competitor pages. Free tier.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
project: z.string().describe('Project slug'),
|
|
204
|
+
url: z.string().describe('Exact page URL (as crawled). Get URLs from get_pages.'),
|
|
205
|
+
limit: z.number().int().positive().max(200).optional().describe('Max headings (default 50)'),
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
async ({ project, url, limit = 50 }) => {
|
|
209
|
+
try {
|
|
210
|
+
const db = getDb();
|
|
211
|
+
const page = db.prepare(
|
|
212
|
+
`SELECT p.id, p.title, p.word_count, d.domain, d.role
|
|
213
|
+
FROM pages p JOIN domains d ON d.id = p.domain_id
|
|
214
|
+
WHERE d.project = ? AND p.url = ?`
|
|
215
|
+
).get(project, url);
|
|
216
|
+
if (!page) {
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: `No crawled page found for url="${url}" in project "${project}". Use get_pages to discover URLs.` }],
|
|
219
|
+
isError: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const headings = db.prepare(
|
|
223
|
+
`SELECT level, text FROM headings WHERE page_id = ? ORDER BY id LIMIT ?`
|
|
224
|
+
).all(page.id, limit);
|
|
225
|
+
const out = { project, url, page_title: page.title, domain: page.domain, role: page.role, word_count: page.word_count, headings };
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
228
|
+
structuredContent: out,
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
async function main() {
|
|
237
|
+
const transport = new StdioServerTransport();
|
|
238
|
+
await server.connect(transport);
|
|
239
|
+
// stderr is fine; the host typically surfaces this in its MCP logs panel.
|
|
240
|
+
console.error(`[seo-intel-mcp] v${VERSION} ready on stdio. Tools: list_projects, get_intel, get_pages, list_keywords, get_headings.`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main().catch(err => {
|
|
244
|
+
console.error('[seo-intel-mcp] fatal:', err);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seo-intel",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.27",
|
|
4
4
|
"description": "Local Ahrefs-style SEO competitor intelligence. Crawl → SQLite → cloud analysis.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"local-first"
|
|
21
21
|
],
|
|
22
22
|
"bin": {
|
|
23
|
-
"seo-intel": "cli.js"
|
|
23
|
+
"seo-intel": "cli.js",
|
|
24
|
+
"seo-intel-mcp": "mcp/server.js"
|
|
24
25
|
},
|
|
25
26
|
"exports": {
|
|
26
27
|
".": "./cli.js",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"analysis/",
|
|
66
67
|
"extractor/",
|
|
67
68
|
"exports/",
|
|
69
|
+
"mcp/",
|
|
68
70
|
"reports/generate-html.js",
|
|
69
71
|
"reports/generate-site-graph.js",
|
|
70
72
|
"reports/gsc-loader.js",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
"postinstall": "echo '\\n SEO Intel installed.\\n Run: seo-intel setup\\n Or: seo-intel serve (opens dashboard)\\n'"
|
|
88
90
|
},
|
|
89
91
|
"dependencies": {
|
|
92
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
90
93
|
"chalk": "^5.3.0",
|
|
91
94
|
"commander": "^12.0.0",
|
|
92
95
|
"dotenv": "^16.4.5",
|