Announcing CIMD support for MCP Client registration
Learn more

OpenSEO MCP vs OpenSEO API for AI Agents

Nishant Choudhary
Tech Evangelist

TL;DR

  • OpenSEO has no public REST API. Its Model Context Protocol (MCP) server is the programmatic interface, so the "API path" means building on what OpenSEO wraps: DataForSEO, Search Console, and GA4.
  • OpenSEO's server registers 57 tools; Scalekit's prebuilt connector exposes 46 of them.
  • Hosted OpenSEO accepts OAuth or a personal oseo_ API key. Both carry one mcp scope, so no credential can be limited to read-only tools.
  • Credits are the constraint that bites. A domain overview typically costs 100 to 300, and the server tells agents to confirm batches over 2,000.
  • Scalekit vaults each user's OpenSEO grant, scopes tools per agent role, and logs every call, so either path runs on the same credential infrastructure.

Why OpenSEO breaks the usual MCP vs API framing

Your agent needs keyword volumes, live SERPs, and Search Console data for every customer site it touches. OpenSEO, the open-source alternative to Semrush and Ahrefs, exposes all of it through one MCP server. Look for the REST API beside it and you will not find one.

The real decision is between OpenSEO's server and the upstream APIs OpenSEO itself calls. It turns on tool coverage, credit governance, and how many credentials you are prepared to own. Here is how to pick.

What OpenSEO MCP and the OpenSEO API path actually are

One side is a product with a curated tool surface. The other is the set of services that product is assembled from.

OpenSEO MCP

OpenSEO is an MIT-licensed project maintained in the every-app/open-seo repository on GitHub, with a managed hosted version. The hosted MCP server lives on the /mcp path of the OpenSEO app over streamable HTTP, and the same server runs on self-hosted Cloudflare and Docker deployments.

On the current main branch it registers 57 tools: keyword research, live SERPs, domain and backlink analysis, local SEO, rank tracking, site audits, Search Console, GA4, shared project context, and HTML reports. The server reports version 0.0.12, so treat the tool contract as pre-1.0. Setup instructions live in the official "Set up OpenSEO MCP" guide in the OpenSEO docs.

The API path underneath OpenSEO

OpenSEO does not maintain its own keyword or backlink index. It fetches SEO data from DataForSEO. Self-hosters bring their own DataForSEO key, and the hosted service adds 28% to each DataForSEO request, per the project README. First-party data comes from Google Search Console and GA4 through Google OAuth.

Going direct means three integrations: the DataForSEO v3 API over HTTP Basic auth, the Search Console API, and the GA4 Data API. The official references are DataForSEO's API v3 documentation and Google's Search Console API and Analytics Data API guides.

Recommended reading: DataForSEO MCP vs DataForSEO API for AI Agents covers the Live versus Standard queue cost split in detail.

Comparing them where it matters for agents

The four dimensions below are the same ones this series uses for every tool. For OpenSEO, auth and operational surface carry most of the weight. Tool names below are OpenSEO's own; Scalekit prefixes each one with openseomcp_.

What your agent can actually do

Capability
OpenSEO MCP
Direct APIs underneath
Keyword research and metrics
Yes: research_keywords, get_keyword_metrics (up to 700 keywords)
Yes: DataForSEO Labs and Keywords Data
Live Google organic SERPs
Yes: 1 to 10 queries per call
Yes: DataForSEO SERP API, Live or Standard queue
Domain overview, ranked keywords, SERP competitors
Yes
Yes: DataForSEO Labs
Backlink overview and row-level profile
Yes
Yes: DataForSEO Backlinks (separate subscription)
Local SEO: Maps SERPs, profiles, reviews, rank grid
Yes
Partial: data via DataForSEO; grid logic is yours
Search Console performance and URL inspection
Yes, read-only
Yes: Search Console API with your own Google OAuth client
GA4 organic, acquisition, and ecommerce reports
Yes, read-only
Yes: GA4 Data API with your own Google OAuth client
Scheduled rank tracking with stored history
Yes
No: you build the scheduler and storage
Site audit crawl with fix guidance
Yes: background crawl up to 10,000 pages
Partial: DataForSEO OnPage crawls; prioritization is yours
Shared project memory across agents
Yes: get_project_context, update_project_context
No
Control of queued delivery and postbacks
No
Yes: DataForSEO Standard queue
Full, untrimmed response payloads
Partial: several tools trim rows
Yes

