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

> Learn how MCP tools in Open SEO validate output schema using a three-layer system including Zod declarations, normalization helpers, and runtime validation with telemetry for robust data integrity. Discover the technical detail...

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-08-18

---

**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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/get-backlinks-profile.ts):

```typescript
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`](https://github.com/every-app/open-seo/blob/main/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:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`

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

```typescript
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:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts) | Central Zod schema definitions and `objectSchema` normalizer |
| [`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) | Example tool with complete output schema declaration |
| [`src/server/mcp/instrumentation.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/instrumentation.ts) | `instrumentMcpToolHandler` implementation with validation & telemetry |
| [`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) | 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`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/output-schemas.ts). Tools import and spread these into their `outputSchema` declarations, ensuring consistency and reducing duplication.