Free lesson · GenAI Solutions Architecture
Validate MCP tool composition correctness and safety
You will build a ToolCompositionValidator that ensures multi-tool workflows are type-safe and do not combine tools in dangerous ways. Critically, this validator integrates with MCP's **OAuth 2.1** authorization framework: MCP servers are classified as OAuth Resource Servers following RFC 8707 (Resource Indicators), meaning each tool invocation in a composition chain must carry a properly-scoped access token. PKCE (Proof Key for Code Exchange) is mandatory for all authorization code flows, and the client_credentials grant is used for machine-to-machine authentication between composition orchestrators and downstream MCP servers. The validator also supports **OpenID Connect Discovery 1.0** -- automatically retrieving each server's .well-known/openid-configuration to validate issuer trust chains across multi-server compositions. Define a ToolComposition Pydantic model with fields composition_id: str, name: str, description: str, steps: list[CompositionStep], input_schema: dict, output_schema: dict, safety_classification: SafetyLevel, created_by: str, and version: int. Each CompositionStep contains step_id: str, tool_name: str, server_id: str, input_mapping: dict[str, str] (maps step input fields to previous step outputs or composition-level inputs using JSONPath expressions), output_alias: str, timeout_ms: int, and retry_on_failure: bool. Implement validate_type_compatibility(composition: ToolComposition) -> ValidationResult that walks the composition DAG starting from the root inputs, verifying each step's input_mapping references valid output fields from prior steps with compatible JSON Schema types, checking that every required input field is mapped and no orphaned outputs exist that could indicate missing steps. Use jsonschema.validate() to check that mapped fields satisfy the target tool's input schema retrieved from the MCPServerRegistry. Return ValidationResult Pydantic model with valid: bool, type_errors: list[TypeCompatibilityError], unmapped_inputs: list[str], orphaned_outputs: list[str], cycle_detected: bool, and validation_duration_ms: float. Build a SafetyAnalyzer that detects dangerous tool combinations using pattern-based rules: flag compositions where code.execute follows web.fetch (potential remote code execution vector), where db.write follows unvalidated LLM output without an intervening guardrail.check step (SQL injection risk), where file.delete appears without preceding file.backup (data loss risk), or where email.send follows llm.generate without content moderation. Store safety rules in the PostgreSQL composition_safety_rules table with columns rule_id, pattern (JSON path expression matching step sequences), severity, description, mitigation, enabled, created_at, last_triggered_at. Implement analyze_safety(composition: ToolComposition) -> list[SafetyFinding] that evaluates all enabled safety rules against the composition graph, matching step sequences against rule patterns using subgraph matching. Each SafetyFinding contains rule_id, severity, matched_steps: list[str], description, and recommended_mitigation. Build a FastAPI endpoint POST /api/v1/mcp/compositions/validate that runs both type validation and safety analysis, returning a CompositionReport with type_errors: list[TypeCompatibilityError], safety_findings: list[SafetyFinding], overall_status: str, and recommendations: list[str]. Emit Prometheus metrics mcp_composition_validations_total{status}, mcp_composition_safety_findings_total{severity}, mcp_composition_type_errors_total, mcp_composition_validation_duration_seconds, and mcp_composition_steps_count{composition_id}. Create a ToolCompositionTestHarness with method test_composition(composition: ToolComposition, iterations: int = 10) -> TestResult that generates synthetic inputs matching the composition's input schema via Instructor structured output, executes the composition end-to-end against test MCP servers, validates outputs match expected schema, and records test results in the composition_test_results table with pass/fail counts and error details.
Course: GenAI Architecture & Design Patterns · Chapter 11 · MCP Tool Mesh
Free to read — no subscription required.
Introduction
When you chain MCP tools into multi-step workflows, a single type mismatch between one tool's output and the next tool's input can silently corrupt data—an array of objects flowing into a slot that expects an array of strings, or a nullable field landing in a required integer slot. Teams that defer composition validation to runtime discover these structural defects only after production incidents, when the blast radius is largest. By the end of this lesson you'll be able to validate MCP tool compositions for structural type compatibility across field mappings, before any token is issued for execution.
Key Terminology
- Composition DAG: The directed acyclic graph representation of a multi-tool workflow where nodes are MCP tool invocations and edges are typed data-flow connections between them.
- CompositionEdge: A dataclass that models one connection in the composition DAG, naming the source tool, the target tool, and a
field_mappingsdictionary that declares which source output field flows into which target input field. - Field Mapping: A declared correspondence between an output field on one tool and an input field on the next, against which structural type compatibility is checked.
- SchemaCompatibilityChecker: The component that performs structural subtyping on two JSON Schemas, recursively comparing types, required properties, and item schemas to decide whether a source can satisfy a target.
- ToolCompositionValidator: The driver class that walks every
CompositionEdge, fetches input and output schemas from the tool registry, and delegates each field-mapping check to theSchemaCompatibilityChecker. - ValidationIssue: A dataclass that captures one structural problem discovered during validation, including severity (
ERRORorWARNING), the source and target tool names, a human-readable message, and thefield_pathwhere the mismatch occurs. - Numeric Widening: A JSON Schema compatibility rule that permits an
integersource to satisfy anumbertarget without producing a type-mismatch error. - Composition Token: An OAuth 2.1 access token whose scope covers every tool in a validated pipeline, issued only after structural type validation passes on every edge.
Concepts
Why Individual Tool Validation Is Insufficient
Each MCP server publishes its tools with JSON Schema definitions for inputs and outputs. The tool registry stores these schemas, and capability discovery exposes them to clients. However, these schemas describe tools in isolation. Consider a three-tool pipeline:
db_query— outputs{"type": "object", "properties": {"rows": {"type": "array", "items": {"type": "object"}}}}transform_csv— expects{"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}}}email_send— expects{"type": "object", "properties": {"body": {"type": "string"}, "to": {"type": "string"}}}
The db_query tool outputs an array of objects, but transform_csv expects an array of strings. This mismatch compiles to None at the individual tool level—both schemas are independently valid—but the composition fails at runtime when the transformer receives nested objects instead of flat strings. Worse, some languages and serializers silently coerce types, producing garbled output rather than a clean error. Composition validation catches these mismatches at definition time, before any tool executes.
Handling Edge Cases in Production
Several composition patterns require special handling that the basic validator must account for:
-
Optional intermediate tools: When a composition includes conditional branches (tool B executes only if tool A returns a specific status), the validator must check type compatibility on all possible paths, not just the happy path. Fields that might be None in the source schema must be matched against nullable targets, or the validator must emit a WARNING that the target tool may receive None for a required field.
-
Fan-out compositions: A single tool's output feeds multiple downstream tools simultaneously. The validator must check type compatibility on each outbound edge independently, because the same source field may need to satisfy different target schemas with different required-property sets or item types.
-
Schema evolution: When the tool registry updates a tool's output schema (a new field is added, or a field's type changes from integer to number), all compositions referencing that tool must be revalidated. The ecosystem governance dashboard tracks composition freshness—the time since last validation relative to the last schema change for any constituent tool—and flags stale compositions for re-validation.
-
Recursive compositions: A composition that references another composition as a sub-pipeline must be flattened before validation. The validator detects cycles by maintaining a visited-set during traversal and rejects any composition that references itself directly or transitively, returning an ERROR with the cycle path for debugging.
Testing Composition Validation
Effective testing of the composition validator requires two categories of structural test cases. First, positive type tests confirm that compatible schemas (e.g., integer source with number target, or a source object that provides all of a target's required properties) produce zero errors. Second, negative type tests verify that incompatible schemas (e.g., string source with integer target, an array of objects feeding a slot expecting an array of strings, or a missing required property) produce specific, actionable error messages including the full field path. Each test should construct a minimal composition DAG, invoke validation, and assert on the exact ValidationIssue list, checking severity, tool names, and message content. This approach ensures that as new tools are added to the registry and as existing tool schemas evolve, the validator continues to catch structural drift across the entire tool mesh.
Code Walkthrough
The Composition Graph Model
A tool composition is a directed acyclic graph (DAG) where nodes represent MCP tool invocations and edges represent data flow between them. Each edge carries a mapping specification that declares which fields from the source tool's output feed into the destination tool's input. The validator traverses this graph topologically, checking type compatibility at every edge and accumulating structural mismatches into a single report.
Code snippet mermaid
Loading diagram...
- Line 1: Declares a Mermaid flowchart with top-down (TD) layout direction.
- Lines 2-3: Define two edges from node A (labeled db_query) — one sends rows as data to node B (transform_csv), and another sends rows as records to node C (aggregate_stats), representing a fan-out from the database query.
- Lines 4-5: Define two edges converging into node D (email_send) — node B sends csv_string as body and node C sends summary as body, representing a fan-in where both the CSV and aggregated stats feed into the email step.
- Line 7: Styles node A (db_query) with a dark green background (#2d6a4f) and white text, visually marking it as the data source.
- Lines 8-9: Style nodes B and C (the transformation nodes) with a dark teal background (#264653) and white text, grouping them visually as intermediate processing steps.
- Line 10: Styles node D (email_send) with a burnt orange background (#e76f51) and white text, visually distinguishing it as the terminal output/sink node.
- Lines 12-16: Define a subgraph named "Type Validation Layer" containing three diamond-shaped Type Check validator nodes — V1, V2, and V3 — connected via dotted arrows (-.->) to A, B, and D respectively, representing the structural type-compatibility checks applied at each pipeline stage.
In this graph, the validator must verify that db_query.rows is type-compatible with transform_csv.data, that db_query.rows is compatible with aggregate_stats.records, and that both paths into email_send produce valid body fields. The fan-in into email_send requires checking each incoming edge independently, because two different source fields (csv_string and summary) must each satisfy the same target schema for body.
Implementing the Type Compatibility Checker
The following implementation defines the core ToolCompositionValidator class alongside a SchemaCompatibilityChecker that performs structural subtyping on JSON Schema types. The SchemaCompatibilityChecker.is_compatible method implements a recursive algorithm that checks whether a source schema can satisfy a target schema's constraints, handling nested objects, arrays with typed items, union types, and nullable fields. The ToolCompositionValidator class consumes composition definitions—lists of CompositionEdge objects—and delegates type checking to the compatibility checker while accumulating validation errors into a structured report. This design separates the concern of schema analysis from pipeline traversal, making each component independently testable against your tool registry's schema catalog.
Code snippet python
1from dataclasses import dataclass, field 2from enum import Enum 3 4class Severity(Enum): 5 ERROR = "error" 6 WARNING = "warning" 7 8@dataclass 9class ValidationIssue: 10 severity: Severity 11 source_tool: str 12 target_tool: str 13 message: str 14 field_path: str = "" 15 16@dataclass 17class CompositionEdge: 18 source_tool: str 19 target_tool: str 20 field_mappings: dict[str, str] # source_field -> target_field 21 22class SchemaCompatibilityChecker: 23 NUMERIC_TYPES = {"integer", "number"} 24 25 def is_compatible(self, source: dict, target: dict) -> list[str]: 26 errors = [] 27 src_type = source.get("type") 28 tgt_type = target.get("type") 29 30 if tgt_type == "number" and src_type in self.NUMERIC_TYPES: 31 return errors 32 if src_type != tgt_type: 33 errors.append( 34 f"Type mismatch: source={src_type}, target={tgt_type}" 35 ) 36 return errors 37 38 if tgt_type == "object": 39 tgt_props = target.get("properties", {}) 40 src_props = source.get("properties", {}) 41 tgt_required = set(target.get("required", [])) 42 for prop_name in tgt_required: 43 if prop_name not in src_props: 44 errors.append(f"Missing required property: {prop_name}") 45 else: 46 nested = self.is_compatible( 47 src_props[prop_name], tgt_props[prop_name] 48 ) 49 errors.extend( 50 f"{prop_name}.{e}" for e in nested 51 ) 52 53 if tgt_type == "array": 54 src_items = source.get("items", {}) 55 tgt_items = target.get("items", {}) 56 if src_items and tgt_items: 57 nested = self.is_compatible(src_items, tgt_items) 58 errors.extend(f"items.{e}" for e in nested) 59 60 return errors
- Lines 1-2: Imports dataclass for structured data containers and Enum for type-safe severity levels, avoiding raw string comparisons throughout the validator.
- Lines 4-5: The Severity enum restricts validation issue classification to ERROR (blocks execution) and WARNING (advisory), ensuring downstream consumers can filter deterministically.
- Lines 7-12: ValidationIssue captures the full context of each problem—which tools are involved, the severity, a human-readable message, and the specific field_path where the incompatibility occurs, defaulting to an empty string for composition-level issues.
- Lines 14-17: CompositionEdge models a single connection in the composition DAG, with field_mappings declaring how source output fields map to target input fields—this is the unit of type-checking.
- Lines 19-20: SchemaCompatibilityChecker groups numeric types into a set so that integer → number coercion is treated as valid, matching JSON Schema's numeric hierarchy.
- Lines 22-30: The is_compatible method first handles numeric widening (integer-to-number is safe), then rejects any other type mismatch immediately, short-circuiting deeper checks when the fundamental types disagree.
- Lines 32-42: For object types, the checker iterates over the target's required properties, verifying each exists in the source schema, then recursively checks nested property schemas, prefixing nested error messages with the property name to build a full dotted path like address.zipcode.Type mismatch.
- Lines 44-49: Array compatibility delegates to item-schema comparison, ensuring that an array of objects is not silently accepted where an array of strings is expected—the exact failure mode described in the db_query → transform_csv scenario.
Driving Validation Across the Composition DAG
The ToolCompositionValidator consumes a list of CompositionEdge objects, fetches each tool's input and output schemas from the registry, and iterates over every field mapping to invoke the SchemaCompatibilityChecker. The result is a single list of ValidationIssue objects covering every structural mismatch across the entire pipeline, so authors fix all type defects in one pass rather than discovering them edge-by-edge at runtime.
Code snippet python
1class ToolCompositionValidator: 2 def __init__(self, registry_client, checker: SchemaCompatibilityChecker): 3 self._registry = registry_client 4 self._checker = checker 5 6 def validate(self, edges: list[CompositionEdge]) -> list[ValidationIssue]: 7 issues: list[ValidationIssue] = [] 8 for edge in edges: 9 src_schema = self._registry.get_output_schema(edge.source_tool) 10 tgt_schema = self._registry.get_input_schema(edge.target_tool) 11 src_props = src_schema.get("properties", {}) 12 tgt_props = tgt_schema.get("properties", {}) 13 for src_field, tgt_field in edge.field_mappings.items(): 14 if src_field not in src_props: 15 issues.append(ValidationIssue( 16 severity=Severity.ERROR, 17 source_tool=edge.source_tool, 18 target_tool=edge.target_tool, 19 message=f"Source field missing: {src_field}", 20 field_path=src_field, 21 )) 22 continue 23 if tgt_field not in tgt_props: 24 issues.append(ValidationIssue( 25 severity=Severity.ERROR, 26 source_tool=edge.source_tool, 27 target_tool=edge.target_tool, 28 message=f"Target field missing: {tgt_field}", 29 field_path=tgt_field, 30 )) 31 continue 32 errors = self._checker.is_compatible( 33 src_props[src_field], tgt_props[tgt_field] 34 ) 35 for err in errors: 36 issues.append(ValidationIssue( 37 severity=Severity.ERROR, 38 source_tool=edge.source_tool, 39 target_tool=edge.target_tool, 40 message=err, 41 field_path=f"{src_field}->{tgt_field}", 42 )) 43 return issues
- Lines 1-4: The constructor takes a registry_client for schema lookups and a SchemaCompatibilityChecker instance, following dependency injection so the validator can be tested with a mock registry and a stub checker.
- Lines 6-9: validate seeds an empty issue list and walks every CompositionEdge in topological order, fetching each tool's published output schema and the downstream tool's input schema from the registry.
- Lines 10-11: Extracts the properties maps once per edge so the inner loop indexes directly without re-walking the schema root for each field mapping.
- Lines 12-21: Verifies that every declared source field actually exists on the source tool's output schema. A missing source field is the most common authoring error (typos, renamed fields after a schema update) and is reported with the field name preserved in field_path for fast remediation.
- Lines 22-30: Mirrors the source-side check on the target field, catching cases where the downstream tool's input schema has been refactored but the composition definition has not been updated.
- Lines 31-41: Delegates structural type comparison to SchemaCompatibilityChecker.is_compatible, then lifts each returned error string into a ValidationIssue with the source_field->target_field mapping recorded in field_path, so the audit log shows precisely which mapping in which edge produced the mismatch.
Integrating with OAuth 2.1 Authorization
MCP's OAuth 2.1 authorization framework issues scoped tokens for tool execution. Type-compatibility validation integrates at the token-issuance boundary: before the authorization server grants a composition-scoped token, it invokes the validator. If validation surfaces any ERROR-severity structural mismatch, the token request is denied with a structured error response. This creates an enforcement point that cannot be bypassed—no valid token means no execution, regardless of whether individual tools would authorize independently.
The integration flow works as follows:
-
Key Terminology:
-
Composition Token: An OAuth 2.1 access token whose scope covers multiple tools in a single pipeline, issued only after structural type validation passes.
-
Structural Subtyping: A type compatibility approach where a source schema satisfies a target if it provides at least all required properties with compatible types, regardless of additional properties.
-
Composition DAG: The directed acyclic graph representation of a multi-tool workflow where nodes are tool invocations and edges are typed data-flow connections.
-
Field Mapping: A declared correspondence between an output field on one tool and an input field on the next, against which structural type compatibility is checked.
-
Numeric Widening: A JSON Schema compatibility rule that permits an integer source to satisfy a number target without producing a type-mismatch error.
Before a client can chain MCP tools together, the OAuth 2.1 Auth Server enforces a type-compatibility gate. The ToolCompositionValidator walks every declared field mapping in the composition DAG and asks the SchemaCompatibilityChecker whether the source schema structurally satisfies the target schema. Only when every edge passes does the TokenStore issue a scoped composition token; otherwise the client receives a 403 Forbidden with the list of mismatches, blocking type-incompatible pipelines before they execute.
Code snippet mermaid
Loading diagram...
- Line 1: Declares a Mermaid sequence diagram, which visualizes interactions between participants over time.
- Lines 2-5: Define the four participants (actors) in the diagram: Client, AuthServer (labeled "OAuth 2.1 Auth Server"), Validator (labeled "ToolCompositionValidator"), and TokenStore (labeled "Token Store").
- Line 7: The Client sends a request to the AuthServer for a composition token, including a tool list and field mappings as payload.
- Line 8: The AuthServer forwards the request to the Validator to check that the types across composed tools are compatible.
- Line 9: The Validator returns the type check results back to the AuthServer (dashed arrow indicates a response/return message).
- Line 11: Begins an alt (alternative) block representing conditional branching — the first branch handles the case where every edge in the composition is type-compatible.
- Line 12: The AuthServer requests the TokenStore to issue a scoped token that is limited to the validated tool composition.
- Line 13: The TokenStore returns the newly created composition token to the AuthServer.
- Line 14: The AuthServer responds to the Client with an HTTP 200 OK status and the issued token.
- Line 15: The else branch handles the case where type validation surfaces one or more structural mismatches.
- Line 16: The AuthServer responds to the Client with an HTTP 403 Forbidden status and an array of ValidationIssue records describing each type incompatibility.
- Line 17: Closes the alt conditional block, ending the sequence diagram's branching logic.
This sequence ensures that type-compatibility validation is not merely advisory—it is a hard gate in the authorization flow. The tool routing layer downstream accepts only composition tokens that embed the validated tool list, preventing runtime substitution of tools whose schemas were never checked against the pipeline's field mappings.
Do's and Don'ts
Do's
- ✓Do validate field mappings at every CompositionEdge before issuing execution tokens — a structural mismatch between
db_query.rows(an array of objects) and a target field expecting an array of strings silently corrupts data at runtime; catching it inSchemaCompatibilityChecker.is_compatibleat composition-definition time shrinks the blast radius to a build-time report rather than a production incident. - ✓Do treat fan-in edges into a single target field as independent checks — when both
transform_csv.csv_stringandaggregate_stats.summarymap toemail_send.body, each incoming edge must be validated separately against the target schema, because a valid path from one source does not guarantee the other source satisfies the samebodyconstraints. - ✓Do separate schema analysis (
SchemaCompatibilityChecker) from graph traversal (ToolCompositionValidator) — keeping recursive type-compatibility logic (nullable handling, nested object required-property checks, array items subtyping) isolated from DAG edge iteration makes each component independently testable against your tool registry's schema catalog without re-exercising the full pipeline.
Don'ts
- ✗Don't conflate
integerandnumbertype compatibility with general type leniency —SchemaCompatibilityCheckerintentionally permitsinteger → numberwidening (viaNUMERIC_TYPES) but nothing else; adding ad-hoc string-to-number coercions or silently dropping type mismatches inis_compatiblewould allow structurally broken compositions to pass validation and corrupt downstream tool inputs. - ✗Don't skip required-property checks on nested objects in composition edges — the validator iterates
target.get("required", [])and flags any property missing from the source schema; omitting this check lets a composition wheredb_query.rowslacks a field thataggregate_stats.recordsrequires appear valid, only to raise a key error when the actual tool executes. - ✗Don't defer composition validation to the first live execution of the DAG — the entire point of topological traversal with accumulated
ValidationIssueobjects is to surface all structural mismatches (type conflicts, missing required fields, array item schema gaps) in a single pre-execution report; skipping the validator and relying on runtime tool errors means every fan-out branch of the graph must fail independently before the full set of incompatibilities is known.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Solutions Architecture subscription.
From · cancel anytime
More free lessons in GenAI Architecture & Design Patterns
- Ch 11Build MCP server registry with capability discovery and health checks
- Ch 11Validate MCP tool composition correctness and safetyYou are here
- Ch 11Build MCP tool routing with load balancing and failover
- Ch 11Create MCP ecosystem governance dashboard
- Ch 12Build A2A agent card registry with capability advertisement
- Ch 12Implement A2A task delegation with streaming artifact exchange
- Ch 12Validate A2A communication reliability with failure injection