# How to Extend OpenSEO with Custom MCP Tools and Skills

> Extend OpenSEO with custom MCP tools and skills. Learn to define Zod schemas, implement handlers, and register them to enhance AI agent capabilities.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-08-14

---

**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](https://github.com/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts), the `createOpenSeoMcpServer` function creates an `McpServer` instance and registers every available tool via the `registerOpenSeoTool` helper

- **MCP Context** – defined in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts), the `createMcpToolContext` helper injects authenticated user and organization data into each tool call

- **MCP Transport** – implemented in [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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:

```typescript
// 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 your `inputSchema`
- `context` – a `ToolContext` containing `authProps` (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`:

```typescript
// 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:

```typescript
// 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:

1. **Prefix tool names consistently** – use `mySkill_doThing`, `mySkill_getStatus`

2. **Add skill descriptions** – include skill documentation in the `McpServer` metadata `instructions` field within `createOpenSeoMcpServer`

3. **Create wrapper modules** – re-export grouped tools from a single file for cleaner imports:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts) | `createOpenSeoMcpServer` constructs and configures the MCP server |
| Context injection | [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) | `McpProps` and `createMcpToolContext` provide auth state |
| HTTP transport | [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) | Entry points handle routing and validation |
| Tool examples | [`src/server/mcp/tools/whoami.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/whoami.ts) | Reference implementation showing minimal tool structure |
| Tool examples | [`src/server/mcp/tools/get-backlinks-profile.ts`](https://github.com/every-app/open-seo/blob/main/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 `CallToolResult` error 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 typed `args` and `context`, and registering via `register()` in [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts)

- The `ToolContext` provides **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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts) and injected automatically for every tool call.