Fixing /sse support (#12)
* /sse still not working * Add transport type selector to inspector UI and comprehensive transport testing - Add transport type dropdown to inspector UI (auto, HTTP, SSE) with session storage persistence - Update test framework to support transport type selection in test scenarios - Add comprehensive test cases covering: * Working /mcp endpoints with auto and HTTP transports * SSE endpoint with SSE transport (expected to pass) * SSE endpoint with auto transport (expected to fail - demonstrates current fallback issue) - Test structure now provides clear visibility into transport-specific behavior - All tests properly named and categorized for easy identification of failing scenarios * Gearing up to "make amp do it" * fix: improve SSE fallback logic in auto transport mode - Fix issue where SSE fallback was not attempted when HTTP transport failed - Specific error types (404, 405, CORS) now correctly trigger SSE fallback - Allow SSE fallback even when component state is 'failed' from previous attempts - Maintain proper error handling for non-fallback HTTP errors - All 6 transport scenarios now pass integration tests The core issue was that failConnection() was being called for HTTP errors before returning 'fallback', which set the component state to 'failed' and prevented the orchestration logic from attempting SSE fallback. Now only specific HTTP errors that should trigger fallback avoid calling failConnection immediately, allowing the SSE transport to be attempted as expected. --------- Co-authored-by: Glen Maddern <glen@glenmaddern.com>
This commit is contained in:
parent
7f019fed7f
commit
fe65d50ec3
5 changed files with 91 additions and 20 deletions
|
|
@ -3,14 +3,22 @@ import { useMcp, type Tool } from 'use-mcp/react'
|
|||
import { Info, X, ChevronRight, ChevronDown } from 'lucide-react'
|
||||
|
||||
// MCP Connection wrapper that only renders when active
|
||||
function McpConnection({ serverUrl, onConnectionUpdate }: { serverUrl: string; onConnectionUpdate: (data: any) => void }) {
|
||||
function McpConnection({
|
||||
serverUrl,
|
||||
transportType,
|
||||
onConnectionUpdate,
|
||||
}: {
|
||||
serverUrl: string
|
||||
transportType: 'auto' | 'http' | 'sse'
|
||||
onConnectionUpdate: (data: any) => void
|
||||
}) {
|
||||
// Use the MCP hook with the server URL
|
||||
const connection = useMcp({
|
||||
url: serverUrl,
|
||||
debug: true,
|
||||
autoRetry: false,
|
||||
popupFeatures: 'width=500,height=600,resizable=yes,scrollbars=yes',
|
||||
transportType: 'auto',
|
||||
transportType,
|
||||
})
|
||||
|
||||
// Update parent component with connection data
|
||||
|
|
@ -26,6 +34,9 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
|
|||
const [serverUrl, setServerUrl] = useState(() => {
|
||||
return sessionStorage.getItem('mcpServerUrl') || ''
|
||||
})
|
||||
const [transportType, setTransportType] = useState<'auto' | 'http' | 'sse'>(() => {
|
||||
return (sessionStorage.getItem('mcpTransportType') as 'auto' | 'http' | 'sse') || 'auto'
|
||||
})
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
|
||||
const [connectionData, setConnectionData] = useState<any>({
|
||||
|
|
@ -293,6 +304,20 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
|
|||
}}
|
||||
disabled={isActive && state !== 'failed'}
|
||||
/>
|
||||
<select
|
||||
className="p-2 border border-gray-200 rounded text-sm focus:outline-none focus:ring-1 focus:ring-blue-300"
|
||||
value={transportType}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value as 'auto' | 'http' | 'sse'
|
||||
setTransportType(newValue)
|
||||
sessionStorage.setItem('mcpTransportType', newValue)
|
||||
}}
|
||||
disabled={isActive && state !== 'failed'}
|
||||
>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="sse">SSE</option>
|
||||
</select>
|
||||
|
||||
{state === 'ready' || (isActive && state !== 'not-connected' && state !== 'failed') ? (
|
||||
<button
|
||||
|
|
@ -477,7 +502,7 @@ export function McpServers({ onToolsUpdate }: { onToolsUpdate?: (tools: Tool[])
|
|||
</div>
|
||||
|
||||
{/* Only render the actual MCP connection when active */}
|
||||
{isActive && <McpConnection serverUrl={serverUrl} onConnectionUpdate={setConnectionData} />}
|
||||
{isActive && <McpConnection serverUrl={serverUrl} transportType={transportType} onConnectionUpdate={setConnectionData} />}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import tailwindcss from '@tailwindcss/vite'
|
|||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
build: {
|
||||
// @ts-ignore
|
||||
minify: !process.env.NO_MINIFY,
|
||||
// @ts-expect-error I don't want to install @types/node for this one line
|
||||
minify: process.env.NO_MINIFY !== 'true',
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -369,9 +369,10 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
|
|||
}
|
||||
|
||||
// Handle other connection errors
|
||||
// Use stable failConnection
|
||||
// For HTTP transport, consider fallback only for specific error types
|
||||
// "Not connected" errors should still be treated as failures, not fallback triggers
|
||||
failConnection(`Failed to connect via ${transportType.toUpperCase()}: ${errorMessage}`, errorInstance)
|
||||
return 'fallback' // If our logic above is wrong, we'd prevent HTTPs servers from ever working. So always fall back as a last resort.
|
||||
return 'failed'
|
||||
}
|
||||
} // End of tryConnectWithTransport helper
|
||||
|
||||
|
|
@ -392,10 +393,18 @@ export function useMcp(options: UseMcpOptions): UseMcpResult {
|
|||
// Auto mode - try HTTP first, fallback to SSE
|
||||
addLog('debug', 'Using auto transport mode (HTTP with SSE fallback)')
|
||||
const httpResult = await tryConnectWithTransport('http')
|
||||
// 2. Try SSE only if HTTP requested fallback and we haven't failed/redirected
|
||||
if (httpResult === 'fallback' && isMountedRef.current && stateRef.current !== 'failed' && stateRef.current !== 'authenticating') {
|
||||
|
||||
// Try SSE only if HTTP requested fallback and we haven't redirected for auth
|
||||
// Allow fallback even if state is 'failed' from a previous HTTP attempt in auto mode
|
||||
if (httpResult === 'fallback' && isMountedRef.current && stateRef.current !== 'authenticating') {
|
||||
addLog('info', 'HTTP failed, attempting SSE fallback...')
|
||||
const sseResult = await tryConnectWithTransport('sse')
|
||||
finalStatus = sseResult // Use SSE result as final status
|
||||
|
||||
// If SSE also failed, we need to properly fail the connection since HTTP didn't call failConnection
|
||||
if (sseResult === 'failed' && isMountedRef.current) {
|
||||
// SSE failure already called failConnection, so we don't need to do anything else
|
||||
}
|
||||
} else {
|
||||
finalStatus = httpResult // Use HTTP result if no fallback was needed/possible
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,13 +33,22 @@ function getMCPServers() {
|
|||
url: `http://localhost:${state.cfAgentsPort}/mcp`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
{
|
||||
name: 'cf-agents-sse',
|
||||
url: `http://localhost:${state.cfAgentsPort}/sse`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
]
|
||||
} catch (error) {
|
||||
throw new Error(`Test environment not properly initialized: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function connectToMCPServer(page: Page, serverUrl: string): Promise<{ success: boolean; tools: string[]; debugLog: string }> {
|
||||
async function connectToMCPServer(
|
||||
page: Page,
|
||||
serverUrl: string,
|
||||
transportType: 'auto' | 'http' | 'sse' = 'auto',
|
||||
): Promise<{ success: boolean; tools: string[]; debugLog: string }> {
|
||||
// Navigate to the inspector
|
||||
const stateData = readFileSync(testStateFile, 'utf-8')
|
||||
const state = JSON.parse(stateData)
|
||||
|
|
@ -58,6 +67,10 @@ async function connectToMCPServer(page: Page, serverUrl: string): Promise<{ succ
|
|||
const urlInput = page.locator('input[placeholder="Enter MCP server URL"]')
|
||||
await urlInput.fill(serverUrl)
|
||||
|
||||
// Set transport type
|
||||
const transportSelect = page.locator('select')
|
||||
await transportSelect.selectOption(transportType)
|
||||
|
||||
// Click connect button
|
||||
const connectButton = page.locator('button:has-text("Connect")')
|
||||
await connectButton.click()
|
||||
|
|
@ -202,13 +215,35 @@ describe('MCP Connection Integration Tests', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('should connect to all MCP servers and retrieve tools', async () => {
|
||||
const servers = getMCPServers()
|
||||
const testScenarios = [
|
||||
// Working examples with auto transport (should pass)
|
||||
{ serverName: 'hono-mcp', transportType: 'auto' as const },
|
||||
{ serverName: 'cf-agents', transportType: 'auto' as const },
|
||||
|
||||
for (const server of servers) {
|
||||
console.log(`\n🔗 Testing connection to ${server.name} at ${server.url}`)
|
||||
// SSE endpoint with SSE transport (should pass)
|
||||
{ serverName: 'cf-agents-sse', transportType: 'sse' as const },
|
||||
|
||||
const result = await connectToMCPServer(page, server.url)
|
||||
// Additional test cases for HTTP transport
|
||||
{ serverName: 'hono-mcp', transportType: 'http' as const },
|
||||
{ serverName: 'cf-agents', transportType: 'http' as const },
|
||||
|
||||
// Failing case: SSE endpoint with auto transport (should fail)
|
||||
{ serverName: 'cf-agents-sse', transportType: 'auto' as const },
|
||||
]
|
||||
|
||||
test.each(testScenarios)(
|
||||
'should connect to $serverName with $transportType transport (expect: $shouldPass)',
|
||||
async ({ serverName, transportType }) => {
|
||||
const servers = getMCPServers()
|
||||
const server = servers.find((s) => s.name === serverName)
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Server ${serverName} not found. Available servers: ${servers.map((s) => s.name).join(', ')}`)
|
||||
}
|
||||
|
||||
console.log(`\n🔗 Testing connection to ${server.name} at ${server.url} with ${transportType} transport`)
|
||||
|
||||
const result = await connectToMCPServer(page, server.url, transportType)
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Successfully connected to ${server.name}`)
|
||||
|
|
@ -228,8 +263,9 @@ describe('MCP Connection Integration Tests', () => {
|
|||
}
|
||||
|
||||
// Fail the test with detailed information
|
||||
throw new Error(`Failed to connect to ${server.name}. Debug log: ${result.debugLog}`)
|
||||
throw new Error(`Expected to connect to ${server.name} with ${transportType} transport but failed. Debug log: ${result.debugLog}`)
|
||||
}
|
||||
}
|
||||
}, 45000)
|
||||
},
|
||||
45000,
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -113,12 +113,13 @@ function findAvailablePortFromBase(basePort: number): Promise<number> {
|
|||
})
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[], cwd: string): Promise<void> {
|
||||
function runCommand(command: string, args: string[], cwd: string, env?: Record<string, string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: env ? { ...process.env, ...env } : process.env,
|
||||
})
|
||||
|
||||
child.on('close', (code) => {
|
||||
|
|
@ -209,7 +210,7 @@ export default async function globalSetup() {
|
|||
// Step 2: Build inspector example
|
||||
console.log('📦 Building inspector example...')
|
||||
const inspectorDir = join(rootDir, 'examples/inspector')
|
||||
await runCommand('pnpm', ['build'], inspectorDir)
|
||||
await runCommand('pnpm', ['build'], inspectorDir, { NO_MINIFY: 'true' })
|
||||
|
||||
// Step 3: Find available port and start hono-mcp server
|
||||
console.log('🔍 Finding available port starting from 9901...')
|
||||
|
|
|
|||
Loading…
Reference in a new issue