Imagine building an AI agent that needs to understand the structure of your codebase, not just individual files, but how modules connect, where configurations live, and what dependencies are in play. Without a common language to describe this “codebase context,” every tool would need its own parser, leading to brittle, non-interoperable systems. This is the challenge MCP addresses, and its foundation lies in defining context with precision.

Why This Chapter Matters

In the previous chapter, we grasped the fundamental concept of Model Context Protocol (MCP) as a bridge for intelligent tools. Now, we dive into the bedrock of that bridge: how context is actually defined and shared. Without a clear, universally understood definition of what “context” means for a given domain, interoperability becomes impossible. This chapter is critical because it teaches you to speak the language of MCP, enabling your applications to accurately describe and consume complex information.

Understanding MCP schemas and dynamic negotiation is not just theoretical; it directly impacts:

  • Interoperability: Tools can confidently exchange context because they agree on its structure.
  • Data Integrity: Context providers deliver data that adheres to a known contract, reducing errors.
  • Flexibility: Clients can request specific types and versions of context, allowing systems to evolve gracefully.
  • Tool Development: You’ll be able to design robust MCP clients and servers that communicate effectively.

Learning Objectives

By the end of this chapter, you will be able to:

  • Explain the role of JSON Schema in defining MCP context types.
  • Design a custom JSON Schema to represent a specific domain context within MCP.
  • Understand the relationship between ContextIdentifier, ContextDescriptor, and context schemas.
  • Describe the process of dynamic context negotiation between an MCP client and server.
  • Implement a basic MCP server that advertises custom context types using the TypeScript SDK.
  • Implement a basic MCP client that queries for and understands advertised context types.

The Problem of Unstructured Context

Intelligent tools often require rich, structured information about their operational environment. For instance:

  • A code review AI needs to know about the project’s file structure, dependency graph, and coding standards.
  • A database interaction tool needs the database schema, table relationships, and indexing strategies.
  • A design system checker needs design token definitions, component hierarchies, and accessibility guidelines.

Without a standardized way to describe this information, each tool would need to invent its own parsing and modeling logic, leading to:

  • Duplication of effort: Every tool re-implements context extraction.
  • Brittleness: Small changes in one tool’s output break others.
  • Limited sharing: Context generated by one tool can’t be easily consumed by another.

📌 Key Idea: MCP solves this by providing a protocol for negotiating and exchanging structured context, where “structured” is enforced by schemas.

MCP’s Schema Foundation: JSON Schema

MCP leverages JSON Schema as its primary language for defining context structures. JSON Schema is a powerful, well-established standard for describing the structure and constraints of JSON data.

Here’s how it fits into MCP:

ContextIdentifier: Naming Your Context

Every unique type of context in MCP is identified by a ContextIdentifier. This is a string that uniquely names the context type, typically following a URI-like pattern to ensure global uniqueness and versioning.

Format: mcp://{domain}/{context_name}/{version}

  • mcp://: The protocol prefix.
  • {domain}: Your organization’s domain or a well-known identifier (e.g., github.com/myorg, modelcontextprotocol.org/core).
  • {context_name}: A descriptive name for the context (e.g., project-structure, dependency-graph).
  • {version}: A semantic version for the schema (e.g., v1.0.0).

Quick Note: The version in the ContextIdentifier refers to the schema version, not necessarily the data version. This allows clients to request compatible schema versions.

ContextDescriptor: Advertising What You Offer

When an MCP server advertises the context it can provide, it does so using ContextDescriptor objects. A ContextDescriptor is a metadata object that contains:

  • identifier: The ContextIdentifier for this context type.
  • schemaUri: A URI pointing to the JSON Schema that defines the structure of this context type. This URI can be a https:// URL or a mcp:// URI if the schema itself is served via MCP.
  • description: A human-readable explanation of what this context type represents.
  • tags: Optional keywords for categorization.

🧠 Important: The schemaUri is crucial. It’s how clients know what shape to expect the context data to be in. Servers are expected to provide context data that strictly conforms to the schema at the schemaUri.

Relationship Flow