OpenSEO's value on the MCP side is not the raw data, which DataForSEO also sells. It is the state OpenSEO keeps: saved keywords, rank history, audit results, and a project memory other agents can read.

Where the capability gap actually sits

The direct path wins on control. Standard-queue delivery, postbacks, and full payloads are there when a nightly job needs thousands of keywords at the cheaper rate. OpenSEO's scheduled rank trackers already use queued tasks internally, with a separately billed live fallback, but the agent cannot choose the delivery method per call.

The MCP path wins on composition. A 3x3 local rank grid is nine Maps searches plus business matching. get_search_opportunities joins Search Console pages in positions 4 to 20 with GA4 outcomes and scores them. Rebuilding either against raw APIs is real work.

The catalog gap you should know about

Scalekit's OpenSEO connector exposes 46 of the 57 tools on OpenSEO's main branch. The 11 not in the catalog today are save_report, list_reports, get_report, delete_report, list_report_templates, save_report_template, delete_report_template, list_site_audits, delete_site_audit, remove_saved_keywords, and search_serp_locations.

Seven of the 11 manage HTML reports and report templates. If your agent's deliverable is an OpenSEO report, plan around that. Every keyword, SERP, domain, backlink, local, audit, rank tracking, Search Console, and GA4 data tool is present. Without the location lookup, pass DataForSEO location codes directly.

The auth path each one puts you on

OpenSEO's auth depends on where it runs, and only the hosted service has a per-user model.

Deployment
How clients authenticate
What one credential represents
Hosted, OAuth
Authorization Code with PKCE and Dynamic Client Registration (DCR, RFC 7591); scopes mcp and offline_access
One user; 24-hour access token, 30-day refresh token
Hosted, API key
oseo_ key sent as Authorization: Bearer or x-api-key
One user acting in their workspace; 5,000 requests per minute
Cloudflare self-host
Cloudflare Access with Managed OAuth, off by default
Every allowed email, sharing one workspace
Docker self-host
None: AUTH_MODE=local_noauth
Anyone who can reach the port
DataForSEO direct
HTTP Basic with API login and password
The whole account, with no scopes
Search Console and GA4 direct
Google OAuth 2.0 with your own client
One Google user and the properties they can read

One scope covers every OpenSEO tool

On the hosted service, the OAuth grant carries a single mcp scope, and API keys receive the same scopes as OAuth grants. There is no read-only credential.

The only boundary OpenSEO enforces is membership. Every call that names a projectId is authorized against the caller's membership in that project's organization, and that organization is billed. That makes least privilege a tool-surface problem: a credential that can read Search Console can also start a live rank check or buy a backlink profile.

What the self-hosted paths change

Self-hosting moves the auth problem rather than removing it. Cloudflare self-hosting puts every allowed user into one shared workspace, so per-user isolation disappears. Docker self-hosting disables auth entirely and is meant for a private network behind your own proxy.

For a multi-tenant B2B agent, the hosted service is the only OpenSEO deployment where each customer's grant is bound to their own organizations. The access control patterns for multi-tenant agents post covers why that boundary matters.

Credits are the rate limit that bites

Hosted API-key traffic is throttled at 5,000 requests per minute per user. An interactive agent rarely approaches that. Credits run out first.

Most research tools charge credits: roughly 5 per keyword for a live SERP at default depth, 30 to 100 per seed in research_keywords, and 100 to 300 for a domain overview. Search Console, GA4, saved keywords, project context, and audit reads are free.

The server's own instructions tell agents to ask before batches over 2,000 credits. run_rank_tracker rejects any run whose fresh estimate exceeds the maxCostCredits the user approved, and scheduled trackers can add separately billed live fallback.

What you own in production

On the MCP path, OpenSEO maintains tool schemas, DataForSEO request construction, caching (domain overviews cache for 12 hours), and, on the hosted service, the Google OAuth clients for Search Console and GA4. You own per-user credential storage and refresh, the tool subset each agent sees, and credit approval.

