Skip to content

Commit 85314a4

Browse files
adileiadileiclaude
authored
Add dynamic MCP routing sample
Demonstrates a Power Platform connector that routes MCP Streamable HTTP traffic to one of several MCP server instances selected via dropdown. Includes: - Multi-instance MCP server with path-based routing - Catalog REST server for instance discovery - Connector with x-ms-agentic-protocol and script.csx URL rewriter - One-step deploy scripts for bash and PowerShell Co-authored-by: adilei <adileibowiz@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 108986c commit 85314a4

File tree

13 files changed

+1111
-0
lines changed

13 files changed

+1111
-0
lines changed

extensibility/mcp/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ MCP servers that provide tools and resources to Copilot Studio agents.
1515
|--------|-------------|
1616
| [pass-resources-as-inputs/](./pass-resources-as-inputs/) | Pass MCP resources as agent inputs |
1717
| [search-species-resources-typescript/](./search-species-resources-typescript/) | Species search MCP server in TypeScript |
18+
| [dynamic-mcp-routing-typescript/](./dynamic-mcp-routing-typescript/) | Dynamic routing to multiple MCP server instances via a Power Platform connector |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
build/
3+
package-lock.json
4+
connector/settings.json
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
---
2+
title: Dynamic MCP Routing
3+
parent: MCP
4+
grand_parent: Extensibility
5+
nav_order: 3
6+
---
7+
8+
# Dynamic MCP Routing
9+
10+
A Power Platform connector that routes MCP Streamable HTTP traffic to one of several MCP server instances, selected at configuration time via a dropdown. Demonstrates how to use a catalog service, `x-ms-dynamic-values`, `x-ms-agentic-protocol`, and a `script.csx` URL rewriter to give a single connector access to multiple independent MCP servers.
11+
12+
## Architecture
13+
14+
```
15+
┌─────────────────────┐
16+
│ Copilot Studio │
17+
│ (MCP client) │
18+
└──────────┬───────────┘
19+
20+
┌────────────┴────────────┐
21+
│ Power Platform Connector│
22+
│ x-ms-agentic-protocol │
23+
│ mcp-streamable-1.0 │
24+
└────────────┬────────────┘
25+
26+
┌─────────────────┼─────────────────┐
27+
│ script.csx rewrites URL based on │
28+
│ selected instance from dropdown │
29+
└─────────────────┬─────────────────┘
30+
31+
┌──────────────────────┼──────────────────────┐
32+
▼ ▼ ▼
33+
/instances/contoso/mcp /instances/fabrikam/mcp /instances/northwind/mcp
34+
│ │ │
35+
└──────────────────────┴──────────────────────┘
36+
MCP Server
37+
(single process, path-based routing)
38+
```
39+
40+
**Three components:**
41+
42+
1. **Catalog server** (`src/catalog/`) — REST endpoint returning a list of MCP server instances with their endpoint URLs. The connector's `ListInstances` operation hits this to populate the instance dropdown.
43+
44+
2. **MCP server** (`src/mcp-server/`) — Single Express server hosting multiple independent MCP servers at `/instances/:id/mcp`. Each instance advertises its own tools (`list_projects`, `get_project_details`) with instance-specific data. Stateless: a fresh `Server` + `StreamableHTTPServerTransport` is created per request.
45+
46+
3. **Power Platform connector** (`connector/`) — Swagger definition with two operations:
47+
- `ListInstances` (internal, for dropdown) — calls the catalog
48+
- `InvokeMCP` — annotated with `x-ms-agentic-protocol: mcp-streamable-1.0`, with an `instanceUrl` parameter populated via `x-ms-dynamic-values`
49+
- `script.csx` rewrites the `InvokeMCP` request URL from the catalog host to the selected instance's MCP endpoint
50+
51+
## How It Works
52+
53+
### 1. Instance Discovery
54+
55+
The connector's `InvokeMCP` parameter uses `x-ms-dynamic-values` to call `ListInstances`, which returns instances with their `mcpUrl`:
56+
57+
```json
58+
[
59+
{ "id": "contoso", "name": "Contoso", "mcpUrl": "https://host/instances/contoso/mcp" },
60+
{ "id": "fabrikam", "name": "Fabrikam", "mcpUrl": "https://host/instances/fabrikam/mcp" }
61+
]
62+
```
63+
64+
The agent builder picks an instance from the dropdown when adding the connector action.
65+
66+
### 2. URL Rewriting
67+
68+
The swagger `host` points at the catalog server. When Copilot Studio calls `InvokeMCP`, `script.csx` intercepts the request, reads the `instanceUrl` query parameter, and rewrites the full URL (scheme, host, port, path) to the selected instance's MCP endpoint:
69+
70+
```csharp
71+
var targetUri = new Uri(instanceUrl);
72+
var builder = new UriBuilder(Context.Request.RequestUri)
73+
{
74+
Scheme = targetUri.Scheme,
75+
Host = targetUri.Host,
76+
Port = targetUri.Port,
77+
Path = targetUri.AbsolutePath
78+
};
79+
```
80+
81+
### 3. MCP Protocol
82+
83+
Each instance endpoint is a fully independent MCP server. Copilot Studio handles the MCP protocol (`initialize`, `tools/list`, `tools/call`) natively. The mock data includes three fictional organizations with project portfolio data.
84+
85+
## Sample Structure
86+
87+
```
88+
dynamic-mcp-routing-typescript/
89+
├── src/
90+
│ ├── catalog/
91+
│ │ └── index.ts # Catalog REST server
92+
│ └── mcp-server/
93+
│ ├── index.ts # Multi-instance MCP server
94+
│ └── data.ts # Mock instances, projects, details
95+
├── connector/
96+
│ ├── apiDefinition.swagger.json # Swagger with x-ms-agentic-protocol
97+
│ ├── apiProperties.json # No connection parameters
98+
│ └── script.csx # URL rewriter for dynamic routing
99+
├── scripts/
100+
│ ├── deploy.sh # One-step deploy for macOS / Linux
101+
│ └── deploy.ps1 # One-step deploy for Windows
102+
├── package.json
103+
├── tsconfig.json
104+
└── README.md
105+
```
106+
107+
## Quick Start
108+
109+
### Prerequisites
110+
111+
- [Node.js 18+](https://nodejs.org/)
112+
- [Dev Tunnels CLI](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started)
113+
- [paconn CLI](https://learn.microsoft.com/connectors/custom-connectors/paconn-cli) (`pip install paconn`)
114+
- A [Power Platform environment](https://admin.powerplatform.microsoft.com/) with Copilot Studio access
115+
116+
### 1. Install and Build
117+
118+
```bash
119+
npm install
120+
npm run build
121+
```
122+
123+
### 2. Start Servers
124+
125+
In two terminals:
126+
127+
```bash
128+
# Terminal 1 — Catalog server (port 3000)
129+
npm run start:catalog
130+
131+
# Terminal 2 — MCP server (port 3001)
132+
npm run start:mcp
133+
```
134+
135+
### 3. Create Dev Tunnels
136+
137+
**Using the CLI:**
138+
139+
```bash
140+
devtunnel host -p 3000 -p 3001 --allow-anonymous
141+
```
142+
143+
This outputs two URLs like:
144+
145+
```
146+
https://abc123-3000.euw.devtunnels.ms (catalog)
147+
https://abc123-3001.euw.devtunnels.ms (MCP)
148+
```
149+
150+
Restart the catalog server with the MCP tunnel URL:
151+
152+
```bash
153+
MCP_SERVER_BASE=https://abc123-3001.euw.devtunnels.ms npm run start:catalog
154+
```
155+
156+
### 4. Deploy the Connector
157+
158+
Update `connector/apiDefinition.swagger.json` — set `host` to your catalog tunnel hostname (e.g. `abc123-3000.euw.devtunnels.ms`), then:
159+
160+
```bash
161+
python3 -m paconn login
162+
python3 -m paconn create \
163+
-e YOUR_ENVIRONMENT_ID \
164+
-d connector/apiDefinition.swagger.json \
165+
-p connector/apiProperties.json \
166+
-x connector/script.csx
167+
```
168+
169+
Or use the one-step deploy script that handles login, servers, tunnel, and connector deployment:
170+
171+
**Bash (macOS / Linux):**
172+
173+
```bash
174+
./scripts/deploy.sh YOUR_ENVIRONMENT_ID [TENANT_ID]
175+
```
176+
177+
**PowerShell (Windows):**
178+
179+
```powershell
180+
.\scripts\deploy.ps1 -EnvironmentId YOUR_ENVIRONMENT_ID [-TenantId TENANT_ID]
181+
```
182+
183+
### 5. Configure Copilot Studio
184+
185+
1. Open your agent in [Copilot Studio](https://copilotstudio.microsoft.com/)
186+
2. Go to **Actions** > **Add an action** > search for "Dynamic MCP Connector"
187+
3. Select an instance from the dropdown
188+
4. The agent now has access to the MCP tools for that instance
189+
190+
## Example Queries
191+
192+
Once the connector is added to an agent:
193+
194+
- "What projects are available?" — calls `list_projects`
195+
- "Show me the details for the ERP rollout" — calls `get_project_details` with `projectId: "erp-rollout"`
196+
- "What are the risks on the supply chain project?" — calls `get_project_details` for Fabrikam
197+
198+
## Development
199+
200+
**Add a new instance:** Add an entry to the `instances` array in `src/mcp-server/data.ts` and `src/catalog/index.ts`, along with its projects and details.
201+
202+
**Add a new tool:** Register additional tools in the `createServer` function in `src/mcp-server/index.ts` using `ListToolsRequestSchema` and `CallToolRequestSchema` handlers.
203+
204+
**Remove dynamic routing:** If you only need a single MCP server, simplify by removing the catalog server and `script.csx`, and point the swagger `host` directly at the MCP server.
205+
206+
## Resources
207+
208+
- [Model Context Protocol](https://modelcontextprotocol.io/)
209+
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
210+
- [MCP in Copilot Studio](https://learn.microsoft.com/microsoft-copilot-studio/mcp-overview)
211+
- [Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/)
212+
- [Custom Connector CLI (paconn)](https://learn.microsoft.com/connectors/custom-connectors/paconn-cli)
213+
- [`x-ms-agentic-protocol`](https://learn.microsoft.com/connectors/custom-connectors/mcp-overview)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"title": "Dynamic MCP Connector",
5+
"description": "Select an MCP server instance from the catalog, then interact via MCP Streamable HTTP.",
6+
"version": "1.0.0"
7+
},
8+
"host": "CATALOG_HOST_PLACEHOLDER",
9+
"basePath": "/",
10+
"schemes": ["https"],
11+
"consumes": ["application/json"],
12+
"produces": ["application/json"],
13+
"paths": {
14+
"/instances": {
15+
"get": {
16+
"operationId": "ListInstances",
17+
"summary": "List Instances",
18+
"description": "Returns the MCP server instances available in the catalog.",
19+
"x-ms-visibility": "internal",
20+
"parameters": [],
21+
"responses": {
22+
"200": {
23+
"description": "OK",
24+
"schema": {
25+
"type": "array",
26+
"items": {
27+
"$ref": "#/definitions/Instance"
28+
}
29+
}
30+
}
31+
}
32+
}
33+
},
34+
"/mcp": {
35+
"post": {
36+
"operationId": "InvokeMCP",
37+
"summary": "Invoke MCP Server",
38+
"description": "Interact with the selected MCP server instance via Streamable HTTP.",
39+
"x-ms-agentic-protocol": "mcp-streamable-1.0",
40+
"parameters": [
41+
{
42+
"name": "instanceUrl",
43+
"in": "query",
44+
"required": true,
45+
"type": "string",
46+
"x-ms-summary": "Instance",
47+
"description": "Select an MCP server instance.",
48+
"x-ms-dynamic-values": {
49+
"operationId": "ListInstances",
50+
"value-path": "mcpUrl",
51+
"value-title": "name"
52+
}
53+
}
54+
],
55+
"responses": {
56+
"200": {
57+
"description": "Success"
58+
}
59+
}
60+
}
61+
}
62+
},
63+
"definitions": {
64+
"Instance": {
65+
"type": "object",
66+
"properties": {
67+
"id": {
68+
"type": "string",
69+
"description": "Instance identifier",
70+
"x-ms-summary": "Instance ID"
71+
},
72+
"name": {
73+
"type": "string",
74+
"description": "Display name of the instance",
75+
"x-ms-summary": "Name"
76+
},
77+
"description": {
78+
"type": "string",
79+
"description": "Description of the instance",
80+
"x-ms-summary": "Description"
81+
},
82+
"mcpUrl": {
83+
"type": "string",
84+
"description": "MCP endpoint URL for this instance",
85+
"x-ms-summary": "MCP URL"
86+
}
87+
}
88+
}
89+
}
90+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"properties": {
3+
"connectionParameters": {}
4+
}
5+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Net;
2+
using System.Web;
3+
4+
public class Script : ScriptBase
5+
{
6+
public override async Task<HttpResponseMessage> ExecuteAsync()
7+
{
8+
// Only rewrite for InvokeMCP — ListInstances goes to the catalog host unchanged.
9+
if (Context.OperationId == "InvokeMCP")
10+
{
11+
// instanceUrl contains the full MCP endpoint URL for the selected instance,
12+
// populated from the ListInstances dropdown (value-path: "mcpUrl").
13+
var query = HttpUtility.ParseQueryString(Context.Request.RequestUri.Query);
14+
var instanceUrl = query["instanceUrl"];
15+
16+
if (!string.IsNullOrEmpty(instanceUrl))
17+
{
18+
var targetUri = new Uri(instanceUrl);
19+
20+
// Rewrite the entire request URL to the instance's MCP endpoint
21+
var builder = new UriBuilder(Context.Request.RequestUri)
22+
{
23+
Scheme = targetUri.Scheme,
24+
Host = targetUri.Host,
25+
Port = targetUri.Port,
26+
Path = targetUri.AbsolutePath
27+
};
28+
29+
// Remove instanceUrl from query string — the MCP server doesn't need it
30+
query.Remove("instanceUrl");
31+
builder.Query = query.ToString();
32+
33+
Context.Request.RequestUri = builder.Uri;
34+
}
35+
}
36+
37+
return await this.Context.SendAsync(this.Context.Request, this.CancellationToken);
38+
}
39+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "dynamic-mcp-routing",
3+
"version": "1.0.0",
4+
"description": "Dynamic MCP routing — a catalog server and multi-instance MCP server with a Power Platform connector that routes to the selected instance",
5+
"type": "module",
6+
"main": "build/mcp-server/index.js",
7+
"scripts": {
8+
"build": "tsc",
9+
"start:catalog": "node build/catalog/index.js",
10+
"start:mcp": "node build/mcp-server/index.js",
11+
"start": "npm run start:mcp",
12+
"dev:catalog": "npx tsx src/catalog/index.ts",
13+
"dev:mcp": "npx tsx src/mcp-server/index.ts",
14+
"deploy": "bash scripts/deploy.sh"
15+
},
16+
"dependencies": {
17+
"@modelcontextprotocol/sdk": "^1.20.2",
18+
"express": "^4.18.2",
19+
"zod": "^3.22.4",
20+
"zod-to-json-schema": "^3.24.6"
21+
},
22+
"devDependencies": {
23+
"@types/express": "^4.17.21",
24+
"@types/node": "^20.0.0",
25+
"tsx": "^4.19.0",
26+
"typescript": "^5.3.0"
27+
}
28+
}

0 commit comments

Comments
 (0)