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
looseObjectOutputSchemawithout property stripping - Missing optional fields (such as
positionin Search Console rows) do not trigger failures - Meta-only responses containing only
url,projectId, orrunIdvalidate 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.tsusing theobjectSchemahelper to accept both raw shapes and Zod objects. - The
mcpMetaOutputSchemaprovides a reusable structure for operational metadata includingcreditsCharged,projectId, andrunId. looseObjectOutputSchemaenables validation of typed class instances from external SDKs like DataForSEO without requiring destructive serialization.- Individual tools register MCP tool output schemas in their
config.outputSchemaproperty, normalized at initialization. - Runtime validation uses
safeParseAsyncto enforce schemas, returning -32602 JSON-RPC errors for invalid payloads. - Comprehensive tests in
output-schema-validation.test.tsverify 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →