Formatting

This commit is contained in:
Justin Spahr-Summers
2025-01-24 15:23:24 +00:00
parent fce6644e30
commit 0882a3e0e5
7 changed files with 60 additions and 45 deletions

View File

@@ -227,7 +227,7 @@ const App = () => {
newUrl.searchParams.delete("serverUrl"); newUrl.searchParams.delete("serverUrl");
window.history.replaceState({}, "", newUrl.toString()); window.history.replaceState({}, "", newUrl.toString());
// Show success toast for OAuth // Show success toast for OAuth
toast.success('Successfully authenticated with OAuth'); toast.success("Successfully authenticated with OAuth");
// Connect to the server // Connect to the server
connectMcpServer(); connectMcpServer();
} }
@@ -446,8 +446,8 @@ const App = () => {
<div className="w-full"> <div className="w-full">
{!serverCapabilities?.resources && {!serverCapabilities?.resources &&
!serverCapabilities?.prompts && !serverCapabilities?.prompts &&
!serverCapabilities?.tools ? ( !serverCapabilities?.tools ? (
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
<p className="text-lg text-gray-500"> <p className="text-lg text-gray-500">
The connected server does not support any MCP capabilities The connected server does not support any MCP capabilities

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
import { handleOAuthCallback } from '../lib/auth'; import { handleOAuthCallback } from "../lib/auth";
import { SESSION_KEYS } from '../lib/constants'; import { SESSION_KEYS } from "../lib/constants";
const OAuthCallback = () => { const OAuthCallback = () => {
const hasProcessedRef = useRef(false); const hasProcessedRef = useRef(false);
@@ -14,12 +14,12 @@ const OAuthCallback = () => {
hasProcessedRef.current = true; hasProcessedRef.current = true;
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const code = params.get('code'); const code = params.get("code");
const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL); const serverUrl = sessionStorage.getItem(SESSION_KEYS.SERVER_URL);
if (!code || !serverUrl) { if (!code || !serverUrl) {
console.error('Missing code or server URL'); console.error("Missing code or server URL");
window.location.href = '/'; window.location.href = "/";
return; return;
} }
@@ -30,8 +30,8 @@ const OAuthCallback = () => {
// 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) {
console.error('OAuth callback error:', error); console.error("OAuth callback error:", error);
window.location.href = '/'; window.location.href = "/";
} }
}; };
@@ -45,4 +45,4 @@ const OAuthCallback = () => {
); );
}; };
export default OAuthCallback; export default OAuthCallback;

View File

@@ -1,32 +1,34 @@
import pkceChallenge from 'pkce-challenge'; import pkceChallenge from "pkce-challenge";
import { SESSION_KEYS } from './constants'; import { SESSION_KEYS } from "./constants";
export interface OAuthMetadata { export interface OAuthMetadata {
authorization_endpoint: string; authorization_endpoint: string;
token_endpoint: string; token_endpoint: string;
} }
export async function discoverOAuthMetadata(serverUrl: string): Promise<OAuthMetadata> { export async function discoverOAuthMetadata(
serverUrl: string,
): Promise<OAuthMetadata> {
try { try {
const url = new URL('/.well-known/oauth-authorization-server', serverUrl); const url = new URL("/.well-known/oauth-authorization-server", serverUrl);
const response = await fetch(url.toString()); const response = await fetch(url.toString());
if (response.ok) { if (response.ok) {
const metadata = await response.json(); const metadata = await response.json();
return { return {
authorization_endpoint: metadata.authorization_endpoint, authorization_endpoint: metadata.authorization_endpoint,
token_endpoint: metadata.token_endpoint token_endpoint: metadata.token_endpoint,
}; };
} }
} catch (error) { } catch (error) {
console.warn('OAuth metadata discovery failed:', error); console.warn("OAuth metadata discovery failed:", error);
} }
// Fall back to default endpoints // Fall back to default endpoints
const baseUrl = new URL(serverUrl); const baseUrl = new URL(serverUrl);
return { return {
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(),
}; };
} }
@@ -44,19 +46,25 @@ export async function startOAuthFlow(serverUrl: string): Promise<string> {
// Build authorization URL // Build authorization URL
const authUrl = new URL(metadata.authorization_endpoint); const authUrl = new URL(metadata.authorization_endpoint);
authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256'); authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set('redirect_uri', window.location.origin + '/oauth/callback'); authUrl.searchParams.set(
"redirect_uri",
window.location.origin + "/oauth/callback",
);
return authUrl.toString(); return authUrl.toString();
} }
export async function handleOAuthCallback(serverUrl: string, code: string): Promise<string> { export async function handleOAuthCallback(
serverUrl: string,
code: string,
): Promise<string> {
// 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) {
throw new Error('No code verifier found'); throw new Error("No code verifier found");
} }
// Discover OAuth endpoints // Discover OAuth endpoints
@@ -64,20 +72,20 @@ export async function handleOAuthCallback(serverUrl: string, code: string): Prom
// 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",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
grant_type: 'authorization_code', grant_type: "authorization_code",
code, code,
code_verifier: codeVerifier, code_verifier: codeVerifier,
redirect_uri: window.location.origin + '/oauth/callback' redirect_uri: window.location.origin + "/oauth/callback",
}) }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Token exchange failed'); throw new Error("Token exchange failed");
} }
const data = await response.json(); const data = await response.json();

