From 66b1b7344840d1b880c65e0d5833082a8d9f5d54 Mon Sep 17 00:00:00 2001
From: = <1936278+evalstate@users.noreply.github.com>
Date: Sun, 1 Dec 2024 10:24:12 +0000
Subject: [PATCH 01/21] Render HTML5 Audio Player if Tool Result resource
mimetype is audio.
---
client/src/components/ToolsTab.tsx | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index 7b85ccc..1645422 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -83,9 +83,19 @@ const ToolsTab = ({
/>
)}
{item.type === "resource" && (
-
- {JSON.stringify(item.resource, null, 2)}
-
+ item.resource?.mimeType?.startsWith("audio/") ? (
+
+ ) : (
+
+ {JSON.stringify(item.resource, null, 2)}
+
+ )
)}
))}
From 068d21387a237c346a3cf6155e9e583990ce0e0e Mon Sep 17 00:00:00 2001
From: = <1936278+evalstate@users.noreply.github.com>
Date: Sun, 1 Dec 2024 12:47:50 +0000
Subject: [PATCH 02/21] extended type for "audio" update to spec
---
client/src/components/ToolsTab.tsx | 41 ++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index 1645422..bea46a5 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -7,13 +7,41 @@ import { Textarea } from "@/components/ui/textarea";
import {
ListToolsResult,
Tool,
- CallToolResultSchema,
+ TextContentSchema,
+ ImageContentSchema,
+ ResultSchema,
+ EmbeddedResourceSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react";
import { useState } from "react";
import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { z } from "zod";
+
+// Define the AudioContent schema
+export const AudioContentSchema = z.object({
+ type: z.literal("audio"),
+ data: z.string().base64(),
+ mimeType: z.string(),
+}).passthrough();
+
+// Extend the CallToolResult schema to include audio content
+export const ExtendedCallToolResultSchema = ResultSchema.extend({
+ content: z.array(
+ z.discriminatedUnion("type", [
+ TextContentSchema,
+ ImageContentSchema,
+ AudioContentSchema,
+ EmbeddedResourceSchema,
+ ])
+ ),
+ isError: z.boolean().default(false).optional(),
+});
+
+// Export the types
+export type AudioContent = z.infer;
+export type ExtendedCallToolResult = z.infer;
const ToolsTab = ({
tools,
@@ -40,7 +68,7 @@ const ToolsTab = ({
if (!toolResult) return null;
if ("content" in toolResult) {
- const parsedResult = CallToolResultSchema.safeParse(toolResult);
+ const parsedResult = ExtendedCallToolResultSchema.safeParse(toolResult);
if (!parsedResult.success) {
return (
<>
@@ -82,6 +110,15 @@ const ToolsTab = ({
className="max-w-full h-auto"
/>
)}
+ {item.type === "audio" && (
+
+ )}
{item.type === "resource" && (
item.resource?.mimeType?.startsWith("audio/") ? (
)}
- {item.type === "resource" && (
- item.resource?.mimeType?.startsWith("audio/") ? (
+ {item.type === "resource" &&
+ (item.resource?.mimeType?.startsWith("audio/") ? (
{JSON.stringify(item.resource, null, 2)}
- )
- )}
+ ))}
))}
>
From d0ad677784bc636c5becf9c1b3257e5c56ce1140 Mon Sep 17 00:00:00 2001
From: Jerome
Date: Mon, 3 Feb 2025 12:24:52 +1300
Subject: [PATCH 08/21] Update ToolsTab.tsx
Linting on originally approved commit
---
client/src/components/ToolsTab.tsx | 45 ++----------------------------
1 file changed, 2 insertions(+), 43 deletions(-)
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index 33df3e5..a3a7ff2 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -7,45 +7,13 @@ import { Textarea } from "@/components/ui/textarea";
import {
ListToolsResult,
Tool,
- TextContentSchema,
- ImageContentSchema,
- ResultSchema,
- EmbeddedResourceSchema,
+ CallToolResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
-import { z } from "zod";
-
-// Define the AudioContent schema
-export const AudioContentSchema = z
- .object({
- type: z.literal("audio"),
- data: z.string().base64(),
- mimeType: z.string(),
- })
- .passthrough();
-
-// Extend the CallToolResult schema to include audio content
-export const ExtendedCallToolResultSchema = ResultSchema.extend({
- content: z.array(
- z.discriminatedUnion("type", [
- TextContentSchema,
- ImageContentSchema,
- AudioContentSchema,
- EmbeddedResourceSchema,
- ]),
- ),
- isError: z.boolean().default(false).optional(),
-});
-
-// Export the types
-export type AudioContent = z.infer;
-export type ExtendedCallToolResult = z.infer<
- typeof ExtendedCallToolResultSchema
->;
const ToolsTab = ({
tools,
@@ -77,7 +45,7 @@ const ToolsTab = ({
if (!toolResult) return null;
if ("content" in toolResult) {
- const parsedResult = ExtendedCallToolResultSchema.safeParse(toolResult);
+ const parsedResult = CallToolResultSchema.safeParse(toolResult);
if (!parsedResult.success) {
return (
<>
@@ -119,15 +87,6 @@ const ToolsTab = ({
className="max-w-full h-auto"
/>
)}
- {item.type === "audio" && (
-
- Your browser does not support audio playback
-
- )}
{item.type === "resource" &&
(item.resource?.mimeType?.startsWith("audio/") ? (
Date: Mon, 3 Feb 2025 19:53:53 -0800
Subject: [PATCH 09/21] Add refresh token handling if returned from server
---
client/src/components/OAuthCallback.tsx | 9 ++++--
client/src/lib/auth.ts | 38 +++++++++++++++++++++++--
client/src/lib/constants.ts | 1 +
client/src/lib/hooks/useConnection.ts | 34 ++++++++++++++++++++--
4 files changed, 74 insertions(+), 8 deletions(-)
diff --git a/client/src/components/OAuthCallback.tsx b/client/src/components/OAuthCallback.tsx
index a7439df..2a9e27a 100644
--- a/client/src/components/OAuthCallback.tsx
+++ b/client/src/components/OAuthCallback.tsx
@@ -24,9 +24,12 @@ const OAuthCallback = () => {
}
try {
- const accessToken = await handleOAuthCallback(serverUrl, code);
- // Store the access token for future use
- sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, accessToken);
+ const tokens = await handleOAuthCallback(serverUrl, code);
+ // Store both access and refresh tokens
+ 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
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
} catch (error) {
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index 0417731..7d70a31 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -6,6 +6,12 @@ export interface OAuthMetadata {
token_endpoint: string;
}
+export interface OAuthTokens {
+ access_token: string;
+ refresh_token?: string;
+ expires_in?: number;
+}
+
export async function discoverOAuthMetadata(
serverUrl: string,
): Promise {
@@ -60,7 +66,7 @@ export async function startOAuthFlow(serverUrl: string): Promise {
export async function handleOAuthCallback(
serverUrl: string,
code: string,
-): Promise {
+): Promise {
// Get stored code verifier
const codeVerifier = sessionStorage.getItem(SESSION_KEYS.CODE_VERIFIER);
if (!codeVerifier) {
@@ -69,7 +75,6 @@ export async function handleOAuthCallback(
// Discover OAuth endpoints
const metadata = await discoverOAuthMetadata(serverUrl);
-
// Exchange code for tokens
const response = await fetch(metadata.token_endpoint, {
method: "POST",
@@ -89,5 +94,32 @@ export async function handleOAuthCallback(
}
const data = await response.json();
- return data.access_token;
+ return data;
+}
+
+export async function refreshAccessToken(serverUrl: string): Promise {
+ 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 data = await response.json();
+ return data;
}
diff --git a/client/src/lib/constants.ts b/client/src/lib/constants.ts
index e302b52..13a2370 100644
--- a/client/src/lib/constants.ts
+++ b/client/src/lib/constants.ts
@@ -3,4 +3,5 @@ export const SESSION_KEYS = {
CODE_VERIFIER: "mcp_code_verifier",
SERVER_URL: "mcp_server_url",
ACCESS_TOKEN: "mcp_access_token",
+ REFRESH_TOKEN: "mcp_refresh_token",
} as const;
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index de2d29e..58ea0a8 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -16,7 +16,7 @@ import {
import { useState } from "react";
import { toast } from "react-toastify";
import { z } from "zod";
-import { startOAuthFlow } from "../auth";
+import { startOAuthFlow, refreshAccessToken } from "../auth";
import { SESSION_KEYS } from "../constants";
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
@@ -121,6 +121,24 @@ export function useConnection({
}
};
+ 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);
+ // Clear tokens and redirect to home
+ sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
+ sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
+ window.location.href = "/";
+ throw error;
+ }
+ };
+
const connect = async () => {
try {
const client = new Client(
@@ -157,7 +175,19 @@ export function useConnection({
const clientTransport = new SSEClientTransport(backendUrl, {
eventSourceInit: {
- fetch: (url, init) => fetch(url, { ...init, headers }),
+ fetch: async (url, init) => {
+ const response = await fetch(url, { ...init, headers });
+
+ if (response.status === 401 && sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
+ // Try to refresh the token
+ const newAccessToken = await handleTokenRefresh();
+ headers["Authorization"] = `Bearer ${newAccessToken}`;
+ // Retry the request with new token
+ return fetch(url, { ...init, headers });
+ }
+
+ return response;
+ },
},
requestInit: {
headers,
From 4c89aed4d9f64f4a62d30b49a6ea441188909d77 Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Mon, 3 Feb 2025 20:04:17 -0800
Subject: [PATCH 10/21] Add check for expired refresh or session token that
exists
---
client/src/lib/hooks/useConnection.ts | 34 ++++++++++++++++++++-------
1 file changed, 26 insertions(+), 8 deletions(-)
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index 58ea0a8..e08e0ae 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -131,10 +131,13 @@ export function useConnection({
return tokens.access_token;
} catch (error) {
console.error("Token refresh failed:", error);
- // Clear tokens and redirect to home
+ // If refresh token is expired/invalid (401) or any other error,
+ // clear tokens and redirect to home to trigger re-authentication
sessionStorage.removeItem(SESSION_KEYS.ACCESS_TOKEN);
sessionStorage.removeItem(SESSION_KEYS.REFRESH_TOKEN);
- window.location.href = "/";
+ sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
+ const redirectUrl = await startOAuthFlow(sseUrl);
+ window.location.href = redirectUrl;
throw error;
}
};
@@ -178,12 +181,27 @@ export function useConnection({
fetch: async (url, init) => {
const response = await fetch(url, { ...init, headers });
- if (response.status === 401 && sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
- // Try to refresh the token
- const newAccessToken = await handleTokenRefresh();
- headers["Authorization"] = `Bearer ${newAccessToken}`;
- // Retry the request with new token
- return fetch(url, { ...init, headers });
+ if (response.status === 401) {
+ // First try to refresh if we have a refresh token
+ if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
+ try {
+ const newAccessToken = await handleTokenRefresh();
+ headers["Authorization"] = `Bearer ${newAccessToken}`;
+ // Retry the request with new token
+ return fetch(url, { ...init, headers });
+ } catch (error) {
+ console.error("Token refresh failed:", error);
+ }
+ }
+
+ // If we have an access token but refresh failed or wasn't available,
+ // we need to re-authenticate since the token is invalid
+ if (sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN)) {
+ sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
+ const redirectUrl = await startOAuthFlow(sseUrl);
+ window.location.href = redirectUrl;
+ return new Response(); // This won't actually be used due to redirect
+ }
}
return response;
From 7957d9f577aeed88844c843977dbb1dc6c64912d Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Mon, 3 Feb 2025 20:06:21 -0800
Subject: [PATCH 11/21] Make OAuth start call modular
---
client/src/lib/hooks/useConnection.ts | 32 ++++++++++-----------------
1 file changed, 12 insertions(+), 20 deletions(-)
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index e08e0ae..9ceafa2 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -121,6 +121,14 @@ export function useConnection({
}
};
+ 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);
@@ -131,13 +139,7 @@ export function useConnection({
return tokens.access_token;
} catch (error) {
console.error("Token refresh failed:", error);
- // If refresh token is expired/invalid (401) or any other error,
- // clear tokens and redirect to home to trigger re-authentication
- 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;
+ await initiateOAuthFlow();
throw error;
}
};
@@ -182,25 +184,19 @@ export function useConnection({
const response = await fetch(url, { ...init, headers });
if (response.status === 401) {
- // First try to refresh if we have a refresh token
if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
try {
const newAccessToken = await handleTokenRefresh();
headers["Authorization"] = `Bearer ${newAccessToken}`;
- // Retry the request with new token
return fetch(url, { ...init, headers });
} catch (error) {
console.error("Token refresh failed:", error);
}
}
- // If we have an access token but refresh failed or wasn't available,
- // we need to re-authenticate since the token is invalid
if (sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN)) {
- sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
- const redirectUrl = await startOAuthFlow(sseUrl);
- window.location.href = redirectUrl;
- return new Response(); // This won't actually be used due to redirect
+ await initiateOAuthFlow();
+ return new Response();
}
}
@@ -231,13 +227,9 @@ export function useConnection({
} catch (error) {
console.error("Failed to connect to MCP server:", error);
if (error instanceof SseError && error.code === 401) {
- // Store the server URL for the callback handler
- sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
- const redirectUrl = await startOAuthFlow(sseUrl);
- window.location.href = redirectUrl;
+ await initiateOAuthFlow();
return;
}
-
throw error;
}
From b5762d53fd6c543524cf5d9d867a253b60c0d12b Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Tue, 4 Feb 2025 14:53:41 -0800
Subject: [PATCH 12/21] Handle infinite loop if server keeps returning 401
---
client/src/lib/hooks/useConnection.ts | 51 ++++++++++++++-------------
1 file changed, 26 insertions(+), 25 deletions(-)
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index 9ceafa2..cd90d7f 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -144,7 +144,23 @@ export function useConnection({
}
};
- const connect = async () => {
+ 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 {
const client = new Client(
{
@@ -180,28 +196,7 @@ export function useConnection({
const clientTransport = new SSEClientTransport(backendUrl, {
eventSourceInit: {
- fetch: async (url, init) => {
- const response = await fetch(url, { ...init, headers });
-
- if (response.status === 401) {
- if (sessionStorage.getItem(SESSION_KEYS.REFRESH_TOKEN)) {
- try {
- const newAccessToken = await handleTokenRefresh();
- headers["Authorization"] = `Bearer ${newAccessToken}`;
- return fetch(url, { ...init, headers });
- } catch (error) {
- console.error("Token refresh failed:", error);
- }
- }
-
- if (sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN)) {
- await initiateOAuthFlow();
- return new Response();
- }
- }
-
- return response;
- },
+ fetch: (url, init) => fetch(url, { ...init, headers }),
},
requestInit: {
headers,
@@ -226,11 +221,17 @@ export function useConnection({
await client.connect(clientTransport);
} catch (error) {
console.error("Failed to connect to MCP server:", error);
+ const shouldRetry = await handleAuthError(error);
+ if (shouldRetry) {
+ return connect(undefined, retryCount + 1);
+ }
+
if (error instanceof SseError && error.code === 401) {
- await initiateOAuthFlow();
+ // Don't set error state if we're about to redirect for auth
return;
}
- throw error;
+ setConnectionStatus("error");
+ return;
}
const capabilities = client.getServerCapabilities();
From b4ae1327b51887cf031f7900d7258bc81f4e8e99 Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Tue, 4 Feb 2025 15:00:14 -0800
Subject: [PATCH 13/21] Update useConnection.ts
---
client/src/lib/hooks/useConnection.ts | 6 ------
1 file changed, 6 deletions(-)
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index cd90d7f..0e843b8 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -225,12 +225,6 @@ export function useConnection({
if (shouldRetry) {
return connect(undefined, retryCount + 1);
}
-
- if (error instanceof SseError && error.code === 401) {
- // Don't set error state if we're about to redirect for auth
- return;
- }
- setConnectionStatus("error");
return;
}
From dd47b574b3ee897e463b700baffb7c2c3a408512 Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Tue, 4 Feb 2025 15:02:12 -0800
Subject: [PATCH 14/21] Update useConnection.ts
---
client/src/lib/hooks/useConnection.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/client/src/lib/hooks/useConnection.ts b/client/src/lib/hooks/useConnection.ts
index 0e843b8..7225184 100644
--- a/client/src/lib/hooks/useConnection.ts
+++ b/client/src/lib/hooks/useConnection.ts
@@ -225,7 +225,12 @@ export function useConnection({
if (shouldRetry) {
return connect(undefined, retryCount + 1);
}
- return;
+
+ if (error instanceof SseError && error.code === 401) {
+ // Don't set error state if we're about to redirect for auth
+ return;
+ }
+ throw error;
}
const capabilities = client.getServerCapabilities();
From 8592cf2d0716063e8e422215f8a610d342bf31ec Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Wed, 5 Feb 2025 11:22:11 -0800
Subject: [PATCH 15/21] Run prettier-fix
---
client/src/components/OAuthCallback.tsx | 5 ++++-
client/src/lib/auth.ts | 8 +++++---
client/src/lib/hooks/useConnection.ts | 5 ++++-
3 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/client/src/components/OAuthCallback.tsx b/client/src/components/OAuthCallback.tsx
index 2a9e27a..869eef1 100644
--- a/client/src/components/OAuthCallback.tsx
+++ b/client/src/components/OAuthCallback.tsx
@@ -28,7 +28,10 @@ const OAuthCallback = () => {
// Store both access and refresh tokens
sessionStorage.setItem(SESSION_KEYS.ACCESS_TOKEN, tokens.access_token);
if (tokens.refresh_token) {
- sessionStorage.setItem(SESSION_KEYS.REFRESH_TOKEN, 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
window.location.href = `/?serverUrl=${encodeURIComponent(serverUrl)}`;
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index 7d70a31..918ac4d 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -97,14 +97,16 @@ export async function handleOAuthCallback(
return data;
}
-export async function refreshAccessToken(serverUrl: string): Promise {
+export async function refreshAccessToken(
+ serverUrl: string,
+): Promise {
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: {
@@ -112,7 +114,7 @@ export async function refreshAccessToken(serverUrl: string): Promise
Date: Wed, 5 Feb 2025 12:38:26 -0800
Subject: [PATCH 16/21] Convert OAuthMetadata and OAuthTokens to zod
---
client/src/lib/auth.ts | 29 ++++++++++++++++-------------
1 file changed, 16 insertions(+), 13 deletions(-)
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index 918ac4d..040d35a 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -1,16 +1,21 @@
import pkceChallenge from "pkce-challenge";
import { SESSION_KEYS } from "./constants";
+import { z } from "zod";
-export interface OAuthMetadata {
- authorization_endpoint: string;
- token_endpoint: string;
-}
+export const OAuthMetadataSchema = z.object({
+ authorization_endpoint: z.string(),
+ token_endpoint: z.string()
+});
-export interface OAuthTokens {
- access_token: string;
- refresh_token?: string;
- expires_in?: number;
-}
+export type OAuthMetadata = z.infer;
+
+export const OAuthTokensSchema = z.object({
+ access_token: z.string(),
+ refresh_token: z.string().optional(),
+ expires_in: z.number().optional()
+});
+
+export type OAuthTokens = z.infer;
export async function discoverOAuthMetadata(
serverUrl: string,
@@ -93,8 +98,7 @@ export async function handleOAuthCallback(
throw new Error("Token exchange failed");
}
- const data = await response.json();
- return data;
+ return await response.json();
}
export async function refreshAccessToken(
@@ -122,6 +126,5 @@ export async function refreshAccessToken(
throw new Error("Token refresh failed");
}
- const data = await response.json();
- return data;
+ return await response.json();
}
From 95bbd60a38f4f34b7f5ae40146bee7488c5383c4 Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Wed, 5 Feb 2025 12:42:09 -0800
Subject: [PATCH 17/21] Add zod parsing for OAuthMetadataSchema and
OAuthTokensSchema
---
client/src/lib/auth.ts | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index 040d35a..d7ddc8c 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -26,10 +26,11 @@ export async function discoverOAuthMetadata(
if (response.ok) {
const metadata = await response.json();
- return {
+ const validatedMetadata = OAuthMetadataSchema.parse({
authorization_endpoint: metadata.authorization_endpoint,
token_endpoint: metadata.token_endpoint,
- };
+ });
+ return validatedMetadata;
}
} catch (error) {
console.warn("OAuth metadata discovery failed:", error);
@@ -37,10 +38,11 @@ export async function discoverOAuthMetadata(
// Fall back to default endpoints
const baseUrl = new URL(serverUrl);
- return {
+ const defaultMetadata = {
authorization_endpoint: new URL("/authorize", baseUrl).toString(),
token_endpoint: new URL("/token", baseUrl).toString(),
};
+ return OAuthMetadataSchema.parse(defaultMetadata);
}
export async function startOAuthFlow(serverUrl: string): Promise {
@@ -98,7 +100,8 @@ export async function handleOAuthCallback(
throw new Error("Token exchange failed");
}
- return await response.json();
+ const tokens = await response.json();
+ return OAuthTokensSchema.parse(tokens);
}
export async function refreshAccessToken(
@@ -126,5 +129,6 @@ export async function refreshAccessToken(
throw new Error("Token refresh failed");
}
- return await response.json();
+ const tokens = await response.json();
+ return OAuthTokensSchema.parse(tokens);
}
From 1b13b574f8aafd96887823d6dfa7aaddde5d9b72 Mon Sep 17 00:00:00 2001
From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com>
Date: Wed, 5 Feb 2025 12:45:11 -0800
Subject: [PATCH 18/21] Update auth.ts
---
client/src/lib/auth.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index d7ddc8c..592dc17 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -4,7 +4,7 @@ import { z } from "zod";
export const OAuthMetadataSchema = z.object({
authorization_endpoint: z.string(),
- token_endpoint: z.string()
+ token_endpoint: z.string(),
});
export type OAuthMetadata = z.infer;
@@ -12,7 +12,7 @@ export type OAuthMetadata = z.infer;
export const OAuthTokensSchema = z.object({
access_token: z.string(),
refresh_token: z.string().optional(),
- expires_in: z.number().optional()
+ expires_in: z.number().optional(),
});
export type OAuthTokens = z.infer;
From e427f7bca592d6d35fee3e76de57bd050cacbcaf Mon Sep 17 00:00:00 2001
From: matv-stripe <55463370+matv-stripe@users.noreply.github.com>
Date: Wed, 12 Feb 2025 12:26:41 -0500
Subject: [PATCH 19/21] Update README.md
---
README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 98b5704..3ffeae9 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ The MCP inspector is a developer tool for testing and debugging MCP servers.
To inspect an MCP server implementation, there's no need to clone this repo. Instead, use `npx`. For example, if your server is built at `build/index.js`:
```bash
-npx @modelcontextprotocol/inspector build/index.js
+npx @modelcontextprotocol/inspector node build/index.js
```
You can pass both arguments and environment variables to your MCP server. Arguments are passed directly to your server, while environment variables can be set using the `-e` flag:
@@ -21,19 +21,19 @@ You can pass both arguments and environment variables to your MCP server. Argume
npx @modelcontextprotocol/inspector build/index.js arg1 arg2
# Pass environment variables only
-npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js
+npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js
# Pass both environment variables and arguments
-npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 build/index.js arg1 arg2
+npx @modelcontextprotocol/inspector -e KEY=value -e KEY2=$VALUE2 node build/index.js arg1 arg2
# Use -- to separate inspector flags from server arguments
-npx @modelcontextprotocol/inspector -e KEY=$VALUE -- build/index.js -e server-flag
+npx @modelcontextprotocol/inspector -e KEY=$VALUE -- node build/index.js -e server-flag
```
The inspector runs both a client UI (default port 5173) and an MCP proxy server (default port 3000). Open the client UI in your browser to use the inspector. You can customize the ports if needed:
```bash
-CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector build/index.js
+CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node build/index.js
```
For more details on ways to use the inspector, see the [Inspector section of the MCP docs site](https://modelcontextprotocol.io/docs/tools/inspector). For help with debugging, see the [Debugging guide](https://modelcontextprotocol.io/docs/tools/debugging).
From b324378b2c0d14755064c4efe14e76efe42b3fca Mon Sep 17 00:00:00 2001
From: Justin Spahr-Summers
Date: Wed, 12 Feb 2025 18:08:31 +0000
Subject: [PATCH 20/21] Bump all versions to 0.4.1
---
client/package.json | 4 ++--
package.json | 8 ++++----
server/package.json | 4 ++--
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/client/package.json b/client/package.json
index 69021c8..29c193f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-client",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "Client-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -57,4 +57,4 @@
"typescript-eslint": "^8.7.0",
"vite": "^5.4.8"
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index cf06f70..3593ee0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -33,8 +33,8 @@
"publish-all": "npm publish --workspaces --access public && npm publish --access public"
},
"dependencies": {
- "@modelcontextprotocol/inspector-client": "0.3.0",
- "@modelcontextprotocol/inspector-server": "0.3.0",
+ "@modelcontextprotocol/inspector-client": "0.4.1",
+ "@modelcontextprotocol/inspector-server": "0.4.1",
"concurrently": "^9.0.1",
"shell-quote": "^1.8.2",
"spawn-rx": "^5.1.0",
@@ -45,4 +45,4 @@
"@types/shell-quote": "^1.7.5",
"prettier": "3.3.3"
}
-}
+}
\ No newline at end of file
diff --git a/server/package.json b/server/package.json
index eb711f5..9522a73 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/inspector-server",
- "version": "0.4.0",
+ "version": "0.4.1",
"description": "Server-side application for the Model Context Protocol inspector",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
@@ -32,4 +32,4 @@
"ws": "^8.18.0",
"zod": "^3.23.8"
}
-}
+}
\ No newline at end of file
From 410a6f33dc48ac5b9213ed2ffba70818268648e8 Mon Sep 17 00:00:00 2001
From: Justin Spahr-Summers
Date: Wed, 12 Feb 2025 18:10:25 +0000
Subject: [PATCH 21/21] Format fixes
---
client/package.json | 2 +-
package.json | 2 +-
server/package.json | 2 +-
server/src/index.ts | 9 ++++-----
4 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/client/package.json b/client/package.json
index 29c193f..e3445da 100644
--- a/client/package.json
+++ b/client/package.json
@@ -57,4 +57,4 @@
"typescript-eslint": "^8.7.0",
"vite": "^5.4.8"
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 3593ee0..0989aa9 100644
--- a/package.json
+++ b/package.json
@@ -45,4 +45,4 @@
"@types/shell-quote": "^1.7.5",
"prettier": "3.3.3"
}
-}
\ No newline at end of file
+}
diff --git a/server/package.json b/server/package.json
index 9522a73..f2e4ddd 100644
--- a/server/package.json
+++ b/server/package.json
@@ -32,4 +32,4 @@
"ws": "^8.18.0",
"zod": "^3.23.8"
}
-}
\ No newline at end of file
+}
diff --git a/server/src/index.ts b/server/src/index.ts
index 96027a9..2874b45 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -184,14 +184,13 @@ const PORT = process.env.PORT || 3000;
try {
const server = app.listen(PORT);
-
- server.on('listening', () => {
+
+ server.on("listening", () => {
const addr = server.address();
- const port = typeof addr === 'string' ? addr : addr?.port;
+ 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);
+ console.error("Failed to start server:", error);
process.exit(1);
}