How to Add Custom MCP Tools to OpenSEO: Complete Developer Guide
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.
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) anddescription(consumed by AI clients) - Configuration: Zod schemas defining
inputSchema(parameters) andoutputSchema(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 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). Define the tool using Zod for runtime validation and TypeScript for type safety.
// 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
servicesand 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 and append it to the mcpTools array. This array powers the tools/list RPC response.
// 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).
// 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:
// 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:
{
"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:
{
"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 |
| MCP server registration | src/server/mcp/server.ts |
| JSON-RPC transport layer | src/server/mcp/transport.ts |
| Test pattern example | src/server/mcp/tools/whoami.test.ts |
| Marketing documentation | web/src/routes/_marketing/features/mcp.tsx |
Summary
- Create a new tool file in
src/server/mcp/tools/exporting aToolobject with Zod schemas and an async handler. - Register the tool by importing it into
src/server/mcp/server.tsand adding it to themcpToolsarray. - Validate inputs and outputs using Zod schemas to ensure MCP clients receive predictable data structures.
- Test your tool using the existing pattern in
*.test.tsfiles to verify schema compliance and credit-limit handling. - Document the tool in
web/src/routes/_marketing/features/mcp.tsxfor 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. The server iterates over the mcpTools array and returns metadata for each registered tool, allowing AI clients to discover capabilities dynamically.
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 →