working metadata fetch
This commit is contained in:
parent
b2fa99668e
commit
73fde03aa4
7 changed files with 63 additions and 112 deletions
|
|
@ -19,7 +19,7 @@ import {
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
|
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
|
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
|
||||||
import { AuthDebuggerState } from "./lib/auth-types";
|
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
|
||||||
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
|
||||||
import React, {
|
import React, {
|
||||||
Suspense,
|
Suspense,
|
||||||
|
|
@ -121,20 +121,7 @@ const App = () => {
|
||||||
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
|
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
|
||||||
|
|
||||||
// Auth debugger state
|
// Auth debugger state
|
||||||
const [authState, setAuthState] = useState<AuthDebuggerState>({
|
const [authState, setAuthState] = useState<AuthDebuggerState>(EMPTY_DEBUGGER_STATE);
|
||||||
isInitiatingAuth: false,
|
|
||||||
oauthTokens: null,
|
|
||||||
loading: true,
|
|
||||||
oauthStep: "metadata_discovery",
|
|
||||||
oauthMetadata: null,
|
|
||||||
resourceMetadata: null,
|
|
||||||
oauthClientInfo: null,
|
|
||||||
authorizationUrl: null,
|
|
||||||
authorizationCode: "",
|
|
||||||
latestError: null,
|
|
||||||
statusMessage: null,
|
|
||||||
validationError: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper function to update specific auth state properties
|
// Helper function to update specific auth state properties
|
||||||
const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
|
const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
|
import { DebugInspectorOAuthClientProvider } from "../lib/auth";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { AuthDebuggerState } from "../lib/auth-types";
|
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "../lib/auth-types";
|
||||||
import { OAuthFlowProgress } from "./OAuthFlowProgress";
|
import { OAuthFlowProgress } from "./OAuthFlowProgress";
|
||||||
import { OAuthStateMachine } from "../lib/oauth-state-machine";
|
import { OAuthStateMachine } from "../lib/oauth-state-machine";
|
||||||
|
|
||||||
|
|
@ -178,14 +178,7 @@ const AuthDebugger = ({
|
||||||
);
|
);
|
||||||
serverAuthProvider.clear();
|
serverAuthProvider.clear();
|
||||||
updateAuthState({
|
updateAuthState({
|
||||||
oauthTokens: null,
|
...EMPTY_DEBUGGER_STATE,
|
||||||
oauthStep: "metadata_discovery",
|
|
||||||
latestError: null,
|
|
||||||
oauthClientInfo: null,
|
|
||||||
authorizationCode: "",
|
|
||||||
authorizationUrl: "",
|
|
||||||
validationError: null,
|
|
||||||
oauthMetadata: null,
|
|
||||||
statusMessage: {
|
statusMessage: {
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "OAuth tokens cleared successfully",
|
message: "OAuth tokens cleared successfully",
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ interface OAuthFlowProgressProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps: Array<OAuthStep> = [
|
const steps: Array<OAuthStep> = [
|
||||||
"resource_metadata_discovery",
|
|
||||||
"metadata_discovery",
|
"metadata_discovery",
|
||||||
"client_registration",
|
"client_registration",
|
||||||
"authorization_redirect",
|
"authorization_redirect",
|
||||||
|
|
@ -139,6 +138,19 @@ export const OAuthFlowProgress = ({
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
|
{authState.resourceMetadataError && (
|
||||||
|
<div className="mt-2 p-3 border border-yellow-300 bg-yellow-50 rounded-md">
|
||||||
|
<p className="text-sm font-small text-yellow-700">
|
||||||
|
Failed to retrieve resource metadata, falling back to /.well-known/oauth-authorization-server:
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-yellow-600 mt-1">
|
||||||
|
{authState.resourceMetadataError.message}
|
||||||
|
{authState.resourceMetadataError instanceof TypeError
|
||||||
|
? " (This could indicate the endpoint doesn't exist or does not have CORS configured)"
|
||||||
|
: authState.resourceMetadataError['status'] && ` (${authState.resourceMetadataError['status']})`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{provider.getServerMetadata() && (
|
{provider.getServerMetadata() && (
|
||||||
<details className="text-xs mt-2">
|
<details className="text-xs mt-2">
|
||||||
<summary className="cursor-pointer text-muted-foreground font-medium">
|
<summary className="cursor-pointer text-muted-foreground font-medium">
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import {
|
||||||
auth,
|
auth,
|
||||||
} from "@modelcontextprotocol/sdk/client/auth.js";
|
} from "@modelcontextprotocol/sdk/client/auth.js";
|
||||||
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
|
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
|
||||||
|
|
||||||
// Type the mocked functions properly
|
// Type the mocked functions properly
|
||||||
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
|
const mockDiscoverOAuthMetadata = discoverOAuthMetadata as jest.MockedFunction<
|
||||||
|
|
@ -84,19 +85,7 @@ Object.defineProperty(window, "location", {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("AuthDebugger", () => {
|
describe("AuthDebugger", () => {
|
||||||
const defaultAuthState = {
|
const defaultAuthState = EMPTY_DEBUGGER_STATE;
|
||||||
isInitiatingAuth: false,
|
|
||||||
oauthTokens: null,
|
|
||||||
loading: false,
|
|
||||||
oauthStep: "metadata_discovery" as const,
|
|
||||||
oauthMetadata: null,
|
|
||||||
oauthClientInfo: null,
|
|
||||||
authorizationUrl: null,
|
|
||||||
authorizationCode: "",
|
|
||||||
latestError: null,
|
|
||||||
statusMessage: null,
|
|
||||||
validationError: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
serverUrl: "https://example.com",
|
serverUrl: "https://example.com",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
OAuthClientInformationFull,
|
OAuthClientInformationFull,
|
||||||
OAuthClientInformation,
|
OAuthClientInformation,
|
||||||
OAuthTokens,
|
OAuthTokens,
|
||||||
|
OAuthProtectedResourceMetadata,
|
||||||
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
|
|
||||||
// OAuth flow steps
|
// OAuth flow steps
|
||||||
|
|
@ -28,8 +29,8 @@ export interface AuthDebuggerState {
|
||||||
oauthTokens: OAuthTokens | null;
|
oauthTokens: OAuthTokens | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
oauthStep: OAuthStep;
|
oauthStep: OAuthStep;
|
||||||
// TODO: use sdk type
|
resourceMetadata: OAuthProtectedResourceMetadata | null;
|
||||||
resourceMetadata: object | null;
|
resourceMetadataError: Error | { status: number; statusText: string; message: string } | null;
|
||||||
oauthMetadata: OAuthMetadata | null;
|
oauthMetadata: OAuthMetadata | null;
|
||||||
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
|
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
|
||||||
authorizationUrl: string | null;
|
authorizationUrl: string | null;
|
||||||
|
|
@ -38,3 +39,19 @@ export interface AuthDebuggerState {
|
||||||
statusMessage: StatusMessage | null;
|
statusMessage: StatusMessage | null;
|
||||||
validationError: string | null;
|
validationError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
|
||||||
|
isInitiatingAuth: false,
|
||||||
|
oauthTokens: null,
|
||||||
|
loading: true,
|
||||||
|
oauthStep: "metadata_discovery",
|
||||||
|
oauthMetadata: null,
|
||||||
|
resourceMetadata: null,
|
||||||
|
resourceMetadataError: null,
|
||||||
|
oauthClientInfo: null,
|
||||||
|
authorizationUrl: null,
|
||||||
|
authorizationCode: "",
|
||||||
|
latestError: null,
|
||||||
|
statusMessage: null,
|
||||||
|
validationError: null,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ import {
|
||||||
registerClient,
|
registerClient,
|
||||||
startAuthorization,
|
startAuthorization,
|
||||||
exchangeAuthorization,
|
exchangeAuthorization,
|
||||||
|
discoverOAuthProtectedResourceMetadata,
|
||||||
} from "@modelcontextprotocol/sdk/client/auth.js";
|
} from "@modelcontextprotocol/sdk/client/auth.js";
|
||||||
import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
|
import { OAuthMetadataSchema, OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||||
|
|
||||||
export interface StateMachineContext {
|
export interface StateMachineContext {
|
||||||
state: AuthDebuggerState;
|
state: AuthDebuggerState;
|
||||||
|
|
@ -20,29 +21,28 @@ export interface StateTransition {
|
||||||
execute: (context: StateMachineContext) => Promise<void>;
|
execute: (context: StateMachineContext) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchProtectedResourceMetadata = async (serverUrl: string): Promise<object> => {
|
|
||||||
// TODO: use sdk
|
|
||||||
const url = new URL("/.well-known/oauth-protected-resource", serverUrl);
|
|
||||||
const response = await fetch(url);
|
|
||||||
const resourceMetadata = await response.json();
|
|
||||||
|
|
||||||
return resourceMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State machine transitions
|
// State machine transitions
|
||||||
export const oauthTransitions: Record<OAuthStep, StateTransition> = {
|
export const oauthTransitions: Record<OAuthStep, StateTransition> = {
|
||||||
metadata_discovery: {
|
metadata_discovery: {
|
||||||
canTransition: async () => true,
|
canTransition: async () => true,
|
||||||
execute: async (context) => {
|
execute: async (context) => {
|
||||||
const authServerUrl = context.serverUrl;
|
let authServerUrl = context.serverUrl;
|
||||||
// try {
|
let resourceMetadata: OAuthProtectedResourceMetadata | null = null;
|
||||||
// const resourceMetadata = await fetchProtectedResourceMetadata(context.serverUrl);
|
let resourceMetadataError: Error | null = null;
|
||||||
// if (resourceMetadata && resourceMetadata) {
|
try {
|
||||||
// authServerUrl = resourceMetadata
|
resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl);
|
||||||
// }
|
if (resourceMetadata && resourceMetadata.authorization_servers?.length) {
|
||||||
// } catch (_error) {
|
authServerUrl = resourceMetadata.authorization_servers[0];
|
||||||
// // pass
|
}
|
||||||
// }
|
} catch (e) {
|
||||||
|
console.info(`Failed to find protected resource metadata: ${e}`);
|
||||||
|
console.log(e);
|
||||||
|
if (e instanceof Error) {
|
||||||
|
resourceMetadataError = e;
|
||||||
|
} else {
|
||||||
|
resourceMetadataError = new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const metadata = await discoverOAuthMetadata(authServerUrl);
|
const metadata = await discoverOAuthMetadata(authServerUrl);
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
|
|
@ -52,6 +52,7 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
|
||||||
context.provider.saveServerMetadata(parsedMetadata);
|
context.provider.saveServerMetadata(parsedMetadata);
|
||||||
context.updateState({
|
context.updateState({
|
||||||
resourceMetadata,
|
resourceMetadata,
|
||||||
|
resourceMetadataError,
|
||||||
oauthMetadata: parsedMetadata,
|
oauthMetadata: parsedMetadata,
|
||||||
oauthStep: "client_registration",
|
oauthStep: "client_registration",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
58
package-lock.json
generated
58
package-lock.json
generated
|
|
@ -17,7 +17,7 @@
|
||||||
"@modelcontextprotocol/inspector-cli": "^0.13.0",
|
"@modelcontextprotocol/inspector-cli": "^0.13.0",
|
||||||
"@modelcontextprotocol/inspector-client": "^0.13.0",
|
"@modelcontextprotocol/inspector-client": "^0.13.0",
|
||||||
"@modelcontextprotocol/inspector-server": "^0.13.0",
|
"@modelcontextprotocol/inspector-server": "^0.13.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.5",
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"open": "^10.1.0",
|
"open": "^10.1.0",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
|
|
@ -2005,12 +2005,11 @@
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.11.5",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.5.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.0.tgz",
|
||||||
"integrity": "sha512-gS7Q7IHpKxjVaNLMUZyTtatZ63ca3h418zPPntAhu/MvG5yfz/8HMcDAOpvpQfx3V3dsw9QQxk8RuFNrQhLlgA==",
|
"integrity": "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^6.12.6",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.5",
|
"cross-spawn": "^7.0.5",
|
||||||
|
|
@ -2026,28 +2025,6 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
|
|
||||||
"version": "8.17.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-deep-equal": "^3.1.3",
|
|
||||||
"fast-uri": "^3.0.1",
|
|
||||||
"json-schema-traverse": "^1.0.0",
|
|
||||||
"require-from-string": "^2.0.2"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -5719,22 +5696,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
|
||||||
"version": "3.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
|
||||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/fastify"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/fastify"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
|
|
@ -9064,15 +9025,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/require-from-string": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/requires-port": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue