using System.Collections;
using System.Reflection;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Telemetry;
using Azure.DataApiBuilder.Mcp.Model;
using Azure.DataApiBuilder.Mcp.Telemetry;
using Azure.DataApiBuilder.Mcp.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol;
namespace Azure.DataApiBuilder.Mcp.Core
{
///
/// MCP stdio server:
/// - Reads JSON-RPC requests (initialize, listTools, callTool) from STDIN
/// - Writes ONLY MCP JSON responses to STDOUT
/// - Writes diagnostics to STDERR (so STDOUT remains “pure MCP”)
///
public class McpStdioServer : IMcpStdioServer
{
private readonly McpToolRegistry _toolRegistry;
private readonly IServiceProvider _serviceProvider;
private readonly McpStdoutWriter _stdoutWriter;
private readonly string _protocolVersion;
private const int MAX_LINE_LENGTH = 1024 * 1024; // 1 MB limit for incoming JSON-RPC requests
// Omit null-valued properties (e.g. SDK ContentBlock.Annotations, ContentBlock._meta) so
// strict MCP clients never see explicit JSON nulls for optional metadata fields.
private static readonly JsonSerializerOptions _jsonOptions = new()
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
public McpStdioServer(McpToolRegistry toolRegistry, IServiceProvider serviceProvider)
{
_toolRegistry = toolRegistry ?? throw new ArgumentNullException(nameof(toolRegistry));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
// Resolve the shared stdout writer so JSON-RPC responses and
// notifications/message frames are serialized through one lock.
// Falls back to a fresh instance if DI didn't register one (defensive).
_stdoutWriter = _serviceProvider.GetService() ?? new McpStdoutWriter();
// Allow protocol version to be configured via IConfiguration, using centralized defaults.
IConfiguration? configuration = _serviceProvider.GetService();
_protocolVersion = McpProtocolDefaults.ResolveProtocolVersion(configuration);
}
///
/// Runs the MCP stdio server loop, reading JSON-RPC requests from STDIN and writing MCP JSON responses to STDOUT.
///
/// Token to signal cancellation of the server loop.
/// A task representing the asynchronous operation.
public async Task RunAsync(CancellationToken cancellationToken)
{
// Use UTF-8 WITHOUT BOM for stdin. Stdout is owned by McpStdoutWriter,
// which serializes all writes from McpStdioServer and the MCP logging
// pipeline so JSON-RPC frames cannot interleave at the byte level.
UTF8Encoding utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
using Stream stdin = Console.OpenStandardInput();
using StreamReader reader = new(stdin, utf8NoBom);
while (!cancellationToken.IsCancellationRequested)
{
string? line = await reader.ReadLineAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (line.Length > MAX_LINE_LENGTH)
{
WriteError(id: null, code: McpStdioJsonRpcErrorCodes.INVALID_REQUEST, message: "Request too large");
continue;
}
JsonDocument doc;
try
{
doc = JsonDocument.Parse(line);
}
catch (JsonException)
{
WriteError(id: null, code: McpStdioJsonRpcErrorCodes.PARSE_ERROR, message: "Parse error");
continue;
}
catch (Exception)
{
WriteError(id: null, code: McpStdioJsonRpcErrorCodes.INTERNAL_ERROR, message: "Internal error");
continue;
}
using (doc)
{
JsonElement root = doc.RootElement;
JsonElement? id = null;
if (root.TryGetProperty("id", out JsonElement idEl))
{
id = idEl; // preserve original type (string or number)
}
if (!root.TryGetProperty("method", out JsonElement methodEl))
{
WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_REQUEST, "Invalid Request");
continue;
}
string method = methodEl.GetString() ?? string.Empty;
try
{
switch (method)
{
case "initialize":
HandleInitialize(id, root);
break;
case "notifications/initialized":
break;
case "tools/list":
HandleListTools(id);
break;
case "tools/call":
await HandleCallToolAsync(id, root, cancellationToken);
break;
case "ping":
WriteResult(id, new { ok = true });
break;
case "logging/setLevel":
HandleSetLogLevel(id, root);
break;
case "shutdown":
WriteResult(id, new { ok = true });
return;
default:
WriteError(id, McpStdioJsonRpcErrorCodes.METHOD_NOT_FOUND, $"Method not found: {method}");
break;
}
}
catch (Exception)
{
WriteError(id, McpStdioJsonRpcErrorCodes.INTERNAL_ERROR, "Internal error");
}
}
}
}
///
/// Handles the "initialize" JSON-RPC method by sending the MCP protocol version, server capabilities, and server info to the client.
///
///
/// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request.
///
/// The incoming initialize request payload.
///
/// This method constructs and writes the MCP "initialize" response to STDOUT. It negotiates the response protocol version from the
/// server-supported version and client-requested version, and includes supported capabilities and server information. No notifications
/// are sent here; the server waits for the client to send "notifications/initialized" before sending any notifications.
///
private void HandleInitialize(JsonElement? id, JsonElement root)
{
string? clientRequestedProtocolVersion = GetClientProtocolVersion(root);
string negotiatedProtocolVersion =
McpProtocolDefaults.ResolveInitializeResponseProtocolVersion(_protocolVersion, clientRequestedProtocolVersion);
// Get the description from runtime config if available
string? description = null;
RuntimeConfigProvider? runtimeConfigProvider = _serviceProvider.GetService();
if (runtimeConfigProvider != null)
{
try
{
RuntimeConfig runtimeConfig = runtimeConfigProvider.GetConfig();
description = runtimeConfig.Runtime?.Mcp?.Description;
}
catch (Exception)
{
// Rethrow to avoid masking configuration errors
throw;
}
}
bool shouldUseServerInfoDescription = McpProtocolDefaults.ShouldUseServerInfoDescription(negotiatedProtocolVersion);
// Create the initialize response - only include description/instructions if non-empty
object result;
if (!string.IsNullOrWhiteSpace(description) && shouldUseServerInfoDescription)
{
result = new
{
protocolVersion = negotiatedProtocolVersion,
capabilities = new
{
tools = new { listChanged = true },
logging = new { }
},
serverInfo = new
{
name = McpProtocolDefaults.MCP_SERVER_NAME,
version = McpProtocolDefaults.MCP_SERVER_VERSION,
description = description
}
};
}
else if (!string.IsNullOrWhiteSpace(description))
{
result = new
{
protocolVersion = negotiatedProtocolVersion,
capabilities = new
{
tools = new { listChanged = true },
logging = new { }
},
serverInfo = new
{
name = McpProtocolDefaults.MCP_SERVER_NAME,
version = McpProtocolDefaults.MCP_SERVER_VERSION
},
instructions = description
};
}
else
{
result = new
{
protocolVersion = negotiatedProtocolVersion,
capabilities = new
{
tools = new { listChanged = true },
logging = new { }
},
serverInfo = new
{
name = McpProtocolDefaults.MCP_SERVER_NAME,
version = McpProtocolDefaults.MCP_SERVER_VERSION
}
};
}
WriteResult(id, result);
}
private static string? GetClientProtocolVersion(JsonElement root)
{
if (!root.TryGetProperty("params", out JsonElement paramsElement) || paramsElement.ValueKind != JsonValueKind.Object)
{
return null;
}
if (!paramsElement.TryGetProperty("protocolVersion", out JsonElement protocolVersionElement) ||
protocolVersionElement.ValueKind != JsonValueKind.String)
{
return null;
}
return protocolVersionElement.GetString();
}
///
/// Handles the "tools/list" JSON-RPC method by sending the list of available tools to the client.
///
///
/// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request.
///
private void HandleListTools(JsonElement? id)
{
List