Expect schema movement. Several parameters are already marked deprecated or legacy, including includeSubdomains in favor of scope, and the US-only market selector in favor of locationCode.

On the direct path you own everything: three auth models, the Live versus Standard choice, pagination, retries, rank history storage, and your own Google Cloud OAuth clients.

When to use OpenSEO MCP

  • Your agent answers a user's SEO questions interactively, such as which pages sit on page two with real traffic, using Search Console and GA4 data the user already connected in OpenSEO.
  • You want scheduled rank tracking, site audits, and saved keyword lists that persist in OpenSEO, where people review them in the UI.
  • Several agents share context: get_project_context and its research log stop a second agent from buying research the first one already paid for.
  • You want Search Console and GA4 data without registering Google Cloud OAuth clients yourself.

When to go direct to the underlying APIs

  • Your pipeline pulls thousands of SERPs or keywords nightly and needs DataForSEO's Standard queue and postbacks to control cost.
  • You need endpoints or fields OpenSEO does not model, such as full payloads or OnPage endpoints beyond its audit.
  • You already run your own storage and scheduling, and OpenSEO's project model would duplicate them.
  • Your customers bring their own DataForSEO accounts and each tenant's usage must bill to that account.

The credential problem that exists on both paths

Pick either path and you still hold one credential per user, per provider. Neither path gives you a vault, refresh orchestration, or a revocation flow.

N users, N grants to keep alive

On the MCP path, each user's OpenSEO grant has an access token that expires daily and a refresh token that lasts 30 days. If a grant is not refreshed inside that window, the next run fails until the user signs in again. The API-key shortcut is worse for B2B: a personal key acts as that user with the full mcp scope.

On the direct path, the count multiplies: a DataForSEO credential per tenant plus Google OAuth grants per user. The token type differs. The storage, refresh, and revocation work does not.

Recommended reading: How to Handle Token Refresh for AI Agents.

Where Scalekit fits

Scalekit's OpenSEO MCP connector runs the OAuth flow, stores each user's grant in its token vault, and refreshes it, so credentials never touch the agent runtime. The same connected-account model covers the direct path through the DataForSEO MCP, Google Search Console, and Google Analytics connectors in AgentKit. The MCP vs API decision does not change your auth infrastructure.

Connecting OpenSEO to your agent with Scalekit

The examples below use Python and LangChain. The sequence is fixed: authorize the user, retrieve the tools their connected account may call, then run the agent.

Prerequisites

Create an OpenSEO MCP connection in the Scalekit dashboard under AgentKit > Connections. Copy your environment URL, client ID, and client secret from Developers > API Credentials into .env.

The connection name in code must match the dashboard exactly. These examples use openseomcp, as in Scalekit's quickstart; replace it with the name your dashboard shows. A mismatch is the most common integration error.

pip install scalekit-sdk-python python-dotenv langchain-anthropic "langchain-mcp-adapters>=0.3,<1"
# .env SCALEKIT_ENVIRONMENT_URL=<your-environment-url> SCALEKIT_CLIENT_ID=<your-client-id> SCALEKIT_CLIENT_SECRET=<your-client-secret> ANTHROPIC_API_KEY=<your-anthropic-api-key>

Authorize each user once

get_or_create_connected_account resolves the user's OpenSEO connected account by your app's identifier. If the account is not ACTIVE, send the user through OpenSEO's login once. Scalekit keeps and refreshes the grant after that.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions # Must match the connection name in AgentKit > Connections exactly OPENSEO_CONNECTION = "openseomcp" USER_ID = "user_123" # your app's stable identifier for this user def ensure_openseo_connected(identifier: str) -> None: account = actions.get_or_create_connected_account( connection_name=OPENSEO_CONNECTION, identifier=identifier ).connected_account if account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=OPENSEO_CONNECTION, identifier=identifier ) print("Authorize OpenSEO:", link.link) input("Press Enter after approving the OpenSEO login...") account = actions.get_or_create_connected_account( connection_name=OPENSEO_CONNECTION, identifier=identifier ).connected_account if account.status != "ACTIVE": raise RuntimeError(f"OpenSEO is {account.status}, not ACTIVE") ensure_openseo_connected(USER_ID)

Retrieve the tools this user's agent may call

