An MCP server can be healthy, reachable, and still be incompatible with the client consuming it.
A tool may have the same name but a different input schema. A required property may disappear. An enum may change. A server upgrade may introduce a response field that an older client does not understand. Documentation can also become stale while the implementation continues to evolve.
These problems are examples of schema drift.
For MCP systems, schema drift is especially important because tools are contracts between an AI client and an external server. The model uses tool descriptions and schemas to decide what arguments to generate. If those contracts change unexpectedly, failures can occur before the underlying business logic is even reached.
The official MCP C# SDK has continued to expand its conformance and protocol-validation coverage as the protocol evolves. Its 2.0 release aligns with the 2026-07-28 MCP specification and includes JSON Schema 2020-12 support, while the SDK repository also maintains conformance testing as part of its development process.
This makes contract testing an important part of an MCP deployment pipeline.
What Is MCP Schema Drift?
Consider an MCP tool initially defined as:
{
"name": "get_customer",
"inputSchema": {
"type": "object",
"properties": {
"customerId": {
"type": "string"
}
},
"required": ["customerId"]
}
}
A client builds its tool-calling behavior around that contract.
Later, the server changes the property:
{
"name": "get_customer",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": ["id"]
}
}
The implementation may still work perfectly from the server's perspective.
The client, however, is now sending:
{
"customerId": "C1001"
}
The server expects:
{
"id": "C1001"
}
Nothing is wrong with the network connection.
Nothing is necessarily wrong with the server process.
The contract changed.
Why Schema Drift Is Dangerous for AI Agents
Traditional APIs are usually called by application code written against a known contract.
AI agents add another layer.
The model receives tool metadata and uses it to construct arguments dynamically.
The flow looks like this:
MCP Server
|
| Tool definition
v
MCP Client
|
| Tool schema
v
LLM
|
| Generated arguments
v
MCP Tool
If the schema is incorrect, incomplete, stale, or incompatible, the model can generate invalid requests.
There are therefore at least three contracts to consider:
Discovery contract — what the server advertises.
Input contract — what arguments the tool accepts.
Output contract — what the tool returns.
A good test strategy validates all three.
What Should an MCP Compatibility Test Validate?
A useful test suite should check:
| Area | Example Validation |
|---|
| Tool name | Expected tool exists |
| Description | Required metadata exists |
| Input schema | Valid JSON Schema |
| Required properties | Expected fields remain required |
| Property types | String remains string |
| Enum values | Supported values remain compatible |
| Output schema | Response contract remains valid |
| Tool invocation | Valid request succeeds |
| Invalid input | Server rejects invalid arguments |
| Error behavior | Errors follow expected structure |
| Protocol behavior | Client and server negotiate correctly |
Do not rely only on an end-to-end happy-path test.
A tool can successfully return a result while still having a contract that will break another client.
Creating a Contract Snapshot
One practical approach is to capture the server's advertised tool definitions and store them as versioned test artifacts.
For example:
{
"name": "get_customer",
"description": "Returns customer information.",
"inputSchema": {
"type": "object",
"properties": {
"customerId": {
"type": "string"
}
},
"required": [
"customerId"
]
}
}
Store this snapshot in your test project:
tests/
└── Contracts/
├── get-customer.json
├── create-order.json
└── search-products.json
When the MCP server changes, the CI pipeline can retrieve the current definitions and compare them with the approved contract.
This does not mean every difference should fail the build.
Some changes are compatible.
Others are breaking.
Breaking vs Non-Breaking Schema Changes
A useful compatibility policy looks like this:
| Change | Compatibility |
|---|
| Fix description typo | Usually safe |
| Add optional property | Usually safe |
| Add required property | Breaking |
| Rename property | Breaking |
| Remove property | Potentially breaking |
| Change string to integer | Breaking |
| Narrow accepted enum values | Potentially breaking |
| Add enum value | Usually compatible |
| Remove enum value | Potentially breaking |
| Change output property type | Breaking |
The exact policy should be defined by your client ecosystem.
For example, adding an optional field is normally less risky than changing a required field, but an especially strict client may still enforce an exact schema.
Testing the MCP Server with .NET
A basic integration test can use the official C# SDK.
A simplified test structure might look like:
public sealed class McpContractTests
{
private readonly IMcpClient _client;
public McpContractTests(IMcpClient client)
{
_client = client;
}
[Fact]
public async Task GetCustomer_Tool_ShouldExist()
{
var tools = await _client.ListToolsAsync();
var tool = tools.FirstOrDefault(
x => x.Name == "get_customer");
Assert.NotNull(tool);
}
}
The exact API surface should be matched to the SDK version used by the project. The official C# SDK provides client and server APIs and is maintained in collaboration with Microsoft.
The important testing principle is that the test should consume the server through MCP rather than directly inspecting its internal C# classes.
That makes the test a contract test rather than an implementation test.
Validating Tool Input
After confirming that a tool exists, validate its schema.
A contract test can verify that:
get_customer
|
+-- customerId
|
+-- type: string
+-- required: true
The test can fail if the server changes it to:
get_customer
|
+-- id
|
+-- type: string
+-- required: true
The test output should explain the difference clearly:
MCP contract violation
Tool: get_customer
Expected required property:
customerId
Actual required property:
id
This is much easier to diagnose than discovering the problem through an agent failure in production.
Testing JSON Schema
MCP tool schemas use JSON Schema. The current C# SDK 2.0 work includes support aligned with JSON Schema 2020-12.
That makes schema validation a useful independent test layer.
For example, a test can validate that a schema contains:
{
"type": "object",
"properties": {
"customerId": {
"type": "string"
}
},
"required": ["customerId"]
}
The test should verify both structure and semantics.
For example:
Assert.Equal(
"object",
schema["type"]?.ToString());
Assert.True(
schema["properties"]?["customerId"] is not null);
For more complete validation, use a JSON Schema validator rather than manually checking every property.
The objective is to validate the schema as a schema, not merely as a JSON document.
Testing Valid and Invalid Tool Calls
Schema validation alone is not enough.
The server should also be tested with representative inputs.
For a customer lookup:
{
"customerId": "C1001"
}
Then test invalid requests:
{}
and:
{
"customerId": 1001
}
The expected behavior should be explicitly defined.
For example:
Valid input
-> Tool executes
Missing customerId
-> Validation error
customerId has wrong type
-> Validation error
This verifies that the advertised schema and actual server behavior agree.
That distinction matters.
A server can advertise one schema while its implementation behaves differently.
Detecting Output Contract Drift
Input schemas receive most of the attention, but output schemas can drift too.
Suppose the expected result is:
{
"customerId": "C1001",
"name": "John",
"status": "active"
}
A later version returns:
{
"customer": "C1001",
"name": "John",
"status": "active"
}
An agent or downstream application that expects customerId can fail even though the tool invocation itself succeeds.
Therefore, contract tests should validate important output fields as well.
A useful test might assert:
Assert.True(result.ContainsKey("customerId"));
Assert.True(result.ContainsKey("name"));
Assert.True(result.ContainsKey("status"));
For strongly structured applications, use a JSON Schema validator or deserialize into a known DTO and validate it.
Testing Tool Compatibility Across Versions
Schema testing becomes especially valuable during server upgrades.
Use a matrix:
MCP Server Version
v1 v2 v3
Client A OK OK ?
Client B OK ? ?
Client C OK ? ?
A compatibility pipeline can test:
Client Version
+
Server Version
+
Protocol Version
+
Tool Contract
The MCP C# SDK 2.0 supports negotiation with earlier protocol versions, and the SDK's release work explicitly tracks interoperability and conformance behavior across protocol revisions.
This is important when an organization cannot upgrade every MCP client simultaneously.
Protocol Compatibility Is Different from Tool Compatibility
These two problems should not be confused.
Protocol compatibility
Can the client and server communicate using the expected MCP protocol behavior?
Tool compatibility
Once communication succeeds, do the available tools and their schemas match what the client expects?
The architecture is:
Protocol Contract
Client <----------------------> Server
|
|
+-------- Tool Contract --------+
|
Tool Schema
|
Tool Output
A successful protocol handshake does not guarantee tool compatibility.
This distinction should appear in your test reports.
A CI Pipeline for MCP Contract Testing
A production pipeline can look like:
Developer Pull Request
|
v
Build MCP Server
|
v
Start Test Server
|
v
Discover Tools
|
v
Validate Schemas
|
v
Run Valid Calls
|
v
Run Invalid Calls
|
v
Compare Approved Contracts
|
v
Compatibility Tests
|
v
Publish Artifact
A simplified GitHub Actions workflow could be:
name: MCP Contract Tests
on:
pull_request:
push:
jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run MCP contract tests
run: dotnet test --no-build
The important part is not the workflow syntax.
It is the policy:
A tool contract change should become visible during CI rather than after deployment.
Golden Contract Files
A useful technique is to maintain approved "golden" contracts.
For example:
contracts/
├── customer-service/
│ ├── get_customer.json
│ ├── update_customer.json
│ └── search_customer.json
During CI:
Current Server Contract
|
v
Compare
|
v
Approved Contract
If the difference is compatible, the pipeline can report it as informational.
If it is breaking, the pipeline should fail.
This provides a clear review process for API-like changes to MCP tools.
Handling Intentional Breaking Changes
Not every breaking change is bad.
Sometimes the business contract genuinely needs to change.
The problem is making that change without acknowledging its impact.
A better process is:
Breaking Change
|
v
Create New Tool Version
|
v
Update Consumers
|
v
Compatibility Period
|
v
Deprecate Old Tool
|
v
Retire Old Tool
For example:
get_customer_v1
get_customer_v2
The exact versioning model can vary, but the principle remains the same: make breaking changes explicit.
Detecting Description Drift
Tool descriptions are also part of the agent-facing contract.
Consider:
Searches customer orders by customer ID.
changing to:
Searches all customer records and may modify order state.
That is not merely documentation.
It can influence how an agent decides to use the tool.
Recent MCP ecosystem research has specifically examined how server descriptions change over time, highlighting that catalog descriptions can become stale and that drift needs continuous validation rather than a one-time audit.
For important tools, review:
Description
Tool name
Input schema
Output schema
Security requirements
Side-effect behavior
as one contract.
Common Mistakes
Testing Only Tool Availability
A test that says "the tool exists" does not prove compatibility.
Comparing Raw JSON Strings
JSON property ordering should not normally determine compatibility.
Compare parsed structures and semantic differences instead.
Treating Every Difference as Breaking
Adding an optional property should not automatically block every deployment.
Define compatibility rules.
Ignoring Descriptions
Descriptions are consumed by agents and can affect tool selection.
Testing Only Successful Calls
Invalid input behavior is part of the contract.
Relying Only on End-to-End Agent Tests
End-to-end tests are valuable but difficult to diagnose.
Contract tests isolate the problem earlier.
Troubleshooting Schema Test Failures
When a contract test fails, identify which layer changed.
Tool disappeared
Check:
Tool registration
Assembly scanning
Feature flags
Deployment configuration
Tool deprecation status
Property disappeared
Check whether the change was intentional and whether consumers have been migrated.
Type changed
Check the DTO and serialization configuration.
Output validation failed
Capture the actual response and compare it against the expected schema.
Old client stopped working
Check protocol negotiation separately from tool schema compatibility.
For MCP SDK upgrades, also review the SDK release notes and migration guidance because protocol and transport behavior can change independently of application-level tool contracts.
Conclusion
MCP tools should be treated as contracts, not simply methods exposed to an AI model.
As MCP servers evolve, schema drift can silently break clients, change agent behavior, or create failures that are difficult to diagnose from production logs alone.
A strong testing strategy therefore validates the complete contract:
Tool Discovery
+
Input Schema
+
Input Validation
+
Output Schema
+
Protocol Compatibility
+
Client Compatibility
The official C# SDK's continued focus on protocol alignment and conformance testing reinforces the importance of this approach. MCP 2.0 introduces substantial protocol changes, including discovery-first negotiation, stateless HTTP behavior, and JSON Schema 2020-12 alignment, making explicit compatibility testing even more valuable.
The goal is not to prevent every schema change.
The goal is to ensure that every meaningful schema change is detected, classified, reviewed, and tested before it reaches the agents depending on it.
That turns MCP compatibility from a production surprise into a normal part of the software delivery pipeline.
Frequently Asked Questions
What is MCP schema drift?
Schema drift occurs when the contract exposed by an MCP server changes over time, such as renamed properties, changed types, removed fields, or modified tool behavior.
Should MCP tool schemas be stored in source control?
For production systems, maintaining approved contract snapshots in source control can provide a useful baseline for automated compatibility testing.
Are MCP protocol tests and tool contract tests the same?
No. Protocol tests verify communication and protocol behavior. Tool contract tests verify the tools, schemas, inputs, outputs, and metadata exposed by the server.
Should every schema change fail CI?
No. Teams should define compatibility rules. Adding an optional property may be acceptable, while removing a required property or changing its type may be breaking.
Why test tool descriptions?
Tool descriptions are part of the agent-facing interface. Changes can affect how an AI system understands and selects tools, even when the underlying API still accepts the same arguments.