flowchart TD A[Context Identifier] --> B[Uniquely names context type] C[JSON Schema] --> D[Defines context data structure] A --> E[Context Descriptor] D --> E E --> F[Advertises context capability] F --> G[Client requests context] G --> H[Server validates and responds]

Designing Effective MCP Context Schemas

Creating a good MCP schema is about balancing expressiveness with simplicity and ensuring it’s fit for purpose.

Best Practices for JSON Schemas in MCP

  1. Be Specific: Define exactly what data is included and what its types and constraints are. Avoid overly generic schemas.
  2. Use description: Add clear, concise descriptions for the schema itself and for individual properties. This aids human understanding.
  3. Define required properties: Clearly state which fields are mandatory.
  4. Leverage JSON Schema Keywords: Use type, properties, items, enum, pattern, minimum, maximum, etc., to enforce structure and validation.
  5. Versioning: Plan for schema evolution. Minor changes might be additive (allowing older clients to still parse new data), while breaking changes necessitate a new major version in the ContextIdentifier.
  6. Modularity (Optional): For very complex contexts, consider breaking down a large schema into smaller, reusable components using $ref.

Example: ProjectStructureContext

Let’s consider a context type that describes the basic structure of a software project.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Project Structure Context",
  "description": "Describes the basic file and directory structure of a software project.",
  "type": "object",
  "properties": {
    "rootPath": {
      "type": "string",
      "description": "The absolute path to the project's root directory."
    },
    "files": {
      "type": "array",
      "description": "A list of relevant file paths relative to the root.",
      "items": {
        "type": "string"
      }
    },
    "directories": {
      "type": "array",
      "description": "A list of relevant directory paths relative to the root.",
      "items": {
        "type": "string"
      }
    },
    "mainEntrypoint": {
      "type": "string",
      "description": "Optional: The main entry point file (e.g., index.ts, main.py).",
      "nullable": true
    },
    "dependencies": {
      "type": "array",
      "description": "Optional: A list of declared dependencies (e.g., npm packages, pip requirements).",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "version": { "type": "string" }
        },
        "required": ["name"]
      },
      "nullable": true
    }
  },
  "required": ["rootPath", "files", "directories"]
}

This schema defines a clear contract for any tool providing or consuming ProjectStructureContext.

Worked Example: A Simple TaskItemContext Schema

Let’s design a schema for a common scenario: a list of tasks or to-do items.

Scenario

An MCP-enabled task manager application wants to provide context about the current user’s active tasks to various intelligent assistants (e.g., an AI that summarizes daily progress, or one that suggests next actions).

Schema Design Process

  1. Identify core entities and properties: A task has an ID, title, description, status, and perhaps a due date.
  2. Choose types: Strings for ID, title, description; an enum for status; string for due date (ISO format).
  3. Define relationships: A list of tasks.
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Task Item Context",
  "description": "Describes a collection of task items for a user or project.",
  "type": "object",
  "properties": {
    "userId": {
      "type": "string",
      "description": "The ID of the user associated with these tasks."
    },
    "tasks": {
      "type": "array",
      "description": "A list of individual task items.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the task."
          },
          "title": {
            "type": "string",
            "description": "Brief title of the task."
          },
          "description": {
            "type": "string",
            "description": "Detailed description of the task.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Current status of the task.",
            "enum": ["todo", "in-progress", "done", "blocked"]
          },
          "dueDate": {
            "type": "string",
            "format": "date-time",
            "description": "Optional: ISO 8601 formatted due date and time.",
            "nullable": true
          }
        },
        "required": ["id", "title", "status"]
      }
    }
  },
  "required": ["userId", "tasks"]
}

This TaskItemContext schema provides a clear, structured way to represent task data, making it readily consumable by any MCP client that understands this ContextIdentifier.

Dynamic Context Negotiation

One of MCP’s most powerful features is its ability to dynamically negotiate context. Clients don’t just blindly ask for “context”; they can query for specific types of context, and servers can advertise what they offer.

The Client’s Role: ContextQuery

