Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3dfd4f9
Add PackageVersion for Kusto.Data and Kusto.Ingest
conniey Oct 23, 2025
1342541
Add Id to shared McpModels.
conniey Oct 23, 2025
3cba80c
Initial commit
conniey Oct 23, 2025
5c9fd41
Adding tool to AzureMcp.sln
conniey Oct 23, 2025
4af70a6
Remove AoT
conniey Oct 23, 2025
4e3adf7
Add initial mock up
conniey Oct 23, 2025
0bbad1a
Split to use Query and Ingestion endpoint.
conniey Oct 23, 2025
fe07ce8
Add Query to create table.
conniey Oct 23, 2025
49e88c3
Add LaunchSettings.
conniey Oct 23, 2025
725dc59
Set Default queries folder.
conniey Oct 23, 2025
c9ba1c1
Adding Appsettings.json. Copy to output
conniey Oct 28, 2025
7207b1a
Update Json serialization to output Enum names.
conniey Oct 27, 2025
b27c786
Start host
conniey Oct 27, 2025
ebd8733
Move JsonOutput parsing. Fix command line arguments to pass to exe
conniey Oct 27, 2025
ec1e149
Pass in command rather than just name in changes.
conniey Oct 27, 2025
901cb27
Update Logging
conniey Oct 27, 2025
238c33b
Change interface to use List. Update Ingestion settings for singlejson
conniey Oct 27, 2025
46e1d4f
Fix column mappings
conniey Oct 27, 2025
5bb8bcc
Change to use Direct Ingestion client
conniey Oct 27, 2025
08dd0f5
Fix formatting issues
conniey Oct 28, 2025
12fd8b5
Fix formatting
conniey Oct 28, 2025
2d0f10b
Add Friend assembly
conniey Oct 28, 2025
dc326ce
Add unit test project
conniey Oct 28, 2025
792c53e
Make method virtual
conniey Oct 28, 2025
e13fd7e
Fix formatting issue.
conniey Oct 28, 2025
bdd8fb5
Add header
conniey Oct 28, 2025
a454034
Add dev settings
conniey Oct 28, 2025
be54a7f
Add header
conniey Oct 29, 2025
901c5a9
Add ServerName support.
conniey Nov 11, 2025
bf8aee9
Remove test template.
conniey Nov 11, 2025
d244e0e
Add ServerName
conniey Nov 11, 2025
bbded74
Propagate cancellation
conniey Nov 11, 2025
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
29 changes: 29 additions & 0 deletions AzureMcp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Tools.AzureAIBest
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{319B94CD-694C-16E8-9E3A-9577B99158DD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Mcp.Server.UnitTests", "servers\Azure.Mcp.Server\tests\Azure.Mcp.Server.UnitTests\Azure.Mcp.Server.UnitTests.csproj", "{ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToolMetadataExporter", "eng\tools\ToolMetadataExporter\ToolMetadataExporter.csproj", "{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToolMetadataExporter.UnitTests", "eng\tools\ToolMetadataExporter.UnitTests\ToolMetadataExporter.UnitTests.csproj", "{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -2132,6 +2135,30 @@ Global
{ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}.Release|x64.Build.0 = Release|Any CPU
{ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}.Release|x86.ActiveCfg = Release|Any CPU
{ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C}.Release|x86.Build.0 = Release|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Debug|x64.ActiveCfg = Debug|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Debug|x64.Build.0 = Debug|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Debug|x86.ActiveCfg = Debug|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Debug|x86.Build.0 = Debug|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Release|Any CPU.Build.0 = Release|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Release|x64.ActiveCfg = Release|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Release|x64.Build.0 = Release|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Release|x86.ActiveCfg = Release|Any CPU
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7}.Release|x86.Build.0 = Release|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Debug|x64.ActiveCfg = Debug|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Debug|x64.Build.0 = Debug|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Debug|x86.ActiveCfg = Debug|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Debug|x86.Build.0 = Debug|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Release|Any CPU.Build.0 = Release|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Release|x64.ActiveCfg = Release|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Release|x64.Build.0 = Release|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Release|x86.ActiveCfg = Release|Any CPU
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -2413,6 +2440,8 @@ Global
{BE8CFF4C-E536-43DB-9D01-001E9A052D37} = {50124EEC-97B0-320E-80D4-8464D7692B22}
{319B94CD-694C-16E8-9E3A-9577B99158DD} = {F7E192D1-DE6C-42A2-B52F-02849D482450}
{ADF14627-FCB5-4BD3-B65F-DDCC3A3F727C} = {319B94CD-694C-16E8-9E3A-9577B99158DD}
{66A2052D-99BD-4F5E-B031-F69E3E7FEDC7} = {DAAE2FFB-70A9-DCEF-23A0-0ABAED0A9720}
{C1AEE42D-62AD-DF8D-1459-5B9FBE6BEDEA} = {DAAE2FFB-70A9-DCEF-23A0-0ABAED0A9720}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {926577F9-9246-44E4-BCE9-25DB003F1C51}
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
<PackageVersion Include="Azure.ResourceManager.SignalR" Version="1.1.4" />
<PackageVersion Include="Azure.ResourceManager.Storage" Version="1.4.4" />
<PackageVersion Include="Azure.ResourceManager.ResourceGraph" Version="1.1.0" />
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="13.0.2" />
<PackageVersion Include="Microsoft.Azure.Kusto.Data" Version="14.0.1" />
<PackageVersion Include="Microsoft.Azure.Kusto.Ingest" Version="14.0.1" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.72.1" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.10.0" />
<PackageVersion Include="Microsoft.Extensions.AI.Evaluation" Version="9.10.0" />
Expand Down
3 changes: 3 additions & 0 deletions eng/tools/ToolDescriptionEvaluator/Models/McpModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public class ToolAnnotations
// Tool definition for azmcp tools list response
public class Tool
{
[JsonPropertyName("id")]
public string? Id { get; set; }

[JsonPropertyName("name")]
public required string Name { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsTestProject>true</IsTestProject>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NSubstitute" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" />
<PackageReference Include="xunit.v3" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ToolMetadataExporter\ToolMetadataExporter.csproj" />
</ItemGroup>
</Project>
19 changes: 19 additions & 0 deletions eng/tools/ToolMetadataExporter/AppConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace ToolMetadataExporter;

public class AppConfiguration
{
public string? IngestionEndpoint { get; set; }

public string? QueryEndpoint { get; set; }

public string? DatabaseName { get; set; }

public string? McpToolEventsTableName { get; set; }

public string? QueriesFolder { get; set; } = "Resources/queries";

public string? WorkDirectory { get; set; }
}
7 changes: 7 additions & 0 deletions eng/tools/ToolMetadataExporter/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ToolMetadataExporter.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
5 changes: 5 additions & 0 deletions eng/tools/ToolMetadataExporter/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

global using System;
global using System.Text.Json;
9 changes: 9 additions & 0 deletions eng/tools/ToolMetadataExporter/Models/AzureMcpTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

Missing copyright header. According to the coding standards, all C# files should include the copyright header:

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Suggested change
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

????????????

// Licensed under the MIT License.

namespace ToolMetadataExporter.Models;

public record AzureMcpTool(
string ToolId,
string ToolName,
string ToolArea);
61 changes: 61 additions & 0 deletions eng/tools/ToolMetadataExporter/Models/Kusto/McpToolEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using Kusto.Data.Common;

namespace ToolMetadataExporter.Models.Kusto;

public class McpToolEvent
{
private const string EventTimeColumn = "EventTime";
private const string EventTypeColumn = "EventType";
private const string ServerNameColumn = "ServerName";
private const string ServerVersionColumn = "ServerVersion";
private const string ToolIdColumn = "ToolId";
private const string ToolNameColumn = "ToolName";
private const string ToolAreaColumn = "ToolArea";
private const string ReplacedByToolNameColumn = "ReplacedByToolName";
private const string ReplacedByToolAreaColumn = "ReplacedByToolArea";

[JsonPropertyName(EventTimeColumn)]
public DateTimeOffset? EventTime { get; set; }

[JsonPropertyName(EventTypeColumn)]
public McpToolEventType? EventType { get; set; }

[JsonPropertyName(ServerNameColumn)]
public string? ServerName { get; set; }

[JsonPropertyName(ServerVersionColumn)]
public string? ServerVersion { get; set; }

[JsonPropertyName(ToolIdColumn)]
public string? ToolId { get; set; }

[JsonPropertyName(ToolNameColumn)]
public string? ToolName { get; set; }

[JsonPropertyName(ToolAreaColumn)]
public string? ToolArea { get; set; }

[JsonPropertyName(ReplacedByToolNameColumn)]
public string? ReplacedByToolName { get; set; }

[JsonPropertyName(ReplacedByToolAreaColumn)]
public string? ReplacedByToolArea { get; set; }

public static ColumnMapping[] GetColumnMappings()
{
return [
new ColumnMapping { ColumnName = EventTimeColumn, ColumnType = "datetime" },
new ColumnMapping { ColumnName = EventTypeColumn, ColumnType = "string"},
new ColumnMapping { ColumnName = ReplacedByToolAreaColumn, ColumnType = "string"},
new ColumnMapping { ColumnName = ReplacedByToolNameColumn, ColumnType = "string"},
new ColumnMapping { ColumnName = ServerVersionColumn, ColumnType = "string" },
new ColumnMapping { ColumnName = ToolAreaColumn , ColumnType = "string" },
new ColumnMapping { ColumnName = ToolIdColumn, ColumnType = "string"},
new ColumnMapping { ColumnName = ToolNameColumn, ColumnType = "string" },
];
}
}
16 changes: 16 additions & 0 deletions eng/tools/ToolMetadataExporter/Models/Kusto/McpToolEventType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace ToolMetadataExporter.Models.Kusto;

public enum McpToolEventType
{
[JsonStringEnumMemberName("Created")]
Created,
[JsonStringEnumMemberName("Updated")]
Updated,
[JsonStringEnumMemberName("Deleted")]
Deleted
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;
using ToolMetadataExporter.Models.Kusto;

namespace ToolMetadataExporter.Models;

[JsonSerializable(typeof(McpToolEvent))]
[JsonSerializable(typeof(McpToolEventType))]
[JsonSerializable(typeof(List<McpToolEvent>))]
[JsonSourceGenerationOptions(Converters = [typeof(JsonStringEnumConverter<McpToolEventType>)])]
public partial class ModelsSerializationContext : JsonSerializerContext
{
}
99 changes: 99 additions & 0 deletions eng/tools/ToolMetadataExporter/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Azure.Core;
using Azure.Identity;
using Kusto.Data;
using Kusto.Data.Common;
using Kusto.Data.Net.Client;
using Kusto.Ingest;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ToolMetadataExporter.Services;

namespace ToolMetadataExporter;

public class Program
{
public static async Task Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);

ConfigureServices(builder.Services, builder.Configuration);
ConfigureAzureServices(builder.Services);

var host = builder.Build();

var analyzer = host.Services.GetRequiredService<ToolAnalyzer>();

await host.StartAsync();

var isDryRunValue = builder.Configuration["IsDryRun"];
var isDryRun = false;
if (bool.TryParse(isDryRunValue, out var parsed))
{
isDryRun = parsed;
}

await analyzer.RunAsync(DateTimeOffset.UtcNow, isDryRun);
}

private static void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddLogging(builder =>
{
builder.AddConsole();
});

services.AddSingleton<IAzureMcpDatastore, AzureMcpKustoDatastore>()
.AddSingleton<AzmcpProgram>()
.AddSingleton<ToolAnalyzer>();

services.Configure<AppConfiguration>(configuration.GetSection("AppConfig"))
.PostConfigure<AppConfiguration>(existing =>
{
if (existing.WorkDirectory == null)
{
string exeDir = AppContext.BaseDirectory;
var repoRoot = Utility.FindRepoRoot(exeDir);
existing.WorkDirectory = Path.Combine(repoRoot, ".work");
}
Comment on lines +58 to +63
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

[nitpick] The PostConfigure block sets a default WorkDirectory if it's null. However, the QueriesFolder property also has a default value ("Resources/queries"), but the AzureMcpKustoDatastore constructor validates this path existence. If QueriesFolder is not explicitly set, it will use the default relative path, which may fail validation. Consider also handling QueriesFolder in PostConfigure to ensure it resolves to an absolute path.

Suggested change
if (existing.WorkDirectory == null)
{
string exeDir = AppContext.BaseDirectory;
var repoRoot = Utility.FindRepoRoot(exeDir);
existing.WorkDirectory = Path.Combine(repoRoot, ".work");
}
string exeDir = AppContext.BaseDirectory;
var repoRoot = Utility.FindRepoRoot(exeDir);
if (existing.WorkDirectory == null)
{
existing.WorkDirectory = Path.Combine(repoRoot, ".work");
}
if (string.IsNullOrEmpty(existing.QueriesFolder) || !Path.IsPathRooted(existing.QueriesFolder))
{
existing.QueriesFolder = Path.Combine(repoRoot, existing.QueriesFolder ?? "Resources/queries");
}

Copilot uses AI. Check for mistakes.
});
}

private static void ConfigureAzureServices(IServiceCollection services)
{
services.AddScoped<TokenCredential>(sp =>
{
var credential = new ChainedTokenCredential(
new ManagedIdentityCredential(),
new DefaultAzureCredential()
);

return credential;
});
services.AddSingleton<ICslQueryProvider>(sp =>
{
var config = sp.GetRequiredService<IOptions<AppConfiguration>>();

var connectionStringBuilder = new KustoConnectionStringBuilder(config.Value.QueryEndpoint)
.WithAadUserPromptAuthentication()
.WithAadAzCliAuthentication(interactive: true);

return KustoClientFactory.CreateCslQueryProvider(connectionStringBuilder);
});
services.AddSingleton<IKustoIngestClient>(sp =>
{
var config = sp.GetRequiredService<IOptions<AppConfiguration>>();

var connectionStringBuilder = new KustoConnectionStringBuilder(config.Value.IngestionEndpoint)
.WithAadUserPromptAuthentication()
.WithAadAzCliAuthentication(interactive: true);

return KustoIngestFactory.CreateDirectIngestClient(connectionStringBuilder);
});
}
}
10 changes: 10 additions & 0 deletions eng/tools/ToolMetadataExporter/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"profiles": {
"ToolMetadataExporter": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.create table McpToolEvents (
EventTime: datetime,
EventType: string,
ServerVersion: string,
ToolId: string,
ToolName: string,
ToolArea: string,
ReplacedByToolName: string,
ReplacedByToolArea: string
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
McpToolEvents
| summarize arg_max(EventTime, *) by ToolId
| where EventType != 'Deleted'
| project EventTime, ToolId, ToolName, ToolArea, ServerVersion, EventType, ReplacedByToolName, ReplacedByToolArea
29 changes: 29 additions & 0 deletions eng/tools/ToolMetadataExporter/Services/AzmcpProgram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Extensions.Options;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

Missing copyright header. According to the coding standards, all C# files should include the copyright header:

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

Copilot uses AI. Check for mistakes.
using ToolSelection.Models;

namespace ToolMetadataExporter.Services;

public class AzmcpProgram
{
private readonly string _toolDirectory;

public Task<string> GetServerNameAsync()
{
return Utility.GetServerName();
}

public AzmcpProgram(IOptions<AppConfiguration> options)
{
_toolDirectory = options.Value.WorkDirectory ?? throw new ArgumentNullException(nameof(AppConfiguration.WorkDirectory));
}

public virtual Task<ListToolsResult?> LoadToolsDynamicallyAsync()
{
return Utility.LoadToolsDynamicallyAsync(_toolDirectory, false);
Copy link
Member Author

Choose a reason for hiding this comment

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

These are currently utility methods but the logic is from ToolDescriptionEvaluator where I think the logic can be shared.

}

public virtual Task<string> GetVersionAsync()
{
return Utility.GetVersionAsync();
}
}
Loading
Loading