Skip to content

Commit 3e94838

Browse files
authored
Add biceps for configuring built-in auth (#45)
* add biceps for configuring built-in auth * update readme with built-in auth content
1 parent 97e6957 commit 3e94838

File tree

8 files changed

+398
-80
lines changed

8 files changed

+398
-80
lines changed

.vscode/mcp.json

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
11
{
2-
"inputs": [
3-
{
4-
"type": "promptString",
5-
"id": "functions-mcp-extension-system-key",
6-
"description": "Azure Functions MCP Extension System Key",
7-
"password": true
8-
},
9-
{
10-
"type": "promptString",
11-
"id": "functionapp-name",
12-
"description": "Azure Functions App Name"
13-
}
14-
],
15-
"servers": {
16-
"remote-mcp-function": {
17-
"type": "http",
18-
"url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp",
19-
"headers": {
20-
"x-functions-key": "${input:functions-mcp-extension-system-key}"
21-
}
22-
},
23-
"local-mcp-function": {
24-
"type": "http",
25-
"url": "http://0.0.0.0:7071/runtime/webhooks/mcp"
26-
}
27-
}
2+
"servers": {
3+
"remote-mcp-function": {
4+
"type": "http",
5+
"url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp"
6+
},
7+
"local-mcp-function": {
8+
"type": "http",
9+
"url": "http://0.0.0.0:7071/runtime/webhooks/mcp"
10+
}
11+
},
12+
"inputs": [
13+
{
14+
"type": "promptString",
15+
"id": "functionapp-name",
16+
"description": "Azure Functions App Name"
17+
}
18+
]
2819
}

README.md

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ urlFragment: remote-mcp-functions-python
1616

1717
# Getting Started with Remote MCP Servers using Azure Functions (Python)
1818

19-
This is a quickstart template to easily build and deploy a custom remote MCP server to the cloud using Azure Functions with Python. You can clone/restore/run on your local machine with debugging, and `azd up` to have it in the cloud in a couple minutes. The MCP server is secured by design using keys and HTTPS, and allows more options for OAuth using built-in auth and/or [API Management](https://aka.ms/mcp-remote-apim-auth) as well as network isolation using VNET.
19+
This is a quickstart template to easily build and deploy a custom remote MCP server to the cloud using Azure Functions with Python. You can clone/restore/run on your local machine with debugging, and `azd up` to have it in the cloud in a couple minutes.
20+
21+
The MCP server is configured with [built-in authentication](https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) using Microsoft Entra as the identity provider.
22+
23+
You can also use [API Management](https://learn.microsoft.com/azure/api-management/secure-mcp-servers) to secure the server, as well as network isolation using VNET.
2024

2125
If you're looking for this sample in more languages check out the [.NET/C#](https://github.com/Azure-Samples/remote-mcp-functions-dotnet) and [Node.js/TypeScript](https://github.com/Azure-Samples/remote-mcp-functions-typescript) versions.
2226

@@ -157,6 +161,12 @@ In the root directory, create a new [azd](https://aka.ms/azd) environment. This
157161
azd env new <reource-group-name>
158162
```
159163
164+
Configure VS Code as an allowed client application to request access tokens from Microsoft Entra:
165+
166+
```shell
167+
azd env set PRE_AUTHORIZED_CLIENT_IDS aebc6443-996d-45c2-90f0-388ff96faa56
168+
```
169+
160170
Run this azd command to provision the function app, with any required Azure resources, and deploy your code:
161171
162172
```shell
@@ -169,30 +179,14 @@ You can opt-in to a VNet being used in the sample. To do so, do this before `azd
169179
azd env set VNET_ENABLED true
170180
```
171181
172-
Additionally, [API Management](https://aka.ms/mcp-remote-apim-auth) can be used for improved security and policies over your MCP Server, and [App Service built-in authentication](https://learn.microsoft.com/azure/app-service/overview-authentication-authorization) can be used to set up your favorite OAuth provider including Entra.
173-
174-
## Connect to your *remote* MCP server function app from a client
175-
176-
Your client will need a key in order to invoke the new hosted MCP endpoint, which will be of the form `https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp`. The hosted function requires a system key by default which can be obtained from the [portal](https://learn.microsoft.com/azure/azure-functions/function-keys-how-to?tabs=azure-portal) or the CLI (`az functionapp keys list --resource-group <resource_group> --name <function_app_name>`). Obtain the system key named `mcp_extension`.
177-
178-
### Connect to remote MCP server in MCP Inspector
179-
For MCP Inspector, you can include the key in the URL:
180-
```plaintext
181-
https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp?code=<your-mcp-extension-system-key>
182-
```
182+
Additionally, [API Management](https://aka.ms/mcp-remote-apim-auth) can be used for improved security and policies over your MCP Server.
183183
184184
### Connect to remote MCP server in VS Code - GitHub Copilot
185-
For GitHub Copilot within VS Code, you should instead set the key as the `x-functions-key` header in `mcp.json`, and you would just use `https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp` for the URL. The following example uses an input and will prompt you to provide the key when you start the server from VS Code. Note [mcp.json](.vscode/mcp.json) has already been included in this repo and will be picked up by VS Code. Click Start on the server to be prompted for values including `functionapp-name` (in your /.azure/*/.env file) and `functions-mcp-extension-system-key` which can be obtained from CLI command above or API Keys in the portal for the Function App.
185+
For GitHub Copilot within VS Code, you would just use `https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp` for the URL. Note [mcp.json](.vscode/mcp.json) has already been included in this repo and will be picked up by VS Code. Click Start on the server to be prompted for values including `functionapp-name` (in your /.azure/*/.env file). The server is configured with buit-in MCP auth, so you'll be asked to login as well.
186186

187187
```json
188188
{
189189
"inputs": [
190-
{
191-
"type": "promptString",
192-
"id": "functions-mcp-extension-system-key",
193-
"description": "Azure Functions MCP Extension System Key",
194-
"password": true
195-
},
196190
{
197191
"type": "promptString",
198192
"id": "functionapp-name",
@@ -202,10 +196,7 @@ For GitHub Copilot within VS Code, you should instead set the key as the `x-func
202196
"servers": {
203197
"remote-mcp-function": {
204198
"type": "http",
205-
"url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp",
206-
"headers": {
207-
"x-functions-key": "${input:functions-mcp-extension-system-key}"
208-
}
199+
"url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp"
209200
},
210201
"local-mcp-function": {
211202
"type": "http",
@@ -215,32 +206,6 @@ For GitHub Copilot within VS Code, you should instead set the key as the `x-func
215206
}
216207
```
217208

218-
For MCP Inspector, you can include the key in the URL: `https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp?code=<your-mcp-extension-system-key>`.
219-
220-
For GitHub Copilot within VS Code, you should instead set the key as the `x-functions-key` header in `mcp.json`, and you would just use `https://<funcappname>.azurewebsites.net/runtime/webhooks/mcp` for the URL. The following example uses an input and will prompt you to provide the key when you start the server from VS Code:
221-
222-
```json
223-
{
224-
"inputs": [
225-
{
226-
"type": "promptString",
227-
"id": "functions-mcp-extension-system-key",
228-
"description": "Azure Functions MCP Extension System Key",
229-
"password": true
230-
}
231-
],
232-
"servers": {
233-
"my-mcp-server": {
234-
"type": "http",
235-
"url": "<funcappname>.azurewebsites.net/runtime/webhooks/mcp",
236-
"headers": {
237-
"x-functions-key": "${input:functions-mcp-extension-system-key}"
238-
}
239-
}
240-
}
241-
}
242-
```
243-
244209
## Redeploy your code
245210

246211
You can run the `azd up` command as many times as you need to both provision your Azure resources and deploy code updates to your function app.
@@ -256,6 +221,15 @@ When you're done working with your function app and related resources, you can u
256221
azd down
257222
```
258223
224+
## Troubleshooting
225+
226+
| Error | Solution |
227+
|---|---|
228+
| `deployment was partially successful` / `KuduSpecializer` restart during `azd up` | This is a transient error. Run `azd deploy` to retry just the deployment step. |
229+
| Connection refused | Ensure Azurite is running (`docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite`) |
230+
| API version not supported by Azurite | Pull the latest Azurite image (`docker pull mcr.microsoft.com/azure-storage/azurite`) then restart Azurite and the app |
231+
232+
259233
## Helpful Azure Commands
260234
261235
Once your application is deployed, you can use these commands to manage and monitor your application:

infra/app/api.bicep

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@ param enableFile bool = false
2323
@allowed(['SystemAssigned', 'UserAssigned'])
2424
param identityType string = 'UserAssigned'
2525

26+
// Authorization parameters
27+
@description('The Entra ID application (client) ID for App Service Authentication')
28+
param authClientId string = ''
29+
30+
@description('The Entra ID identifier URI for App Service Authentication')
31+
param authIdentifierUri string = ''
32+
33+
@description('The OAuth2 scopes exposed by the application for App Service Authentication')
34+
param authExposedScopes array = []
35+
36+
@description('The Azure AD tenant ID for App Service Authentication')
37+
param authTenantId string = ''
38+
39+
@description('OAuth2 delegated permissions for App Service Authentication login flow')
40+
param delegatedPermissions array = ['User.Read']
41+
42+
@description('Client application IDs to pre-authorize for the default scope')
43+
param preAuthorizedClientIds array = []
44+
2645
var applicationInsightsIdentity = 'ClientId=${identityClientId};Authorization=AAD'
2746
var kind = 'functionapp,linux'
2847

@@ -43,14 +62,22 @@ var queueSettings = enableQueue ? { AzureWebJobsStorage__queueServiceUri: stg.pr
4362
var tableSettings = enableTable ? { AzureWebJobsStorage__tableServiceUri: stg.properties.primaryEndpoints.table } : {}
4463
var fileSettings = enableFile ? { AzureWebJobsStorage__fileServiceUri: stg.properties.primaryEndpoints.file } : {}
4564

65+
// Create auth-specific app settings when auth parameters are provided
66+
var authAppSettings = (!empty(authIdentifierUri) && !empty(identityClientId)) ? {
67+
WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES: '${authIdentifierUri}/user_impersonation'
68+
OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID: identityClientId
69+
WEBSITE_AUTH_AAD_ALLOWED_TENANTS: authTenantId
70+
} : {}
71+
4672
// Merge all app settings
4773
var allAppSettings = union(
4874
appSettings,
4975
blobSettings,
5076
queueSettings,
5177
tableSettings,
5278
fileSettings,
53-
baseAppSettings
79+
baseAppSettings,
80+
authAppSettings
5481
)
5582

5683
resource stg 'Microsoft.Storage/storageAccounts@2022-09-01' existing = {
@@ -62,7 +89,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing
6289
}
6390

6491
// Create a Flex Consumption Function App to host the API
65-
module api 'br/public:avm/res/web/site:0.15.1' = {
92+
module api 'br/public:avm/res/web/site:0.21.0' = {
6693
name: '${serviceName}-flex-consumption'
6794
params: {
6895
kind: kind
@@ -76,6 +103,87 @@ module api 'br/public:avm/res/web/site:0.15.1' = {
76103
'${identityId}'
77104
]
78105
}
106+
configs: !empty(authClientId) && !empty(authTenantId) ? [
107+
{
108+
name: 'appsettings'
109+
properties: allAppSettings
110+
}
111+
{
112+
name: 'authsettingsV2'
113+
properties: {
114+
globalValidation: {
115+
requireAuthentication: true
116+
unauthenticatedClientAction: 'Return401'
117+
redirectToProvider: 'azureactivedirectory'
118+
}
119+
httpSettings: {
120+
requireHttps: true
121+
routes: {
122+
apiPrefix: '/.auth'
123+
}
124+
forwardProxy: {
125+
convention: 'NoProxy'
126+
}
127+
}
128+
identityProviders: {
129+
azureActiveDirectory: {
130+
enabled: true
131+
registration: {
132+
openIdIssuer: '${environment().authentication.loginEndpoint}${authTenantId}/v2.0'
133+
clientId: authClientId
134+
clientSecretSettingName: 'OVERRIDE_USE_MI_FIC_ASSERTION_CLIENTID'
135+
}
136+
login: {
137+
loginParameters: [
138+
'scope=openid profile email ${join(delegatedPermissions, ' ')}'
139+
]
140+
}
141+
validation: {
142+
jwtClaimChecks: {}
143+
allowedAudiences: [
144+
authIdentifierUri
145+
]
146+
defaultAuthorizationPolicy: {
147+
allowedPrincipals: {}
148+
allowedApplications: union([authClientId], preAuthorizedClientIds)
149+
}
150+
}
151+
isAutoProvisioned: false
152+
}
153+
}
154+
login: {
155+
routes: {
156+
logoutEndpoint: '/.auth/logout'
157+
}
158+
tokenStore: {
159+
enabled: true
160+
tokenRefreshExtensionHours: 72
161+
fileSystem: {}
162+
azureBlobStorage: {}
163+
}
164+
preserveUrlFragmentsForLogins: false
165+
allowedExternalRedirectUrls: []
166+
cookieExpiration: {
167+
convention: 'FixedTime'
168+
timeToExpiration: '08:00:00'
169+
}
170+
nonce: {
171+
validateNonce: true
172+
nonceExpirationInterval: '00:05:00'
173+
}
174+
}
175+
platform: {
176+
enabled: true
177+
runtimeVersion: '~1'
178+
}
179+
}
180+
}
181+
] : [
182+
{
183+
name: 'appsettings'
184+
properties: allAppSettings
185+
}
186+
]
79187
functionAppConfig: {
80188
deployment: {
81189
storage: {
@@ -99,11 +207,16 @@ module api 'br/public:avm/res/web/site:0.15.1' = {
99207
siteConfig: {
100208
alwaysOn: false
101209
}
102-
virtualNetworkSubnetId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null
103-
appSettingsKeyValuePairs: allAppSettings
210+
virtualNetworkSubnetResourceId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null
104211
}
105212
}
106213

107214
output SERVICE_API_NAME string = api.outputs.name
215+
output SERVICE_MCP_DEFAULT_HOSTNAME string = api.outputs.defaultHostname
108216
// Ensure output is always string, handle potential null from module output if SystemAssigned is not used
109217
output SERVICE_API_IDENTITY_PRINCIPAL_ID string = identityType == 'SystemAssigned' ? api.outputs.?systemAssignedMIPrincipalId ?? '' : ''
218+
219+
// Authorization outputs
220+
var scopeValues = [for scope in authExposedScopes: scope.value]
221+
output AUTH_ENABLED bool = !empty(authClientId) && !empty(authTenantId)
222+
output CONFIGURED_SCOPES string = !empty(authExposedScopes) ? join(scopeValues, ','): ''

0 commit comments

Comments
 (0)