# How to Add Custom MCP Tools to OpenSEO: Complete Developer Guide

> Learn how to add custom MCP tools to OpenSEO with this developer guide. Follow simple steps to create, import, and register your new tools for enhanced functionality.

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

---

**To add a custom MCP tool to OpenSEO, create a TypeScript file in `src/server/mcp/tools/` that exports a `Tool` object with Zod schemas for input/output and an async handler, then import and register it in the `mcpTools` array inside [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts).**

OpenSEO ships with a built-in **MCP (Model Context Protocol)** server that exposes SEO-focused capabilities to AI clients like Claude, Cursor, or Codex. If you need to extend this functionality with proprietary business logic or third-party integrations, you can add custom MCP tools by following the same pattern used by built-in utilities. This guide walks you through the exact file structure, registration process, and testing patterns implemented in the `every-app/open-seo` repository.

## Understanding the MCP Tool Architecture

OpenSEO’s MCP implementation follows a strict contract-based design. Each tool is a plain object that conforms to the `Tool` interface from `@opencode-ai/sdk`, consisting of:

- **Metadata**: `name` (snake_case identifier) and `description` (consumed by AI clients)
- **Configuration**: Zod schemas defining `inputSchema` (parameters) and `outputSchema` (return values)
- **Handler**: An async function receiving `{ input, context, projectId }` and returning JSON-serializable data

The server aggregates all tools in [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts) and exposes them via the standard MCP `tools/list` JSON-RPC endpoint.

## Step-by-Step Implementation

### 1. Create the Tool Definition

