202 lines
6.6 KiB
TypeScript
202 lines
6.6 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
/**
|
|
* Test MCP Server Connection
|
|
* Discovers available tools by calling the MCP server's tools/list endpoint
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json();
|
|
const { url, authToken, headers: customHeaders } = body;
|
|
|
|
if (!url) {
|
|
return NextResponse.json(
|
|
{ error: 'URL is required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
console.log('Testing MCP connection to:', url);
|
|
|
|
// Substitute environment variables in URL
|
|
let resolvedUrl = url;
|
|
const envVarMatch = url.match(/\{([A-Z_]+)\}/g);
|
|
if (envVarMatch) {
|
|
envVarMatch.forEach((match: string) => {
|
|
const envVar = match.slice(1, -1); // Remove { and }
|
|
const envValue = process.env[envVar];
|
|
if (envValue) {
|
|
resolvedUrl = resolvedUrl.replace(match, envValue);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Build headers (some MCP servers require accepting both JSON and SSE)
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json, text/event-stream',
|
|
};
|
|
|
|
// Add custom headers if provided (e.g., Context7's CONTEXT7_API_KEY)
|
|
if (customHeaders) {
|
|
Object.keys(customHeaders).forEach((key) => {
|
|
// Resolve environment variables in header values
|
|
let headerValue = customHeaders[key];
|
|
if (typeof headerValue === 'string') {
|
|
if (headerValue.startsWith('${') && headerValue.endsWith('}')) {
|
|
const envVar = headerValue.slice(2, -1);
|
|
headerValue = process.env[envVar] || headerValue;
|
|
} else if (headerValue.match(/\{([A-Z_]+)\}/)) {
|
|
// Handle format like {API_KEY}
|
|
const envVar = headerValue.replace(/\{|\}/g, '');
|
|
headerValue = process.env[envVar] || headerValue;
|
|
}
|
|
}
|
|
headers[key] = headerValue;
|
|
});
|
|
}
|
|
|
|
// Add Bearer token if provided (legacy support)
|
|
if (authToken && !customHeaders) {
|
|
// Handle environment variable substitution for access tokens
|
|
let resolvedToken = authToken;
|
|
if (authToken.startsWith('${') && authToken.endsWith('}')) {
|
|
const envVar = authToken.slice(2, -1);
|
|
resolvedToken = process.env[envVar] || authToken;
|
|
}
|
|
headers['Authorization'] = `Bearer ${resolvedToken}`;
|
|
}
|
|
|
|
// Call MCP server to list tools
|
|
const mcpRequest = {
|
|
jsonrpc: '2.0',
|
|
method: 'tools/list',
|
|
params: {},
|
|
id: Date.now(),
|
|
};
|
|
|
|
console.log('Making request to:', resolvedUrl);
|
|
console.log('Request:', mcpRequest);
|
|
|
|
const response = await fetch(resolvedUrl, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(mcpRequest),
|
|
signal: AbortSignal.timeout(10000), // 10 second timeout
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('MCP connection failed:', response.status, errorText);
|
|
|
|
let errorMessage = `HTTP ${response.status}`;
|
|
if (response.status === 404) {
|
|
errorMessage = 'MCP server not found - check URL';
|
|
} else if (response.status === 401 || response.status === 403) {
|
|
errorMessage = 'Authentication failed - check access token';
|
|
} else if (response.status === 500) {
|
|
errorMessage = 'Server error - MCP server may be down';
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: errorMessage,
|
|
details: errorText.substring(0, 500),
|
|
statusCode: response.status,
|
|
testedUrl: resolvedUrl, // Show resolved URL for debugging
|
|
}, { status: 200 }); // Return 200 so frontend can show user-friendly error
|
|
}
|
|
|
|
// Parse response: some servers reply with SSE (text/event-stream)
|
|
const contentType = response.headers.get('content-type') || '';
|
|
let result: any;
|
|
if (contentType.includes('text/event-stream')) {
|
|
const text = await response.text();
|
|
// Try to find the last JSON chunk after a 'data:' prefix
|
|
// Example chunk: "event: message\ndata: { ... }\n\n"
|
|
const dataMatches = Array.from(text.matchAll(/\ndata:\s*(\{[\s\S]*?\})\s*(?:\n|$)/g));
|
|
const last = dataMatches.length > 0 ? dataMatches[dataMatches.length - 1][1] : null;
|
|
if (!last) {
|
|
console.error('Failed to parse SSE response as JSON:', text.slice(0, 300));
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Invalid SSE response from MCP server',
|
|
details: text.slice(0, 500),
|
|
testedUrl: resolvedUrl,
|
|
}, { status: 200 });
|
|
}
|
|
try {
|
|
result = JSON.parse(last);
|
|
} catch (e) {
|
|
console.error('SSE JSON parse error:', e);
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Invalid JSON in SSE response',
|
|
details: last.slice(0, 500),
|
|
testedUrl: resolvedUrl,
|
|
}, { status: 200 });
|
|
}
|
|
} else {
|
|
// Regular JSON response
|
|
result = await response.json();
|
|
}
|
|
console.log('MCP response:', result);
|
|
|
|
// Check for JSON-RPC error
|
|
if (result.error) {
|
|
console.error('MCP server returned error:', result.error);
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'MCP server error',
|
|
details: result.error.message || JSON.stringify(result.error),
|
|
testedUrl: resolvedUrl,
|
|
}, { status: 200 });
|
|
}
|
|
|
|
// Extract tools from the response
|
|
const tools = result.result?.tools || result.tools || [];
|
|
|
|
if (!Array.isArray(tools)) {
|
|
console.error('Invalid tools response:', result);
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Invalid response from MCP server',
|
|
details: `Expected tools array, got: ${JSON.stringify(result).substring(0, 200)}`,
|
|
testedUrl: resolvedUrl,
|
|
}, { status: 200 });
|
|
}
|
|
|
|
if (tools.length === 0) {
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'No tools found',
|
|
details: 'MCP server returned empty tools list',
|
|
testedUrl: resolvedUrl,
|
|
}, { status: 200 });
|
|
}
|
|
|
|
// Extract tool names
|
|
const toolNames = tools.map((tool: any) => tool.name || tool);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
tools: toolNames,
|
|
toolsDetailed: tools,
|
|
serverInfo: {
|
|
name: result.result?.name || 'Unknown',
|
|
version: result.result?.version || 'Unknown',
|
|
},
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('MCP connection test error:', error);
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: 'Connection test failed',
|
|
details: error instanceof Error ? error.message : 'Unknown error',
|
|
}, { status: 200 });
|
|
}
|
|
}
|