Files
openwork/packages/enterprise-mcp-client/test/enterprise-mcp-client.test.ts
T

2491 lines
96 KiB
TypeScript

import assert from "node:assert/strict"
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
import { describe, it } from "node:test"
import { z } from "zod"
import {
createEnterpriseMcpClient,
EnterpriseMcpCatalogError,
EnterpriseMcpClientError,
EnterpriseMcpToolResultError,
EnterpriseMcpToolInputError,
EnterpriseMcpOAuthContractError,
validateMcpAuthorizationResponseIssuer,
type EnterpriseMcpDiagnosticEvent,
type EnterpriseMcpConnection,
type EnterpriseMcpFetch,
type EnterpriseMcpOAuthAuthorizationHandle,
type EnterpriseMcpOAuthClientRegistration,
type EnterpriseMcpOAuthCredential,
type EnterpriseMcpOAuthPersistence,
type StoredOAuthClientInformation,
type StoredOAuthTokens,
} from "../src/index.js"
import { EnterpriseMcpOAuthProvider } from "../src/oauth-provider.js"
import { createEnterpriseMcpRequestObserver } from "../src/request-observer.js"
import { collectEnterpriseMcpTools } from "../src/tool-catalog.js"
import {
assertEnterpriseMcpResourceResult,
collectEnterpriseMcpResources,
} from "../src/resource-catalog.js"
import { selectClientAuthMethod } from "@modelcontextprotocol/client"
import type { OAuthDiscoveryState } from "@modelcontextprotocol/client"
const rpcRequestSchema = z.object({
id: z.union([z.string(), z.number()]).optional(),
method: z.string(),
}).passthrough()
const rpcProgressRequestSchema = rpcRequestSchema.extend({
params: z.object({
_meta: z.object({
progressToken: z.union([z.string(), z.number()]).optional(),
}).passthrough().optional(),
}).passthrough().optional(),
})
const textToolResultSchema = z.object({
content: z.array(z.object({ type: z.literal("text"), text: z.string() })),
}).passthrough()
type MockMcpOptions = {
toolError?: boolean
toolErrorText?: string
expectedApiKey?: string
}
function requestText(body: BodyInit | null | undefined): string {
return typeof body === "string" ? body : ""
}
function mockMcpFetch(options: MockMcpOptions = {}): EnterpriseMcpFetch {
return async (_url, init) => {
if (options.expectedApiKey) {
const headers = new Headers(init?.headers)
assert.equal(headers.get("authorization"), `Bearer ${options.expectedApiKey}`)
}
const body = requestText(init?.body)
if (!body) return new Response(null, { status: 202 })
const parsed: unknown = JSON.parse(body)
const request = rpcRequestSchema.parse(parsed)
if (request.method === "notifications/initialized") {
return new Response(null, { status: 202 })
}
if (request.method === "initialize") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2025-06-18",
capabilities: {
tools: {},
resources: {},
extensions: { "io.modelcontextprotocol/ui": { mimeTypes: ["text/html;profile=mcp-app"] } },
},
serverInfo: { name: "enterprise-mcp-test", version: "1.0.0" },
},
})
}
if (request.method === "tools/list") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
tools: [{
name: "lookup-record",
description: "Looks up an enterprise record",
inputSchema: { type: "object", properties: {} },
}],
},
})
}
if (request.method === "tools/call") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
content: [{ type: "text", text: options.toolError ? (options.toolErrorText ?? "Provider rejected the operation") : "Record found" }],
structuredContent: { record: { id: "incident-42" } },
_meta: { providerTrace: "trace-42" },
isError: options.toolError ?? false,
},
})
}
if (request.method === "resources/list") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
resources: [{
name: "Incident App",
uri: "ui://incident/view.html",
mimeType: "text/html;profile=mcp-app",
_meta: { ui: { csp: { connectDomains: [] } } },
}],
},
})
}
if (request.method === "resources/templates/list") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: { resourceTemplates: [{ name: "Incident", uriTemplate: "incident://{id}" }] },
})
}
if (request.method === "resources/read") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
contents: [{
uri: "ui://incident/view.html",
mimeType: "text/html;profile=mcp-app",
text: "<!doctype html><html><body>Incident</body></html>",
_meta: { ui: { prefersBorder: true } },
}],
},
})
}
return new Response(null, { status: 404 })
}
}
async function requestBody(request: IncomingMessage): Promise<string> {
let body = ""
for await (const chunk of request) {
body += typeof chunk === "string" ? chunk : chunk.toString("utf8")
}
return body
}
function sendJson(response: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {
response.writeHead(status, { "content-type": "application/json", ...headers })
response.end(JSON.stringify(body))
}
async function sendMcpResponse(request: IncomingMessage, response: ServerResponse, options: { rejectToolsList?: boolean } = {}): Promise<void> {
const parsed: unknown = JSON.parse(await requestBody(request))
const rpc = rpcRequestSchema.parse(parsed)
if (rpc.method === "notifications/initialized") {
response.writeHead(202)
response.end()
return
}
if (rpc.method === "initialize") {
sendJson(response, 200, {
jsonrpc: "2.0",
id: rpc.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "oauth-enterprise-mcp-test", version: "1.0.0" },
},
})
return
}
if (rpc.method === "tools/list") {
if (options.rejectToolsList) {
sendJson(response, 403, { error: "provider_tool_policy_denied" })
return
}
sendJson(response, 200, {
jsonrpc: "2.0",
id: rpc.id,
result: {
tools: [{ name: "oauth-tool", inputSchema: { type: "object", properties: {} } }],
},
})
return
}
sendJson(response, 404, { error: "not_found" })
}
async function startOAuthMcpServer(options: {
rejectAuthenticatedMcp?: boolean
rejectAuthenticatedToolsList?: boolean
clientMetadataSupported?: boolean
scopeLessChallenge?: boolean
advertisedScopes?: string[]
resourceAdvertisedScopes?: string[]
authorizationAdvertisedScopes?: string[]
} = {}) {
let origin = ""
let capturedRegistration: Record<string, unknown> | null = null
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url ?? "/", origin)
if (url.pathname === "/.well-known/oauth-protected-resource/mcp") {
sendJson(response, 200, {
resource: `${origin}/mcp`,
authorization_servers: [origin],
scopes_supported: options.resourceAdvertisedScopes ?? options.advertisedScopes ?? ["tools.read"],
bearer_methods_supported: ["header"],
})
return
}
if (url.pathname === "/.well-known/oauth-authorization-server") {
sendJson(response, 200, {
issuer: origin,
authorization_endpoint: `${origin}/authorize`,
token_endpoint: `${origin}/token`,
registration_endpoint: `${origin}/register`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported: ["none"],
code_challenge_methods_supported: ["S256"],
scopes_supported: options.authorizationAdvertisedScopes ?? options.advertisedScopes ?? ["tools.read"],
...(options.clientMetadataSupported ? { client_id_metadata_document_supported: true } : {}),
})
return
}
if (url.pathname === "/register") {
const registration: unknown = JSON.parse(await requestBody(request))
const metadata = z.object({ redirect_uris: z.array(z.string()) }).passthrough().parse(registration)
capturedRegistration = metadata
sendJson(response, 201, {
client_id: "enterprise-test-client",
client_id_issued_at: Math.floor(Date.now() / 1000),
token_endpoint_auth_method: "none",
redirect_uris: metadata.redirect_uris,
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
scope: "tools.read",
})
return
}
if (url.pathname === "/token") {
const form = new URLSearchParams(await requestBody(request))
const grantType = form.get("grant_type")
if (grantType === "authorization_code") {
assert.equal(form.get("code"), "approved-code")
assert.ok(form.get("code_verifier"))
} else {
assert.equal(grantType, "refresh_token")
assert.equal(form.get("refresh_token"), "enterprise-refresh-token")
}
sendJson(response, 200, {
access_token: "enterprise-access-token",
refresh_token: "enterprise-refresh-token",
token_type: "Bearer",
expires_in: 3600,
scope: "tools.read",
}, { "cache-control": "no-store" })
return
}
if (url.pathname === "/mcp") {
if (request.headers.authorization !== "Bearer enterprise-access-token") {
response.writeHead(401, {
"www-authenticate": options.scopeLessChallenge
? `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource/mcp"`
: `Bearer resource_metadata="${origin}/.well-known/oauth-protected-resource/mcp", scope="tools.read"`,
})
response.end()
return
}
if (options.rejectAuthenticatedMcp) {
sendJson(response, 403, { error: "provider_policy_denied" })
return
}
await sendMcpResponse(request, response, { rejectToolsList: options.rejectAuthenticatedToolsList })
return
}
sendJson(response, 404, { error: "not_found" })
} catch {
sendJson(response, 500, { error: "oauth_test_server_error" })
}
})
await new Promise<void>((resolve, reject) => {
server.once("error", reject)
server.listen(0, "127.0.0.1", resolve)
})
const address = server.address()
if (!address || typeof address === "string") {
server.close()
throw new Error("OAuth MCP test server did not bind to a TCP port.")
}
origin = `http://127.0.0.1:${address.port}`
return {
origin,
registration: () => capturedRegistration,
close: () => new Promise<void>((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve())
}),
}
}
async function startProgressMcpServer(options: {
resultDelayMs: number
progressIntervalMs: number
}) {
let origin = ""
const server = createServer(async (request, response) => {
try {
const url = new URL(request.url ?? "/", origin)
if (url.pathname !== "/mcp") {
sendJson(response, 404, { error: "not_found" })
return
}
const body = await requestBody(request)
if (!body) {
response.writeHead(202)
response.end()
return
}
const parsed: unknown = JSON.parse(body)
const rpc = rpcProgressRequestSchema.parse(parsed)
if (rpc.method === "notifications/initialized") {
response.writeHead(202)
response.end()
return
}
if (rpc.method === "initialize") {
sendJson(response, 200, {
jsonrpc: "2.0",
id: rpc.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "progress-enterprise-mcp-test", version: "1.0.0" },
},
})
return
}
if (rpc.method !== "tools/call") {
sendJson(response, 404, { error: "not_found" })
return
}
response.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
})
const progressToken = rpc.params?._meta?.progressToken
let progress = 0
const writeMessage = (message: unknown) => {
response.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`)
}
const progressTimer = setInterval(() => {
if (progressToken === undefined) return
progress += 1
writeMessage({
jsonrpc: "2.0",
method: "notifications/progress",
params: { progressToken, progress },
})
}, options.progressIntervalMs)
const resultTimer = setTimeout(() => {
clearInterval(progressTimer)
writeMessage({
jsonrpc: "2.0",
id: rpc.id,
result: { content: [{ type: "text", text: "streamed result" }] },
})
response.end()
}, options.resultDelayMs)
response.on("close", () => {
clearInterval(progressTimer)
clearTimeout(resultTimer)
})
} catch {
sendJson(response, 500, { error: "progress_test_server_error" })
}
})
await new Promise<void>((resolve, reject) => {
server.once("error", reject)
server.listen(0, "127.0.0.1", resolve)
})
const address = server.address()
if (!address || typeof address === "string") {
server.close()
throw new Error("Progress MCP test server did not bind to a TCP port.")
}
origin = `http://127.0.0.1:${address.port}`
return {
origin,
close: () => new Promise<void>((resolve, reject) => {
server.close((error) => error ? reject(error) : resolve())
}),
}
}
function noAuthConnection(): EnterpriseMcpConnection {
return {
id: "connection-1",
serverUrl: "https://mcp.example.test/mcp",
authorization: { type: "none" },
}
}
describe("enterprise MCP client", () => {
it("preserves the last failed OAuth phase when a later cleanup request succeeds", async () => {
const controller = new AbortController()
const observer = createEnterpriseMcpRequestObserver({
connectionId: "connection-1",
operationPhase: "authorization-callback",
fetch: async (url) => new URL(url).pathname.endsWith("/token")
? Response.json({ error: "invalid_client" }, { status: 401 })
: Response.json({ issuer: "https://provider.example.test" }),
signal: controller.signal,
clock: { now: () => Date.now() },
})
await observer.fetch("https://provider.example.test/token", {
method: "POST",
body: new URLSearchParams({ grant_type: "authorization_code", code: "code" }),
})
await observer.fetch("https://provider.example.test/.well-known/oauth-authorization-server")
assert.equal(observer.lastRequestPhase(), "oauth-server-discovery")
assert.equal(observer.lastFailedRequestPhase(), "oauth-token-exchange")
})
it("preserves tools, resources, UI metadata, and tool results over MCP Streamable HTTP", async () => {
const events: EnterpriseMcpDiagnosticEvent[] = []
const client = createEnterpriseMcpClient({
fetch: mockMcpFetch(),
diagnosticSink: (event) => events.push(event),
})
const connection = noAuthConnection()
const redirectUri = "https://den.example.test/v1/mcp-connections/connection-1/connect/callback"
assert.deepEqual(await client.connect({ connection, redirectUri }), { status: "connected" })
const tools = await client.listTools({ connection, redirectUri })
assert.equal(tools[0]?.name, "lookup-record")
const result = await client.callTool({
connection,
redirectUri,
toolName: "lookup-record",
arguments: { table: "incident" },
})
assert.equal("isError" in result ? result.isError : undefined, false)
assert.deepEqual(result.structuredContent, { record: { id: "incident-42" } })
assert.deepEqual(result._meta, { providerTrace: "trace-42" })
const resources = await client.listResources({ connection, redirectUri })
assert.deepEqual(resources[0]?._meta, { ui: { csp: { connectDomains: [] } } })
const templates = await client.listResourceTemplates({ connection, redirectUri })
assert.equal(templates[0]?.uriTemplate, "incident://{id}")
const resource = await client.readResource({ connection, redirectUri, uri: "ui://incident/view.html" })
assert.deepEqual(resource.contents[0]?._meta, { ui: { prefersBorder: true } })
const descriptor = await client.describeServer({ connection, redirectUri })
assert.deepEqual(descriptor.serverInfo, { name: "enterprise-mcp-test", version: "1.0.0" })
assert.deepEqual(descriptor.capabilities.extensions, {
"io.modelcontextprotocol/ui": { mimeTypes: ["text/html;profile=mcp-app"] },
})
assert.ok(events.some((event) => event.kind !== "credential-invalidation" && event.requestPhase === "mcp-initialize" && event.outcome === "succeeded"))
assert.ok(events.some((event) => event.kind !== "credential-invalidation" && event.requestPhase === "mcp-tool-discovery" && event.outcome === "succeeded"))
assert.ok(events.some((event) => event.kind !== "credential-invalidation" && event.requestPhase === "mcp-tool-execution" && event.outcome === "succeeded"))
assert.ok(events.some((event) => event.kind !== "credential-invalidation" && event.requestPhase === "mcp-resource-discovery" && event.outcome === "succeeded"))
assert.ok(events.some((event) => event.kind !== "credential-invalidation" && event.requestPhase === "mcp-resource-read" && event.outcome === "succeeded"))
})
it("connects to a spec-legal MCP server that does not advertise tools", async () => {
let toolsListCalls = 0
const client = createEnterpriseMcpClient({
fetch: async (_url, init) => {
const body = requestText(init?.body)
if (!body) return new Response(null, { status: 202 })
const request = rpcRequestSchema.parse(JSON.parse(body))
if (request.method === "notifications/initialized") return new Response(null, { status: 202 })
if (request.method === "tools/list") {
toolsListCalls += 1
return Response.json({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } })
}
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { resources: {} },
serverInfo: { name: "resources-only", version: "1.0.0" },
},
})
},
})
assert.deepEqual(await client.connect({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
}), { status: "connected" })
assert.equal(toolsListCalls, 0)
})
it("sends Den's API key as a bearer credential", async () => {
const client = createEnterpriseMcpClient({ fetch: mockMcpFetch({ expectedApiKey: "secret-test-key" }) })
const result = await client.connect({
connection: {
...noAuthConnection(),
authorization: { type: "api-key", token: "secret-test-key" },
},
redirectUri: "https://den.example.test/callback",
})
assert.deepEqual(result, { status: "connected" })
})
it("does not let a diagnostic consumer change a connection outcome", async () => {
const client = createEnterpriseMcpClient({
fetch: mockMcpFetch(),
diagnosticSink: () => {
throw new Error("diagnostic sink failed")
},
})
assert.deepEqual(await client.connect({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
}), { status: "connected" })
})
it("identifies the exact request phase when endpoint access fails", async () => {
const client = createEnterpriseMcpClient({
fetch: async () => {
throw new Error("simulated network failure")
},
})
await assert.rejects(
client.connect({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
}),
(error: unknown) => {
assert.ok(error instanceof EnterpriseMcpClientError)
assert.equal(error.code, "MCP_CONNECTION_HANDSHAKE_FAILED")
assert.equal(error.operationPhase, "connection-handshake")
assert.equal(error.requestPhase, "mcp-discovery")
assert.match(error.message, /MCP connection handshake/)
return true
},
)
})
it("honors an injected absolute lifecycle deadline", async () => {
const controller = new AbortController()
const expiresAt = Date.now() + 40
const timer = setTimeout(() => controller.abort(new Error("shared deadline reached")), 40)
const client = createEnterpriseMcpClient({
lifecycle: { expiresAt, signal: controller.signal },
fetch: async (_url, init) => new Promise<Response>((_resolve, reject) => {
if (init?.signal?.aborted) {
reject(init.signal.reason)
return
}
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true })
}),
})
const startedAt = Date.now()
try {
await assert.rejects(client.connect({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
}))
assert.ok(Date.now() - startedAt < 500)
} finally {
clearTimeout(timer)
}
})
it("honors an injected lifecycle longer than the per-request timeout while progress streams", async () => {
const server = await startProgressMcpServer({ resultDelayMs: 500, progressIntervalMs: 40 })
try {
const client = createEnterpriseMcpClient({
fetch,
operationTimeoutMs: 150,
lifecycle: { expiresAt: Date.now() + 1_200, signal: new AbortController().signal },
})
const result = await client.callTool({
connection: {
id: "progress-connection",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "none" },
},
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: {},
})
const parsedResult = textToolResultSchema.parse(result)
assert.equal(parsedResult.content[0]?.text, "streamed result")
} finally {
await server.close()
}
})
it("still stops a progressing provider at the injected absolute lifecycle", async () => {
const server = await startProgressMcpServer({ resultDelayMs: 1_500, progressIntervalMs: 40 })
try {
const client = createEnterpriseMcpClient({
fetch,
operationTimeoutMs: 150,
lifecycle: { expiresAt: Date.now() + 700, signal: new AbortController().signal },
})
const startedAt = Date.now()
await assert.rejects(
client.callTool({
connection: {
id: "progress-deadline-connection",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "none" },
},
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: {},
}),
(error: unknown) => error instanceof EnterpriseMcpClientError
&& error.operationPhase === "tool-execution",
)
const elapsedMs = Date.now() - startedAt
assert.ok(elapsedMs >= 500, `expected lifecycle timeout to outlast per-request timeout, got ${elapsedMs}ms`)
assert.ok(elapsedMs < 2_000, `expected lifecycle timeout before the provider result, got ${elapsedMs}ms`)
} finally {
await server.close()
}
})
it("treats an MCP isError tool result as a failed operation", async () => {
const client = createEnterpriseMcpClient({ fetch: mockMcpFetch({ toolError: true }) })
await assert.rejects(
client.callTool({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: {},
}),
(error: unknown) => {
assert.ok(error instanceof EnterpriseMcpClientError)
assert.equal(error.code, "MCP_TOOL_EXECUTION_FAILED")
assert.ok(error.cause instanceof EnterpriseMcpToolResultError)
return true
},
)
})
it("preserves MCP isError results for transparent proxy callers", async () => {
const client = createEnterpriseMcpClient({ fetch: mockMcpFetch({ toolError: true }) })
const result = await client.callToolRaw({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: {},
})
assert.equal(result.isError, true)
assert.match(JSON.stringify(result.content), /Provider rejected the operation/)
})
it("rejects resource URIs over the byte limit before opening a provider connection", async () => {
let fetchCount = 0
const client = createEnterpriseMcpClient({
fetch: async () => {
fetchCount += 1
return new Response(null, { status: 500 })
},
})
await assert.rejects(
client.readResource({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
uri: `ui://${"🚀".repeat(4_096)}`,
}),
(error: unknown) => error instanceof EnterpriseMcpClientError
&& error.operationPhase === "configuration",
)
assert.equal(fetchCount, 0)
})
it("retains only a safe invalid-argument signal from standardized MCP SDK tool errors", async () => {
const privateText = "Input validation error: Invalid arguments for tool lookup-record: private provider detail"
const client = createEnterpriseMcpClient({
fetch: mockMcpFetch({ toolError: true, toolErrorText: privateText }),
})
await assert.rejects(
client.callTool({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: {},
}),
(error: unknown) => {
assert.ok(error instanceof EnterpriseMcpClientError)
assert.ok(error.cause instanceof EnterpriseMcpToolResultError)
assert.deepEqual(error.cause.providerSignal, { category: "invalid_arguments" })
assert.doesNotMatch(JSON.stringify(error.cause), /private provider detail/)
return true
},
)
})
it("rejects oversized or cyclic tool arguments before opening a provider connection", async () => {
let fetchCount = 0
const client = createEnterpriseMcpClient({
fetch: async () => {
fetchCount += 1
return new Response(null, { status: 500 })
},
})
await assert.rejects(
client.callTool({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: { payload: "x".repeat(1024 * 1024) },
}),
(error: unknown) => error instanceof EnterpriseMcpClientError
&& error.cause instanceof EnterpriseMcpToolInputError
&& error.cause.code === "MCP_TOOL_ARGUMENT_SIZE_LIMIT",
)
const cyclic: Record<string, unknown> = {}
cyclic.self = cyclic
await assert.rejects(
client.callTool({
connection: noAuthConnection(),
redirectUri: "https://den.example.test/callback",
toolName: "lookup-record",
arguments: cyclic,
}),
(error: unknown) => error instanceof EnterpriseMcpClientError
&& error.cause instanceof EnterpriseMcpToolInputError
&& error.cause.code === "MCP_TOOL_ARGUMENT_CYCLE",
)
assert.equal(fetchCount, 0)
})
})
class MemoryOAuthPersistence implements EnterpriseMcpOAuthPersistence {
registration: EnterpriseMcpOAuthClientRegistration | undefined
credential: EnterpriseMcpOAuthCredential | undefined
authorizationRecords = new Map<string, { handle: EnterpriseMcpOAuthAuthorizationHandle; codeVerifier: string }>()
invalidationCount = 0
revision = 0
discoveryState: OAuthDiscoveryState | undefined
private nextRevision(): string {
this.revision += 1
return `revision-${this.revision}`
}
private assertActive(input: { commitExpiresAt: number; signal: AbortSignal }): void {
if (input.signal.aborted || input.commitExpiresAt <= Date.now()) throw new Error("persistence deadline expired")
}
readonly clientRegistrations = {
load: async () => this.registration,
save: async (input: {
context: { commitExpiresAt: number; signal: AbortSignal }
clientInformation: StoredOAuthClientInformation
expiresAt?: number
source: "client-metadata" | "dynamic"
}) => {
this.assertActive(input.context)
if (!this.registration) {
this.registration = {
clientInformation: input.clientInformation,
revision: this.nextRevision(),
expiresAt: input.expiresAt,
source: input.source,
}
}
return this.registration
},
invalidate: async () => {
this.registration = undefined
},
}
readonly authorizations = {
begin: async (input: {
context: { commitExpiresAt: number; signal: AbortSignal }
id: string
codeVerifier: string
expiresAt: number
clientRegistrationRevision?: string
}) => {
this.assertActive(input.context)
this.authorizationRecords.set(input.id, {
handle: {
id: input.id,
revision: this.nextRevision(),
expiresAt: input.expiresAt,
clientRegistrationRevision: input.clientRegistrationRevision,
},
codeVerifier: input.codeVerifier,
})
},
load: async (input: { id: string }) => this.authorizationRecords.get(input.id),
invalidate: async (input: { id: string }) => {
this.authorizationRecords.delete(input.id)
},
}
readonly discovery = {
load: async () => this.discoveryState,
save: async (input: { state: OAuthDiscoveryState }) => {
this.discoveryState = input.state
},
invalidate: async () => {
this.discoveryState = undefined
},
}
readonly credentials = {
load: async () => this.credential,
save: async (input: {
context: { commitExpiresAt: number; signal: AbortSignal }
tokens: StoredOAuthTokens
expiresAt?: number
source: "authorization-code" | "refresh"
authorization?: EnterpriseMcpOAuthAuthorizationHandle
clientRegistrationRevision?: string
expectedCredentialRevision?: string
}) => {
this.assertActive(input.context)
if (input.source === "authorization-code") {
const pending = input.authorization ? this.authorizationRecords.get(input.authorization.id) : undefined
if (!pending || pending.handle.revision !== input.authorization?.revision) throw new Error("authorization was not active")
if (pending.handle.clientRegistrationRevision !== input.clientRegistrationRevision) {
throw new Error("client registration changed")
}
this.authorizationRecords.delete(pending.handle.id)
} else if (
!input.expectedCredentialRevision
|| input.expectedCredentialRevision !== this.credential?.revision
) {
throw new EnterpriseMcpOAuthContractError(
"MCP_OAUTH_CREDENTIAL_CHANGED",
"The OAuth credential changed while refresh was in progress.",
)
}
this.credential = {
tokens: input.tokens,
expiresAt: input.expiresAt,
revision: this.nextRevision(),
}
this.assertActive(input.context)
},
invalidate: async () => {
this.credential = undefined
this.invalidationCount += 1
},
}
seedRegistration(clientInformation: StoredOAuthClientInformation, expiresAt?: number): void {
this.registration = {
clientInformation,
revision: this.nextRevision(),
expiresAt,
source: "pre-registered",
}
}
seedCredential(tokens: StoredOAuthTokens, expiresAt?: number): void {
this.credential = { tokens, expiresAt, revision: this.nextRevision() }
}
}
function oauthMetadataFetch(issuer: string): EnterpriseMcpFetch {
const canonical = new URL(issuer)
return async (url) => {
const target = new URL(url)
if (
target.origin === canonical.origin
&& (
target.pathname.startsWith("/.well-known/oauth-authorization-server")
|| target.pathname.startsWith("/.well-known/openid-configuration")
)
) {
return Response.json({
issuer,
authorization_endpoint: `${canonical.origin}/authorize`,
token_endpoint: `${canonical.origin}/token`,
response_types_supported: ["code"],
})
}
return new Response(null, { status: 404 })
}
}
function oauthProvider(input: {
persistence: EnterpriseMcpOAuthPersistence
flow: { kind: "connect"; authorizationId?: string } | { kind: "callback"; authorizationId: string } | { kind: "runtime" }
now?: () => number
authorizationTransactionTtlMs?: number
expirationSkewMs?: number
}): EnterpriseMcpOAuthProvider {
const controller = new AbortController()
const now = input.now ?? (() => Date.now())
return new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/callback",
connectionId: "connection-1",
persistence: input.persistence,
flow: input.flow,
clientName: "OpenWork",
clock: { now },
lifecycle: { expiresAt: now() + 30_000, signal: controller.signal },
authorizationTransactionTtlMs: input.authorizationTransactionTtlMs ?? 600_000,
expirationSkewMs: input.expirationSkewMs ?? 0,
fetch: async () => new Response(null, { status: 404 }),
})
}
function runtimeRejectingMcpFetch(input: {
rejectMethod: "tools/call" | "tools/list"
status: number
wwwAuthenticate?: string
}): EnterpriseMcpFetch {
return async (_url, init) => {
const body = requestText(init?.body)
if (!body) return new Response(null, { status: 202 })
const request = rpcRequestSchema.parse(JSON.parse(body))
if (request.method === "notifications/initialized") return new Response(null, { status: 202 })
if (request.method === "initialize") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "runtime-rejection-test", version: "1.0.0" },
},
})
}
if (request.method === input.rejectMethod) {
const headers = new Headers()
if (input.wwwAuthenticate) headers.set("www-authenticate", input.wwwAuthenticate)
return new Response(null, { status: input.status, headers })
}
if (request.method === "tools/list") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: { tools: [{ name: "oauth-tool", inputSchema: { type: "object", properties: {} } }] },
})
}
if (request.method === "tools/call") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: { content: [{ type: "text", text: "ok" }], isError: false },
})
}
return new Response(null, { status: 404 })
}
}
function oauthRuntimeConnection(id: string, persistence: EnterpriseMcpOAuthPersistence): EnterpriseMcpConnection {
return {
id,
serverUrl: "https://mcp.example.test/mcp",
authorization: { type: "oauth", persistence },
}
}
describe("enterprise MCP OAuth persistence contract", () => {
it("uses discovery or an explicit registration hint to select confidential-client authentication", () => {
const confidentialClient = {
client_id: "confidential-client",
client_secret: "confidential-secret",
}
assert.equal(selectClientAuthMethod(confidentialClient, ["client_secret_post"]), "client_secret_post")
assert.equal(selectClientAuthMethod(confidentialClient, []), "client_secret_basic")
assert.equal(selectClientAuthMethod({
...confidentialClient,
redirect_uris: ["https://den.example.test/v1/mcp-connections/oauth/callback"],
token_endpoint_auth_method: "client_secret_post",
}, []), "client_secret_post")
})
it("validates authorization-response issuers exactly before token exchange", () => {
const discoveryState = {
authorizationServerUrl: "https://identity.example.test/tenant",
authorizationServerMetadata: {
issuer: "https://identity.example.test/tenant",
authorization_response_iss_parameter_supported: true,
},
resourceMetadata: {
authorization_servers: ["https://identity.example.test/tenant"],
},
}
assert.doesNotThrow(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: "https://identity.example.test/tenant",
discoveryState,
responseIssuer: "https://identity.example.test/tenant",
}))
assert.throws(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: "https://identity.example.test/tenant",
discoveryState,
responseIssuer: "https://identity.example.test/tenant/",
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
assert.throws(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: "https://identity.example.test/tenant",
discoveryState,
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("uses a distinct redirect URI as the mix-up defense for servers that do not advertise response issuers", () => {
const expectedIssuer = "https://identity.example.test/tenant"
const discoveryState = {
authorizationServerUrl: expectedIssuer,
authorizationServerMetadata: {
issuer: expectedIssuer,
authorization_response_iss_parameter_supported: false,
},
resourceMetadata: { authorization_servers: [expectedIssuer] },
}
assert.deepEqual(validateMcpAuthorizationResponseIssuer({
expectedIssuer,
discoveryState,
mixUpDefense: "distinct-redirect-uri",
}), { defense: "distinct-redirect-uri" })
assert.deepEqual(validateMcpAuthorizationResponseIssuer({
expectedIssuer,
discoveryState,
responseIssuer: "stytch.com/project-live-provider-value",
mixUpDefense: "distinct-redirect-uri",
}), {
defense: "distinct-redirect-uri",
ignoredResponseIssuer: "stytch.com/project-live-provider-value",
})
})
it("supports a pinned authorization transaction when a provider omits response issuers", () => {
const expectedIssuer = "https://identity.example.test/tenant"
const discoveryState = {
authorizationServerUrl: expectedIssuer,
authorizationServerMetadata: {
issuer: expectedIssuer,
authorization_response_iss_parameter_supported: false,
},
resourceMetadata: { authorization_servers: [expectedIssuer] },
}
assert.deepEqual(validateMcpAuthorizationResponseIssuer({
expectedIssuer,
discoveryState,
mixUpDefense: "pinned-transaction",
}), { defense: "pinned-transaction" })
assert.throws(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer,
discoveryState,
responseIssuer: "https://attacker.example.test",
mixUpDefense: "pinned-transaction",
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("never lets PKCE or an isolated callback override advertised RFC 9207 support", () => {
const expectedIssuer = "https://identity.example.test/tenant"
const discoveryState = {
authorizationServerUrl: expectedIssuer,
authorizationServerMetadata: {
issuer: expectedIssuer,
authorization_response_iss_parameter_supported: true,
code_challenge_methods_supported: ["S256"],
},
resourceMetadata: { authorization_servers: [expectedIssuer] },
}
assert.throws(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer,
discoveryState,
responseIssuer: "https://attacker.example.test",
mixUpDefense: "distinct-redirect-uri",
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("replaces resource-alias endpoints with strictly discovered canonical metadata", async () => {
const alias = "https://api.salesforce.example:443/platform/mcp/v1/platform/sobject-all"
const canonicalIssuer = "https://login.salesforce.example"
const discoveryState: OAuthDiscoveryState = {
authorizationServerUrl: alias,
authorizationServerMetadata: {
issuer: canonicalIssuer,
authorization_endpoint: "https://attacker.example.test/authorize",
token_endpoint: "https://attacker.example.test/token",
response_types_supported: ["code"],
},
resourceMetadata: {
resource: alias,
authorization_servers: [alias],
},
}
const persistence = new MemoryOAuthPersistence()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch(canonicalIssuer),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: canonicalIssuer,
},
})
await assert.doesNotReject(provider.saveDiscoveryState(discoveryState))
assert.equal(persistence.discoveryState?.authorizationServerUrl, canonicalIssuer)
assert.equal(persistence.discoveryState?.authorizationServerMetadata?.authorization_endpoint, `${canonicalIssuer}/authorize`)
assert.equal(persistence.discoveryState?.authorizationServerMetadata?.token_endpoint, `${canonicalIssuer}/token`)
const reloadedProvider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "runtime" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: async () => {
throw new Error("verified cached metadata must not be rediscovered")
},
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: canonicalIssuer,
},
})
assert.equal((await reloadedProvider.discoveryState())?.authorizationServerUrl, canonicalIssuer)
assert.doesNotThrow(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: canonicalIssuer,
discoveryState,
responseIssuer: canonicalIssuer,
}))
})
it("binds an equivalent root discovery alias while keeping callback issuer checks exact", async () => {
const canonicalIssuer = "https://vercel.example"
const discoveryState: OAuthDiscoveryState = {
authorizationServerUrl: "https://mcp.vercel.example",
authorizationServerMetadata: {
issuer: canonicalIssuer,
authorization_endpoint: `${canonicalIssuer}/oauth/authorize`,
token_endpoint: `${canonicalIssuer}/oauth/token`,
response_types_supported: ["code"],
},
resourceMetadata: {
resource: "https://mcp.vercel.example/",
authorization_servers: ["https://mcp.vercel.example"],
},
}
const persistence = new MemoryOAuthPersistence()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch(canonicalIssuer),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: canonicalIssuer,
},
})
await assert.doesNotReject(provider.saveDiscoveryState(discoveryState))
assert.doesNotThrow(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: canonicalIssuer,
discoveryState,
responseIssuer: canonicalIssuer,
}))
assert.throws(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: canonicalIssuer,
discoveryState,
responseIssuer: `${canonicalIssuer}/`,
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("binds an equivalent authorization-server root alias while keeping callback issuer checks exact", async () => {
const canonicalIssuer = "https://api.close.example"
const discoveryState: OAuthDiscoveryState = {
authorizationServerUrl: `${canonicalIssuer}/`,
authorizationServerMetadata: {
issuer: canonicalIssuer,
authorization_endpoint: "https://app.close.example/oauth2/authorize/",
token_endpoint: `${canonicalIssuer}/oauth2/token/`,
response_types_supported: ["code"],
},
resourceMetadata: {
resource: "https://mcp.close.example/",
authorization_servers: [`${canonicalIssuer}/`],
},
}
const persistence = new MemoryOAuthPersistence()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch(canonicalIssuer),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: canonicalIssuer,
},
})
await assert.doesNotReject(provider.saveDiscoveryState(discoveryState))
assert.doesNotThrow(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: canonicalIssuer,
discoveryState,
responseIssuer: canonicalIssuer,
}))
assert.throws(() => validateMcpAuthorizationResponseIssuer({
expectedIssuer: canonicalIssuer,
discoveryState,
responseIssuer: `${canonicalIssuer}/`,
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("does not normalize trailing slashes on non-root authorization-server issuers", async () => {
const canonicalIssuer = "https://identity.example.test/tenant"
const persistence = new MemoryOAuthPersistence()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch(canonicalIssuer),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: canonicalIssuer,
},
})
await assert.rejects(provider.saveDiscoveryState({
authorizationServerUrl: `${canonicalIssuer}/`,
authorizationServerMetadata: {
issuer: canonicalIssuer,
authorization_endpoint: `${canonicalIssuer}/authorize`,
token_endpoint: `${canonicalIssuer}/token`,
response_types_supported: ["code"],
},
resourceMetadata: {
resource: "https://mcp.example.test/",
authorization_servers: [`${canonicalIssuer}/`],
},
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("rejects a non-HTTPS client metadata document before OAuth performs network work", async () => {
let fetchCount = 0
const client = createEnterpriseMcpClient({
fetch: async () => {
fetchCount += 1
return new Response(null, { status: 500 })
},
})
await assert.rejects(client.connect({
connection: {
id: "invalid-cimd-url",
serverUrl: "https://mcp.example.test/mcp",
authorization: {
type: "oauth",
persistence: new MemoryOAuthPersistence(),
configuration: {
applicationType: "web",
clientMetadataUrl: "http://den.example.test/oauth/client-metadata.json",
},
},
},
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-state",
}), (error: unknown) => error instanceof EnterpriseMcpClientError
&& error.code === "MCP_CONFIGURATION_FAILED")
assert.equal(fetchCount, 0)
})
it("publishes a web client identity and records CIMD registration provenance", async () => {
const persistence = new MemoryOAuthPersistence()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: async () => new Response(null, { status: 404 }),
oauthConfiguration: {
applicationType: "web",
clientMetadataUrl: "https://den.example.test/oauth/client-metadata.json",
requestedScopes: ["tools.read"],
},
})
assert.equal(provider.clientMetadata.application_type, "web")
assert.deepEqual(provider.clientMetadata.redirect_uris, ["https://den.example.test/v1/mcp-connections/oauth/callback"])
assert.equal(provider.clientMetadata.scope, "tools.read")
await provider.saveClientInformation({ client_id: "https://den.example.test/oauth/client-metadata.json" })
assert.equal(persistence.registration?.source, "client-metadata")
})
it("rejects discovery state that does not match the selected issuer", async () => {
const persistence = new MemoryOAuthPersistence()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch("https://identity.example.test/tenant-a"),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: "https://identity.example.test/tenant-a",
},
})
await assert.rejects(
provider.saveDiscoveryState({
authorizationServerUrl: "https://identity.example.test/tenant-b",
authorizationServerMetadata: {
issuer: "https://identity.example.test/tenant-b",
authorization_endpoint: "https://identity.example.test/authorize",
token_endpoint: "https://identity.example.test/token",
response_types_supported: ["code"],
},
resourceMetadata: {
resource: "https://mcp.example.test",
authorization_servers: ["https://identity.example.test/tenant-a"],
},
}),
(error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH",
)
assert.equal(persistence.discoveryState, undefined)
})
it("preserves a cached issuer mismatch through the modern discovery probe", async () => {
const selectedIssuer = "https://mcp.example.test"
const canonicalIssuer = "https://identity.example.test"
const persistence = new MemoryOAuthPersistence()
persistence.discoveryState = {
authorizationServerUrl: canonicalIssuer,
authorizationServerMetadata: {
issuer: canonicalIssuer,
authorization_endpoint: `${canonicalIssuer}/authorize`,
token_endpoint: `${canonicalIssuer}/token`,
response_types_supported: ["code"],
},
resourceMetadata: {
resource: selectedIssuer,
authorization_servers: [selectedIssuer],
},
}
const client = createEnterpriseMcpClient({
fetch: async () => new Response(null, {
status: 401,
headers: {
"www-authenticate": `Bearer resource_metadata="${selectedIssuer}/.well-known/oauth-protected-resource"`,
},
}),
})
await assert.rejects(client.connect({
connection: {
id: "issuer-mismatch-probe",
serverUrl: selectedIssuer,
authorization: {
type: "oauth",
persistence,
configuration: {
applicationType: "web",
authorizationServerIssuer: selectedIssuer,
},
},
},
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-state",
}), (error: unknown) => error instanceof EnterpriseMcpClientError
&& error.cause instanceof EnterpriseMcpOAuthContractError
&& error.cause.code === "MCP_OAUTH_ISSUER_MISMATCH")
})
it("returns a typed configuration requirement when neither CIMD nor DCR is advertised", async () => {
const persistence = new MemoryOAuthPersistence()
const provider = oauthProvider({ persistence, flow: { kind: "connect", authorizationId: "signed-state" } })
await provider.saveDiscoveryState({
authorizationServerUrl: "https://identity.example.test",
authorizationServerMetadata: {
issuer: "https://identity.example.test",
authorization_endpoint: "https://identity.example.test/authorize",
token_endpoint: "https://identity.example.test/token",
response_types_supported: ["code"],
},
resourceMetadata: {
resource: "https://mcp.example.test",
authorization_servers: ["https://identity.example.test"],
},
})
await assert.rejects(
provider.clientInformation(),
(error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_CONFIGURATION_REQUIRED",
)
})
it("round-trips state, client registration, tokens, and PKCE through the injected store", async () => {
const persistence = new MemoryOAuthPersistence()
const controller = new AbortController()
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "connect", authorizationId: "signed-state" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: controller.signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: async () => new Response(null, { status: 404 }),
})
assert.equal(provider.state(), "signed-state")
assert.equal(provider.redirectUrl, "https://den.example.test/callback")
await provider.saveClientInformation({ client_id: "registered-client" })
assert.equal((await provider.clientInformation())?.client_id, "registered-client")
await provider.saveCodeVerifier("pkce-verifier")
assert.equal(persistence.authorizationRecords.get("signed-state")?.codeVerifier, "pkce-verifier")
provider.redirectToAuthorization(new URL("https://identity.example.test/authorize"))
assert.equal(provider.authorizeUrl, "https://identity.example.test/authorize")
})
it("completes discovery, dynamic registration, PKCE exchange, and authenticated MCP initialization", async () => {
const server = await startOAuthMcpServer()
try {
const persistence = new MemoryOAuthPersistence()
const events: EnterpriseMcpDiagnosticEvent[] = []
const client = createEnterpriseMcpClient({ fetch, diagnosticSink: (event) => events.push(event) })
const connection: EnterpriseMcpConnection = {
id: "oauth-connection",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "oauth", persistence },
}
const redirectUri = "https://den.example.test/v1/mcp-connections/oauth-connection/connect/callback"
const started = await client.connect({ connection, redirectUri, authorizationId: "signed-den-state" })
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
const authorizeUrl = new URL(started.authorizeUrl)
assert.equal(authorizeUrl.searchParams.get("state"), "signed-den-state")
assert.equal(authorizeUrl.searchParams.get("client_id"), "enterprise-test-client")
assert.equal(authorizeUrl.searchParams.get("scope"), "tools.read")
assert.equal(server.registration()?.application_type, "web")
assert.equal(server.registration()?.scope, "tools.read")
assert.ok(persistence.authorizationRecords.get("signed-den-state")?.codeVerifier)
await client.completeAuthorization({
connection,
redirectUri,
code: "approved-code",
authorizationId: "signed-den-state",
})
assert.equal(persistence.registration?.clientInformation.issuer, server.origin)
assert.equal(persistence.credential?.tokens.access_token, "enterprise-access-token")
assert.equal(persistence.credential?.tokens.issuer, server.origin)
assert.equal(persistence.authorizationRecords.size, 0)
assert.deepEqual(await client.connect({
connection,
redirectUri,
authorizationId: "signed-den-state-after-callback",
}), { status: "connected" })
const tools = await client.listTools({ connection, redirectUri })
assert.equal(tools[0]?.name, "oauth-tool")
for (const phase of [
"oauth-resource-discovery",
"oauth-server-discovery",
"oauth-client-registration",
"oauth-token-exchange",
"mcp-initialize",
"mcp-tool-discovery",
]) {
assert.ok(events.some((event) => event.requestPhase === phase), `Expected a diagnostic event for ${phase}`)
}
} finally {
await server.close()
}
})
it("rejects stored OAuth clients and tokens stamped for another issuer", async () => {
const selectedIssuer = "https://identity.example.test"
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({
client_id: "wrong-issuer-client",
issuer: "https://attacker.example.test",
})
persistence.seedCredential({
access_token: "wrong-issuer-token",
token_type: "Bearer",
issuer: "https://attacker.example.test",
})
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "runtime" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch(selectedIssuer),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: selectedIssuer,
},
})
await assert.rejects(provider.clientInformation({ issuer: selectedIssuer }), (error: unknown) => (
error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH"
))
await assert.rejects(provider.tokens({ issuer: selectedIssuer }), (error: unknown) => (
error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_ISSUER_MISMATCH"
))
})
it("accepts stored credentials whose issuer is the root trailing-slash alias of the selected issuer", async () => {
const selectedIssuer = "https://identity.example.test/"
const canonicalIssuer = "https://identity.example.test"
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({
client_id: "canonical-issuer-client",
issuer: canonicalIssuer,
})
persistence.seedCredential({
access_token: "canonical-issuer-token",
token_type: "Bearer",
issuer: canonicalIssuer,
})
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "runtime" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: oauthMetadataFetch(canonicalIssuer),
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: selectedIssuer,
},
})
assert.equal((await provider.clientInformation({ issuer: canonicalIssuer }))?.client_id, "canonical-issuer-client")
assert.equal((await provider.tokens({ issuer: canonicalIssuer }))?.access_token, "canonical-issuer-token")
})
it("keeps persisted discovery state when canonical metadata is temporarily unavailable", async () => {
const selectedIssuer = "https://identity.example.test"
const persistence = new MemoryOAuthPersistence()
const state: OAuthDiscoveryState = {
authorizationServerUrl: selectedIssuer,
authorizationServerMetadata: {
issuer: selectedIssuer,
authorization_endpoint: `${selectedIssuer}/authorize`,
token_endpoint: `${selectedIssuer}/token`,
response_types_supported: ["code"],
},
}
persistence.discoveryState = state
const provider = new EnterpriseMcpOAuthProvider({
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
connectionId: "connection-1",
persistence,
flow: { kind: "runtime" },
clientName: "OpenWork",
clock: { now: () => Date.now() },
lifecycle: { expiresAt: Date.now() + 30_000, signal: new AbortController().signal },
authorizationTransactionTtlMs: 600_000,
expirationSkewMs: 0,
fetch: async () => {
throw new TypeError("fetch failed")
},
oauthConfiguration: {
applicationType: "web",
authorizationServerIssuer: selectedIssuer,
},
})
await assert.rejects(provider.saveDiscoveryState(state), (error: unknown) => (
error instanceof TypeError && error.message === "fetch failed"
))
assert.deepEqual(persistence.discoveryState, state)
})
it("falls back to advertised scopes when the challenge and requested scopes are empty", async () => {
const server = await startOAuthMcpServer({ scopeLessChallenge: true })
try {
const persistence = new MemoryOAuthPersistence()
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "oauth-advertised-scope-fallback",
serverUrl: `${server.origin}/mcp`,
authorization: {
type: "oauth",
persistence,
configuration: { applicationType: "web", requestedScopes: [] },
},
}
const started = await client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-fallback-state",
})
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
assert.equal(new URL(started.authorizeUrl).searchParams.get("scope"), "tools.read")
assert.equal(server.registration()?.scope, "tools.read")
} finally {
await server.close()
}
})
it("prefers protected-resource scopes over unrelated authorization-server scopes", async () => {
const server = await startOAuthMcpServer({
scopeLessChallenge: true,
resourceAdvertisedScopes: ["tools.read"],
authorizationAdvertisedScopes: ["openid", "profile"],
})
try {
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "oauth-resource-scope-fallback",
serverUrl: `${server.origin}/mcp`,
authorization: {
type: "oauth",
persistence: new MemoryOAuthPersistence(),
configuration: { applicationType: "web", requestedScopes: [] },
},
}
const started = await client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-resource-scope-state",
})
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
assert.equal(new URL(started.authorizeUrl).searchParams.get("scope"), "tools.read")
assert.equal(server.registration()?.scope, "tools.read")
} finally {
await server.close()
}
})
it("keeps an administrator's selected scopes narrower than a scope-less provider advertisement", async () => {
const server = await startOAuthMcpServer({
scopeLessChallenge: true,
resourceAdvertisedScopes: ["tools.read", "tools.write"],
authorizationAdvertisedScopes: ["openid", "profile"],
})
try {
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "oauth-selected-scope",
serverUrl: `${server.origin}/mcp`,
authorization: {
type: "oauth",
persistence: new MemoryOAuthPersistence(),
configuration: { applicationType: "web", requestedScopes: ["tools.read"] },
},
}
const started = await client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-selected-scope-state",
})
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
assert.equal(new URL(started.authorizeUrl).searchParams.get("scope"), "tools.read")
assert.equal(server.registration()?.scope, "tools.read")
} finally {
await server.close()
}
})
it("rejects selected scopes that neither the resource nor authorization server advertises", async () => {
const server = await startOAuthMcpServer({
scopeLessChallenge: true,
resourceAdvertisedScopes: ["tools.read"],
authorizationAdvertisedScopes: ["openid", "profile"],
})
try {
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "oauth-unsupported-selected-scope",
serverUrl: `${server.origin}/mcp`,
authorization: {
type: "oauth",
persistence: new MemoryOAuthPersistence(),
configuration: { applicationType: "web", requestedScopes: ["admin.write"] },
},
}
await assert.rejects(client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-unsupported-scope-state",
}), (error: unknown) => error instanceof EnterpriseMcpClientError
&& error.cause instanceof Error
&& error.cause.message.includes("selected OAuth scope is not advertised"))
assert.equal(server.registration(), null)
} finally {
await server.close()
}
})
it("uses a client metadata document before an advertised dynamic registration endpoint", async () => {
const server = await startOAuthMcpServer({ clientMetadataSupported: true })
try {
const persistence = new MemoryOAuthPersistence()
const client = createEnterpriseMcpClient({ fetch })
const metadataUrl = "https://den.example.test/oauth/client-metadata.json"
const connection: EnterpriseMcpConnection = {
id: "oauth-cimd-priority",
serverUrl: `${server.origin}/mcp`,
authorization: {
type: "oauth",
persistence,
configuration: {
applicationType: "web",
clientMetadataUrl: metadataUrl,
requestedScopes: ["tools.read"],
},
},
}
const started = await client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-cimd-state",
})
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
assert.equal(new URL(started.authorizeUrl).searchParams.get("client_id"), metadataUrl)
assert.equal(server.registration(), null)
assert.equal(persistence.registration?.source, "client-metadata")
} finally {
await server.close()
}
})
it("starts OAuth when initialize is public but tool discovery requires authorization", async () => {
const origin = "https://api.descript.example"
const metadataUrl = "https://den.example.test/oauth/client-metadata.json"
const fetch: EnterpriseMcpFetch = async (url, init) => {
const target = new URL(url)
if (target.pathname === "/.well-known/oauth-protected-resource/v2/mcp") {
return Response.json({
resource: `${origin}/v2/mcp`,
authorization_servers: [origin],
})
}
if (target.pathname === "/.well-known/oauth-authorization-server") {
return Response.json({
issuer: origin,
authorization_endpoint: `${origin}/oauth/authorize`,
token_endpoint: `${origin}/oauth/token`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported: ["none"],
code_challenge_methods_supported: ["S256"],
client_id_metadata_document_supported: true,
})
}
if (target.pathname === "/v2/mcp") {
const body = typeof init?.body === "string" ? init.body : ""
if (!body) return new Response(null, { status: 202 })
const request = rpcRequestSchema.parse(JSON.parse(body))
if (request.method === "initialize") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "descript-style-test", version: "1.0.0" },
},
})
}
if (request.method === "notifications/initialized") return new Response(null, { status: 202 })
if (request.method === "tools/list") {
return new Response(null, {
status: 401,
headers: {
"www-authenticate": `Bearer resource_metadata=\"${origin}/.well-known/oauth-protected-resource/v2/mcp\"`,
},
})
}
}
return new Response(null, { status: 404 })
}
const persistence = new MemoryOAuthPersistence()
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "oauth-public-initialize",
serverUrl: `${origin}/v2/mcp`,
authorization: {
type: "oauth",
persistence,
configuration: {
applicationType: "web",
clientMetadataUrl: metadataUrl,
},
},
}
const started = await client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-descript-state",
})
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
assert.equal(new URL(started.authorizeUrl).searchParams.get("client_id"), metadataUrl)
assert.equal(persistence.registration?.source, "client-metadata")
})
it("starts OAuth when initialize and tool discovery are both public but no member credential exists", async () => {
const resourceOrigin = "https://bigquery.googleapis.test"
const authorizationOrigin = "https://accounts.google.test"
const mcpMethods: string[] = []
const fetch: EnterpriseMcpFetch = async (url, init) => {
const target = new URL(url)
if (target.href === `${resourceOrigin}/.well-known/oauth-protected-resource/mcp`) {
return Response.json({
resource: `${resourceOrigin}/mcp`,
authorization_servers: [`${authorizationOrigin}/`],
scopes_supported: ["https://www.googleapis.test/auth/bigquery"],
bearer_methods_supported: ["header"],
})
}
if (target.href === `${authorizationOrigin}/.well-known/oauth-authorization-server`) {
return Response.json({
issuer: authorizationOrigin,
authorization_endpoint: `${authorizationOrigin}/o/oauth2/v2/auth`,
token_endpoint: `${authorizationOrigin}/token`,
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
code_challenge_methods_supported: ["plain", "S256"],
})
}
if (target.href === `${resourceOrigin}/mcp`) {
const body = typeof init?.body === "string" ? init.body : ""
if (!body) return new Response(null, { status: 202 })
const request = rpcRequestSchema.parse(JSON.parse(body))
mcpMethods.push(request.method)
if (request.method === "notifications/initialized") return new Response(null, { status: 202 })
if (request.method === "initialize") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "bigquery-style-test", version: "1.0.0" },
},
})
}
if (request.method === "tools/list") {
return Response.json({
jsonrpc: "2.0",
id: request.id,
result: { tools: [{ name: "execute_sql", inputSchema: { type: "object" } }] },
})
}
}
return new Response(null, { status: 404 })
}
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({
client_id: "pre-registered-google-client",
client_secret: "test-client-secret",
token_endpoint_auth_method: "client_secret_post",
})
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "oauth-public-bigquery-style",
serverUrl: `${resourceOrigin}/mcp`,
authorization: { type: "oauth", persistence },
}
const started = await client.connect({
connection,
redirectUri: "https://den.example.test/v1/mcp-connections/oauth/callback",
authorizationId: "signed-bigquery-state",
})
assert.equal(started.status, "needs_auth")
if (started.status !== "needs_auth") throw new Error("Expected OAuth authorization to be required.")
const authorizeUrl = new URL(started.authorizeUrl)
assert.equal(authorizeUrl.origin, authorizationOrigin)
assert.equal(authorizeUrl.searchParams.get("client_id"), "pre-registered-google-client")
assert.equal(authorizeUrl.searchParams.get("scope"), "https://www.googleapis.test/auth/bigquery")
assert.equal(authorizeUrl.searchParams.get("state"), "signed-bigquery-state")
assert.ok(mcpMethods.includes("initialize"))
assert.ok(mcpMethods.includes("tools/list"))
assert.equal(persistence.credential, undefined)
})
it("refreshes an expired enterprise OAuth credential and persists the replacement", async () => {
const server = await startOAuthMcpServer()
try {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "enterprise-test-client" })
persistence.seedCredential({
access_token: "expired-access-token",
refresh_token: "enterprise-refresh-token",
token_type: "Bearer",
}, Date.now() - 1_000)
const events: EnterpriseMcpDiagnosticEvent[] = []
const client = createEnterpriseMcpClient({ fetch, diagnosticSink: (event) => events.push(event) })
const connection: EnterpriseMcpConnection = {
id: "oauth-refresh-connection",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "oauth", persistence },
}
assert.deepEqual(await client.connect({
connection,
redirectUri: "https://den.example.test/oauth-refresh-callback",
authorizationId: "signed-refresh-state",
}), { status: "connected" })
assert.equal(persistence.credential?.tokens.access_token, "enterprise-access-token")
assert.equal(persistence.credential?.tokens.refresh_token, "enterprise-refresh-token")
assert.ok(events.some((event) => event.requestPhase === "oauth-token-refresh"))
} finally {
await server.close()
}
})
it("preserves a credential when tool execution gets a 401 without invalid_token", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "registered-client" })
persistence.seedCredential({ access_token: "rejected-access-token", token_type: "Bearer" })
const client = createEnterpriseMcpClient({
fetch: runtimeRejectingMcpFetch({ rejectMethod: "tools/call", status: 401 }),
})
await assert.rejects(client.callTool({
connection: oauthRuntimeConnection("runtime-plain-401", persistence),
redirectUri: "https://den.example.test/oauth/callback",
toolName: "oauth-tool",
arguments: {},
}))
assert.equal(persistence.credential?.tokens.access_token, "rejected-access-token")
assert.equal(persistence.invalidationCount, 0)
})
it("invalidates a credential and emits diagnostics when tool execution gets invalid_token", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "registered-client" })
persistence.seedCredential({ access_token: "rejected-access-token", token_type: "Bearer" })
const events: EnterpriseMcpDiagnosticEvent[] = []
const client = createEnterpriseMcpClient({
fetch: runtimeRejectingMcpFetch({
rejectMethod: "tools/call",
status: 401,
wwwAuthenticate: 'Bearer error="invalid_token"',
}),
diagnosticSink: (event) => events.push(event),
})
await assert.rejects(client.callTool({
connection: oauthRuntimeConnection("runtime-invalid-token", persistence),
redirectUri: "https://den.example.test/oauth/callback",
toolName: "oauth-tool",
arguments: {},
}))
assert.equal(persistence.credential, undefined)
assert.equal(persistence.invalidationCount, 1)
const invalidationEvents = events.filter((event) => event.kind === "credential-invalidation")
assert.equal(invalidationEvents.length, 1)
const [event] = invalidationEvents
if (!event || event.kind !== "credential-invalidation") throw new Error("Expected a credential invalidation event.")
assert.deepEqual(event, {
kind: "credential-invalidation",
connectionId: "runtime-invalid-token",
operationPhase: "tool-execution",
requestPhase: "mcp-tool-execution",
httpStatus: 401,
invalidToken: true,
})
})
it("preserves a credential when tool discovery gets invalid_token", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "registered-client" })
persistence.seedCredential({ access_token: "rejected-access-token", token_type: "Bearer" })
const client = createEnterpriseMcpClient({
fetch: runtimeRejectingMcpFetch({
rejectMethod: "tools/list",
status: 401,
wwwAuthenticate: 'Bearer error="invalid_token"',
}),
})
await assert.rejects(client.listTools({
connection: oauthRuntimeConnection("discovery-invalid-token", persistence),
redirectUri: "https://den.example.test/oauth/callback",
}))
assert.equal(persistence.credential?.tokens.access_token, "rejected-access-token")
assert.equal(persistence.invalidationCount, 0)
})
it("preserves a credential when a provider returns a plain tool-policy 403", async () => {
const server = await startOAuthMcpServer({ rejectAuthenticatedToolsList: true })
try {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "enterprise-test-client" })
persistence.seedCredential({
access_token: "enterprise-access-token",
refresh_token: "enterprise-refresh-token",
token_type: "Bearer",
})
const client = createEnterpriseMcpClient({ fetch })
const connection: EnterpriseMcpConnection = {
id: "provider-acl-denial",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "oauth", persistence },
}
await assert.rejects(client.listTools({
connection,
redirectUri: "https://den.example.test/oauth/callback",
}))
assert.equal(persistence.credential?.tokens.access_token, "enterprise-access-token")
assert.equal(persistence.invalidationCount, 0)
} finally {
await server.close()
}
})
it("still invalidates a credential when tool execution gets a 403 bearer challenge", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "registered-client" })
persistence.seedCredential({ access_token: "rejected-access-token", token_type: "Bearer" })
const client = createEnterpriseMcpClient({
fetch: runtimeRejectingMcpFetch({
rejectMethod: "tools/call",
status: 403,
wwwAuthenticate: "Bearer realm=\"provider\"",
}),
})
await assert.rejects(client.callTool({
connection: oauthRuntimeConnection("runtime-bearer-403", persistence),
redirectUri: "https://den.example.test/oauth/callback",
toolName: "oauth-tool",
arguments: {},
}))
assert.equal(persistence.credential, undefined)
assert.equal(persistence.invalidationCount, 1)
})
it("rejects a stale concurrent refresh response instead of overwriting newer credentials", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedCredential({
access_token: "original-access-token",
refresh_token: "original-refresh-token",
token_type: "Bearer",
}, Date.now() + 60_000)
const first = oauthProvider({ persistence, flow: { kind: "runtime" } })
const second = oauthProvider({ persistence, flow: { kind: "runtime" } })
await first.tokens()
await second.tokens()
await first.saveTokens({
access_token: "first-refreshed-access-token",
refresh_token: "first-rotated-refresh-token",
token_type: "Bearer",
})
await assert.rejects(second.saveTokens({
access_token: "stale-refreshed-access-token",
refresh_token: "stale-rotated-refresh-token",
token_type: "Bearer",
}), (error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_CREDENTIAL_CHANGED")
assert.equal(persistence.credential?.tokens.access_token, "first-refreshed-access-token")
assert.equal(persistence.credential?.tokens.refresh_token, "first-rotated-refresh-token")
})
it("invalidates exchanged tokens when callback validation cannot initialize MCP", async () => {
const server = await startOAuthMcpServer({ rejectAuthenticatedMcp: true })
try {
const persistence = new MemoryOAuthPersistence()
const client = createEnterpriseMcpClient({ fetch, operationTimeoutMs: 5_000 })
const connection: EnterpriseMcpConnection = {
id: "oauth-validation-failure",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "oauth", persistence },
}
const redirectUri = "https://den.example.test/oauth-validation-failure"
const started = await client.connect({ connection, redirectUri, authorizationId: "signed-state" })
assert.equal(started.status, "needs_auth")
await assert.rejects(client.completeAuthorization({
connection,
redirectUri,
code: "approved-code",
authorizationId: "signed-state",
}))
assert.equal(persistence.credential, undefined)
assert.equal(persistence.invalidationCount, 1)
} finally {
await server.close()
}
})
it("reports credential invalidation failures alongside callback validation failures", async () => {
const server = await startOAuthMcpServer({ rejectAuthenticatedMcp: true })
try {
const persistence = new MemoryOAuthPersistence()
Object.defineProperty(persistence.credentials, "invalidate", {
value: async () => { throw new Error("credential store unavailable") },
})
const client = createEnterpriseMcpClient({ fetch, operationTimeoutMs: 5_000 })
const connection: EnterpriseMcpConnection = {
id: "oauth-cleanup-failure",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "oauth", persistence },
}
const redirectUri = "https://den.example.test/oauth-cleanup-failure"
const started = await client.connect({ connection, redirectUri, authorizationId: "signed-state" })
assert.equal(started.status, "needs_auth")
await assert.rejects(client.completeAuthorization({
connection,
redirectUri,
code: "approved-code",
authorizationId: "signed-state",
}), (error: unknown) => error instanceof EnterpriseMcpClientError
&& error.cause instanceof AggregateError
&& error.cause.errors.some((cause) => cause instanceof Error && cause.message === "credential store unavailable"))
} finally {
await server.close()
}
})
it("invalidates exchanged tokens when callback initialization succeeds but tool discovery fails", async () => {
const server = await startOAuthMcpServer({ rejectAuthenticatedToolsList: true })
try {
const persistence = new MemoryOAuthPersistence()
const client = createEnterpriseMcpClient({ fetch, operationTimeoutMs: 5_000 })
const connection: EnterpriseMcpConnection = {
id: "oauth-tool-validation-failure",
serverUrl: `${server.origin}/mcp`,
authorization: { type: "oauth", persistence },
}
const redirectUri = "https://den.example.test/oauth-tool-validation-failure"
const started = await client.connect({ connection, redirectUri, authorizationId: "signed-tool-state" })
assert.equal(started.status, "needs_auth")
await assert.rejects(client.completeAuthorization({
connection,
redirectUri,
code: "approved-code",
authorizationId: "signed-tool-state",
}))
assert.equal(persistence.credential, undefined)
assert.equal(persistence.invalidationCount, 1)
} finally {
await server.close()
}
})
it("requires an explicit signed authorization id before OAuth performs network or persistence work", async () => {
const persistence = new MemoryOAuthPersistence()
let fetchCount = 0
const client = createEnterpriseMcpClient({
fetch: async () => {
fetchCount += 1
return new Response(null, { status: 500 })
},
})
await assert.rejects(
client.connect({
connection: {
id: "oauth-missing-state",
serverUrl: "https://mcp.example.test/mcp",
authorization: { type: "oauth", persistence },
},
redirectUri: "https://den.example.test/callback",
}),
(error: unknown) => error instanceof EnterpriseMcpClientError
&& error.code === "MCP_CONFIGURATION_FAILED",
)
assert.equal(fetchCount, 0)
assert.equal(persistence.authorizationRecords.size, 0)
})
it("keeps concurrent PKCE transactions isolated by signed state", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "registered-client" })
const first = oauthProvider({
persistence,
flow: { kind: "connect", authorizationId: "signed-state-a" },
})
const second = oauthProvider({
persistence,
flow: { kind: "connect", authorizationId: "signed-state-b" },
})
await first.clientInformation()
await second.clientInformation()
await first.saveCodeVerifier("a".repeat(43))
await second.saveCodeVerifier("b".repeat(43))
assert.equal(persistence.authorizationRecords.size, 2)
const firstCallback = oauthProvider({
persistence,
flow: { kind: "callback", authorizationId: "signed-state-a" },
})
const secondCallback = oauthProvider({
persistence,
flow: { kind: "callback", authorizationId: "signed-state-b" },
})
await firstCallback.clientInformation()
await secondCallback.clientInformation()
assert.equal(await firstCallback.codeVerifier(), "a".repeat(43))
assert.equal(await secondCallback.codeVerifier(), "b".repeat(43))
})
it("rejects an expired authorization transaction with a stable source code", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "registered-client" })
const base = Date.now()
let now = base
const start = oauthProvider({
persistence,
flow: { kind: "connect", authorizationId: "signed-expiring-state" },
now: () => now,
authorizationTransactionTtlMs: 100,
})
await start.clientInformation()
await start.saveCodeVerifier("v".repeat(43))
now = base + 101
const callback = oauthProvider({
persistence,
flow: { kind: "callback", authorizationId: "signed-expiring-state" },
now: () => now,
})
await callback.clientInformation()
await assert.rejects(
callback.codeVerifier(),
(error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_AUTHORIZATION_EXPIRED",
)
assert.equal(persistence.authorizationRecords.size, 0)
})
it("rejects callbacks when the OAuth client changed after authorization started", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedRegistration({ client_id: "client-a" })
const start = oauthProvider({
persistence,
flow: { kind: "connect", authorizationId: "signed-client-revision" },
})
await start.clientInformation()
await start.saveCodeVerifier("v".repeat(43))
persistence.seedRegistration({ client_id: "client-b" })
const callback = oauthProvider({
persistence,
flow: { kind: "callback", authorizationId: "signed-client-revision" },
})
await callback.clientInformation()
await assert.rejects(
callback.codeVerifier(),
(error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED",
)
})
it("invalidates an expired access token when no refresh token exists", async () => {
const persistence = new MemoryOAuthPersistence()
persistence.seedCredential({ access_token: "expired", token_type: "Bearer" }, 999)
const provider = oauthProvider({
persistence,
flow: { kind: "runtime" },
now: () => 1_000,
})
await assert.rejects(
provider.tokens(),
(error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_CREDENTIAL_EXPIRED",
)
assert.equal(persistence.credential, undefined)
})
it("fails a losing concurrent dynamic registration instead of using the wrong client", async () => {
const persistence = new MemoryOAuthPersistence()
const winner = oauthProvider({ persistence, flow: { kind: "connect", authorizationId: "winner-state" } })
const loser = oauthProvider({ persistence, flow: { kind: "connect", authorizationId: "loser-state" } })
await winner.saveClientInformation({ client_id: "winner-client" })
await assert.rejects(
loser.saveClientInformation({ client_id: "loser-client" }),
(error: unknown) => error instanceof EnterpriseMcpOAuthContractError
&& error.code === "MCP_OAUTH_AUTHORIZATION_CLIENT_CHANGED",
)
})
})
describe("enterprise MCP catalog contract", () => {
it("collects resources without changing standard UI metadata", async () => {
const resources = await collectEnterpriseMcpResources({
requestOptions: {},
listPage: async (cursor) => cursor
? { resources: [{ name: "Second", uri: "ui://second", _meta: { ui: { prefersBorder: false } } }] }
: {
resources: [{ name: "First", uri: "ui://first", _meta: { ui: { prefersBorder: true } } }],
nextCursor: "page-2",
},
})
assert.deepEqual(resources.map((resource) => [resource.uri, resource._meta]), [
["ui://first", { ui: { prefersBorder: true } }],
["ui://second", { ui: { prefersBorder: false } }],
])
})
it("rejects duplicate resource URIs and oversized resource results", async () => {
await assert.rejects(collectEnterpriseMcpResources({
requestOptions: {},
listPage: async (cursor) => cursor
? { resources: [{ name: "Again", uri: "ui://duplicate" }] }
: { resources: [{ name: "First", uri: "ui://duplicate" }], nextCursor: "again" },
}), (error: unknown) => error instanceof EnterpriseMcpCatalogError
&& error.code === "MCP_CATALOG_DUPLICATE_RESOURCE")
assert.throws(
() => assertEnterpriseMcpResourceResult({ contents: [{ text: "x".repeat(2 * 1024 * 1024) }] }),
(error: unknown) => error instanceof EnterpriseMcpCatalogError
&& error.code === "MCP_RESOURCE_RESULT_LIMIT",
)
})
it("collects a bounded paginated tool catalog", async () => {
const tools = await collectEnterpriseMcpTools({
requestOptions: {},
listPage: async (cursor) => cursor
? {
tools: [{ name: "second-tool", inputSchema: { type: "object" } }],
}
: {
tools: [{ name: "first-tool", inputSchema: { type: "object" } }],
nextCursor: "page-2",
},
})
assert.deepEqual(tools.map((tool) => tool.name), ["first-tool", "second-tool"])
})
it("rejects duplicate tools across catalog pages", async () => {
await assert.rejects(
collectEnterpriseMcpTools({
requestOptions: {},
listPage: async (cursor) => cursor
? { tools: [{ name: "duplicate", inputSchema: { type: "object" } }] }
: {
tools: [{ name: "duplicate", inputSchema: { type: "object" } }],
nextCursor: "page-2",
},
}),
(error: unknown) => {
assert.ok(error instanceof EnterpriseMcpCatalogError)
assert.equal(error.code, "MCP_CATALOG_DUPLICATE_TOOL")
return true
},
)
})
it("rejects a repeated pagination cursor", async () => {
await assert.rejects(
collectEnterpriseMcpTools({
requestOptions: {},
listPage: async () => ({ tools: [], nextCursor: "repeated-cursor" }),
}),
(error: unknown) => error instanceof EnterpriseMcpCatalogError
&& error.code === "MCP_CATALOG_CURSOR_LOOP",
)
})
it("enforces the absolute catalog page limit", async () => {
let page = 0
await assert.rejects(
collectEnterpriseMcpTools({
requestOptions: {},
listPage: async () => {
page += 1
return { tools: [], nextCursor: `page-${page}` }
},
}),
(error: unknown) => error instanceof EnterpriseMcpCatalogError
&& error.code === "MCP_CATALOG_PAGE_LIMIT",
)
assert.equal(page, 20)
})
it("rejects oversized tool names and deeply nested schemas", async () => {
await assert.rejects(
collectEnterpriseMcpTools({
requestOptions: {},
listPage: async () => ({
tools: [{ name: "x".repeat(513), inputSchema: { type: "object" } }],
}),
}),
(error: unknown) => error instanceof EnterpriseMcpCatalogError
&& error.code === "MCP_CATALOG_TOOL_NAME_LIMIT",
)
let nested: Record<string, unknown> = { type: "string" }
for (let depth = 0; depth < 70; depth += 1) nested = { nested }
await assert.rejects(
collectEnterpriseMcpTools({
requestOptions: {},
listPage: async () => ({
tools: [{
name: "deep-schema",
inputSchema: { type: "object", properties: { value: nested } },
}],
}),
}),
(error: unknown) => error instanceof EnterpriseMcpCatalogError
&& error.code === "MCP_CATALOG_SCHEMA_DEPTH_LIMIT",
)
})
})