Skip to content

Conversation

@conniey
Copy link
Member

@conniey conniey commented Oct 28, 2025

What does this PR do?

Adds tool to export current metadata from azmcp.

GitHub issue number?

[Link to the GitHub issue this PR addresses]
https://github.com/microsoft/mcp-pr/issues/123

Pre-merge Checklist

  • Required for All PRs
    • Read contribution guidelines
    • PR title clearly describes the change
    • Commit history is clean with descriptive messages (cleanup guide)
    • Added comprehensive tests for new/modified functionality
    • Updated servers/Azure.Mcp.Server/CHANGELOG.md and/or servers/Fabric.Mcp.Server/CHANGELOG.md for product changes (features, bug fixes, UI/UX, updated dependencies)
  • For MCP tool changes:
    • One tool per PR: This PR adds or modifies only one MCP tool for faster review cycles
    • Updated servers/Azure.Mcp.Server/README.md and/or servers/Fabric.Mcp.Server/README.md documentation
    • Validate README.md changes using script at eng/scripts/Process-PackageReadMe.ps1. See Package README
    • Updated command list in /servers/Azure.Mcp.Server/docs/azmcp-commands.md and/or /docs/fabric-commands.md
    • Run .\eng\scripts\Update-AzCommandsMetadata.ps1 to update tool metadata in azmcp-commands.md (required for CI)
    • For new or modified tool descriptions, ran ToolDescriptionEvaluator and obtained a score of 0.4 or more and a top 3 ranking for all related test prompts
    • For new tools associated with Azure services or publicly available tools/APIs/products, add URL to documentation in the PR description
  • Extra steps for Azure MCP Server tool changes:
    • Updated test prompts in /servers/Azure.Mcp.Server/docs/e2eTestPrompts.md
    • 👉 For Community (non-Microsoft team member) PRs:
      • Security review: Reviewed code for security vulnerabilities, malicious code, or suspicious activities before running tests (crypto mining, spam, data exfiltration, etc.)
      • Manual tests run: added comment /azp run mcp - pullrequest - live to run Live Test Pipeline

@joshfree joshfree added this to the 2025-11 milestone Oct 29, 2025
@joshfree joshfree moved this from Untriaged to In Progress in Azure MCP Server Oct 29, 2025

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.


namespace ToolMetadataExporter;

internal class Utility
Copy link
Member Author

Choose a reason for hiding this comment

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

Taken from ToolDescriptionEvaluator and modified. Need to update logic together.

@conniey conniey changed the title (WIP) Add ToolMetadataExporter Add ToolMetadataExporter Nov 11, 2025
@conniey conniey marked this pull request as ready for review November 11, 2025 19:33
@conniey conniey requested a review from a team as a code owner November 11, 2025 19:33
Copilot AI review requested due to automatic review settings November 11, 2025 19:33
@conniey conniey requested review from a team as code owners November 11, 2025 19:33
Copilot finished reviewing on behalf of conniey November 11, 2025 19:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a new ToolMetadataExporter tool that exports and tracks metadata changes from the Azure MCP server (azmcp). The tool compares current tool definitions against historical data stored in Azure Data Explorer (Kusto) and records creation, update, and deletion events.

Key changes:

  • New ToolMetadataExporter console application that dynamically loads tools from azmcp and compares them against a Kusto datastore
  • Integration with Azure Data Explorer for querying existing tools and ingesting change events
  • Support for tracking tool lifecycle events (Created, Updated, Deleted) with version information

Reviewed Changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
eng/tools/ToolMetadataExporter/Program.cs Entry point configuring DI, Azure authentication, and Kusto clients
eng/tools/ToolMetadataExporter/ToolAnalyzer.cs Core analysis logic comparing current tools against datastore and detecting changes
eng/tools/ToolMetadataExporter/Utility.cs Helper methods for executing azmcp commands and parsing tool metadata
eng/tools/ToolMetadataExporter/Services/AzmcpProgram.cs Service wrapper for interacting with the azmcp executable
eng/tools/ToolMetadataExporter/Services/AzureMcpKustoDatastore.cs Kusto datastore implementation for querying and ingesting tool events
eng/tools/ToolMetadataExporter/Services/IAzureMcpDatastore.cs Interface defining datastore operations
eng/tools/ToolMetadataExporter/Models/Kusto/McpToolEvent.cs Model representing tool lifecycle events with Kusto column mappings
eng/tools/ToolMetadataExporter/Models/Kusto/McpToolEventType.cs Enum defining event types (Created, Updated, Deleted)
eng/tools/ToolMetadataExporter/Models/ModelsSerializationContext.cs JSON source generation context for AOT compatibility
eng/tools/ToolMetadataExporter/Models/AzureMcpTool.cs Record representing a tool with ID, name, and area
eng/tools/ToolMetadataExporter/AppConfiguration.cs Configuration model for Kusto endpoints and settings
eng/tools/ToolMetadataExporter/appsettings.json Production configuration template
eng/tools/ToolMetadataExporter/appsettings.Development.json Development environment configuration
eng/tools/ToolMetadataExporter/Resources/queries/GetAvailableTools.kql KQL query to retrieve latest tool states
eng/tools/ToolMetadataExporter/Resources/queries/CreateTable.kql KQL script to create the McpToolEvents table
eng/tools/ToolMetadataExporter/ToolMetadataExporter.csproj Project file with dependencies on Azure.Identity and Kusto SDKs
eng/tools/ToolMetadataExporter.UnitTests/ToolAnalyzerTests.cs Empty test class placeholder for unit tests
eng/tools/ToolMetadataExporter.UnitTests/ToolMetadataExporter.UnitTests.csproj Test project configuration
eng/tools/ToolDescriptionEvaluator/Models/McpModels.cs Added Id property to Tool model
Directory.Packages.props Updated Kusto package versions to 14.0.1 and added Ingest package
AzureMcp.sln Added ToolMetadataExporter projects to solution

