mcp/server.mjs is a stdio Model Context Protocol server that exposes Umbodi's tools to any MCP client (Claude Desktop, Claude Code, …). It speaks line-delimited JSON-RPC over stdin/stdout — one JSON object per line, no HTTP/SSE transport.
import { runServer, defineTools, resolveBackend } from '@ascent/seo-agent/mcp'> Availability. The package is not published to a public registry yet; licensed customers get > it from Umbodi on request. You do not need it to use Umbodi programmatically: the hosted API > at https://umbodi.ai/v1 answers every action below over plain HTTP with your license key — see > SERVICE.md.
Configure a client
Run the server with --config (or --help) to print the client-config snippet for your OS:
node mcp/server.mjs --configIt prints the path to your client's config file (e.g. ~/Library/Application Support/Claude/claude_desktop_config.json on macOS) and the JSON to add:
{
"mcpServers": {
"ascent-seo": {
"command": "node",
"args": ["/path/to/seo-agent/mcp/server.mjs"],
"env": { "ASCENT_LICENSE": "your-license-key" }
}
}
}A license key alone uses Umbodi's hosted service. Add ASCENT_SERVICE_URL only to point the key at a service you host yourself.
Tools
defineTools() registers seven tools, each a thin wrapper over backend.invoke(action, args):
| Tool | Args | What it does | Hosted (umbodi.ai/v1) |
|---|---|---|---|
analyze | { url } | audit one URL for on-page issues; answers { issues } | yes, free |
generate | { site, project?, lang? } | build the site profile — what it is about, its lines of business and their keywords — from a crawl of the site | yes, paid |
rankings | { site, project?, keywords?, lang? } | live Google positions; keywords default to the project's targets | yes, paid |
competitors | { site, project?, keywords?, lang?, deep?, competitor? } | discover who outranks the site; with deep: true and competitor: { domain }, analyse that one competitor | yes, paid |
content | { site, project?, topic?, keyword?, lang? } | draft a page for a topic or keyword, stating only what the project's profile says about the business; a topic about a tracked competitor is refused and not charged | yes, paid |
status | {} | which actions the service answers and what is configured | yes, open |
apply | { project?, only?, preview? } | open a pull request with the improvements you approved. PROPOSES; cannot merge | yes |
site is { domain } or { url }. On the hosted service it must be one of the key's own projects (matched by host) or the call is refused and nothing is charged; project names one by id instead. Paid actions are charged in credits to the key's account, at the same prices as the app. only on apply narrows the pull request to those proposal ids; it can never add one you did not approve. preview: true answers what would change and writes nothing.
There is no run tool: the scheduled loop runs on Umbodi's own schedule for each project.
Backends: remote vs local
resolveBackend(env) picks a backend from environment variables:
- Remote —
ASCENT_LICENSEset: every call isPOST {service}/{action}with
Authorization: Bearer {ASCENT_LICENSE}. The service is https://umbodi.ai/v1 unless ASCENT_SERVICE_URL names a self-hosted one (SERVICE.md).
- Local —
ASCENT_LOCAL=1and no license: calls run the SDK in-process viacreateSeoAgent, with
no license or network call. Only analyze and status work out of the box. analyze builds a sitemap-driven listIndexableEntities from the request site, so an unreachable or empty site yields { issues: [] } rather than a throw. generate needs provider and storage seams that are not wired by default and fails soft with a hint until you supply them. rankings, competitors, content and apply are remote-only.
- Unconfigured — neither set: every call fails soft.
Remote takes precedence when both are set.
What a tool returns when it cannot answer
Tools never throw for a missing or wrong configuration; they return a result the client can render:
- A named refusal from the hosted service —
{ error: <why>, status, projects? }, keeping the
service's own reason: no_license, license_invalid, license_expired, insufficient_credits, project_cap_reached, project_not_on_this_account, request_must_name_a_project (with the account's projects), no_keywords, topic_or_keyword_required, topic_refused (with reason: names_competitor or meta_statement; nothing charged), no_response / parse_failed (with refunded: true when the credits were given back), …
- Not configured — an unconfigured backend, a local call to a remote-only action, or a service
that is unreachable: { error: 'not configured', hint }.
- Not licensed for the action (a self-hosted service's
403 { licensed: false, reason }):
{ error: 'not licensed for this action', reason }.
- A thrown error inside a handler is returned as an MCP
isErrorcontent block, not a protocol
failure.
Not shipped
- HTTP or SSE transport — stdio only.
- The official
@modelcontextprotocol/sdk— this is a minimal JSON-RPC subset (initialize,
tools/list, tools/call).
- A
runtool. - MERGING, from MCP or any license-authenticated caller. A license key can reach only the actions
above; apply proposes and the hosted service has no action that merges. Merging is done by the project owner's own scheduled agent, behind a per-project opt-in a key cannot set.