diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 0000000..af2a769 --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,38 @@ +name: Build and Publish Gateway Container Image + +on: + workflow_dispatch: + push: + branches: [main] + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout source + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + + - name: Restore dependencies + run: dotnet restore dotnet/Microsoft.McpGateway.sln --runtime linux-x64 + + - name: Publish and push the container image + run: dotnet publish dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj --configuration Release --no-restore /p:PublishProfile=github.pubxml /p:ContainerRepository=${{ github.repository }} diff --git a/README.md b/README.md index bfc350e..0834b0a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,15 @@ **MCP Gateway** is a reverse proxy and management layer for [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) servers, enabling scalable, session-aware routing and lifecycle management of MCP servers in Kubernetes environments. +## Table of Contents + +- [Overview](#overview) +- [Key Concepts](#key-concepts) +- [Architecture](#architecture) +- [Features](#features) +- [Getting Started – Local Deployment](#getting-started---local-deployment) +- [Getting Started – Cloud Deployment (Azure)](#getting-started---cloud-deployment-azure) + ## Overview This project provides: @@ -80,73 +89,208 @@ flowchart LR - Stateless reverse proxy with a distributed session store (production mode). - Kubernetes-native deployment using StatefulSets and headless services. -## 🚀 Getting Started +## Getting Started - Local Deployment -1. **Prepare Local Development Environment** - - [Install .NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) - - [Install Docker Desktop](https://docs.docker.com/desktop/) - - [Install and turn on Kubernetes](https://docs.docker.com/desktop/features/kubernetes/#install-and-turn-on-kubernetes) +### 1. **Prepare Local Development Environment** +- [Install .NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) +- [Install Docker Desktop](https://docs.docker.com/desktop/) +- [Install and turn on Kubernetes](https://docs.docker.com/desktop/features/kubernetes/#install-and-turn-on-kubernetes) -2. **Run Local Docker Registry** +### 2. **Run Local Docker Registry** ```sh docker run -d -p 5000:5000 --name registry registry:2.7 ``` -3. **Build & Publish MCP Server Images** - Build and push the MCP server images to your local registry (`localhost:5000`). - ```sh - docker build -f mcp-example-server/Dockerfile mcp-example-server -t localhost:5000/mcp-example:1.0.0 - docker push localhost:5000/mcp-example:1.0.0 - ``` +### 3. **Build & Publish MCP Server Images** +Build and push the MCP server images to your local registry (`localhost:5000`). +```sh +docker build -f mcp-example-server/Dockerfile mcp-example-server -t localhost:5000/mcp-example:1.0.0 +docker push localhost:5000/mcp-example:1.0.0 +``` -4. **Build & Publish MCP Gateway** - (Optional) Open `dotnet/Microsoft.McpGateway.sln` with Visual Studio. +### 4. **Build & Publish MCP Gateway** +(Optional) Open `dotnet/Microsoft.McpGateway.sln` with Visual Studio. - Publish the MCP Gateway image by right-clicking `Publish` on `Microsoft.McpGateway.Service` in Visual Studio, or run: - ```sh - dotnet publish dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj -c Release /p:PublishProfile=localhost_5000.pubxml - ``` +Publish the MCP Gateway image by right-clicking `Publish` on `Microsoft.McpGateway.Service` in Visual Studio, or run: +```sh +dotnet publish dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj -c Release /p:PublishProfile=localhost_5000.pubxml +``` -5. **Deploy MCP Gateway to Kubernetes Cluster** - Apply the deployment manifests: - ```sh - kubectl apply -f k8s/deployment.yml +### 5. **Deploy MCP Gateway to Kubernetes Cluster** +Apply the deployment manifests: +```sh +kubectl apply -f deployment/k8s/local-deployment.yml +``` + +### 6. **Enable Port Forwarding** +Forward the gateway service port: +```sh +kubectl port-forward -n adapter svc/mcpgateway-service 8000:8000 +``` + +### 7. **Test the API** + +- Import the OpenAPI definition from `openapi/mcp-gateway.openapi.json` into tools like [Postman](https://www.postman.com/), [Bruno](https://www.usebruno.com/), or [Swagger Editor](https://editor.swagger.io/). + +- Send a request to create a new adapter resource: + ```http + POST http://localhost:8000/adapters + Content-Type: application/json + ``` + ```json + { + "name": "mcp-example", + "imageName": "mcp-example", + "imageVersion": "1.0.0", + "description": "test" + } ``` -6. **Enable Port Forwarding** - Forward the gateway service port: +- After deploying the MCP server, use a client like [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) to test the connection. + + To connect to the deployed `mcp-example` server, use: + - `http://localhost:8000/adapters/mcp-example/mcp` (Streamable HTTP) + + For other servers: + - `http://localhost:8000/adapters/{name}/mcp` (Streamable HTTP) + - `http://localhost:8000/adapters/{name}/sse` (SSE) + +### 8. **Clean the Environment** + To remove all deployed resources, delete the Kubernetes namespace: ```sh - kubectl port-forward -n adapter svc/mcpgateway-service 8000:8000 + kubectl delete namespace adapter ``` -7. **Test the API** +## Getting Started - Cloud Deployment (Azure) - - Import the OpenAPI definition from `openapi/mcp-gateway.openapi.json` into tools like [Postman](https://www.postman.com/), [Bruno](https://www.usebruno.com/), or [Swagger Editor](https://editor.swagger.io/). +### Cloud Infrastructure +![Architecture Diagram](infra-diagram.png) - - Send a POST request to `http://localhost:8000/adapters` to create a new adapter resource: - ```json - { - "name": "mcp-example", - "imageName": "mcp-example", - "imageVersion": "1.0.0", - "description": "test" - } - ``` +### 1. Prepare Cloud Development Environment +- An active [Azure subscription](https://azure.microsoft.com) with **Owner** access +- [Install Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) +- [Install Docker Desktop](https://docs.docker.com/desktop/) - - After deploying the MCP server, use a client like [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) to test the connection. +### 2. Setup Entra ID (Azure Active Directory) +The cloud-deployed service needs authentication. Here we configure the basic bearer token authentication using Azure Entra ID. +- Go to [App Registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) +- Create a **single-tenant** app registration +- Add a platform - Mobile and desktop applications +- Under **Redirect URIs**, add: `http://localhost` +- Copy the **Application (client) ID** and **Directory (tenant) ID** from the overview page - To connect to the deployed `mcp-example` server, use: - - `http://localhost:8000/adapters/mcp-example/mcp` (Streamable HTTP) +### 3. Deploy Infrastructure Resources - For other servers: - - `http://localhost:8000/adapters/{name}/mcp` (Streamable HTTP) - - `http://localhost:8000/adapters/{name}/sse` (SSE) +Run the deployment script: -8. **Clean up the environment** - To remove all deployed resources, delete the Kubernetes namespace: - ```sh - kubectl delete namespace adapter - ``` +```sh +deployment/azure-deploy.ps1 -ResourceGroupName -ClientId -Location +``` + +**Parameters:** + +| Name | Description | +|--------------------|-------------------------------------------------------| +| `ResourceGroupName`| All lowercase, letters and numbers only | +| `ClientId` | Client ID from your app registration | +| `Location` | Azure region (default: `westus3`) | + +This script will: +- Create a resource group named `` +- Deploy Azure infrastructure via Bicep templates + + | Resource Name | Resource Type | + |-----------------------------------|-----------------------------| + | acr\ | Container Registry | + | cosmos\ | Azure Cosmos DB Account | + | mg-aag-\ | Application Gateway | + | mg-ai-\ | Application Insights | + | mg-aks-\ | Kubernetes Service (AKS) | + | mg-identity-\ | Managed Identity | + | mg-pip-\ | Public IP Address | + | mg-vnet-\ | Virtual Network | + +- Deploy Kubernetes resources (including `mcp-gateway`) to the provisioned AKS cluster + +> **Note:** It's recommended to use Managed Identity for credential-less authentication. This deployment follows that design. + +### 4. Build & Publish MCP Server Images +The gateway service pulls the MCP server image from the newly provisioned Azure Container Registry (ACR) during deployment. + +Build and push the MCP server image to ACR: +> **Note:** Ensure that Docker Engine is running before proceeding. + +```sh +az acr login -n acr +docker build -f mcp-example-server/Dockerfile mcp-example-server -t acr.azurecr.io/mcp-example:1.0.0 +docker push acr.azurecr.io/mcp-example:1.0.0 +``` + +### 5. Test the API + +- Import the OpenAPI spec from `openapi/mcp-gateway.openapi.json` into [Postman](https://www.postman.com/), [Bruno](https://www.usebruno.com/), or [Swagger Editor](https://editor.swagger.io/) + +- Acquire a bearer token using this python script locally: + ```sh + pip install azure-identity + ``` + ```python + from azure.identity import InteractiveBrowserCredential + tenant_id = "" + client_id = "" + credential = InteractiveBrowserCredential(tenant_id=tenant_id, client_id=client_id) + access_token = credential.get_token(f"{client_id}/.default").token + print(access_token) + ``` + +- Send a POST request to create an adapter resource: + ```http + POST http://..cloudapp.azure.com/adapters + Authorization: Bearer + Content-Type: application/json + ``` + ```json + { + "name": "mcp-example", + "imageName": "mcp-example", + "imageVersion": "1.0.0", + "description": "test" + } + ``` + +- After deploying the MCP server, use a client like [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) to test the connection. + > **Note:** A valid bearer token is still required in the Authorization header when connecting to the server. + + - To connect to the deployed `mcp-example` server, use: + - `http://..cloudapp.azure.com/adapters/mcp-example/mcp` (Streamable HTTP) + + - For other servers: + - `http://..cloudapp.azure.com/adapters/{name}/mcp` (Streamable HTTP) + - `http://..cloudapp.azure.com/adapters/{name}/sse` (SSE) + +### 6. Clean the Environment +To remove all deployed resources, delete the Azure resource group: +```sh +az group delete --name --yes +``` + +## 7. Production Onboarding (Follow-up) + +- **TLS Configuration** + Set up HTTPS on Azure Application Gateway (AAG) listener using valid TLS certificates. + +- **Network Security** + Restrict incoming traffic within the virtual network and configure Private Endpoints for enhanced network security. + +- **Telemetry** + Enable advanced telemetry, detailed metrics, and alerts to support monitoring and troubleshooting in production. + +- **Scaling** + Adjust scaling for `mcp-gateway` services and MCP servers based on expected load. + +- **Authentication & Authorization** + Set up OAuth 2.0 with Azure Entra ID (AAD) for authentication. + Implement fine-grained access control using RBAC or custom ACLs for `adapter` level permissions. ## Contributing diff --git a/deployment/azure-deploy.ps1 b/deployment/azure-deploy.ps1 new file mode 100644 index 0000000..40cb74c --- /dev/null +++ b/deployment/azure-deploy.ps1 @@ -0,0 +1,45 @@ +param( + [Parameter(Mandatory=$true)] + [ValidateScript({ + if ($_ -match '^[a-z0-9]+$') { + $true + } else { + throw "ResourceGroupName must contain only lowercase letters and numbers (no spaces or special characters)." + } + })] + [string]$ResourceGroupName, + + [Parameter(Mandatory=$true)] + [ValidateScript({ + if ($_ -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$') { + $true + } else { + throw "ClientId must be a valid GUID in the format 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'." + } + })] + [string]$ClientId, + + [string]$Location = "westus3" +) + +$TenantId = az account show --query tenantId --output tsv + +# Create Resource Group +az group create --name "$ResourceGroupName" --location "$Location" + +# Deploy Bicep +$deployment = az deployment group create ` + --resource-group $ResourceGroupName ` + --template-file deployment/infra/azure-deployment.bicep | ConvertFrom-Json + +# Set cloud-deployment.yml +(Get-Content -Raw "deployment/k8s/cloud-deployment-template.yml") ` + -replace "{IDENTIFIER}", $ResourceGroupName ` + -replace "{TENANT_ID}", $TenantId ` + -replace "{CLIENT_ID}", $ClientId ` + -replace "{AZURE_CLIENT_ID}", $deployment.properties.outputs.identityClientId.value ` + -replace "{APPINSIGHTS_CONNECTION_STRING}", $deployment.properties.outputs.applicationInsightConnectionString.value ` + | Set-Content "deployment/k8s/cloud-deployment.yml" + +# Deploy to AKS +az aks command invoke -g $ResourceGroupName -n mg-aks-"$ResourceGroupName" --command "kubectl apply -f cloud-deployment.yml" --file deployment/k8s/cloud-deployment.yml diff --git a/deployment/infra/azure-deployment.bicep b/deployment/infra/azure-deployment.bicep new file mode 100644 index 0000000..c06c5e5 --- /dev/null +++ b/deployment/infra/azure-deployment.bicep @@ -0,0 +1,369 @@ +// Parameters +param identifer string = resourceGroup().name +param location string = resourceGroup().location +param aksName string = 'mg-aks-${identifer}' +param acrName string = 'acr${identifer}' +param cosmosDbAccountName string = 'cosmos${identifer}' +param userAssignedIdentityName string = 'mg-identity-${identifer}' +param appInsightsName string = 'mg-ai-${identifer}' +param vnetName string = 'mg-vnet-${identifer}' +param aksSubnetName string = 'aks-subnet-${identifer}' +param appGwSubnetName string = 'mg-subnet-${identifer}' +param appGwName string = 'mg-aag-${identifer}' +param domainNameLabel string = identifer + +// VNet +resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = { + name: vnetName + location: location + properties: { + addressSpace: { + addressPrefixes: [ + '10.0.0.0/16' + ] + } + subnets: [ + { + name: aksSubnetName + properties: { + addressPrefix: '10.0.1.0/24' + } + } + { + name: appGwSubnetName + properties: { + addressPrefix: '10.0.2.0/24' + } + } + ] + } +} + +resource networkContributorRA 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(vnet.name, aks.name, aksSubnetName, 'network-contributor') + scope: vnet + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7') // Network Contributor + principalId: aks.identity.principalId + principalType: 'ServicePrincipal' + } +} + +// ACR +resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { + name: acrName + location: location + sku: { + name: 'Standard' + } + properties: { + policies: { + quarantinePolicy: { + status: 'disabled' + } + } + publicNetworkAccess: 'Enabled' + anonymousPullEnabled: false + networkRuleBypassOptions: 'AzureServices' + dataEndpointEnabled: false + adminUserEnabled: false + } +} + +// AKS Cluster +resource aks 'Microsoft.ContainerService/managedClusters@2023-04-01' = { + name: aksName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + dnsPrefix: aksName + agentPoolProfiles: [ + { + name: 'nodepool1' + count: 2 + vmSize: 'Standard_D4ds_v5' + osType: 'Linux' + mode: 'System' + osSKU: 'Ubuntu' + vnetSubnetID: resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, aksSubnetName) + } + ] + enableRBAC: true + aadProfile: { + managed: true + enableAzureRBAC: true + } + networkProfile: { + networkPlugin: 'azure' + loadBalancerSku: 'standard' + serviceCidr: '192.168.0.0/16' + dnsServiceIP: '192.168.0.10' + } + addonProfiles: {} + apiServerAccessProfile: { + enablePrivateCluster: false + } + oidcIssuerProfile: { + enabled: true + } + securityProfile: { + workloadIdentity: { + enabled: true + } + } + } + dependsOn: [vnet] +} + +// Attach ACR to AKS +resource acrRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(aks.id, acr.id, 'acrpull') + scope: acr + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull + principalId: aks.properties.identityProfile.kubeletidentity.objectId + principalType: 'ServicePrincipal' + } +} + +// Public IP for App Gateway +resource appGwPublicIp 'Microsoft.Network/publicIPAddresses@2022-05-01' = { + name: 'mg-pip-${identifer}' + location: location + sku: { + name: 'Standard' + } + properties: { + publicIPAllocationMethod: 'Static' + dnsSettings: { + domainNameLabel: domainNameLabel + } + } +} + +// Application Gateway +resource appGw 'Microsoft.Network/applicationGateways@2022-09-01' = { + name: appGwName + location: location + properties: { + sku: { + name: 'Standard_v2' + tier: 'Standard_v2' + capacity: 1 + } + gatewayIPConfigurations: [ + { + name: 'appGwIpConfig' + properties: { + subnet: { + id: resourceId('Microsoft.Network/virtualNetworks/subnets', vnetName, appGwSubnetName) + } + } + } + ] + frontendIPConfigurations: [ + { + name: 'appGwFrontendIP' + properties: { + publicIPAddress: { + id: appGwPublicIp.id + } + } + } + ] + frontendPorts: [ + { + name: 'httpPort' + properties: { + port: 80 + } + } + ] + backendAddressPools: [ + { + name: 'aksBackendPool' + properties: { + backendAddresses: [ + { + ipAddress: '10.0.1.100' + } + ] + } + } + ] + backendHttpSettingsCollection: [ + { + name: 'httpSettings' + properties: { + port: 8000 + protocol: 'Http' + pickHostNameFromBackendAddress: false + requestTimeout: 20 + probe: { + id: resourceId('Microsoft.Network/applicationGateways/probes', appGwName, 'mcpgateway-probe') + } + } + } + ] + httpListeners: [ + { + name: 'httpListener' + properties: { + frontendIPConfiguration: { + id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', appGwName, 'appGwFrontendIP') + } + frontendPort: { + id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', appGwName, 'httpPort') + } + protocol: 'Http' + } + } + ] + requestRoutingRules: [ + { + name: 'rule1' + properties: { + ruleType: 'Basic' + httpListener: { + id: resourceId('Microsoft.Network/applicationGateways/httpListeners', appGwName, 'httpListener') + } + backendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', appGwName, 'aksBackendPool') + } + backendHttpSettings: { + id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGwName, 'httpSettings') + } + priority: 100 + } + } + ] + probes: [ + { + name: 'mcpgateway-probe' + properties: { + protocol: 'Http' + host: '10.0.1.100' + path: '/ping' + interval: 30 + timeout: 30 + unhealthyThreshold: 3 + pickHostNameFromBackendHttpSettings: false + minServers: 0 + match: { + statusCodes: [ + '200-399' + ] + } + } + } + ] + } + dependsOn: [vnet] +} + + +// User Assigned Identity +resource uai 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: userAssignedIdentityName + location: location +} + +// Federated Credential +resource federatedCred 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = { + parent: uai + name: 'mg-sa-federation-${identifer}' + properties: { + audiences: [ + 'api://AzureADTokenExchange' + ] + issuer: aks.properties.oidcIssuerProfile.issuerURL + subject: 'system:serviceaccount:adapter:mcpgateway-sa' + } +} + +// CosmosDB Account +resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { + name: cosmosDbAccountName + location: location + kind: 'GlobalDocumentDB' + properties: { + databaseAccountOfferType: 'Standard' + locations: [ + { + locationName: location + failoverPriority: 0 + } + ] + capabilities: [] + consistencyPolicy: { + defaultConsistencyLevel: 'Session' + } + enableFreeTier: false + } +} + +// Cosmos DB SQL Database +resource cosmosDbSqlDb 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' = { + parent: cosmosDb + name: 'McpGatewayDb' + properties: { + resource: { + id: 'McpGatewayDb' + } + } +} + +// Cosmos DB SQL Containers +resource adapterContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = { + name: 'AdapterContainer' + parent: cosmosDbSqlDb + properties: { + resource: { + id: 'AdapterContainer' + partitionKey: { + paths: ['/id'] + kind: 'Hash' + } + } + } +} + +resource cacheContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = { + name: 'CacheContainer' + parent: cosmosDbSqlDb + properties: { + resource: { + id: 'CacheContainer' + partitionKey: { + paths: ['/id'] + kind: 'Hash' + } + } + } +} + +// Cosmos DB Data Contributor Role Assignment to UAI +resource cosmosDbRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-11-15' = { + parent: cosmosDb + name: guid(cosmosDb.name, uai.id, 'data-contributor') + properties: { + roleDefinitionId: resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', cosmosDb.name, '00000000-0000-0000-0000-000000000002') + principalId: uai.properties.principalId + scope: cosmosDb.id + } +} + +// Application Insights +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + } +} + +output identityClientId string = uai.properties.clientId +output applicationInsightConnectionString string = appInsights.properties.ConnectionString diff --git a/deployment/k8s/cloud-deployment-template.yml b/deployment/k8s/cloud-deployment-template.yml new file mode 100644 index 0000000..01501e1 --- /dev/null +++ b/deployment/k8s/cloud-deployment-template.yml @@ -0,0 +1,107 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: adapter +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mcpgateway + namespace: adapter +spec: + replicas: 1 + selector: + matchLabels: + app: mcpgateway + template: + metadata: + labels: + app: mcpgateway + azure.workload.identity/use: "true" + spec: + serviceAccountName: mcpgateway-sa + containers: + - name: mcpgateway-container + image: ghcr.io/microsoft/mcp-gateway:latest + ports: + - containerPort: 8000 + env: + - name: ASPNETCORE_ENVIRONMENT + value: "Production" + envFrom: + - configMapRef: + name: app-config +--- +apiVersion: v1 +kind: Service +metadata: + name: mcpgateway-service + namespace: adapter + annotations: + service.beta.kubernetes.io/azure-load-balancer-internal: "true" +spec: + type: LoadBalancer + loadBalancerIP: 10.0.1.100 + selector: + app: mcpgateway + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mcpgateway-sa + namespace: adapter + annotations: + azure.workload.identity/client-id: "{AZURE_CLIENT_ID}" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: app-manager + namespace: adapter +rules: + - apiGroups: ["apps"] + resources: ["statefulsets"] + verbs: ["get", "list", "create", "update", "delete"] + - apiGroups: [""] + resources: ["services", "pods"] + verbs: ["get", "list", "create", "update", "delete"] + - apiGroups: [""] + resources: ["pods/log"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: app-manager-binding + namespace: adapter +subjects: + - kind: ServiceAccount + name: mcpgateway-sa + namespace: adapter +roleRef: + kind: Role + name: app-manager + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + namespace: adapter +data: + AzureAd__Instance: "https://login.microsoftonline.com/" + AzureAd__TenantId: "{TENANT_ID}" + AzureAd__ClientId: "{CLIENT_ID}" + AzureAd__Audience: "{CLIENT_ID}" + + CosmosSettings__AccountEndpoint: "https://cosmos{IDENTIFIER}.documents.azure.com:443/" + CosmosSettings__DatabaseName: "McpGatewayDb" + CosmosSettings__ConnectionString: "" + + ApplicationInsights__ConnectionString: "{APPINSIGHTS_CONNECTION_STRING}" + + ContainerRegistrySettings__Endpoint: "acr{IDENTIFIER}.azurecr.io" diff --git a/k8s/deployment.yml b/deployment/k8s/local-deployment.yml similarity index 100% rename from k8s/deployment.yml rename to deployment/k8s/local-deployment.yml diff --git a/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs b/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs index 46493f1..cb3f3fa 100644 --- a/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs +++ b/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs @@ -8,6 +8,7 @@ namespace Microsoft.McpGateway.Management.Contracts public class AdapterResource : AdapterData { [JsonPropertyOrder(-1)] + [JsonPropertyName("id")] public required string Id { get; set; } /// diff --git a/dotnet/Microsoft.McpGateway.Management/src/Store/CosmosAdapterResourceStore.cs b/dotnet/Microsoft.McpGateway.Management/src/Store/CosmosAdapterResourceStore.cs index 0b02ef6..1f6086b 100644 --- a/dotnet/Microsoft.McpGateway.Management/src/Store/CosmosAdapterResourceStore.cs +++ b/dotnet/Microsoft.McpGateway.Management/src/Store/CosmosAdapterResourceStore.cs @@ -50,7 +50,7 @@ await db.Database.CreateContainerIfNotExistsAsync( var response = await _container.ReadItemAsync( id: name, partitionKey: new PartitionKey(name), - cancellationToken: cancellationToken); + cancellationToken: cancellationToken).ConfigureAwait(false); return response.Resource; } @@ -64,11 +64,13 @@ await db.Database.CreateContainerIfNotExistsAsync( public async Task UpsertAsync(AdapterResource adapter, CancellationToken cancellationToken) { using var stream = new MemoryStream(); - await JsonSerializer.SerializeAsync(stream, adapter, cancellationToken: cancellationToken); - await _container.UpsertItemStreamAsync( + await JsonSerializer.SerializeAsync(stream, adapter, cancellationToken: cancellationToken).ConfigureAwait(false); + stream.Position = 0; + var response = await _container.UpsertItemStreamAsync( stream, partitionKey: new PartitionKey(adapter.Id), - cancellationToken: cancellationToken); + cancellationToken: cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); } public async Task DeleteAsync(string name, CancellationToken cancellationToken) @@ -78,7 +80,7 @@ public async Task DeleteAsync(string name, CancellationToken cancellationToken) await _container.DeleteItemAsync( id: name, partitionKey: new PartitionKey(name), - cancellationToken: cancellationToken); + cancellationToken: cancellationToken).ConfigureAwait(false); } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { @@ -93,7 +95,7 @@ public async Task> ListAsync(CancellationToken canc while (query.HasMoreResults) { - var response = await query.ReadNextAsync(cancellationToken); + var response = await query.ReadNextAsync(cancellationToken).ConfigureAwait(false); results.AddRange(response.Resource); } diff --git a/dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj b/dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj index 4a75a64..8035576 100644 --- a/dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj +++ b/dotnet/Microsoft.McpGateway.Service/src/Microsoft.McpGateway.Service.csproj @@ -1,4 +1,4 @@ - + net8.0 diff --git a/dotnet/Microsoft.McpGateway.Service/src/Program.cs b/dotnet/Microsoft.McpGateway.Service/src/Program.cs index 901e29e..f4ecad8 100644 --- a/dotnet/Microsoft.McpGateway.Service/src/Program.cs +++ b/dotnet/Microsoft.McpGateway.Service/src/Program.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Fluent; -using Microsoft.Extensions.Caching.Cosmos; using Microsoft.Identity.Web; using Microsoft.McpGateway.Management.Deployment; using Microsoft.McpGateway.Management.Service; @@ -15,8 +14,10 @@ using Microsoft.McpGateway.Service.Session; var builder = WebApplication.CreateBuilder(args); +var credential = new DefaultAzureCredential(); builder.Services.AddApplicationInsightsTelemetry(); +builder.Services.AddLogging(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -37,10 +38,10 @@ { var config = builder.Configuration.GetSection("CosmosSettings"); var connectionString = config["ConnectionString"]; - var client = string.IsNullOrEmpty(connectionString) ? new CosmosClient(config["AccountEndpoint"], new DefaultAzureCredential()) : new CosmosClient(connectionString); + var client = string.IsNullOrEmpty(connectionString) ? new CosmosClient(config["AccountEndpoint"], credential) : new CosmosClient(connectionString); return new CosmosAdapterResourceStore(client, config["DatabaseName"]!, "AdapterContainer", c.GetRequiredService>()); }); - builder.Services.AddCosmosCache((CosmosCacheOptions options) => + builder.Services.AddCosmosCache(options => { var config = builder.Configuration.GetSection("CosmosSettings"); var endpoint = config["AccountEndpoint"]; @@ -50,7 +51,7 @@ options.DatabaseName = config["DatabaseName"]!; options.CreateIfNotExists = true; - options.ClientBuilder = string.IsNullOrEmpty(connectionString) ? new CosmosClientBuilder(endpoint, new DefaultAzureCredential()) : new CosmosClientBuilder(connectionString); + options.ClientBuilder = string.IsNullOrEmpty(connectionString) ? new CosmosClientBuilder(endpoint, credential) : new CosmosClientBuilder(connectionString); }); } diff --git a/dotnet/Microsoft.McpGateway.Service/src/Properties/PublishProfiles/github.pubxml b/dotnet/Microsoft.McpGateway.Service/src/Properties/PublishProfiles/github.pubxml new file mode 100644 index 0000000..13fd4cb --- /dev/null +++ b/dotnet/Microsoft.McpGateway.Service/src/Properties/PublishProfiles/github.pubxml @@ -0,0 +1,17 @@ + + + + + Container + NetSdk + ghcr.io + + latest + ContainerRegistry + Release + x64 + linux-x64 + a014200e-7f7d-a05f-7168-e3d6969022fe + <_TargetId>NetSdkCustomContainerRegistry + + \ No newline at end of file diff --git a/dotnet/Microsoft.McpGateway.Service/src/appsettings.json b/dotnet/Microsoft.McpGateway.Service/src/appsettings.json index b7dd04f..c38a802 100644 --- a/dotnet/Microsoft.McpGateway.Service/src/appsettings.json +++ b/dotnet/Microsoft.McpGateway.Service/src/appsettings.json @@ -14,7 +14,7 @@ }, "CosmosSettings": { "AccountEndpoint": "https://.documents.azure.com:443/", - "ConnectionString": "", + "ConnectionString": "", "DatabaseName": "McpGatewayDb" }, "ApplicationInsights": { diff --git a/infra-diagram.png b/infra-diagram.png new file mode 100644 index 0000000..874ef01 Binary files /dev/null and b/infra-diagram.png differ diff --git a/openapi/mcp-gateway.openapi.json b/openapi/mcp-gateway.openapi.json index d45fb88..5d35b1f 100644 --- a/openapi/mcp-gateway.openapi.json +++ b/openapi/mcp-gateway.openapi.json @@ -6,6 +6,10 @@ "description": "RESTful APIs for MCP Server Management" }, "servers": [ + { + "url": "http://..cloudapp.azure.com", + "description": "Azure Deployment" + }, { "url": "http://localhost:8000", "description": "Local development"