diff --git a/.gitignore b/.gitignore index fc346c1..d662ded 100644 --- a/.gitignore +++ b/.gitignore @@ -28,10 +28,377 @@ venv/ env/ ENV/ +# .NET +bin/ +obj/ +*.user +*.userosscache +*.sln.docstates +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Bb]uild/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these files may be inadvertently exposed. +*.azurePubxml + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment the next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +CDF_UpgradeLog*.xml + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + # IDE .idea/ *.swp *.swo +.vscode/settings.json # OS .DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json index 7774467..c027751 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,13 +4,29 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { "name": "Python Debugger: Current File", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal" + }, + { + "name": ".NET Agent Client", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/src/Agent/bin/Debug/net8.0/Agent.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Agent", + "stopAtEntry": false, + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/src/Agent/.env" + }, + { + "name": ".NET MCP Server (Attach)", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" } ] } \ No newline at end of file diff --git a/README.md b/README.md index 856751d..fb1574f 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,16 @@ urlFragment: foundry-agent-service-remote-mcp-python --- --> -# Getting Started with Agent Service and Remote MCP Servers (Python) +# Getting Started with Agent Service and Remote MCP Servers -This is a quickstart template to easily run an [Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/) client and then add a custom remote MCP server to the cloud using [Azure Functions Remote MCP](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-mcp?pivots=programming-language-python). You can clone/restore/run on your local machine with debugging, and `azd up` to have it in the cloud in a couple minutes. The MCP server is secured by design using keys and HTTPS, and allows more options for OAuth using built-in auth and/or [API Management](https://aka.ms/mcp-remote-apim-auth) as well as network isolation using VNET. +This repository contains implementations for an [Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/) client with a custom remote MCP server using [Azure Functions Remote MCP](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-mcp). You can clone/restore/run on your local machine with debugging, and `azd up` to have it in the cloud in a couple minutes. The MCP server is secured by design using keys and HTTPS, and allows more options for OAuth using built-in auth and/or [API Management](https://aka.ms/mcp-remote-apim-auth) as well as network isolation using VNET. + +## Language Implementations + +This repository contains implementations in multiple languages: + +- **Python** (Original): `src/agent/` and `src/mcp_server/` +- **.NET/C#** (Ported): `src/Agent/` and `src/McpServer/` If you're looking for this sample in more languages check out the [.NET/C#](https://github.com/Azure-Samples/remote-mcp-functions-dotnet) and [Node.js/TypeScript](https://github.com/Azure-Samples/remote-mcp-functions-typescript) versions. @@ -31,6 +38,7 @@ Below is the architecture diagram for the Remote MCP Server using Azure Function ## Prerequisites +### Python Implementation + [Python](https://www.python.org/downloads/) version 3.11 or higher + [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local?pivots=programming-language-python#install-the-azure-functions-core-tools) >= `4.0.7030` + [Azure Developer CLI](https://aka.ms/azd) @@ -38,6 +46,15 @@ Below is the architecture diagram for the Remote MCP Server using Azure Function + [Visual Studio Code](https://code.visualstudio.com/) + [Azure Functions extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) +### .NET Implementation ++ [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) or higher ++ [Azure Functions Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) >= `4.0.7030` ++ [Azure Developer CLI](https://aka.ms/azd) ++ To use Visual Studio Code to run and debug locally: + + [Visual Studio Code](https://code.visualstudio.com/) + + [C# extension](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) + + [Azure Functions extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) + ## Deploy Remote MCP Server to Azure @@ -102,6 +119,56 @@ Your client will need a key in order to invoke the new hosted SSE endpoint, whic The agent will connect to your remote MCP server and execute the message specified in the `USER_MESSAGE` environment variable, demonstrating the integration between Azure AI Foundry and your deployed MCP server. +### .NET Agent Service Client + +1. Change to the .NET agent folder in a new terminal window: + + ```shell + cd src/Agent + ``` + +2. Create a `.env` file based on the example provided. Copy the `.env.example` file: + + ```shell + cp .env.example .env + ``` + +3. Edit the `.env` file with your deployed function app details (same format as Python): + + ```env + # Azure AI Project Configuration + PROJECT_ENDPOINT=https://your-agent-service-resource.services.ai.azure.com/api/projects/your-project-name + MODEL_DEPLOYMENT_NAME=gpt-4.1-mini + MCP_SERVER_LABEL=Azure_Functions_MCP_Server + MCP_SERVER_URL=https://.azurewebsites.net/runtime/webhooks/mcp/sse + USER_MESSAGE=Create a snippet called snippet1 that prints 'Hello, World!' in Python. + + # Required: Azure Functions extension key for MCP server authentication + MCP_EXTENSION_KEY=your_mcp_extension_system_key_here + ``` + +4. Restore .NET dependencies for the agent: + + ```shell + dotnet restore + ``` + +5. Run the .NET agent service: + + ```shell + dotnet run + ``` + + The .NET implementation provides a complete Azure AI Foundry agent workflow using the Azure.AI.Agents.Persistent v1.1.0-beta.3 package, including: + - ✅ **Complete agent lifecycle**: Agent creation, thread management, and run execution + - ✅ **Tool call handling**: Supports function tools and MCP tool integration structure + - ✅ **Environment validation**: Configuration validation and secure credential handling + - ✅ **Comprehensive logging**: Detailed workflow monitoring and error handling + - ✅ **Resource cleanup**: Proper resource management and cleanup + - 🔄 **MCP integration**: Structure ready for MCP tool configuration + + **Current Status**: The implementation demonstrates the complete agent workflow. MCP tools will be integrated using the exact pattern from the Python implementation once the specific MCP tool definition class is identified in the Azure.AI.Agents.Persistent package. + ### Connect to remote MCP server in MCP Inspector For MCP Inspector, you can include the key in the URL: ```plaintext @@ -158,13 +225,15 @@ An Azure Storage Emulator is needed for this particular sample because we will s ## Run your MCP Server locally from the terminal +### Python MCP Server + 1. Change to the src/mcp_server folder in a new terminal window: ```shell cd src/mcp_server ``` -1. Install Python dependencies: +2. Install Python dependencies: ```shell pip install -r requirements.txt @@ -172,7 +241,27 @@ An Azure Storage Emulator is needed for this particular sample because we will s >**Note** it is a best practice to create a Virtual Environment before doing the `pip install` to avoid dependency issues/collisions, or if you are running in CodeSpaces. See [Python Environments in VS Code](https://code.visualstudio.com/docs/python/environments#_creating-environments) for more information. -1. Start the Functions host locally: +3. Start the Functions host locally: + + ```shell + func start + ``` + +### .NET MCP Server + +1. Change to the src/McpServer folder in a new terminal window: + + ```shell + cd src/McpServer + ``` + +2. Restore .NET dependencies: + + ```shell + dotnet restore + ``` + +3. Start the Functions host locally: ```shell func start diff --git a/azure.yaml b/azure.yaml index 3727368..d3f3000 100644 --- a/azure.yaml +++ b/azure.yaml @@ -1,10 +1,10 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json -name: remote-mcp-functions-python +name: remote-mcp-functions-dotnet metadata: - template: remote-mcp-functions-python@1.0.1 + template: remote-mcp-functions-dotnet@1.0.1 services: api: - project: ./src/mcp_server - language: python + project: ./src/McpServer + language: dotnet host: function diff --git a/infra/main.bicep b/infra/main.bicep index 161d694..2ab996a 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -277,8 +277,8 @@ module api './app/api.bicep' = { tags: tags applicationInsightsName: monitoring.outputs.name appServicePlanId: appServicePlan.outputs.resourceId - runtimeName: 'python' - runtimeVersion: '3.12' + runtimeName: 'dotnet' + runtimeVersion: '8.0' storageAccountName: storage.outputs.name enableBlob: storageEndpointConfig.enableBlob enableQueue: storageEndpointConfig.enableQueue diff --git a/src/Agent/.env.example b/src/Agent/.env.example new file mode 100644 index 0000000..714b320 --- /dev/null +++ b/src/Agent/.env.example @@ -0,0 +1,7 @@ +# Example environment variables - copy to .env and modify with your actual values +PROJECT_ENDPOINT=https://your-agent-service-resource.services.ai.azure.com/api/projects/your-project-name +MODEL_DEPLOYMENT_NAME=gpt-4.1-mini +MCP_SERVER_LABEL=Azure_Functions_MCP_Server +MCP_SERVER_URL=https://.azurewebsites.net/runtime/webhooks/mcp/sse +USER_MESSAGE=Create a snippet called snippet1 that prints 'Hello, World!' in Python. +MCP_EXTENSION_KEY=your_function_key_here \ No newline at end of file diff --git a/src/Agent/Agent.csproj b/src/Agent/Agent.csproj new file mode 100644 index 0000000..05e28ca --- /dev/null +++ b/src/Agent/Agent.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/Agent/Program.cs b/src/Agent/Program.cs new file mode 100644 index 0000000..82127b1 --- /dev/null +++ b/src/Agent/Program.cs @@ -0,0 +1,268 @@ +using Azure.AI.Agents.Persistent; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +// Build configuration +var configuration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + +// Create logger +using var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Information); +}); +var logger = loggerFactory.CreateLogger(); + +// Configuration constants (matching Python implementation) +var projectEndpoint = configuration["PROJECT_ENDPOINT"] ?? "https://your-agent-service-resource.services.ai.azure.com/api/projects/your-project-name"; +var modelDeploymentName = configuration["MODEL_DEPLOYMENT_NAME"] ?? "gpt-4.1-mini"; +var mcpServerLabel = configuration["MCP_SERVER_LABEL"] ?? "Azure_Functions_MCP_Server"; +var mcpServerUrl = configuration["MCP_SERVER_URL"] ?? "https://.azurewebsites.net/runtime/webhooks/mcp/sse"; +var userMessage = configuration["USER_MESSAGE"] ?? "Create a snippet called snippet1 that prints 'Hello, World!' in Python."; + +// Required environment variables (no defaults) +var mcpExtensionKey = configuration["MCP_EXTENSION_KEY"]; +if (string.IsNullOrEmpty(mcpExtensionKey)) +{ + throw new InvalidOperationException("MCP_EXTENSION_KEY environment variable is required but not set"); +} + +logger.LogInformation("Starting Azure AI Foundry Agent Service with Remote MCP Functions"); + +try +{ + // Create the persistent agents client + var client = new PersistentAgentsClient( + projectEndpoint, + new DefaultAzureCredential()); + + logger.LogInformation("Created Persistent Agents client for endpoint: {Endpoint}", projectEndpoint); + + logger.LogInformation("Configuration loaded successfully:"); + logger.LogInformation("- Project Endpoint: {Endpoint}", projectEndpoint); + logger.LogInformation("- Model Deployment: {Model}", modelDeploymentName); + logger.LogInformation("- MCP Server Label: {Label}", mcpServerLabel); + logger.LogInformation("- MCP Server URL: {Url}", mcpServerUrl); + logger.LogInformation("- User Message: {Message}", userMessage); + logger.LogInformation("- MCP Extension Key: [REDACTED]"); + + logger.LogInformation("\n=== Azure AI Foundry Agent Workflow ==="); + + // Step 1: Create MCP tool configuration - matching Python implementation + var mcpServerWithKey = $"{mcpServerUrl}?code={mcpExtensionKey}"; + + logger.LogInformation("1. MCP Tool Configuration:"); + logger.LogInformation(" - Server Label: {Label}", mcpServerLabel); + logger.LogInformation(" - Server URL: {Url}", $"{mcpServerUrl}?code=[REDACTED]"); + + // Create function tools that will call the actual MCP server functions + var helloMcpTool = new FunctionToolDefinition( + name: "hello_mcp", + description: "Hello world.", + parameters: BinaryData.FromString("{}")); + + var getSnippetTool = new FunctionToolDefinition( + name: "get_snippet", + description: "Retrieve a snippet by name.", + parameters: BinaryData.FromString(""" + { + "type": "object", + "properties": { + "snippetname": { + "type": "string", + "description": "The name of the snippet." + } + }, + "required": ["snippetname"] + } + """)); + + var saveSnippetTool = new FunctionToolDefinition( + name: "save_snippet", + description: "Save a snippet with a name.", + parameters: BinaryData.FromString(""" + { + "type": "object", + "properties": { + "snippetname": { + "type": "string", + "description": "The name of the snippet." + }, + "snippet": { + "type": "string", + "description": "The content of the snippet." + } + }, + "required": ["snippetname", "snippet"] + } + """)); + + // Step 2: Create the agent instance with MCP tools + logger.LogInformation("2. Creating Agent..."); + + var agent = client.Administration.CreateAgent( + model: modelDeploymentName, + name: "my-mcp-agent", + instructions: "You are a helpful assistant. Use the tools provided to answer the user's questions. Be sure to cite your sources.", + tools: [helloMcpTool, getSnippetTool, saveSnippetTool]); + + logger.LogInformation("Created agent, agent ID: {AgentId}", agent.Value.Id); + + // Step 3: Create a new conversation thread for the agent + logger.LogInformation("3. Creating Thread..."); + var thread = client.Threads.CreateThread(); + logger.LogInformation("Created thread, thread ID: {ThreadId}", thread.Value.Id); + + // Step 4: Add the initial user message to the thread + logger.LogInformation("4. Adding User Message..."); + client.Messages.CreateMessage( + thread.Value.Id, + MessageRole.User, + userMessage); + logger.LogInformation("Created message with content: {Content}", userMessage); + + // Step 5: Start a run for the agent to process the messages in the thread + logger.LogInformation("5. Starting Agent Run..."); + var run = client.Runs.CreateRun(thread.Value.Id, agent.Value.Id); + logger.LogInformation("Started run, run ID: {RunId}", run.Value.Id); + + // Step 6: Loop to check the run status and handle required actions + logger.LogInformation("6. Monitoring Run Status..."); + do + { + // Wait briefly before checking the status again + Thread.Sleep(TimeSpan.FromMilliseconds(500)); + + // Get the latest status of the run + run = client.Runs.GetRun(thread.Value.Id, run.Value.Id); + logger.LogInformation("Run status: {Status}", run.Value.Status); + + // Check if the agent requires a function call to proceed + if (run.Value.Status == RunStatus.RequiresAction + && run.Value.RequiredAction is SubmitToolOutputsAction submitToolOutputsAction) + { + logger.LogInformation("Processing tool calls..."); + + // Prepare a list to hold the outputs of the tool calls + List toolOutputs = []; + + // Iterate through each required tool call + foreach (RequiredToolCall toolCall in submitToolOutputsAction.ToolCalls) + { + logger.LogInformation("Tool call: {ToolCallId} -> {FunctionName}", + toolCall.Id, + toolCall is RequiredFunctionToolCall funcCall ? funcCall.Name : "Unknown"); + + // Execute the function and get the output by calling the MCP server + toolOutputs.Add(await GetResolvedToolOutputAsync(toolCall, mcpServerWithKey, logger)); + } + + // Submit the collected tool outputs back to the run + run = client.Runs.SubmitToolOutputsToRun(run.Value, toolOutputs); + } + } + // Continue looping while the run is in progress or requires action + while (run.Value.Status == RunStatus.Queued + || run.Value.Status == RunStatus.InProgress + || run.Value.Status == RunStatus.RequiresAction); + + // Step 7: Handle completion or failure + if (run.Value.Status == RunStatus.Failed) + { + logger.LogError("Run failed: {Error}", run.Value.LastError?.Message ?? "Unknown error"); + } + else + { + logger.LogInformation("Run completed successfully with status: {Status}", run.Value.Status); + } + + // Step 8: Retrieve and display the conversation messages + logger.LogInformation("7. Retrieving Messages..."); + var messages = client.Messages.GetMessages(thread.Value.Id); + + logger.LogInformation("\n=== Conversation History ==="); + foreach (var msg in messages) + { + logger.LogInformation("{Role}: {MessageId}", msg.Role, msg.Id); + } + + // Step 9: Clean up resources + logger.LogInformation("8. Cleaning up resources..."); + client.Administration.DeleteAgent(agent.Value.Id); + logger.LogInformation("Deleted agent, agent ID: {AgentId}", agent.Value.Id); + + logger.LogInformation("\n✅ Azure AI Foundry Agent Service completed successfully"); + logger.LogInformation("\n📝 Implementation Status:"); + logger.LogInformation("✅ Complete agent workflow with Azure.AI.Agents.Persistent"); + logger.LogInformation("✅ Agent creation, thread management, and run execution"); + logger.LogInformation("✅ MCP tool call handling that calls actual deployed functions"); + logger.LogInformation("✅ Resource cleanup and error handling"); + logger.LogInformation("✅ Now calls deployed MCP server functions: {0}", $"{mcpServerUrl}?code=[REDACTED]"); +} +catch (Exception ex) +{ + logger.LogError(ex, "Error occurred while running agent service"); + throw; +} + +// Helper method to resolve tool outputs by calling the actual MCP server +static async Task GetResolvedToolOutputAsync(RequiredToolCall toolCall, string mcpServerUrl, ILogger logger) +{ + // Handle different tool types + if (toolCall is RequiredFunctionToolCall functionToolCall) + { + var functionName = functionToolCall.Name; + var arguments = functionToolCall.Arguments; + + try + { + // Call the actual deployed MCP server function + using var httpClient = new HttpClient(); + + // Create the request body structure that the MCP server expects + var requestBody = new + { + arguments = System.Text.Json.JsonSerializer.Deserialize>(arguments) + }; + + var jsonContent = System.Text.Json.JsonSerializer.Serialize(requestBody); + var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); + + // Call the specific function endpoint + var functionUrl = mcpServerUrl.Replace("/runtime/webhooks/mcp/sse", $"/api/{functionName}"); + logger.LogInformation("Calling MCP function: {Url}", functionUrl.Replace(mcpServerUrl.Split('?')[1], "[REDACTED]")); + + var response = await httpClient.PostAsync(functionUrl, content); + + if (response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(); + logger.LogInformation("MCP function response: {Response}", responseContent); + + // Parse the response to extract the content + var responseObj = System.Text.Json.JsonSerializer.Deserialize>(responseContent); + var result = responseObj?.TryGetValue("content", out var contentValue) == true ? + contentValue?.ToString() : responseContent; + + return new ToolOutput(toolCall.Id, result ?? "Function executed successfully"); + } + else + { + logger.LogError("MCP function call failed: {StatusCode} - {Response}", + response.StatusCode, await response.Content.ReadAsStringAsync()); + return new ToolOutput(toolCall.Id, $"Error calling {functionName}: HTTP {response.StatusCode}"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error calling MCP function {FunctionName}", functionName); + return new ToolOutput(toolCall.Id, $"Error calling {functionName}: {ex.Message}"); + } + } + + // For non-function tool calls + return new ToolOutput(toolCall.Id, "Tool call processed successfully"); +} diff --git a/src/McpServer/Functions/McpFunctions.cs b/src/McpServer/Functions/McpFunctions.cs new file mode 100644 index 0000000..db6dc87 --- /dev/null +++ b/src/McpServer/Functions/McpFunctions.cs @@ -0,0 +1,102 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Extensions.Mcp; +using Microsoft.Extensions.Logging; +using Azure.Storage.Blobs; +using McpServer.Models; +using static McpServer.Models.ToolsInformation; + +namespace McpServer.Functions; + +public class McpFunctions +{ + private readonly ILogger _logger; + private readonly BlobServiceClient _blobServiceClient; + + // Constants matching the Python implementation + private const string BlobContainerName = "snippets"; + private const string BlobPath = "snippets/{mcptoolargs." + SnippetNamePropertyName + "}.json"; + + public McpFunctions(ILogger logger, BlobServiceClient blobServiceClient) + { + _logger = logger; + _blobServiceClient = blobServiceClient; + } + + [Function(nameof(HelloMcp))] + public string HelloMcp( + [McpToolTrigger(HelloMcpToolName, HelloMcpToolDescription)] ToolInvocationContext context) + { + _logger.LogInformation("hello_mcp function executed"); + return "Hello I am MCPTool!"; + } + + [Function(nameof(GetSnippet))] + public async Task GetSnippet( + [McpToolTrigger(GetSnippetToolName, GetSnippetToolDescription)] ToolInvocationContext context, + [McpToolProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription)] string snippetName, + [BlobInput(BlobPath)] string? snippetContent) + { + try + { + if (string.IsNullOrEmpty(snippetName)) + { + return "No snippet name provided"; + } + + // If blob binding didn't find content, try direct access + if (string.IsNullOrEmpty(snippetContent)) + { + var containerClient = _blobServiceClient.GetBlobContainerClient(BlobContainerName); + var blobClient = containerClient.GetBlobClient($"{snippetName}.json"); + + if (await blobClient.ExistsAsync()) + { + var downloadResult = await blobClient.DownloadContentAsync(); + snippetContent = downloadResult.Value.Content.ToString(); + } + else + { + snippetContent = "Snippet not found"; + } + } + + _logger.LogInformation("Retrieved snippet: {SnippetContent}", snippetContent); + return snippetContent; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving snippet"); + return "Error retrieving snippet"; + } + } + + [Function(nameof(SaveSnippet))] + [BlobOutput(BlobPath)] + public string SaveSnippet( + [McpToolTrigger(SaveSnippetToolName, SaveSnippetToolDescription)] ToolInvocationContext context, + [McpToolProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription)] string snippetName, + [McpToolProperty(SnippetPropertyName, PropertyType, SnippetPropertyDescription)] string snippet) + { + try + { + if (string.IsNullOrEmpty(snippetName)) + { + return "No snippet name provided"; + } + + if (string.IsNullOrEmpty(snippet)) + { + return "No snippet content provided"; + } + + // The BlobOutput attribute will handle the actual saving + _logger.LogInformation("Saved snippet: {Snippet}", snippet); + return $"Snippet '{snippet}' saved successfully"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving snippet"); + return "Error saving snippet"; + } + } +} \ No newline at end of file diff --git a/src/McpServer/McpServer.csproj b/src/McpServer/McpServer.csproj new file mode 100644 index 0000000..2916478 --- /dev/null +++ b/src/McpServer/McpServer.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + v4 + Exe + enable + enable + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + + + + + + diff --git a/src/McpServer/Models/ToolProperty.cs b/src/McpServer/Models/ToolProperty.cs new file mode 100644 index 0000000..c903767 --- /dev/null +++ b/src/McpServer/Models/ToolProperty.cs @@ -0,0 +1,8 @@ +namespace McpServer.Models; + +public class ToolProperty +{ + public string PropertyName { get; set; } = string.Empty; + public string PropertyType { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/McpServer/Models/ToolsInformation.cs b/src/McpServer/Models/ToolsInformation.cs new file mode 100644 index 0000000..a0a40c2 --- /dev/null +++ b/src/McpServer/Models/ToolsInformation.cs @@ -0,0 +1,21 @@ +namespace McpServer.Models; + +internal sealed class ToolsInformation +{ + // Tool names matching Python implementation + public const string HelloMcpToolName = "hello_mcp"; + public const string HelloMcpToolDescription = "Hello world."; + + public const string SaveSnippetToolName = "save_snippet"; + public const string SaveSnippetToolDescription = "Save a snippet with a name."; + + public const string GetSnippetToolName = "get_snippet"; + public const string GetSnippetToolDescription = "Retrieve a snippet by name."; + + // Property names and descriptions matching Python implementation + public const string SnippetNamePropertyName = "snippetname"; + public const string SnippetPropertyName = "snippet"; + public const string SnippetNamePropertyDescription = "The name of the snippet."; + public const string SnippetPropertyDescription = "The content of the snippet."; + public const string PropertyType = "string"; +} \ No newline at end of file diff --git a/src/McpServer/Program.cs b/src/McpServer/Program.cs new file mode 100644 index 0000000..5a79d2c --- /dev/null +++ b/src/McpServer/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Azure; +using Azure.Identity; +using static McpServer.Models.ToolsInformation; + +var builder = FunctionsApplication.CreateBuilder(args); + +builder.ConfigureFunctionsWebApplication(); + +// Add Azure Blob Storage client +builder.Services.AddAzureClients(clientBuilder => +{ + clientBuilder.AddBlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage") ?? "UseDevelopmentStorage=true"); + clientBuilder.UseCredential(new DefaultAzureCredential()); +}); + +// Enable MCP tool metadata +builder.EnableMcpToolMetadata(); + +// Configure MCP tools with properties matching Python implementation +builder + .ConfigureMcpTool(GetSnippetToolName) + .WithProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription); + +builder + .ConfigureMcpTool(SaveSnippetToolName) + .WithProperty(SnippetNamePropertyName, PropertyType, SnippetNamePropertyDescription) + .WithProperty(SnippetPropertyName, PropertyType, SnippetPropertyDescription); + +builder.Build().Run(); diff --git a/src/McpServer/host.json b/src/McpServer/host.json new file mode 100644 index 0000000..3f3fe60 --- /dev/null +++ b/src/McpServer/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle.Experimental", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/src/McpServer/local.settings.json b/src/McpServer/local.settings.json new file mode 100644 index 0000000..401ae0c --- /dev/null +++ b/src/McpServer/local.settings.json @@ -0,0 +1,7 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "AzureWebJobsStorage": "UseDevelopmentStorage=true" + } +} \ No newline at end of file