Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3784c3c
Initial plan
Copilot Feb 6, 2026
da7c4cd
Add OpenTelemetry tracing infrastructure for MCP tools
Copilot Feb 6, 2026
d015084
Add comprehensive telemetry tests for MCP tools
Copilot Feb 6, 2026
db3f83d
Refactor: Extract telemetry helpers to shared utility and add public …
Copilot Feb 6, 2026
7b9a376
Improve error handling and use specific error codes in telemetry
Copilot Feb 6, 2026
61d54c9
Apply code formatting fixes
Copilot Feb 6, 2026
b4c119d
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
souvikghosh04 Feb 9, 2026
1e5dc4c
Merge branch 'main' of https://github.com/Azure/data-api-builder into…
souvikghosh04 Feb 11, 2026
5ece5ff
Refactor and optimize
souvikghosh04 Feb 11, 2026
19bd5b0
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
souvikghosh04 Feb 11, 2026
44b7937
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
Aniruddh25 Feb 12, 2026
0d9d882
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
Aniruddh25 Feb 12, 2026
f5e9d71
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
souvikghosh04 Feb 16, 2026
98105c3
Address MCP telemetry review comments
Copilot Feb 16, 2026
785a819
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
Aniruddh25 Feb 17, 2026
234017a
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
souvikghosh04 Feb 17, 2026
7b3d217
Merge branch 'main' into copilot/add-telemetry-for-custom-tools
souvikghosh04 Feb 17, 2026
c0fe63c
Merge branch 'copilot/add-telemetry-for-custom-tools' of https://gith…
souvikghosh04 Feb 17, 2026
4cd0a8b
Fix formatting
souvikghosh04 Feb 17, 2026
b0f714a
Fix IDE0032: Use auto property for MockMcpTool.ToolType
Copilot Feb 18, 2026
eb118ac
Restore global.json (was accidentally renamed)
Copilot Feb 18, 2026
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
Expand Up @@ -6,6 +6,10 @@
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Azure.DataApiBuilder.Service.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="ModelContextProtocol.AspNetCore" />
Expand Down
34 changes: 19 additions & 15 deletions src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ namespace Azure.DataApiBuilder.Mcp.Core
/// </summary>
public class DynamicCustomTool : IMcpTool
{
private readonly string _entityName;
private readonly Entity _entity;

/// <summary>
Expand All @@ -48,7 +47,7 @@ public class DynamicCustomTool : IMcpTool
/// <param name="entity">The entity configuration object.</param>
public DynamicCustomTool(string entityName, Entity entity)
{
_entityName = entityName ?? throw new ArgumentNullException(nameof(entityName));
EntityName = entityName ?? throw new ArgumentNullException(nameof(entityName));
_entity = entity ?? throw new ArgumentNullException(nameof(entity));

// Validate that this is a stored procedure
Expand All @@ -65,12 +64,17 @@ public DynamicCustomTool(string entityName, Entity entity)
/// </summary>
public ToolType ToolType { get; } = ToolType.Custom;

/// <summary>
/// Gets the entity name associated with this custom tool.
/// </summary>
public string EntityName { get; }

/// <summary>
/// Gets the metadata for this custom tool, including name, description, and input schema.
/// </summary>
public Tool GetToolMetadata()
{
string toolName = ConvertToToolName(_entityName);
string toolName = ConvertToToolName(EntityName);
string description = _entity.Description ?? $"Executes the {toolName} stored procedure";

// Build input schema based on parameters
Expand Down Expand Up @@ -114,25 +118,25 @@ public async Task<CallToolResult> ExecuteAsync(
}

// 3) Validate entity still exists in configuration
if (!config.Entities.TryGetValue(_entityName, out Entity? entityConfig))
if (!config.Entities.TryGetValue(EntityName, out Entity? entityConfig))
{
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{_entityName}' not found in configuration.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{EntityName}' not found in configuration.", logger);
}

if (entityConfig.Source.Type != EntitySourceType.StoredProcedure)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity {_entityName} is not a stored procedure.", logger);
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity {EntityName} is not a stored procedure.", logger);
}

// Check if custom tool is still enabled for this entity
if (entityConfig.Mcp?.CustomToolEnabled != true)
{
return McpErrorHelpers.ToolDisabled(toolName, logger, $"Custom tool is disabled for entity '{_entityName}'.");
return McpErrorHelpers.ToolDisabled(toolName, logger, $"Custom tool is disabled for entity '{EntityName}'.");
}

// 4) Resolve metadata
if (!McpMetadataHelper.TryResolveMetadata(
_entityName,
EntityName,
config,
serviceProvider,
out ISqlMetadataProvider sqlMetadataProvider,
Expand All @@ -150,18 +154,18 @@ public async Task<CallToolResult> ExecuteAsync(

if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError))
{
return McpErrorHelpers.PermissionDenied(toolName, _entityName, "execute", roleError, logger);
return McpErrorHelpers.PermissionDenied(toolName, EntityName, "execute", roleError, logger);
}

if (!McpAuthorizationHelper.TryResolveAuthorizedRole(
httpContext!,
authResolver,
_entityName,
EntityName,
EntityActionOperation.Execute,
out string? effectiveRole,
out string authError))
{
return McpErrorHelpers.PermissionDenied(toolName, _entityName, "execute", authError, logger);
return McpErrorHelpers.PermissionDenied(toolName, EntityName, "execute", authError, logger);
}

// 6) Build request payload
Expand All @@ -175,7 +179,7 @@ public async Task<CallToolResult> ExecuteAsync(

// 7) Build stored procedure execution context
StoredProcedureRequestContext context = new(
entityName: _entityName,
entityName: EntityName,
dbo: dbObject,
requestPayloadRoot: requestPayloadRoot,
operationType: EntityActionOperation.Execute);
Expand Down Expand Up @@ -218,7 +222,7 @@ public async Task<CallToolResult> ExecuteAsync(
}
catch (DataApiBuilderException dabEx)
{
logger?.LogError(dabEx, "Error executing custom tool {ToolName} for entity {Entity}", toolName, _entityName);
logger?.LogError(dabEx, "Error executing custom tool {ToolName} for entity {Entity}", toolName, EntityName);
return McpResponseBuilder.BuildErrorResult(toolName, "ExecutionError", dabEx.Message, logger);
}
catch (SqlException sqlEx)
Expand All @@ -238,15 +242,15 @@ public async Task<CallToolResult> ExecuteAsync(
}

// 9) Build success response
return BuildExecuteSuccessResponse(toolName, _entityName, parameters, queryResult, logger);
return BuildExecuteSuccessResponse(toolName, EntityName, parameters, queryResult, logger);
}
catch (OperationCanceledException)
{
return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The operation was canceled.", logger);
}
catch (Exception ex)
{
logger?.LogError(ex, "Unexpected error in DynamicCustomTool for {EntityName}", _entityName);
logger?.LogError(ex, "Unexpected error in DynamicCustomTool for {EntityName}", EntityName);
return McpResponseBuilder.BuildErrorResult(toolName, "UnexpectedError", "An unexpected error occurred.", logger);
}
}
Expand Down
26 changes: 14 additions & 12 deletions src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text.Json;
using Azure.DataApiBuilder.Mcp.Model;
using Azure.DataApiBuilder.Mcp.Utils;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
Expand Down Expand Up @@ -61,22 +62,23 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se
}

JsonDocument? arguments = null;
if (request.Params?.Arguments != null)
try
{
// Convert IReadOnlyDictionary<string, JsonElement> to JsonDocument
Dictionary<string, object?> jsonObject = new();
foreach (KeyValuePair<string, JsonElement> kvp in request.Params.Arguments)
if (request.Params?.Arguments != null)
{
jsonObject[kvp.Key] = kvp.Value;
}
// Convert IReadOnlyDictionary<string, JsonElement> to JsonDocument
Dictionary<string, object?> jsonObject = new();
foreach (KeyValuePair<string, JsonElement> kvp in request.Params.Arguments)
{
jsonObject[kvp.Key] = kvp.Value;
}

string json = JsonSerializer.Serialize(jsonObject);
arguments = JsonDocument.Parse(json);
}
string json = JsonSerializer.Serialize(jsonObject);
arguments = JsonDocument.Parse(json);
}

