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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please consider renaming this to something like McpStdioJsonRpcErrorCodes or something to reflect its related to only MCP. also, we should move this inside "../src/Azure.DataApiBuilder.Mcp/Model" where we already have other error codes and enums, specifically for MCP.

{
/// <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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please consider using existing format "SCREAMING_SNAKE_CASE or UPPER_SNAKE_CASE" for all constant variables. refer- ../src/Config/HealthCheck/HealthCheckConstants.cs


/// <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"
Comment on lines +171 to +172
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please also update the name and version in ../src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs? for consistency, try to refactor it and use the same name and version through a configurable value or use the DAB version as the version will keep changing.
tagging @Aniruddh25 to confirm if we can use the DAB version itself or have a separate versioning for MCP server.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not a different product, we can use the already configurable DAB version, please use ProductInfo.GetVersion function to get this version.

}
};

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should mention mcpRole defaults to anonymous

/// <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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessary? can we instead just initialize as mcpRole = "anonymous"; instead of mcpRole = null; in the beginning at line: 26?


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