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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changes:
- section: "Bugs Fixed"
description: "Fixed the SQL `db get` (list databases) and `elastic-pool list` tools returning resources from every server in the resource group instead of only the requested server. The queries are now scoped to the specified server."
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,20 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
cancellationToken);

context.Response.Results = ResponseResult.Create(
new([database], false),
new([database]),
SqlJsonContext.Default.DatabaseGetListResult);
}
else
{
var result = await _sqlService.ListDatabasesAsync(
var databases = await _sqlService.ListDatabasesAsync(
options.Server!,
options.ResourceGroup!,
options.Subscription!,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(
new(result?.Results ?? [], result?.AreResultsTruncated ?? false),
new(databases ?? []),
SqlJsonContext.Default.DatabaseGetListResult);
}
}
Expand All @@ -109,5 +109,5 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
_ => base.GetErrorMessage(ex)
};

internal record DatabaseGetListResult(List<SqlDatabase> Databases, bool AreResultsTruncated);
internal record DatabaseGetListResult(List<SqlDatabase> Databases);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
options.RetryPolicy,
cancellationToken);

context.Response.Results = ResponseResult.Create(new(elasticPools?.Results ?? [], elasticPools?.AreResultsTruncated ?? false), SqlJsonContext.Default.ElasticPoolListResult);
context.Response.Results = ResponseResult.Create(new(elasticPools ?? []), SqlJsonContext.Default.ElasticPoolListResult);
}
catch (Exception ex)
{
Expand All @@ -75,5 +75,5 @@ public override async Task<CommandResponse> ExecuteAsync(CommandContext context,
_ => base.GetErrorMessage(ex)
};

internal record ElasticPoolListResult(List<SqlElasticPool> ElasticPools, bool AreResultsTruncated);
internal record ElasticPoolListResult(List<SqlElasticPool> ElasticPools);
}
3 changes: 0 additions & 3 deletions tools/Azure.Mcp.Tools.Sql/src/Commands/SqlJsonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ namespace Azure.Mcp.Tools.Sql.Commands;
[JsonSerializable(typeof(SqlDatabaseData))]
[JsonSerializable(typeof(SqlDatabaseProperties))]
[JsonSerializable(typeof(SqlServerAadAdministratorData))]
[JsonSerializable(typeof(SqlElasticPoolData))]
[JsonSerializable(typeof(SqlElasticPoolProperties))]
[JsonSerializable(typeof(SqlElasticPoolPerDatabaseSettings))]
[JsonSerializable(typeof(SqlFirewallRuleData))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
Expand Down
5 changes: 2 additions & 3 deletions tools/Azure.Mcp.Tools.Sql/src/Services/ISqlService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Tools.Sql.Models;
using Microsoft.Mcp.Core.Options;

Expand Down Expand Up @@ -125,7 +124,7 @@ Task<SqlDatabase> RenameDatabaseAsync(
/// <param name="retryPolicy">Optional retry policy options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A list of SQL databases</returns>
Task<ResourceQueryResults<SqlDatabase>> ListDatabasesAsync(
Task<List<SqlDatabase>> ListDatabasesAsync(
string serverName,
string resourceGroup,
string subscription,
Expand Down Expand Up @@ -157,7 +156,7 @@ Task<List<SqlServerEntraAdministrator>> GetEntraAdministratorsAsync(
/// <param name="retryPolicy">Optional retry policy options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A list of SQL elastic pools</returns>
Task<ResourceQueryResults<SqlElasticPool>> GetElasticPoolsAsync(
Task<List<SqlElasticPool>> GetElasticPoolsAsync(
string serverName,
string resourceGroup,
string subscription,
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

117 changes: 80 additions & 37 deletions tools/Azure.Mcp.Tools.Sql/src/Services/SqlService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,28 @@ namespace Azure.Mcp.Tools.Sql.Services;

public class SqlService(ISubscriptionService subscriptionService, ITenantService tenantService, ILogger<SqlService> logger) : BaseAzureResourceService(subscriptionService, tenantService), ISqlService
{
private readonly ISubscriptionService _subscriptionService = subscriptionService;
private readonly ILogger<SqlService> _logger = logger;

/// <summary>
/// Resolves a subscription name or ID to a subscription ID. When the value is already a
/// subscription ID no additional ARM request is made, preserving the existing network
/// behavior for ID-based callers.
/// </summary>
/// <param name="subscription">The subscription ID or name</param>
/// <param name="retryPolicy">Optional retry policy configuration</param>
/// <param name="cancellationToken">Token to observe for cancellation requests</param>
/// <returns>The resolved subscription ID</returns>
private async Task<string> ResolveSubscriptionIdAsync(
string subscription,
RetryPolicyOptions? retryPolicy,
CancellationToken cancellationToken)
{
return _subscriptionService.IsSubscriptionId(subscription)
? subscription
: await _subscriptionService.GetSubscriptionIdByName(subscription, null, retryPolicy, cancellationToken);
}

/// <summary>
/// Helper method to navigate the Azure resource hierarchy and retrieve a SQL Server resource.
/// </summary>
Expand Down Expand Up @@ -355,20 +375,32 @@ public async Task<SqlDatabase> RenameDatabaseAsync(
/// <param name="cancellationToken">Token to observe for cancellation requests</param>
/// <returns>A list of SQL databases on the specified server</returns>
/// <exception cref="ArgumentException">Thrown when required parameters are null or empty</exception>
public async Task<ResourceQueryResults<SqlDatabase>> ListDatabasesAsync(
public async Task<List<SqlDatabase>> ListDatabasesAsync(
string serverName,
string resourceGroup,
string subscription,
RetryPolicyOptions? retryPolicy,
CancellationToken cancellationToken = default)
{
return await ExecuteResourceQueryAsync(
"Microsoft.Sql/servers/databases",
resourceGroup,
subscription,
retryPolicy,
ConvertToSqlDatabaseModel,
cancellationToken: cancellationToken);
ValidateRequiredParameters(
(nameof(serverName), serverName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));

var subscriptionId = await ResolveSubscriptionIdAsync(subscription, retryPolicy, cancellationToken);
var sqlServerResource = await GetSqlServerResourceAsync(serverName, resourceGroup, subscriptionId, retryPolicy, cancellationToken);

Comment thread
vcolin7 marked this conversation as resolved.
var databases = new List<SqlDatabase>();
await foreach (var database in sqlServerResource.GetSqlDatabases().GetAllAsync(cancellationToken: cancellationToken))
{
databases.Add(ConvertToSqlDatabaseModel(database));
}

_logger.LogInformation(
"Successfully listed SQL databases. Server: {Server}, ResourceGroup: {ResourceGroup}, Count: {Count}",
serverName, resourceGroup, databases.Count);

return databases;
}

/// <summary>
Expand Down Expand Up @@ -429,20 +461,32 @@ public async Task<List<SqlServerEntraAdministrator>> GetEntraAdministratorsAsync
/// <param name="cancellationToken">Token to observe for cancellation requests</param>
/// <returns>A list of elastic pools configured on the SQL server</returns>
/// <exception cref="ArgumentException">Thrown when required parameters are null or empty</exception>
public async Task<ResourceQueryResults<SqlElasticPool>> GetElasticPoolsAsync(
public async Task<List<SqlElasticPool>> GetElasticPoolsAsync(
string serverName,
string resourceGroup,
string subscription,
RetryPolicyOptions? retryPolicy,
CancellationToken cancellationToken = default)
{
return await ExecuteResourceQueryAsync(
"Microsoft.Sql/servers/elasticPools",
resourceGroup,
subscription,
retryPolicy,
ConvertToSqlElasticPoolModel,
cancellationToken: cancellationToken);
ValidateRequiredParameters(
(nameof(serverName), serverName),
(nameof(resourceGroup), resourceGroup),
(nameof(subscription), subscription));

var subscriptionId = await ResolveSubscriptionIdAsync(subscription, retryPolicy, cancellationToken);
var sqlServerResource = await GetSqlServerResourceAsync(serverName, resourceGroup, subscriptionId, retryPolicy, cancellationToken);

Comment thread
vcolin7 marked this conversation as resolved.
var elasticPools = new List<SqlElasticPool>();
await foreach (var elasticPool in sqlServerResource.GetElasticPools().GetAllAsync(cancellationToken: cancellationToken))
{
elasticPools.Add(ConvertToSqlElasticPoolModel(elasticPool));
}

_logger.LogInformation(
"Successfully listed SQL elastic pools. Server: {Server}, ResourceGroup: {ResourceGroup}, Count: {Count}",
serverName, resourceGroup, elasticPools.Count);

return elasticPools;
}

/// <summary>
Expand Down Expand Up @@ -917,32 +961,31 @@ private static SqlServer ConvertToSqlServerModel(SqlServerResource serverResourc
Tags: tags.Count > 0 ? tags : null);
}

private static SqlElasticPool ConvertToSqlElasticPoolModel(JsonElement item)
private static SqlElasticPool ConvertToSqlElasticPoolModel(ElasticPoolResource elasticPoolResource)
{
SqlElasticPoolData? elasticPool = SqlElasticPoolData.FromJson(item)
?? throw new InvalidOperationException("Failed to parse SQL elastic pool data");
var data = elasticPoolResource.Data;

return new(
Name: elasticPool.ResourceName ?? "Unknown",
Id: elasticPool.ResourceId ?? "Unknown",
Type: elasticPool.ResourceType ?? "Unknown",
Location: elasticPool.Location,
Sku: elasticPool.Sku != null ? new(
Name: elasticPool.Sku.Name,
Tier: elasticPool.Sku.Tier,
Capacity: elasticPool.Sku.Capacity,
Family: elasticPool.Sku.Family,
Size: elasticPool.Sku.Size
Name: data.Name,
Id: data.Id.ToString(),
Type: data.ResourceType.ToString(),
Location: data.Location.ToString(),
Sku: data.Sku != null ? new(
Name: data.Sku.Name,
Tier: data.Sku.Tier,
Capacity: data.Sku.Capacity,
Family: data.Sku.Family,
Size: data.Sku.Size
) : null,
State: elasticPool.Properties?.State,
CreationDate: elasticPool.Properties?.CreatedOn,
MaxSizeBytes: elasticPool.Properties?.MaxSizeBytes,
PerDatabaseSettings: elasticPool.Properties?.PerDatabaseSettings != null ? new(
MinCapacity: elasticPool.Properties.PerDatabaseSettings.MinCapacity,
MaxCapacity: elasticPool.Properties.PerDatabaseSettings.MaxCapacity
State: data.State?.ToString(),
CreationDate: data.CreatedOn,
MaxSizeBytes: data.MaxSizeBytes,
PerDatabaseSettings: data.PerDatabaseSettings != null ? new(
MinCapacity: data.PerDatabaseSettings.MinCapacity,
MaxCapacity: data.PerDatabaseSettings.MaxCapacity
) : null,
ZoneRedundant: elasticPool.Properties?.IsZoneRedundant,
LicenseType: elasticPool.Properties?.LicenseType,
ZoneRedundant: data.IsZoneRedundant,
LicenseType: data.LicenseType?.ToString(),
DatabaseDtuMin: null, // DTU properties not available in current SDK
DatabaseDtuMax: null,
Dtu: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

using System.Net;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Tools.Sql.Commands.Database;
using Azure.Mcp.Tools.Sql.Models;
using Azure.Mcp.Tools.Sql.Services;
Expand Down Expand Up @@ -59,7 +58,7 @@ public async Task ExecuteAsync_WithDatabaseName_ReturnsSingleDatabase()
public async Task ExecuteAsync_WithoutDatabaseName_ReturnsAllDatabases()
{
// Arrange
var mockDatabases = new ResourceQueryResults<SqlDatabase>([CreateMockDatabase("db1"), CreateMockDatabase("db2")], false);
var mockDatabases = new List<SqlDatabase> { CreateMockDatabase("db1"), CreateMockDatabase("db2") };

Service.ListDatabasesAsync(
Arg.Is("server1"),
Expand Down Expand Up @@ -171,7 +170,7 @@ public async Task ExecuteAsync_ValidatesRequiredParameters(string commandArgs, b
{
Service
.ListDatabasesAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>(), Arg.Any<CancellationToken>())
.Returns(new ResourceQueryResults<SqlDatabase>([], false));
.Returns(new List<SqlDatabase>());
Service
.GetDatabaseAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<RetryPolicyOptions>(), Arg.Any<CancellationToken>())
.Returns(CreateMockDatabase("db1"));
Expand Down
Loading