diff --git a/Dockerfile.awesome-azd b/Dockerfile.awesome-azd new file mode 100644 index 0000000..cb4e4ad --- /dev/null +++ b/Dockerfile.awesome-azd @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:1 + +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build + +COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared +COPY ./awesome-azd/src/McpSamples.AwesomeAzd.HybridApp /source/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp + +WORKDIR /source/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp + +ARG TARGETARCH +RUN case "$TARGETARCH" in \ + "amd64") RID="linux-musl-x64" ;; \ + "arm64") RID="linux-musl-arm64" ;; \ + *) RID="linux-musl-x64" ;; \ + esac && \ + dotnet publish -c Release -o /app -r $RID --self-contained false + +FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final + +WORKDIR /app + +COPY --from=build /app . + +USER $APP_UID + +ENTRYPOINT ["dotnet", "McpSamples.AwesomeAzd.HybridApp.dll"] diff --git a/Dockerfile.awesome-azd-azure b/Dockerfile.awesome-azd-azure new file mode 100644 index 0000000..e9a74c8 --- /dev/null +++ b/Dockerfile.awesome-azd-azure @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 + +FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build + +COPY ./shared/McpSamples.Shared /source/shared/McpSamples.Shared +COPY ./awesome-azd/src/McpSamples.AwesomeAzd.HybridApp /source/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp + +WORKDIR /source/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp + +RUN dotnet publish -c Release -o /app --self-contained false + +FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final + +WORKDIR /app + +COPY --from=build /app . + +USER $APP_UID + +ENTRYPOINT ["dotnet", "McpSamples.AwesomeAzd.HybridApp.dll"] diff --git a/awesome-azd/.vscode/mcp.http.container.json b/awesome-azd/.vscode/mcp.http.container.json new file mode 100644 index 0000000..8574142 --- /dev/null +++ b/awesome-azd/.vscode/mcp.http.container.json @@ -0,0 +1,8 @@ +{ + "servers": { + "awesome-azd": { + "type": "http", + "url": "http://localhost:8080/mcp" + } + } +} diff --git a/awesome-azd/.vscode/mcp.http.local.json b/awesome-azd/.vscode/mcp.http.local.json new file mode 100644 index 0000000..27e5753 --- /dev/null +++ b/awesome-azd/.vscode/mcp.http.local.json @@ -0,0 +1,8 @@ +{ + "servers": { + "awesome-azd": { + "type": "http", + "url": "http://0.0.0.0:5201/mcp" + } + } +} diff --git a/awesome-azd/.vscode/mcp.http.remote.json b/awesome-azd/.vscode/mcp.http.remote.json new file mode 100644 index 0000000..bb4c092 --- /dev/null +++ b/awesome-azd/.vscode/mcp.http.remote.json @@ -0,0 +1,15 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "acaapp-server-fqdn", + "description": "Azure Container Apps FQDN" + } + ], + "servers": { + "awesome-azd": { + "type": "http", + "url": "https://${input:acaapp-server-fqdn}/mcp" + } + } +} diff --git a/awesome-azd/.vscode/mcp.stdio.container.json b/awesome-azd/.vscode/mcp.stdio.container.json new file mode 100644 index 0000000..f8d0eea --- /dev/null +++ b/awesome-azd/.vscode/mcp.stdio.container.json @@ -0,0 +1,14 @@ +{ + "servers": { + "awesome-azd": { + "type": "stdio", + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "awesome-azd:latest" + ] + } + } +} diff --git a/awesome-azd/.vscode/mcp.stdio.local.json b/awesome-azd/.vscode/mcp.stdio.local.json new file mode 100644 index 0000000..3a1a7c2 --- /dev/null +++ b/awesome-azd/.vscode/mcp.stdio.local.json @@ -0,0 +1,20 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "consoleapp-project-path", + "description": "The absolute path to the console app project Directory" + } + ], + "servers": { + "awesome-azd": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "${input:consoleapp-project-path}" + ] + } + } +} diff --git a/awesome-azd/McpAwesomeAzd.sln b/awesome-azd/McpAwesomeAzd.sln new file mode 100644 index 0000000..69a72db --- /dev/null +++ b/awesome-azd/McpAwesomeAzd.sln @@ -0,0 +1,53 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpSamples.Shared", "..\shared\McpSamples.Shared\McpSamples.Shared.csproj", "{1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "McpSamples.AwesomeAzd.HybridApp", "src\McpSamples.AwesomeAzd.HybridApp\McpSamples.AwesomeAzd.HybridApp.csproj", "{268B3C6A-8A50-4B8F-A74C-194E3875786C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Debug|x64.Build.0 = Debug|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Debug|x86.Build.0 = Debug|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Release|Any CPU.Build.0 = Release|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Release|x64.ActiveCfg = Release|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Release|x64.Build.0 = Release|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Release|x86.ActiveCfg = Release|Any CPU + {1A5BAB62-0AA5-4A38-B2FC-262A3E03C382}.Release|x86.Build.0 = Release|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Debug|x64.ActiveCfg = Debug|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Debug|x64.Build.0 = Debug|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Debug|x86.ActiveCfg = Debug|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Debug|x86.Build.0 = Debug|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Release|Any CPU.Build.0 = Release|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Release|x64.ActiveCfg = Release|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Release|x64.Build.0 = Release|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Release|x86.ActiveCfg = Release|Any CPU + {268B3C6A-8A50-4B8F-A74C-194E3875786C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {268B3C6A-8A50-4B8F-A74C-194E3875786C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/awesome-azd/README.md b/awesome-azd/README.md new file mode 100644 index 0000000..569b9c0 --- /dev/null +++ b/awesome-azd/README.md @@ -0,0 +1,270 @@ +# MCP Server: Awesome Azd + +This is an MCP server that provides search functionality for Awesome AZD templates from the [awesome-azd](https://github.com/Azure/awesome-azd) repository. + +## Install + + +## Prerequisites + +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) +- [Visual Studio Code](https://code.visualstudio.com/) with + - [C# Dev Kit](https://marketplace.visualstudio.com/items/?itemName=ms-dotnettools.csdevkit) extension +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) +- [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) +- [Docker Desktop](https://docs.docker.com/get-started/get-docker/) + +## What's Included + +Awesome Azd MCP server includes: + +| Building Block | Name | Description | Usage | +|----------------|--------------------------------|-----------------------------------------------------------------------|-----------------------------------------------| +| Tools | `get_templates` | Searches templates based on keywords in their descriptions. | `#get_templates` | +| Tools | `make_command` | Generates the azd init command to be executed. | `#make_command` | +| Prompts | `get_template_search_prompt` | Get a prompt for searching azd templates. | `/mcp.awesome-azd.get_template_search_prompt` | + +## Getting Started + +- [Getting repository root](#getting-repository-root) +- [Running MCP server](#running-mcp-server) + - [On a local machine](#on-a-local-machine) + - [In a container](#in-a-container) + - [On Azure](#on-azure) +- [Connect MCP server to an MCP host/client](#connect-mcp-server-to-an-mcp-hostclient) + - [VS Code + Agent Mode + Local MCP server](#vs-code--agent-mode--local-mcp-server) + +### Getting repository root + +1. Get the repository root. + + ```bash + # bash/zsh + REPOSITORY_ROOT=$(git rev-parse --show-toplevel) + ``` + + ```powershell + # PowerShell + $REPOSITORY_ROOT = git rev-parse --show-toplevel + ``` + +### Running MCP server + +#### On a local machine + +1. Run the MCP server app. + + ```bash + cd $REPOSITORY_ROOT/awesome-azd + dotnet run --project ./src/McpSamples.AwesomeAzd.HybridApp + ``` + + > Make sure take note the absolute directory path of the `McpSamples.AwesomeAzd.HybridApp` project. + + **Parameters**: + + - `--http`: The switch that indicates to run this MCP server as a streamable HTTP type. When this switch is added, the MCP server URL is `http://localhost:5201`. + + With this parameter, you can run the MCP server like: + + ```bash + dotnet run --project ./src/McpSamples.AwesomeAzd.HybridApp -- --http + ``` + +#### In a container + +1. Build the MCP server app as a container image. + + ```bash + cd $REPOSITORY_ROOT + docker build -f Dockerfile.awesome-azd -t awesome-azd:latest . + ``` + +1. Run the MCP server app in a container. + + ```bash + docker run -i --rm -p 8080:8080 awesome-azd:latest + ``` + + **Parameters**: + + - `--http`: The switch that indicates to run this MCP server as a streamable HTTP type. When this switch is added, the MCP server URL is `http://localhost:8080`. + + With this parameter, you can run the MCP server like: + + ```bash + # use local container image + docker run -i --rm -p 8080:8080 awesome-azd:latest --http + ``` + +#### On Azure + +1. Navigate to the directory. + + ```bash + cd $REPOSITORY_ROOT/awesome-azd + ``` + +1. Login to Azure. + + ```bash + # Login with Azure Developer CLI + azd auth login + ``` + +1. Deploy the MCP server app to Azure. + + ```bash + azd up + ``` + + While provisioning and deploying, you'll be asked to provide subscription ID, location, environment name. + +1. After the deployment is complete, get the information by running the following commands: + + - Azure Container Apps FQDN: + + ```bash + azd env get-value AZURE_RESOURCE_MCP_AWESOME_AZD_FQDN + ``` + +### Connect MCP server to an MCP host/client + +#### VS Code + Agent Mode + Local MCP server + +1. Copy `mcp.json` to the repository root. + + **For locally running MCP server (STDIO):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.stdio.local.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.stdio.local.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For locally running MCP server (HTTP):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.http.local.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.http.local.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For locally running MCP server in a container (STDIO):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.stdio.container.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.stdio.container.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For locally running MCP server in a container (HTTP):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.http.container.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.http.container.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + + **For remotely running MCP server in a container (HTTP):** + + ```bash + mkdir -p $REPOSITORY_ROOT/.vscode + cp $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.http.remote.json \ + $REPOSITORY_ROOT/.vscode/mcp.json + ``` + + ```powershell + New-Item -Type Directory -Path $REPOSITORY_ROOT/.vscode -Force + Copy-Item -Path $REPOSITORY_ROOT/awesome-azd/.vscode/mcp.http.remote.json ` + -Destination $REPOSITORY_ROOT/.vscode/mcp.json -Force + ``` + +1. Open Command Palette by typing `F1` or `Ctrl`+`Shift`+`P` on Windows or `Cmd`+`Shift`+`P` on Mac OS, and search `MCP: List Servers`. +1. Choose `awesome-azd` then click `Start Server`. +1. When prompted, enter one of the following values: + - The absolute directory path of the `McpSamples.AwesomeAzd.HybridApp` project + - The FQDN of Azure Container Apps. +1. Use a prompt by typing `/mcp.awesome-azd.get_template_search_prompt` and enter keywords to search. You'll get a prompt like: + + ```text + Please search all Azure templates related to the keyword `{keyword}`. + + ------------------------------------------------------------ + 1. Search templates + ------------------------------------------------------------ + - Use the `awesome-azd` MCP server. + - Call the `get_templates` tool with the keyword `{keyword}`. + - Return the results as a table with the columns: Title | Description | Source. + - Only include templates whose metadata matches the keyword. + + ------------------------------------------------------------ + 2. Collect user input for executi on and Backend execution + ------------------------------------------------------------ + After displaying the search results, ask the user to provide: + + 1. **Selected Template Source** (must match the Source column; e.g., GitHub URL) + 2. **Working Directory** (optional; leave empty for default/null) + 3. **Environment Name** (optional; leave empty for default/null) + + - If the user provides a **Selected Template Source**, you may proceed to execution immediately. + + **IMPORTANT** - If the user does not provide a working directory or an environment name, keep asking repeatedly until they provide one. + + Example input: + + https://github.com/Azure-Samples/azure-search-openai-demo-csharp ; C:\Users\\Projects ; AzureEnv + + + - Call the `make_command` MCP tool with a JSON object like this: + + "srcPath": "Selected Template Source", + "workingDirectory", + "envName" + + - The tool will return an `AzdCommand` object. + - Use `run in terminal` to execute it: + + - mkdir "" -Force; cd ""; (Window) + - mkdir -p "" && cd "" && (MacOS, Linux) + + - Once the command is generated, execute it immediately without further confirmation. + - The command will initialize the template in the specified directory and return success status, output, and any errors. + + ``` + +1. After the search results appear, enter the following three values when prompted: + + - **Selected Template Source**: The template URL shown in the search result (Source column) + - **Working Directory**: The directory where the azd template will be initialized + - **Environment Name**: The name of the Azure Developer CLI environment to create or use + + Once you provide these values and confirm, the MCP server will automatically generate + the appropriate `azd init` command and execute it in your terminal. + + +1. Confirm the result. diff --git a/awesome-azd/azure.yaml b/awesome-azd/azure.yaml new file mode 100644 index 0000000..7708741 --- /dev/null +++ b/awesome-azd/azure.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: awesome-azd + +metadata: + template: azd-init@1.14.0 + +services: + awesome-azd: + project: src/McpSamples.AwesomeAzd.HybridApp + host: containerapp + language: dotnet + docker: + path: ../../../Dockerfile.awesome-azd-azure + context: ../../../ + remoteBuild: true diff --git a/awesome-azd/infra/abbreviations.json b/awesome-azd/infra/abbreviations.json new file mode 100644 index 0000000..1533dee --- /dev/null +++ b/awesome-azd/infra/abbreviations.json @@ -0,0 +1,136 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "documentDBMongoDatabaseAccounts": "cosmon-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} diff --git a/awesome-azd/infra/main.bicep b/awesome-azd/infra/main.bicep new file mode 100644 index 0000000..7167fd1 --- /dev/null +++ b/awesome-azd/infra/main.bicep @@ -0,0 +1,47 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +param mcpAwesomeAzdExists bool + +@description('Id of the user or app to assign application roles') +param principalId string + +// Tags that should be applied to all resources. +// +// Note that 'azd-service-name' tags should be applied separately to service host resources. +// Example usage: +// tags: union(tags, { 'azd-service-name': }) +var tags = { + 'azd-env-name': environmentName +} + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + mcpAwesomeAzdExists: mcpAwesomeAzdExists + } +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_RESOURCE_MCP_AWESOME_AZD_ID string = resources.outputs.AZURE_RESOURCE_MCP_AWESOME_AZD_ID +output AZURE_RESOURCE_MCP_AWESOME_AZD_NAME string = resources.outputs.AZURE_RESOURCE_MCP_AWESOME_AZD_NAME +output AZURE_RESOURCE_MCP_AWESOME_AZD_FQDN string = resources.outputs.AZURE_RESOURCE_MCP_AWESOME_AZD_FQDN diff --git a/awesome-azd/infra/main.parameters.json b/awesome-azd/infra/main.parameters.json new file mode 100644 index 0000000..1889ec6 --- /dev/null +++ b/awesome-azd/infra/main.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "mcpAwesomeAzdExists": { + "value": "${SERVICE_AWESOME_AZD_RESOURCE_EXISTS=false}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} diff --git a/awesome-azd/infra/modules/fetch-container-image.bicep b/awesome-azd/infra/modules/fetch-container-image.bicep new file mode 100644 index 0000000..78d1e7e --- /dev/null +++ b/awesome-azd/infra/modules/fetch-container-image.bicep @@ -0,0 +1,8 @@ +param exists bool +param name string + +resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { + name: name +} + +output containers array = exists ? existingApp.properties.template.containers : [] diff --git a/awesome-azd/infra/resources.bicep b/awesome-azd/infra/resources.bicep new file mode 100644 index 0000000..c89e455 --- /dev/null +++ b/awesome-azd/infra/resources.bicep @@ -0,0 +1,144 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location + +@description('Tags that will be applied to all resources') +param tags object = {} + +param mcpAwesomeAzdExists bool + +@description('Id of the user or app to assign application roles') +param principalId string + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) + +// Monitor application with Azure Monitor +module monitoring 'br/public:avm/ptn/azd/monitoring:0.1.0' = { + name: 'monitoring' + params: { + logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: '${abbrs.portalDashboards}${resourceToken}' + location: location + tags: tags + } +} + +// Container registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + params: { + name: '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments: [ + { + principalId: mcpAwesomeAzdIdentity.outputs.principalId + principalType: 'ServicePrincipal' + // ACR pull role + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } + ] + } +} + +// Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { + name: 'container-apps-environment' + params: { + logAnalyticsWorkspaceResourceId: monitoring.outputs.logAnalyticsWorkspaceResourceId + name: '${abbrs.appManagedEnvironments}${resourceToken}' + location: location + zoneRedundant: false + } +} + +// User assigned identity +module mcpAwesomeAzdIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'mcpAwesomeAzdIdentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}mcpawesomeazd-${resourceToken}' + location: location + } +} + +// Azure Container Apps +module mcpAwesomeAzdFetchLatestImage './modules/fetch-container-image.bicep' = { + name: 'mcpAwesomeAzd-fetch-image' + params: { + exists: mcpAwesomeAzdExists + name: 'awesome-azd' + } +} + +module mcpAwesomeAzd 'br/public:avm/res/app/container-app:0.8.0' = { + name: 'mcpAwesomeAzd' + params: { + name: 'awesome-azd' + ingressTargetPort: 8080 + scaleMinReplicas: 1 + scaleMaxReplicas: 10 + secrets: { + secureList: [ + ] + } + containers: [ + { + image: mcpAwesomeAzdFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'main' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + env: [ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString + } + { + name: 'AZURE_CLIENT_ID' + value: mcpAwesomeAzdIdentity.outputs.clientId + } + { + name: 'PORT' + value: '8080' + } + ] + args: [ + '--http' + ] + } + ] + managedIdentities: { + systemAssigned: false + userAssignedResourceIds: [ + mcpAwesomeAzdIdentity.outputs.resourceId + ] + } + registries: [ + { + server: containerRegistry.outputs.loginServer + identity: mcpAwesomeAzdIdentity.outputs.resourceId + } + ] + environmentResourceId: containerAppsEnvironment.outputs.resourceId + corsPolicy: { + allowedOrigins: [ + 'https://make.preview.powerapps.com' + 'https://make.powerapps.com' + 'https://make.preview.powerautomate.com' + 'https://make.powerautomate.com' + 'https://copilotstudio.preview.microsoft.com' + 'https://copilotstudio.microsoft.com' + ] + } + location: location + tags: union(tags, { 'azd-service-name': 'awesome-azd' }) + } +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_RESOURCE_MCP_AWESOME_AZD_ID string = mcpAwesomeAzd.outputs.resourceId +output AZURE_RESOURCE_MCP_AWESOME_AZD_NAME string = mcpAwesomeAzd.outputs.name +output AZURE_RESOURCE_MCP_AWESOME_AZD_FQDN string = mcpAwesomeAzd.outputs.fqdn diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Configurations/AwesomeAzdAppSettings.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Configurations/AwesomeAzdAppSettings.cs new file mode 100644 index 0000000..d43b3ed --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Configurations/AwesomeAzdAppSettings.cs @@ -0,0 +1,19 @@ +using McpSamples.Shared.Configurations; + +using Microsoft.OpenApi.Models; + +namespace McpSamples.AwesomeAzd.HybridApp.Configurations; + +/// +/// This represents the application settings for awesome-azd app. +/// +public class AwesomeAzdAppSettings : AppSettings +{ + /// + public override OpenApiInfo OpenApi { get; set; } = new() + { + Title = "MCP Awesome Azd", + Version = "1.0.0", + Description = "A simple MCP server for searching and loading custom instructions from the awesome-azd repository." + }; +} diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/McpSamples.AwesomeAzd.HybridApp.csproj b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/McpSamples.AwesomeAzd.HybridApp.csproj new file mode 100644 index 0000000..97267b2 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/McpSamples.AwesomeAzd.HybridApp.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + latest + + enable + enable + + McpSamples.AwesomeAzd.HybridApp + McpSamples.AwesomeAzd.HybridApp + + e01e0699-c7a8-4900-be2b-6c4e27440ed7 + + + + + + + + diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AwesomeAzdTemplateDomain.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AwesomeAzdTemplateDomain.cs new file mode 100644 index 0000000..43648b7 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AwesomeAzdTemplateDomain.cs @@ -0,0 +1,56 @@ +namespace McpSamples.AwesomeAzd.HybridApp.Models; + +/// +/// This represents a template from the awesome-azd API. +/// +public class AwesomeAzdTemplateDomain +{ + /// + /// Gets or sets the unique identifier for the awesome-azd template. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the title for the awesome-azd template. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the description for the awesome-azd template. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the author URL (e.g., GitHub profile or team page). + /// The keys "AuthorUrl" and "website" have the same meaning but are spelled differently in the JSON file. + /// + public string AuthorUrl { get; set; } = string.Empty; + + /// + /// Gets or sets the author name of the awesome-azd template. + /// + public string Author { get; set; } = string.Empty; + + /// + /// Gets or sets the source repository URL of the awesome-azd template. + /// + public string Source { get; set; } = string.Empty; + + /// + /// Gets or sets the tags that describe the awesome-azd template. + /// + public List Tags { get; set; } = new(); + + /// + /// Gets or sets the list of Azure services used by the awesome-azd template. + /// The keys "azure_service" and "AzureService" have the same meaning but are spelled differently in the JSON file. + /// + public List AzureServices { get; set; } = new(); + + /// + /// Gets or sets the list of main programming languages used by the awesome-azd template. + /// The keys "language" and "languages" have the same meaning but are spelled differently in the JSON file. + /// + public List Languages { get; set; } = new(); +} + diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AwesomeAzdTemplateResponse.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AwesomeAzdTemplateResponse.cs new file mode 100644 index 0000000..f97434b --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AwesomeAzdTemplateResponse.cs @@ -0,0 +1,28 @@ +namespace McpSamples.AwesomeAzd.HybridApp.Models; + +/// +/// This represents a simplified model of an awesome-azd template for LLM responses. +/// +public class AwesomeAzdTemplateResponse +{ + /// + /// Gets or sets the unique identifier for the awesome-azd template. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the title for the awesome-azd template. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the description for the awesome-azd template. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the source repository URL of the awesome-azd template. + /// + public string Source { get; set; } = string.Empty; + +} diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AzdCommand.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AzdCommand.cs new file mode 100644 index 0000000..740e456 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Models/AzdCommand.cs @@ -0,0 +1,17 @@ +namespace McpSamples.AwesomeAzd.HybridApp.Models; + +/// +/// Represents a terminal command and its working directory. +/// +public class AzdCommand +{ + /// + /// The full command string to execute (e.g., azd init ...). + /// + public string Command { get; set; } = string.Empty; + + /// + /// The working directory in which the command should be executed. + /// + public string WorkingDirectory { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Program.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Program.cs new file mode 100644 index 0000000..e7bcb43 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Program.cs @@ -0,0 +1,20 @@ +using McpSamples.AwesomeAzd.HybridApp.Services; +using McpSamples.AwesomeAzd.HybridApp.Configurations; +using McpSamples.AwesomeAzd.HybridApp.Tools; +using McpSamples.Shared.Configurations; +using McpSamples.Shared.Extensions; +using McpSamples.Shared.OpenApi; + +var useStreamableHttp = AppSettings.UseStreamableHttp(Environment.GetEnvironmentVariables(), args); + +IHostApplicationBuilder builder = useStreamableHttp + ? WebApplication.CreateBuilder(args) + : Host.CreateApplicationBuilder(args); + +builder.Services.AddAppSettings(builder.Configuration, args); + +builder.Services.AddHttpClient(); + +IHost app = builder.BuildApp(useStreamableHttp); + +await app.RunAsync(); diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Prompts/TemplatePrompt.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Prompts/TemplatePrompt.cs new file mode 100644 index 0000000..82bbda9 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Prompts/TemplatePrompt.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +namespace McpSamples.AwesomeAzd.HybridApp.Prompts; + +/// +/// Provides an interface for generating prompts that guide template searches. +/// +public interface ITemplatePrompt +{ + /// + /// Gets a prompt for searching Azure templates by keyword. + /// + /// The keyword to search for. + /// A formatted search prompt. + string GetSearchPrompt(string keyword); +} + +/// +/// Represents the prompt entity for the Awesome AZD templates repository. +/// +[McpServerPromptType] +public class TemplatePrompt : ITemplatePrompt +{ + /// + [McpServerPrompt(Name = "get_template_search_prompt", Title = "Prompt for searching AZD templates")] + [Description("Get a prompt for searching Azure templates by keyword.")] + public string GetSearchPrompt( + [Description("The keyword to search for")] string keyword) + { + return $""" + Please search all Azure templates related to the keyword `{keyword}`. + + ------------------------------------------------------------ + 1. Search templates + ------------------------------------------------------------ + - Use the `awesome-azd` MCP server. + - Call the `get_templates` tool with the keyword `{keyword}`. + - Return the results as a table with the columns: Title | Description | Source. + - Only include templates whose metadata matches the keyword. + + ------------------------------------------------------------ + 2. Collect user input for execution and Backend execution + ------------------------------------------------------------ + After displaying the search results, ask the user to provide: + + 1. **Selected Template Source** (must match the Source column; e.g., GitHub URL) + 2. **Working Directory** (optional; leave empty for default/null) + 3. **Environment Name** (optional; leave empty for default/null) + + - If the user provides a **Selected Template Source**, you may proceed to execution immediately. + + **IMPORTANT** - If the user does not provide a working directory or an environment name, keep asking repeatedly until they provide one. + + Example input: + + https://github.com/Azure-Samples/azure-search-openai-demo-csharp ; C:\Users\\Projects ; AzureEnv + + + - Call the `make_command` MCP tool with a JSON object like this: + + "srcPath": "Selected Template Source", + "workingDirectory", + "envName" + + - The tool will return an `AzdCommand` object. + - Use `run in terminal` to execute it: + + - mkdir "" -Force; cd ""; (Window) + - mkdir -p "" && cd "" && (MacOS, Linux) + + - Once the command is generated, execute it immediately without further confirmation. + - The command will initialize the template in the specified directory and return success status, output, and any errors. + """; + + } +} diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Properties/launchSettings.json b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Properties/launchSettings.json new file mode 100644 index 0000000..4645609 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:45201;http://localhost:5201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Services/AwesomeAzdService.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Services/AwesomeAzdService.cs new file mode 100644 index 0000000..4e5193d --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Services/AwesomeAzdService.cs @@ -0,0 +1,89 @@ +namespace McpSamples.AwesomeAzd.HybridApp.Services; + +using System.Diagnostics; +using System.Text.Json; +using System; +using System.Threading; +using System.Threading.Tasks; +using McpSamples.AwesomeAzd.HybridApp.Models; + +public class AwesomeAzdService(HttpClient http, ILogger logger) : IAwesomeAzdService +{ + private const string AwesomeAzdTemplateFileUrl = "https://raw.githubusercontent.com/Azure/awesome-azd/main/website/static/templates.json"; + + public async Task> GetTemplateListAsync(string keywords, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(keywords)) + { + return new List(); + } + + var templates = await GetTemplatesAsync(cancellationToken).ConfigureAwait(false); + + var searchTerms = keywords.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Select(term => term.Trim().ToLowerInvariant()) + .Where(term => string.IsNullOrWhiteSpace(term) != true) + .ToArray(); + + logger.LogInformation("Search terms: {terms}", string.Join(", ", searchTerms)); + + var searchResult = templates + .Where(t => ContainsAnyKeyword(t.Title, searchTerms) + || ContainsAnyKeyword(t.Description, searchTerms) + || ContainsAnyKeyword(t.Author, searchTerms) + || ContainsAnyKeyword(t.Source, searchTerms) + || (t.Tags?.Any(tag => ContainsAnyKeyword(tag, searchTerms)) ?? false) + || (t.Languages?.Any(lang => ContainsAnyKeyword(lang, searchTerms)) ?? false) + || (t.AzureServices?.Any(svc => ContainsAnyKeyword(svc, searchTerms)) ?? false)) + .ToList(); + + var responseList = searchResult.Select(m => new AwesomeAzdTemplateResponse + { + Id = m.Id, + Title = m.Title, + Description = m.Description, + Source = m.Source + }).ToList(); + + return responseList; + } + + + private async Task> GetTemplatesAsync(CancellationToken cancellationToken) + { + + try + { + logger.LogInformation("Fetching templates from {url}", AwesomeAzdTemplateFileUrl); + + var response = await http.GetAsync(AwesomeAzdTemplateFileUrl, cancellationToken); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(cancellationToken); + + var result = JsonSerializer.Deserialize>(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) + ?? new List(); + + logger.LogInformation("Loaded {count} templates.", result.Count); + + return result; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to fetch or deserialize templates."); + return new List(); + } + } + + private static bool ContainsAnyKeyword(string? text, string[] searchTerms) + { + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + return searchTerms.Any(term => text.Contains(term, StringComparison.InvariantCultureIgnoreCase)); + } + +} \ No newline at end of file diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Services/IAwesomeAzdService.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Services/IAwesomeAzdService.cs new file mode 100644 index 0000000..aae8d01 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Services/IAwesomeAzdService.cs @@ -0,0 +1,19 @@ +using McpSamples.AwesomeAzd.HybridApp.Models; + +namespace McpSamples.AwesomeAzd.HybridApp.Services +{ + /// + /// Provides interfaces for metadata service operations for Awesome AZD templates. + /// + public interface IAwesomeAzdService + { + /// + /// Searches for relevant templates in the Awesome AZD repository based on keywords. + /// + /// The keywords to search for. + /// Cancellation token for the async operation. + /// A containing all matching search results. + Task> GetTemplateListAsync(string keywords, CancellationToken cancellationToken = default); + + } +} diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Tools/AwesomeAzdTool.cs b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Tools/AwesomeAzdTool.cs new file mode 100644 index 0000000..e99e868 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/Tools/AwesomeAzdTool.cs @@ -0,0 +1,101 @@ +using System.ComponentModel; +using System.Text.RegularExpressions; + +using McpSamples.AwesomeAzd.HybridApp.Services; +using McpSamples.AwesomeAzd.HybridApp.Models; + +using ModelContextProtocol.Server; + +namespace McpSamples.AwesomeAzd.HybridApp.Tools; + +/// +/// Provides MCP tool operations for Azure Developer templates. +/// +public interface IAwesomeAzdTool +{ + /// + /// Searches available Azure Developer templates by keyword. + /// + /// The keyword to search templates for + /// A list of matching template titles. + Task> GetTemplateListAsync(string keywords); + + /// + /// Generates an AzdCommand object with default working directory and environment. + /// + /// GitHub repository URL for the template + /// Working directory where the command would run + /// Name of the environment to apply + /// A task that resolves to an . + Task CreateCommandAsync( + [Description("GitHub repository URL for the template")] string srcPath, + [Description("Working directory where the command would run")] string workingDirectory, + [Description("Name of the environment to apply")] string envName); + +} + +/// +/// This represents the tools entity for Awesome Azd template operations. +/// +[McpServerToolType] +public class AwesomeAzdTool(IAwesomeAzdService service, ILogger logger) : IAwesomeAzdTool +{ + /// + [McpServerTool(Name = "get_templates", Title = "Search Azure Developer templates")] + [Description("Searches available Azure Developer templates by keyword.")] + public async Task> GetTemplateListAsync( + [Description("The keyword to search templates for")] string keywords) + { + var result = new List(); + + try + { + var templates = await service.GetTemplateListAsync(keywords).ConfigureAwait(false); + result = templates; + + logger.LogInformation("Template search completed successfully for keyword '{Keywords}'.", keywords); + } + catch (Exception ex) + { + logger.LogError(ex, "Error occurred while searching templates with keyword '{Keywords}'.", keywords); + result.Add(new AwesomeAzdTemplateResponse + { + Title = "Error", + Description = ex.Message + }); + } + + return result; + } + + /// + [McpServerTool(Name = "make_command", Title = "Generate an AzdCommand with defaults")] + [Description("Generates an AzdCommand with default working directory.")] + public Task CreateCommandAsync( + [Description("GitHub repository URL for the template")] string srcPath, + [Description("Working directory where the command would run")] string workingDirectory, + [Description("Name of the environment to apply")] string envName) + { + string ownerRepo = srcPath; + + var match = Regex.Match(srcPath, @"github\.com/([^/]+/[^/]+)"); + if (match.Success) + { + ownerRepo = match.Groups[1].Value; + } + + var command = $"azd init -t {ownerRepo} --environment {envName}"; + + logger.LogInformation("Generated AzdCommand for ownerRepo '{ownerRepo}' at directory '{workingDirectory}'", ownerRepo, workingDirectory); + + var azdCommand = new AzdCommand + { + Command = command, + WorkingDirectory = workingDirectory + }; + + return Task.FromResult(azdCommand); + } + + +} diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/appsettings.Development.json b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/appsettings.json b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/awesome-azd/src/McpSamples.AwesomeAzd.HybridApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}