View File

@@ -1,6 +1,6 @@
// OAuth-related session storage keys // OAuth-related session storage keys
export const SESSION_KEYS = { 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",
} as const; } as const;

View File

@@ -1,5 +1,8 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js"; import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import { import {
ClientNotification, ClientNotification,
ClientRequest, ClientRequest,
@@ -149,7 +152,7 @@ export function useConnection({
const headers: HeadersInit = {}; const headers: HeadersInit = {};
const accessToken = sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN); const accessToken = sessionStorage.getItem(SESSION_KEYS.ACCESS_TOKEN);
if (accessToken) { if (accessToken) {
headers['Authorization'] = `Bearer ${accessToken}`; headers["Authorization"] = `Bearer ${accessToken}`;
} }
const clientTransport = new SSEClientTransport(backendUrl, { const clientTransport = new SSEClientTransport(backendUrl, {

View File

@@ -5,8 +5,7 @@ import { defineConfig } from "vite";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {},
},
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),

View File

@@ -4,7 +4,10 @@ import cors from "cors";
import { parseArgs } from "node:util"; import { parseArgs } from "node:util";
import { parse as shellParseArgs } from "shell-quote"; import { parse as shellParseArgs } from "shell-quote";
import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js"; import {
SSEClientTransport,
SseError,
} from "@modelcontextprotocol/sdk/client/sse.js";
import { import {
StdioClientTransport, StdioClientTransport,
getDefaultEnvironment, getDefaultEnvironment,
@@ -14,7 +17,7 @@ import express from "express";
import { findActualExecutable } from "spawn-rx"; import { findActualExecutable } from "spawn-rx";
import mcpProxy from "./mcpProxy.js"; import mcpProxy from "./mcpProxy.js";
const SSE_HEADERS_PASSTHROUGH = ['authorization']; const SSE_HEADERS_PASSTHROUGH = ["authorization"];
const defaultEnvironment = { const defaultEnvironment = {
...getDefaultEnvironment(), ...getDefaultEnvironment(),
@@ -67,7 +70,6 @@ const createTransport = async (req: express.Request) => {
for (const key of SSE_HEADERS_PASSTHROUGH) { for (const key of SSE_HEADERS_PASSTHROUGH) {
if (req.headers[key] === undefined) { if (req.headers[key] === undefined) {
continue; continue;
} }
const value = req.headers[key]; const value = req.headers[key];
@@ -103,7 +105,10 @@ app.get("/sse", async (req, res) => {
backingServerTransport = await createTransport(req); backingServerTransport = await createTransport(req);
} catch (error) { } catch (error) {
if (error instanceof SseError && error.code === 401) { if (error instanceof SseError && error.code === 401) {
console.error("Received 401 Unauthorized from MCP server:", error.message); console.error(
"Received 401 Unauthorized from MCP server:",
error.message,
);
res.status(401).json(error); res.status(401).json(error);
return; return;
} }
@@ -176,4 +181,4 @@ app.get("/config", (req, res) => {
}); });
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { }); app.listen(PORT, () => {});