@softeria/ms-365-mcp-server 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/endpoints.json +7 -0
- package/dist/generated/client.js +100 -2
- package/docs/deployment.md +2 -0
- package/examples/azure-container-apps/README.md +188 -0
- package/examples/azure-container-apps/deploy.ps1 +170 -0
- package/examples/azure-container-apps/main.bicep +252 -0
- package/package.json +1 -1
- package/src/endpoints.json +7 -0
package/README.md
CHANGED
|
@@ -273,6 +273,8 @@ Then add connection with URL `http://localhost:3000/mcp` and ID `ms-365`.
|
|
|
273
273
|
|
|
274
274
|

|
|
275
275
|
|
|
276
|
+
> **Running in Docker behind a reverse proxy?** Set `--public-url https://your-domain.com` so the OAuth authorize URL handed to the user's browser is reachable from outside the container network. See [docs/deployment.md](docs/deployment.md) for the full guide.
|
|
277
|
+
|
|
276
278
|
### Local Development
|
|
277
279
|
|
|
278
280
|
For local development or testing:
|
|
@@ -449,7 +451,15 @@ npx @softeria/ms-365-mcp-server --list-presets # See all available presets
|
|
|
449
451
|
|
|
450
452
|
Available presets: `mail`, `calendar`, `files`, `personal`, `work`, `excel`, `contacts`, `tasks`, `onenote`, `search`, `users`, `all`
|
|
451
453
|
|
|
452
|
-
|
|
454
|
+
## Dynamic Tool Discovery
|
|
455
|
+
|
|
456
|
+
Instead of loading all 90+ tools upfront, use dynamic discovery so the LLM finds and loads tools only when it needs them:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
npx @softeria/ms-365-mcp-server --discovery
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Keeps the initial context small and cuts token usage, especially useful for long sessions or cost-sensitive setups (e.g. Open WebUI running against a paid API).
|
|
453
463
|
|
|
454
464
|
## CLI Options
|
|
455
465
|
|
|
@@ -481,7 +491,8 @@ When running as an MCP server, the following options can be used:
|
|
|
481
491
|
--preset <names> Use preset tool categories (comma-separated). See "Tool Presets" section above
|
|
482
492
|
--list-presets List all available presets and exit
|
|
483
493
|
--toon (experimental) Enable TOON output format for 30-60% token reduction
|
|
484
|
-
--discovery
|
|
494
|
+
--discovery Dynamic tool discovery: loads tools on demand to reduce initial token usage (see "Dynamic Tool Discovery" above)
|
|
495
|
+
--public-url <url> Public base URL for OAuth when behind a reverse proxy (see Open WebUI section and docs/deployment.md)
|
|
485
496
|
```
|
|
486
497
|
|
|
487
498
|
Environment variables:
|
package/dist/endpoints.json
CHANGED
|
@@ -1442,5 +1442,12 @@
|
|
|
1442
1442
|
"toolName": "update-place",
|
|
1443
1443
|
"workScopes": ["Place.ReadWrite.All"],
|
|
1444
1444
|
"llmTip": "Updates properties of a place (room, room list, building, floor, section, desk, workspace). Body can include: displayName, phone, capacity, building, floorNumber, floorLabel, tags, audioDeviceName, videoDeviceName, displayDeviceName, isWheelChairAccessible. Use get-room or get-room-list to verify current values before updating."
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
"pathPattern": "/me/insights/trending",
|
|
1448
|
+
"method": "get",
|
|
1449
|
+
"toolName": "list-trending-insights",
|
|
1450
|
+
"workScopes": ["Sites.Read.All"],
|
|
1451
|
+
"llmTip": "Lists documents trending around the current user. Each item has resourceVisualization (title, type, previewImageUrl, containerDisplayName), resourceReference (webUrl, id, type), and weight (relevance score). Use $filter=resourceVisualization/type eq 'PowerPoint' to filter by file type. Use $orderby=weight/value desc to sort by relevance."
|
|
1445
1452
|
}
|
|
1446
1453
|
]
|
package/dist/generated/client.js
CHANGED
|
@@ -2455,6 +2455,51 @@ const decline_calendar_event_Body = z.object({
|
|
|
2455
2455
|
SendResponse: z.boolean().nullable().default(false),
|
|
2456
2456
|
Comment: z.string().nullable()
|
|
2457
2457
|
}).partial().passthrough();
|
|
2458
|
+
const microsoft_graph_resourceReference = z.object({
|
|
2459
|
+
id: z.string().describe("The item's unique identifier.").nullish(),
|
|
2460
|
+
type: z.string().describe(
|
|
2461
|
+
"A string value that can be used to classify the item, such as 'microsoft.graph.driveItem'"
|
|
2462
|
+
).nullish(),
|
|
2463
|
+
webUrl: z.string().describe("A URL leading to the referenced item.").nullish()
|
|
2464
|
+
}).passthrough();
|
|
2465
|
+
const microsoft_graph_resourceVisualization = z.object({
|
|
2466
|
+
containerDisplayName: z.string().describe(
|
|
2467
|
+
"A string describing where the item is stored. For example, the name of a SharePoint site or the user name identifying the owner of the OneDrive storing the item."
|
|
2468
|
+
).nullish(),
|
|
2469
|
+
containerType: z.string().describe(
|
|
2470
|
+
"Can be used for filtering by the type of container in which the file is stored. Such as Site or OneDriveBusiness."
|
|
2471
|
+
).nullish(),
|
|
2472
|
+
containerWebUrl: z.string().describe("A path leading to the folder in which the item is stored.").nullish(),
|
|
2473
|
+
mediaType: z.string().describe(
|
|
2474
|
+
"The item's media type. Can be used for filtering for a specific type of file based on supported IANA Media Mime Types. Not all Media Mime Types are supported."
|
|
2475
|
+
).nullish(),
|
|
2476
|
+
previewImageUrl: z.string().describe("A URL leading to the preview image for the item.").nullish(),
|
|
2477
|
+
previewText: z.string().describe("A preview text for the item.").nullish(),
|
|
2478
|
+
title: z.string().describe("The item's title text.").nullish(),
|
|
2479
|
+
type: z.string().describe(
|
|
2480
|
+
"The item's media type. Can be used for filtering for a specific file based on a specific type. See the section Type property values for supported types."
|
|
2481
|
+
).nullish()
|
|
2482
|
+
}).passthrough();
|
|
2483
|
+
const microsoft_graph_entity = z.object({ id: z.string().describe("The unique identifier for an entity. Read-only.").optional() }).passthrough();
|
|
2484
|
+
const microsoft_graph_trending = z.object({
|
|
2485
|
+
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
2486
|
+
lastModifiedDateTime: z.string().regex(
|
|
2487
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
2488
|
+
).datetime({ offset: true }).describe(
|
|
2489
|
+
"The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z"
|
|
2490
|
+
).nullish(),
|
|
2491
|
+
resourceReference: microsoft_graph_resourceReference.optional(),
|
|
2492
|
+
resourceVisualization: microsoft_graph_resourceVisualization.optional(),
|
|
2493
|
+
weight: z.number().describe(
|
|
2494
|
+
"Value indicating how much the document is currently trending. The larger the number, the more the document is currently trending around the user (the more relevant it is). Returned documents are sorted by this value. [Simplified from 3 options]"
|
|
2495
|
+
).nullish(),
|
|
2496
|
+
resource: microsoft_graph_entity.optional()
|
|
2497
|
+
}).passthrough();
|
|
2498
|
+
const microsoft_graph_trendingCollectionResponse = z.object({
|
|
2499
|
+
"@odata.count": z.number().int().nullable(),
|
|
2500
|
+
"@odata.nextLink": z.string().nullable(),
|
|
2501
|
+
value: z.array(microsoft_graph_trending)
|
|
2502
|
+
}).partial().passthrough();
|
|
2458
2503
|
const microsoft_graph_giphyRatingType = z.enum(["strict", "moderate", "unknownFutureValue"]);
|
|
2459
2504
|
const microsoft_graph_teamFunSettings = z.object({
|
|
2460
2505
|
allowCustomMemes: z.boolean().describe("If set to true, enables users to include custom memes.").nullish(),
|
|
@@ -3918,7 +3963,6 @@ const microsoft_graph_searchAggregation = z.object({
|
|
|
3918
3963
|
buckets: z.array(microsoft_graph_searchBucket).optional(),
|
|
3919
3964
|
field: z.string().nullish()
|
|
3920
3965
|
}).passthrough();
|
|
3921
|
-
const microsoft_graph_entity = z.object({ id: z.string().describe("The unique identifier for an entity. Read-only.").optional() }).passthrough();
|
|
3922
3966
|
const microsoft_graph_searchHit = z.object({
|
|
3923
3967
|
contentSource: z.string().describe("The name of the content source that the externalItem is part of.").nullish(),
|
|
3924
3968
|
hitId: z.string().describe(
|
|
@@ -4504,6 +4548,11 @@ const schemas = {
|
|
|
4504
4548
|
microsoft_graph_driveCollectionResponse,
|
|
4505
4549
|
accept_calendar_event_Body,
|
|
4506
4550
|
decline_calendar_event_Body,
|
|
4551
|
+
microsoft_graph_resourceReference,
|
|
4552
|
+
microsoft_graph_resourceVisualization,
|
|
4553
|
+
microsoft_graph_entity,
|
|
4554
|
+
microsoft_graph_trending,
|
|
4555
|
+
microsoft_graph_trendingCollectionResponse,
|
|
4507
4556
|
microsoft_graph_giphyRatingType,
|
|
4508
4557
|
microsoft_graph_teamFunSettings,
|
|
4509
4558
|
microsoft_graph_teamGuestSettings,
|
|
@@ -4650,7 +4699,6 @@ const schemas = {
|
|
|
4650
4699
|
search_query_Body,
|
|
4651
4700
|
microsoft_graph_searchBucket,
|
|
4652
4701
|
microsoft_graph_searchAggregation,
|
|
4653
|
-
microsoft_graph_entity,
|
|
4654
4702
|
microsoft_graph_searchHit,
|
|
4655
4703
|
microsoft_graph_searchHitsContainer,
|
|
4656
4704
|
microsoft_graph_alteredQueryToken,
|
|
@@ -7347,6 +7395,56 @@ Based on this value, you can better adjust the parameters and call findMeetingTi
|
|
|
7347
7395
|
],
|
|
7348
7396
|
response: z.void()
|
|
7349
7397
|
},
|
|
7398
|
+
{
|
|
7399
|
+
method: "get",
|
|
7400
|
+
path: "/me/insights/trending",
|
|
7401
|
+
alias: "list-trending-insights",
|
|
7402
|
+
description: `Calculated insight that includes a list of documents trending around the user.`,
|
|
7403
|
+
requestFormat: "json",
|
|
7404
|
+
parameters: [
|
|
7405
|
+
{
|
|
7406
|
+
name: "$top",
|
|
7407
|
+
type: "Query",
|
|
7408
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
7409
|
+
},
|
|
7410
|
+
{
|
|
7411
|
+
name: "$skip",
|
|
7412
|
+
type: "Query",
|
|
7413
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
7414
|
+
},
|
|
7415
|
+
{
|
|
7416
|
+
name: "$search",
|
|
7417
|
+
type: "Query",
|
|
7418
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
7419
|
+
},
|
|
7420
|
+
{
|
|
7421
|
+
name: "$filter",
|
|
7422
|
+
type: "Query",
|
|
7423
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
7424
|
+
},
|
|
7425
|
+
{
|
|
7426
|
+
name: "$count",
|
|
7427
|
+
type: "Query",
|
|
7428
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
7429
|
+
},
|
|
7430
|
+
{
|
|
7431
|
+
name: "$orderby",
|
|
7432
|
+
type: "Query",
|
|
7433
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
7434
|
+
},
|
|
7435
|
+
{
|
|
7436
|
+
name: "$select",
|
|
7437
|
+
type: "Query",
|
|
7438
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
7439
|
+
},
|
|
7440
|
+
{
|
|
7441
|
+
name: "$expand",
|
|
7442
|
+
type: "Query",
|
|
7443
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
7444
|
+
}
|
|
7445
|
+
],
|
|
7446
|
+
response: z.void()
|
|
7447
|
+
},
|
|
7350
7448
|
{
|
|
7351
7449
|
method: "get",
|
|
7352
7450
|
path: "/me/joinedTeams",
|
package/docs/deployment.md
CHANGED
|
@@ -49,6 +49,8 @@ docker run -p 3000:3000 \
|
|
|
49
49
|
|
|
50
50
|
## Azure Container Apps
|
|
51
51
|
|
|
52
|
+
> **Turnkey Bicep example**: see [`examples/azure-container-apps/`](../examples/azure-container-apps/) for a complete Bicep template + PowerShell deploy script that provisions Log Analytics, UAMI, Key Vault (RBAC), Container Apps Environment and the Container App in one command.
|
|
53
|
+
|
|
52
54
|
1. **Push the image** to Azure Container Registry:
|
|
53
55
|
|
|
54
56
|
```bash
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Azure Container Apps deployment example
|
|
2
|
+
|
|
3
|
+
> **Community-contributed example.** This is a turnkey starter — adapt it to your tenant, naming conventions, and security policies before running it in production. See [`docs/deployment.md`](../../docs/deployment.md) for the baseline deployment guide.
|
|
4
|
+
|
|
5
|
+
Deploys `ms-365-mcp-server` to Azure Container Apps in **Streamable HTTP** mode, with secrets stored in Azure Key Vault and fetched at startup by a user-assigned managed identity (UAMI). No credentials are written to environment variables.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- `main.bicep` — full infrastructure: Log Analytics, UAMI, Key Vault (RBAC), Container Apps Environment, Container App
|
|
10
|
+
- `deploy.ps1` — PowerShell 7 orchestrator (login, Resource Group, deployment, outputs)
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
MCP client (Claude Desktop / claude.ai / ...)
|
|
16
|
+
│ HTTPS + OAuth 2.0 (Dynamic Client Registration)
|
|
17
|
+
▼
|
|
18
|
+
Container App (<baseName>-app)
|
|
19
|
+
- args: --http 3000 [--org-mode] [--read-only]
|
|
20
|
+
- scale 0-3 on concurrent HTTP requests
|
|
21
|
+
│ DefaultAzureCredential (UAMI)
|
|
22
|
+
▼
|
|
23
|
+
Key Vault (<baseName>kv<suffix>)
|
|
24
|
+
- ms365-mcp-client-id
|
|
25
|
+
- ms365-mcp-tenant-id
|
|
26
|
+
- ms365-mcp-cloud-type
|
|
27
|
+
- ms365-mcp-client-secret (optional — confidential client)
|
|
28
|
+
│
|
|
29
|
+
▼
|
|
30
|
+
Microsoft Graph API (per-user delegated token)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
### 1. Azure
|
|
36
|
+
|
|
37
|
+
- Subscription with **Contributor** + **User Access Administrator** roles on the target Resource Group (User Access Administrator is required for the UAMI → Key Vault RBAC assignment).
|
|
38
|
+
- [Azure CLI 2.60+](https://learn.microsoft.com/cli/azure/install-azure-cli).
|
|
39
|
+
- [PowerShell 7+](https://learn.microsoft.com/powershell/scripting/install/installing-powershell).
|
|
40
|
+
|
|
41
|
+
### 2. Entra ID app registration
|
|
42
|
+
|
|
43
|
+
Create an app registration in your tenant:
|
|
44
|
+
|
|
45
|
+
- **Supported account types**: single tenant
|
|
46
|
+
- **Redirect URIs** (initial): `http://localhost:3000/oauth/callback` — update after the first deploy once you know the Container App FQDN
|
|
47
|
+
- **API permissions**: delegated scopes matching your run mode. Print the exact list with:
|
|
48
|
+
```bash
|
|
49
|
+
npx @softeria/ms-365-mcp-server --list-permissions [--org-mode] [--read-only]
|
|
50
|
+
```
|
|
51
|
+
Grant admin consent in Entra ID for all `*.All` scopes.
|
|
52
|
+
- **Authentication → Allow public client flows**: Yes (if you will not use a client secret)
|
|
53
|
+
- **Certificates & secrets** (optional): create a client secret for confidential-client mode
|
|
54
|
+
|
|
55
|
+
Collect `tenantId`, `clientId`, and (optionally) `clientSecret`.
|
|
56
|
+
|
|
57
|
+
### 3. Container image
|
|
58
|
+
|
|
59
|
+
Default: `ghcr.io/softeria/ms-365-mcp-server:latest`.
|
|
60
|
+
Pin a version with `ghcr.io/softeria/ms-365-mcp-server:<tag>`, or push to your own Azure Container Registry and pass the reference via `-ContainerImage`.
|
|
61
|
+
|
|
62
|
+
## Initial deployment
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
cd examples/azure-container-apps
|
|
66
|
+
|
|
67
|
+
# Your own object ID, so you can rotate Key Vault secrets later
|
|
68
|
+
$myOid = az ad signed-in-user show --query id -o tsv
|
|
69
|
+
|
|
70
|
+
./deploy.ps1 `
|
|
71
|
+
-ResourceGroup 'rg-ms365mcp' `
|
|
72
|
+
-Location 'eastus' `
|
|
73
|
+
-BaseName 'ms365mcp' `
|
|
74
|
+
-TenantId '<TENANT_GUID>' `
|
|
75
|
+
-McpClientId '<APP_CLIENT_ID>' `
|
|
76
|
+
-KvAdminObjectIds @($myOid) `
|
|
77
|
+
-OrgMode $true
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The script prompts for the client secret (leave empty for a public client).
|
|
81
|
+
|
|
82
|
+
### Dry-run
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
./deploy.ps1 ... -WhatIf
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Post-deployment
|
|
89
|
+
|
|
90
|
+
### 1. Update the redirect URI in Entra ID
|
|
91
|
+
|
|
92
|
+
The first deployment produces an FQDN such as `ms365mcp-app.<suffix>.<region>.azurecontainerapps.io`. Add `https://<fqdn>/oauth/callback` to your app registration → Authentication → Redirect URIs.
|
|
93
|
+
|
|
94
|
+
### 2. Redeploy with `publicBaseUrl`
|
|
95
|
+
|
|
96
|
+
So the server advertises the correct public URL in OAuth metadata:
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
./deploy.ps1 ... -PublicBaseUrl 'https://<fqdn>'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. Smoke test
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# OAuth metadata (should return 200 with JSON)
|
|
106
|
+
curl https://<fqdn>/.well-known/oauth-authorization-server
|
|
107
|
+
|
|
108
|
+
# MCP endpoint (should return 401 without Authorization header)
|
|
109
|
+
curl -i https://<fqdn>/mcp
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. Connect an MCP client
|
|
113
|
+
|
|
114
|
+
See the [Client Configuration section](../../docs/deployment.md#client-configuration) in the main deployment guide.
|
|
115
|
+
|
|
116
|
+
## Operations
|
|
117
|
+
|
|
118
|
+
### Stream logs
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
az containerapp logs show -n <baseName>-app -g <rg> --follow
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Force a new revision
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
az containerapp update -n <baseName>-app -g <rg> --revision-suffix "manual$(date +%s)"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Update the image
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
az containerapp update -n <baseName>-app -g <rg> \
|
|
134
|
+
--image ghcr.io/softeria/ms-365-mcp-server:<new-tag>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Rotate the client secret
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# 1. Create a new secret in Entra ID (Certificates & secrets)
|
|
141
|
+
# 2. Update Key Vault
|
|
142
|
+
az keyvault secret set --vault-name <kv-name> --name ms365-mcp-client-secret --value "<new-secret>"
|
|
143
|
+
# 3. Restart to pick up the new value
|
|
144
|
+
az containerapp update -n <baseName>-app -g <rg> --revision-suffix "rotate$(date +%s)"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Pin minimum replicas (eliminate cold start)
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
az containerapp update -n <baseName>-app -g <rg> --min-replicas 1 --max-replicas 5
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Cost (order of magnitude)
|
|
154
|
+
|
|
155
|
+
| Resource | Config | Monthly (idle) |
|
|
156
|
+
| ------------- | ---------------------- | ------------------------------------------ |
|
|
157
|
+
| Container App | Consumption, scale 0-3 | ~$0 with scale-to-zero, ~$15-25 with min=1 |
|
|
158
|
+
| Log Analytics | 30-day retention | ~$2-5 depending on log volume |
|
|
159
|
+
| Key Vault | Standard, low ops | <$1 |
|
|
160
|
+
| UAMI | — | free |
|
|
161
|
+
|
|
162
|
+
Regional pricing varies — consult the [Azure pricing calculator](https://azure.microsoft.com/pricing/calculator/) for your region. Scale-to-zero introduces a ~2–5 s cold start on the first request after idle.
|
|
163
|
+
|
|
164
|
+
## Security notes
|
|
165
|
+
|
|
166
|
+
- **Secrets**: never in environment variables — always via Key Vault + UAMI
|
|
167
|
+
- **Ingress**: external HTTPS only. To restrict by IP, add `ipSecurityRestrictions` under `configuration.ingress`
|
|
168
|
+
- **RBAC**: UAMI receives only `Key Vault Secrets User` (read). Rotation is done by the admin principals listed in `-KvAdminObjectIds`
|
|
169
|
+
- **Purge protection** is enabled on Key Vault (90-day soft-delete) — accidental deletions are recoverable
|
|
170
|
+
- **CORS**: defaults to `http://localhost:3000`. Set `-CorsOrigin 'https://claude.ai'` (or your client URL) for hosted clients
|
|
171
|
+
|
|
172
|
+
## Cleanup
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
az group delete -n <rg> --yes --no-wait
|
|
176
|
+
# Key Vault remains in soft-delete for 30 days. To purge immediately:
|
|
177
|
+
az keyvault purge --name <kv-name>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Troubleshooting
|
|
181
|
+
|
|
182
|
+
| Symptom | Likely cause | Fix |
|
|
183
|
+
| ---------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
184
|
+
| Container restarts in a loop | UAMI can't read Key Vault secrets | Wait ~5 min for role propagation, then check `az role assignment list --assignee <uami-principal-id>` |
|
|
185
|
+
| 401 on `/mcp` even with a valid token | Wrong `tenantId`/`clientId` in Key Vault | `az keyvault secret show --vault-name <kv> --name ms365-mcp-client-id` |
|
|
186
|
+
| OAuth fails with `redirect_uri mismatch` | Redirect URI in Entra ID not updated | Add `https://<fqdn>/oauth/callback` in the app registration |
|
|
187
|
+
| Graph returns 403 | Missing scope / admin consent not granted | Re-run `--list-permissions` and grant admin consent |
|
|
188
|
+
| Cold start > 10 s | Heavy startup work in custom image | Verify the image does not run `npm install` in its ENTRYPOINT |
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#Requires -Version 7.0
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Deploys ms-365-mcp-server to Azure Container Apps using the colocated Bicep template.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Creates (or updates) the target Resource Group and runs the Bicep deployment.
|
|
8
|
+
The Entra ID client secret is prompted interactively and stored in Key Vault.
|
|
9
|
+
|
|
10
|
+
.EXAMPLE
|
|
11
|
+
./deploy.ps1 -ResourceGroup rg-ms365mcp -BaseName ms365mcp `
|
|
12
|
+
-TenantId "<tenant-guid>" -McpClientId "<app-guid>" `
|
|
13
|
+
-KvAdminObjectIds @("<your-object-id>")
|
|
14
|
+
|
|
15
|
+
.NOTES
|
|
16
|
+
Requirements:
|
|
17
|
+
- Azure CLI 2.60+ (az)
|
|
18
|
+
- PowerShell 7+
|
|
19
|
+
- Azure subscription with Contributor + User Access Administrator roles
|
|
20
|
+
(User Access Administrator is needed for the UAMI -> Key Vault RBAC assignment)
|
|
21
|
+
- Entra ID app registration created beforehand, with a redirect URI matching
|
|
22
|
+
the Container App FQDN (update it after the first deployment).
|
|
23
|
+
#>
|
|
24
|
+
[CmdletBinding()]
|
|
25
|
+
param(
|
|
26
|
+
[Parameter(Mandatory)][string]$ResourceGroup,
|
|
27
|
+
|
|
28
|
+
[Parameter(Mandatory)]
|
|
29
|
+
[ValidatePattern('^[a-z0-9]{3,20}$')]
|
|
30
|
+
[string]$BaseName,
|
|
31
|
+
|
|
32
|
+
[Parameter(Mandatory)][string]$TenantId,
|
|
33
|
+
[Parameter(Mandatory)][string]$McpClientId,
|
|
34
|
+
|
|
35
|
+
[string]$Location = 'eastus',
|
|
36
|
+
[string]$ContainerImage = 'ghcr.io/softeria/ms-365-mcp-server:latest',
|
|
37
|
+
[ValidateSet('global', 'gcc-high', 'dod', 'china')]
|
|
38
|
+
[string]$CloudType = 'global',
|
|
39
|
+
[string]$CorsOrigin = 'http://localhost:3000',
|
|
40
|
+
[string]$PublicBaseUrl = '',
|
|
41
|
+
[string[]]$KvAdminObjectIds = @(),
|
|
42
|
+
[bool]$OrgMode = $true,
|
|
43
|
+
[bool]$ReadOnly = $false,
|
|
44
|
+
[int]$MinReplicas = 0,
|
|
45
|
+
[int]$MaxReplicas = 3,
|
|
46
|
+
[switch]$SkipLogin,
|
|
47
|
+
[switch]$WhatIf
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
$ErrorActionPreference = 'Stop'
|
|
51
|
+
$bicepFile = Join-Path $PSScriptRoot 'main.bicep'
|
|
52
|
+
|
|
53
|
+
if (-not (Test-Path $bicepFile)) {
|
|
54
|
+
throw "Bicep file not found: $bicepFile"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# --- Prerequisites ---
|
|
58
|
+
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
|
|
59
|
+
throw 'Azure CLI not found. Install via: https://learn.microsoft.com/cli/azure/install-azure-cli'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Write-Host 'Checking/installing Bicep CLI...' -ForegroundColor DarkGray
|
|
63
|
+
az bicep install 2>$null | Out-Null
|
|
64
|
+
az bicep upgrade 2>$null | Out-Null
|
|
65
|
+
|
|
66
|
+
# --- Authentication ---
|
|
67
|
+
if (-not $SkipLogin) {
|
|
68
|
+
$acct = az account show --only-show-errors 2>$null | ConvertFrom-Json
|
|
69
|
+
if (-not $acct) {
|
|
70
|
+
Write-Host 'No active Azure session — launching az login...' -ForegroundColor Yellow
|
|
71
|
+
az login --tenant $TenantId --only-show-errors | Out-Null
|
|
72
|
+
$acct = az account show | ConvertFrom-Json
|
|
73
|
+
}
|
|
74
|
+
Write-Host "Active subscription : $($acct.name) ($($acct.id))" -ForegroundColor Green
|
|
75
|
+
Write-Host "Tenant : $($acct.tenantId)" -ForegroundColor Green
|
|
76
|
+
if ($acct.tenantId -ne $TenantId) {
|
|
77
|
+
Write-Warning "Active tenant ($($acct.tenantId)) does not match target tenant ($TenantId)."
|
|
78
|
+
$confirm = Read-Host 'Continue anyway? [y/N]'
|
|
79
|
+
if ($confirm -ne 'y') { throw 'Cancelled by user.' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# --- Client secret (interactive, SecureString) ---
|
|
84
|
+
$secretSecure = Read-Host 'Entra ID client secret (leave empty for public client)' -AsSecureString
|
|
85
|
+
$secretPlain = ''
|
|
86
|
+
if ($secretSecure.Length -gt 0) {
|
|
87
|
+
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secretSecure)
|
|
88
|
+
try {
|
|
89
|
+
$secretPlain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
|
|
90
|
+
} finally {
|
|
91
|
+
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# --- Resource Group ---
|
|
96
|
+
$rg = az group show -n $ResourceGroup --only-show-errors 2>$null | ConvertFrom-Json
|
|
97
|
+
if (-not $rg) {
|
|
98
|
+
Write-Host "Creating Resource Group '$ResourceGroup' in '$Location'..." -ForegroundColor Cyan
|
|
99
|
+
az group create -n $ResourceGroup -l $Location --tags project=ms-365-mcp-server managedBy=bicep -o none
|
|
100
|
+
} else {
|
|
101
|
+
Write-Host "Resource Group '$ResourceGroup' exists ($($rg.location))" -ForegroundColor Green
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# --- Deployment parameters ---
|
|
105
|
+
$deployName = "ms365mcp-$(Get-Date -Format 'yyyyMMddHHmmss')"
|
|
106
|
+
$params = @{
|
|
107
|
+
baseName = @{ value = $BaseName }
|
|
108
|
+
tenantId = @{ value = $TenantId }
|
|
109
|
+
mcpClientId = @{ value = $McpClientId }
|
|
110
|
+
mcpClientSecret = @{ value = $secretPlain }
|
|
111
|
+
cloudType = @{ value = $CloudType }
|
|
112
|
+
containerImage = @{ value = $ContainerImage }
|
|
113
|
+
corsOrigin = @{ value = $CorsOrigin }
|
|
114
|
+
publicBaseUrl = @{ value = $PublicBaseUrl }
|
|
115
|
+
orgMode = @{ value = $OrgMode }
|
|
116
|
+
readOnly = @{ value = $ReadOnly }
|
|
117
|
+
minReplicas = @{ value = $MinReplicas }
|
|
118
|
+
maxReplicas = @{ value = $MaxReplicas }
|
|
119
|
+
kvAdminObjectIds = @{ value = $KvAdminObjectIds }
|
|
120
|
+
}
|
|
121
|
+
$paramsFile = New-TemporaryFile
|
|
122
|
+
try {
|
|
123
|
+
@{
|
|
124
|
+
'$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#'
|
|
125
|
+
contentVersion = '1.0.0.0'
|
|
126
|
+
parameters = $params
|
|
127
|
+
} | ConvertTo-Json -Depth 10 | Set-Content -Path $paramsFile.FullName
|
|
128
|
+
|
|
129
|
+
# --- Validation (what-if) ---
|
|
130
|
+
if ($WhatIf) {
|
|
131
|
+
Write-Host '--- what-if ---' -ForegroundColor Magenta
|
|
132
|
+
az deployment group what-if `
|
|
133
|
+
-g $ResourceGroup `
|
|
134
|
+
-n $deployName `
|
|
135
|
+
-f $bicepFile `
|
|
136
|
+
-p "@$($paramsFile.FullName)"
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# --- Deployment ---
|
|
141
|
+
Write-Host "Starting deployment '$deployName'..." -ForegroundColor Cyan
|
|
142
|
+
$outputs = az deployment group create `
|
|
143
|
+
-g $ResourceGroup `
|
|
144
|
+
-n $deployName `
|
|
145
|
+
-f $bicepFile `
|
|
146
|
+
-p "@$($paramsFile.FullName)" `
|
|
147
|
+
--query properties.outputs `
|
|
148
|
+
-o json | ConvertFrom-Json
|
|
149
|
+
|
|
150
|
+
# --- Summary ---
|
|
151
|
+
Write-Host ''
|
|
152
|
+
Write-Host 'Deployment succeeded.' -ForegroundColor Green
|
|
153
|
+
Write-Host " Public URL : $($outputs.appUrl.value)" -ForegroundColor Yellow
|
|
154
|
+
Write-Host " Key Vault URI : $($outputs.keyVaultUri.value)"
|
|
155
|
+
Write-Host " Key Vault name : $($outputs.keyVaultName.value)"
|
|
156
|
+
Write-Host " Managed identity : $($outputs.uamiName.value)"
|
|
157
|
+
Write-Host " UAMI clientId : $($outputs.uamiClientId.value)"
|
|
158
|
+
Write-Host " Log Analytics : $($outputs.logAnalyticsName.value)"
|
|
159
|
+
Write-Host ''
|
|
160
|
+
Write-Host 'Next steps:' -ForegroundColor Cyan
|
|
161
|
+
Write-Host " 1. Add '$($outputs.appUrl.value)/oauth/callback' as a redirect URI in your Entra ID app."
|
|
162
|
+
Write-Host " 2. Re-run with -PublicBaseUrl '$($outputs.appUrl.value)' so OAuth metadata returns the public URL."
|
|
163
|
+
Write-Host " 3. Test : curl $($outputs.appUrl.value)/.well-known/oauth-authorization-server"
|
|
164
|
+
Write-Host " 4. Logs : az containerapp logs show -n '$BaseName-app' -g '$ResourceGroup' --follow"
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
Remove-Item $paramsFile.FullName -ErrorAction SilentlyContinue
|
|
168
|
+
$secretPlain = $null
|
|
169
|
+
[GC]::Collect()
|
|
170
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// ms-365-mcp-server — Azure Container Apps deployment (community example)
|
|
2
|
+
//
|
|
3
|
+
// Deploys a turnkey stack:
|
|
4
|
+
// - Log Analytics workspace
|
|
5
|
+
// - User-Assigned Managed Identity (UAMI)
|
|
6
|
+
// - Key Vault (RBAC) with MCP secrets
|
|
7
|
+
// - Container Apps Environment (Consumption)
|
|
8
|
+
// - Container App running the server in Streamable HTTP mode
|
|
9
|
+
//
|
|
10
|
+
// The container uses DefaultAzureCredential (via the UAMI) to read secrets
|
|
11
|
+
// from Key Vault at startup — credentials never appear in environment variables.
|
|
12
|
+
|
|
13
|
+
@description('Base name prefix for all resources (lowercase, 3-20 chars, e.g. ms365mcpprod)')
|
|
14
|
+
@minLength(3)
|
|
15
|
+
@maxLength(20)
|
|
16
|
+
param baseName string
|
|
17
|
+
|
|
18
|
+
@description('Azure region. Defaults to the resource group location.')
|
|
19
|
+
param location string = resourceGroup().location
|
|
20
|
+
|
|
21
|
+
@description('Container image reference (public image by default).')
|
|
22
|
+
param containerImage string = 'ghcr.io/softeria/ms-365-mcp-server:latest'
|
|
23
|
+
|
|
24
|
+
@description('Entra ID tenant ID (GUID).')
|
|
25
|
+
param tenantId string
|
|
26
|
+
|
|
27
|
+
@description('Entra ID app registration clientId used by the MCP server.')
|
|
28
|
+
param mcpClientId string
|
|
29
|
+
|
|
30
|
+
@secure()
|
|
31
|
+
@description('Client secret for the MCP app registration. Leave empty for a public client.')
|
|
32
|
+
param mcpClientSecret string = ''
|
|
33
|
+
|
|
34
|
+
@description('Microsoft cloud type: global, gcc-high, dod, china. Defaults to global.')
|
|
35
|
+
@allowed([
|
|
36
|
+
'global'
|
|
37
|
+
'gcc-high'
|
|
38
|
+
'dod'
|
|
39
|
+
'china'
|
|
40
|
+
])
|
|
41
|
+
param cloudType string = 'global'
|
|
42
|
+
|
|
43
|
+
@description('CORS origin for the MCP HTTP endpoint. Set to your client URL (e.g. https://claude.ai).')
|
|
44
|
+
param corsOrigin string = 'http://localhost:3000'
|
|
45
|
+
|
|
46
|
+
@description('Public base URL advertised in OAuth metadata. Derived from ingress FQDN after first deploy — leave empty initially, then redeploy with the assigned URL.')
|
|
47
|
+
param publicBaseUrl string = ''
|
|
48
|
+
|
|
49
|
+
@description('Object IDs of users/groups granted Key Vault Administrator role (for rotating secrets).')
|
|
50
|
+
param kvAdminObjectIds array = []
|
|
51
|
+
|
|
52
|
+
@description('Enable --org-mode (Teams, SharePoint, Groups, etc.). Defaults to true.')
|
|
53
|
+
param orgMode bool = true
|
|
54
|
+
|
|
55
|
+
@description('Enable --read-only flag (disables write operations).')
|
|
56
|
+
param readOnly bool = false
|
|
57
|
+
|
|
58
|
+
@description('Min replicas for autoscale. Set to 0 for scale-to-zero (cold start ~2-5s).')
|
|
59
|
+
param minReplicas int = 0
|
|
60
|
+
|
|
61
|
+
@description('Max replicas for autoscale.')
|
|
62
|
+
param maxReplicas int = 3
|
|
63
|
+
|
|
64
|
+
@description('Tags applied to all resources.')
|
|
65
|
+
param tags object = {
|
|
66
|
+
project: 'ms-365-mcp-server'
|
|
67
|
+
managedBy: 'bicep'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------- Derived ----------
|
|
71
|
+
var suffix = toLower(substring(uniqueString(resourceGroup().id), 0, 5))
|
|
72
|
+
var logName = '${baseName}-log-${suffix}'
|
|
73
|
+
var uamiName = '${baseName}-uami'
|
|
74
|
+
var kvName = take('${baseName}kv${suffix}', 24)
|
|
75
|
+
var caeName = '${baseName}-cae'
|
|
76
|
+
var appName = '${baseName}-app'
|
|
77
|
+
|
|
78
|
+
// Built-in role definition IDs
|
|
79
|
+
var roleKvSecretsUser = '4633458b-17de-408a-b874-0445c86b69e6'
|
|
80
|
+
var roleKvAdministrator = '00482a5a-887f-4fb3-b363-3b7fe8e74483'
|
|
81
|
+
|
|
82
|
+
// ---------- Log Analytics ----------
|
|
83
|
+
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
|
|
84
|
+
name: logName
|
|
85
|
+
location: location
|
|
86
|
+
tags: tags
|
|
87
|
+
properties: {
|
|
88
|
+
sku: { name: 'PerGB2018' }
|
|
89
|
+
retentionInDays: 30
|
|
90
|
+
features: { enableLogAccessUsingOnlyResourcePermissions: true }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------- User-Assigned Managed Identity ----------
|
|
95
|
+
resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = {
|
|
96
|
+
name: uamiName
|
|
97
|
+
location: location
|
|
98
|
+
tags: tags
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------- Key Vault (RBAC) ----------
|
|
102
|
+
resource kv 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
|
|
103
|
+
name: kvName
|
|
104
|
+
location: location
|
|
105
|
+
tags: tags
|
|
106
|
+
properties: {
|
|
107
|
+
sku: { family: 'A', name: 'standard' }
|
|
108
|
+
tenantId: tenantId
|
|
109
|
+
enableRbacAuthorization: true
|
|
110
|
+
enableSoftDelete: true
|
|
111
|
+
softDeleteRetentionInDays: 30
|
|
112
|
+
enablePurgeProtection: true
|
|
113
|
+
publicNetworkAccess: 'Enabled'
|
|
114
|
+
networkAcls: {
|
|
115
|
+
bypass: 'AzureServices'
|
|
116
|
+
defaultAction: 'Allow'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
resource secretClientId 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
|
|
122
|
+
parent: kv
|
|
123
|
+
name: 'ms365-mcp-client-id'
|
|
124
|
+
properties: { value: mcpClientId }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
resource secretTenantId 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
|
|
128
|
+
parent: kv
|
|
129
|
+
name: 'ms365-mcp-tenant-id'
|
|
130
|
+
properties: { value: tenantId }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
resource secretCloudType 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
|
|
134
|
+
parent: kv
|
|
135
|
+
name: 'ms365-mcp-cloud-type'
|
|
136
|
+
properties: { value: cloudType }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
resource secretClientSecret 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = if (!empty(mcpClientSecret)) {
|
|
140
|
+
parent: kv
|
|
141
|
+
name: 'ms365-mcp-client-secret'
|
|
142
|
+
properties: { value: mcpClientSecret }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Grant UAMI the Key Vault Secrets User role on the vault
|
|
146
|
+
resource roleUami 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
|
|
147
|
+
scope: kv
|
|
148
|
+
name: guid(kv.id, uami.id, roleKvSecretsUser)
|
|
149
|
+
properties: {
|
|
150
|
+
principalId: uami.properties.principalId
|
|
151
|
+
principalType: 'ServicePrincipal'
|
|
152
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleKvSecretsUser)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Grant human admins the Key Vault Administrator role (to rotate secrets)
|
|
157
|
+
resource roleAdmins 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for oid in kvAdminObjectIds: {
|
|
158
|
+
scope: kv
|
|
159
|
+
name: guid(kv.id, oid, roleKvAdministrator)
|
|
160
|
+
properties: {
|
|
161
|
+
principalId: oid
|
|
162
|
+
principalType: 'User'
|
|
163
|
+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleKvAdministrator)
|
|
164
|
+
}
|
|
165
|
+
}]
|
|
166
|
+
|
|
167
|
+
// ---------- Container Apps Environment ----------
|
|
168
|
+
resource cae 'Microsoft.App/managedEnvironments@2024-03-01' = {
|
|
169
|
+
name: caeName
|
|
170
|
+
location: location
|
|
171
|
+
tags: tags
|
|
172
|
+
properties: {
|
|
173
|
+
appLogsConfiguration: {
|
|
174
|
+
destination: 'log-analytics'
|
|
175
|
+
logAnalyticsConfiguration: {
|
|
176
|
+
customerId: logAnalytics.properties.customerId
|
|
177
|
+
sharedKey: logAnalytics.listKeys().primarySharedKey
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
workloadProfiles: [
|
|
181
|
+
{ name: 'Consumption', workloadProfileType: 'Consumption' }
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------- Container App ----------
|
|
187
|
+
var containerArgs = concat(['--http', '3000'], orgMode ? ['--org-mode'] : [], readOnly ? ['--read-only'] : [])
|
|
188
|
+
|
|
189
|
+
resource app 'Microsoft.App/containerApps@2024-03-01' = {
|
|
190
|
+
name: appName
|
|
191
|
+
location: location
|
|
192
|
+
tags: tags
|
|
193
|
+
identity: {
|
|
194
|
+
type: 'UserAssigned'
|
|
195
|
+
userAssignedIdentities: {
|
|
196
|
+
'${uami.id}': {}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
properties: {
|
|
200
|
+
managedEnvironmentId: cae.id
|
|
201
|
+
configuration: {
|
|
202
|
+
activeRevisionsMode: 'Single'
|
|
203
|
+
ingress: {
|
|
204
|
+
external: true
|
|
205
|
+
targetPort: 3000
|
|
206
|
+
transport: 'auto'
|
|
207
|
+
allowInsecure: false
|
|
208
|
+
traffic: [ { latestRevision: true, weight: 100 } ]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
template: {
|
|
212
|
+
containers: [
|
|
213
|
+
{
|
|
214
|
+
name: 'mcp'
|
|
215
|
+
image: containerImage
|
|
216
|
+
args: containerArgs
|
|
217
|
+
resources: {
|
|
218
|
+
cpu: json('0.5')
|
|
219
|
+
memory: '1.0Gi'
|
|
220
|
+
}
|
|
221
|
+
env: [
|
|
222
|
+
{ name: 'MS365_MCP_KEYVAULT_URL', value: kv.properties.vaultUri }
|
|
223
|
+
{ name: 'MS365_MCP_CORS_ORIGIN', value: corsOrigin }
|
|
224
|
+
{ name: 'MS365_MCP_BASE_URL', value: empty(publicBaseUrl) ? '' : publicBaseUrl }
|
|
225
|
+
{ name: 'AZURE_CLIENT_ID', value: uami.properties.clientId }
|
|
226
|
+
{ name: 'NODE_ENV', value: 'production' }
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
scale: {
|
|
231
|
+
minReplicas: minReplicas
|
|
232
|
+
maxReplicas: maxReplicas
|
|
233
|
+
rules: [
|
|
234
|
+
{
|
|
235
|
+
name: 'http-scale'
|
|
236
|
+
http: { metadata: { concurrentRequests: '10' } }
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
dependsOn: [ roleUami, secretClientId, secretTenantId, secretCloudType ]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------- Outputs ----------
|
|
246
|
+
output appFqdn string = app.properties.configuration.ingress.fqdn
|
|
247
|
+
output appUrl string = 'https://${app.properties.configuration.ingress.fqdn}'
|
|
248
|
+
output keyVaultUri string = kv.properties.vaultUri
|
|
249
|
+
output keyVaultName string = kv.name
|
|
250
|
+
output uamiName string = uami.name
|
|
251
|
+
output uamiClientId string = uami.properties.clientId
|
|
252
|
+
output logAnalyticsName string = logAnalytics.name
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.0",
|
|
4
4
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/endpoints.json
CHANGED
|
@@ -1442,5 +1442,12 @@
|
|
|
1442
1442
|
"toolName": "update-place",
|
|
1443
1443
|
"workScopes": ["Place.ReadWrite.All"],
|
|
1444
1444
|
"llmTip": "Updates properties of a place (room, room list, building, floor, section, desk, workspace). Body can include: displayName, phone, capacity, building, floorNumber, floorLabel, tags, audioDeviceName, videoDeviceName, displayDeviceName, isWheelChairAccessible. Use get-room or get-room-list to verify current values before updating."
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
"pathPattern": "/me/insights/trending",
|
|
1448
|
+
"method": "get",
|
|
1449
|
+
"toolName": "list-trending-insights",
|
|
1450
|
+
"workScopes": ["Sites.Read.All"],
|
|
1451
|
+
"llmTip": "Lists documents trending around the current user. Each item has resourceVisualization (title, type, previewImageUrl, containerDisplayName), resourceReference (webUrl, id, type), and weight (relevance score). Use $filter=resourceVisualization/type eq 'PowerPoint' to filter by file type. Use $orderby=weight/value desc to sort by relevance."
|
|
1445
1452
|
}
|
|
1446
1453
|
]
|