try
{
return await tool!.ExecuteAsync(arguments, request.Services!, ct);
return await McpTelemetryHelper.ExecuteWithTelemetryAsync(
tool!, toolName, arguments, request.Services!, ct);
}
finally
{
Expand Down
9 changes: 6 additions & 3 deletions src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Mcp.Model;
using Azure.DataApiBuilder.Mcp.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -284,7 +285,7 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel
Console.Error.WriteLine($"[MCP DEBUG] callTool → tool: {toolName}, args: <none>");
}

// Execute the tool.
// Execute the tool with telemetry.
// If a MCP stdio role override is set in the environment, create
// a request HttpContext with the X-MS-API-ROLE header so tools and authorization
// helpers that read IHttpContextAccessor will see the role. We also ensure the
Expand Down Expand Up @@ -319,7 +320,8 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel
try
{
// Execute the tool with the scoped service provider so any scoped services resolve correctly.
callResult = await tool.ExecuteAsync(argsDoc, scopedProvider, ct);
callResult = await McpTelemetryHelper.ExecuteWithTelemetryAsync(
tool, toolName!, argsDoc, scopedProvider, ct);
}
finally
{
Expand All @@ -332,7 +334,8 @@ private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, Cancel
}
else
{
callResult = await tool.ExecuteAsync(argsDoc, _serviceProvider, ct);
callResult = await McpTelemetryHelper.ExecuteWithTelemetryAsync(
tool, toolName!, argsDoc, _serviceProvider, ct);
}

// Normalize to MCP content blocks (array). We try to pass through if a 'Content' property exists,
Expand Down
41 changes: 41 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/Utils/McpTelemetryErrorCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Mcp.Utils
{
/// <summary>
/// Constants for MCP telemetry error codes.
/// </summary>
internal static class McpTelemetryErrorCodes
{
/// <summary>
/// Generic execution failure error code.
/// </summary>
public const string EXECUTION_FAILED = "ExecutionFailed";

/// <summary>
/// Authentication failure error code.
/// </summary>
public const string AUTHENTICATION_FAILED = "AuthenticationFailed";

/// <summary>
/// Authorization failure error code.
/// </summary>
public const string AUTHORIZATION_FAILED = "AuthorizationFailed";

/// <summary>
/// Database operation failure error code.
/// </summary>
public const string DATABASE_ERROR = "DatabaseError";

/// <summary>
/// Invalid request or arguments error code.
/// </summary>
public const string INVALID_REQUEST = "InvalidRequest";

/// <summary>
/// Operation cancelled error code.
/// </summary>
public const string OPERATION_CANCELLED = "OperationCancelled";
}
}
Loading