Skip to content
reaatech

@reaatech/mcp-schema-evolution

npm v0.0.0

A function that compares two MCP `Tool[]` snapshots and returns a classified list of schema changes (breaking, non-breaking, or patch) with migration guidance, including heuristic-based field rename detection.

@reaatech/mcp-schema-evolution

npm version License: MIT CI

Status: Pre-1.0 — APIs may change in minor versions. Pin to a specific version in production.

Schema diffing, change classification, and field rename detection for MCP tool definitions. Compare two Tool[] snapshots and get a fully classified list of changes — breaking, non-breaking, or patch — with migration guidance for every change.

Installation

terminal
npm install @reaatech/mcp-schema-evolution
# or
pnpm add @reaatech/mcp-schema-evolution

Feature Overview

  • Schema diffing — compare two Tool[] snapshots and detect added, removed, changed, and renamed fields
  • Recursive diff — drills into nested properties and items schemas
  • Change classification — every change is classified as breaking, non-breaking, or patch with severity (high, medium, low)
  • Field rename detection — heuristic-based similarity scoring with configurable confidence threshold (default 0.8)
  • Migration guidance — actionable suggestions and automated flag on every classified change
  • Result-based error handling — all public functions return Result<T> (never throws)
  • Zero runtime dependencies — only requires Node.js built-ins

Quick Start

typescript
import { diffToolSnapshots, loadToolsFromFile, type SchemaChange } from "@reaatech/mcp-schema-evolution";
 
// Load snapshots from disk
const v1 = loadToolsFromFile("snapshots/v1.json");
const v2 = loadToolsFromFile("snapshots/v2.json");
 
if (v1.ok && v2.ok) {
  const result = diffToolSnapshots(v1.value, v2.value);
 
  if (result.ok) {
    const changes: SchemaChange[] = result.value;
    const breaking = changes.filter((c) => c.type === "breaking");
    const nonBreaking = changes.filter((c) => c.type === "non-breaking");
 
    console.log(`${changes.length} changes: ${breaking.length} breaking, ${nonBreaking.length} non-breaking`);
 
    for (const change of changes) {
      console.log(`[${change.severity}] ${change.toolName} ${change.path}: ${change.description}`);
      if (change.migration) {
        console.log(`  → ${change.migration.suggestion}`);
      }
    }
  } else {
    console.error(result.error.message);
  }
}

API Reference

Core Functions

FunctionDescription
diffToolSnapshots(old, new, options?)Compare two tool snapshots. Returns Result<SchemaChange[]>.
classifyChange(detected)Classify a raw detected change into a full SchemaChange with type and severity.
detectFieldRenames(old, new, options?)Detect probable field renames between two tools. Returns FieldRename[].
loadToolsFromFile(path)Load and validate a tool snapshot from a JSON file. Returns Result<Tool[]>.

diffToolSnapshots(oldTools: Tool[], newTools: Tool[], options?: DiffOptions): Result<SchemaChange[]>

Compares two tool snapshots at the tool level, field level, and property level. Recursively diffs nested properties and items schemas. Automatically detects field renames using the configured threshold and aggregates rename + remove + add pairs into a single field_renamed change.

typescript
import { diffToolSnapshots } from "@reaatech/mcp-schema-evolution";
 
const result = diffToolSnapshots(oldTools, newTools, { renameThreshold: 0.7 });

Options:

OptionTypeDefaultDescription
renameThresholdnumber (0–1)0.8Minimum similarity score for field rename detection

loadToolsFromFile(path: string): Result<Tool[]>

Reads a JSON file from disk, parses it, and validates the structure:

  • Must be a JSON array
  • Each element must have a name (string) and inputSchema (object with type string)

Error codes:

CodeWhen
FILE_READ_ERRORFile does not exist or cannot be read
JSON_PARSE_ERRORFile content is invalid JSON
INVALID_FORMATRoot element is not an array
INVALID_TOOLAn element lacks valid name or inputSchema

classifyChange(detected: DetectedChange): SchemaChange

Classifies a raw DetectedChange into a fully populated SchemaChange with type, severity, and optional migration guidance. Always succeeds.

detectFieldRenames(oldTool: Tool, newTool: Tool, options?: { threshold?: number }): FieldRename[]

Uses a weighted similarity heuristic to detect probable field renames:

FactorWeightScoring
Type match3Deep equality of JSON Schema type
Description match1Full match or substring containment
Enum match0.5Deep equality of enum arrays
Pattern match0.5Exact pattern string match

