Fix Claude Desktop MCP OAuth registration and resume flows (#173)
This commit is contained in:
parent
f58efea164
commit
db10ffa250
@ -1,5 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { normalizeAuthRedirect } from "@/lib/auth-redirect";
|
import {
|
||||||
|
getCurrentAuthRedirect,
|
||||||
|
getOAuthSignedQuery,
|
||||||
|
} from "@/lib/auth-redirect";
|
||||||
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
||||||
import {
|
import {
|
||||||
getFieldError as getSharedFieldError,
|
getFieldError as getSharedFieldError,
|
||||||
@ -11,11 +14,16 @@ export const authRedirectSearchSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function useAuthPageState(redirect: string | undefined) {
|
export function useAuthPageState(redirect: string | undefined) {
|
||||||
const redirectTo = normalizeAuthRedirect(redirect);
|
const redirectTo = getCurrentAuthRedirect(redirect);
|
||||||
|
const oauthQuery =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? getOAuthSignedQuery(window.location.search)
|
||||||
|
: null;
|
||||||
const isHostedMode = isHostedClientAuthMode();
|
const isHostedMode = isHostedClientAuthMode();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirectTo,
|
redirectTo,
|
||||||
|
oauthQuery,
|
||||||
isHostedMode,
|
isHostedMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/lib/auth-redirect.test.ts
Normal file
63
src/lib/auth-redirect.test.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
getAuthRedirectFromSearch,
|
||||||
|
getOAuthAuthorizeRedirectFromSearch,
|
||||||
|
getOAuthSignedQuery,
|
||||||
|
normalizeAuthRedirect,
|
||||||
|
} from "./auth-redirect";
|
||||||
|
|
||||||
|
const oauthSearch = new URLSearchParams({
|
||||||
|
response_type: "code",
|
||||||
|
client_id: "claude-client",
|
||||||
|
redirect_uri: "https://claude.ai/api/mcp/auth_callback",
|
||||||
|
scope: "offline_access mcp",
|
||||||
|
state: "state-123",
|
||||||
|
code_challenge: "challenge-123",
|
||||||
|
code_challenge_method: "S256",
|
||||||
|
resource: "https://app.openseo.so/mcp",
|
||||||
|
exp: "1778271800",
|
||||||
|
sig: "signed-value",
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
describe("auth redirect helpers", () => {
|
||||||
|
it("defaults unsafe or missing redirects to the app root", () => {
|
||||||
|
expect(normalizeAuthRedirect(undefined)).toBe("/");
|
||||||
|
expect(normalizeAuthRedirect("https://evil.example/app")).toBe("/");
|
||||||
|
expect(normalizeAuthRedirect("//evil.example/app")).toBe("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves safe internal redirects", () => {
|
||||||
|
expect(getAuthRedirectFromSearch("", "/app")).toBe("/app");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts Better Auth signed OAuth query parameters through sig", () => {
|
||||||
|
const signedQuery = getOAuthSignedQuery(
|
||||||
|
`${oauthSearch}&ignored_after_sig=true`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(signedQuery).toBe(oauthSearch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds an internal OAuth authorize continuation redirect", () => {
|
||||||
|
const redirect = getOAuthAuthorizeRedirectFromSearch(oauthSearch);
|
||||||
|
|
||||||
|
expect(redirect).toBe(`/api/auth/oauth2/authorize?${oauthSearch}`);
|
||||||
|
expect(redirect).toContain("client_id=claude-client");
|
||||||
|
expect(redirect).toContain(
|
||||||
|
"redirect_uri=https%3A%2F%2Fclaude.ai%2Fapi%2Fmcp%2Fauth_callback",
|
||||||
|
);
|
||||||
|
expect(redirect).toContain("scope=offline_access+mcp");
|
||||||
|
expect(redirect).toContain("state=state-123");
|
||||||
|
expect(redirect).toContain("code_challenge=challenge-123");
|
||||||
|
expect(redirect).toContain("code_challenge_method=S256");
|
||||||
|
expect(redirect).toContain("resource=https%3A%2F%2Fapp.openseo.so%2Fmcp");
|
||||||
|
expect(redirect).toContain("exp=1778271800");
|
||||||
|
expect(redirect).toContain("sig=signed-value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers OAuth continuation over a generic redirect", () => {
|
||||||
|
expect(getAuthRedirectFromSearch(oauthSearch, "/app")).toBe(
|
||||||
|
`/api/auth/oauth2/authorize?${oauthSearch}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,3 +1,7 @@
|
|||||||
|
const OAUTH_AUTHORIZE_PATH = "/api/auth/oauth2/authorize";
|
||||||
|
const OAUTH_SIGNED_QUERY_END = "sig";
|
||||||
|
const OAUTH_AUTHORIZE_MARKERS = ["response_type", "client_id", "redirect_uri"];
|
||||||
|
|
||||||
export function normalizeAuthRedirect(value: string | null | undefined) {
|
export function normalizeAuthRedirect(value: string | null | undefined) {
|
||||||
if (!value || !value.startsWith("/") || value.startsWith("//")) {
|
if (!value || !value.startsWith("/") || value.startsWith("//")) {
|
||||||
return "/";
|
return "/";
|
||||||
@ -6,6 +10,56 @@ export function normalizeAuthRedirect(value: string | null | undefined) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOAuthSignedQuery(search: string | null | undefined) {
|
||||||
|
if (!search) return null;
|
||||||
|
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
if (
|
||||||
|
!params.has(OAUTH_SIGNED_QUERY_END) ||
|
||||||
|
!OAUTH_AUTHORIZE_MARKERS.every((marker) => params.has(marker))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better Auth signs the authorize params it appends to `loginPage`.
|
||||||
|
// Preserve only the signed segment through `sig`; any later params belong to
|
||||||
|
// the app page URL and must not be folded into the OAuth continuation.
|
||||||
|
const signedParams = new URLSearchParams();
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
signedParams.append(key, value);
|
||||||
|
if (key === OAUTH_SIGNED_QUERY_END) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return signedParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOAuthAuthorizeRedirectFromSearch(
|
||||||
|
search: string | null | undefined,
|
||||||
|
) {
|
||||||
|
const signedQuery = getOAuthSignedQuery(search);
|
||||||
|
return signedQuery ? `${OAUTH_AUTHORIZE_PATH}?${signedQuery}` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthRedirectFromSearch(
|
||||||
|
search: string | null | undefined,
|
||||||
|
redirect: string | null | undefined,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
getOAuthAuthorizeRedirectFromSearch(search) ??
|
||||||
|
normalizeAuthRedirect(redirect)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentAuthRedirect(
|
||||||
|
redirect: string | null | undefined,
|
||||||
|
location: Pick<Location, "search"> | null | undefined = typeof window !==
|
||||||
|
"undefined"
|
||||||
|
? window.location
|
||||||
|
: null,
|
||||||
|
) {
|
||||||
|
return getAuthRedirectFromSearch(location?.search, redirect);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentAuthRedirectFromHref(href: string) {
|
export function getCurrentAuthRedirectFromHref(href: string) {
|
||||||
const url = new URL(href, "https://openseo.local");
|
const url = new URL(href, "https://openseo.local");
|
||||||
return normalizeAuthRedirect(`${url.pathname}${url.search}${url.hash}`);
|
return normalizeAuthRedirect(`${url.pathname}${url.search}${url.hash}`);
|
||||||
|
|||||||
@ -26,7 +26,9 @@ export const Route = createFileRoute("/_auth/sign-in")({
|
|||||||
|
|
||||||
function SignInPage() {
|
function SignInPage() {
|
||||||
const search = Route.useSearch();
|
const search = Route.useSearch();
|
||||||
const { redirectTo, isHostedMode } = useAuthPageState(search.redirect);
|
const { redirectTo, oauthQuery, isHostedMode } = useAuthPageState(
|
||||||
|
search.redirect,
|
||||||
|
);
|
||||||
const [verificationEmail, setVerificationEmail] = useState<string | null>(
|
const [verificationEmail, setVerificationEmail] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@ -52,6 +54,7 @@ function SignInPage() {
|
|||||||
email,
|
email,
|
||||||
password: value.password,
|
password: value.password,
|
||||||
callbackURL: redirectTo,
|
callbackURL: redirectTo,
|
||||||
|
...(oauthQuery ? { oauth_query: oauthQuery } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.error) {
|
if (!result.error) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
} from "@/client/features/auth/AuthPage";
|
} from "@/client/features/auth/AuthPage";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
||||||
import { normalizeAuthRedirect } from "@/lib/auth-redirect";
|
import { getCurrentAuthRedirect } from "@/lib/auth-redirect";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth")({
|
export const Route = createFileRoute("/_auth")({
|
||||||
validateSearch: authRedirectSearchSchema,
|
validateSearch: authRedirectSearchSchema,
|
||||||
@ -18,7 +18,7 @@ function AuthPageLayout() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: session, isPending } = useSession();
|
const { data: session, isPending } = useSession();
|
||||||
const isHostedMode = isHostedClientAuthMode();
|
const isHostedMode = isHostedClientAuthMode();
|
||||||
const redirectTo = normalizeAuthRedirect(search.redirect);
|
const redirectTo = getCurrentAuthRedirect(search.redirect);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
|
|||||||
@ -5,6 +5,29 @@ import { isHostedAuthMode } from "@/lib/auth-mode";
|
|||||||
import { getMcpResource } from "@/lib/oauth-resource";
|
import { getMcpResource } from "@/lib/oauth-resource";
|
||||||
|
|
||||||
const TOKEN_PATH = "/api/auth/oauth2/token";
|
const TOKEN_PATH = "/api/auth/oauth2/token";
|
||||||
|
const REGISTER_PATH = "/api/auth/oauth2/register";
|
||||||
|
const PUBLIC_CLIENT_AUTH_METHOD = "none";
|
||||||
|
|
||||||
|
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRequestAuthContext(request: Request) {
|
||||||
|
return Boolean(
|
||||||
|
request.headers.get("authorization") || request.headers.get("cookie"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestWithReplacedBody(request: Request, body: BodyInit) {
|
||||||
|
const headers = new Headers(request.headers);
|
||||||
|
headers.delete("content-length");
|
||||||
|
|
||||||
|
return new Request(request.url, {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Inject RFC 8707 `resource` into /oauth2/token requests when the client
|
// Inject RFC 8707 `resource` into /oauth2/token requests when the client
|
||||||
// omitted it. Some MCP clients (notably codex as of 2026-05) skip the
|
// omitted it. Some MCP clients (notably codex as of 2026-05) skip the
|
||||||
@ -35,11 +58,48 @@ export async function maybeInjectMcpResource(
|
|||||||
|
|
||||||
params.set("resource", getMcpResource(getHostedBaseUrl()));
|
params.set("resource", getMcpResource(getHostedBaseUrl()));
|
||||||
|
|
||||||
return new Request(request.url, {
|
return requestWithReplacedBody(request, params.toString());
|
||||||
method: request.method,
|
}
|
||||||
headers: request.headers,
|
|
||||||
body: params.toString(),
|
// Some hosted MCP clients attempt unauthenticated DCR while sending a
|
||||||
});
|
// confidential-client auth method. Better Auth only permits unauthenticated DCR
|
||||||
|
// for public clients, so normalize that case to the compatible public shape.
|
||||||
|
// Claude Desktop hit this path during connector setup: it had no session or
|
||||||
|
// registration bearer token, but sent a non-`none` token endpoint auth method.
|
||||||
|
export async function maybeDefaultMcpClientRegistrationAuthMethod(
|
||||||
|
request: Request,
|
||||||
|
): Promise<Request> {
|
||||||
|
if (request.method !== "POST") return request;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.pathname !== REGISTER_PATH) return request;
|
||||||
|
|
||||||
|
const contentType = request.headers.get("content-type") ?? "";
|
||||||
|
if (!contentType.includes("application/json")) return request;
|
||||||
|
|
||||||
|
let body: unknown;
|
||||||
|
try {
|
||||||
|
body = await request.clone().json();
|
||||||
|
} catch {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isJsonObject(body)) return request;
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasRequestAuthContext(request) ||
|
||||||
|
body.token_endpoint_auth_method === PUBLIC_CLIENT_AUTH_METHOD
|
||||||
|
) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestWithReplacedBody(
|
||||||
|
request,
|
||||||
|
JSON.stringify({
|
||||||
|
...body,
|
||||||
|
token_endpoint_auth_method: PUBLIC_CLIENT_AUTH_METHOD,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAuthRequest(request: Request) {
|
async function handleAuthRequest(request: Request) {
|
||||||
@ -56,7 +116,12 @@ async function handleAuthRequest(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
return auth.handler(await maybeInjectMcpResource(request));
|
const requestWithRegistrationDefaults =
|
||||||
|
await maybeDefaultMcpClientRegistrationAuthMethod(request);
|
||||||
|
const requestWithResource = await maybeInjectMcpResource(
|
||||||
|
requestWithRegistrationDefaults,
|
||||||
|
);
|
||||||
|
return auth.handler(requestWithResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute("/api/auth/$")({
|
export const Route = createFileRoute("/api/auth/$")({
|
||||||
|
|||||||
@ -23,6 +23,7 @@ describe("maybeInjectMcpResource", () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
"Content-Length": "13",
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
@ -36,6 +37,7 @@ describe("maybeInjectMcpResource", () => {
|
|||||||
expect(params.get("resource")).toBe("https://open-seo.test/mcp");
|
expect(params.get("resource")).toBe("https://open-seo.test/mcp");
|
||||||
expect(params.get("grant_type")).toBe("authorization_code");
|
expect(params.get("grant_type")).toBe("authorization_code");
|
||||||
expect(params.get("code")).toBe("code_123");
|
expect(params.get("code")).toBe("code_123");
|
||||||
|
expect(result.headers.has("content-length")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("leaves token requests alone when a resource is already present", async () => {
|
it("leaves token requests alone when a resource is already present", async () => {
|
||||||
@ -71,6 +73,7 @@ describe("maybeInjectMcpResource", () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": "13",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ grant_type: "authorization_code" }),
|
body: JSON.stringify({ grant_type: "authorization_code" }),
|
||||||
}),
|
}),
|
||||||
@ -81,3 +84,98 @@ describe("maybeInjectMcpResource", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("maybeDefaultMcpClientRegistrationAuthMethod", () => {
|
||||||
|
it("defaults JSON dynamic client registration to a public client when omitted", async () => {
|
||||||
|
const { maybeDefaultMcpClientRegistrationAuthMethod } =
|
||||||
|
await import("@/routes/api/auth/$");
|
||||||
|
const request = new Request(
|
||||||
|
"https://open-seo.test/api/auth/oauth2/register",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_name: "Claude",
|
||||||
|
redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await maybeDefaultMcpClientRegistrationAuthMethod(request);
|
||||||
|
const body = await result.json();
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
client_name: "Claude",
|
||||||
|
redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
|
||||||
|
token_endpoint_auth_method: "none",
|
||||||
|
});
|
||||||
|
expect(result.headers.has("content-length")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forces unauthenticated dynamic client registration to a public client", async () => {
|
||||||
|
const { maybeDefaultMcpClientRegistrationAuthMethod } =
|
||||||
|
await import("@/routes/api/auth/$");
|
||||||
|
const request = new Request(
|
||||||
|
"https://open-seo.test/api/auth/oauth2/register",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token_endpoint_auth_method: "client_secret_basic",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await maybeDefaultMcpClientRegistrationAuthMethod(request);
|
||||||
|
const body = await result.json();
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
token_endpoint_auth_method: "none",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves explicit registration auth methods alone when auth context exists", async () => {
|
||||||
|
const { maybeDefaultMcpClientRegistrationAuthMethod } =
|
||||||
|
await import("@/routes/api/auth/$");
|
||||||
|
const request = new Request(
|
||||||
|
"https://open-seo.test/api/auth/oauth2/register",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Cookie: "better-auth.session_token=session_123",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token_endpoint_auth_method: "client_secret_basic",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
maybeDefaultMcpClientRegistrationAuthMethod(request),
|
||||||
|
).resolves.toBe(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves malformed JSON registration requests for the auth handler", async () => {
|
||||||
|
const { maybeDefaultMcpClientRegistrationAuthMethod } =
|
||||||
|
await import("@/routes/api/auth/$");
|
||||||
|
const request = new Request(
|
||||||
|
"https://open-seo.test/api/auth/oauth2/register",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: "{",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
maybeDefaultMcpClientRegistrationAuthMethod(request),
|
||||||
|
).resolves.toBe(request);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -68,20 +68,22 @@ function unauthorizedResponse(resource: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isPublicMcpRequest(request: Request) {
|
async function getMcpJsonRpcMethod(request: Request) {
|
||||||
if (request.method === "OPTIONS") return true;
|
if (request.method !== "POST") return null;
|
||||||
if (request.method !== "POST") return false;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body: McpJsonRpcRequest = await request.clone().json();
|
const body: McpJsonRpcRequest = await request.clone().json();
|
||||||
return (
|
return typeof body.method === "string" ? body.method : null;
|
||||||
typeof body.method === "string" && PUBLIC_MCP_METHODS.has(body.method)
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPublicMcpRequest(request: Request, jsonRpcMethod: string | null) {
|
||||||
|
if (request.method === "OPTIONS") return true;
|
||||||
|
return jsonRpcMethod != null && PUBLIC_MCP_METHODS.has(jsonRpcMethod);
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleMcpRequest(
|
export async function handleMcpRequest(
|
||||||
request: Request,
|
request: Request,
|
||||||
env: { AUTH_MODE?: unknown },
|
env: { AUTH_MODE?: unknown },
|
||||||
@ -89,6 +91,7 @@ export async function handleMcpRequest(
|
|||||||
) {
|
) {
|
||||||
const authMode =
|
const authMode =
|
||||||
typeof env.AUTH_MODE === "string" ? env.AUTH_MODE : undefined;
|
typeof env.AUTH_MODE === "string" ? env.AUTH_MODE : undefined;
|
||||||
|
const jsonRpcMethod = await getMcpJsonRpcMethod(request);
|
||||||
|
|
||||||
if (!isHostedAuthMode(authMode)) {
|
if (!isHostedAuthMode(authMode)) {
|
||||||
return new Response("Not found", { status: 404 });
|
return new Response("Not found", { status: 404 });
|
||||||
@ -107,7 +110,7 @@ export async function handleMcpRequest(
|
|||||||
const organizationIdClaim = getMcpOrganizationIdClaim(baseUrl);
|
const organizationIdClaim = getMcpOrganizationIdClaim(baseUrl);
|
||||||
const server = createOpenSeoMcpServer();
|
const server = createOpenSeoMcpServer();
|
||||||
|
|
||||||
if (await isPublicMcpRequest(request)) {
|
if (isPublicMcpRequest(request, jsonRpcMethod)) {
|
||||||
return createMcpHandler(server, {
|
return createMcpHandler(server, {
|
||||||
route: MCP_ROUTE,
|
route: MCP_ROUTE,
|
||||||
enableJsonResponse: true,
|
enableJsonResponse: true,
|
||||||
|
|||||||
@ -23,10 +23,13 @@ export async function mcpProtectedResourceMetadataResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = getHostedBaseUrl();
|
const baseUrl = getHostedBaseUrl();
|
||||||
|
const resource = getMcpResource(baseUrl);
|
||||||
|
const authorizationServer = `${baseUrl}/api/auth`;
|
||||||
|
|
||||||
const metadata =
|
const metadata =
|
||||||
await getOAuthProviderResourceActions().getProtectedResourceMetadata({
|
await getOAuthProviderResourceActions().getProtectedResourceMetadata({
|
||||||
resource: getMcpResource(baseUrl),
|
resource,
|
||||||
authorization_servers: [`${baseUrl}/api/auth`],
|
authorization_servers: [authorizationServer],
|
||||||
scopes_supported: [...MCP_OAUTH_SCOPES],
|
scopes_supported: [...MCP_OAUTH_SCOPES],
|
||||||
resource_name: "OpenSEO MCP",
|
resource_name: "OpenSEO MCP",
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user