# How to Integrate Your Own Services with the OpenSEO MCP Server

> Integrate custom services with the OpenSEO MCP server by creating Zod-validated tool modules. Discover how to easily expose your services to AI agents without core server changes.

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

---

**You integrate custom services by creating a Zod-validated tool module in `src/server/mcp/tools/`; the `createOpenSeoMcpServer` factory auto-discovers these files and exposes them to AI agents via the Model Context Protocol (MCP) without requiring changes to core server logic.**

The OpenSEO MCP server is a production-ready implementation built on the Agents SDK that exposes SEO data—keywords, backlinks, SERP results, and rank tracking—to AI agents through a JSON-RPC-style API. According to the every-app/open-seo source code, the server uses a strict plugin architecture where each integration lives as an isolated tool, making it straightforward to add your own services while inheriting built-in authentication, CORS handling, and legacy JSON support.

## OpenSEO MCP Server Architecture Overview

Before adding your service, understand the three-layer architecture defined in the source:

- **Transport Layer** ([`src/server/mcp/transport.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/transport.ts)): Handles CORS headers, legacy JSON request validation, and routes incoming calls to `handleAuthenticatedOpenSeoMcpRequest` or `handleSelfHostedOpenSeoMcpRequest`. This layer ensures your tool inherits automatic authentication checks.

- **Context Provider** ([`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts)): Constructs the `McpProps` object containing the authenticated user identity, organization ID, and base URL. Your tool receives this context automatically, allowing you to enforce row-level security or tenant isolation.

- **Tool Registry** (`src/server/mcp/tools/`): A directory of thin wrappers around service-side functions. Each tool validates input using Zod and returns a `structuredContent` payload. The server serializes this for JSON-RPC callers automatically.

## Step-by-Step Integration Guide

### Step 1: Create a New Tool Module in `src/server/mcp/tools/`

Create a TypeScript file in the tools directory. The file must export a tool object conforming to the `McpTool` interface (defined in [`src/server/mcp/table.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/table.ts)). The server uses a static import map to auto-discover any file placed here, so no manual registration is required.

```typescript
// src/server/mcp/tools/echo.ts
import { z } from "zod";
import { createMcpTool } from "agents/mcp/server";
import { outputSchema } from "@/server/mcp/output-schemas";

const inputSchema = z.object({
  message: z.string(),
});

const resultSchema = outputSchema;

async function handler(args: { message: string }) {
  // Replace with your actual service call
  const echoed = `You said: ${args.message}`;
  return { result: echoed };
}

export const echo = createMcpTool({
  name: "echo",
  inputSchema,
  resultSchema,
  handler,
});

```

### Step 2: Define Input and Output Schemas with Zod

Every tool must declare a strict input schema using Zod to validate JSON-RPC parameters before your handler executes. The output must conform to the centralized `outputSchema` exported from [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts), ensuring consistent serialization across the MCP boundary.

- **Input validation**: Use `z.object()` to define required fields, types, and constraints.
- **Output wrapping**: Return an object with a `result` key containing your serializable payload.

### Step 3: Implement the Handler Function

The handler receives validated arguments and the `McpProps` context. Implement your business logic here—whether querying a database, calling an internal microservice, or fetching from a third-party REST API.

**Key implementation rules**:
- Keep handlers async and stateless.
- Access environment variables for secrets (e.g., `process.env.WEATHER_API_KEY`).
- Throw standard errors; the transport layer in [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts) catches these and returns proper MCP error codes.

### Step 4: Expose the Tool to Clients

Once the file is saved in `src/server/mcp/tools/`, the `createOpenSeoMcpServer` factory automatically includes it in the tool registry. Clients can immediately invoke it via the MCP SDK:

```typescript
// Client-side invocation
const response = await mcp.call("echo", { message: "Hello World" });
console.log(response.result); // "You said: Hello World"

```

Authentication is handled transparently by the existing OAuth flow managed in [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts), so your tool executes under the same user context as built-in tools.

## Real-World Example: Integrating an External REST API

Below is a complete implementation for integrating a weather API, demonstrating environment variable usage and external HTTP calls:

```typescript
// src/server/mcp/tools/weather.ts
import { z } from "zod";
import { createMcpTool } from "agents/mcp/server";
import { outputSchema } from "@/server/mcp/output-schemas";

const inputSchema = z.object({
  city: z.string().min(1),
});

const resultSchema = outputSchema;

async function handler({ city }: { city: string }) {
  const apiKey = process.env.WEATHER_API_KEY;
  
  const resp = await fetch(
    `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}`
  );
  
  if (!resp.ok) {
    throw new Error(`Weather API returned ${resp.status}`);
  }
  
  const data = await resp.json();
  return { result: data };
}

export const weather = createMcpTool({
  name: "weather",
  inputSchema,
  resultSchema,
  handler,
});

```

Client usage remains consistent:

```typescript
const forecast = await mcp.call("weather", { city: "Berlin" });
console.log(forecast.result.main.temp);

```

## Handling Advanced Authentication and Scopes

If your service requires dedicated OAuth scopes beyond the default `MCP_SCOPE`, modify [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts):

1. Add your scope constant to `MCP_OAUTH_SCOPES`.
2. Include the scope in the client authorization request.
3. Access the authenticated context in your handler via the `McpProps` object created in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts).

The transport layer ([`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts)) validates the token against these scopes before invoking your tool, ensuring zero-touch security for custom integrations.

## Summary

- **Create**: Add a TypeScript file to `src/server/mcp/tools/` exporting a `createMcpTool` instance.
- **Validate**: Use Zod for input schemas and `outputSchema` from [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts) for responses.
- **Discover**: The `createOpenSeoMcpServer` factory auto-registers tools via static imports—no core file edits needed.
- **Secure**: Authentication flows through `handleAuthenticatedOpenSeoMcpRequest` in [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts) and context building in [`context.ts`](https://github.com/every-app/open-seo/blob/main/context.ts).
- **Extend**: Add custom OAuth scopes in [`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts) if required.

## Frequently Asked Questions

### Do I need to restart the server to add a new tool?

Yes. Because the tool registry relies on static imports discovered at build time, you must deploy the new file and restart the Node.js process. The architecture does not support hot-reloading of tool modules in production.

### Can I use TypeScript interfaces instead of Zod for validation?

No. The `createMcpTool` factory requires a Zod schema for runtime validation. The MCP server uses this schema to validate JSON-RPC requests before they reach your handler, ensuring type safety across the wire.

### How do I access the authenticated user inside my tool handler?

The handler receives the `McpProps` context object automatically, which includes the user ID, organization, and base URL constructed in [`src/server/mcp/context.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/context.ts). You can destructure this from the second argument if your tool definition includes context injection, or use the global request context managed by the transport layer.

### Is there a performance limit on how many tools I can add?

There is no explicit limit in the OpenSEO source code. However, because all tools are imported statically at startup in [`transport.ts`](https://github.com/every-app/open-seo/blob/main/transport.ts), startup time increases linearly with the number of tool files. Keep tool implementations lightweight and defer heavy initialization to the handler execution phase.