Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

## Overview

#### *New*: Support for **Proxying Local & Remote MCP Servers**. See [examples and usage](mcp-proxy-server/README.md).

This project provides:

- A data gateway for routing traffic to MCP servers with session affinity.
Expand Down Expand Up @@ -146,7 +148,7 @@ kubectl port-forward -n adapter svc/mcpgateway-service 8000:8000
```

### 8. Test the API - MCP Server Access
- After deploying the MCP server, use a client like [VS Code](https://code.visualstudio.com/) to test the connection. Refer to the guide: [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
- After deploying the MCP server, use a client like [VS Code](https://code.visualstudio.com/) to test the connection. Refer to the guide: [Use MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
> **Note:** Ensure VSCode is up to date to access the latest MCP features.

- To connect to the deployed `mcp-example` server, use:
Expand Down Expand Up @@ -288,7 +290,7 @@ az acr build -r "mgreg$resourceLabel" -f mcp-example-server/Dockerfile mcp-examp

### 6. Test the API - MCP Server Access

- After deploying the MCP server, use a client like [VS Code](https://code.visualstudio.com/) to test the connection. Refer to the guide: [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
- After deploying the MCP server, use a client like [VS Code](https://code.visualstudio.com/) to test the connection. Refer to the guide: [Use MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
> **Note:** Ensure VSCode is up to date to access the latest MCP features.

- To connect to the deployed `mcp-example` server, use:
Expand Down
26 changes: 25 additions & 1 deletion deployment/infra/azure-deployment.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,18 @@ resource uai 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
location: location
}

// User Assigned Identity for amdin
// User Assigned Identity for admin
resource uaiAdmin 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: '${userAssignedIdentityName}-admin'
location: location
}

// User Assigned Identity for server workload instance
resource uaiWorkload 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: '${userAssignedIdentityName}-workload'
location: location
}

resource uaiAdminContributorOnAks 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(aks.name, uaiAdmin.name, 'AKSContributor')
scope: aks
Expand Down Expand Up @@ -319,6 +325,19 @@ resource federatedCred 'Microsoft.ManagedIdentity/userAssignedIdentities/federat
}
}

// Federated Credential
resource federatedCredWorkload 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = {
parent: uaiWorkload
name: '${federatedCredName}-workload'
properties: {
audiences: [
'api://AzureADTokenExchange'
]
issuer: aks.properties.oidcIssuerProfile.issuerURL
subject: 'system:serviceaccount:adapter:workload-sa'
}
}

// CosmosDB Account
resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
name: cosmosDbAccountName
Expand Down Expand Up @@ -417,6 +436,7 @@ resource kubernetesDeployment 'Microsoft.Resources/deploymentScripts@2023-08-01'
retentionInterval: 'P1D'
scriptContent: '''
sed -i "s|\${AZURE_CLIENT_ID}|$AZURE_CLIENT_ID|g" cloud-deployment-template.yml
sed -i "s|\${WORKLOAD_CLIENT_ID}|$WORKLOAD_CLIENT_ID|g" cloud-deployment-template.yml
sed -i "s|\${TENANT_ID}|$TENANT_ID|g" cloud-deployment-template.yml
sed -i "s|\${CLIENT_ID}|$CLIENT_ID|g" cloud-deployment-template.yml
sed -i "s|\${APPINSIGHTS_CONNECTION_STRING}|$APPINSIGHTS_CONNECTION_STRING|g" cloud-deployment-template.yml
Expand All @@ -441,6 +461,10 @@ resource kubernetesDeployment 'Microsoft.Resources/deploymentScripts@2023-08-01'
name: 'AZURE_CLIENT_ID'
value: uai.properties.clientId
}
{
name: 'WORKLOAD_CLIENT_ID'
value: uaiWorkload.properties.clientId
}
{
name: 'APPINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
Expand Down
33 changes: 30 additions & 3 deletions deployment/infra/azure-deployment.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"_generator": {
"name": "bicep",
"version": "0.36.1.42791",
"templateHash": "4470748714752194409"
"templateHash": "7651876796214077404"
}
},
"parameters": {
Expand Down Expand Up @@ -323,6 +323,12 @@
"name": "[format('{0}-admin', variables('userAssignedIdentityName'))]",
"location": "[parameters('location')]"
},
{
"type": "Microsoft.ManagedIdentity/userAssignedIdentities",
"apiVersion": "2023-01-31",
"name": "[format('{0}-workload', variables('userAssignedIdentityName'))]",
"location": "[parameters('location')]"
},
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
Expand Down Expand Up @@ -369,6 +375,22 @@
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('userAssignedIdentityName'))]"
]
},
{
"type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials",
"apiVersion": "2023-01-31",
"name": "[format('{0}/{1}', format('{0}-workload', variables('userAssignedIdentityName')), format('{0}-workload', variables('federatedCredName')))]",
"properties": {
"audiences": [
"api://AzureADTokenExchange"
],
"issuer": "[reference(resourceId('Microsoft.ContainerService/managedClusters', variables('aksName')), '2023-04-01').oidcIssuerProfile.issuerURL]",
"subject": "system:serviceaccount:adapter:workload-sa"
},
"dependsOn": [
"[resourceId('Microsoft.ContainerService/managedClusters', variables('aksName'))]",
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-workload', variables('userAssignedIdentityName')))]"
]
},
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"apiVersion": "2023-04-15",
Expand Down Expand Up @@ -481,7 +503,7 @@
"azCliVersion": "2.60.0",
"timeout": "PT30M",
"retentionInterval": "P1D",
"scriptContent": " sed -i \"s|\\${AZURE_CLIENT_ID}|$AZURE_CLIENT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${TENANT_ID}|$TENANT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${CLIENT_ID}|$CLIENT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${APPINSIGHTS_CONNECTION_STRING}|$APPINSIGHTS_CONNECTION_STRING|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${IDENTIFIER}|$IDENTIFIER|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${REGION}|$REGION|g\" cloud-deployment-template.yml\r\n\r\n az aks command invoke -g $ResourceGroupName -n mg-aks-\"$ResourceGroupName\" --command \"kubectl apply -f cloud-deployment-template.yml\" --file cloud-deployment-template.yml\r\n ",
"scriptContent": " sed -i \"s|\\${AZURE_CLIENT_ID}|$AZURE_CLIENT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${WORKLOAD_CLIENT_ID}|$WORKLOAD_CLIENT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${TENANT_ID}|$TENANT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${CLIENT_ID}|$CLIENT_ID|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${APPINSIGHTS_CONNECTION_STRING}|$APPINSIGHTS_CONNECTION_STRING|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${IDENTIFIER}|$IDENTIFIER|g\" cloud-deployment-template.yml\r\n sed -i \"s|\\${REGION}|$REGION|g\" cloud-deployment-template.yml\r\n\r\n az aks command invoke -g $ResourceGroupName -n mg-aks-\"$ResourceGroupName\" --command \"kubectl apply -f cloud-deployment-template.yml\" --file cloud-deployment-template.yml\r\n ",
"supportingScriptUris": [
"https://raw.githubusercontent.com/microsoft/mcp-gateway/refs/heads/main/deployment/k8s/cloud-deployment-template.yml"
],
Expand All @@ -498,6 +520,10 @@
"name": "AZURE_CLIENT_ID",
"value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('userAssignedIdentityName')), '2023-01-31').clientId]"
},
{
"name": "WORKLOAD_CLIENT_ID",
"value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-workload', variables('userAssignedIdentityName'))), '2023-01-31').clientId]"
},
{
"name": "APPINSIGHTS_CONNECTION_STRING",
"value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2020-02-02').ConnectionString]"
Expand All @@ -520,7 +546,8 @@
"[resourceId('Microsoft.ContainerService/managedClusters', variables('aksName'))]",
"[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]",
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('userAssignedIdentityName'))]",
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-admin', variables('userAssignedIdentityName')))]"
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-admin', variables('userAssignedIdentityName')))]",
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('{0}-workload', variables('userAssignedIdentityName')))]"
]
}
]
Expand Down
13 changes: 12 additions & 1 deletion deployment/k8s/cloud-deployment-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ metadata:
annotations:
azure.workload.identity/client-id: "${AZURE_CLIENT_ID}"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: workload-sa
namespace: adapter
annotations:
azure.workload.identity/client-id: "${WORKLOAD_CLIENT_ID}"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
Expand All @@ -65,13 +73,16 @@ metadata:
rules:
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "create", "update", "delete"]
verbs: ["get", "list", "create", "update", "delete", "patch"]
- apiGroups: [""]
resources: ["services", "pods"]
verbs: ["get", "list", "create", "update", "delete"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public class AdapterData
[JsonPropertyOrder(8)]
public string Description { get; set; } = string.Empty;

/// <summary>
/// Indicates whether to use workload identity for the deployed adapter instance. Default is false.
/// </summary>
[JsonPropertyOrder(9)]
public bool UseWorkloadIdentity { get; set; } = false;

public AdapterData(
string name,
string imageName,
Expand All @@ -68,7 +74,8 @@ public AdapterData(
int? replicaCount = 1,
string description = "",
ServerProtocol protocol = ServerProtocol.MCP,
ConnectionType connectionType = ConnectionType.StreamableHttp)
ConnectionType connectionType = ConnectionType.StreamableHttp,
bool useWorkloadIdentity = false)
{
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(imageName);
Expand All @@ -82,6 +89,7 @@ public AdapterData(
EnvironmentVariables = environmentVariables ?? [];
ReplicaCount = replicaCount ?? 1;
Description = description;
UseWorkloadIdentity = useWorkloadIdentity;
}

public AdapterData() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ public class AdapterResource : AdapterData
/// <summary>
/// The ID of the user who created the adapter.
/// </summary>
[JsonPropertyOrder(9)]
[JsonPropertyOrder(30)]
public required string CreatedBy { get; set; }

/// <summary>
/// The date and time when the adapter was created.
/// </summary>
[JsonPropertyOrder(10)]
[JsonPropertyOrder(31)]
public DateTimeOffset CreatedAt { get; set; }

/// <summary>
/// The date and time when the adapter was created.
/// </summary>
[JsonPropertyOrder(11)]
[JsonPropertyOrder(32)]
public DateTimeOffset LastUpdatedAt { get; set; }

public AdapterResource(AdapterData adapterData, string createdBy, DateTimeOffset createdAt, DateTimeOffset lastUpdatedAt)
: base(adapterData.Name, adapterData.ImageName, adapterData.ImageVersion, adapterData.EnvironmentVariables, adapterData.ReplicaCount, adapterData.Description, adapterData.Protocol, adapterData.ConnectionType)
: base(adapterData.Name, adapterData.ImageName, adapterData.ImageVersion, adapterData.EnvironmentVariables, adapterData.ReplicaCount, adapterData.Description, adapterData.Protocol, adapterData.ConnectionType, adapterData.UseWorkloadIdentity)
{
CreatedBy = createdBy;
CreatedAt = createdAt;
Expand All @@ -51,7 +51,8 @@ public static AdapterResource Create(AdapterData data, string createdBy, DateTim
ConnectionType = data.ConnectionType,
CreatedBy = createdBy,
CreatedAt = createdAt,
LastUpdatedAt = DateTime.UtcNow
LastUpdatedAt = DateTime.UtcNow,
UseWorkloadIdentity = data.UseWorkloadIdentity
};

public AdapterResource() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
var labels = new Dictionary<string, string>
{
{ $"{AdapterNamespace}/type", "mcp" },
{ $"{AdapterNamespace}/name", request.Name }
{ $"{AdapterNamespace}/name", request.Name },
{ "azure.workload.identity/use", request.UseWorkloadIdentity.ToString().ToLowerInvariant() }
};

var statefulSet = new V1StatefulSet
Expand All @@ -48,6 +49,7 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
Metadata = new V1ObjectMeta { Labels = labels },
Spec = new V1PodSpec
{
ServiceAccountName = "workload-sa",
SecurityContext = new V1PodSecurityContext
{
RunAsUser = 1100,
Expand All @@ -72,7 +74,6 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
SecurityContext = new V1SecurityContext
{
AllowPrivilegeEscalation = false,
ReadOnlyRootFilesystem = true,
Capabilities = new V1Capabilities { Drop = ["ALL"] }
},
Resources = new V1ResourceRequirements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ private static Uri ReplaceUriAddress(Uri originalUri, string newAddress)

var newBaseUri = new Uri(newAddress, UriKind.Absolute);
var path = '/' + string.Join('/', segments.Skip(2));
if (path.EndsWith("/messages") || path.EndsWith("/mcp"))
if (path.EndsWith("/messages"))
path += "/";

var newUriBuilder = new UriBuilder(newBaseUri.Scheme, newBaseUri.Host, newBaseUri.Port)
Expand Down
2 changes: 1 addition & 1 deletion mcp-example-server/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
fastmcp==2.6.1
fastmcp==2.11.3
47 changes: 47 additions & 0 deletions mcp-proxy-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
FROM python:3.12-slim

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y curl ca-certificates gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | tee /etc/apt/keyrings/nodesource.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" \
| tee /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates tini dumb-init \
&& rm -rf /var/lib/apt/lists/* \
&& curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh

RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*

ENV PATH="/root/.local/bin:${PATH}"

RUN python --version && node --version && npm --version && npx --version && uv --version

# Create non-root user
RUN adduser --disabled-password --gecos '' --uid 1100 mcpuser

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src/main.py /app/main.py

# Change ownership to non-root user
RUN chown -R mcpuser:mcpuser /app \
&& chown -R mcpuser:mcpuser /home/mcpuser

# Switch to non-root user
USER 1100

EXPOSE 8080

ENTRYPOINT ["python", "/app/main.py"]
Loading