An MCP client initiates negotiation by sending a ContextQuery. This query can specify:

  • contextIdentifiers: An array of ContextIdentifier strings the client is interested in. This allows the client to express preferences or requirements for multiple context types.
  • preferredVersions: For each identifier, the client might specify a preferred version or a range of acceptable versions (though the core spec often implies exact match or semantic versioning rules).

The client essentially says, “Hey server, can you give me mcp://myorg/task-item/v1.0.0 or mcp://modelcontextprotocol.org/core/project-structure/v1.1.0?”

The Server’s Role: Advertising and Matching

An MCP server, upon receiving a ContextQuery, performs the following:

  1. Advertises Capabilities: It maintains a list of ContextDescriptors for all the context types it can provide. This list is often exposed via a specific MCP endpoint (e.g., /mcp/capabilities).
  2. Matches Query: It compares the contextIdentifiers requested by the client against its advertised ContextDescriptors.
  3. Responds: It returns a list of ContextDescriptors that match the client’s query and are available. If no match is found, it returns an empty list or an error indicating unavailability.

This negotiation allows for:

  • Discovery: Clients can discover what context a server offers without prior knowledge.
  • Version Compatibility: Clients can request specific versions, and servers can respond with the best available match.
  • Graceful Degradation: If a preferred context type isn’t available, the client might fall back to a less preferred but still useful one.
flowchart TD Client[MCP Client] --> Query[Context Request]; Query --> Server[MCP Server]; Server --> CheckCapabilities[Check Descriptors]; CheckCapabilities --> Match[Match Capabilities]; Match --> Response[Context List]; Response --> Client; Client --> Process[Process Context Types];

Real-world insight: This dynamic negotiation is critical in environments with many microservices or intelligent agents. A new client can join the ecosystem and immediately understand what context is available from existing services without needing manual configuration or hardcoding.

Deep Dive: Schema Versioning and Evolution

Schema versioning is a critical consideration for long-lived systems. As your context models evolve, you’ll need a strategy to handle changes without breaking existing clients.

Semantic Versioning for Schemas

The recommended approach for ContextIdentifier versions is semantic versioning (e.g., v1.0.0, v1.1.0, v2.0.0).

  • PATCH (v1.0.1): Backward-compatible bug fixes or minor documentation updates to the schema. No change to the data structure.
  • MINOR (v1.1.0): Backward-compatible new features. This usually means adding optional properties to an existing schema. Older clients will ignore the new properties but can still process the rest of the data.
  • MAJOR (v2.0.0): Breaking changes. This includes:
    • Removing required properties.
    • Changing the type of an existing property.
    • Renaming existing properties.
    • Making an optional property required.

⚠️ What can go wrong: Failing to adhere to semantic versioning can lead to clients receiving data they can’t parse, resulting in runtime errors or incorrect behavior.

Handling Schema Evolution in Practice

  • Server Side: A server might offer multiple versions of a context type simultaneously (e.g., mcp://myorg/project-structure/v1.0.0 and mcp://myorg/project-structure/v1.1.0). When a client queries, the server returns the latest compatible version it can provide.
  • Client Side: Clients should be designed to be robust to minor version changes (e.g., by ignoring unknown properties). For major version changes, they might need to explicitly request a specific major version or implement logic to handle different major versions.
  • Schema Registry: In larger deployments, a centralized schema registry can host all JSON Schemas, providing a single source of truth and simplifying schema discovery and validation. The schemaUri in the ContextDescriptor would then point to this registry.

🔥 Optimization / Pro tip: When designing schemas, prioritize making new fields optional. This allows for backward-compatible evolution and defers major version bumps. Only introduce major versions when absolutely necessary.

Code Lab: Implementing Schema Advertisement and Query

In this lab, you’ll create a simple MCP server that advertises our TaskItemContext and a client that queries for it.

Setup

You’ll need Node.js and npm installed. Create a new directory:

mkdir mcp-schema-lab
cd mcp-schema-lab
npm init -y
npm install @model-context-protocol/sdk express body-parser

Create a file named taskItemSchema.json in the mcp-schema-lab directory:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Task Item Context",
  "description": "Describes a collection of task items for a user or project.",
  "type": "object",
  "properties": {
    "userId": {
      "type": "string",
      "description": "The ID of the user associated with these tasks."
    },
    "tasks": {
      "type": "array",
      "description": "A list of individual task items.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the task."
          },
          "title": {
            "type": "string",
            "description": "Brief title of the task."
          },
          "description": {
            "type": "string",
            "description": "Detailed description of the task.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Current status of the task.",
            "enum": ["todo", "in-progress", "done", "blocked"]
          },
          "dueDate": {
            "type": "string",
            "format": "date-time",
            "description": "Optional: ISO 8601 formatted due date and time.",
            "nullable": true
          }
        },
        "required": ["id", "title", "status"]
      }
    }
  },
  "required": ["userId", "tasks"]
}

