How OpenSEO Defines and Validates MCP Tool Output Schemas

OpenSEO defines MCP tool output schemas using Zod validators normalized through a central objectSchema helper in 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

The foundation of OpenSEO's validation pipeline lives in 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.

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

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:

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

This permissive pattern is critical for tools in 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 combines the normalization helper with a specific payload schema:

// 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.

// 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 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 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 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, 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. 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →