seo-intel 1.5.28 → 1.5.30
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 +30 -0
- package/db/db.js +54 -0
- package/mcp/server.js +234 -2
- package/package.json +1 -1
- package/seo-intel.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.30 (2026-05-17)
|
|
4
|
+
|
|
5
|
+
### MCP — paid analysis tools (the full Solo surface for AI agents)
|
|
6
|
+
Solo subscribers can now reach the full analysis layer from any MCP host, not just the dashboard. Four new tools, all paid, all wrap existing `analyses/*` modules — same library-first pattern.
|
|
7
|
+
|
|
8
|
+
- **`run_citability_audit(project, include_competitors?)`** — Run AEO scoring across all crawled pages (6 signals: entity authority, structured claims, answer density, Q&A proximity, freshness, schema coverage). Persists scores to `citability_scores` and upserts `citability_gap` insights into the ledger. Pure function — fast, no LLM calls. Returns target/competitor page counts, average score, top 20 low-score pages.
|
|
9
|
+
- **`get_competitor_positioning(project)`** — Latest positioning analysis (from analyze runs or agent ingests) + per-competitor crawl stats (page counts, keyword counts, last crawl). The strategic narrative + the raw coverage in one envelope.
|
|
10
|
+
- **`prescore_draft(draft_md)`** — Pre-publish AEO scorer for agent-written content. Same scorer the dashboard uses; takes markdown (frontmatter-aware) and returns 0–100 score, tier, signal breakdown, AI intents. Includes revision hints for sub-60 drafts. Pair with `draft_blog_prompt` for a write→score→revise loop.
|
|
11
|
+
- **`draft_blog_prompt(project, topic?, lang?, content_type?)`** — Assemble an AEO-aware prompt seeded with the project's keyword gaps, citability gaps, entities, brand voice, and competitor heading patterns. The agent's own flagship LLM (Opus 4.7 / GPT-4o / Gemini) writes the draft. Supports `en` and `fi`. Topic optional — if omitted, prompt asks the LLM to pick the highest-leverage topic from gap data.
|
|
12
|
+
|
|
13
|
+
**MCP surface now:** 12 tools total — 8 free (read raw data, trigger crawls, persist findings) + 4 paid (`get_intel` audit/blog/competitor slices, `run_citability_audit`, `get_competitor_positioning`, `prescore_draft`, `draft_blog_prompt`). Paid tools share a unified gate message that surfaces the Ahrefs/Semrush price comparison.
|
|
14
|
+
|
|
15
|
+
A Solo agent session now looks like: `run_citability_audit` → `get_competitor_positioning` → `draft_blog_prompt(topic)` → agent's LLM writes the draft → `prescore_draft(output)` → revise if < 60 → `ingest_insight` to persist the gap that motivated the draft. Closed loop, all via MCP, no dashboard required.
|
|
16
|
+
|
|
17
|
+
### Deferred
|
|
18
|
+
- `run_gap_intel` (Ollama-based, long-running) — deferred to v1.5.31 where it'll use the detached-spawn pattern from `run_crawl`.
|
|
19
|
+
|
|
20
|
+
## 1.5.29 (2026-05-17)
|
|
21
|
+
|
|
22
|
+
### MCP — `ingest_insight` closes the loop (agents become collaborators, not consumers)
|
|
23
|
+
The MCP server now accepts write-back. An agent can read your raw data, do its own analysis with its own flagship LLM, and persist findings into the Intelligence Ledger — surviving across sessions, surfacing in the dashboard, deduplicating against future runs.
|
|
24
|
+
|
|
25
|
+
- **`ingest_insight(project, type, data, agent_name?)`** — **free tier**. The agent's LLM did the analysis; we just provide storage. Allowed types mirror what `analyze` writes: `keyword_gap`, `long_tail`, `quick_win`, `new_page`, `content_gap`, `technical_gap`, `positioning`.
|
|
26
|
+
- **Dedup contract**: same `(project, type, fingerprint)` returns the existing row with `deduped: true` and bumps `last_seen` — no duplicate accumulation across sessions.
|
|
27
|
+
- **Provenance**: source is stored as `agent:<name>` (e.g. `agent:claude-opus-4-7`) when `agent_name` is supplied, else just `agent`. Also stamped into the `data` JSON blob as `_source` for downstream consumers that only read `data`.
|
|
28
|
+
- **Schema**: idempotent `ALTER TABLE insights ADD COLUMN source TEXT DEFAULT 'cli'` — existing rows backfill to `'cli'`; analyze-time writes stay as `'cli'`; agent writes flip to `'agent:*'`. Safe on existing DBs.
|
|
29
|
+
|
|
30
|
+
### Logo
|
|
31
|
+
- Updated product logo to the sharp / soft-corners v1 variant. Size dropped 1.46 MB → 953 KB. Dashboard favicon + npm package both pick up the new asset.
|
|
32
|
+
|
|
3
33
|
## 1.5.28 (2026-05-17)
|
|
4
34
|
|
|
5
35
|
### MCP — agents can now trigger crawls and watch progress
|
package/db/db.js
CHANGED
|
@@ -29,6 +29,7 @@ export function getDb(dbPath = './seo-intel.db') {
|
|
|
29
29
|
try { _db.exec('ALTER TABLE pages ADD COLUMN x_robots_tag TEXT'); } catch { /* already exists */ }
|
|
30
30
|
try { _db.exec('ALTER TABLE analyses ADD COLUMN technical_gaps TEXT'); } catch { /* already exists */ }
|
|
31
31
|
try { _db.exec('ALTER TABLE extractions ADD COLUMN intent_scores TEXT'); } catch { /* already exists */ }
|
|
32
|
+
try { _db.exec("ALTER TABLE insights ADD COLUMN source TEXT DEFAULT 'cli'"); } catch { /* already exists */ }
|
|
32
33
|
|
|
33
34
|
// Backfill first_seen_at from crawled_at for existing rows
|
|
34
35
|
_db.exec('UPDATE pages SET first_seen_at = crawled_at WHERE first_seen_at IS NULL');
|
|
@@ -225,6 +226,59 @@ export function upsertInsightsFromKeywords(db, project, keywordsReport) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
229
|
+
// ── Agent-ingested insight (write-back from MCP) ────────────────────────────
|
|
230
|
+
|
|
231
|
+
export const AGENT_INSIGHT_TYPES = ['keyword_gap', 'long_tail', 'quick_win', 'new_page', 'content_gap', 'technical_gap', 'positioning'];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Insert a single insight on behalf of an external agent (e.g. via MCP).
|
|
235
|
+
* Uses the same dedup contract as analyze-time inserts (UNIQUE on
|
|
236
|
+
* project + type + fingerprint), so an agent repeating the same finding
|
|
237
|
+
* across sessions updates `last_seen` instead of duplicating rows.
|
|
238
|
+
*
|
|
239
|
+
* Returns { ok, id, fingerprint, deduped } — `deduped: true` when the row
|
|
240
|
+
* already existed and we only refreshed last_seen.
|
|
241
|
+
*/
|
|
242
|
+
export function insertAgentInsight(db, { project, type, data, agentName }) {
|
|
243
|
+
if (!AGENT_INSIGHT_TYPES.includes(type)) {
|
|
244
|
+
return { ok: false, error: `Unsupported type "${type}". Allowed: ${AGENT_INSIGHT_TYPES.join(', ')}` };
|
|
245
|
+
}
|
|
246
|
+
if (!project) return { ok: false, error: 'project is required' };
|
|
247
|
+
if (!data || typeof data !== 'object') return { ok: false, error: 'data must be an object' };
|
|
248
|
+
|
|
249
|
+
const fingerprint = _insightFingerprint(type, data);
|
|
250
|
+
if (!fingerprint) {
|
|
251
|
+
return { ok: false, error: `data is missing the identifier field this type needs (see _insightFingerprint in db/db.js for the per-type contract)` };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const source = agentName ? `agent:${agentName}` : 'agent';
|
|
255
|
+
const ts = Date.now();
|
|
256
|
+
|
|
257
|
+
// Stash provenance inside the data blob too — survives if/when the source
|
|
258
|
+
// column is ever queried separately, but also keeps it visible to consumers
|
|
259
|
+
// that only read `data`.
|
|
260
|
+
const enriched = { ...data, _source: source, _ingested_at: new Date(ts).toISOString() };
|
|
261
|
+
|
|
262
|
+
const existing = db.prepare(
|
|
263
|
+
'SELECT id FROM insights WHERE project = ? AND type = ? AND fingerprint = ?'
|
|
264
|
+
).get(project, type, fingerprint);
|
|
265
|
+
|
|
266
|
+
db.prepare(`
|
|
267
|
+
INSERT INTO insights (project, type, status, fingerprint, first_seen, last_seen, source_analysis_id, data, source)
|
|
268
|
+
VALUES (?, ?, 'active', ?, ?, ?, NULL, ?, ?)
|
|
269
|
+
ON CONFLICT(project, type, fingerprint) DO UPDATE SET
|
|
270
|
+
last_seen = excluded.last_seen,
|
|
271
|
+
data = excluded.data,
|
|
272
|
+
source = excluded.source
|
|
273
|
+
`).run(project, type, fingerprint, ts, ts, JSON.stringify(enriched), source);
|
|
274
|
+
|
|
275
|
+
const row = db.prepare(
|
|
276
|
+
'SELECT id FROM insights WHERE project = ? AND type = ? AND fingerprint = ?'
|
|
277
|
+
).get(project, type, fingerprint);
|
|
278
|
+
|
|
279
|
+
return { ok: true, id: row.id, fingerprint, deduped: !!existing, source, last_seen: ts };
|
|
280
|
+
}
|
|
281
|
+
|
|
228
282
|
// ── Read active insights (accumulated across all runs) ──────────────────────
|
|
229
283
|
|
|
230
284
|
export function getActiveInsights(db, project) {
|
package/mcp/server.js
CHANGED
|
@@ -25,11 +25,29 @@ import { spawn } from 'child_process';
|
|
|
25
25
|
import { dirname, join } from 'path';
|
|
26
26
|
import { fileURLToPath } from 'url';
|
|
27
27
|
|
|
28
|
-
import { getDb } from '../db/db.js';
|
|
28
|
+
import { getDb, insertAgentInsight, AGENT_INSIGHT_TYPES, getActiveInsights, getCompetitorSummary } from '../db/db.js';
|
|
29
29
|
import { getIntel, INTEL_SLICES, FREE_SLICES } from '../lib/intel.js';
|
|
30
30
|
import { isPro } from '../lib/license.js';
|
|
31
31
|
import { readProgress } from '../lib/progress.js';
|
|
32
32
|
|
|
33
|
+
import { runAeoAnalysis, persistAeoScores, upsertCitabilityInsights } from '../analyses/aeo/index.js';
|
|
34
|
+
import { prescore } from '../analyses/blog-draft/prescorer.js';
|
|
35
|
+
import { gatherBlogDraftContext, buildBlogDraftPrompt } from '../analyses/blog-draft/index.js';
|
|
36
|
+
|
|
37
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
38
|
+
function paidGate(toolName) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: `The "${toolName}" tool requires SEO Intel Solo (€19.99/mo — vs Ahrefs ~$129/mo or Semrush ~$140/mo). Free tier already covers list_projects, get_intel(raw), get_pages, list_keywords, get_headings, run_crawl, get_crawl_status, ingest_insight. Activate at https://ukkometa.fi/en/seo-intel/ — set SEO_INTEL_LICENSE=SI-xxxx-xxxx-xxxx-xxxx in your env.` }],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadProjectConfig(project) {
|
|
46
|
+
const p = join(CONFIG_DIR, `${project}.json`);
|
|
47
|
+
if (!existsSync(p)) return null;
|
|
48
|
+
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
52
|
const ROOT = join(__dirname, '..');
|
|
35
53
|
const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
@@ -307,11 +325,225 @@ server.registerTool(
|
|
|
307
325
|
}
|
|
308
326
|
);
|
|
309
327
|
|
|
328
|
+
// ── Tool: ingest_insight (free — write-back closes the loop) ──────────────
|
|
329
|
+
server.registerTool(
|
|
330
|
+
'ingest_insight',
|
|
331
|
+
{
|
|
332
|
+
description: [
|
|
333
|
+
'Persist an agent-generated insight into the SEO Intel Intelligence Ledger so it shows up in the dashboard and survives across sessions. Free tier — the agent\'s own LLM did the analysis; we just provide storage.',
|
|
334
|
+
'',
|
|
335
|
+
'Dedup contract: same (project, type, fingerprint) updates `last_seen` instead of creating a duplicate row. So an agent rediscovering the same finding across sessions cleanly bumps the timestamp.',
|
|
336
|
+
'',
|
|
337
|
+
'Allowed types (mirror what the cloud `analyze` command writes):',
|
|
338
|
+
' keyword_gap data: { keyword, ... } fingerprint = keyword',
|
|
339
|
+
' long_tail data: { phrase, ... } fingerprint = phrase',
|
|
340
|
+
' quick_win data: { page, issue, ... } fingerprint = page::issue',
|
|
341
|
+
' new_page data: { target_keyword | title, ... }',
|
|
342
|
+
' content_gap data: { topic, ... } fingerprint = topic',
|
|
343
|
+
' technical_gap data: { gap, ... } fingerprint = gap',
|
|
344
|
+
' positioning data: { ...free-form... } one slot per project',
|
|
345
|
+
'',
|
|
346
|
+
'data must include the identifier field above; otherwise the tool returns an error.',
|
|
347
|
+
].join('\n'),
|
|
348
|
+
inputSchema: {
|
|
349
|
+
project: z.string().describe('Project slug'),
|
|
350
|
+
type: z.enum(AGENT_INSIGHT_TYPES).describe('Insight type from the allowed set'),
|
|
351
|
+
data: z.record(z.any()).describe('Insight payload — JSON object. Must include the identifier field for the chosen type.'),
|
|
352
|
+
agent_name: z.string().optional().describe('Optional provenance tag (e.g. "claude-opus-4-7"). Stored as source="agent:<name>".'),
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
async ({ project, type, data, agent_name }) => {
|
|
356
|
+
try {
|
|
357
|
+
const db = getDb();
|
|
358
|
+
const result = insertAgentInsight(db, { project, type, data, agentName: agent_name });
|
|
359
|
+
if (!result.ok) {
|
|
360
|
+
return { content: [{ type: 'text', text: `seo-intel ingest error: ${result.error}` }], isError: true };
|
|
361
|
+
}
|
|
362
|
+
const payload = {
|
|
363
|
+
ok: true,
|
|
364
|
+
project,
|
|
365
|
+
type,
|
|
366
|
+
insight_id: result.id,
|
|
367
|
+
fingerprint: result.fingerprint,
|
|
368
|
+
deduped: result.deduped,
|
|
369
|
+
source: result.source,
|
|
370
|
+
last_seen: new Date(result.last_seen).toISOString(),
|
|
371
|
+
hint: result.deduped
|
|
372
|
+
? 'Insight already existed; last_seen refreshed.'
|
|
373
|
+
: 'New insight persisted. It will appear in the dashboard ledger and in get_intel(for=audit).',
|
|
374
|
+
};
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
377
|
+
structuredContent: payload,
|
|
378
|
+
};
|
|
379
|
+
} catch (err) {
|
|
380
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// ── Tool: run_citability_audit (PAID) ─────────────────────────────────────
|
|
386
|
+
server.registerTool(
|
|
387
|
+
'run_citability_audit',
|
|
388
|
+
{
|
|
389
|
+
description: 'Run AEO citability scoring across all crawled pages (6 signals: entity authority, structured claims, answer density, Q&A proximity, freshness, schema coverage). Persists scores to citability_scores and upserts citability_gap insights into the ledger. Pure function — fast, no LLM calls. Paid tier.',
|
|
390
|
+
inputSchema: {
|
|
391
|
+
project: z.string(),
|
|
392
|
+
include_competitors: z.boolean().optional().describe('Score competitor pages too (default true)'),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
async ({ project, include_competitors = true }) => {
|
|
396
|
+
if (!isPro()) return paidGate('run_citability_audit');
|
|
397
|
+
if (!loadProjectConfig(project)) {
|
|
398
|
+
return { content: [{ type: 'text', text: `Project "${project}" not found. Use list_projects to discover.` }], isError: true };
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const db = getDb();
|
|
402
|
+
const results = runAeoAnalysis(db, project, { includeCompetitors: include_competitors, log: () => {} });
|
|
403
|
+
persistAeoScores(db, results);
|
|
404
|
+
upsertCitabilityInsights(db, project, results.target);
|
|
405
|
+
const competitorPageCount = [...results.competitors.values()].reduce((a, list) => a + list.length, 0);
|
|
406
|
+
const avgTargetScore = results.target.length
|
|
407
|
+
? Math.round(results.target.reduce((s, p) => s + p.score, 0) / results.target.length)
|
|
408
|
+
: 0;
|
|
409
|
+
const lowScorePages = results.target
|
|
410
|
+
.filter(p => p.score < 40)
|
|
411
|
+
.sort((a, b) => a.score - b.score)
|
|
412
|
+
.slice(0, 20)
|
|
413
|
+
.map(p => ({ url: p.url, score: p.score, tier: p.tier }));
|
|
414
|
+
const summary = {
|
|
415
|
+
ok: true,
|
|
416
|
+
project,
|
|
417
|
+
target_pages_scored: results.target.length,
|
|
418
|
+
competitor_pages_scored: competitorPageCount,
|
|
419
|
+
avg_target_score: avgTargetScore,
|
|
420
|
+
low_score_target_pages: lowScorePages,
|
|
421
|
+
hint: 'Scores persisted to DB. Call get_intel(project, for=audit) to see the full citability matrix + insights ledger.',
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
|
425
|
+
structuredContent: summary,
|
|
426
|
+
};
|
|
427
|
+
} catch (err) {
|
|
428
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// ── Tool: get_competitor_positioning (PAID) ───────────────────────────────
|
|
434
|
+
server.registerTool(
|
|
435
|
+
'get_competitor_positioning',
|
|
436
|
+
{
|
|
437
|
+
description: 'Return the latest positioning analysis for a project + per-competitor crawl stats. Combines the positioning insight from the ledger (from `analyze` or agent ingests) with raw competitor coverage (page counts, keyword counts, last crawl). Paid tier.',
|
|
438
|
+
inputSchema: {
|
|
439
|
+
project: z.string(),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
async ({ project }) => {
|
|
443
|
+
if (!isPro()) return paidGate('get_competitor_positioning');
|
|
444
|
+
if (!loadProjectConfig(project)) {
|
|
445
|
+
return { content: [{ type: 'text', text: `Project "${project}" not found. Use list_projects to discover.` }], isError: true };
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const db = getDb();
|
|
449
|
+
const insights = getActiveInsights(db, project);
|
|
450
|
+
const competitorSummary = getCompetitorSummary(db, project);
|
|
451
|
+
const out = {
|
|
452
|
+
project,
|
|
453
|
+
positioning: insights.positioning, // null if never analysed
|
|
454
|
+
competitor_summary: competitorSummary,
|
|
455
|
+
last_insight_at: insights.generated_at ? new Date(insights.generated_at).toISOString() : null,
|
|
456
|
+
hint: insights.positioning ? 'Positioning is from the most recent analyze run or agent ingest.' : 'No positioning insight yet — run `seo-intel analyze <project>` or ingest one via ingest_insight(type=positioning).',
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
460
|
+
structuredContent: out,
|
|
461
|
+
};
|
|
462
|
+
} catch (err) {
|
|
463
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// ── Tool: prescore_draft (PAID) ───────────────────────────────────────────
|
|
469
|
+
server.registerTool(
|
|
470
|
+
'prescore_draft',
|
|
471
|
+
{
|
|
472
|
+
description: 'Run the AEO scorer on a markdown draft before publishing. Returns the same 6-signal breakdown the dashboard uses (entity authority, structured claims, answer density, Q&A proximity, freshness, schema coverage) plus the overall 0-100 score and tier (excellent / good / fair / poor). Use this as a pre-publish gate when drafting via draft_blog_prompt — score < 60 means revise. Paid tier.',
|
|
473
|
+
inputSchema: {
|
|
474
|
+
draft_md: z.string().describe('Full markdown of the draft, including YAML frontmatter if present. The scorer extracts headings, word count, schema_type from frontmatter, etc.'),
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
async ({ draft_md }) => {
|
|
478
|
+
if (!isPro()) return paidGate('prescore_draft');
|
|
479
|
+
try {
|
|
480
|
+
const score = prescore(draft_md);
|
|
481
|
+
const out = {
|
|
482
|
+
ok: true,
|
|
483
|
+
score: score.score,
|
|
484
|
+
tier: score.tier,
|
|
485
|
+
signals: score.signals,
|
|
486
|
+
ai_intents: score.ai_intents,
|
|
487
|
+
hint: score.score >= 60
|
|
488
|
+
? 'Draft scores well. Safe to publish.'
|
|
489
|
+
: 'Below 60 — consider strengthening: add FAQ schema for Q&A proximity, increase entity authority via named experts/citations, shorten paragraphs for answer density, add structured claims (numbers/dates).',
|
|
490
|
+
};
|
|
491
|
+
return {
|
|
492
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
493
|
+
structuredContent: out,
|
|
494
|
+
};
|
|
495
|
+
} catch (err) {
|
|
496
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// ── Tool: draft_blog_prompt (PAID) ────────────────────────────────────────
|
|
502
|
+
server.registerTool(
|
|
503
|
+
'draft_blog_prompt',
|
|
504
|
+
{
|
|
505
|
+
description: 'Generate an AEO-aware blog draft prompt seeded with full project context — keyword gaps, citability gaps, top entities, brand voice notes, competitor heading patterns. The agent\'s own LLM writes the draft using this prompt. Pair with prescore_draft for a write→score→revise loop. Paid tier.',
|
|
506
|
+
inputSchema: {
|
|
507
|
+
project: z.string(),
|
|
508
|
+
topic: z.string().optional().describe('Specific topic to draft about. If omitted, the prompt asks the LLM to pick the highest-leverage topic from the gap data.'),
|
|
509
|
+
lang: z.enum(['en', 'fi']).optional().describe('Output language (default en)'),
|
|
510
|
+
content_type: z.enum(['blog', 'article', 'guide']).optional().describe('Content type framing (default blog)'),
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
async ({ project, topic, lang = 'en', content_type = 'blog' }) => {
|
|
514
|
+
if (!isPro()) return paidGate('draft_blog_prompt');
|
|
515
|
+
const config = loadProjectConfig(project);
|
|
516
|
+
if (!config) {
|
|
517
|
+
return { content: [{ type: 'text', text: `Project "${project}" not found. Use list_projects to discover.` }], isError: true };
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const db = getDb();
|
|
521
|
+
const context = gatherBlogDraftContext(db, project, topic);
|
|
522
|
+
const prompt = buildBlogDraftPrompt(context, { config, lang, topic, contentType: content_type });
|
|
523
|
+
const out = {
|
|
524
|
+
project,
|
|
525
|
+
topic: topic || '(LLM to pick from gap data)',
|
|
526
|
+
lang,
|
|
527
|
+
content_type,
|
|
528
|
+
prompt_length_chars: prompt.length,
|
|
529
|
+
prompt,
|
|
530
|
+
hint: 'Pass `prompt` to your flagship LLM (Opus 4.7 / GPT-4o / etc) to generate the draft. Then run prescore_draft on the output to AEO-score before publishing.',
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
content: [{ type: 'text', text: JSON.stringify(out, null, 2) }],
|
|
534
|
+
structuredContent: out,
|
|
535
|
+
};
|
|
536
|
+
} catch (err) {
|
|
537
|
+
return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
|
|
310
542
|
async function main() {
|
|
311
543
|
const transport = new StdioServerTransport();
|
|
312
544
|
await server.connect(transport);
|
|
313
545
|
// stderr is fine; the host typically surfaces this in its MCP logs panel.
|
|
314
|
-
console.error(`[seo-intel-mcp] v${VERSION} ready on stdio.
|
|
546
|
+
console.error(`[seo-intel-mcp] v${VERSION} ready on stdio. 12 tools — free: list_projects, get_intel(raw), get_pages, list_keywords, get_headings, run_crawl, get_crawl_status, ingest_insight; paid: get_intel(audit/blog/competitor), run_citability_audit, get_competitor_positioning, prescore_draft, draft_blog_prompt.`);
|
|
315
547
|
}
|
|
316
548
|
|
|
317
549
|
main().catch(err => {
|
package/package.json
CHANGED
package/seo-intel.png
CHANGED
|
Binary file
|