Part 1: MCP Server with Advertised Schema

Create server.ts:

import express from 'express';
import bodyParser from 'body-parser';
import { MCP } from '@model-context-protocol/sdk';
import { ContextIdentifier, ContextDescriptor, ContextQuery } from '@model-context-protocol/sdk/dist/types'; // Corrected import path for types
import * as fs from 'fs';
import * as path from 'path';

const app = express();
const port = 3000;

app.use(bodyParser.json());

// Load our custom TaskItemContext schema
const taskItemSchema = JSON.parse(fs.readFileSync(path.join(__dirname, 'taskItemSchema.json'), 'utf-8'));

// Define the ContextIdentifier for our custom context
const TASK_ITEM_CONTEXT_IDENTIFIER: ContextIdentifier = 'mcp://myorg.com/task-item/v1.0.0';

// Define the ContextDescriptor for our custom context
// In a real system, schemaUri would point to a publicly accessible URL.
// For this example, we'll use a local path, assuming the client can resolve it.
// In a production setup, you'd likely serve this schema via an HTTP endpoint.
const taskItemContextDescriptor: ContextDescriptor = {
  identifier: TASK_ITEM_CONTEXT_IDENTIFIER,
  schemaUri: `http://localhost:${port}/schemas/task-item/v1.0.0`, // Server will serve this schema
  description: 'Provides a list of task items for a user.',
  tags: ['tasks', 'productivity', 'user-data'],
};

// A simple in-memory store for context data (for demonstration)
const userTasksContextData = {
  userId: 'user-123',
  tasks: [
    { id: 't1', title: 'Finish MCP Chapter 3', status: 'in-progress' },
    { id: 't2', title: 'Review PR #45', status: 'todo', dueDate: '2026-04-25T17:00:00Z' },
    { id: 't3', title: 'Schedule team sync', status: 'done' },
  ],
};

// MCP Server instance
const mcpServer = new MCP.Server();

// Register our custom context descriptor
mcpServer.registerContextDescriptor(taskItemContextDescriptor);

// Implement a handler for the custom context data
mcpServer.registerContextHandler(TASK_ITEM_CONTEXT_IDENTIFIER, async (query: ContextQuery) => {
  console.log(`Server received query for ${query.contextIdentifiers}`);
  // In a real application, you'd fetch data based on query parameters or current user
  return {
    identifier: TASK_ITEM_CONTEXT_IDENTIFIER,
    data: userTasksContextData,
    metadata: { timestamp: new Date().toISOString() },
  };
});

// Expose the MCP server's HTTP handler
app.post('/mcp/query', async (req, res) => {
  try {
    const queryResult = await mcpServer.handleContextQuery(req.body as ContextQuery);
    res.json(queryResult);
  } catch (error: any) {
    console.error('Error handling MCP query:', error.message);
    res.status(500).json({ error: error.message });
  }
});

// Endpoint to serve the JSON Schema itself
app.get('/schemas/task-item/v1.0.0', (req, res) => {
  res.json(taskItemSchema);
});

// Endpoint to advertise capabilities (ContextDescriptors)
app.get('/mcp/capabilities', (req, res) => {
  res.json(mcpServer.getAdvertisedContextDescriptors());
});

