# How OpenSEO Defines and Validates MCP Tool Output Schemas

> Learn how OpenSEO defines MCP tool output schemas with Zod validators and validates them at runtime using safeParseAsync for type safety and robust error handling.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-09-05

---

**OpenSEO defines MCP tool output schemas using Zod validators normalized through a central `objectSchema` helper in [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts), then validates responses at runtime with `safeParseAsync` to ensure type safety and return JSON-RPC errors on failure.**

The `every-app/open-seo` repository implements a robust type-safety layer for its Model Context Protocol (MCP) server. Each MCP tool declares its return shape through **MCP tool output schemas** built with Zod, enabling AI agents to receive consistently structured data while handling both plain objects and complex class instances.

## Core Schema Utilities in [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts)

The foundation of OpenSEO's validation pipeline lives in [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts). This module exports helper functions and base schemas that normalize varying input formats into strict Zod types.

### Normalizing Schema Definitions with `objectSchema`

The `objectSchema` helper bridges the gap between raw Zod shapes and fully instantiated `z.object()` schemas. According to the OpenSEO source code, this function accepts either a `z.ZodType`, a `z.ZodRawShape`, or `undefined`, and returns a unified `ZodType` that the MCP SDK can consume.

```typescript
// src/server/mcp/output-schemas.ts
import { z } from "zod";

export function objectSchema(
  schema: z.ZodType | z.ZodRawShape | undefined,
): z.ZodType | undefined {
  if (!schema) return undefined;
  return schema instanceof z.ZodType ? schema : z.object(schema);
}

```

Tools call this helper when registering their configuration, ensuring that developers can pass either a raw shape or a constructed schema object.

### Standardizing Metadata with `mcpMetaOutputSchema`

Every MCP tool in OpenSEO can include optional operational metadata. The `mcpMetaOutputSchema` defines these common fields using `passthrough()` to allow additional properties while enforcing type safety on known keys:

```typescript
export const mcpMetaOutputSchema = z
  .object({
    url: z.string().optional(),
    projectId: z.string().optional(),
    runId: z.string().optional(),
    creditsCharged: z.number().optional(),
    creditsRemaining: z.number().optional(),
  })
  .passthrough();

```

This schema ensures that **MCP tool output schemas** consistently track resource usage and execution context across the platform.

### Handling SDK Class Instances with `looseObjectOutputSchema`

When integrating with external SDKs like DataForSEO, tools receive typed class instances rather than plain JavaScript objects. The `looseObjectOutputSchema` prevents Zod's `record` validation errors by accepting any object shape:

```typescript
export const looseObjectOutputSchema = z.object({}).passthrough();

```

This permissive pattern is critical for tools in [`src/server/mcp/tools/dataforseo-research-tools.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/dataforseo-research-tools.ts) that forward raw API responses without destructive transformation.

## Configuring Tool-Specific Output Schemas

Individual tools declare their expected return types through an `outputSchema` field in their configuration object. For example, the backlinks profile tool in [`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) combines the normalization helper with a specific payload schema:

```typescript
// src/server/mcp/tools/get-backlinks-profile.ts
import { objectSchema } from "@/server/mcp/output-schemas";
import { backlinksProfileOutputSchema } from "./schemas";

export const getBacklinksProfileTool = {
  name: "get_backlinks_profile",
  config: {
    outputSchema: objectSchema(backlinksProfileOutputSchema),
  },
  async handler(args, ctx) {
    const fetchedPage = await fetchBacklinks(args);
    return {
      structuredContent: { backlinks: fetchedPage },
      url: args.url,
      creditsCharged: 1,
    };
  },
};

```

This architecture allows each tool to define its contract independently while leveraging the centralized validation infrastructure.

## Runtime Validation Pipeline

When the MCP server processes a tool execution request, it invokes the validation pipeline to guarantee response integrity.

### Safe Parsing and JSON-RPC Error Handling

The server retrieves the concrete Zod schema by calling `objectSchema(tool.config.outputSchema)`, then executes `safeParseAsync` on the tool's result. If validation fails, the server returns a **-32602** JSON-RPC validation error to the client, preventing malformed data from reaching AI agents.

```typescript
// Conceptual implementation based on output-schema-validation.test.ts
const schema = objectSchema(getBacklinksProfileTool.config.outputSchema);
const validation = await schema.safeParseAsync({
  structuredContent: { backlinks: somePage },
  projectId: "proj_123",
});

if (!validation.success) {
  // MCP server translates this into a -32602 JSON-RPC error
  throw new Error("Invalid tool output: validation failed");
}

```

This runtime check catches missing required fields, type mismatches, and unexpected null values before the response leaves the server boundary.

## Testing Schema Compliance

The test suite in [`src/server/mcp/tools/output-schema-validation.test.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/output-schema-validation.test.ts) verifies that the validation layer handles edge cases correctly. Tests confirm that:

- Typed class instances pass validation via `looseObjectOutputSchema` without property stripping
- Missing optional fields (such as `position` in Search Console rows) do not trigger failures
- Meta-only responses containing only `url`, `projectId`, or `runId` validate successfully
- Invalid payloads return structured error messages suitable for JSON-RPC responses

These tests ensure that **MCP tool output schemas** remain resilient across SDK updates and schema evolution.

## Summary

- OpenSEO centralizes schema normalization in [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts) using the `objectSchema` helper to accept both raw shapes and Zod objects.
- The `mcpMetaOutputSchema` provides a reusable structure for operational metadata including `creditsCharged`, `projectId`, and `runId`.
- `looseObjectOutputSchema` enables validation of typed class instances from external SDKs like DataForSEO without requiring destructive serialization.
- Individual tools register **MCP tool output schemas** in their `config.outputSchema` property, normalized at initialization.
- Runtime validation uses `safeParseAsync` to enforce schemas, returning **-32602** JSON-RPC errors for invalid payloads.
- Comprehensive tests in [`output-schema-validation.test.ts`](https://github.com/every-app/open-seo/blob/main/output-schema-validation.test.ts) verify handling of optional fields, class instances, and meta-only responses.

## Frequently Asked Questions

### How does OpenSEO handle typed class instances from external SDKs in MCP tool outputs?

OpenSEO uses the `looseObjectOutputSchema` defined in [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts), which implements `z.object({}).passthrough()`. This pattern accepts any object shape without stripping extra properties, preventing Zod validation errors when tools return DataForSEO SDK classes or similar typed instances instead of plain JavaScript objects.

### What error code does OpenSEO return when an MCP tool output fails schema validation?

When validation fails during the `safeParseAsync` check, the MCP server returns a **-32602** JSON-RPC error code. This indicates an invalid parameter or return value according to the JSON-RPC 2.0 specification, allowing AI agents and clients to identify and handle malformed tool responses appropriately.

### Can MCP tools in OpenSEO return metadata without structured content?

Yes. The `mcpMetaOutputSchema` marks all fields as optional using `.optional()`, and the validation tests explicitly verify that meta-only responses containing just `url`, `projectId`, `runId`, or credit information pass validation. Tools can return operational metadata even when no primary structured content is generated.

### Where is the central schema normalization logic located in the OpenSEO repository?

The core normalization logic resides in [`src/server/mcp/output-schemas.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts). This file exports the `objectSchema` helper function that converts raw Zod shapes or existing ZodTypes into unified schemas, along with shared schemas for metadata and loose object validation used across the MCP tool suite.