mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 09:10:47 -04:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 593fcbdc2e | |||
| cb85fbc11e | |||
| 5823deb7c8 |
@@ -769,7 +769,10 @@ export const McpDebugCommand = effectCmd({
|
|||||||
prompts.log.info(`WWW-Authenticate: ${wwwAuth}`)
|
prompts.log.info(`WWW-Authenticate: ${wwwAuth}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (
|
||||||
|
response.status === 401 &&
|
||||||
|
!Object.keys(serverConfig.headers ?? {}).some((key) => key.toLowerCase() === "authorization")
|
||||||
|
) {
|
||||||
prompts.log.warn("Server returned 401 Unauthorized")
|
prompts.log.warn("Server returned 401 Unauthorized")
|
||||||
|
|
||||||
// Try to discover OAuth metadata
|
// Try to discover OAuth metadata
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ export const layer = Layer.effect(
|
|||||||
key: string,
|
key: string,
|
||||||
mcp: ConfigMCPV1.Info & { type: "remote" },
|
mcp: ConfigMCPV1.Info & { type: "remote" },
|
||||||
) {
|
) {
|
||||||
const oauthDisabled = mcp.oauth === false
|
const oauthDisabled =
|
||||||
|
mcp.oauth === false || Object.keys(mcp.headers ?? {}).some((key) => key.toLowerCase() === "authorization")
|
||||||
const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
|
const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined
|
||||||
const url = remoteURL(mcp.url)
|
const url = remoteURL(mcp.url)
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -797,6 +798,8 @@ export const layer = Layer.effect(
|
|||||||
const mcpConfig = yield* requireMcpConfig(mcpName)
|
const mcpConfig = yield* requireMcpConfig(mcpName)
|
||||||
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
|
if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`)
|
||||||
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`)
|
||||||
|
if (Object.keys(mcpConfig.headers ?? {}).some((key) => key.toLowerCase() === "authorization"))
|
||||||
|
throw new Error(`MCP server ${mcpName} uses static Authorization`)
|
||||||
const url = remoteURL(mcpConfig.url)
|
const url = remoteURL(mcpConfig.url)
|
||||||
if (!url) throw new Error(`Invalid MCP URL for "${mcpName}"`)
|
if (!url) throw new Error(`Invalid MCP URL for "${mcpName}"`)
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const { MCP } = await import("../../src/mcp/index")
|
|||||||
const it = testEffect(MCP.defaultLayer)
|
const it = testEffect(MCP.defaultLayer)
|
||||||
|
|
||||||
describe("mcp.headers", () => {
|
describe("mcp.headers", () => {
|
||||||
it.instance("headers are passed to transports when oauth is enabled (default)", () =>
|
it.instance("static authorization is passed to transports without oauth", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
yield* mcp
|
yield* mcp
|
||||||
@@ -72,8 +72,8 @@ describe("mcp.headers", () => {
|
|||||||
Authorization: "Bearer test-token",
|
Authorization: "Bearer test-token",
|
||||||
"X-Custom-Header": "custom-value",
|
"X-Custom-Header": "custom-value",
|
||||||
})
|
})
|
||||||
// OAuth should be enabled by default, so authProvider should exist
|
// Static Authorization and OAuth are mutually exclusive.
|
||||||
expect(call.options.authProvider).toBeDefined()
|
expect(call.options.authProvider).toBeUndefined()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -105,6 +105,28 @@ describe("mcp.headers", () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance("case-insensitive authorization disables oauth", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const mcp = yield* MCP.Service
|
||||||
|
yield* mcp
|
||||||
|
.add("test-server-lowercase-auth", {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://example.com/mcp",
|
||||||
|
headers: { authorization: "Bearer test-token", "X-Custom-Header": "custom-value" },
|
||||||
|
})
|
||||||
|
.pipe(Effect.catch(() => Effect.void))
|
||||||
|
|
||||||
|
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
||||||
|
for (const call of transportCalls) {
|
||||||
|
expect(call.options.requestInit?.headers).toEqual({
|
||||||
|
authorization: "Bearer test-token",
|
||||||
|
"X-Custom-Header": "custom-value",
|
||||||
|
})
|
||||||
|
expect(call.options.authProvider).toBeUndefined()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.instance("no requestInit when headers are not provided", () =>
|
it.instance("no requestInit when headers are not provided", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||||
|
import type {
|
||||||
|
OAuthClientInformationMixed,
|
||||||
|
OAuthClientMetadata,
|
||||||
|
OAuthTokens,
|
||||||
|
} from "@modelcontextprotocol/sdk/shared/auth.js"
|
||||||
|
// @ts-expect-error Bun's query cache key avoids process-global module mocks from other test files.
|
||||||
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js?oauth-header-isolation"
|
||||||
|
|
||||||
|
const canary = "resource-secret"
|
||||||
|
const staticAuthorization = "Bearer static-resource-token"
|
||||||
|
const basicAuthorization = `Basic ${btoa("client:secret")}`
|
||||||
|
|
||||||
|
describe("MCP OAuth header isolation", () => {
|
||||||
|
test("keeps resource headers on same-origin protected resource metadata", async () => {
|
||||||
|
using authorizationServer = serve(() =>
|
||||||
|
Response.json({
|
||||||
|
issuer: authorizationServer.origin,
|
||||||
|
authorization_endpoint: `${authorizationServer.origin}/authorize`,
|
||||||
|
token_endpoint: `${authorizationServer.origin}/token`,
|
||||||
|
response_types_supported: ["code"],
|
||||||
|
code_challenge_methods_supported: ["S256"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
using resource = serve((request) => {
|
||||||
|
if (new URL(request.url).pathname === "/prm") {
|
||||||
|
return Response.json({
|
||||||
|
resource: `${resource.origin}/mcp`,
|
||||||
|
authorization_servers: [authorizationServer.origin],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return new Response(null, {
|
||||||
|
status: 401,
|
||||||
|
headers: { "WWW-Authenticate": `Bearer resource_metadata="${resource.origin}/prm"` },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(`${resource.origin}/mcp`), {
|
||||||
|
authProvider: createProvider({
|
||||||
|
clientInformation: { client_id: "client", token_endpoint_auth_method: "none" },
|
||||||
|
}),
|
||||||
|
requestInit: { headers: { "X-Resource-Canary": canary, Authorization: staticAuthorization } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await transport.start()
|
||||||
|
await expect(transport.send(request())).rejects.toThrow("Unauthorized")
|
||||||
|
await transport.close()
|
||||||
|
|
||||||
|
expect(resource.requests.map((request) => new URL(request.url).pathname)).toEqual(["/mcp", "/prm"])
|
||||||
|
expectResourceHeaders(resource.requests)
|
||||||
|
expectNoResourceHeaders(authorizationServer.requests)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not follow protected resource metadata redirects with resource headers", async () => {
|
||||||
|
using target = serve(() => Response.json({}))
|
||||||
|
using resource = serve((request) => {
|
||||||
|
if (new URL(request.url).pathname === "/prm") {
|
||||||
|
return Response.redirect(`${target.origin}/prm`)
|
||||||
|
}
|
||||||
|
return new Response(null, {
|
||||||
|
status: 401,
|
||||||
|
headers: { "WWW-Authenticate": `Bearer resource_metadata="${resource.origin}/prm"` },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(`${resource.origin}/mcp`), {
|
||||||
|
authProvider: createProvider({
|
||||||
|
clientInformation: { client_id: "client", token_endpoint_auth_method: "none" },
|
||||||
|
}),
|
||||||
|
requestInit: { headers: { "X-Resource-Canary": canary } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await transport.start()
|
||||||
|
await expect(transport.send(request())).rejects.toThrow("Unauthorized")
|
||||||
|
await transport.close()
|
||||||
|
|
||||||
|
expect(target.requests).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps resource headers out of discovery, registration, and token exchange", async () => {
|
||||||
|
using token = serve(() => Response.json({ access_token: "access", token_type: "Bearer", refresh_token: "refresh" }))
|
||||||
|
using registration = serve(() =>
|
||||||
|
Response.json({
|
||||||
|
client_id: "client",
|
||||||
|
client_secret: "secret",
|
||||||
|
token_endpoint_auth_method: "client_secret_basic",
|
||||||
|
redirect_uris: ["http://127.0.0.1/callback"],
|
||||||
|
client_name: "OpenCode test",
|
||||||
|
grant_types: ["authorization_code", "refresh_token"],
|
||||||
|
response_types: ["code"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
using authorizationServer = serve(() =>
|
||||||
|
Response.json({
|
||||||
|
issuer: authorizationServer.origin,
|
||||||
|
authorization_endpoint: `${authorizationServer.origin}/authorize`,
|
||||||
|
token_endpoint: `${token.origin}/token`,
|
||||||
|
registration_endpoint: `${registration.origin}/register`,
|
||||||
|
response_types_supported: ["code"],
|
||||||
|
code_challenge_methods_supported: ["S256"],
|
||||||
|
token_endpoint_auth_methods_supported: ["client_secret_basic"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
using metadata = serve(() =>
|
||||||
|
Response.json({
|
||||||
|
resource: `${resource.origin}/mcp`,
|
||||||
|
authorization_servers: [authorizationServer.origin],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
using resource = serve(
|
||||||
|
() =>
|
||||||
|
new Response(null, {
|
||||||
|
status: 401,
|
||||||
|
headers: { "WWW-Authenticate": `Bearer resource_metadata="${metadata.origin}/prm"` },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const provider = createProvider()
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(`${resource.origin}/mcp`), {
|
||||||
|
authProvider: provider,
|
||||||
|
requestInit: { headers: { "X-Resource-Canary": canary, aUtHoRiZaTiOn: staticAuthorization } },
|
||||||
|
})
|
||||||
|
|
||||||
|
await transport.start()
|
||||||
|
await expect(transport.send(request())).rejects.toThrow("Unauthorized")
|
||||||
|
await transport.finishAuth("authorization-code")
|
||||||
|
await transport.close()
|
||||||
|
|
||||||
|
expectResourceHeaders(resource.requests)
|
||||||
|
expectNoResourceHeaders(metadata.requests)
|
||||||
|
expectNoResourceHeaders(authorizationServer.requests)
|
||||||
|
expectNoResourceHeaders(registration.requests)
|
||||||
|
expectNoResourceHeaders(token.requests)
|
||||||
|
expect(registration.requests).toHaveLength(1)
|
||||||
|
expect(token.requests).toHaveLength(1)
|
||||||
|
expectAuthorization(token.requests[0], basicAuthorization)
|
||||||
|
expect(new URLSearchParams(token.requests[0].body).get("grant_type")).toBe("authorization_code")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps resource headers out of token refresh", async () => {
|
||||||
|
using refresh = serve(() => Response.json({ access_token: "fresh-access", token_type: "Bearer" }))
|
||||||
|
using authorizationServer = serve(() =>
|
||||||
|
Response.json({
|
||||||
|
issuer: authorizationServer.origin,
|
||||||
|
authorization_endpoint: `${authorizationServer.origin}/authorize`,
|
||||||
|
token_endpoint: `${refresh.origin}/refresh`,
|
||||||
|
response_types_supported: ["code"],
|
||||||
|
code_challenge_methods_supported: ["S256"],
|
||||||
|
token_endpoint_auth_methods_supported: ["client_secret_basic"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
using metadata = serve(() =>
|
||||||
|
Response.json({
|
||||||
|
resource: `${resource.origin}/mcp`,
|
||||||
|
authorization_servers: [authorizationServer.origin],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
using resource = serve((request) => {
|
||||||
|
if (resource.requests.length === 1) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 401,
|
||||||
|
headers: { "WWW-Authenticate": `Bearer resource_metadata="${metadata.origin}/prm"` },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Response.json({ jsonrpc: "2.0", id: 1, result: {} })
|
||||||
|
})
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(`${resource.origin}/mcp`), {
|
||||||
|
authProvider: createProvider({
|
||||||
|
clientInformation: {
|
||||||
|
client_id: "client",
|
||||||
|
client_secret: "secret",
|
||||||
|
token_endpoint_auth_method: "client_secret_basic",
|
||||||
|
},
|
||||||
|
tokens: { access_token: "expired-access", token_type: "Bearer", refresh_token: "refresh-token" },
|
||||||
|
}),
|
||||||
|
requestInit: { headers: { "X-Resource-Canary": canary, authorization: staticAuthorization } },
|
||||||
|
})
|
||||||
|
transport.onmessage = () => {}
|
||||||
|
|
||||||
|
await transport.start()
|
||||||
|
await transport.send(request())
|
||||||
|
await transport.close()
|
||||||
|
|
||||||
|
expect(resource.requests).toHaveLength(2)
|
||||||
|
expectResourceHeaders(resource.requests, ["Bearer expired-access", "Bearer fresh-access"])
|
||||||
|
expectNoResourceHeaders(metadata.requests)
|
||||||
|
expectNoResourceHeaders(authorizationServer.requests)
|
||||||
|
expectNoResourceHeaders(refresh.requests)
|
||||||
|
expect(refresh.requests).toHaveLength(1)
|
||||||
|
expectAuthorization(refresh.requests[0], basicAuthorization)
|
||||||
|
expect(new URLSearchParams(refresh.requests[0].body).get("grant_type")).toBe("refresh_token")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function createProvider(initial?: {
|
||||||
|
clientInformation?: OAuthClientInformationMixed
|
||||||
|
tokens?: OAuthTokens
|
||||||
|
}): OAuthClientProvider {
|
||||||
|
let clientInformation = initial?.clientInformation
|
||||||
|
let tokens = initial?.tokens
|
||||||
|
let codeVerifier = ""
|
||||||
|
return {
|
||||||
|
redirectUrl: "http://127.0.0.1/callback",
|
||||||
|
clientMetadata: {
|
||||||
|
redirect_uris: ["http://127.0.0.1/callback"],
|
||||||
|
client_name: "OpenCode test",
|
||||||
|
grant_types: ["authorization_code", "refresh_token"],
|
||||||
|
response_types: ["code"],
|
||||||
|
} satisfies OAuthClientMetadata,
|
||||||
|
clientInformation: () => clientInformation,
|
||||||
|
saveClientInformation: (value) => {
|
||||||
|
clientInformation = value
|
||||||
|
},
|
||||||
|
tokens: () => tokens,
|
||||||
|
saveTokens: (value) => {
|
||||||
|
tokens = value
|
||||||
|
},
|
||||||
|
redirectToAuthorization: () => {},
|
||||||
|
saveCodeVerifier: (value) => {
|
||||||
|
codeVerifier = value
|
||||||
|
},
|
||||||
|
codeVerifier: () => codeVerifier,
|
||||||
|
state: () => "state",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function request() {
|
||||||
|
return {
|
||||||
|
jsonrpc: "2.0" as const,
|
||||||
|
id: 1,
|
||||||
|
method: "initialize",
|
||||||
|
params: {
|
||||||
|
protocolVersion: "2025-06-18",
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: "opencode-test", version: "1" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectResourceHeaders(requests: CapturedRequest[], authorization: string | string[] = staticAuthorization) {
|
||||||
|
expect(requests.length).toBeGreaterThan(0)
|
||||||
|
for (const [index, request] of requests.entries()) {
|
||||||
|
expect(request.headers.get("x-resource-canary")).toBe(canary)
|
||||||
|
expectAuthorization(request, Array.isArray(authorization) ? authorization[index] : authorization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectNoResourceHeaders(requests: CapturedRequest[]) {
|
||||||
|
expect(requests.length).toBeGreaterThan(0)
|
||||||
|
for (const request of requests) {
|
||||||
|
expect(request.headers.get("x-resource-canary")).toBeNull()
|
||||||
|
expect(request.headers.get("authorization")).not.toBe(staticAuthorization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectAuthorization(request: CapturedRequest, value: string) {
|
||||||
|
expect([...request.headers].filter(([name]) => name === "authorization")).toEqual([["authorization", value]])
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CapturedRequest {
|
||||||
|
url: string
|
||||||
|
headers: Headers
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function serve(handler: (request: CapturedRequest) => Response | Promise<Response>) {
|
||||||
|
const requests: CapturedRequest[] = []
|
||||||
|
const server = Bun.serve({
|
||||||
|
hostname: "127.0.0.1",
|
||||||
|
port: 0,
|
||||||
|
async fetch(request) {
|
||||||
|
const captured = {
|
||||||
|
url: request.url,
|
||||||
|
headers: new Headers(request.headers),
|
||||||
|
body: await request.text(),
|
||||||
|
}
|
||||||
|
requests.push(captured)
|
||||||
|
return handler(captured)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
origin: `http://127.0.0.1:${server.port}`,
|
||||||
|
requests,
|
||||||
|
[Symbol.dispose]: () => server.stop(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,59 @@
|
|||||||
|
diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
|
||||||
|
index c2e4fa91d26f5336889f6afa416147db75fc4872..6fe80dd336252838adf47b0a8e022e55d11e3657 100644
|
||||||
|
--- a/dist/cjs/client/auth.js
|
||||||
|
+++ b/dist/cjs/client/auth.js
|
||||||
|
@@ -186,7 +186,7 @@ async function auth(provider, options) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
-async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
||||||
|
+async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, resourceMetadataFetchFn }) {
|
||||||
|
// Check if the provider has cached discovery state to skip discovery
|
||||||
|
const cachedState = await provider.discoveryState?.();
|
||||||
|
let resourceMetadata;
|
||||||
|
@@ -198,6 +198,9 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||||
|
if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) {
|
||||||
|
effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
|
||||||
|
}
|
||||||
|
+ const protectedResourceFetchFn = !effectiveResourceMetadataUrl || effectiveResourceMetadataUrl.origin === new URL(serverUrl).origin
|
||||||
|
+ ? (resourceMetadataFetchFn ?? fetchFn)
|
||||||
|
+ : fetchFn;
|
||||||
|
if (cachedState?.authorizationServerUrl) {
|
||||||
|
// Restore discovery state from cache
|
||||||
|
authorizationServerUrl = cachedState.authorizationServerUrl;
|
||||||
|
@@ -207,7 +210,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||||
|
// If resource metadata wasn't cached, try to fetch it for selectResourceURL
|
||||||
|
if (!resourceMetadata) {
|
||||||
|
try {
|
||||||
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
|
||||||
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, protectedResourceFetchFn);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// RFC 9728 not available — selectResourceURL will handle undefined
|
||||||
|
@@ -225,7 +228,11 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Full discovery via RFC 9728
|
||||||
|
- const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
|
||||||
|
+ const serverInfo = await discoverOAuthServerInfo(serverUrl, {
|
||||||
|
+ resourceMetadataUrl: effectiveResourceMetadataUrl,
|
||||||
|
+ fetchFn,
|
||||||
|
+ resourceMetadataFetchFn: protectedResourceFetchFn
|
||||||
|
+ });
|
||||||
|
authorizationServerUrl = serverInfo.authorizationServerUrl;
|
||||||
|
metadata = serverInfo.authorizationServerMetadata;
|
||||||
|
resourceMetadata = serverInfo.resourceMetadata;
|
||||||
|
@@ -688,7 +695,7 @@ async function discoverOAuthServerInfo(serverUrl, opts) {
|
||||||
|
let resourceMetadata;
|
||||||
|
let authorizationServerUrl;
|
||||||
|
try {
|
||||||
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
|
||||||
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.resourceMetadataFetchFn ?? opts?.fetchFn);
|
||||||
|
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
|
||||||
|
authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
||||||
|
}
|
||||||
diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
|
diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
|
||||||
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
|
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
|
||||||
--- a/dist/cjs/client/index.d.ts
|
--- a/dist/cjs/client/index.d.ts
|
||||||
+++ b/dist/cjs/client/index.d.ts
|
+++ b/dist/cjs/client/index.d.ts
|
||||||
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
||||||
@@ -7,25 +61,12 @@ index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d01
|
|||||||
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
||||||
*/
|
*/
|
||||||
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
||||||
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
|
||||||
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
|
||||||
[x: string]: unknown;
|
|
||||||
content: ({
|
|
||||||
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
|
|
||||||
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
|
|
||||||
--- a/dist/esm/client/index.d.ts
|
|
||||||
+++ b/dist/esm/client/index.d.ts
|
|
||||||
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
|
||||||
*
|
|
||||||
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
|
||||||
*/
|
|
||||||
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
|
||||||
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
||||||
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
||||||
[x: string]: unknown;
|
[x: string]: unknown;
|
||||||
content: ({
|
content: ({
|
||||||
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
|
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
|
||||||
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644
|
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..88e58b90b673cb0f9c60920edec7d2ebb16f112b 100644
|
||||||
--- a/dist/cjs/client/index.js
|
--- a/dist/cjs/client/index.js
|
||||||
+++ b/dist/cjs/client/index.js
|
+++ b/dist/cjs/client/index.js
|
||||||
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
|
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
|
||||||
@@ -112,11 +153,119 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1
|
|||||||
/**
|
/**
|
||||||
* After initialization has completed, this will be populated with the server's reported capabilities.
|
* After initialization has completed, this will be populated with the server's reported capabilities.
|
||||||
*/
|
*/
|
||||||
|
diff --git a/dist/cjs/client/sse.js b/dist/cjs/client/sse.js
|
||||||
|
index 8ca8dad652e76c299927f836cf622c0be877e1d4..14cc1b969ac51ecb6f894608f079f4f04d8acd62 100644
|
||||||
|
--- a/dist/cjs/client/sse.js
|
||||||
|
+++ b/dist/cjs/client/sse.js
|
||||||
|
@@ -27,7 +27,9 @@ class SSEClientTransport {
|
||||||
|
this._requestInit = opts?.requestInit;
|
||||||
|
this._authProvider = opts?.authProvider;
|
||||||
|
this._fetch = opts?.fetch;
|
||||||
|
- this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
|
||||||
|
+ this._resourceFetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
||||||
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
||||||
|
+ this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, oauthRequestInit);
|
||||||
|
}
|
||||||
|
async _authThenStart() {
|
||||||
|
if (!this._authProvider) {
|
||||||
|
@@ -39,7 +41,8 @@ class SSEClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
@@ -63,10 +66,11 @@ class SSEClientTransport {
|
||||||
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
||||||
|
}
|
||||||
|
const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
|
||||||
|
- return new Headers({
|
||||||
|
- ...headers,
|
||||||
|
- ...extraHeaders
|
||||||
|
- });
|
||||||
|
+ const result = new Headers(extraHeaders);
|
||||||
|
+ for (const [name, value] of Object.entries(headers)) {
|
||||||
|
+ result.set(name, value);
|
||||||
|
+ }
|
||||||
|
+ return result;
|
||||||
|
}
|
||||||
|
_startOrAuth() {
|
||||||
|
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
|
||||||
|
@@ -149,7 +153,8 @@ class SSEClientTransport {
|
||||||
|
authorizationCode,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new auth_js_1.UnauthorizedError('Failed to authorize');
|
||||||
|
@@ -185,7 +190,8 @@ class SSEClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new auth_js_1.UnauthorizedError();
|
||||||
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
|
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
|
||||||
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
|
index a29a7d3a0f14d9cd800ef5b296485237350c666f..54ac759eb19df7c6f16fc031a689270acb5c3922 100644
|
||||||
--- a/dist/cjs/client/streamableHttp.js
|
--- a/dist/cjs/client/streamableHttp.js
|
||||||
+++ b/dist/cjs/client/streamableHttp.js
|
+++ b/dist/cjs/client/streamableHttp.js
|
||||||
@@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
|
@@ -33,7 +33,9 @@ class StreamableHTTPClientTransport {
|
||||||
|
this._requestInit = opts?.requestInit;
|
||||||
|
this._authProvider = opts?.authProvider;
|
||||||
|
this._fetch = opts?.fetch;
|
||||||
|
- this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
|
||||||
|
+ this._resourceFetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
||||||
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
||||||
|
+ this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, oauthRequestInit);
|
||||||
|
this._sessionId = opts?.sessionId;
|
||||||
|
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
|
||||||
|
}
|
||||||
|
@@ -47,7 +49,8 @@ class StreamableHTTPClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
@@ -74,10 +77,11 @@ class StreamableHTTPClientTransport {
|
||||||
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
||||||
|
}
|
||||||
|
const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
|
||||||
|
- return new Headers({
|
||||||
|
- ...headers,
|
||||||
|
- ...extraHeaders
|
||||||
|
- });
|
||||||
|
+ const result = new Headers(extraHeaders);
|
||||||
|
+ for (const [name, value] of Object.entries(headers)) {
|
||||||
|
+ result.set(name, value);
|
||||||
|
+ }
|
||||||
|
+ return result;
|
||||||
|
}
|
||||||
|
async _startOrAuthSse(options) {
|
||||||
|
const { resumptionToken } = options;
|
||||||
|
@@ -275,7 +279,8 @@ class StreamableHTTPClientTransport {
|
||||||
|
authorizationCode,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new auth_js_1.UnauthorizedError('Failed to authorize');
|
||||||
|
@@ -290,7 +295,38 @@ class StreamableHTTPClientTransport {
|
||||||
this.onclose?.();
|
this.onclose?.();
|
||||||
}
|
}
|
||||||
async send(message, options) {
|
async send(message, options) {
|
||||||
@@ -155,7 +304,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
|
|||||||
const { resumptionToken, onresumptiontoken } = options || {};
|
const { resumptionToken, onresumptiontoken } = options || {};
|
||||||
if (resumptionToken) {
|
if (resumptionToken) {
|
||||||
// If we have at last event ID, we need to reconnect the SSE stream
|
// If we have at last event ID, we need to reconnect the SSE stream
|
||||||
@@ -298,6 +329,7 @@ class StreamableHTTPClientTransport {
|
@@ -298,6 +334,7 @@ class StreamableHTTPClientTransport {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headers = await this._commonHeaders();
|
const headers = await this._commonHeaders();
|
||||||
@@ -163,7 +312,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
|
|||||||
headers.set('content-type', 'application/json');
|
headers.set('content-type', 'application/json');
|
||||||
headers.set('accept', 'application/json, text/event-stream');
|
headers.set('accept', 'application/json, text/event-stream');
|
||||||
const init = {
|
const init = {
|
||||||
@@ -310,11 +342,20 @@ class StreamableHTTPClientTransport {
|
@@ -310,11 +347,20 @@ class StreamableHTTPClientTransport {
|
||||||
const response = await (this._fetch ?? fetch)(this._url, init);
|
const response = await (this._fetch ?? fetch)(this._url, init);
|
||||||
// Handle session ID received during initialization
|
// Handle session ID received during initialization
|
||||||
const sessionId = response.headers.get('mcp-session-id');
|
const sessionId = response.headers.get('mcp-session-id');
|
||||||
@@ -185,7 +334,17 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
|
|||||||
if (response.status === 401 && this._authProvider) {
|
if (response.status === 401 && this._authProvider) {
|
||||||
// Prevent infinite recursion when server returns 401 after successful auth
|
// Prevent infinite recursion when server returns 401 after successful auth
|
||||||
if (this._hasCompletedAuthFlow) {
|
if (this._hasCompletedAuthFlow) {
|
||||||
@@ -335,7 +376,7 @@ class StreamableHTTPClientTransport {
|
@@ -327,7 +373,8 @@ class StreamableHTTPClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new auth_js_1.UnauthorizedError();
|
||||||
|
@@ -335,7 +382,7 @@ class StreamableHTTPClientTransport {
|
||||||
// Mark that we completed auth flow
|
// Mark that we completed auth flow
|
||||||
this._hasCompletedAuthFlow = true;
|
this._hasCompletedAuthFlow = true;
|
||||||
// Purposely _not_ awaited, so we don't call onerror twice
|
// Purposely _not_ awaited, so we don't call onerror twice
|
||||||
@@ -194,7 +353,14 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
|
|||||||
}
|
}
|
||||||
if (response.status === 403 && this._authProvider) {
|
if (response.status === 403 && this._authProvider) {
|
||||||
const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
|
const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
|
||||||
@@ -362,7 +403,7 @@ class StreamableHTTPClientTransport {
|
@@ -357,12 +404,13 @@ class StreamableHTTPClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetch
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
if (result !== 'AUTHORIZED') {
|
if (result !== 'AUTHORIZED') {
|
||||||
throw new auth_js_1.UnauthorizedError();
|
throw new auth_js_1.UnauthorizedError();
|
||||||
}
|
}
|
||||||
@@ -204,7 +370,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
|
|||||||
}
|
}
|
||||||
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
||||||
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
|
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
|
||||||
index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644
|
index 3617e787f0ba70447c99501aee7aa67584d89758..4ee4d158391558fdc1f977f5134b7cacfc45e8c3 100644
|
||||||
--- a/dist/cjs/shared/protocol.js
|
--- a/dist/cjs/shared/protocol.js
|
||||||
+++ b/dist/cjs/shared/protocol.js
|
+++ b/dist/cjs/shared/protocol.js
|
||||||
@@ -744,7 +744,12 @@ class Protocol {
|
@@ -744,7 +744,12 @@ class Protocol {
|
||||||
@@ -221,8 +387,75 @@ index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb7
|
|||||||
this._cleanupTimeout(messageId);
|
this._cleanupTimeout(messageId);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
|
diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
|
||||||
|
index e183040fc2bba22ca1ccc784984f3310854403b7..6b88910ca88f9714abe7cad26fbf3a31c4fb376c 100644
|
||||||
|
--- a/dist/esm/client/auth.js
|
||||||
|
+++ b/dist/esm/client/auth.js
|
||||||
|
@@ -161,7 +161,7 @@ export async function auth(provider, options) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
-async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
||||||
|
+async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, resourceMetadataFetchFn }) {
|
||||||
|
// Check if the provider has cached discovery state to skip discovery
|
||||||
|
const cachedState = await provider.discoveryState?.();
|
||||||
|
let resourceMetadata;
|
||||||
|
@@ -173,6 +173,9 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||||
|
if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) {
|
||||||
|
effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
|
||||||
|
}
|
||||||
|
+ const protectedResourceFetchFn = !effectiveResourceMetadataUrl || effectiveResourceMetadataUrl.origin === new URL(serverUrl).origin
|
||||||
|
+ ? (resourceMetadataFetchFn ?? fetchFn)
|
||||||
|
+ : fetchFn;
|
||||||
|
if (cachedState?.authorizationServerUrl) {
|
||||||
|
// Restore discovery state from cache
|
||||||
|
authorizationServerUrl = cachedState.authorizationServerUrl;
|
||||||
|
@@ -182,7 +185,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||||
|
// If resource metadata wasn't cached, try to fetch it for selectResourceURL
|
||||||
|
if (!resourceMetadata) {
|
||||||
|
try {
|
||||||
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
|
||||||
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, protectedResourceFetchFn);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// RFC 9728 not available — selectResourceURL will handle undefined
|
||||||
|
@@ -200,7 +203,11 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Full discovery via RFC 9728
|
||||||
|
- const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
|
||||||
|
+ const serverInfo = await discoverOAuthServerInfo(serverUrl, {
|
||||||
|
+ resourceMetadataUrl: effectiveResourceMetadataUrl,
|
||||||
|
+ fetchFn,
|
||||||
|
+ resourceMetadataFetchFn: protectedResourceFetchFn
|
||||||
|
+ });
|
||||||
|
authorizationServerUrl = serverInfo.authorizationServerUrl;
|
||||||
|
metadata = serverInfo.authorizationServerMetadata;
|
||||||
|
resourceMetadata = serverInfo.resourceMetadata;
|
||||||
|
@@ -663,7 +670,7 @@ export async function discoverOAuthServerInfo(serverUrl, opts) {
|
||||||
|
let resourceMetadata;
|
||||||
|
let authorizationServerUrl;
|
||||||
|
try {
|
||||||
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
|
||||||
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.resourceMetadataFetchFn ?? opts?.fetchFn);
|
||||||
|
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
|
||||||
|
authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
||||||
|
}
|
||||||
|
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
|
||||||
|
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
|
||||||
|
--- a/dist/esm/client/index.d.ts
|
||||||
|
+++ b/dist/esm/client/index.d.ts
|
||||||
|
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
||||||
|
*
|
||||||
|
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
||||||
|
*/
|
||||||
|
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
||||||
|
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
||||||
|
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
||||||
|
[x: string]: unknown;
|
||||||
|
content: ({
|
||||||
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
|
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
|
||||||
index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644
|
index 49b12c6cd918c457420fef7ad5528a9443d1a191..339153cb7e9299b7a9bdec0e56e41cedf425fb31 100644
|
||||||
--- a/dist/esm/client/index.js
|
--- a/dist/esm/client/index.js
|
||||||
+++ b/dist/esm/client/index.js
|
+++ b/dist/esm/client/index.js
|
||||||
@@ -284,41 +284,16 @@ export class Client extends Protocol {
|
@@ -284,41 +284,16 @@ export class Client extends Protocol {
|
||||||
@@ -309,8 +542,69 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e
|
|||||||
/**
|
/**
|
||||||
* After initialization has completed, this will be populated with the server's reported capabilities.
|
* After initialization has completed, this will be populated with the server's reported capabilities.
|
||||||
*/
|
*/
|
||||||
|
diff --git a/dist/esm/client/sse.js b/dist/esm/client/sse.js
|
||||||
|
index 58c474156ba4b33090ac092be6f3284e695a7ffd..b813e467b497697208167ad9bfccdecd1200ffac 100644
|
||||||
|
--- a/dist/esm/client/sse.js
|
||||||
|
+++ b/dist/esm/client/sse.js
|
||||||
|
@@ -23,7 +23,9 @@ export class SSEClientTransport {
|
||||||
|
this._requestInit = opts?.requestInit;
|
||||||
|
this._authProvider = opts?.authProvider;
|
||||||
|
this._fetch = opts?.fetch;
|
||||||
|
- this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
|
||||||
|
+ this._resourceFetchWithInit = createFetchWithInit(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
||||||
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
||||||
|
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, oauthRequestInit);
|
||||||
|
}
|
||||||
|
async _authThenStart() {
|
||||||
|
if (!this._authProvider) {
|
||||||
|
@@ -35,7 +37,8 @@ export class SSEClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
@@ -59,10 +62,11 @@ export class SSEClientTransport {
|
||||||
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
||||||
|
}
|
||||||
|
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
|
||||||
|
- return new Headers({
|
||||||
|
- ...headers,
|
||||||
|
- ...extraHeaders
|
||||||
|
- });
|
||||||
|
+ const result = new Headers(extraHeaders);
|
||||||
|
+ for (const [name, value] of Object.entries(headers)) {
|
||||||
|
+ result.set(name, value);
|
||||||
|
+ }
|
||||||
|
+ return result;
|
||||||
|
}
|
||||||
|
_startOrAuth() {
|
||||||
|
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
|
||||||
|
@@ -145,7 +149,8 @@ export class SSEClientTransport {
|
||||||
|
authorizationCode,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new UnauthorizedError('Failed to authorize');
|
||||||
|
@@ -181,7 +186,8 @@ export class SSEClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new UnauthorizedError();
|
||||||
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
|
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
|
||||||
index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
|
index 624172aa24ae255a67c083f9c19053343e4a0581..88f7f5c3528d0ce6115efbebfdda98eba7077b8b 100644
|
||||||
--- a/dist/esm/client/streamableHttp.js
|
--- a/dist/esm/client/streamableHttp.js
|
||||||
+++ b/dist/esm/client/streamableHttp.js
|
+++ b/dist/esm/client/streamableHttp.js
|
||||||
@@ -1,5 +1,5 @@
|
@@ -1,5 +1,5 @@
|
||||||
@@ -320,7 +614,54 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
|||||||
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
|
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
|
||||||
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||||
// Default reconnection options for StreamableHTTP connections
|
// Default reconnection options for StreamableHTTP connections
|
||||||
@@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
|
@@ -29,7 +29,9 @@ export class StreamableHTTPClientTransport {
|
||||||
|
this._requestInit = opts?.requestInit;
|
||||||
|
this._authProvider = opts?.authProvider;
|
||||||
|
this._fetch = opts?.fetch;
|
||||||
|
- this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
|
||||||
|
+ this._resourceFetchWithInit = createFetchWithInit(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
||||||
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
||||||
|
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, oauthRequestInit);
|
||||||
|
this._sessionId = opts?.sessionId;
|
||||||
|
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
|
||||||
|
}
|
||||||
|
@@ -43,7 +45,8 @@ export class StreamableHTTPClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
@@ -70,10 +73,11 @@ export class StreamableHTTPClientTransport {
|
||||||
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
||||||
|
}
|
||||||
|
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
|
||||||
|
- return new Headers({
|
||||||
|
- ...headers,
|
||||||
|
- ...extraHeaders
|
||||||
|
- });
|
||||||
|
+ const result = new Headers(extraHeaders);
|
||||||
|
+ for (const [name, value] of Object.entries(headers)) {
|
||||||
|
+ result.set(name, value);
|
||||||
|
+ }
|
||||||
|
+ return result;
|
||||||
|
}
|
||||||
|
async _startOrAuthSse(options) {
|
||||||
|
const { resumptionToken } = options;
|
||||||
|
@@ -271,7 +275,8 @@ export class StreamableHTTPClientTransport {
|
||||||
|
authorizationCode,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new UnauthorizedError('Failed to authorize');
|
||||||
|
@@ -286,7 +291,38 @@ export class StreamableHTTPClientTransport {
|
||||||
this.onclose?.();
|
this.onclose?.();
|
||||||
}
|
}
|
||||||
async send(message, options) {
|
async send(message, options) {
|
||||||
@@ -359,7 +700,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
|||||||
const { resumptionToken, onresumptiontoken } = options || {};
|
const { resumptionToken, onresumptiontoken } = options || {};
|
||||||
if (resumptionToken) {
|
if (resumptionToken) {
|
||||||
// If we have at last event ID, we need to reconnect the SSE stream
|
// If we have at last event ID, we need to reconnect the SSE stream
|
||||||
@@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport {
|
@@ -294,6 +330,7 @@ export class StreamableHTTPClientTransport {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const headers = await this._commonHeaders();
|
const headers = await this._commonHeaders();
|
||||||
@@ -367,7 +708,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
|||||||
headers.set('content-type', 'application/json');
|
headers.set('content-type', 'application/json');
|
||||||
headers.set('accept', 'application/json, text/event-stream');
|
headers.set('accept', 'application/json, text/event-stream');
|
||||||
const init = {
|
const init = {
|
||||||
@@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport {
|
@@ -306,11 +343,20 @@ export class StreamableHTTPClientTransport {
|
||||||
const response = await (this._fetch ?? fetch)(this._url, init);
|
const response = await (this._fetch ?? fetch)(this._url, init);
|
||||||
// Handle session ID received during initialization
|
// Handle session ID received during initialization
|
||||||
const sessionId = response.headers.get('mcp-session-id');
|
const sessionId = response.headers.get('mcp-session-id');
|
||||||
@@ -389,7 +730,17 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
|||||||
if (response.status === 401 && this._authProvider) {
|
if (response.status === 401 && this._authProvider) {
|
||||||
// Prevent infinite recursion when server returns 401 after successful auth
|
// Prevent infinite recursion when server returns 401 after successful auth
|
||||||
if (this._hasCompletedAuthFlow) {
|
if (this._hasCompletedAuthFlow) {
|
||||||
@@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport {
|
@@ -323,7 +369,8 @@ export class StreamableHTTPClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetchWithInit
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
|
if (result !== 'AUTHORIZED') {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
@@ -331,7 +378,7 @@ export class StreamableHTTPClientTransport {
|
||||||
// Mark that we completed auth flow
|
// Mark that we completed auth flow
|
||||||
this._hasCompletedAuthFlow = true;
|
this._hasCompletedAuthFlow = true;
|
||||||
// Purposely _not_ awaited, so we don't call onerror twice
|
// Purposely _not_ awaited, so we don't call onerror twice
|
||||||
@@ -398,7 +749,14 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
|||||||
}
|
}
|
||||||
if (response.status === 403 && this._authProvider) {
|
if (response.status === 403 && this._authProvider) {
|
||||||
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
|
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
|
||||||
@@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport {
|
@@ -353,12 +400,13 @@ export class StreamableHTTPClientTransport {
|
||||||
|
serverUrl: this._url,
|
||||||
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||||
|
scope: this._scope,
|
||||||
|
- fetchFn: this._fetch
|
||||||
|
+ fetchFn: this._fetchWithInit,
|
||||||
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
||||||
|
});
|
||||||
if (result !== 'AUTHORIZED') {
|
if (result !== 'AUTHORIZED') {
|
||||||
throw new UnauthorizedError();
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
@@ -408,7 +766,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
|||||||
}
|
}
|
||||||
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
||||||
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
|
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
|
||||||
index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644
|
index bfa2b7120a0f50c569364ea5264e6f811076f44f..dec477d16a0fd796854542c1144279a6e86567f2 100644
|
||||||
--- a/dist/esm/shared/protocol.js
|
--- a/dist/esm/shared/protocol.js
|
||||||
+++ b/dist/esm/shared/protocol.js
|
+++ b/dist/esm/shared/protocol.js
|
||||||
@@ -740,7 +740,12 @@ export class Protocol {
|
@@ -740,7 +740,12 @@ export class Protocol {
|
||||||
|
|||||||
Reference in New Issue
Block a user