A project-agnostic SEO agent: pure ESM cores that produce always-correct technical SEO (sitemap / robots / JSON-LD / hreflang / canonical / meta-OG) automatically, plus autonomous research (live rankings, competitor analysis) and an AI step that proposes content SEO (keyword clusters, per-page meta, landing-page + content-gap ideas) into an admin inbox for one-click approve/edit/reject. A learning loop tunes the inbox from your decisions and actual rank movement. It understands a project from its own docs and can be re-run anytime ("Update SEO"). Zero dependencies, no build step.
> Commercial software. Licensed per deployment — see LICENSE-COMMERCIAL, > EULA.md, and RELEASING.md (in the licensed distribution).
Scope — what this engine covers, and what it does not
This engine is the SEARCH half. Its whole surface is six actions — analyze, generate, rankings, competitors, content, status — and every way of reaching it (SDK, HTTP service, MCP server, the Claude skill, the widgets) is a different door onto those same six.
Two capabilities of the hosted Umbodi product are deliberately not here, and licensing this engine does not obtain them:
| Not in this engine | Where it lives |
|---|---|
| AI-answer visibility — answer-engine citation checking across Google AI Overview, Perplexity, OpenAI, Anthropic and Gemini | the hosted product only |
| The write channel — opening a pull request against your repository, merging it on green, verifying the deploy live | the hosted product only |
This engine researches and drafts. It does not write to your repository and it does not measure answer engines. Stating that here costs you less than discovering it after a licence — and it is the same information the developer portal carries, so the two cannot disagree.
> Naming. The product is Umbodi; the package is still @ascent/seo-agent (its MCP server > is the seo-agent-mcp bin, mcp/server.mjs), and the environment variables are still > ASCENT_SERVICE_URL, ASCENT_LICENSE and ASCENT_LOCAL. Those are real identifiers, pinned until a > major version. Copy them exactly as written. > > Availability. The package is not on a public registry; licensed customers receive it from > Umbodi. Without it, the hosted API at https://umbodi.ai/v1 answers every action with a license > key over plain HTTP — see SERVICE.md.
Buyer docs (docs/)
| Doc | Contents |
|---|---|
| INSTALL | Scaffold → adapter → migrate → deploy → seed. |
| ARCHITECTURE | Pure cores + thin shells, the two seams, data flow, signatures. |
| PROVIDERS | SERP (google/serper), web search, Search Console, LLM — configure/swap. |
| SECURITY (in the licensed distribution) | Revoke-from-anon, SSRF guard, human gate, budget cap, secrets. |
| OPERATIONS (in the licensed distribution) | Worker actions, cron cadence, Agent Health, budget, runbook. |
| REFERENCE-DEPLOYMENT (in the licensed distribution) | Bea's Stories as a worked example. |
> Deep-dive engineering reference: GUIDE.md. Quickstart: node cli/init.mjs <targetDir>.
Honesty note (read this first)
No tool can guarantee a #1 Google ranking — rankings are Google's call (relevance + authority + technical health + freshness). This engine makes a site maximally rankable and keeps it that way with minimal human effort. It does not promise literal first place, and it deliberately does not mass-auto-publish AI content (the #1 cause of Google spam penalties).
v2 — Autonomous Research Agent
v2 turns the prompt-once generator into an autonomous SEO research agent: it perceives multiple sources (docs, own site, live web, competitors, search rankings), remembers what it learns in a versioned knowledge base, reasons with tool-using researchers, and acts by keeping technical SEO perfect and proposing content + ranking improvements — on a governed, tiered schedule. It adds a rank monitor (track where you rank + who's #1), a competitor registry (auto-discovered + manual, each analyzed), and a self-testing / self-healing loop (Phase 5). Everything is configurable in admin: the SERP provider is Google-default and swappable, runs are budget-capped with a hard monthly stop, and the agent self-tests its own invariants and halts autonomous actions on any regression. No self-modifying code, no mass-auto-publishing — a human still approves anything Google reads as content. Same module, same two seams (adapter + config). See GUIDE.md; DB is db/schema.v2.sql (+ db/rollback.v2.sql).
Autonomy model — AUTO technical vs PROPOSED content
| AUTO (no human, always-on) | PROPOSED (admin approves) |
|---|---|
sitemap.xml, robots.txt | keyword targets / clusters |
| JSON-LD structured data | per-page meta / OG copy |
| canonical URLs | new landing-page ideas |
hreflang alternates (all languages) | content-gap suggestions |
| deterministic health audit | — |
Technical artifacts are computed from your live content and served directly — they stay correct as content changes, no approval needed. Everything the AI generates lands in seo_proposals as pending and mutates nothing until a human approves it.
The two consumer seams
The module is 100% generic. A consuming project provides exactly two things:
1. seo.adapter.mjs — implements one function:
listIndexableEntities(): Promise<SeoEntity[]>It enumerates the project's publicly-indexable pages as a uniform SeoEntity[] ({ type, url_path, title, body_sample, lang, updated_at, schema_type, alternates?, image_url? }). Every core (engine, sitemap, JSON-LD, health) consumes this shape — so no core knows what a "story" or "author" is. body_sample is an excerpt only — never paywalled/full body text. 2. docs_source — { repo, ref, paths[] }: which repo docs describe the project. The engine reads them (via GitHub raw for the "Update SEO" button, or straight from disk for the CLI seed) and derives the project profile from them.
New project = write one adapter + set docs_source + run the CLI seed. Everything else drops in.
Quick start (5 steps)
1. Vendor the cores. seo/install.sh <dest> copies core/*.mjs + core/types.d.ts into your project (e.g. src/lib/seo/). For no-build sites use the secure-vendoring flow for drift checks. 2. Write your adapter. Copy adapter/seo.adapter.example.mjs → seo.adapter.mjs and implement listIndexableEntities() for your published content. Return excerpts only, never full body text. 3. Apply the schema. Run db/schema.sql (all seo_* tables + RLS + RPCs). It requires one host predicate public.seo_is_admin() — alias your existing admin check (see the file's footer). 4. Wrap the backend. Add a thin host shell around backend/seo-generate.mjs per backend/_interface.md (inject auth, fetchDocs, listIndexableEntities, callClaude, db). 5. Run the CLI seed. SEO_ENDPOINT=… INTERNAL_SECRET=… SEO_DOCS=context.md,CLAUDE.md,docs/… node cli/seed-seo-profile.mjs — derives the profile + first proposals from your docs.
Then wire the serving side (sitemap/robots routes, <JsonLd> + meta merge on your routes) and the admin "SEO" area (Profile, Keywords, Proposals, Health). See GUIDE.md.
Tests
npm test # → node --test — the pure cores + engine orchestration, zero depsThe cores are pure (no I/O, no DOM, injected now), so every decision is unit-tested with plain objects: sitemap XML shape, robots, all JSON-LD builders, hreflang (incl. x-default), meta merge precedence, keyword normalize/dedupe, proposal parse (fail-closed), and the health rules.