Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/proposals-impl/default-mcp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ So a fleet targeting organization `<org>` 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://<service>.<namespace>.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.
Expand Down
18 changes: 12 additions & 6 deletions packages/sdk/src/mcp-workload-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("");

/**
Expand Down Expand Up @@ -81,7 +83,7 @@ export function parseMcpWorkloadIdentityScopes(
return bindings;
}

function deploymentHttpsUrl(
function deploymentMcpUrl(
serverName: string,
config: McpWorkloadIdentityServerConfig | undefined,
): string {
Expand All @@ -103,23 +105,27 @@ 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 '<service>.<namespace>.svc.cluster.local', without embedded credentials or a fragment.`,
);
}
return config.url;
}

/**
* 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;
Expand All @@ -141,7 +147,7 @@ export function createMcpWorkloadIdentityHeadersProvider(options: {
}
return {
...binding,
expectedUrl: deploymentHttpsUrl(
expectedUrl: deploymentMcpUrl(
binding.serverName,
options.deploymentMcpServers[binding.serverName],
),
Expand Down
37 changes: 29 additions & 8 deletions packages/sdk/test/unit/mcp-workload-identity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ 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: { url: "https://kusto.example.test/mcp" },
internal: {
type: "http",
url: "http://internal-mcp.platform.svc.cluster.local/mcp",
},
},
credential: {
async getToken(scope) {
Expand Down Expand Up @@ -107,25 +110,26 @@ test("MCP workload identity requests one token per scope and binds deployment UR
),
},
},
kusto: {
expectedUrl: "https://kusto.example.test/mcp",
internal: {
expectedUrl:
"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);
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({
Expand All @@ -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://internal-mcp.svc.cluster.local/mcp",
"http://internal-mcp.platform.svc.cluster.local.example.test/mcp",
"http://10.0.0.10/mcp",
"http://internal-mcp.platform.svc.cluster.local/mcp#fragment",
]) {
assert.throws(
() =>
createMcpWorkloadIdentityHeadersProvider({
scopeBindings: "internal=api://internal-mcp/.default",
deploymentMcpServers: {
internal: { 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 () => {
Expand Down
Loading