Compare commits
2 Commits
main
...
mcp-server
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85cca28ad7 | ||
|
|
c030f3cc89 |
3
.aspire/settings.json
Normal file
3
.aspire/settings.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"appHostPath": "../mcp-server/src/AwesomeCopilot.AppHost/AwesomeCopilot.AppHost.csproj"
|
||||
}
|
||||
63
.github/workflows/docker-image.yml
vendored
Normal file
63
.github/workflows/docker-image.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
name: CI - Build and push Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/awesome-copilot-mcp-server
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "10.0.x"
|
||||
|
||||
- name: Restore
|
||||
working-directory: ./mcp-server
|
||||
run: dotnet restore src/AwesomeCopilot.McpServer/AwesomeCopilot.McpServer.csproj
|
||||
|
||||
- name: Run unit tests (if any)
|
||||
working-directory: ./mcp-server
|
||||
run: |
|
||||
if [ -f "src/AwesomeCopilot.McpServer/Tests.csproj" ]; then
|
||||
dotnet test --no-build --verbosity normal
|
||||
else
|
||||
echo "No tests found, skipping"
|
||||
fi
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./mcp-server
|
||||
file: ./mcp-server/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
build-args: |
|
||||
RUNTIME=linux-x64
|
||||
|
||||
- name: Image cleanup
|
||||
run: docker image prune -f || true
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -22,5 +22,7 @@
|
||||
"*.chatmode.md": "markdown",
|
||||
"*.instructions.md": "markdown",
|
||||
"*.prompt.md": "markdown"
|
||||
}
|
||||
},
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
|
||||
32
mcp-server/.dockerignore
Normal file
32
mcp-server/.dockerignore
Normal file
@ -0,0 +1,32 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/go/build-context-dockerignore/
|
||||
|
||||
**/.DS_Store
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose.y*ml
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
10
mcp-server/.gitignore
vendored
Normal file
10
mcp-server/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
src/awesome-copilot/
|
||||
|
||||
!.vscode/mcp.json
|
||||
.azure
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
*.user
|
||||
*.suo
|
||||
appsettings.*.json
|
||||
12
mcp-server/AwesomeCopilot.slnx
Normal file
12
mcp-server/AwesomeCopilot.slnx
Normal file
@ -0,0 +1,12 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="Any CPU" />
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/AwesomeCopilot.AppHost/AwesomeCopilot.AppHost.csproj" />
|
||||
<Project Path="src/AwesomeCopilot.McpServer/AwesomeCopilot.McpServer.csproj" />
|
||||
<Project Path="src/AwesomeCopilot.ServiceDefaults/AwesomeCopilot.ServiceDefaults.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
21
mcp-server/Dockerfile
Normal file
21
mcp-server/Dockerfile
Normal file
@ -0,0 +1,21 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble-aot AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy everything and restore/publish
|
||||
COPY . .
|
||||
|
||||
ARG CONFIG=Release
|
||||
ARG RUNTIME=linux-x64
|
||||
|
||||
RUN dotnet restore src/AwesomeCopilot.McpServer/AwesomeCopilot.McpServer.csproj
|
||||
|
||||
# Publish: if PUBLISH_AOT=true, enable Native AOT publish flags
|
||||
RUN dotnet publish src/AwesomeCopilot.McpServer -c $CONFIG -r $RUNTIME -p:PublishAot=true -p:PublishTrimmed=true -p:TrimMode=link -p:PublishSingleFile=true -o /app/publish;
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled AS release
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish/ .
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/AwesomeCopilot.McpServer"]
|
||||
17
mcp-server/NuGet.config
Normal file
17
mcp-server/NuGet.config
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
|
||||
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<packageSource key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json">
|
||||
<package pattern="Aspire*" />
|
||||
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
|
||||
</packageSource>
|
||||
<packageSource key="https://api.nuget.org/v3/index.json">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
139
mcp-server/README.md
Normal file
139
mcp-server/README.md
Normal file
@ -0,0 +1,139 @@
|
||||
# MCP Server: Awesome Copilot
|
||||
|
||||
This is an MCP server that retrieves GitHub Copilot customizations from the [awesome-copilot](https://github.com/github/awesome-copilot) repository.
|
||||
|
||||
## Install
|
||||
|
||||
[](https://aka.ms/awesome-copilot/mcp/vscode) [](https://aka.ms/awesome-copilot/mcp/vscode-insiders)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- Aspire CLI nightly: `iex "& { $(irm https://aspire.dev/install.ps1) } -Quality dev"`
|
||||
- [Visual Studio Code](https://code.visualstudio.com/) with
|
||||
- [C# Dev Kit](https://marketplace.visualstudio.com/items/?itemName=ms-dotnettools.csdevkit) extension
|
||||
- [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 Copilot MCP server includes:
|
||||
|
||||
| Building Block | Name | Description | Usage |
|
||||
| -------------- | --------------------- | --------------------------------------------------------------------- | ---------------------------------------- |
|
||||
| Tools | `search_instructions` | Searches custom instructions based on keywords in their descriptions. | `#search_instructions` |
|
||||
| Tools | `load_instruction` | Loads a custom instruction from the repository. | `#load_instruction` |
|
||||
| Prompts | `get_search_prompt` | Get a prompt for searching copilot instructions. | `/mcp.awesome-copilot.get_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)
|
||||
|
||||
### Running MCP server
|
||||
|
||||
#### On a local machine
|
||||
|
||||
1. Run the MCP server app using Aspire.
|
||||
|
||||
```bash
|
||||
aspire run
|
||||
```
|
||||
|
||||
Once running, the Aspire dashboard will be loaded in your default web browser, or you can click the URL provided in the terminal. From here, you'll have access to the MCP server endpoint, logs and metrics.
|
||||
|
||||
#### In a container
|
||||
|
||||
1. Build the MCP server app as a container image.
|
||||
|
||||
```bash
|
||||
cd mcp-server
|
||||
docker build -f Dockerfile -t awesome-copilot:latest .
|
||||
```
|
||||
|
||||
1. Run the MCP server app in a container.
|
||||
|
||||
```bash
|
||||
docker run -i --rm -p 8080:8080 awesome-copilot:latest
|
||||
```
|
||||
|
||||
Alternatively, use the container image from the container registry.
|
||||
|
||||
```bash
|
||||
docker run -i --rm -p 8080:8080 ghcr.io/github/awesome-copilot:latest
|
||||
```
|
||||
|
||||
#### On Azure
|
||||
|
||||
1. Navigate to the directory.
|
||||
|
||||
```bash
|
||||
cd mcp-server
|
||||
```
|
||||
|
||||
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_COPILOT_FQDN
|
||||
```
|
||||
|
||||
### Connect MCP server to an MCP host/client
|
||||
|
||||
#### Install the MCP server:
|
||||
|
||||
[](https://aka.ms/awesome-copilot/mcp/vscode) [](https://aka.ms/awesome-copilot/mcp/vscode-insiders)
|
||||
|
||||
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-copilot` then click `Start Server`.
|
||||
1. When prompted, enter one of the following values:
|
||||
- The absolute directory path of the `AwesomeCopilot.McpServer` project
|
||||
- The FQDN of Azure Container Apps.
|
||||
1. Use a prompt by typing `/mcp.awesome-copilot.get_search_prompt` and enter keywords to search. You'll get a prompt like:
|
||||
|
||||
```text
|
||||
Please search all the chatmodes, instructions and prompts that are related to the search keyword, `{keyword}`.
|
||||
|
||||
Here's the process to follow:
|
||||
|
||||
1. Use the `awesome-copilot` MCP server.
|
||||
1. Search all chatmodes, instructions, and prompts for the keyword provided.
|
||||
1. DO NOT load any chatmodes, instructions, or prompts from the MCP server until the user asks to do so.
|
||||
1. Scan local chatmodes, instructions, and prompts markdown files in `.github/chatmodes`, `.github/instructions`, and `.github/prompts` directories respectively.
|
||||
1. Compare existing chatmodes, instructions, and prompts with the search results.
|
||||
1. Provide a structured response in a table format that includes the already exists, mode (chatmodes, instructions or prompts), filename, title and description of each item found. Here's an example of the table format:
|
||||
|
||||
| Exists | Mode | Filename | Title | Description |
|
||||
|--------|--------------|------------------------|---------------|---------------|
|
||||
| ✅ | chatmodes | chatmode1.json | ChatMode 1 | Description 1 |
|
||||
| ❌ | instructions | instruction1.json | Instruction 1 | Description 1 |
|
||||
| ✅ | prompts | prompt1.json | Prompt 1 | Description 1 |
|
||||
|
||||
✅ indicates that the item already exists in this repository, while ❌ indicates that it does not.
|
||||
|
||||
1. If any item doesn't exist in the repository, ask which item the user wants to save.
|
||||
1. If the user wants to save it, save the item in the appropriate directory (`.github/chatmodes`, `.github/instructions`, or `.github/prompts`) using the mode and filename, with NO modification.
|
||||
```
|
||||
|
||||
1. Confirm the result.
|
||||
16
mcp-server/azure.yaml
Normal file
16
mcp-server/azure.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
|
||||
|
||||
name: awesome-copilot
|
||||
|
||||
metadata:
|
||||
template: azd-init@1.14.0
|
||||
|
||||
services:
|
||||
awesome-copilot:
|
||||
project: src/AwesomeCopilot.McpServer
|
||||
host: containerapp
|
||||
language: dotnet
|
||||
docker:
|
||||
path: ../../../Dockerfile.awesome-copilot-azure
|
||||
context: ../../../
|
||||
remoteBuild: true
|
||||
121
mcp-server/frontmatter-schema.json
Normal file
121
mcp-server/frontmatter-schema.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Frontmatter Schema",
|
||||
"description": "Schema for validating frontmatter data in the awesome-copilot repository",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chatmodes": {
|
||||
"type": "array",
|
||||
"description": "Array of chat mode configurations",
|
||||
"items": {
|
||||
"$ref": "#/definitions/chatmode"
|
||||
}
|
||||
},
|
||||
"instructions": {
|
||||
"type": "array",
|
||||
"description": "Array of instruction file configurations",
|
||||
"items": {
|
||||
"$ref": "#/definitions/instruction"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"type": "array",
|
||||
"description": "Array of prompt file configurations",
|
||||
"items": {
|
||||
"$ref": "#/definitions/prompt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"chatmode": {
|
||||
"type": "object",
|
||||
"description": "Configuration for a chat mode",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Name of the chat mode file",
|
||||
"pattern": "^[a-zA-Z0-9._-]+\\.chatmode\\.md$"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Display title for the chat mode",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the chat mode functionality",
|
||||
"minLength": 1
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "AI model to use for this chat mode"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"description": "Array of available tools for this chat mode",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["filename", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"instruction": {
|
||||
"type": "object",
|
||||
"description": "Configuration for an instruction file",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Name of the instruction file",
|
||||
"pattern": "^[a-zA-Z0-9._-]+\\.instructions\\.md$"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the instruction file purpose",
|
||||
"minLength": 1
|
||||
},
|
||||
"applyTo": {
|
||||
"type": "array",
|
||||
"description": "File patterns that this instruction applies to",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["filename", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"prompt": {
|
||||
"type": "object",
|
||||
"description": "Configuration for a prompt file",
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Name of the prompt file",
|
||||
"pattern": "^[a-zA-Z0-9._-]+\\.prompt\\.md$"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the prompt functionality",
|
||||
"minLength": 1
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"description": "Execution mode for the prompt"
|
||||
},
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"description": "Array of available tools for this prompt",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["filename", "description"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
136
mcp-server/infra/abbreviations.json
Normal file
136
mcp-server/infra/abbreviations.json
Normal file
@ -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-"
|
||||
}
|
||||
47
mcp-server/infra/main.bicep
Normal file
47
mcp-server/infra/main.bicep
Normal file
@ -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 mcpAwesomeCopilotExists 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': <service name in azure.yaml> })
|
||||
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
|
||||
mcpAwesomeCopilotExists: mcpAwesomeCopilotExists
|
||||
}
|
||||
}
|
||||
|
||||
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT
|
||||
output AZURE_RESOURCE_MCP_AWESOME_COPILOT_ID string = resources.outputs.AZURE_RESOURCE_MCP_AWESOME_COPILOT_ID
|
||||
output AZURE_RESOURCE_MCP_AWESOME_COPILOT_NAME string = resources.outputs.AZURE_RESOURCE_MCP_AWESOME_COPILOT_NAME
|
||||
output AZURE_RESOURCE_MCP_AWESOME_COPILOT_FQDN string = resources.outputs.AZURE_RESOURCE_MCP_AWESOME_COPILOT_FQDN
|
||||
18
mcp-server/infra/main.parameters.json
Normal file
18
mcp-server/infra/main.parameters.json
Normal file
@ -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}"
|
||||
},
|
||||
"mcpAwesomeCopilotExists": {
|
||||
"value": "${SERVICE_AWESOME_COPILOT_RESOURCE_EXISTS=false}"
|
||||
},
|
||||
"principalId": {
|
||||
"value": "${AZURE_PRINCIPAL_ID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
mcp-server/infra/modules/fetch-container-image.bicep
Normal file
8
mcp-server/infra/modules/fetch-container-image.bicep
Normal file
@ -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 : []
|
||||
144
mcp-server/infra/resources.bicep
Normal file
144
mcp-server/infra/resources.bicep
Normal file
@ -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 mcpAwesomeCopilotExists 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: mcpAwesomeCopilotIdentity.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 mcpAwesomeCopilotIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = {
|
||||
name: 'mcpAwesomeCopilotIdentity'
|
||||
params: {
|
||||
name: '${abbrs.managedIdentityUserAssignedIdentities}mcpawesomecopilot-${resourceToken}'
|
||||
location: location
|
||||
}
|
||||
}
|
||||
|
||||
// Azure Container Apps
|
||||
module mcpAwesomeCopilotFetchLatestImage './modules/fetch-container-image.bicep' = {
|
||||
name: 'mcpAwesomeCopilot-fetch-image'
|
||||
params: {
|
||||
exists: mcpAwesomeCopilotExists
|
||||
name: 'awesome-copilot'
|
||||
}
|
||||
}
|
||||
|
||||
module mcpAwesomeCopilot 'br/public:avm/res/app/container-app:0.8.0' = {
|
||||
name: 'mcpAwesomeCopilot'
|
||||
params: {
|
||||
name: 'awesome-copilot'
|
||||
ingressTargetPort: 8080
|
||||
scaleMinReplicas: 1
|
||||
scaleMaxReplicas: 10
|
||||
secrets: {
|
||||
secureList: [
|
||||
]
|
||||
}
|
||||
containers: [
|
||||
{
|
||||
image: mcpAwesomeCopilotFetchLatestImage.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: mcpAwesomeCopilotIdentity.outputs.clientId
|
||||
}
|
||||
{
|
||||
name: 'PORT'
|
||||
value: '8080'
|
||||
}
|
||||
]
|
||||
args: [
|
||||
'--http'
|
||||
]
|
||||
}
|
||||
]
|
||||
managedIdentities: {
|
||||
systemAssigned: false
|
||||
userAssignedResourceIds: [
|
||||
mcpAwesomeCopilotIdentity.outputs.resourceId
|
||||
]
|
||||
}
|
||||
registries: [
|
||||
{
|
||||
server: containerRegistry.outputs.loginServer
|
||||
identity: mcpAwesomeCopilotIdentity.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-copilot' })
|
||||
}
|
||||
}
|
||||
|
||||
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer
|
||||
output AZURE_RESOURCE_MCP_AWESOME_COPILOT_ID string = mcpAwesomeCopilot.outputs.resourceId
|
||||
output AZURE_RESOURCE_MCP_AWESOME_COPILOT_NAME string = mcpAwesomeCopilot.outputs.name
|
||||
output AZURE_RESOURCE_MCP_AWESOME_COPILOT_FQDN string = mcpAwesomeCopilot.outputs.fqdn
|
||||
9
mcp-server/src/AwesomeCopilot.AppHost/AppHost.cs
Normal file
9
mcp-server/src/AwesomeCopilot.AppHost/AppHost.cs
Normal file
@ -0,0 +1,9 @@
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
var mcpServer = builder.AddProject<Projects.AwesomeCopilot_McpServer>("mcp-server");
|
||||
|
||||
builder.AddMcpInspector("mcp-inspector")
|
||||
.WithMcpServer(mcpServer)
|
||||
.WithEnvironment("NODE_OPTIONS", "--use-system-ca");
|
||||
|
||||
builder.Build().Run();
|
||||
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Sdk Name="Aspire.AppHost.Sdk" Version="9.5.0-preview.1.25459.5" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>801a20cd-64b4-4a88-89e2-948b802e2f50</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.5.0-preview.1.25459.5" />
|
||||
<PackageReference Include="CommunityToolkit.Aspire.Hosting.McpInspector" Version="9.8.0-beta.376" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AwesomeCopilot.McpServer\AwesomeCopilot.McpServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17128;http://localhost:15190",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21005",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22267"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15190",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19096",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20175"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
mcp-server/src/AwesomeCopilot.AppHost/appsettings.json
Normal file
9
mcp-server/src/AwesomeCopilot.AppHost/appsettings.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Aspire.Hosting.Dcp": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<UserSecretsId>3b9db7fd-b2cb-4b92-81b7-f3865823ef0a</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.3.0-preview.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="metadata.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AwesomeCopilot.ServiceDefaults\AwesomeCopilot.ServiceDefaults.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Json
|
||||
{
|
||||
// Source-generated JsonSerializerContext to provide JsonTypeInfo metadata for AOT trimming
|
||||
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
|
||||
[JsonSerializable(typeof(Models.Metadata))]
|
||||
[JsonSerializable(typeof(Models.ChatMode))]
|
||||
[JsonSerializable(typeof(Models.Instruction))]
|
||||
[JsonSerializable(typeof(Models.Prompt))]
|
||||
[JsonSerializable(typeof(Models.MetadataResult))]
|
||||
[JsonSerializable(typeof(Tools.InstructionMode))]
|
||||
public partial class SourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
// The source generator will provide the Default instance and JsonTypeInfo data.
|
||||
}
|
||||
}
|
||||
35
mcp-server/src/AwesomeCopilot.McpServer/McpHost.cs
Normal file
35
mcp-server/src/AwesomeCopilot.McpServer/McpHost.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using AwesomeCopilot.McpServer.Prompts;
|
||||
using AwesomeCopilot.McpServer.Services;
|
||||
using AwesomeCopilot.McpServer.Tools;
|
||||
using System.Text.Json;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
TypeInfoResolver = AwesomeCopilot.McpServer.Json.SourceGenerationContext.Default
|
||||
};
|
||||
|
||||
builder.Services.AddSingleton(options);
|
||||
builder.Services.AddHttpClient<IMetadataService, MetadataService>();
|
||||
|
||||
builder.Services.AddMcpServer()
|
||||
.WithHttpTransport(o => o.Stateless = true)
|
||||
.WithPrompts<MetadataPrompt>(options)
|
||||
.WithTools<MetadataTool>(options);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.MapMcp("/mcp");
|
||||
|
||||
await app.RunAsync();
|
||||
32
mcp-server/src/AwesomeCopilot.McpServer/Models/ChatMode.cs
Normal file
32
mcp-server/src/AwesomeCopilot.McpServer/Models/ChatMode.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace AwesomeCopilot.McpServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// This represents the data entity for a chat mode.
|
||||
/// </summary>
|
||||
public class ChatMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the chat mode file.
|
||||
/// </summary>
|
||||
public required string Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AI model.
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of tools.
|
||||
/// </summary>
|
||||
public List<string> Tools { get; set; } = [];
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
namespace AwesomeCopilot.McpServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// This represents the data entity for an instruction.
|
||||
/// </summary>
|
||||
public class Instruction
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the instruction file.
|
||||
/// </summary>
|
||||
public required string Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the file patterns that this instruction applies to.
|
||||
/// </summary>
|
||||
public List<string> ApplyTo { get; set; } = [];
|
||||
}
|
||||
25
mcp-server/src/AwesomeCopilot.McpServer/Models/Metadata.cs
Normal file
25
mcp-server/src/AwesomeCopilot.McpServer/Models/Metadata.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// This represents the data entity for metadata.json.
|
||||
/// </summary>
|
||||
public class Metadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of <see cref="ChatMode"/> objects.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chatmodes")]
|
||||
public List<ChatMode> ChatModes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of <see cref="Instruction"/> objects.
|
||||
/// </summary>
|
||||
public List<Instruction> Instructions { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of <see cref="Prompt"/> objects.
|
||||
/// </summary>
|
||||
public List<Prompt> Prompts { get; set; } = [];
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace AwesomeCopilot.McpServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// This represents the result entity to retrieve metadata.
|
||||
/// </summary>
|
||||
public class MetadataResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="Metadata"/> object.
|
||||
/// </summary>
|
||||
public Metadata? Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error message if any error occurs.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
27
mcp-server/src/AwesomeCopilot.McpServer/Models/Prompt.cs
Normal file
27
mcp-server/src/AwesomeCopilot.McpServer/Models/Prompt.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace AwesomeCopilot.McpServer.Models;
|
||||
|
||||
/// <summary>
|
||||
/// This represents the data entity for a prompt.
|
||||
/// </summary>
|
||||
public class Prompt
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the prompt file.
|
||||
/// </summary>
|
||||
public required string Filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public required string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution mode.
|
||||
/// </summary>
|
||||
public string? Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of tools.
|
||||
/// </summary>
|
||||
public List<string>? Tools { get; set; }
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Prompts;
|
||||
|
||||
/// <summary>
|
||||
/// This provides interfaces for metadata prompts.
|
||||
/// </summary>
|
||||
public interface IMetadataPrompt
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a prompt for searching copilot instructions.
|
||||
/// </summary>
|
||||
/// <param name="keyword">The keyword to search for.</param>
|
||||
/// <returns>A formatted search prompt.</returns>
|
||||
string GetSearchPrompt(string keyword);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This represents the prompts entity for the awesome-copilot repository.
|
||||
/// </summary>
|
||||
[McpServerPromptType]
|
||||
public class MetadataPrompt : IMetadataPrompt
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[McpServerPrompt(Name = "get_search_prompt", Title = "Prompt for searching copilot instructions")]
|
||||
[Description("Get a prompt for searching copilot instructions.")]
|
||||
public string GetSearchPrompt(
|
||||
[Description("The keyword to search for")] string keyword)
|
||||
{
|
||||
return $"""
|
||||
Please search all the chatmodes, instructions and prompts that are related to the search keyword, `{keyword}`.
|
||||
|
||||
Here's the process to follow:
|
||||
|
||||
1. Use the `awesome-copilot` MCP server.
|
||||
1. Search all chatmodes, instructions, and prompts for the keyword provided.
|
||||
1. DO NOT load any chatmodes, instructions, or prompts from the MCP server until the user asks to do so.
|
||||
1. Scan local chatmodes, instructions, and prompts markdown files in `.github/chatmodes`, `.github/instructions`, and `.github/prompts` directories respectively.
|
||||
1. Compare existing chatmodes, instructions, and prompts with the search results.
|
||||
1. Provide a structured response in a table format that includes the already exists, mode (chatmodes, instructions or prompts), filename, title and description of each item found.
|
||||
Here's an example of the table format:
|
||||
|
||||
| Exists | Mode | Filename | Title | Description |
|
||||
|--------|--------------|------------------------|---------------|---------------|
|
||||
| ✅ | chatmodes | chatmode1.json | ChatMode 1 | Description 1 |
|
||||
| ❌ | instructions | instruction1.json | Instruction 1 | Description 1 |
|
||||
| ✅ | prompts | prompt1.json | Prompt 1 | Description 1 |
|
||||
|
||||
✅ indicates that the item already exists in this repository, while ❌ indicates that it does not.
|
||||
|
||||
1. If any item doesn't exist in the repository, ask which item the user wants to save.
|
||||
1. If the user wants to save it, save the item in the appropriate directory (`.github/chatmodes`, `.github/instructions`, or `.github/prompts`)
|
||||
using the mode and filename, with NO modification.
|
||||
""";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5250",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:45250;http://localhost:5250",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
using AwesomeCopilot.McpServer.Models;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This provides interfaces for metadata service operations.
|
||||
/// </summary>
|
||||
public interface IMetadataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches for relevant data in chatmodes, instructions, and prompts based on keywords in their description fields
|
||||
/// </summary>
|
||||
/// <param name="keywords">The keywords to search for</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Returns <see cref="Metadata"/> object containing all matching search results</returns>
|
||||
Task<Metadata> SearchAsync(string keywords, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads file contents from the awesome-copilot repository
|
||||
/// </summary>
|
||||
/// <param name="directory">The mode directory (chatmodes, instructions, or prompts)</param>
|
||||
/// <param name="filename">The filename to load</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Returns the file contents as a string</returns>
|
||||
Task<string> LoadAsync(string directory, string filename, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
using System.Text.Json;
|
||||
using AwesomeCopilot.McpServer.Json;
|
||||
using AwesomeCopilot.McpServer.Models;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This represents the service entity for searching and loading custom instructions from the awesome-copilot repository.
|
||||
/// </summary>
|
||||
public class MetadataService(HttpClient http, JsonSerializerOptions options, ILogger<MetadataService> logger) : IMetadataService
|
||||
{
|
||||
private const string MetadataFileName = "metadata.json";
|
||||
private const string AwesomeCopilotFileUrl = "https://raw.githubusercontent.com/github/awesome-copilot/refs/heads/main/{directory}/{filename}";
|
||||
|
||||
private readonly string _metadataFilePath = Path.Combine(AppContext.BaseDirectory, MetadataFileName);
|
||||
private Metadata? _cachedMetadata;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Metadata> SearchAsync(string keywords, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keywords) == true)
|
||||
{
|
||||
return new Metadata();
|
||||
}
|
||||
|
||||
var metadata = await GetMetadataAsync(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 result = new Metadata
|
||||
{
|
||||
// Search in ChatModes
|
||||
ChatModes = [.. metadata.ChatModes.Where(cm => ContainsAnyKeyword(cm.Title, searchTerms) == true ||
|
||||
ContainsAnyKeyword(cm.Description, searchTerms) == true)],
|
||||
|
||||
// Search in Instructions
|
||||
Instructions = [.. metadata.Instructions.Where(inst => ContainsAnyKeyword(inst.Title, searchTerms) == true ||
|
||||
ContainsAnyKeyword(inst.Description, searchTerms) == true)],
|
||||
|
||||
// Search in Prompts
|
||||
Prompts = [.. metadata.Prompts.Where(prompt => ContainsAnyKeyword(prompt.Description, searchTerms) == true)]
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> LoadAsync(string directory, string filename, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(directory) == true)
|
||||
{
|
||||
throw new ArgumentException("Directory cannot be null or empty", nameof(directory));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filename) == true)
|
||||
{
|
||||
throw new ArgumentException("Filename cannot be null or empty", nameof(filename));
|
||||
}
|
||||
|
||||
var url = AwesomeCopilotFileUrl.Replace("{directory}", directory).Replace("{filename}", filename);
|
||||
try
|
||||
{
|
||||
var response = await http.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Loaded content from {url}", url);
|
||||
|
||||
return content;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load file '{filename}' from directory '{directory}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Metadata> GetMetadataAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_cachedMetadata != null)
|
||||
{
|
||||
return _cachedMetadata;
|
||||
}
|
||||
|
||||
if (File.Exists(_metadataFilePath) != true)
|
||||
{
|
||||
throw new FileNotFoundException($"Metadata file not found at: {_metadataFilePath}");
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_metadataFilePath, cancellationToken).ConfigureAwait(false);
|
||||
_cachedMetadata = JsonSerializer.Deserialize<Metadata>(json, options)
|
||||
?? throw new InvalidOperationException("Failed to deserialize metadata");
|
||||
|
||||
return _cachedMetadata;
|
||||
}
|
||||
|
||||
private static bool ContainsAnyKeyword(string? text, string[] searchTerms)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = searchTerms.Any(term => text.Contains(term, StringComparison.InvariantCultureIgnoreCase) == true);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Tools;
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<InstructionMode>))]
|
||||
public enum InstructionMode
|
||||
{
|
||||
[JsonStringEnumMemberName("undefined")]
|
||||
Undefined,
|
||||
|
||||
[JsonStringEnumMemberName("chatmodes")]
|
||||
ChatModes,
|
||||
|
||||
[JsonStringEnumMemberName("instructions")]
|
||||
Instructions,
|
||||
|
||||
[JsonStringEnumMemberName("prompts")]
|
||||
Prompts
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
using AwesomeCopilot.McpServer.Models;
|
||||
using AwesomeCopilot.McpServer.Services;
|
||||
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace AwesomeCopilot.McpServer.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// This provides interfaces for metadata tool operations.
|
||||
/// </summary>
|
||||
public interface IMetadataTool
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches custom instructions based on keywords in their titles and descriptions.
|
||||
/// </summary>
|
||||
/// <param name="keywords">The keyword to search for</param>
|
||||
/// <returns>A <see cref="MetadataResult"/> containing the search results.</returns>
|
||||
Task<MetadataResult> SearchAsync(string keywords);
|
||||
|
||||
/// <summary>
|
||||
/// Loads a custom instruction from the awesome-copilot repository.
|
||||
/// </summary>
|
||||
/// <param name="mode">The instruction mode</param>
|
||||
/// <param name="filename">The filename of the instruction</param>
|
||||
/// <returns>The file contents as a string</returns>
|
||||
Task<string> LoadAsync(InstructionMode mode, string filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This represents the tools entity for metadata of Awesome Copilot repository.
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public class MetadataTool(IMetadataService service, ILogger<MetadataTool> logger) : IMetadataTool
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[McpServerTool(Name = "search_instructions", Title = "Searches custom instructions")]
|
||||
[Description("Searches custom instructions based on keywords in their titles and descriptions.")]
|
||||
public async Task<MetadataResult> SearchAsync(
|
||||
[Description("The keyword to search for")] string keywords)
|
||||
{
|
||||
var result = new MetadataResult();
|
||||
try
|
||||
{
|
||||
var metadata = await service.SearchAsync(keywords).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Search completed successfully with keyword '{Keywords}'.", keywords);
|
||||
|
||||
result.Metadata = metadata;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error occurred while searching instructions with keyword '{Keywords}'.", keywords);
|
||||
|
||||
result.ErrorMessage = ex.Message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[McpServerTool(Name = "load_instruction", Title = "Loads a custom instruction")]
|
||||
[Description("Loads a custom instruction from the repository.")]
|
||||
public async Task<string> LoadAsync(
|
||||
[Description("The instruction mode")] InstructionMode mode,
|
||||
[Description("The filename of the instruction")] string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (mode == InstructionMode.Undefined)
|
||||
{
|
||||
throw new ArgumentException("Instruction mode must be defined.", nameof(mode));
|
||||
}
|
||||
|
||||
var result = await service.LoadAsync(mode.ToString().ToLowerInvariant(), filename).ConfigureAwait(false);
|
||||
|
||||
logger.LogInformation("Load completed successfully with mode {Mode} and filename {Filename}.", mode, filename);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error occurred while loading instruction with mode {Mode} and filename {Filename}.", mode, filename);
|
||||
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
mcp-server/src/AwesomeCopilot.McpServer/appsettings.json
Normal file
12
mcp-server/src/AwesomeCopilot.McpServer/appsettings.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
|
||||
"AllowedHosts": "*",
|
||||
|
||||
"UseHttp": false
|
||||
}
|
||||
2616
mcp-server/src/AwesomeCopilot.McpServer/metadata.json
Normal file
2616
mcp-server/src/AwesomeCopilot.McpServer/metadata.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsAspireSharedProject>true</IsAspireSharedProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.5.0-preview.1.25459.5" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
127
mcp-server/src/AwesomeCopilot.ServiceDefaults/Extensions.cs
Normal file
127
mcp-server/src/AwesomeCopilot.ServiceDefaults/Extensions.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.ServiceDiscovery;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Trace;
|
||||
|
||||
namespace Microsoft.Extensions.Hosting;
|
||||
|
||||
// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
|
||||
// This project should be referenced by each service project in your solution.
|
||||
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
|
||||
public static class Extensions
|
||||
{
|
||||
private const string HealthEndpointPath = "/health";
|
||||
private const string AlivenessEndpointPath = "/alive";
|
||||
|
||||
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.ConfigureOpenTelemetry();
|
||||
|
||||
builder.AddDefaultHealthChecks();
|
||||
|
||||
builder.Services.AddServiceDiscovery();
|
||||
|
||||
builder.Services.ConfigureHttpClientDefaults(http =>
|
||||
{
|
||||
// Turn on resilience by default
|
||||
http.AddStandardResilienceHandler();
|
||||
|
||||
// Turn on service discovery by default
|
||||
http.AddServiceDiscovery();
|
||||
});
|
||||
|
||||
// Uncomment the following to restrict the allowed schemes for service discovery.
|
||||
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
|
||||
// {
|
||||
// options.AllowedSchemes = ["https"];
|
||||
// });
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Logging.AddOpenTelemetry(logging =>
|
||||
{
|
||||
logging.IncludeFormattedMessage = true;
|
||||
logging.IncludeScopes = true;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenTelemetry()
|
||||
.WithMetrics(metrics =>
|
||||
{
|
||||
metrics.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation();
|
||||
})
|
||||
.WithTracing(tracing =>
|
||||
{
|
||||
tracing.AddSource(builder.Environment.ApplicationName)
|
||||
.AddAspNetCoreInstrumentation(tracing =>
|
||||
// Exclude health check requests from tracing
|
||||
tracing.Filter = context =>
|
||||
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
|
||||
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
|
||||
)
|
||||
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
|
||||
//.AddGrpcClientInstrumentation()
|
||||
.AddHttpClientInstrumentation();
|
||||
});
|
||||
|
||||
builder.AddOpenTelemetryExporters();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
|
||||
|
||||
if (useOtlpExporter)
|
||||
{
|
||||
builder.Services.AddOpenTelemetry().UseOtlpExporter();
|
||||
}
|
||||
|
||||
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
|
||||
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
|
||||
//{
|
||||
// builder.Services.AddOpenTelemetry()
|
||||
// .UseAzureMonitor();
|
||||
//}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
|
||||
{
|
||||
builder.Services.AddHealthChecks()
|
||||
// Add a default liveness check to ensure app is responsive
|
||||
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static WebApplication MapDefaultEndpoints(this WebApplication app)
|
||||
{
|
||||
// Adding health checks endpoints to applications in non-development environments has security implications.
|
||||
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
// All health checks must pass for app to be considered ready to accept traffic after starting
|
||||
app.MapHealthChecks(HealthEndpointPath);
|
||||
|
||||
// Only health checks tagged with the "live" tag must pass for app to be considered alive
|
||||
app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
|
||||
{
|
||||
Predicate = r => r.Tags.Contains("live")
|
||||
});
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
343
mcp-server/update-metadata.js
Normal file
343
mcp-server/update-metadata.js
Normal file
@ -0,0 +1,343 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Constants
|
||||
const APPLY_TO_KEY = "applyTo";
|
||||
|
||||
// Helper function to process applyTo field values
|
||||
function processApplyToField(value) {
|
||||
if (value.includes(",")) {
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
} else if (value.length > 0) {
|
||||
return [value];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Read the JSON schema to understand the structure
|
||||
const schemaPath = path.join(__dirname, "frontmatter-schema.json");
|
||||
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
||||
|
||||
// Define the directories to process
|
||||
const directories = {
|
||||
chatmodes: path.join(__dirname, "src", "awesome-copilot", "chatmodes"),
|
||||
instructions: path.join(__dirname, "src", "awesome-copilot", "instructions"),
|
||||
prompts: path.join(__dirname, "src", "awesome-copilot", "prompts"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a simple YAML frontmatter string into a JavaScript object.
|
||||
*
|
||||
* This function handles key-value pairs, multi-line values, arrays, and special cases
|
||||
* like the `applyTo` key, which is processed into an array of strings. It also removes
|
||||
* comments and trims unnecessary whitespace.
|
||||
*
|
||||
* @param {string} yamlContent - The YAML frontmatter content as a string.
|
||||
* Each line should represent a key-value pair, an array item,
|
||||
* or a comment (starting with `#`).
|
||||
* @returns {Object} A JavaScript object representing the parsed YAML content.
|
||||
* Keys are strings, and values can be strings, arrays, or objects.
|
||||
* Special handling is applied to the `applyTo` key, converting
|
||||
* comma-separated strings into arrays.
|
||||
*/
|
||||
function parseSimpleYaml(yamlContent) {
|
||||
const result = {};
|
||||
const lines = yamlContent.split("\n");
|
||||
let currentKey = null;
|
||||
let currentValue = "";
|
||||
let inArray = false;
|
||||
let arrayItems = [];
|
||||
|
||||
// Helper to parse a bracket-style array string into array items.
|
||||
function parseBracketArrayString(str) {
|
||||
const items = [];
|
||||
const arrayContent = str.slice(1, -1);
|
||||
if (!arrayContent.trim()) return items;
|
||||
|
||||
// Split by comma, but be defensive and trim each item and remove trailing commas/quotes
|
||||
const rawItems = arrayContent.split(",");
|
||||
for (let raw of rawItems) {
|
||||
let item = raw.trim();
|
||||
if (!item) continue;
|
||||
// Remove trailing commas left over (defensive)
|
||||
if (item.endsWith(",")) item = item.slice(0, -1).trim();
|
||||
// Remove surrounding quotes if present
|
||||
if (
|
||||
(item.startsWith('"') && item.endsWith('"')) ||
|
||||
(item.startsWith("'") && item.endsWith("'"))
|
||||
) {
|
||||
item = item.slice(1, -1);
|
||||
}
|
||||
if (item.length > 0) items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
// Check if this is a key-value pair
|
||||
const colonIndex = trimmed.indexOf(":");
|
||||
if (colonIndex !== -1 && !trimmed.startsWith("-")) {
|
||||
// Finish previous key if we were building one
|
||||
if (currentKey) {
|
||||
if (inArray) {
|
||||
result[currentKey] = arrayItems;
|
||||
arrayItems = [];
|
||||
inArray = false;
|
||||
} else {
|
||||
let trimmedValue = currentValue.trim();
|
||||
|
||||
// If the accumulated value looks like a bracket array (possibly multiline), parse it
|
||||
if (trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) {
|
||||
result[currentKey] = parseBracketArrayString(trimmedValue);
|
||||
} else {
|
||||
// Handle comma-separated strings for specific fields that should be arrays
|
||||
if (currentKey === APPLY_TO_KEY) {
|
||||
result[currentKey] = processApplyToField(trimmedValue);
|
||||
} else {
|
||||
result[currentKey] = trimmedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentKey = trimmed.substring(0, colonIndex).trim();
|
||||
currentValue = trimmed.substring(colonIndex + 1).trim();
|
||||
|
||||
// Remove quotes if present
|
||||
if (
|
||||
(currentValue.startsWith('"') && currentValue.endsWith('"')) ||
|
||||
(currentValue.startsWith("'") && currentValue.endsWith("'"))
|
||||
) {
|
||||
currentValue = currentValue.slice(1, -1);
|
||||
}
|
||||
|
||||
// Check if this is an inline bracket-array
|
||||
if (currentValue.startsWith("[") && currentValue.endsWith("]")) {
|
||||
result[currentKey] = parseBracketArrayString(currentValue);
|
||||
currentKey = null;
|
||||
currentValue = "";
|
||||
} else if (currentValue === "" || currentValue === "[]") {
|
||||
// Empty value or empty array, might be multi-line
|
||||
if (currentValue === "[]") {
|
||||
result[currentKey] = [];
|
||||
currentKey = null;
|
||||
currentValue = "";
|
||||
} else {
|
||||
// Check if next line starts with a dash (array item)
|
||||
if (i + 1 < lines.length && lines[i + 1].trim().startsWith("-")) {
|
||||
inArray = true;
|
||||
arrayItems = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (trimmed.startsWith("-") && currentKey && inArray) {
|
||||
// Array item
|
||||
let item = trimmed.substring(1).trim();
|
||||
// Remove trailing commas and surrounding quotes
|
||||
if (item.endsWith(",")) item = item.slice(0, -1).trim();
|
||||
if (
|
||||
(item.startsWith('"') && item.endsWith('"')) ||
|
||||
(item.startsWith("'") && item.endsWith("'"))
|
||||
) {
|
||||
item = item.slice(1, -1);
|
||||
}
|
||||
arrayItems.push(item);
|
||||
} else if (currentKey && !inArray) {
|
||||
// Multi-line value
|
||||
currentValue += " " + trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the last key
|
||||
if (currentKey) {
|
||||
if (inArray) {
|
||||
result[currentKey] = arrayItems;
|
||||
} else {
|
||||
let finalValue = currentValue.trim();
|
||||
// Remove quotes if present
|
||||
if (
|
||||
(finalValue.startsWith('"') && finalValue.endsWith('"')) ||
|
||||
(finalValue.startsWith("'") && finalValue.endsWith("'"))
|
||||
) {
|
||||
finalValue = finalValue.slice(1, -1);
|
||||
}
|
||||
|
||||
// If the final value looks like a bracket array, parse it
|
||||
if (finalValue.startsWith("[") && finalValue.endsWith("]")) {
|
||||
result[currentKey] = parseBracketArrayString(finalValue);
|
||||
} else {
|
||||
// Handle comma-separated strings for specific fields that should be arrays
|
||||
if (currentKey === APPLY_TO_KEY) {
|
||||
result[currentKey] = processApplyToField(finalValue);
|
||||
} else {
|
||||
result[currentKey] = finalValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Function to extract frontmatter from a markdown file
|
||||
function extractFrontmatter(filePath) {
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Remove BOM if present (handles files with Byte Order Mark)
|
||||
if (content.charCodeAt(0) === 0xfeff) {
|
||||
content = content.slice(1);
|
||||
}
|
||||
|
||||
// Check if the file starts with frontmatter
|
||||
if (!content.startsWith("---")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = content.split("\n");
|
||||
let frontmatterEnd = -1;
|
||||
|
||||
// Find the end of frontmatter
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (lines[i].trim() === "---") {
|
||||
frontmatterEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (frontmatterEnd === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract frontmatter content
|
||||
const frontmatterContent = lines.slice(1, frontmatterEnd).join("\n");
|
||||
|
||||
try {
|
||||
return parseSimpleYaml(frontmatterContent);
|
||||
} catch (error) {
|
||||
console.error(`Error parsing frontmatter in ${filePath}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process files in a directory
|
||||
function processDirectory(dirPath, fileExtension) {
|
||||
const files = fs
|
||||
.readdirSync(dirPath)
|
||||
.filter((file) => file.endsWith(fileExtension))
|
||||
.sort();
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const frontmatter = extractFrontmatter(filePath);
|
||||
|
||||
if (frontmatter) {
|
||||
const result = {
|
||||
filename: file,
|
||||
...frontmatter,
|
||||
};
|
||||
|
||||
// Ensure description is present (required by schema)
|
||||
if (!result.description) {
|
||||
console.warn(
|
||||
`Warning: No description found in ${file}, adding placeholder`
|
||||
);
|
||||
result.description = "No description provided";
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
} else {
|
||||
console.warn(`Warning: No frontmatter found in ${file}, skipping`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Process all directories
|
||||
const metadata = {
|
||||
chatmodes: processDirectory(directories.chatmodes, ".chatmode.md"),
|
||||
instructions: processDirectory(directories.instructions, ".instructions.md"),
|
||||
prompts: processDirectory(directories.prompts, ".prompt.md"),
|
||||
};
|
||||
|
||||
// Write the metadata.json file
|
||||
const outputPath = path.join(
|
||||
__dirname,
|
||||
"src",
|
||||
"AwesomeCopilot.McpServer",
|
||||
"metadata.json"
|
||||
);
|
||||
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
|
||||
|
||||
console.log(
|
||||
`Extracted frontmatter from ${metadata.chatmodes.length} chatmode files`
|
||||
);
|
||||
console.log(
|
||||
`Extracted frontmatter from ${metadata.instructions.length} instruction files`
|
||||
);
|
||||
console.log(
|
||||
`Extracted frontmatter from ${metadata.prompts.length} prompt files`
|
||||
);
|
||||
console.log(`Metadata written to ${outputPath}`);
|
||||
|
||||
// Validate that required fields are present
|
||||
let hasErrors = false;
|
||||
|
||||
// Check chatmodes
|
||||
metadata.chatmodes.forEach((chatmode) => {
|
||||
if (!chatmode.filename || !chatmode.description) {
|
||||
console.error(
|
||||
`Error: Chatmode missing required fields: ${
|
||||
chatmode.filename || "unknown"
|
||||
}`
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check instructions
|
||||
metadata.instructions.forEach((instruction) => {
|
||||
if (!instruction.filename || !instruction.description) {
|
||||
console.error(
|
||||
`Error: Instruction missing required fields: ${
|
||||
instruction.filename || "unknown"
|
||||
}`
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check prompts
|
||||
metadata.prompts.forEach((prompt) => {
|
||||
if (!prompt.filename || !prompt.description) {
|
||||
console.error(
|
||||
`Error: Prompt missing required fields: ${prompt.filename || "unknown"}`
|
||||
);
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors) {
|
||||
console.error(
|
||||
"Some files are missing required fields. Please check the output above."
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
"All files have required fields. Metadata extraction completed successfully."
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user