Create a new file inside `src/server/mcp/tools/` (e.g., [`my-custom-tool.ts`](https://github.com/every-app/open-seo/blob/main/my-custom-tool.ts)). Define the tool using Zod for runtime validation and TypeScript for type safety.

```typescript
// src/server/mcp/tools/my-custom-tool.ts
import { z } from "zod";
import type { Tool } from "@opencode-ai/sdk";

export const myCustomTool: Tool = {
  name: "my_custom_tool",
  description: "Runs a custom SEO lookup that returns a simple score.",
  config: {
    inputSchema: z.object({
      url: z.string().url(),
      keyword: z.string().min(1),
    }),
    outputSchema: z.object({
      score: z.number().min(0).max(100),
      details: z.string(),
    }),
  },

  async handler({ input, context, projectId }) {
    // Access service layer through context.services
    const result = await context.services.myService.runLookup({
      url: input.url,
      keyword: input.keyword,
      projectId,
    });

    return { score: result.score, details: result.message };
  },
};

```

The `handler` receives three arguments:
- **input**: The validated input object matching `inputSchema`
- **context**: Contains `services` and other server-side dependencies
- **projectId**: The active project identifier for scoping data access

### 2. Register the Tool in the MCP Server

Import your tool into [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts) and append it to the `mcpTools` array. This array powers the `tools/list` RPC response.

```typescript
// src/server/mcp/server.ts
import { whoamiTool } from "@/server/mcp/tools/whoami";
import { myCustomTool } from "@/server/mcp/tools/my-custom-tool";

export const mcpTools = [
  whoamiTool,
  myCustomTool,
  // …other existing tools
];

```

The server automatically iterates over `mcpTools` when responding to MCP client discovery requests.

### 3. (Optional) Add Unit Tests

Verify your tool respects input contracts, output schemas, and credit-usage limits by creating a test file alongside your tool (e.g., [`my-custom-tool.test.ts`](https://github.com/every-app/open-seo/blob/main/my-custom-tool.test.ts)).

```typescript
// src/server/mcp/tools/my-custom-tool.test.ts
import { myCustomTool } from "./my-custom-tool";
import { normalizeObjectSchema } from "@/shared/json";

describe("myCustomTool", () => {
  it("validates input schema", async () => {
    await expect(
      myCustomTool.config.inputSchema.parseAsync({ 
        url: "invalid", 
        keyword: "test" 
      })
    ).rejects.toThrow();
  });

  it("returns data that matches output schema", async () => {
    const out = await myCustomTool.handler({
      input: { url: "https://example.com", keyword: "example" },
      context: { 
        services: { 
          myService: { 
            runLookup: async () => ({ score: 42, message: "OK" }) 
          } 
        } 
      },
      projectId: "proj_1",
    });
    
    await expect(
      myCustomTool.config.outputSchema.parseAsync(out)
    ).resolves.toBeDefined();
  });
});

```

### 4. (Optional) Update Documentation

Add your tool to the marketing page so users know it exists. Edit [`web/src/routes/_marketing/features/mcp.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/mcp.tsx):

```tsx
// web/src/routes/_marketing/features/mcp.tsx
<li className="mt-2">
  <p className="font-medium">my_custom_tool</p>
  <p className="text-sm text-neutral-500">
    Runs a custom SEO lookup and returns a numeric score.
  </p>
</li>

```

## Invoking Your Custom Tool via JSON-RPC

Once registered and the server is restarted, any MCP-compatible client can invoke your tool using standard JSON-RPC format:

```json
{
  "jsonrpc": "2.0",
  "method": "my_custom_tool",
  "params": {
    "url": "https://example.com",
    "keyword": "open seo"
  },
  "id": 1
}

```

The server responds with validated output or an error if credit limits are exceeded:

```json
{
  "jsonrpc": "2.0",
  "result": {
    "score": 84,
    "details": "Strong keyword density and backlink profile detected."
  },
  "id": 1
}

```

## Key Files and References

| Purpose | File Path |
|---------|-----------|
| Tool definitions folder | `src/server/mcp/tools/` |
| Reference implementation | [`src/server/mcp/tools/whoami.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/whoami.ts) |
| MCP server registration | [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts) |
| JSON-RPC transport layer | [`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts) |
| Test pattern example | [`src/server/mcp/tools/whoami.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/whoami.test.ts) |
| Marketing documentation | [`web/src/routes/_marketing/features/mcp.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/mcp.tsx) |

## Summary

- **Create** a new tool file in `src/server/mcp/tools/` exporting a `Tool` object with Zod schemas and an async handler.
- **Register** the tool by importing it into [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts) and adding it to the `mcpTools` array.
- **Validate** inputs and outputs using Zod schemas to ensure MCP clients receive predictable data structures.
- **Test** your tool using the existing pattern in `*.test.ts` files to verify schema compliance and credit-limit handling.
- **Document** the tool in [`web/src/routes/_marketing/features/mcp.tsx`](https://github.com/every-app/open-seo/blob/main/web/src/routes/_marketing/features/mcp.tsx) for visibility.

## Frequently Asked Questions

### What is the MCP tool interface in OpenSEO?

The **MCP tool interface** is defined in `@opencode-ai/sdk` and requires a `name`, `description`, `config` object containing Zod `inputSchema` and `outputSchema`, and an async `handler` function. The handler receives `{ input, context, projectId }` and must return JSON-serializable data matching the output schema.

### How do I handle credit limits in custom MCP tools?

Credit-usage validation is typically enforced in the service layer accessed via `context.services` within your handler. The handler should propagate any credit-exceeded errors from the underlying service, which the MCP transport layer will format as a standard JSON-RPC error response.

### Can I call external APIs from a custom MCP tool handler?

Yes. The handler runs server-side and can execute any asynchronous operation, including HTTP requests to third-party APIs. Use the `context` object to access configured HTTP clients or service layers that manage external API authentication and rate limiting.

### Where does OpenSEO expose the list of available MCP tools?

OpenSEO exposes available tools through the **`tools/list` JSON-RPC method**, implemented in [`src/server/mcp/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/server.ts). The server iterates over the `mcpTools` array and returns metadata for each registered tool, allowing AI clients to discover capabilities dynamically.