app.listen(port, () => {
  console.log(`MCP Server listening at http://localhost:${port}`);
  console.log('Advertised Context Descriptors:', mcpServer.getAdvertisedContextDescriptors());
});

Part 2: MCP Client Querying for Schema

Create client.ts:

import { MCP } from '@model-context-protocol/sdk';
import { ContextIdentifier, ContextQuery } from '@model-context-protocol/sdk/dist/types'; // Corrected import path for types
import axios from 'axios';

const serverUrl = 'http://localhost:3000';

async function queryServerCapabilities() {
  console.log('--- Querying Server Capabilities ---');
  try {
    const response = await axios.get(`${serverUrl}/mcp/capabilities`);
    console.log('Advertised Capabilities:', JSON.stringify(response.data, null, 2));
    return response.data;
  } catch (error: any) {
    console.error('Error querying capabilities:', error.message);
    return [];
  }
}

async function querySpecificContext(contextId: ContextIdentifier) {
  console.log(`\n--- Querying for Context: ${contextId} ---`);
  const query: ContextQuery = {
    contextIdentifiers: [contextId],
    // In a real client, you might add more query parameters like 'targetId' or 'versionRange'
  };

  try {
    const response = await axios.post(`${serverUrl}/mcp/query`, query);
    console.log(`Received Context for ${contextId}:`, JSON.stringify(response.data, null, 2));

    // After receiving context, a client would typically validate it against the schema
    // For this lab, we'll just log it.
    if (response.data && response.data.contexts && response.data.contexts.length > 0) {
      const receivedContext = response.data.contexts[0];
      console.log('Actual Context Data:', receivedContext.data);
      console.log('Context Metadata:', receivedContext.metadata);
    } else {
      console.log('No matching context received.');
    }
  } catch (error: any) {
    console.error(`Error querying for ${contextId}:`, error.message);
  }
}

async function run() {
  // First, discover what the server can provide
  const capabilities = await queryServerCapabilities();

  // Find our specific task item context
  const taskItemDescriptor = capabilities.find(
    (desc: any) => desc.identifier === 'mcp://myorg.com/task-item/v1.0.0'
  );

  if (taskItemDescriptor) {
    console.log(`Found Task Item Context Descriptor. Schema URI: ${taskItemDescriptor.schemaUri}`);
    // Now query for the actual context data using its identifier
    await querySpecificContext(taskItemDescriptor.identifier);

    // Optionally, fetch the schema itself to perform client-side validation
    try {
      console('\n--- Fetching Schema for Client-Side Validation ---');
      const schemaResponse = await axios.get(taskItemDescriptor.schemaUri);
      console.log('Fetched Schema:', JSON.stringify(schemaResponse.data, null, 2));
      // In a full client, you'd use a JSON Schema validator library here
      // to ensure the received context data conforms to this schema.
    } catch (error: any) {
      console.error('Error fetching schema:', error.message);
    }

  } else {
    console('Task Item Context (mcp://myorg.com/task-item/v1.0.0) not advertised by server.');
  }
}

run();

Running the Lab

  1. Compile TypeScript files: npx tsc server.ts client.ts
  2. Start the server: node server.js
  3. In a separate terminal, run the client: node client.js

You should see the server log that it’s listening and advertising the context. The client will query capabilities, find the TaskItemContext, then query for the actual context data, and finally fetch the schema itself.

Checkpoint

Consider a scenario where an MCP server provides context about a “CI/CD Pipeline Status.” The ContextIdentifier is mcp://devops.org/ci-cd-status/v1.0.0.

  1. What would be the most important properties you would include in the JSON Schema for this context? List at least 3, along with their type and a brief description.
  2. Imagine the server later adds an optional estimatedCompletionTime field. Would this require a major or minor version bump in the ContextIdentifier? Why?

