How to Extend OpenSEO with Custom MCP Tools and Skills
OpenSEO exposes its AI-agent capabilities through the Model Context Protocol (MCP), allowing developers to add custom tools by defining Zod schemas, implementing handlers, and registering them in the server configuration.
The OpenSEO platform, built on the every-app/open-seo repository, provides a clean extensibility layer for adding new SEO functionality that AI agents can invoke. This guide walks through the exact steps to extend OpenSEO with custom MCP tools and skills, using the same patterns as the built-in implementations like whoami and getBacklinksProfile.
Understanding OpenSEO's MCP Architecture
Before adding custom tools, you need to understand how OpenSEO structures its MCP layer. All MCP functionality lives under src/server/mcp/ and consists of three core components:
-
MCP Server – instantiated in
src/server/mcp/server.ts, thecreateOpenSeoMcpServerfunction creates anMcpServerinstance and registers every available tool via theregisterOpenSeoToolhelper -
MCP Context – defined in
src/server/mcp/context.ts, thecreateMcpToolContexthelper injects authenticated user and organization data into each tool call -
MCP Transport – implemented in
src/server/mcp/transport.ts, handles HTTP routing, CORS validation, and authentication resolution for both hosted and self-hosted deployments
The entry points handleAuthenticatedOpenSeoMcpRequest and handleSelfHostedOpenSeoMcpRequest in transport.ts forward validated requests to your registered tools. This means your custom tools automatically inherit authentication, error handling, and CORS protection without additional configuration.
Step-by-Step: Adding a Custom MCP Tool
Follow these five steps to add a new tool to OpenSEO. Each step mirrors the pattern used by existing built-in tools.
Step 1: Create the Tool File
Create a new TypeScript file under src/server/mcp/tools/. The file must export a constant matching the OpenSeoToolDefinition shape:
// src/server/mcp/tools/url-shortener.ts
import { z } from "zod";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { createShortUrl } from "@/services/urlShortener";
export const urlShortenerTool = {
name: "url_shortener",
config: {
title: "Shorten a URL",
description: "Creates a short, share-able link for a given URL.",
inputSchema: z.object({
url: z.string().url(),
}),
outputSchema: z.object({
shortUrl: z.string(),
}),
},
async handler(args: { url: string }, context): Promise<CallToolResult> {
const shortUrl = await createShortUrl(args.url, context.authProps);
return { output: { shortUrl } };
},
};
The name field becomes the tool identifier that MCP clients use when invoking your functionality.
Step 2: Define Input and Output Schemas
Use Zod for schema validation. The inputSchema validates arguments before your handler runs, while outputSchema (optional) describes the return shape. For conversion utilities during registration, use the objectSchema helper when needed.
Step 3: Implement the Handler
The handler receives two parameters:
args– validated input matching yourinputSchemacontext– aToolContextcontainingauthProps(authenticated user/organization),db(database access), and other helpers
Return a CallToolResult with either { output } for success or { error } for failures.
Step 4: Register the Tool
Import and register your tool in createOpenSeoMcpServer:
// src/server/mcp/server.ts
import { urlShortenerTool } from "@/server/mcp/tools/url-shortener";
export function createOpenSeoMcpServer(authProps: McpProps) {
const server = new McpServer({ /* metadata */ }, { /* instructions */ });
const register = <Input extends ToolSchema>(tool: OpenSeoToolDefinition<Input>) =>
registerOpenSeoTool(server, tool, authProps);
// Existing registrations...
register(urlShortenerTool); // ← add your tool here
return server;
}
Place your register() call near existing registrations (lines 17-44 in the source). The server automatically includes your tool in the MCP manifest served at GET /mcp.
Step 5: Add Tests (Optional but Recommended)
Create a corresponding test file following the existing pattern:
// src/server/mcp/tools/url-shortener.test.ts
import { describe, it, expect } from "vitest";
import { urlShortenerTool } from "./url-shortener";
describe("urlShortenerTool", () => {
it("validates URL input", async () => {
const result = await urlShortenerTool.handler(
{ url: "not-a-url" },
mockContext
);
expect(result.error).toBeDefined();
});
});
Creating MCP Skills for the Agents SDK
A skill in OpenSEO terminology is a logical grouping of related tools. Since the Agents SDK treats each tool as an individual capability, you can create skills through naming conventions and documentation:
-
Prefix tool names consistently – use
mySkill_doThing,mySkill_getStatus -
Add skill descriptions – include skill documentation in the
McpServermetadatainstructionsfield withincreateOpenSeoMcpServer -
Create wrapper modules – re-export grouped tools from a single file for cleaner imports:
// src/server/mcp/skills/competitor-analysis.ts
export { analyzeCompetitorTool } from "../tools/analyze-competitor";
export { compareBacklinksTool } from "../tools/compare-backlinks";
export { trackRankingChangesTool } from "../tools/track-ranking-changes";
Key Implementation Details from Source Code
| Component | File Path | Purpose |
|---|---|---|
| Server factory | src/server/mcp/server.ts |
createOpenSeoMcpServer constructs and configures the MCP server |
| Context injection | src/server/mcp/context.ts |
McpProps and createMcpToolContext provide auth state |
| HTTP transport | src/server/mcp/transport.ts |
Entry points handle routing and validation |
| Tool examples | src/server/mcp/tools/whoami.ts |
Reference implementation showing minimal tool structure |
| Tool examples | src/server/mcp/tools/get-backlinks-profile.ts |
Complex example with external API calls |
The registerOpenSeoTool helper (used internally by the register closure) automatically wraps your handler with context injection and error normalization. Your custom code only needs to focus on business logic.
Testing Your Custom MCP Tools
OpenSEO's MCP layer includes built-in test patterns. Verify your tools by checking:
- Schema validation – ensure Zod schemas reject malformed inputs
- Handler behavior – mock the context and verify correct output shapes
- Error handling – confirm your tool returns proper
CallToolResulterror objects
Run tests with your standard test command; the MCP infrastructure doesn't require special test configuration.
Summary
-
OpenSEO extensions use the Model Context Protocol (MCP) with a three-layer architecture: server, context, and transport
-
Add custom tools by creating files in
src/server/mcp/tools/, defining Zod schemas, implementing handlers with typedargsandcontext, and registering viaregister()insrc/server/mcp/server.ts -
The
ToolContextprovides authenticated user data (authProps) and database access automatically -
Skills are logical groupings achieved through naming prefixes and documentation in the server metadata
-
All custom tools inherit authentication, CORS, and error handling from the existing transport layer without additional code
Frequently Asked Questions
What is the Model Context Protocol in OpenSEO?
The Model Context Protocol (MCP) is the standardized interface that OpenSEO uses to expose SEO functionality to AI agents. It defines how tools are discovered, invoked, and how context (authentication, user data) flows between the agent and the platform. According to the open-seo source code, all MCP functionality is concentrated in src/server/mcp/ with clear separation between transport, context, and tool implementation layers.
Do I need to modify authentication code when adding custom tools?
No. The handleAuthenticatedOpenSeoMcpRequest and handleSelfHostedOpenSeoMcpRequest functions in src/server/mcp/transport.ts resolve authentication before your handler executes. Your tool receives verified authentication data through context.authProps without any additional configuration.
Can I use raw JavaScript objects instead of Zod schemas?
While Zod is the recommended approach used throughout OpenSEO's built-in tools, you can use raw shape objects. The registration process in registerOpenSeoTool handles conversion when needed. However, using Zod provides runtime validation and better TypeScript inference for your handler's args parameter.
How do I access the database from a custom MCP tool?
The ToolContext passed to your handler includes a db property. Access it directly within your handler: const result = await context.db.query(...). The context is created by createMcpToolContext in src/server/mcp/context.ts and injected automatically for every tool call.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →