Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.DevTools.Cli.Constants;

/// <summary>
/// JSON-RPC 2.0 specification constants
/// </summary>
public static class JsonRpcConstants
{
/// <summary>
/// JSON-RPC version string
/// </summary>
public const string Version = "2.0";

/// <summary>
/// JSON-RPC error codes per JSON-RPC 2.0 specification
/// See: https://www.jsonrpc.org/specification#error_object
/// </summary>
public static class ErrorCodes
{
/// <summary>
/// Invalid Request (-32600) - The JSON sent is not a valid Request object
/// </summary>
public const int InvalidRequest = -32600;

/// <summary>
/// Method not found (-32601) - The method does not exist / is not available
/// </summary>
public const int MethodNotFound = -32601;

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

/// <summary>
/// Internal error (-32603) - Internal JSON-RPC error
/// </summary>
public const int InternalError = -32603;
}

/// <summary>
/// HTTP status codes for MCP protocol
/// </summary>
public static class HttpStatusCodes
{
/// <summary>
/// Accepted (202) - Used for MCP notifications per Streamable HTTP spec
/// Notifications do not expect a response body
/// </summary>
public const int Accepted = 202;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.DevTools.MockToolingServer.Constants;

/// <summary>
/// JSON-RPC 2.0 specification constants
/// </summary>
public static class JsonRpcConstants
{
/// <summary>
/// JSON-RPC version string
/// </summary>
public const string Version = "2.0";

/// <summary>
/// JSON-RPC error codes per JSON-RPC 2.0 specification
/// See: https://www.jsonrpc.org/specification#error_object
/// </summary>
public static class ErrorCodes
{
/// <summary>
/// Invalid Request (-32600) - The JSON sent is not a valid Request object
/// </summary>
public const int InvalidRequest = -32600;

/// <summary>
/// Method not found (-32601) - The method does not exist / is not available
/// </summary>
public const int MethodNotFound = -32601;

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

/// <summary>
/// Internal error (-32603) - Internal JSON-RPC error
/// </summary>
public const int InternalError = -32603;
}

/// <summary>
/// HTTP status codes for MCP protocol
/// </summary>
public static class HttpStatusCodes
{
/// <summary>
/// Accepted (202) - Used for MCP notifications per Streamable HTTP spec
/// Notifications do not expect a response body
/// </summary>
public const int Accepted = 202;
}
}
109 changes: 96 additions & 13 deletions src/Microsoft.Agents.A365.DevTools.MockToolingServer/Server.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.MockToolingServer.Constants;

namespace Microsoft.Agents.A365.DevTools.MockToolingServer;

public static class Server
Expand Down Expand Up @@ -87,11 +89,13 @@ public static async Task Start(string[] args)
// JSON-RPC over HTTP for mock tools at /mcp-mock
app.MapPost("/agents/servers/{mcpServerName}", async (string mcpServerName, HttpRequest httpRequest, IMockToolExecutor executor, ILogger<Program> log) =>
{
// Declare idValue outside try block so catch handler can preserve original request ID.
// This ensures error responses include the correct ID from the client's request.
object? idValue = null;
try
{
using var doc = await JsonDocument.ParseAsync(httpRequest.Body);
var root = doc.RootElement;
object? idValue = null;
if (root.TryGetProperty("id", out var idProp))
{
if (idProp.ValueKind == JsonValueKind.Number)
Expand All @@ -108,6 +112,8 @@ public static async Task Start(string[] args)
}
}

// Validate that 'method' field exists and is a string (JSON-RPC 2.0 requirement).
// All subsequent code can safely assume 'method' is non-null after this check.
if (!root.TryGetProperty("method", out var methodProp) || methodProp.ValueKind != JsonValueKind.String)
{
return Results.BadRequest(new { error = "Invalid or missing 'method' property." });
Expand Down Expand Up @@ -145,17 +151,35 @@ public static async Task Start(string[] args)
},
instructions = "Optional instructions for the client"
};
return Results.Json(new { jsonrpc = "2.0", id = idValue, result = initializeResult });
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = initializeResult });
}
if (string.Equals(method, "logging/setLevel", StringComparison.OrdinalIgnoreCase))
{
// Acknowledge but do nothing
return Results.Json(new { jsonrpc = "2.0", id = idValue, result = new { } });
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = new { } });
}
if (string.Equals(method, "tools/list", StringComparison.OrdinalIgnoreCase))
{
var listResult = await executor.ListToolsAsync(mcpServerName);
return Results.Json(new { jsonrpc = "2.0", id = idValue, result = listResult });
try
{
var listResult = await executor.ListToolsAsync(mcpServerName);
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = listResult });
}
catch (ArgumentException ex)
{
// Unknown MCP server name - return JSON-RPC error (consistent with tools/call)
log.LogWarning(ex, "No mock tool store for '{McpServerName}' - returning error", mcpServerName);
return Results.Json(new
{
jsonrpc = JsonRpcConstants.Version,
id = idValue,
error = new
{
code = JsonRpcConstants.ErrorCodes.InvalidParams,
message = $"MCP server '{mcpServerName}' not found"
}
});
}
}
if (string.Equals(method, "tools/call", StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -187,22 +211,81 @@ public static async Task Start(string[] args)
argsDict[prop.Name] = converted;
}
}
var callResult = await executor.CallToolAsync(mcpServerName, name, argsDict!);
// Detect error shape
var errorProp = callResult.GetType().GetProperty("error");
if (errorProp != null)
try
{
return Results.Json(new { jsonrpc = "2.0", id = idValue, error = errorProp.GetValue(callResult) });
var callResult = await executor.CallToolAsync(mcpServerName, name, argsDict!);
// Detect error shape
var errorProp = callResult.GetType().GetProperty("error");
if (errorProp != null)
{
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, error = errorProp.GetValue(callResult) });
}
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = callResult });
}
catch (ArgumentException ex)
{
// Unknown MCP server name
log.LogWarning(ex, "No mock tools available for server '{McpServerName}'", mcpServerName);
return Results.Json(new
{
jsonrpc = JsonRpcConstants.Version,
id = idValue,
error = new
{
code = JsonRpcConstants.ErrorCodes.InvalidParams,
message = $"No mock tools available for server '{mcpServerName}'"
}
});
}
return Results.Json(new { jsonrpc = "2.0", id = idValue, result = callResult });
}

