diff --git a/README.md b/README.md
index e59241b..d2f55b2 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -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:
@@ -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:
diff --git a/deployment/infra/azure-deployment.bicep b/deployment/infra/azure-deployment.bicep
index 00eb191..a3b1768 100644
--- a/deployment/infra/azure-deployment.bicep
+++ b/deployment/infra/azure-deployment.bicep
@@ -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
@@ -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
@@ -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
@@ -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
diff --git a/deployment/infra/azure-deployment.json b/deployment/infra/azure-deployment.json
index 0ddad78..7a99175 100644
--- a/deployment/infra/azure-deployment.json
+++ b/deployment/infra/azure-deployment.json
@@ -5,7 +5,7 @@
"_generator": {
"name": "bicep",
"version": "0.36.1.42791",
- "templateHash": "4470748714752194409"
+ "templateHash": "7651876796214077404"
}
},
"parameters": {
@@ -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",
@@ -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",
@@ -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"
],
@@ -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]"
@@ -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')))]"
]
}
]
diff --git a/deployment/k8s/cloud-deployment-template.yml b/deployment/k8s/cloud-deployment-template.yml
index 12e1b74..8b97b44 100644
--- a/deployment/k8s/cloud-deployment-template.yml
+++ b/deployment/k8s/cloud-deployment-template.yml
@@ -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:
@@ -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
diff --git a/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterData.cs b/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterData.cs
index b5f23ea..fda9f88 100644
--- a/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterData.cs
+++ b/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterData.cs
@@ -60,6 +60,12 @@ public class AdapterData
[JsonPropertyOrder(8)]
public string Description { get; set; } = string.Empty;
+ ///
+ /// Indicates whether to use workload identity for the deployed adapter instance. Default is false.
+ ///
+ [JsonPropertyOrder(9)]
+ public bool UseWorkloadIdentity { get; set; } = false;
+
public AdapterData(
string name,
string imageName,
@@ -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);
@@ -82,6 +89,7 @@ public AdapterData(
EnvironmentVariables = environmentVariables ?? [];
ReplicaCount = replicaCount ?? 1;
Description = description;
+ UseWorkloadIdentity = useWorkloadIdentity;
}
public AdapterData() { }
diff --git a/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs b/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs
index cb3f3fa..d080c4d 100644
--- a/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs
+++ b/dotnet/Microsoft.McpGateway.Management/src/Contracts/AdapterResource.cs
@@ -14,23 +14,23 @@ public class AdapterResource : AdapterData
///
/// The ID of the user who created the adapter.
///
- [JsonPropertyOrder(9)]
+ [JsonPropertyOrder(30)]
public required string CreatedBy { get; set; }
///
/// The date and time when the adapter was created.
///
- [JsonPropertyOrder(10)]
+ [JsonPropertyOrder(31)]
public DateTimeOffset CreatedAt { get; set; }
///
/// The date and time when the adapter was created.
///
- [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;
@@ -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() { }
diff --git a/dotnet/Microsoft.McpGateway.Management/src/Deployment/KubernetesAdapterDeploymentManager.cs b/dotnet/Microsoft.McpGateway.Management/src/Deployment/KubernetesAdapterDeploymentManager.cs
index f0b34b6..d3aac5c 100644
--- a/dotnet/Microsoft.McpGateway.Management/src/Deployment/KubernetesAdapterDeploymentManager.cs
+++ b/dotnet/Microsoft.McpGateway.Management/src/Deployment/KubernetesAdapterDeploymentManager.cs
@@ -32,7 +32,8 @@ public async Task CreateDeploymentAsync(AdapterData request, CancellationToken c
var labels = new Dictionary
{
{ $"{AdapterNamespace}/type", "mcp" },
- { $"{AdapterNamespace}/name", request.Name }
+ { $"{AdapterNamespace}/name", request.Name },
+ { "azure.workload.identity/use", request.UseWorkloadIdentity.ToString().ToLowerInvariant() }
};
var statefulSet = new V1StatefulSet
@@ -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,
@@ -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
diff --git a/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs b/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs
index e0e96ea..fc41540 100644
--- a/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs
+++ b/dotnet/Microsoft.McpGateway.Service/src/Controllers/AdapterReverseProxyController.cs
@@ -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)
diff --git a/mcp-example-server/requirements.txt b/mcp-example-server/requirements.txt
index 74cf909..76cb1af 100644
--- a/mcp-example-server/requirements.txt
+++ b/mcp-example-server/requirements.txt
@@ -1 +1 @@
-fastmcp==2.6.1
+fastmcp==2.11.3
diff --git a/mcp-proxy-server/Dockerfile b/mcp-proxy-server/Dockerfile
new file mode 100644
index 0000000..28391c0
--- /dev/null
+++ b/mcp-proxy-server/Dockerfile
@@ -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"]
diff --git a/mcp-proxy-server/README.md b/mcp-proxy-server/README.md
new file mode 100644
index 0000000..75e2d51
--- /dev/null
+++ b/mcp-proxy-server/README.md
@@ -0,0 +1,119 @@
+# Proxying Local & Remote MCP Servers
+
+## What’s New
+
+### Proxying Local Stdio MCP Server
+- Spin up local MCP servers behind the gateway by specifying a command (`npx`, `uvx`, etc.).
+- Expose them remotely via HTTP/Streamable so cloud agents can connect.
+- Support workload identity for secure authentication with Azure resources.
+
+With this, you can transform **local-only MCP servers** into **cloud-accessible services** that plug directly into your AI workflows.
+
+### Proxying Remote HTTP MCP Server
+- Forward requests from the gateway to an existing MCP server over Streamable HTTP.
+
+
+## Instructions
+
+### Preparation
+
+- Make sure the cloud deployment has been done.
+- Build the MCP proxy server image in ACR.
+ ```sh
+ az acr build -r "mgreg$resourceLabel" -f mcp-proxy-server/Dockerfile mcp-proxy-server -t "mgreg$resourceLabel.azurecr.io/mcp-proxy:1.0.0"
+ ```
+
+- Configure permissions for the workload identity principal (If setting up a local mcp server)
+`mg-identity--workload`.
+This identity is created by deployment. The MCP server will use the workload identity for upstream resouce access.
+
+### Proxying Local Servers
+For starting a local MCP server in stdio and proxying the traffice through gateway to it.
+Set server startup command and arguments in environment variables:
+ - `MCP_COMMAND`
+ - `MCP_ARGS`
+
+Set `useWorkloadIdentity` to be true if need the server to use the workload identity.
+
+ > **Note:** When using a bridged local server, certain system packages may be missing by default. To address this, you can install the required packages within a custom Dockerfile and build your own `mcp-proxy` image.
+
+### Proxying Remote Servers
+For proxying another internal mcp server hosted in streamable HTTP. Set the target ednpoint in environment variable
+ - `MCP_PROXY_URL`
+
+
+## Examples
+
+Example payloads to send to `mcp-gateway` using the `POST /adapters` endpoint to launch a mcp server remotely.
+
+#### Example 1: Bridged [Azure MCP Server](https://github.com/microsoft/mcp/tree/main/servers/Azure.Mcp.Server)
+```json
+{
+ "name": "ado-remote",
+ "imageName": "mcp-proxy",
+ "imageVersion": "1.0.0",
+ "environmentVariables": {
+ "MCP_COMMAND": "npx",
+ "MCP_ARGS": "-y @azure/mcp@latest server start",
+ "AZURE_MCP_INCLUDE_PRODUCTION_CREDENTIALS": "true",
+ "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT": "1"
+ },
+ "description": "Bridged ADO local MCP server"
+}
+```
+
+#### Example 2: Bridged [Azure AI Foundry MCP Server](https://github.com/azure-ai-foundry/mcp-foundry)
+```json
+{
+ "name": "foundry",
+ "imageName": "mcp-proxy",
+ "imageVersion": "1.0.0",
+ "environmentVariables": {
+ "MCP_COMMAND": "uvx",
+ "MCP_ARGS": "--prerelease=allow --from git+https://github.com/azure-ai-foundry/mcp-foundry.git run-azure-ai-foundry-mcp"
+ },
+ "useWorkloadIdentity": true,
+ "description": "Bridged Azure AI Foundry Local MCP Server"
+}
+```
+
+#### Example 3: Bridged [Azure DevOps MCP Server](https://github.com/microsoft/azure-devops-mcp)
+```json
+{
+ "name": "ado-remote",
+ "imageName": "mcp-proxy",
+ "imageVersion": "1.0.0",
+ "environmentVariables": {
+ "MCP_COMMAND": "npx",
+ "MCP_ARGS": "-y @azure-devops/mcp contoso",
+ "ADO_MCP_AZURE_TOKEN_CREDENTIALS": "WorkloadIdentityCredential",
+ "AZURE_TOKEN_CREDENTIALS": "WorkloadIdentityCredential"
+ },
+ "useWorkloadIdentity": true,
+ "description": "Bridged ADO MCP Local Server"
+}
+```
+
+> **Note:** Different MCP servers have different conventions for reading credentials from the environment for setting up `TokenCredential` and connect to upstream resources. You may need to adjust the environment variable names/values per server.
+Examples:
+Some servers expect a general switch like `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential`
+Others use service-specific variables (e.g., `ADO_MCP_AZURE_TOKEN_CREDENTIALS`)
+
+#### Example 4: Proxied Internal MCP Server (Streamable HTTP)
+```json
+{
+ "name": "internal-mcp",
+ "imageName": "mcp-proxy",
+ "imageVersion": "1.0.0",
+ "environmentVariables": {
+ "MCP_PROXY_URL": "https://internal-mcp-server/mcp"
+ },
+ "description": "Proxied Internal MCP Server"
+}
+```
+
+## Security Considerations
+
+Before running in production
+- Implement appropriate access controls on the gateway level to prevent users from exploiting the workload identity access through it.
+- Always only register trusted MCP servers, enable network access policies on the server pods.
diff --git a/mcp-proxy-server/requirements.txt b/mcp-proxy-server/requirements.txt
new file mode 100644
index 0000000..8e064ac
--- /dev/null
+++ b/mcp-proxy-server/requirements.txt
@@ -0,0 +1,2 @@
+fastmcp==2.11.3
+validators==0.35.0
diff --git a/mcp-proxy-server/src/main.py b/mcp-proxy-server/src/main.py
new file mode 100644
index 0000000..824c5f4
--- /dev/null
+++ b/mcp-proxy-server/src/main.py
@@ -0,0 +1,64 @@
+from fastmcp import FastMCP
+import os, sys, shlex
+import validators
+
+MAX_ARGS = 64
+MAX_ARG_LEN = 512
+
+def bad(msg: str):
+ print(f"[shim] {msg}", file=sys.stderr)
+ sys.exit(1)
+
+def safe(arg: str) -> bool:
+ return (
+ isinstance(arg, str)
+ and 0 < len(arg) <= MAX_ARG_LEN
+ and "\x00" not in arg
+ and "\n" not in arg
+ and "\r" not in arg
+ )
+
+def main():
+ proxy_url = os.environ.get("MCP_PROXY_URL")
+ if proxy_url:
+ if not validators.url(proxy_url):
+ bad("Invalid MCP_PROXY_URL: must be http(s) and a well-formed URL")
+ config = {
+ "mcpServers": {
+ "default": {
+ "url": proxy_url,
+ "transport": "http"
+ }
+ }
+ }
+ else:
+ cmd = os.environ.get("MCP_COMMAND")
+ if not cmd:
+ bad("Must set either MCP_PROXY_URL or MCP_COMMAND environment variable")
+ raw_args = os.environ.get("MCP_ARGS", "")
+ args = shlex.split(raw_args) if raw_args else []
+
+ if not safe(cmd):
+ bad("Unsafe command")
+ if len(args) > MAX_ARGS:
+ bad("Too many args")
+ for a in args:
+ if not safe(a):
+ bad(f"Unsafe arg: {a!r}")
+
+ config = {
+ "mcpServers": {
+ "default": {
+ "type": "stdio",
+ "command": cmd,
+ "args": args,
+ "env": dict(os.environ)
+ }
+ }
+ }
+
+ app = FastMCP.as_proxy(config, name="MCP Proxy Server")
+ app.run(transport="streamable-http", host="0.0.0.0", port=8000)
+
+if __name__ == "__main__":
+ main()