if (string.IsNullOrEmpty(latestEvent.ToolId))
{
throw new InvalidOperationException(
$"Cannot have an event with no id. Name: {latestEvent.ToolArea}, Area: {latestEvent.ToolArea}");
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.

The error message incorrectly displays ToolArea for the Name parameter. It should display latestEvent.ToolName instead of latestEvent.ToolArea for the name field.

Fix:

$"Cannot have an event with no id. Name: {latestEvent.ToolName}, Area: {latestEvent.ToolArea}");
Suggested change
$"Cannot have an event with no id. Name: {latestEvent.ToolArea}, Area: {latestEvent.ToolArea}");
$"Cannot have an event with no id. Name: {latestEvent.ToolName}, Area: {latestEvent.ToolArea}");

Copilot uses AI. Check for mistakes.
Comment on lines +261 to +268
private static string EscapeCharacters(string text)
{
if (string.IsNullOrEmpty(text))
return text;

// Normalize only the fancy “curly” quotes to straight ASCII. Identity replacements were removed.
return text.Replace(UnicodeChars.LeftSingleQuote, "'")
.Replace(UnicodeChars.RightSingleQuote, "'")
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.

The variable dir can be null after the while loop exits if no repo root is found. The null check should occur before dereferencing dir.Parent in the loop.

Consider using a null-conditional operator:

dir = dir?.Parent;

Or check for null before the iteration:

while (dir != null)
{
    if (File.Exists(Path.Combine(dir.FullName, "AzureMcp.sln")) ||
        Directory.Exists(Path.Combine(dir.FullName, ".git")))
    {
        return dir.FullName;
    }
    dir = dir.Parent;
}

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +63
if (existing.WorkDirectory == null)
{
string exeDir = AppContext.BaseDirectory;
var repoRoot = Utility.FindRepoRoot(exeDir);
existing.WorkDirectory = Path.Combine(repoRoot, ".work");
}
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.
}

if (cliArtifact != null)
{
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.

The argumentsToUse variable is incorrectly assigned when isDll is true. It should append arguments to the DLL path, but currently it only includes the DLL path without the actual arguments.

Fix:

var argumentsToUse = isDll ? $"{cliArtifact.FullName} {arguments}" : arguments;

Copilot uses AI. Check for mistakes.
_logger = logger;

_workingDirectory = configuration.Value.WorkDirectory ?? throw new ArgumentNullException(nameof(AppConfiguration.WorkDirectory));
;
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.

Extraneous semicolon on an empty line. This should be removed.

Suggested change
;

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +218

private static async Task SaveToolsToJsonAsync(ListToolsResult toolsResult, string filePath)
{
try
{
// Normalize only tool and option descriptions instead of escaping the entire JSON document
if (toolsResult.Tools != null)
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.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +85

internal static async Task<string> ExecuteAzmcpAsync(string arguments, bool isCiMode = false, bool checkErrorCode = true)
{
// Locate azmcp artifact across common build outputs (servers/core, Debug/Release)
var exeDir = AppContext.BaseDirectory;
var repoRoot = FindRepoRoot(exeDir);
var searchRoots = new List<string>
{
Path.Combine(repoRoot, "servers", "Azure.Mcp.Server", "src", "bin", "Debug"),
Path.Combine(repoRoot, "servers", "Azure.Mcp.Server", "src", "bin", "Release")
};
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.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +118
}
}

if (cliArtifact == null)
{
if (isCiMode)
{
return string.Empty; // Graceful fallback in CI
}

throw new FileNotFoundException("Could not locate azmcp CLI artifact in Debug/Release outputs under servers.");
}
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.

Disposable 'Process' is created but not disposed.

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +165
catch (Exception ex)
{
_logger.LogError(ex, "Error writing to {FileName}", outputFile);
}
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.

Generic catch clause.

Suggested change
catch (Exception ex)
{
_logger.LogError(ex, "Error writing to {FileName}", outputFile);
}
catch (IOException ex)
{
_logger.LogError(ex, "IO error writing to {FileName}", outputFile);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Unauthorized access writing to {FileName}", outputFile);
}

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +241
}
}

var writerOptions = new JsonWriterOptions
{
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.

Generic catch clause.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants