seo-intel 1.5.24 → 1.5.26
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/cli.js +41 -0
- package/lib/gate.js +3 -0
- package/lib/intel.js +177 -0
- package/mcp/server.js +129 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.26 (2026-05-16)
|
|
4
|
+
|
|
5
|
+
### New — MCP server (`seo-intel-mcp`)
|
|
6
|
+
- 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.
|
|
7
|
+
- Install for Claude Code: `claude mcp add seo-intel "npx seo-intel-mcp"`
|
|
8
|
+
- Stdio transport — the host spawns the server as a subprocess; zero infrastructure.
|
|
9
|
+
- Tools shipped in this release:
|
|
10
|
+
- `list_projects` (**free**) — every configured project on this machine + crawled page count
|
|
11
|
+
- `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.
|
|
12
|
+
- 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.
|
|
13
|
+
- New dependency: `@modelcontextprotocol/sdk ^1.29.0`.
|
|
14
|
+
|
|
15
|
+
## 1.5.25 (2026-05-16)
|
|
16
|
+
|
|
17
|
+
### New — `seo-intel intel <project>` — canonical agent-facing entry point
|
|
18
|
+
- Returns structured project intelligence as JSON or markdown — the single source of truth that upcoming MCP server, dashboard, and prompt-copy modal will all wrap (one function, four surfaces).
|
|
19
|
+
- Slices:
|
|
20
|
+
- `--for=raw` (**free**) — page/keyword/heading/schema/sitemap inventory per domain. Pipe into your own AI agent for self-service analysis.
|
|
21
|
+
- `--for=audit` (paid) — citability scores + active insights ledger
|
|
22
|
+
- `--for=blog` (paid) — keyword gaps + long tails + drafting hints
|
|
23
|
+
- `--for=competitor` (paid) — competitor summary + keyword matrix + positioning
|
|
24
|
+
- `--format=json` for agents; `--format=md` for humans / agent context windows
|
|
25
|
+
- Paid slices use the existing `requirePro()` gate — free users see a standard upgrade message; paid users get the data.
|
|
26
|
+
- New library: `lib/intel.js` exports `getIntel(db, project, opts)` + `intelToMarkdown(envelope)` for reuse from any surface.
|
|
27
|
+
|
|
3
28
|
## 1.5.24 (2026-05-16)
|
|
4
29
|
|
|
5
30
|
### Dashboard — projects with owned subdomains + sitemap data no longer vanish
|
package/cli.js
CHANGED
|
@@ -1415,6 +1415,47 @@ program
|
|
|
1415
1415
|
process.exit(0);
|
|
1416
1416
|
});
|
|
1417
1417
|
|
|
1418
|
+
// ── INTEL (canonical agent-facing entry point) ─────────────────────────────
|
|
1419
|
+
// Returns structured intelligence about a project. Same function backs MCP +
|
|
1420
|
+
// future surfaces. Free tier gets `raw`; paid slices are license-gated.
|
|
1421
|
+
program
|
|
1422
|
+
.command('intel <project>')
|
|
1423
|
+
.description('Structured project intelligence for AI agents (raw=free; audit/blog/competitor=paid)')
|
|
1424
|
+
.option('--for <slice>', 'Slice: raw | audit | blog | competitor', 'raw')
|
|
1425
|
+
.option('--format <fmt>', 'Output format: json | md', 'json')
|
|
1426
|
+
.action(async (project, opts) => {
|
|
1427
|
+
const isJson = opts.format === 'json';
|
|
1428
|
+
const { getIntel, intelToMarkdown, FREE_SLICES, INTEL_SLICES } = await import('./lib/intel.js');
|
|
1429
|
+
|
|
1430
|
+
if (!INTEL_SLICES.includes(opts.for)) {
|
|
1431
|
+
const msg = `Unknown slice "${opts.for}". Available: ${INTEL_SLICES.join(', ')}`;
|
|
1432
|
+
if (isJson) { console.log(JSON.stringify({ error: msg })); process.exit(1); }
|
|
1433
|
+
console.error(chalk.red(msg)); process.exit(1);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Free slices skip the gate; paid slices use the standard requirePro pattern.
|
|
1437
|
+
if (!FREE_SLICES.includes(opts.for)) {
|
|
1438
|
+
if (!requirePro(`intel-${opts.for}`)) return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Validate project exists (loadConfig will throw with a helpful message otherwise)
|
|
1442
|
+
loadConfig(project);
|
|
1443
|
+
const db = getDb();
|
|
1444
|
+
|
|
1445
|
+
try {
|
|
1446
|
+
const envelope = getIntel(db, project, { for: opts.for });
|
|
1447
|
+
if (isJson) {
|
|
1448
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1449
|
+
} else {
|
|
1450
|
+
console.log(intelToMarkdown(envelope));
|
|
1451
|
+
}
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
if (isJson) { console.log(JSON.stringify({ error: err.message })); process.exit(1); }
|
|
1454
|
+
console.error(chalk.red('intel failed: ') + err.message);
|
|
1455
|
+
process.exit(1);
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1418
1459
|
// ── STATUS ─────────────────────────────────────────────────────────────────
|
|
1419
1460
|
program
|
|
1420
1461
|
.command('status')
|
package/lib/gate.js
CHANGED
|
@@ -60,6 +60,9 @@ const FEATURE_NAMES = {
|
|
|
60
60
|
'unlimited-pages': 'Unlimited Crawl Pages',
|
|
61
61
|
'unlimited-projects': 'Unlimited Projects',
|
|
62
62
|
'blog-draft': 'AEO Blog Draft Generator',
|
|
63
|
+
'intel-audit': 'Intel Audit Digest (AI-agent-ready)',
|
|
64
|
+
'intel-blog': 'Intel Blog Digest (AI-agent-ready)',
|
|
65
|
+
'intel-competitor': 'Intel Competitor Digest (AI-agent-ready)',
|
|
63
66
|
};
|
|
64
67
|
|
|
65
68
|
// ── CLI Gate — blocks command and shows upgrade message ──────────────────────
|
package/lib/intel.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intel.js — Canonical "give me intelligence about this project" entry point.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth backing every agent-facing surface:
|
|
5
|
+
* - CLI: `seo-intel intel <project>` (this file's first consumer)
|
|
6
|
+
* - MCP: `seo-intel-mcp` server (v1.5.26 — wraps this same function)
|
|
7
|
+
* - HTTP: dashboard / future REST endpoint
|
|
8
|
+
*
|
|
9
|
+
* Slices:
|
|
10
|
+
* raw (free) — page/keyword/heading inventory, no analysis
|
|
11
|
+
* audit (paid) — citability + technical + active insights
|
|
12
|
+
* blog (paid) — gaps + tone hints for drafting
|
|
13
|
+
* competitor (paid) — competitor summary + schema landscape
|
|
14
|
+
*
|
|
15
|
+
* Output is a stable structured object — agents should be able to chain calls
|
|
16
|
+
* without prompt gymnastics. Keep the schema additive across versions.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
20
|
+
import { join, dirname } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
import { getActiveInsights, getCompetitorSummary, getKeywordMatrix } from '../db/db.js';
|
|
23
|
+
import { getCitabilityScores } from '../analyses/aeo/index.js';
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
27
|
+
|
|
28
|
+
export const INTEL_SLICES = ['raw', 'audit', 'blog', 'competitor'];
|
|
29
|
+
export const FREE_SLICES = ['raw'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {import('node:sqlite').DatabaseSync} db
|
|
33
|
+
* @param {string} project
|
|
34
|
+
* @param {{ for?: string }} [opts]
|
|
35
|
+
* @returns {object} structured intel digest
|
|
36
|
+
*/
|
|
37
|
+
export function getIntel(db, project, opts = {}) {
|
|
38
|
+
const slice = opts.for || 'raw';
|
|
39
|
+
if (!INTEL_SLICES.includes(slice)) {
|
|
40
|
+
throw new Error(`Unknown intel slice "${slice}". Available: ${INTEL_SLICES.join(', ')}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const envelope = {
|
|
44
|
+
project,
|
|
45
|
+
for: slice,
|
|
46
|
+
tier: FREE_SLICES.includes(slice) ? 'free' : 'paid',
|
|
47
|
+
generated_at: new Date().toISOString(),
|
|
48
|
+
seo_intel_version: VERSION,
|
|
49
|
+
data: {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (slice === 'raw') envelope.data = collectRaw(db, project);
|
|
53
|
+
if (slice === 'audit') envelope.data = collectAudit(db, project);
|
|
54
|
+
if (slice === 'blog') envelope.data = collectBlog(db, project);
|
|
55
|
+
if (slice === 'competitor') envelope.data = collectCompetitor(db, project);
|
|
56
|
+
|
|
57
|
+
return envelope;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Slice collectors ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function collectRaw(db, project) {
|
|
63
|
+
const domains = db.prepare(
|
|
64
|
+
`SELECT d.domain, d.role, d.last_crawled,
|
|
65
|
+
COUNT(p.id) AS pages,
|
|
66
|
+
SUM(CASE WHEN p.status_code = 200 THEN 1 ELSE 0 END) AS pages_ok
|
|
67
|
+
FROM domains d
|
|
68
|
+
LEFT JOIN pages p ON p.domain_id = d.id
|
|
69
|
+
WHERE d.project = ?
|
|
70
|
+
GROUP BY d.id
|
|
71
|
+
ORDER BY d.role, d.domain`
|
|
72
|
+
).all(project);
|
|
73
|
+
|
|
74
|
+
const totals = db.prepare(
|
|
75
|
+
`SELECT
|
|
76
|
+
(SELECT COUNT(*) FROM pages p JOIN domains d ON d.id=p.domain_id WHERE d.project=?) AS pages,
|
|
77
|
+
(SELECT COUNT(*) FROM keywords k JOIN pages p ON p.id=k.page_id JOIN domains d ON d.id=p.domain_id WHERE d.project=?) AS keywords,
|
|
78
|
+
(SELECT COUNT(*) FROM headings h JOIN pages p ON p.id=h.page_id JOIN domains d ON d.id=p.domain_id WHERE d.project=?) AS headings,
|
|
79
|
+
(SELECT COUNT(*) FROM page_schemas s JOIN pages p ON p.id=s.page_id JOIN domains d ON d.id=p.domain_id WHERE d.project=?) AS schemas,
|
|
80
|
+
(SELECT COUNT(*) FROM sitemap_urls u JOIN domains d ON d.id=u.domain_id WHERE d.project=?) AS sitemap_urls`
|
|
81
|
+
).get(project, project, project, project, project) || {};
|
|
82
|
+
|
|
83
|
+
const lastCrawl = domains.reduce((m, d) => Math.max(m, d.last_crawled || 0), 0);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
domains: domains.map(d => ({
|
|
87
|
+
domain: d.domain,
|
|
88
|
+
role: d.role,
|
|
89
|
+
pages: d.pages,
|
|
90
|
+
pages_ok: d.pages_ok,
|
|
91
|
+
last_crawled: d.last_crawled ? new Date(d.last_crawled).toISOString() : null,
|
|
92
|
+
})),
|
|
93
|
+
totals: {
|
|
94
|
+
pages: totals.pages || 0,
|
|
95
|
+
keywords: totals.keywords || 0,
|
|
96
|
+
headings: totals.headings || 0,
|
|
97
|
+
schemas: totals.schemas || 0,
|
|
98
|
+
sitemap_urls: totals.sitemap_urls || 0,
|
|
99
|
+
},
|
|
100
|
+
last_crawl: lastCrawl ? new Date(lastCrawl).toISOString() : null,
|
|
101
|
+
note: 'Free tier — raw crawl inventory. Pipe into your own AI for analysis, or upgrade to Solo for citability/gap/competitor intel.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function collectAudit(db, project) {
|
|
106
|
+
const insights = getActiveInsights(db, project);
|
|
107
|
+
let citability = null;
|
|
108
|
+
try { citability = getCitabilityScores(db, project); } catch { /* citability_scores table may not exist if AEO never run */ }
|
|
109
|
+
return {
|
|
110
|
+
citability,
|
|
111
|
+
insights: {
|
|
112
|
+
keyword_gaps: insights.keyword_gaps,
|
|
113
|
+
content_gaps: insights.content_gaps,
|
|
114
|
+
technical_gaps: insights.technical_gaps,
|
|
115
|
+
quick_wins: insights.quick_wins,
|
|
116
|
+
site_watch: insights.site_watch,
|
|
117
|
+
},
|
|
118
|
+
last_insight_at: insights.generated_at ? new Date(insights.generated_at).toISOString() : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectBlog(db, project) {
|
|
123
|
+
const insights = getActiveInsights(db, project);
|
|
124
|
+
return {
|
|
125
|
+
keyword_gaps: insights.keyword_gaps,
|
|
126
|
+
long_tails: insights.long_tails,
|
|
127
|
+
content_gaps: insights.content_gaps,
|
|
128
|
+
keyword_inventor: insights.keyword_inventor,
|
|
129
|
+
positioning: insights.positioning,
|
|
130
|
+
drafting_hint: 'Each keyword_gap or long_tail is a candidate draft target. Pair with topic clusters from `seo-intel templates <project>` and citability gaps from `--for=audit` for AEO-aware drafts.',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function collectCompetitor(db, project) {
|
|
135
|
+
const summary = getCompetitorSummary(db, project);
|
|
136
|
+
const matrix = getKeywordMatrix(db, project);
|
|
137
|
+
const insights = getActiveInsights(db, project);
|
|
138
|
+
return {
|
|
139
|
+
summary,
|
|
140
|
+
keyword_matrix: matrix,
|
|
141
|
+
positioning: insights.positioning,
|
|
142
|
+
new_pages: insights.new_pages,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Markdown formatter ──────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export function intelToMarkdown(envelope) {
|
|
149
|
+
const { project, for: slice, tier, generated_at, data } = envelope;
|
|
150
|
+
const lines = [
|
|
151
|
+
`# SEO Intel — ${project}`,
|
|
152
|
+
`> slice: \`${slice}\` · tier: \`${tier}\` · generated: ${generated_at}`,
|
|
153
|
+
'',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
if (slice === 'raw') {
|
|
157
|
+
lines.push('## Crawl inventory', '');
|
|
158
|
+
lines.push(`- **Total pages:** ${data.totals.pages}`);
|
|
159
|
+
lines.push(`- **Keywords:** ${data.totals.keywords}`);
|
|
160
|
+
lines.push(`- **Headings:** ${data.totals.headings}`);
|
|
161
|
+
lines.push(`- **Schemas:** ${data.totals.schemas}`);
|
|
162
|
+
lines.push(`- **Sitemap URLs:** ${data.totals.sitemap_urls}`);
|
|
163
|
+
lines.push(`- **Last crawl:** ${data.last_crawl || 'never'}`, '');
|
|
164
|
+
lines.push('## Domains', '');
|
|
165
|
+
lines.push('| Domain | Role | Pages (200) | Last crawled |');
|
|
166
|
+
lines.push('| --- | --- | --- | --- |');
|
|
167
|
+
for (const d of data.domains) {
|
|
168
|
+
lines.push(`| ${d.domain} | ${d.role} | ${d.pages_ok}/${d.pages} | ${d.last_crawled || '—'} |`);
|
|
169
|
+
}
|
|
170
|
+
lines.push('', `> ${data.note}`);
|
|
171
|
+
} else {
|
|
172
|
+
// Generic JSON-in-fence fallback for paid slices — agents can parse either way.
|
|
173
|
+
lines.push(`## ${slice} (data)`, '', '```json', JSON.stringify(data, null, 2), '```');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
package/mcp/server.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
async function main() {
|
|
120
|
+
const transport = new StdioServerTransport();
|
|
121
|
+
await server.connect(transport);
|
|
122
|
+
// stderr is fine; the host typically surfaces this in its MCP logs panel.
|
|
123
|
+
console.error(`[seo-intel-mcp] v${VERSION} ready on stdio. Tools: list_projects, get_intel.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch(err => {
|
|
127
|
+
console.error('[seo-intel-mcp] fatal:', err);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seo-intel",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.26",
|
|
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",
|