Chapter 3: MCP Protocol Specification Explained
Haiyue
29min
Chapter 3: MCP Protocol Specification Explained
Learning Objectives
- Deep understanding of MCP protocol message formats
- Master request-response patterns and event streams
- Learn protocol versioning and compatibility
- Understand error handling and exceptional situations
- Master protocol extension mechanisms
3.1 MCP Protocol Basics
Protocol Architecture Overview
// MCP protocol is based on JSON-RPC 2.0 with the following features:
interface MCPProtocolFeatures {
// Transport layer support
transport: {
stdio: 'Standard input/output';
websocket: 'WebSocket connection';
http: 'HTTP long polling';
};
// Message format
messageFormat: 'JSON-RPC 2.0';
// Bidirectional communication
bidirectional: true;
// Streaming support
streaming: boolean;
// Protocol version
version: '2024-11-05';
}
// Protocol version definition
const MCP_PROTOCOL_VERSION = '2024-11-05';
// Supported transport methods
enum TransportType {
STDIO = 'stdio',
WEBSOCKET = 'websocket',
HTTP = 'http'
}
JSON-RPC 2.0 Basics
// JSON-RPC 2.0 basic message structure
interface JSONRPCMessage {
jsonrpc: '2.0'; // Protocol version, must be '2.0'
id?: string | number | null; // Message ID, can be omitted for notifications
}
// Request message
interface JSONRPCRequest extends JSONRPCMessage {
method: string; // Method name
params?: any; // Parameters, can be object or array
id: string | number; // Request ID, must exist
}
// Response message
interface JSONRPCResponse extends JSONRPCMessage {
id: string | number | null; // Corresponding request ID
result?: any; // Success result
error?: { // Error information
code: number;
message: string;
data?: any;
};
}
// Notification message
interface JSONRPCNotification extends JSONRPCMessage {
method: string; // Method name
params?: any; // Parameters
// Note: Notification messages do not have an id field
}
3.2 MCP Message Types and Formats
Initialization Flow
// 1. Initialize request (client -> server)
interface InitializeRequest {
jsonrpc: '2.0';
id: string | number;
method: 'initialize';
params: {
protocolVersion: string; // MCP protocol version
capabilities: {
tools?: {
listChanged?: boolean; // Support tool list change notifications
};
resources?: {
subscribe?: boolean; // Support resource subscriptions
listChanged?: boolean; // Support resource list change notifications
};
prompts?: {
listChanged?: boolean; // Support prompt template list change notifications
};
logging?: {}; // Logging support
experimental?: Record<string, any>; // Experimental features
};
clientInfo: {
name: string; // Client name
version: string; // Client version
};
};
}
// 2. Initialize response (server -> client)
interface InitializeResponse {
jsonrpc: '2.0';
id: string | number;
result: {
protocolVersion: string; // Protocol version supported by server
capabilities: {
tools?: {
listChanged?: boolean;
};
resources?: {
subscribe?: boolean;
listChanged?: boolean;
};
prompts?: {
listChanged?: boolean;
};
logging?: {};
experimental?: Record<string, any>;
};
serverInfo: {
name: string; // Server name
version: string; // Server version
};
instructions?: string; // Optional server usage instructions
};
}
// 3. Initialization complete notification (client -> server)
interface InitializedNotification {
jsonrpc: '2.0';
method: 'notifications/initialized';
params?: {};
}
// Initialization flow example
class MCPInitializationFlow {
async performHandshake(): Promise<void> {
// Step 1: Client sends initialization request
const initRequest: InitializeRequest = {
jsonrpc: '2.0',
id: 'init-1',
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true },
logging: {}
},
clientInfo: {
name: 'example-client',
version: '1.0.0'
}
}
};
console.log('Sending initialization request:', JSON.stringify(initRequest, null, 2));
// Step 2: Server responds
const initResponse: InitializeResponse = {
jsonrpc: '2.0',
id: 'init-1',
result: {
protocolVersion: '2024-11-05',
capabilities: {
tools: { listChanged: true },
resources: { subscribe: false, listChanged: true },
prompts: { listChanged: false },
logging: {}
},
serverInfo: {
name: 'example-server',
version: '1.0.0'
},
instructions: 'This is an example MCP server providing file operations and calculation capabilities.'
}
};
console.log('Received initialization response:', JSON.stringify(initResponse, null, 2));
// Step 3: Client sends initialization complete notification
const initializedNotification: InitializedNotification = {
jsonrpc: '2.0',
method: 'notifications/initialized',
params: {}
};
console.log('Sending initialization complete notification:', JSON.stringify(initializedNotification, null, 2));
}
}
Tool-Related Messages
// List tools request
interface ListToolsRequest {
jsonrpc: '2.0';
id: string | number;
method: 'tools/list';
params?: {
cursor?: string; // Pagination cursor
};
}
// List tools response
interface ListToolsResponse {
jsonrpc: '2.0';
id: string | number;
result: {
tools: Array<{
name: string;
description?: string;
inputSchema: {
type: 'object';
properties?: Record<string, any>;
required?: string[];
};
}>;
nextCursor?: string; // Next page cursor
};
}
// Call tool request
interface CallToolRequest {
jsonrpc: '2.0';
id: string | number;
method: 'tools/call';
params: {
name: string;
arguments?: Record<string, any>;
};
}
// Call tool response
interface CallToolResponse {
jsonrpc: '2.0';
id: string | number;
result: {
content: Array<{
type: 'text' | 'image' | 'resource';
text?: string;
data?: string; // Base64 encoded data
mimeType?: string;
uri?: string; // Resource URI
}>;
isError?: boolean; // Indicates if tool execution had an error
};
}
// Tool list changed notification
interface ToolsListChangedNotification {
jsonrpc: '2.0';
method: 'notifications/tools/list_changed';
params?: {};
}
// Tool message handling example
class MCPToolsHandler {
private tools: Map<string, any> = new Map();
// Handle list tools request
async handleListTools(request: ListToolsRequest): Promise<ListToolsResponse> {
const toolsList = Array.from(this.tools.values()).map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}));
return {
jsonrpc: '2.0',
id: request.id,
result: {
tools: toolsList
}
};
}
// Handle tool call request
async handleCallTool(request: CallToolRequest): Promise<CallToolResponse> {
const { name, arguments: args = {} } = request.params;
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await tool.handler(args);
return {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
}
]
}
};
} catch (error) {
return {
jsonrpc: '2.0',
id: request.id,
result: {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
],
isError: true
}
};
}
}
// Send tool list changed notification
notifyToolsListChanged(): ToolsListChangedNotification {
return {
jsonrpc: '2.0',
method: 'notifications/tools/list_changed',
params: {}
};
}
}
Resource-Related Messages
// List resources request
interface ListResourcesRequest {
jsonrpc: '2.0';
id: string | number;
method: 'resources/list';
params?: {
cursor?: string;
};
}
// List resources response
interface ListResourcesResponse {
jsonrpc: '2.0';
id: string | number;
result: {
resources: Array<{
uri: string;
name: string;
description?: string;
mimeType?: string;
}>;
nextCursor?: string;
};
}
// Read resource request
interface ReadResourceRequest {
jsonrpc: '2.0';
id: string | number;
method: 'resources/read';
params: {
uri: string;
};
}
// Read resource response
interface ReadResourceResponse {
jsonrpc: '2.0';
id: string | number;
result: {
contents: Array<{
uri: string;
mimeType?: string;
text?: string;
blob?: string; // Base64 encoded binary data
}>;
};
}
// Subscribe resource request
interface SubscribeResourceRequest {
jsonrpc: '2.0';
id: string | number;
method: 'resources/subscribe';
params: {
uri: string;
};
}
// Unsubscribe resource request
interface UnsubscribeResourceRequest {
jsonrpc: '2.0';
id: string | number;
method: 'resources/unsubscribe';
params: {
uri: string;
};
}
// Resource updated notification
interface ResourceUpdatedNotification {
jsonrpc: '2.0';
method: 'notifications/resources/updated';
params: {
uri: string;
};
}
// Resource list changed notification
interface ResourcesListChangedNotification {
jsonrpc: '2.0';
method: 'notifications/resources/list_changed';
params?: {};
}
// Resource handling example
class MCPResourcesHandler {
private resources: Map<string, any> = new Map();
private subscribers: Map<string, Set<string>> = new Map();
// Handle resource subscription
async handleSubscribe(request: SubscribeResourceRequest): Promise<any> {
const { uri } = request.params;
if (!this.resources.has(uri)) {
throw new Error(`Resource not found: ${uri}`);
}
// Record subscription
if (!this.subscribers.has(uri)) {
this.subscribers.set(uri, new Set());
}
this.subscribers.get(uri)!.add(request.id.toString());
return {
jsonrpc: '2.0',
id: request.id,
result: {}
};
}
// Notify resource update
notifyResourceUpdate(uri: string): ResourceUpdatedNotification[] {
const subscribers = this.subscribers.get(uri);
if (!subscribers || subscribers.size === 0) {
return [];
}
return Array.from(subscribers).map(subscriberId => ({
jsonrpc: '2.0',
method: 'notifications/resources/updated',
params: { uri }
}));
}
}
Prompt Template-Related Messages
// List prompts request
interface ListPromptsRequest {
jsonrpc: '2.0';
id: string | number;
method: 'prompts/list';
params?: {
cursor?: string;
};
}
// List prompts response
interface ListPromptsResponse {
jsonrpc: '2.0';
id: string | number;
result: {
prompts: Array<{
name: string;
description?: string;
arguments?: Array<{
name: string;
description: string;
required?: boolean;
}>;
}>;
nextCursor?: string;
};
}
// Get prompt request
interface GetPromptRequest {
jsonrpc: '2.0';
id: string | number;
method: 'prompts/get';
params: {
name: string;
arguments?: Record<string, any>;
};
}
// Get prompt response
interface GetPromptResponse {
jsonrpc: '2.0';
id: string | number;
result: {
description?: string;
messages: Array<{
role: 'user' | 'assistant' | 'system';
content: {
type: 'text' | 'image';
text?: string;
data?: string;
mimeType?: string;
};
}>;
};
}
// Prompt list changed notification
interface PromptsListChangedNotification {
jsonrpc: '2.0';
method: 'notifications/prompts/list_changed';
params?: {};
}
3.3 Error Handling and Status Codes
MCP Error Codes
// Standard error codes defined by MCP
enum MCPErrorCode {
// JSON-RPC standard errors
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
// MCP-specific errors
INVALID_TOOL = -32000,
TOOL_EXECUTION_ERROR = -32001,
RESOURCE_NOT_FOUND = -32002,
RESOURCE_ACCESS_DENIED = -32003,
PROMPT_NOT_FOUND = -32004,
INVALID_PROMPT_ARGS = -32005
}
// Error response format
interface MCPErrorResponse {
jsonrpc: '2.0';
id: string | number | null;
error: {
code: MCPErrorCode;
message: string;
data?: {
type?: string;
description?: string;
details?: any;
};
};
}
// Error handling utility class
class MCPErrorHandler {
// Create standard error response
static createErrorResponse(
id: string | number | null,
code: MCPErrorCode,
message: string,
data?: any
): MCPErrorResponse {
return {
jsonrpc: '2.0',
id,
error: {
code,
message,
data
}
};
}
// Create tool error
static createToolError(
id: string | number,
toolName: string,
error: Error
): MCPErrorResponse {
return this.createErrorResponse(
id,
MCPErrorCode.TOOL_EXECUTION_ERROR,
`Tool execution failed: ${toolName}`,
{
type: 'tool_error',
description: error.message,
details: {
toolName,
originalError: error.stack
}
}
);
}
// Create resource error
static createResourceError(
id: string | number,
uri: string,
message: string
): MCPErrorResponse {
return this.createErrorResponse(
id,
MCPErrorCode.RESOURCE_NOT_FOUND,
message,
{
type: 'resource_error',
uri
}
);
}
}
// Error handling example
class MCPServerErrorHandling {
async handleRequest(request: any): Promise<any> {
try {
// Validate request format
if (!request.jsonrpc || request.jsonrpc !== '2.0') {
return MCPErrorHandler.createErrorResponse(
request.id || null,
MCPErrorCode.INVALID_REQUEST,
'Invalid JSON-RPC version'
);
}
if (!request.method) {
return MCPErrorHandler.createErrorResponse(
request.id || null,
MCPErrorCode.INVALID_REQUEST,
'Missing method'
);
}
// Route to specific handler method
switch (request.method) {
case 'tools/call':
return await this.handleToolCall(request);
case 'resources/read':
return await this.handleResourceRead(request);
default:
return MCPErrorHandler.createErrorResponse(
request.id,
MCPErrorCode.METHOD_NOT_FOUND,
`Method not found: ${request.method}`
);
}
} catch (error) {
return MCPErrorHandler.createErrorResponse(
request.id || null,
MCPErrorCode.INTERNAL_ERROR,
'Internal server error',
{
type: 'internal_error',
description: error.message
}
);
}
}
private async handleToolCall(request: any): Promise<any> {
try {
// Tool call logic
return { /* success response */ };
} catch (error) {
return MCPErrorHandler.createToolError(
request.id,
request.params?.name || 'unknown',
error
);
}
}
private async handleResourceRead(request: any): Promise<any> {
const uri = request.params?.uri;
if (!uri) {
return MCPErrorHandler.createErrorResponse(
request.id,
MCPErrorCode.INVALID_PARAMS,
'Missing required parameter: uri'
);
}
// Resource read logic
return { /* success response */ };
}
}
3.4 Protocol Versioning and Compatibility
Version Negotiation
// Version compatibility check
class MCPVersionManager {
private readonly supportedVersions = ['2024-11-05', '2024-10-07'];
private readonly currentVersion = '2024-11-05';
// Check version compatibility
isVersionSupported(clientVersion: string): boolean {
return this.supportedVersions.includes(clientVersion);
}
// Get compatible version
getNegotiatedVersion(clientVersion: string): string | null {
if (this.isVersionSupported(clientVersion)) {
return clientVersion;
}
// Return highest supported version
return this.supportedVersions[0];
}
// Version feature check
hasFeature(version: string, feature: string): boolean {
const features = {
'2024-11-05': [
'streaming_support',
'resource_subscriptions',
'tool_list_changes',
'enhanced_error_handling'
],
'2024-10-07': [
'basic_tools',
'basic_resources',
'basic_prompts'
]
};
return features[version]?.includes(feature) || false;
}
// Version negotiation during initialization
negotiateVersion(initRequest: InitializeRequest): {
version: string;
capabilities: any;
errors?: string[];
} {
const clientVersion = initRequest.params.protocolVersion;
const negotiatedVersion = this.getNegotiatedVersion(clientVersion);
const errors: string[] = [];
if (!negotiatedVersion) {
errors.push(`Unsupported protocol version: ${clientVersion}`);
return { version: this.currentVersion, capabilities: {}, errors };
}
if (negotiatedVersion !== clientVersion) {
errors.push(`Version downgraded from ${clientVersion} to ${negotiatedVersion}`);
}
// Adjust capabilities based on negotiated version
const capabilities = this.getCapabilitiesForVersion(
negotiatedVersion,
initRequest.params.capabilities
);
return { version: negotiatedVersion, capabilities, errors };
}
private getCapabilitiesForVersion(version: string, clientCapabilities: any): any {
const baseCapabilities = {
tools: {},
resources: {},
prompts: {}
};
if (this.hasFeature(version, 'tool_list_changes')) {
baseCapabilities.tools = { listChanged: true };
}
if (this.hasFeature(version, 'resource_subscriptions')) {
baseCapabilities.resources = { subscribe: true, listChanged: true };
}
if (this.hasFeature(version, 'streaming_support')) {
baseCapabilities.experimental = {
streaming: true
};
}
return baseCapabilities;
}
}
Backward Compatibility Handling
// Compatibility adapter
class MCPCompatibilityAdapter {
// Adapt message format for different versions
adaptMessage(message: any, fromVersion: string, toVersion: string): any {
if (fromVersion === toVersion) {
return message;
}
// Adapt from old version to new version
if (fromVersion === '2024-10-07' && toVersion === '2024-11-05') {
return this.adaptFrom20241007To20241105(message);
}
// Adapt from new version to old version
if (fromVersion === '2024-11-05' && toVersion === '2024-10-07') {
return this.adaptFrom20241105To20241007(message);
}
return message;
}
private adaptFrom20241007To20241105(message: any): any {
// Add fields supported by new version
if (message.method === 'tools/call' && message.result) {
if (!message.result.content) {
message.result.content = [
{
type: 'text',
text: message.result.text || JSON.stringify(message.result)
}
];
delete message.result.text;
}
}
return message;
}
private adaptFrom20241105To20241007(message: any): any {
// Remove fields not supported by old version
if (message.method === 'tools/call' && message.result?.content) {
const firstContent = message.result.content[0];
if (firstContent?.type === 'text') {
message.result.text = firstContent.text;
delete message.result.content;
}
}
return message;
}
}
3.5 Streaming and Batch Operations
Streaming Support
// Streaming response interface
interface StreamingResponse {
jsonrpc: '2.0';
id: string | number;
result?: {
stream: true;
streamId: string;
};
error?: any;
}
// Stream chunk
interface StreamChunk {
jsonrpc: '2.0';
method: 'notifications/stream/chunk';
params: {
streamId: string;
chunk: {
type: 'text' | 'image' | 'data';
content: any;
final?: boolean; // Whether this is the last chunk
};
};
}
// Stream manager
class MCPStreamManager {
private streams: Map<string, any> = new Map();
// Create streaming response
createStream(requestId: string | number): { response: StreamingResponse; streamId: string } {
const streamId = `stream_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.streams.set(streamId, {
requestId,
startTime: Date.now(),
chunks: []
});
return {
response: {
jsonrpc: '2.0',
id: requestId,
result: {
stream: true,
streamId
}
},
streamId
};
}
// Send stream chunk
sendChunk(streamId: string, content: any, type: 'text' | 'image' | 'data' = 'text', final: boolean = false): StreamChunk {
const stream = this.streams.get(streamId);
if (!stream) {
throw new Error(`Stream not found: ${streamId}`);
}
const chunk: StreamChunk = {
jsonrpc: '2.0',
method: 'notifications/stream/chunk',
params: {
streamId,
chunk: {
type,
content,
final
}
}
};
stream.chunks.push(chunk);
if (final) {
this.streams.delete(streamId);
}
return chunk;
}
// Streaming tool call example
async handleStreamingToolCall(request: CallToolRequest): Promise<StreamingResponse> {
const { response, streamId } = this.createStream(request.id);
// Execute tool asynchronously and return results as stream
this.executeToolWithStreaming(request.params.name, request.params.arguments || {}, streamId);
return response;
}
private async executeToolWithStreaming(toolName: string, args: any, streamId: string): Promise<void> {
try {
// Simulate long-running tool
const results = await this.simulateLongRunningTool(toolName, args);
for (let i = 0; i < results.length; i++) {
const isLast = i === results.length - 1;
this.sendChunk(streamId, results[i], 'text', isLast);
// Simulate processing delay
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
this.sendChunk(streamId, { error: error.message }, 'text', true);
}
}
private async simulateLongRunningTool(toolName: string, args: any): Promise<string[]> {
// Simulate tool execution, returning results in chunks
return [
'Step 1: Initializing...',
'Step 2: Processing data...',
'Step 3: Computing results...',
'Step 4: Finalizing...',
'Completed successfully!'
];
}
}
Batch Operations Support
// Batch request
interface BatchRequest {
jsonrpc: '2.0';
id: string | number;
method: 'batch';
params: {
requests: Array<{
method: string;
params?: any;
id: string | number;
}>;
};
}
// Batch response
interface BatchResponse {
jsonrpc: '2.0';
id: string | number;
result: {
responses: Array<{
id: string | number;
result?: any;
error?: any;
}>;
};
}
// Batch operation handler
class MCPBatchHandler {
// Handle batch request
async handleBatch(request: BatchRequest): Promise<BatchResponse> {
const responses = [];
for (const subRequest of request.params.requests) {
try {
const response = await this.handleSingleRequest({
jsonrpc: '2.0',
id: subRequest.id,
method: subRequest.method,
params: subRequest.params
});
responses.push({
id: subRequest.id,
result: response.result,
error: response.error
});
} catch (error) {
responses.push({
id: subRequest.id,
error: {
code: MCPErrorCode.INTERNAL_ERROR,
message: error.message
}
});
}
}
return {
jsonrpc: '2.0',
id: request.id,
result: {
responses
}
};
}
private async handleSingleRequest(request: any): Promise<any> {
// Single request handling logic
return { result: { success: true } };
}
}
3.6 Protocol Extension Mechanism
Experimental Feature Support
// Experimental features definition
interface ExperimentalFeatures {
streaming?: {
supported: boolean;
maxStreams?: number;
};
batch?: {
supported: boolean;
maxBatchSize?: number;
};
customTransports?: {
supported: boolean;
transports: string[];
};
advancedAuth?: {
supported: boolean;
methods: string[];
};
}
// Extension capability negotiation
class MCPExtensionManager {
private supportedExtensions: Map<string, any> = new Map();
registerExtension(name: string, definition: any): void {
this.supportedExtensions.set(name, definition);
}
// Handle extension feature initialization
negotiateExtensions(clientCapabilities: any): ExperimentalFeatures {
const experimental: ExperimentalFeatures = {};
// Streaming extension
if (clientCapabilities.experimental?.streaming && this.supportedExtensions.has('streaming')) {
experimental.streaming = {
supported: true,
maxStreams: 10
};
}
// Batch operations extension
if (clientCapabilities.experimental?.batch && this.supportedExtensions.has('batch')) {
experimental.batch = {
supported: true,
maxBatchSize: 100
};
}
return experimental;
}
// Handle extension messages
async handleExtensionMessage(method: string, params: any): Promise<any> {
const [namespace, action] = method.split('/');
if (namespace === 'experimental') {
return await this.handleExperimentalMessage(action, params);
}
throw new Error(`Unknown extension namespace: ${namespace}`);
}
private async handleExperimentalMessage(action: string, params: any): Promise<any> {
switch (action) {
case 'stream_start':
return await this.handleStreamStart(params);
case 'batch_execute':
return await this.handleBatchExecute(params);
default:
throw new Error(`Unknown experimental action: ${action}`);
}
}
private async handleStreamStart(params: any): Promise<any> {
// Implement stream start logic
return { streamId: 'stream_123', started: true };
}
private async handleBatchExecute(params: any): Promise<any> {
// Implement batch execution logic
return { batchId: 'batch_456', status: 'processing' };
}
}
Summary
Through this chapter, we’ve gained a deep understanding of the MCP protocol technical specifications:
- Protocol Basics: Bidirectional communication protocol based on JSON-RPC 2.0
- Message Types: Message formats for initialization, tools, resources, and prompt templates
- Error Handling: Standardized error codes and handling mechanisms
- Version Control: Version negotiation and backward compatibility
- Advanced Features: Streaming, batch operations, and protocol extensions
These specifications lay a solid foundation for developing robust and reliable MCP Servers. In the next sections, we will learn how to develop specific tool functionalities.