Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa723abbe0 | ||
|
|
410a6f33dc | ||
|
|
b324378b2c | ||
|
|
6d930ecae7 | ||
|
|
9c3fee1442 | ||
|
|
688752ea77 | ||
|
|
1b13b574f8 | ||
|
|
95bbd60a38 | ||
|
|
96ba6fd531 | ||
|
|
8592cf2d07 | ||
|
|
dd47b574b3 | ||
|
|
b4ae1327b5 | ||
|
|
b5762d53fd | ||
|
|
7957d9f577 | ||
|
|
4c89aed4d9 | ||
|
|
79547143a8 | ||
|
|
f980763381 | ||
|
|
d754395a9a | ||
|
|
df955cfdb5 | ||
|
|
5b884b55b5 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-client",
|
"name": "@modelcontextprotocol/inspector-client",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Client-side application for the Model Context Protocol inspector",
|
"description": "Client-side application for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ const OAuthCallback = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accessToken = await handleOAuthCallback(serverUrl, code);
|
const tokens = await handleOAuthCallback(serverUrl, code);
|
||||||
// Store the access token for future use
|
// Store both access and refresh tokens
|
||||||
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, accessToken);
|
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
|
||||||
|
if (tokens.refresh_token) {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
SESSION_KEYS.REFRESH_TOKEN,
|
||||||
|
tokens.refresh_token,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Redirect back to the main app with server URL to trigger auto-connect
|
// Redirect back to the main app with server URL to trigger auto-connect
|
||||||
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
|
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import pkceChallenge from "pkce-challenge";
|
import pkceChallenge from "pkce-challenge";
|
||||||
import { SESSION_KEYS } from "./constants";
|
import { SESSION_KEYS } from "./constants";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export interface OAuthMetadata {
|
export const OAuthMetadataSchema = z.object({
|
||||||
authorization_endpoint: string;
|
authorization_endpoint: z.string(),
|
||||||
token_endpoint: string;
|
token_endpoint: z.string(),
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
|
||||||
|
|
||||||
|
export const OAuthTokensSchema = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
refresh_token: z.string().optional(),
|
||||||
|
expires_in: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
|
||||||
|
|
||||||
export async function discoverOAuthMetadata(
|
export async function discoverOAuthMetadata(
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
@@ -15,10 +26,11 @@ export async function discoverOAuthMetadata(
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const metadata = await response.json();
|
const metadata = await response.json();
|
||||||
return {
|
const validatedMetadata = OAuthMetadataSchema.parse({
|
||||||
authorization_endpoint: metadata.authorization_endpoint,
|
authorization_endpoint: metadata.authorization_endpoint,
|
||||||
token_endpoint: metadata.token_endpoint,
|
token_endpoint: metadata.token_endpoint,
|
||||||
};
|
});
|
||||||
|
return validatedMetadata;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("OAuth metadata discovery failed:", error);
|
console.warn("OAuth metadata discovery failed:", error);
|
||||||
@@ -26,10 +38,11 @@ export async function discoverOAuthMetadata(
|
|||||||
|
|
||||||
// Fall back to default endpoints
|
// Fall back to default endpoints
|
||||||
const baseUrl = new URL(serverUrl);
|
const baseUrl = new URL(serverUrl);
|
||||||
return {
|
const defaultMetadata = {
|
||||||
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
|
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
|
||||||
token_endpoint: new URL("/token", baseUrl).toString(),
|
token_endpoint: new URL("/token", baseUrl).toString(),
|
||||||
};
|
};
|
||||||
|
return OAuthMetadataSchema.parse(defaultMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startOAuthFlow(serverUrl: string): Promise<string> {
|
export async function startOAuthFlow(serverUrl: string): Promise<string> {
|
||||||
@@ -60,7 +73,7 @@ export async function startOAuthFlow(serverUrl: string): Promise<string> {
|
|||||||
export async function handleOAuthCallback(
|
export async function handleOAuthCallback(
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
code: string,
|
code: string,
|
||||||
): Promise<string> {
|
): Promise<OAuthTokens> {
|
||||||
// Get stored code verifier
|
// Get stored code verifier
|
||||||
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
|
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
|
||||||
if (!codeVerifier) {
|
if (!codeVerifier) {
|
||||||
@@ -69,7 +82,6 @@ export async function handleOAuthCallback(
|
|||||||
|
|
||||||
// Discover OAuth endpoints
|
// Discover OAuth endpoints
|
||||||
const metadata = await discoverOAuthMetadata(serverUrl);
|
const metadata = await discoverOAuthMetadata(serverUrl);
|
||||||
|
|
||||||
// Exchange code for tokens
|
// Exchange code for tokens
|
||||||
const response = await fetch(metadata.token_endpoint, {
|
const response = await fetch(metadata.token_endpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -88,6 +100,35 @@ export async function handleOAuthCallback(
|
|||||||
throw new Error("Token exchange failed");
|
throw new Error("Token exchange failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const tokens = await response.json();
|
||||||
return data.access_token;
|
return OAuthTokensSchema.parse(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshAccessToken(
|
||||||
|
serverUrl: string,
|
||||||
|
): Promise<OAuthTokens> {
|
||||||
|
const refreshToken = sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN);
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error("No refresh token available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await discoverOAuthMetadata(serverUrl);
|
||||||
|
|
||||||
|
const response = await fetch(metadata.token_endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Token refresh failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await response.json();
|
||||||
|
return OAuthTokensSchema.parse(tokens);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export const SESSION_KEYS = {
|
|||||||
CODE_VERIFIER: "mcp_code_verifier",
|
CODE_VERIFIER: "mcp_code_verifier",
|
||||||
SERVER_URL: "mcp_server_url",
|
SERVER_URL: "mcp_server_url",
|
||||||
ACCESS_TOKEN: "mcp_access_token",
|
ACCESS_TOKEN: "mcp_access_token",
|
||||||
|
REFRESH_TOKEN: "mcp_refresh_token",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { startOAuthFlow } from "../auth";
|
import { startOAuthFlow, refreshAccessToken } from "../auth";
|
||||||
import { SESSION_KEYS } from "../constants";
|
import { SESSION_KEYS } from "../constants";
|
||||||
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
||||||
|
|
||||||
@@ -121,7 +121,49 @@ export function useConnection({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const connect = async () => {
|
const initiateOAuthFlow = async () => {
|
||||||
|
sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
|
||||||
|
sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
|
||||||
|
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||||
|
const redirectUrl = await startOAuthFlow(sseUrl);
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTokenRefresh = async () => {
|
||||||
|
try {
|
||||||
|
const tokens = await refreshAccessToken(sseUrl);
|
||||||
|
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
|
||||||
|
if (tokens.refresh_token) {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
SESSION_KEYS.REFRESH_TOKEN,
|
||||||
|
tokens.refresh_token,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tokens.access_token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token refresh failed:", error);
|
||||||
|
await initiateOAuthFlow();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthError = async (error: unknown) => {
|
||||||
|
if (error instanceof SseError && error.code === 401) {
|
||||||
|
if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
|
||||||
|
try {
|
||||||
|
await handleTokenRefresh();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token refresh failed:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await initiateOAuthFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
||||||
try {
|
try {
|
||||||
const client = new Client<Request, Notification, Result>(
|
const client = new Client<Request, Notification, Result>(
|
||||||
{
|
{
|
||||||
@@ -182,14 +224,15 @@ export function useConnection({
|
|||||||
await client.connect(clientTransport);
|
await client.connect(clientTransport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to connect to MCP server:", error);
|
console.error("Failed to connect to MCP server:", error);
|
||||||
if (error instanceof SseError && error.code === 401) {
|
const shouldRetry = await handleAuthError(error);
|
||||||
// Store the server URL for the callback handler
|
if (shouldRetry) {
|
||||||
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
return connect(undefined, retryCount + 1);
|
||||||
const redirectUrl = await startOAuthFlow(sseUrl);
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof SseError && error.code === 401) {
|
||||||
|
// Don't set error state if we're about to redirect for auth
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector",
|
"name": "@modelcontextprotocol/inspector",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Model Context Protocol inspector",
|
"description": "Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/inspector-client": "0.3.0",
|
"@modelcontextprotocol/inspector-client": "0.4.1",
|
||||||
"@modelcontextprotocol/inspector-server": "0.3.0",
|
"@modelcontextprotocol/inspector-server": "0.4.1",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"shell-quote": "^1.8.2",
|
"shell-quote": "^1.8.2",
|
||||||
"spawn-rx": "^5.1.0",
|
"spawn-rx": "^5.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@modelcontextprotocol/inspector-server",
|
"name": "@modelcontextprotocol/inspector-server",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Server-side application for the Model Context Protocol inspector",
|
"description": "Server-side application for the Model Context Protocol inspector",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||||
|
|||||||
@@ -181,4 +181,16 @@ app.get("/config", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
app.listen(PORT, () => {});
|
|
||||||
|
try {
|
||||||
|
const server = app.listen(PORT);
|
||||||
|
|
||||||
|
server.on("listening", () => {
|
||||||
|
const addr = server.address();
|
||||||
|
const port = typeof addr === "string" ? addr : addr?.port;
|
||||||
|
console.log(`Proxy server listening on port ${port}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user