How MCP Tools Handle Output Schema Validation in Open SEO: A Complete Technical Guide

MCP tools in Open SEO enforce strict output schema validation through a three-layer system: tool-level Zod schema declarations, normalization helpers in output-schemas.ts, and runtime validation with telemetry capture via instrumentMcpToolHandler.

Open SEO's MCP (Machine-Client Protocol) tools guarantee data contract integrity by validating every structured response against declared schemas. This prevents silent JSON-RPC failures and provides full observability into validation mismatches. This guide examines the complete validation pipeline, from declaration to runtime enforcement.


Tool-Level Schema Declaration

Every MCP tool defines its output shape in config.outputSchema. This declarative approach makes contracts explicit and discoverable.

Consider the Backlinks Profile tool in src/server/mcp/tools/get-backlinks-profile.ts:

import {
  backlinksProfileOutputSchema,
  optionalMetaOutputSchema,
} from "@/server/mcp/output-schemas";

export const getBacklinksProfileTool = {
  name: "get_backlinks_profile",
  config: {
    title: "Get backlinks profile",
    description: "Returns a paginated list of backlink rows...",
    inputSchema,
    outputSchema: {
      backlinks: backlinksProfileOutputSchema,
      ...optionalMetaOutputSchema,
    },
  },
  // handler implementation
};

The reusable schemas live in src/server/mcp/output-schemas.ts. This centralization ensures consistency across tools and simplifies maintenance when data models evolve.


Schema Normalization with objectSchema

Raw schema definitions vary—some tools pass complete z.object() instances, others provide plain shapes. The objectSchema helper normalizes these inputs:

// src/server/mcp/output-schemas.ts
export function objectSchema(
  schema: z.ZodType | z.ZodRawShape | undefined,
) {
  if (!schema) return undefined;
  return schema instanceof z.ZodType ? schema : z.object(schema);
}

This flexibility reduces boilerplate while guaranteeing that downstream instrumentation receives a concrete z.ZodType. Tool authors can declare schemas idiomatically without worrying about internal wrapping requirements.


Runtime Validation and Telemetry Capture

The critical enforcement layer resides in instrumentMcpToolHandler (src/server/mcp/instrumentation.ts). This wrapper intercepts every tool execution, validates outputs, and records results.

Validation Flow

After the handler returns a CallToolResult, the wrapper performs three operations:

  1. Parses structured content against the tool's outputSchema using safeParseAsync
  2. Marks validation failures as tool-call errors (JSON-RPC -32602)
  3. Reports mismatches to PostHog with error code MCP_OUTPUT_VALIDATION
// src/server/mcp/instrumentation.ts (excerpt)
if (outputSchema && !result.isError && result.structuredContent) {
  const validation = await outputSchema.safeParseAsync(
    result.structuredContent,
  );
  
  if (!validation.success) {
    outputValidationFailed = true;
    waitUntil(
      captureServerError(
        new Error(`MCP output validation failed for ${toolName}`),
        {
          errorCode: "MCP_OUTPUT_VALIDATION",
          tool: toolName,
          issues: formatValidationIssues(validation.error),
        },
      ),
    );
  }
}

Telemetry Integration

The wrapper extracts meta information—project ID, row counts, quota details—from validated responses to enrich observability data. Success and failure paths both emit structured telemetry:

const succeeded = !result.isError && !outputValidationFailed;
captureMcpToolCall(toolName, context, {
  success: succeeded,
  errorCode: succeeded ? undefined : "MCP_OUTPUT_VALIDATION",
  durationMs: Math.round(performance.now() - startedAt),
});

This dual reporting ensures validation failures surface to callers (as protocol errors) and to operators (through monitoring dashboards).


Complete Handler Wrapping Example

Here's how the instrumentation layer assembles into production-ready tool handlers:

// src/server/mcp/instrumentation.ts
export function instrumentMcpToolHandler<TArgs>(
  toolName: string,
  outputSchema: z.ZodType | undefined,
  handler: ToolHandler<TArgs>,
) {
  return async (args, context) => {
    const startedAt = performance.now();
    
    try {
      const result = await handler(args, context);
      let outputValidationFailed = false;

      // Schema validation block
      if (outputSchema && !result.isError && result.structuredContent) {
        const validation = await outputSchema.safeParseAsync(
          result.structuredContent,
        );
        
        if (!validation.success) {
          outputValidationFailed = true;
          waitUntil(
            captureServerError(
              new Error(`MCP output validation failed for ${toolName}`),
              {
                errorCode: "MCP_OUTPUT_VALIDATION",
                tool: toolName,
                issues: formatValidationIssues(validation.error),
              },
            ),
          );
        }
      }

      // Unified telemetry
      captureMcpToolCall(toolName, context, {
        success: !result.isError && !outputValidationFailed,
        durationMs: Math.round(performance.now() - startedAt),
      });

      return result;
    } catch (error) {
      // Additional error handling
      throw error;
    }
  };
}

Key Source Files

File Purpose
src/server/mcp/output-schemas.ts Central Zod schema definitions and objectSchema normalizer
src/server/mcp/tools/get-backlinks-profile.ts Example tool with complete output schema declaration
src/server/mcp/instrumentation.ts instrumentMcpToolHandler implementation with validation & telemetry
src/server/mcp/tools/output-schema-validation.test.ts Test suite confirming rejection of invalid structured content

Summary

Open SEO's MCP output schema validation operates through three coordinated mechanisms:

  • Declarative schemas in tool config.outputSchema establish explicit contracts
  • objectSchema normalization accommodates flexible declaration styles while ensuring z.ZodType compatibility
  • instrumentMcpToolHandler runtime enforcement validates every response, reports failures via JSON-RPC errors, and captures telemetry for observability

This architecture prevents schema drift, eliminates silent failures, and provides complete visibility into tool behavior.


Frequently Asked Questions

What happens when MCP output validation fails?

The instrumentMcpToolHandler wrapper marks the call as failed, emits a JSON-RPC -32602 error to the client, and reports the mismatch to PostHog with error code MCP_OUTPUT_VALIDATION. The validation issues are formatted and included in the error payload for debugging.

Can MCP tools omit output schema validation?

Tools can omit outputSchema entirely, but this disables validation and telemetry enrichment. The instrumentMcpToolHandler checks if (outputSchema && ...) before parsing, so undefined schemas pass through without enforcement—not recommended for production tools.

Why does Open SEO use safeParseAsync instead of parseAsync?

safeParseAsync allows graceful failure handling without throwing. The wrapper can capture detailed validation issues, record telemetry, and return a controlled error response rather than letting exceptions propagate unhandled through the MCP protocol layer.

How are common output schema patterns shared across tools?

Reusable schema fragments like backlinksProfileOutputSchema and optionalMetaOutputSchema are exported from src/server/mcp/output-schemas.ts. Tools import and spread these into their outputSchema declarations, ensuring consistency and reducing duplication.

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 →