We already ship an official MCP server that gives Claude, Cursor, and Windsurf direct access to SEO audits. This post does the same thing for the OpenAI ecosystem using function calling — so a GPT-powered assistant, a custom GPT, or any app built on the OpenAI SDK can audit a URL, read its scores, and explain what to fix, all in one conversation.

The result is an SEO analyst you can talk to: "Audit my pricing page and tell me what's hurting my AI search visibility." GPT calls the API, gets structured JSON back, and turns it into a plain-language answer with specific fixes.

Why function calling and not just paste the JSON?

Because the model decides when and with what arguments to call the tool. You don't have to detect that the user wants an audit, parse out the URL, or pre-fetch anything. You describe the tool once, and the model handles intent, extraction, and follow-up calls on its own. Paste-the-JSON works for one-shot questions; function calling is what you want for a real assistant that handles "now check the competitor too" without you writing branching logic.

The tool definition

You describe the audit endpoint to the model as a JSON schema. pip install openai seoscoreapi, then:

import json
from openai import OpenAI
from seoscoreapi import audit

client = OpenAI()
API_KEY = "your_seoscoreapi_key"

tools = [
    {
        "type": "function",
        "function": {
            "name": "seo_audit",
            "description": (
                "Run a full SEO, performance, accessibility, and AI-readability "
                "audit on a URL. Returns an overall score (0-100), a letter grade, "
                "and a prioritized list of issues to fix."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {
                        "type": "string",
                        "description": "The full URL to audit, including https://",
                    }
                },
                "required": ["url"],
            },
        },
    }
]


def run_audit(url: str) -> dict:
    """The actual function the model's tool call maps to."""
    result = audit(url, API_KEY)
    # Trim to what the model needs — don't pay for tokens on raw check arrays
    return {
        "url": result["url"],
        "score": result["score"],
        "grade": result["grade"],
        "category_scores": {k: v["score"] for k, v in result["audit"].items()},
        "aio": result.get("aio"),
        "aeo": result.get("aeo"),
        "priorities": [
            {"issue": p["issue"], "fix": p["fix"]}
            for p in result.get("priorities", [])
        ],
    }

Note the trimming in run_audit. The raw audit response includes every individual check; feeding all of it to the model wastes tokens and dilutes its attention. Hand it the scores and the prioritized fixes — that's what it needs to give a good answer.

The conversation loop

def ask(question: str) -> str:
    messages = [{"role": "user", "content": question}]

    # First pass: the model decides whether to call the tool
    resp = client.chat.completions.create(
        model="gpt-4o", messages=messages, tools=tools
    )
    msg = resp.choices[0].message
    messages.append(msg)

    # If it asked for an audit, run it and feed the result back
    if msg.tool_calls:
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            result = run_audit(args["url"])
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result),
            })
        # Second pass: the model explains the result
        resp = client.chat.completions.create(
            model="gpt-4o", messages=messages
        )

    return resp.choices[0].message.content


print(ask("Audit https://example.com and tell me the 3 most important fixes."))

That's the complete loop. The model calls seo_audit, you run the real audit, you hand the JSON back, and the model writes the answer. The same pattern handles multi-turn follow-ups — "now compare it to stripe.com" triggers a second tool call automatically.

The GEO angle: this is how you optimize for AI search

Here's why this integration is more than a novelty. The audit returns AIO (AI Optimization) and AEO (Answer Engine Optimization) scores — our measures of how well a page is structured for AI crawlers and answer engines like ChatGPT, Perplexity, and Google's AI Overviews. When you expose those to a model via function calling, you can ask:

"Audit my docs page and tell me specifically what's lowering my AEO score and why AI engines might skip it."

The model reads the AEO score and the structural priorities, and explains them in the context of generative-engine visibility. You're using one LLM to make your content more legible to all the others. That's the practical core of Generative Engine Optimization, and the llms.txt guide covers the file-level side of the same goal.

The AIO/AEO/AIO scores require a Starter plan or higher — they're bundled into every /audit response for paid keys, so no separate call is needed.

How do I keep it from burning my quota?

Two guardrails. First, the model only calls seo_audit when the user's intent clearly needs it — the description field does that work, so write it carefully. Second, cache results per URL for the length of a session; if a user asks three questions about the same page, you should audit once, not three times. A simple dict keyed by URL is enough. With those in place, an interactive assistant uses one audit per page discussed, not per message — comfortably inside the Basic plan (1,000 audits/mo) for most apps.

Does this work with custom GPTs and Assistants?

Yes. The same schema drops into a custom GPT's Actions as an OpenAPI definition, or into the Assistants API as a function tool. The audit endpoint is a plain GET /audit?url= with an API key header, so it maps cleanly to an OpenAPI spec — point the Action at https://seoscoreapi.com and add your key as the auth header. The custom-GPT route is the no-code version: your GPT gains a "audit this URL" power without you running any server at all.

Where this fits

If your stack is Claude or Cursor, use the MCP server — it's purpose-built and needs zero glue code. If you're on OpenAI, function calling is the equivalent, and this post is your template. Either way the underlying move is the same: give the model a real SEO tool instead of asking it to guess from training data, and it stops hallucinating audits and starts running them. For the fully autonomous version that loops over a whole site, see Build an AI SEO Agent.