seo-intel 1.5.29 → 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 +17 -0
- package/mcp/server.js +177 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
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
|
+
|
|
3
20
|
## 1.5.29 (2026-05-17)
|
|
4
21
|
|
|
5
22
|
### MCP — `ingest_insight` closes the loop (agents become collaborators, not consumers)
|
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, insertAgentInsight, AGENT_INSIGHT_TYPES } 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;
|
|
@@ -364,11 +382,168 @@ server.registerTool(
|
|
|
364
382
|
}
|
|
365
383
|
);
|
|
366
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
|
+
|
|
367
542
|
async function main() {
|
|
368
543
|
const transport = new StdioServerTransport();
|
|
369
544
|
await server.connect(transport);
|
|
370
545
|
// stderr is fine; the host typically surfaces this in its MCP logs panel.
|
|
371
|
-
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.`);
|
|
372
547
|
}
|
|
373
548
|
|
|
374
549
|
main().catch(err => {
|