The agent does not load the 46-tool catalog. actions.langchain.get_tools calls list_scoped_tools for this user's connected account and returns native LangChain StructuredTool objects.

Passing tool_names narrows that surface further, here to six tools that spend no OpenSEO credits. Because OpenSEO has one scope, this is where read-only access actually gets expressed.

# Six OpenSEO tools that spend no credits READ_ONLY_TOOLS = [ "openseomcp_list_projects", # project IDs every other tool needs "openseomcp_get_project_context", # goals, competitors, research log "openseomcp_list_saved_keywords", # keywords the team already saved "openseomcp_get_search_console_performance", # clicks, CTR, position "openseomcp_get_google_analytics_organic_landing_pages", # GA4 outcomes "openseomcp_get_search_opportunities", # positions 4 to 20 joined with GA4 ] tools = actions.langchain.get_tools( identifier=USER_ID, connection_names=[OPENSEO_CONNECTION], tool_names=READ_ONLY_TOOLS, page_size=100, ) tool_map = {tool.name: tool for tool in tools} print(sorted(tool_map))

Run the agent loop

The loop is plain LangChain. Scalekit resolves the user's OpenSEO token inside each tool call, so the model never sees a credential.

from langchain_anthropic import ChatAnthropic from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage llm = ChatAnthropic( model=os.getenv("ANTHROPIC_MODEL", "claude-sonnet-5"), max_tokens=4096, ).bind_tools(tools) messages = [ SystemMessage( "You are an SEO analyst. Call openseomcp_list_projects first, then pass " "the returned project id as projectId to every other OpenSEO tool." ), HumanMessage( "In my main project, find pages ranking in positions 4 to 20 that already " "get organic sessions, and rank the top five by upside." ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.text) break for call in response.tool_calls: result = tool_map[call["name"]].invoke(call["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=call["id"]))

Keep credit decisions in code, not in the model

For deterministic jobs, skip the model and call execute_tool directly. OpenSEO's live rank check expects the approved ceiling as maxCostCredits, so the approval step belongs in your code, where a model cannot talk itself past it. Live checks require a paid plan on the hosted service.

PROJECT_ID = os.environ["OPENSEO_PROJECT_ID"] TRACKER_ID = os.environ["OPENSEO_TRACKER_ID"] estimate = actions.execute_tool( tool_name="openseomcp_estimate_rank_tracker_cost", tool_input={"projectId": PROJECT_ID, "trackerId": TRACKER_ID}, connection_name=OPENSEO_CONNECTION, identifier=USER_ID, ) print("Estimate:", estimate.data) approved = int(input("Approve a credit ceiling for this live check: ")) run = actions.execute_tool( tool_name="openseomcp_run_rank_tracker", tool_input={ "projectId": PROJECT_ID, "trackerId": TRACKER_ID, "maxCostCredits": approved, }, connection_name=OPENSEO_CONNECTION, identifier=USER_ID, ) print("Run:", run.data, "execution_id:", run.execution_id)

Keep the returned execution_id with your job record so each credit spend maps to one call and one user.

Multi-tool and multi-tenant agents with a Virtual MCP server

Most SEO agents do not stop at research. They post a digest to Slack or file a ticket. A Virtual MCP server gives that agent one endpoint covering both connectors, scoped to named tools and to one user per session.

Why OpenSEO needs a scoped endpoint

Hand an agent the full OpenSEO server and it sees every tool, including the ones that spend credits. At roughly 200 tokens per tool, 46 tools consume about 9,200 tokens before the agent does any work.

A Virtual MCP server declares exactly which tools each agent role sees. One server definition serves every user; before each run you mint a short-lived session token bound to that user's connected accounts. The endpoint is static; the identity is per-user. There is no MCP server to deploy, host, or maintain.

Create the server once per agent role

This digest agent gets four read-only OpenSEO tools and one Slack write. The Slack connection name must also match your dashboard. The full walkthrough is in the Virtual MCP setup guide.

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping SLACK_CONNECTION = "slack" # must match AgentKit > Connections exactly config = actions.mcp.create_config( name="seo-opportunity-digest", description="Read-only OpenSEO analysis plus one Slack write", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name=OPENSEO_CONNECTION, tools=[ "openseomcp_list_projects", "openseomcp_get_project_context", "openseomcp_get_search_console_performance", "openseomcp_get_search_opportunities", ], ), McpConfigConnectionToolMapping( connection_name=SLACK_CONNECTION, tools=["slack_send_message"], ), ], ).config # Store both values; every user and every run reuses them print(f"SCALEKIT_MCP_CONFIG_ID={config.id}") print(f"SCALEKIT_MCP_SERVER_URL={config.mcp_server_url}")

Mint a session token for each user

Before each run, confirm every connection in the config is active for this user, then mint a token. Authorization links from list_mcp_connected_accounts are valid for one minute, so surface them immediately. Session tokens default to about one hour, and minting again is the refresh.

from datetime import timedelta CONFIG_ID = os.environ["SCALEKIT_MCP_CONFIG_ID"] MCP_URL = os.environ["SCALEKIT_MCP_SERVER_URL"] def mint_session_token(identifier: str) -> str: state = actions.mcp.list_mcp_connected_accounts( config_id=CONFIG_ID, identifier=identifier, include_auth_link=True, ) inactive = { account.connection_name: account.authentication_link for account in state.connected_accounts if (account.connected_account_status or "").upper() != "ACTIVE" } if inactive: # Links expire after one minute; send them to the user right away raise RuntimeError(f"Authorization needed: {inactive}") return actions.mcp.create_session_token( mcp_config_id=CONFIG_ID, identifier=identifier, expiry=timedelta(minutes=30), ).token

Connect LangChain to the scoped endpoint

The same config serves every tenant. Only the identifier and the session token change between runs.

import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient # Reuses actions, ChatAnthropic, HumanMessage, and ToolMessage from the blocks above async def run_digest(identifier: str) -> None: token = mint_session_token(identifier) client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": MCP_URL, "headers": {"Authorization": f"Bearer {token}"}, } } ) tools = await client.get_tools() tool_map = {tool.name: tool for tool in tools} llm = ChatAnthropic( model=os.getenv("ANTHROPIC_MODEL", "claude-sonnet-5"), max_tokens=4096, ).bind_tools(tools) messages = [ HumanMessage( "List my OpenSEO projects, pick the main one, find this week's top three " "striking-distance pages, and post a short summary to #seo in Slack." ) ] while True: response = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.text) break for call in response.tool_calls: result = await tool_map[call["name"]].ainvoke(call["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=call["id"])) for tenant_user in ["user_123", "user_456"]: asyncio.run(run_digest(tenant_user))

Observability for downstream tool calls

When a customer asks why their credits dropped, you need the answer by user, not by agent. Scalekit gives you that at two levels.

What you can see per connected account

In the Scalekit dashboard, AgentKit > Connected Accounts shows each user's connection status, token refresh history, and tool execution logs. Every OpenSEO call runs under the connected account that authorized it, so a disputed run_rank_tracker traces to one user and one grant, not a shared key.

Recommended reading: Audit Trails for Agent Auth in B2B SaaS.

Hear about expired grants before the agent does

Subscribe to the connected_account.status_updated webhook. It fires on every status transition and carries both the old and new status, so filter on ACTIVE moving to EXPIRED and send the user a fresh authorization link. Add connected_account.token_refresh_failed for refresh failures.

With OpenSEO's 30-day refresh window, this is the difference between a re-authorization email and a weekly report that silently never arrives. The connected accounts guide has the payload details.

Which one to build against

If your agent works alongside a user inside their own SEO data, build on OpenSEO MCP. It keeps rank history, audits, and project memory that raw APIs do not, and hosted OAuth gives each user a real identity.

If your agent is a high-volume pipeline that needs queued delivery, full payloads, or per-tenant DataForSEO billing, go direct to DataForSEO and Google's APIs. Many teams will run both.

Either way, you hold one credential per user per provider, and OpenSEO's single scope makes tool scoping your only least-privilege control. That is the layer that needs production-grade infrastructure. Understanding secure token management for AI agents at scale is a prerequisite before either path goes to production.

Build your OpenSEO agent with Scalekit

Building an OpenSEO agent and want help with the auth design? Talk to us for immediate help.

Start with the OpenSEO MCP connector docs.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.