MCQs

  1. What is the primary purpose of using JSON Schema within the Model Context Protocol? a) To define the network communication protocol between clients and servers. b) To specify the data types for HTTP request headers. c) To formally define the structure and constraints of context data, ensuring interoperability. d) To encrypt context data during transmission.

    Answerc) To formally define the structure and constraints of context data, ensuring interoperability. JSON Schema provides a contract for context data, allowing tools to understand and validate the information they exchange.
  2. Which of the following best describes the role of a ContextDescriptor? a) It’s the actual context data payload transmitted from server to client. b) It’s a client-side object used to construct a query for context. c) It’s a metadata object that advertises a server’s capability to provide a specific context type, including its ContextIdentifier and schemaUri. d) It’s a unique identifier for a specific instance of context data.

    Answerc) It's a metadata object that advertises a server's capability to provide a specific context type, including its `ContextIdentifier` and `schemaUri`. This allows clients to discover what context is available and how it's structured.
  3. An MCP server currently provides mcp://myorg/design-tokens/v1.0.0. If the server decides to change a previously required property in this schema to optional, what kind of version bump is generally recommended according to semantic versioning principles for the ContextIdentifier? a) Patch version (e.g., v1.0.1) b) Minor version (e.g., v1.1.0) c) Major version (e.g., v2.0.0) d) No version bump is necessary.

    Answerc) Major version (e.g., `v2.0.0`). Changing a required property to optional is a breaking change for older clients that might still expect that property to be present. It fundamentally alters the contract.

Challenge

Design a “Database Schema Context” and Negotiation Strategy

You are tasked with designing an MCP context type that describes a relational database schema. This context will be used by intelligent query builders, schema migration tools, and data analysis agents.

  1. ContextIdentifier: Propose a suitable ContextIdentifier for this context, including a domain and version.
  2. JSON Schema: Create a JSON Schema (just the properties and required sections are fine, you don’t need the full boilerplate) for this DatabaseSchemaContext. It should include:
    • The database name.
    • A list of tables.
    • For each table:
      • Table name.
      • A list of columns.
      • For each column:
        • Column name.
        • Data type (e.g., VARCHAR, INT, BOOLEAN).
        • Whether it’s nullable.
        • Whether it’s a primary key.
      • An optional list of foreign key relationships (e.g., referencing table, referencing column, target table, target column).
  3. Negotiation Scenario: Describe how an MCP client (e.g., an AI query builder) would use a ContextQuery to request this DatabaseSchemaContext from an MCP server, and how the server would respond. What if the client requests an older, incompatible version?

Summary

This chapter has taken us from the abstract concept of “context” to its concrete definition within the Model Context Protocol. We’ve learned that JSON Schema is the backbone for structuring this context, enabling precision and interoperability. The ContextIdentifier uniquely names a context type, and the ContextDescriptor advertises a server’s capabilities, including the crucial schemaUri. Furthermore, we explored dynamic context negotiation, a powerful mechanism where clients query for specific context types and versions, and servers respond with available matches. Finally, we delved into the critical importance of schema versioning for maintaining compatibility in evolving systems. Mastering these concepts is fundamental to building robust, adaptable, and interoperable intelligent tools with MCP.

📌 TL;DR

  • MCP uses JSON Schema to formally define the structure of context data.
  • ContextIdentifier uniquely names a context type (e.g., mcp://myorg/context/v1.0.0).
  • ContextDescriptor advertises a server’s context capabilities, including the ContextIdentifier and schemaUri.
  • Dynamic negotiation allows clients to query for specific context types/versions, and servers respond with available ContextDescriptors.
  • Schema versioning (preferably semantic) is crucial for managing changes and maintaining backward compatibility.

🧠 Core Flow

  1. Define Schema: Engineer creates a JSON Schema for a specific context type (e.g., TaskItemContext).
  2. Register Descriptor: MCP Server registers a ContextDescriptor with the ContextIdentifier and schemaUri for this context.
  3. Client Query: MCP Client sends a ContextQuery requesting specific ContextIdentifiers.
  4. Server Response: MCP Server matches the query to its registered ContextDescriptors and returns the relevant ones.
  5. Context Exchange: If a match is found, the client can then request the actual context data, which the server provides, conforming to the advertised schema.

🚀 Key Takeaway

Structured schemas and dynamic negotiation are the pillars of interoperability in MCP, allowing intelligent tools to speak a common, evolvable language for understanding their operational environment.