Returns candidates whose similarity score meets or exceeds the threshold.

Core Types

Tool

typescript
interface Tool {
  name: string;
  description?: string;
  inputSchema: {
    $schema?: string;
    type: "object";
    properties?: Record<string, unknown>;
    required?: string[];
  };
}

SchemaChange

typescript
interface SchemaChange {
  type: ChangeType;              // "breaking" | "non-breaking" | "patch"
  category: ChangeCategory;      // see classification table below
  toolName: string;              // affected MCP tool name
  path: string;                  // dot-separated path (e.g. "search.inputSchema.properties.query")
  description: string;           // human-readable change description
  severity: ChangeSeverity;      // "high" | "medium" | "low"
  migration?: MigrationGuidance; // actionable migration advice
}

MigrationGuidance

typescript
interface MigrationGuidance {
  suggestion: string;     // human-readable migration advice
  codeExample?: string;   // optional code example
  automated: boolean;     // true if tooling can handle this automatically
}

DetectedChange

typescript
interface DetectedChange {
  category: ChangeCategory;
  toolName: string;
  path: string;
  description: string;
  oldValue?: unknown;
  newValue?: unknown;
  required?: boolean;       // only for field_added
  constraintName?: string;   // only for constraint_changed
}

FieldRename

typescript
interface FieldRename {
  from: string;         // old field name
  to: string;           // new field name
  confidence: number;   // similarity score (0–1)
}

Result<T>

typescript
type Result<T> = { ok: true; value: T } | { ok: false; error: EvolutionError };

EvolutionError

typescript
class EvolutionError extends Error {
  readonly code: string;
  readonly path: string | undefined;
  readonly suggestion: string | undefined;
}

Supporting Types

typescript
type ChangeType = "breaking" | "non-breaking" | "patch";
type ChangeSeverity = "high" | "medium" | "low";
type ChangeCategory =
  | "tool_added"
  | "tool_removed"
  | "field_added"
  | "field_removed"
  | "field_renamed"
  | "type_changed"
  | "required_changed"
  | "default_changed"
  | "constraint_changed"
  | "deprecated";
type ToolSnapshot = Tool[];
type SemVer = `${number}.${number}.${number}`;

Change Classification Reference

ChangeCategoryClassificationSeverity
Removing a tooltool_removedbreakinghigh
Removing a required fieldfield_removedbreakinghigh
Adding a required field (no default)field_addedbreakinghigh
Renaming a fieldfield_renamedbreakinghigh
Narrowing a type (stringinteger)type_changedbreakinghigh
Field becoming requiredrequired_changedbreakinghigh
Tightening constraints (enum smaller, minLength up)constraint_changedbreakinghigh
Adding an optional fieldfield_addednon-breakingmedium
Adding a new tooltool_addednon-breakinglow
Widening a type (integernumber)type_changednon-breakingmedium
Field becoming optionalrequired_changednon-breakingmedium
Relaxing constraints (enum larger, maxLength up)constraint_changednon-breakingmedium
Default value changeddefault_changednon-breakinglow
Deprecating a fielddeprecatednon-breakingmedium

Usage Pattern

typescript
import {
  diffToolSnapshots,
  loadToolsFromFile,
  classifyChange,
  type DetectedChange,
  type SchemaChange,
} from "@reaatech/mcp-schema-evolution";
 
// Compare snapshots
const result = diffToolSnapshots(oldTools, newTools, { renameThreshold: 0.7 });
if (result.ok) {
  for (const change of result.value) {
    if (change.type === "breaking") {
      console.warn(`BREAKING: ${change.description}`);
      if (change.migration) {
        console.warn(`  Fix: ${change.migration.suggestion}`);
      }
    }
  }
}
 
// Manual classification
const raw: DetectedChange = {
  category: "field_removed",
  toolName: "search",
  path: "search.inputSchema.properties.query",
  description: 'Field "query" removed',
};
const classified: SchemaChange = classifyChange(raw);
// classified.type === "breaking", classified.severity === "high"
 
// Standalone rename detection
import { detectFieldRenames } from "@reaatech/mcp-schema-evolution";
const renames = detectFieldRenames(oldTool, newTool, { threshold: 0.5 });
for (const rename of renames) {
  console.log(`${rename.from} → ${rename.to} (${(rename.confidence * 100).toFixed(0)}%)`);
}

License

MIT