return Results.Json(new { jsonrpc = "2.0", id = idValue, error = new { code = -32601, message = $"Method ({method}) not found" } });
// Handle MCP ping requests (used by clients to verify connection health)
if (string.Equals(method, "ping", StringComparison.OrdinalIgnoreCase))
{
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = new { } });
}
// Handle prompts/list requests (return empty list - no mock prompts)
if (string.Equals(method, "prompts/list", StringComparison.OrdinalIgnoreCase))
{
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = new { prompts = Array.Empty<object>() } });
}
// Handle resources/list requests (return empty list - no mock resources)
if (string.Equals(method, "resources/list", StringComparison.OrdinalIgnoreCase))
{
return Results.Json(new { jsonrpc = JsonRpcConstants.Version, id = idValue, result = new { resources = Array.Empty<object>() } });
}

// Handle MCP notifications (e.g., notifications/initialized, notifications/cancelled)
// Per MCP Streamable HTTP spec: return 202 Accepted with no body for notifications
if (method?.StartsWith("notifications/", StringComparison.OrdinalIgnoreCase) == true)
{
return Results.StatusCode(JsonRpcConstants.HttpStatusCodes.Accepted);
}

return Results.Json(new
{
jsonrpc = JsonRpcConstants.Version,
id = idValue,
error = new
{
code = JsonRpcConstants.ErrorCodes.MethodNotFound,
message = $"Method ({method}) not found"
}
});
}
catch (Exception ex)
{
log.LogError(ex, "Mock JSON-RPC failure");
return Results.Json(new { jsonrpc = "2.0", id = (object?)null, error = new { code = -32603, message = ex.Message } });
return Results.Json(new
{
jsonrpc = JsonRpcConstants.Version,
id = idValue,
error = new
{
code = JsonRpcConstants.ErrorCodes.InternalError,
message = ex.Message
}
});
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Agents.A365.DevTools.Cli\Microsoft.Agents.A365.DevTools.Cli.csproj" />
<ProjectReference Include="..\..\Microsoft.Agents.A365.DevTools.MockToolingServer\Microsoft.Agents.A365.DevTools.MockToolingServer.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using FluentAssertions;
using Microsoft.Agents.A365.DevTools.MockToolingServer.Constants;

namespace Microsoft.Agents.A365.DevTools.Cli.Tests.MockToolingServer;

/// <summary>
/// Unit tests for JSON-RPC 2.0 constants used in MockToolingServer.
/// Ensures error codes and HTTP status codes match the JSON-RPC 2.0 specification.
/// </summary>
public class JsonRpcConstantsTests
{
[Fact]
public void Version_ShouldBeJsonRpc20()
{
JsonRpcConstants.Version.Should().Be("2.0");
}

[Theory]
[InlineData(JsonRpcConstants.ErrorCodes.InvalidRequest, -32600)]
[InlineData(JsonRpcConstants.ErrorCodes.MethodNotFound, -32601)]
[InlineData(JsonRpcConstants.ErrorCodes.InvalidParams, -32602)]
[InlineData(JsonRpcConstants.ErrorCodes.InternalError, -32603)]
public void ErrorCodes_ShouldMatchJsonRpcSpecification(int actual, int expected)
{
actual.Should().Be(expected);
}

[Fact]
public void HttpStatusCodes_Accepted_ShouldBe202()
{
JsonRpcConstants.HttpStatusCodes.Accepted.Should().Be(202);
}

[Fact]
public void ErrorCodes_MethodNotFound_ShouldBeUsedForUnknownMethods()
{
// This test documents the intended use of MethodNotFound (-32601)
// It should be returned when the requested JSON-RPC method does not exist
JsonRpcConstants.ErrorCodes.MethodNotFound.Should().Be(-32601);
}

[Fact]
public void ErrorCodes_InvalidParams_ShouldBeUsedForBadParameters()
{
// This test documents the intended use of InvalidParams (-32602)
// It should be returned when method parameters are invalid (e.g., unknown MCP server name)
JsonRpcConstants.ErrorCodes.InvalidParams.Should().Be(-32602);
}

[Fact]
public void ErrorCodes_InternalError_ShouldBeUsedForUnexpectedFailures()
{
// This test documents the intended use of InternalError (-32603)
// It should be returned when an unexpected exception occurs during request processing
JsonRpcConstants.ErrorCodes.InternalError.Should().Be(-32603);
}
}
Loading