Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/Core/JsonRpcErrorCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Azure.DataApiBuilder.Mcp.Core
{
/// <summary>
/// JSON-RPC 2.0 standard error codes used by the MCP stdio server.
/// These values come from the JSON-RPC 2.0 specification and are shared
/// so they are not hard-coded throughout the codebase.
/// </summary>
internal static class JsonRpcErrorCodes
{
/// <summary>
/// Invalid JSON was received by the server.
/// An error occurred on the server while parsing the JSON text.
/// </summary>
public const int PARSEERROR = -32700;

/// <summary>
/// The JSON sent is not a valid Request object.
/// </summary>
public const int INVALIDREQUEST = -32600;

/// <summary>
/// The method does not exist / is not available.
/// </summary>
public const int METHODNOTFOUND = -32601;

/// <summary>
/// Invalid method parameter(s).
/// </summary>
public const int INVALIDPARAMS = -32602;

/// <summary>
/// Internal JSON-RPC error.
/// </summary>
public const int INTERNALERROR = -32603;
}
}
50 changes: 20 additions & 30 deletions src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public async Task RunAsync(CancellationToken cancellationToken)

if (line.Length > MAX_LINE_LENGTH)
{
WriteError(id: null, code: -32600, message: "Request too large");
WriteError(id: null, code: JsonRpcErrorCodes.INVALIDREQUEST, message: "Request too large");
continue;
}

Expand All @@ -77,13 +77,13 @@ public async Task RunAsync(CancellationToken cancellationToken)
catch (JsonException jsonEx)
{
Console.Error.WriteLine($"[MCP DEBUG] JSON parse error: {jsonEx.Message}");
WriteError(id: null, code: -32700, message: "Parse error");
WriteError(id: null, code: JsonRpcErrorCodes.PARSEERROR, message: "Parse error");
continue;
}
catch (Exception ex)
{
Console.Error.WriteLine($"[MCP DEBUG] Unexpected error parsing request: {ex.Message}");
WriteError(id: null, code: -32603, message: "Internal error");
WriteError(id: null, code: JsonRpcErrorCodes.INTERNALERROR, message: "Internal error");
continue;
}

Expand All @@ -99,7 +99,7 @@ public async Task RunAsync(CancellationToken cancellationToken)

if (!root.TryGetProperty("method", out JsonElement methodEl))
{
WriteError(id, -32600, "Invalid Request");
WriteError(id, JsonRpcErrorCodes.INVALIDREQUEST, "Invalid Request");
continue;
}

Expand Down Expand Up @@ -133,13 +133,13 @@ public async Task RunAsync(CancellationToken cancellationToken)
return;

default:
WriteError(id, -32601, $"Method not found: {method}");
WriteError(id, JsonRpcErrorCodes.METHODNOTFOUND, $"Method not found: {method}");
break;
}
}
catch (Exception)
{
WriteError(id, -32603, "Internal error");
WriteError(id, JsonRpcErrorCodes.INTERNALERROR, "Internal error");
}
}
}
Expand All @@ -158,32 +158,22 @@ public async Task RunAsync(CancellationToken cancellationToken)
/// </remarks>
private void HandleInitialize(JsonElement? id)
{
// Extract the actual id value from the request
object? requestId = id.HasValue ? GetIdValue(id.Value) : null;

// Create the initialize response
var response = new
var result = new
{
jsonrpc = "2.0",
id = requestId,
result = new
protocolVersion = _protocolVersion,
capabilities = new
{
protocolVersion = _protocolVersion,
capabilities = new
{
tools = new { listChanged = true },
logging = new { }
},
serverInfo = new
{
name = "Data API Builder",
version = "1.0.0"
}
tools = new { listChanged = true },
logging = new { }
},
serverInfo = new
{
name = "SQL MCP Server",
version = "1.0.0"
}
};

string json = JsonSerializer.Serialize(response);
Console.Out.WriteLine(json);
WriteResult(id, result);
}

/// <summary>
Expand Down Expand Up @@ -225,7 +215,7 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel
{
if (!root.TryGetProperty("params", out JsonElement @params) || @params.ValueKind != JsonValueKind.Object)
{
WriteError(id, -32602, "Missing params");
WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, "Missing params");
return;
}

Expand All @@ -247,14 +237,14 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel
if (string.IsNullOrWhiteSpace(toolName))
{
Console.Error.WriteLine("[MCP DEBUG] callTool → missing tool name.");
WriteError(id, -32602, "Missing tool name");
WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, "Missing tool name");
return;
}

if (!_toolRegistry.TryGetTool(toolName!, out IMcpTool? tool) || tool is null)
{
Console.Error.WriteLine($"[MCP DEBUG] callTool → tool not found: {toolName}");
WriteError(id, -32602, $"Tool not found: {toolName}");
WriteError(id, JsonRpcErrorCodes.INVALIDPARAMS, $"Tool not found: {toolName}");
return;
}

Expand Down
3 changes: 3 additions & 0 deletions src/Service/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.CommandLine;
using System.CommandLine.Parsing;
Expand Down
23 changes: 14 additions & 9 deletions src/Service/Utilities/McpStdioHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand All @@ -15,9 +19,9 @@ internal static class McpStdioHelper
/// Determines if MCP stdio mode should be run based on command line arguments.
/// </summary>
/// <param name="args"> The command line arguments.</param>
/// <param name="mcpRole"> The role for MCP stdio mode, if specified.</param>
/// <returns></returns>
public static bool ShouldRunMcpStdio(string[] args, out string? mcpRole)
/// <param name="mcpRole"> The role for MCP stdio mode. When this method returns true, the role is guaranteed to be non-null.</param>
/// <returns>True when MCP stdio mode should be enabled; otherwise false.</returns>
public static bool ShouldRunMcpStdio(string[] args, [NotNullWhen(true)] out string? mcpRole)
{
mcpRole = null;

Expand All @@ -43,6 +47,11 @@ public static bool ShouldRunMcpStdio(string[] args, out string? mcpRole)
}
}

// Ensure that when MCP stdio is enabled, mcpRole is always non-null.
// This matches the NotNullWhen(true) contract and avoids nullable warnings
// for callers while still allowing an implicit default when no role is provided.
mcpRole ??= "anonymous";

return true;
}

Expand Down Expand Up @@ -76,17 +85,13 @@ public static bool RunMcpStdioHost(IHost host)

foreach (Mcp.Model.IMcpTool tool in tools)
{
_ = tool.GetToolMetadata();
registry.RegisterTool(tool);
}

IServiceScopeFactory scopeFactory =
host.Services.GetRequiredService<IServiceScopeFactory>();
using IServiceScope scope = scopeFactory.CreateScope();
IHostApplicationLifetime lifetime =
scope.ServiceProvider.GetRequiredService<IHostApplicationLifetime>();
host.Services.GetRequiredService<IHostApplicationLifetime>();
Mcp.Core.IMcpStdioServer stdio =
scope.ServiceProvider.GetRequiredService<Mcp.Core.IMcpStdioServer>();
host.Services.GetRequiredService<Mcp.Core.IMcpStdioServer>();

stdio.RunAsync(lifetime.ApplicationStopping).GetAwaiter().GetResult();
host.StopAsync().GetAwaiter().GetResult();
Expand Down