From 92b1abbd9d00df6ebebf12680be415dd5a175d58 Mon Sep 17 00:00:00 2001 From: Andrew Chung <47454279+andrewkcchung@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:42:27 -0400 Subject: [PATCH 1/2] Allow workload identity for cluster-local MCP services Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/proposals-impl/default-mcp-servers.md | 5 ++++ packages/sdk/src/mcp-workload-identity.ts | 18 ++++++++---- .../test/unit/mcp-workload-identity.test.mjs | 29 ++++++++++++++++--- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/docs/proposals-impl/default-mcp-servers.md b/docs/proposals-impl/default-mcp-servers.md index 3762bacb..92b26e3d 100644 --- a/docs/proposals-impl/default-mcp-servers.md +++ b/docs/proposals-impl/default-mcp-servers.md @@ -71,6 +71,11 @@ So a fleet targeting organization `` would carry: to its Entra scope through `MCP_WORKLOAD_IDENTITY_SCOPES`. The worker acquires the token and attaches it only when the effective server URL matches the deployment-owned URL. +- **Authenticated transport is constrained.** Workload-identity headers require + HTTPS except for deployment-owned Kubernetes Services addressed by the exact + cluster-local form + `http://..svc.cluster.local[/path]`. Arbitrary HTTP hosts, + IP addresses, embedded credentials, and URL fragments remain rejected. - **Optional toolset narrowing** — the ADO MCP exposes a large surface; it can be trimmed with an `X-MCP-Toolsets` request header (e.g. `core,work-items,repositories,search`) to cut tool count / context. diff --git a/packages/sdk/src/mcp-workload-identity.ts b/packages/sdk/src/mcp-workload-identity.ts index ba43c8d6..aa8a203e 100644 --- a/packages/sdk/src/mcp-workload-identity.ts +++ b/packages/sdk/src/mcp-workload-identity.ts @@ -19,6 +19,8 @@ export type McpServerHeadersProvider = () => Promise< >; const SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; +const KUBERNETES_SERVICE_FQDN_PATTERN = + /^(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.){2}svc\.cluster\.local$/; const BEARER_SCHEME = ["Bear", "er"].join(""); /** @@ -81,7 +83,7 @@ export function parseMcpWorkloadIdentityScopes( return bindings; } -function deploymentHttpsUrl( +function deploymentMcpUrl( serverName: string, config: McpWorkloadIdentityServerConfig | undefined, ): string { @@ -103,14 +105,17 @@ function deploymentHttpsUrl( `MCP workload identity server '${serverName}' has an invalid URL.`, ); } + const isHttps = url.protocol === "https:"; + const isClusterLocalHttp = url.protocol === "http:" + && KUBERNETES_SERVICE_FQDN_PATTERN.test(url.hostname); if ( - url.protocol !== "https:" + (!isHttps && !isClusterLocalHttp) || url.username || url.password || url.hash ) { throw new Error( - `MCP workload identity server '${serverName}' must use an HTTPS URL without embedded credentials or a fragment.`, + `MCP workload identity server '${serverName}' must use HTTPS or an in-cluster HTTP service URL in the form '..svc.cluster.local', without embedded credentials or a fragment.`, ); } return config.url; @@ -118,8 +123,9 @@ function deploymentHttpsUrl( /** * Creates fresh worker-owned authorization headers for explicitly mapped, - * deployment-owned HTTPS MCP servers. The SessionManager additionally binds - * each returned header to `expectedUrl` before injecting it. + * deployment-owned MCP servers. HTTPS is required except for exact Kubernetes + * Service FQDNs on the cluster-local network. The SessionManager additionally + * binds each returned header to `expectedUrl` before injecting it. */ export function createMcpWorkloadIdentityHeadersProvider(options: { scopeBindings: unknown; @@ -141,7 +147,7 @@ export function createMcpWorkloadIdentityHeadersProvider(options: { } return { ...binding, - expectedUrl: deploymentHttpsUrl( + expectedUrl: deploymentMcpUrl( binding.serverName, options.deploymentMcpServers[binding.serverName], ), diff --git a/packages/sdk/test/unit/mcp-workload-identity.test.mjs b/packages/sdk/test/unit/mcp-workload-identity.test.mjs index e801bc0c..99aac53c 100644 --- a/packages/sdk/test/unit/mcp-workload-identity.test.mjs +++ b/packages/sdk/test/unit/mcp-workload-identity.test.mjs @@ -76,7 +76,10 @@ test("MCP workload identity requests one token per scope and binds deployment UR deploymentMcpServers: { ado: { type: "http", url: "https://mcp.dev.azure.com/org" }, boards: { type: "sse", url: "https://mcp.dev.azure.com/org/boards" }, - kusto: { url: "https://kusto.example.test/mcp" }, + kusto: { + type: "http", + url: "http://kusto-mcp.pilotswarm.svc.cluster.local/mcp", + }, }, credential: { async getToken(scope) { @@ -108,7 +111,8 @@ test("MCP workload identity requests one token per scope and binds deployment UR }, }, kusto: { - expectedUrl: "https://kusto.example.test/mcp", + expectedUrl: + "http://kusto-mcp.pilotswarm.svc.cluster.local/mcp", headers: { Authorization: authorization( "token-for-https://kusto.kusto.windows.net/.default", @@ -125,7 +129,7 @@ test("MCP workload identity requests one token per scope and binds deployment UR assert.equal(requestedScopes.length, 4); }); -test("MCP workload identity accepts only trusted deployment HTTPS servers", () => { +test("MCP workload identity accepts only trusted deployment server URLs", () => { assert.throws( () => createMcpWorkloadIdentityHeadersProvider({ @@ -152,8 +156,25 @@ test("MCP workload identity accepts only trusted deployment HTTPS servers", () = plain: { type: "http", url: "http://example.test/mcp" }, }, }), - /must use an HTTPS URL/, + /must use HTTPS or an in-cluster HTTP service URL/, ); + for (const url of [ + "http://kusto-mcp.svc.cluster.local/mcp", + "http://kusto-mcp.pilotswarm.svc.cluster.local.example.test/mcp", + "http://10.0.0.10/mcp", + "http://kusto-mcp.pilotswarm.svc.cluster.local/mcp#fragment", + ]) { + assert.throws( + () => + createMcpWorkloadIdentityHeadersProvider({ + scopeBindings: "kusto=api://kusto/.default", + deploymentMcpServers: { + kusto: { type: "http", url }, + }, + }), + /must use HTTPS or an in-cluster HTTP service URL/, + ); + } }); test("MCP workload identity fails closed when a scope returns no token", async () => { From e8386bff50ce78de7a7107f8bffdc71a4757bed5 Mon Sep 17 00:00:00 2001 From: Andrew Chung <47454279+andrewkcchung@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:48:16 -0400 Subject: [PATCH 2/2] Use abstract MCP workload identity examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/unit/mcp-workload-identity.test.mjs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/sdk/test/unit/mcp-workload-identity.test.mjs b/packages/sdk/test/unit/mcp-workload-identity.test.mjs index 99aac53c..e7ba0b46 100644 --- a/packages/sdk/test/unit/mcp-workload-identity.test.mjs +++ b/packages/sdk/test/unit/mcp-workload-identity.test.mjs @@ -72,13 +72,13 @@ test("MCP workload identity requests one token per scope and binds deployment UR scopeBindings: "ado=https://mcp.dev.azure.com/.default," + "boards=https://mcp.dev.azure.com/.default," + - "kusto=https://kusto.kusto.windows.net/.default", + "internal=api://internal-mcp/.default", deploymentMcpServers: { ado: { type: "http", url: "https://mcp.dev.azure.com/org" }, boards: { type: "sse", url: "https://mcp.dev.azure.com/org/boards" }, - kusto: { + internal: { type: "http", - url: "http://kusto-mcp.pilotswarm.svc.cluster.local/mcp", + url: "http://internal-mcp.platform.svc.cluster.local/mcp", }, }, credential: { @@ -110,19 +110,19 @@ test("MCP workload identity requests one token per scope and binds deployment UR ), }, }, - kusto: { + internal: { expectedUrl: - "http://kusto-mcp.pilotswarm.svc.cluster.local/mcp", + "http://internal-mcp.platform.svc.cluster.local/mcp", headers: { Authorization: authorization( - "token-for-https://kusto.kusto.windows.net/.default", + "token-for-api://internal-mcp/.default", ), }, }, }; assert.deepEqual(await provider(), expectedHeaders); assert.deepEqual(requestedScopes.sort(), [ - "https://kusto.kusto.windows.net/.default", + "api://internal-mcp/.default", "https://mcp.dev.azure.com/.default", ]); assert.deepEqual(await provider(), expectedHeaders); @@ -159,17 +159,17 @@ test("MCP workload identity accepts only trusted deployment server URLs", () => /must use HTTPS or an in-cluster HTTP service URL/, ); for (const url of [ - "http://kusto-mcp.svc.cluster.local/mcp", - "http://kusto-mcp.pilotswarm.svc.cluster.local.example.test/mcp", + "http://internal-mcp.svc.cluster.local/mcp", + "http://internal-mcp.platform.svc.cluster.local.example.test/mcp", "http://10.0.0.10/mcp", - "http://kusto-mcp.pilotswarm.svc.cluster.local/mcp#fragment", + "http://internal-mcp.platform.svc.cluster.local/mcp#fragment", ]) { assert.throws( () => createMcpWorkloadIdentityHeadersProvider({ - scopeBindings: "kusto=api://kusto/.default", + scopeBindings: "internal=api://internal-mcp/.default", deploymentMcpServers: { - kusto: { type: "http", url }, + internal: { type: "http", url }, }, }), /must use HTTPS or an in-cluster HTTP service URL/,