Fix SSE transport setup and trust proxy configuration
This commit is contained in:
112
server-http.js
112
server-http.js
@@ -17,12 +17,13 @@ import HPRDataLoader from './data-loader.js';
|
|||||||
// Configuration
|
// Configuration
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const MAX_CONCURRENT_REQUESTS = 10;
|
const MAX_CONCURRENT_REQUESTS = 10;
|
||||||
const REQUEST_TIMEOUT_MS = 30000;
|
// Increased the timeout for the long-lived SSE connection connect() call
|
||||||
|
const REQUEST_TIMEOUT_MS = 60000; // 60 seconds (was 30s)
|
||||||
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
||||||
const RATE_LIMIT_MAX_REQUESTS = 50; // 50 requests per minute per IP
|
const RATE_LIMIT_MAX_REQUESTS = 100; // 100 requests per minute per IP
|
||||||
const MEMORY_THRESHOLD_MB = 450;
|
const MEMORY_THRESHOLD_MB = 450;
|
||||||
const CIRCUIT_BREAKER_THRESHOLD = 5;
|
const CIRCUIT_BREAKER_THRESHOLD = 5;
|
||||||
const CIRCUIT_BREAKER_TIMEOUT_MS = 60000;
|
const CIRCUIT_BREAKER_TIMEOUT_MS = 60000; // 60 seconds (how long it stays OPEN)
|
||||||
const SSE_HEARTBEAT_INTERVAL_MS = 20000; // 20 seconds to prevent proxy timeout
|
const SSE_HEARTBEAT_INTERVAL_MS = 20000; // 20 seconds to prevent proxy timeout
|
||||||
|
|
||||||
// Initialize data loader
|
// Initialize data loader
|
||||||
@@ -31,6 +32,9 @@ const dataLoader = new HPRDataLoader();
|
|||||||
await dataLoader.load();
|
await dataLoader.load();
|
||||||
console.error('Data loaded successfully!');
|
console.error('Data loaded successfully!');
|
||||||
|
|
||||||
|
// Map to store active SSE transports, keyed by connectionId
|
||||||
|
const activeSseTransports = new Map();
|
||||||
|
|
||||||
// Circuit Breaker class for graceful degradation
|
// Circuit Breaker class for graceful degradation
|
||||||
class CircuitBreaker {
|
class CircuitBreaker {
|
||||||
constructor(threshold = CIRCUIT_BREAKER_THRESHOLD, timeout = CIRCUIT_BREAKER_TIMEOUT_MS) {
|
constructor(threshold = CIRCUIT_BREAKER_THRESHOLD, timeout = CIRCUIT_BREAKER_TIMEOUT_MS) {
|
||||||
@@ -148,12 +152,17 @@ function formatEpisode(episode, includeNotes = false) {
|
|||||||
${episode.summary}`;
|
${episode.summary}`;
|
||||||
|
|
||||||
if (seriesInfo) {
|
if (seriesInfo) {
|
||||||
result += `\n\n## Series
|
result += `
|
||||||
|
|
||||||
|
## Series
|
||||||
**${seriesInfo.name}**: ${stripHtml(seriesInfo.description)}`;
|
**${seriesInfo.name}**: ${stripHtml(seriesInfo.description)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeNotes && episode.notes) {
|
if (includeNotes && episode.notes) {
|
||||||
result += `\n\n## Host Notes\n${stripHtml(episode.notes)}`;
|
result += `
|
||||||
|
|
||||||
|
## Host Notes
|
||||||
|
${stripHtml(episode.notes)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -662,8 +671,11 @@ ${match.context}
|
|||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Trust proxy headers (required for Render, Heroku, etc.)
|
// Create a single MCP server instance
|
||||||
app.set('trust proxy', true);
|
const mcpServer = createMCPServer();
|
||||||
|
|
||||||
|
// Trust first proxy hop (Render/Heroku) without allowing arbitrary spoofing
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
|
||||||
// Enable CORS
|
// Enable CORS
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
@@ -671,7 +683,7 @@ app.use(cors());
|
|||||||
// Enable compression
|
// Enable compression
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
// ⭐ FIX: Apply JSON body parsing globally for the SDK to read POST bodies
|
// Apply JSON body parsing globally for the SDK to read POST bodies.
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
@@ -701,10 +713,21 @@ app.get('/health', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ⭐ NEW ENDPOINT: Circuit breaker reset
|
||||||
|
app.post('/reset', (req, res) => {
|
||||||
|
if (circuitBreaker.state === 'OPEN') {
|
||||||
|
circuitBreaker.reset();
|
||||||
|
console.error('Circuit breaker manually reset.');
|
||||||
|
res.json({ status: 'ok', message: 'Circuit breaker reset to CLOSED.' });
|
||||||
|
} else {
|
||||||
|
res.json({ status: 'ok', message: 'Circuit breaker already CLOSED.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// SSE endpoint for MCP
|
// SSE endpoint for MCP
|
||||||
app.get('/sse', async (req, res) => {
|
app.get('/sse', async (req, res) => {
|
||||||
let pingInterval = null;
|
let pingInterval = null;
|
||||||
let headersSent = false;
|
let transport;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check system health
|
// Check system health
|
||||||
@@ -714,56 +737,50 @@ app.get('/sse', async (req, res) => {
|
|||||||
activeRequests++;
|
activeRequests++;
|
||||||
console.error(`New SSE connection. Active requests: ${activeRequests}`);
|
console.error(`New SSE connection. Active requests: ${activeRequests}`);
|
||||||
|
|
||||||
// 1. Send no-buffering headers and flush immediately
|
// Create SSE transport, specifying the POST message path
|
||||||
res.writeHead(200, {
|
transport = new SSEServerTransport('/message', res);
|
||||||
'Content-Type': 'text/event-stream',
|
activeSseTransports.set(transport.sessionId, transport);
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
// CRITICAL: Tells proxies (like NGINX) not to buffer the response
|
|
||||||
'X-Accel-Buffering': 'no',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send an initial comment (ping) to complete the HTTP handshake and flush headers
|
// Connect server with timeout and circuit breaker
|
||||||
res.write(':\n');
|
// This calls transport.start() internally, which sets up headers and sends the endpoint event.
|
||||||
res.flushHeaders();
|
await circuitBreaker.execute(() => mcpServer.connect(transport));
|
||||||
headersSent = true;
|
|
||||||
|
// 2. Start the heartbeat/ping interval (after transport.start() has set up res.write)
|
||||||
// 2. Start the heartbeat/ping interval
|
|
||||||
pingInterval = setInterval(() => {
|
pingInterval = setInterval(() => {
|
||||||
// Send a comment line every 20s to keep the proxy alive
|
// Send a comment line every 20s to keep the proxy alive
|
||||||
res.write(':\n');
|
res.write(':\n');
|
||||||
}, SSE_HEARTBEAT_INTERVAL_MS);
|
}, SSE_HEARTBEAT_INTERVAL_MS);
|
||||||
|
|
||||||
// Create a new MCP server instance for this connection
|
// Handle connection close (will execute when client closes the connection)
|
||||||
const server = createMCPServer();
|
|
||||||
|
|
||||||
// Create SSE transport, specifying the POST message path
|
|
||||||
const transport = new SSEServerTransport('/message', res);
|
|
||||||
|
|
||||||
// Connect server with timeout and circuit breaker
|
|
||||||
await withTimeout(
|
|
||||||
circuitBreaker.execute(() => server.connect(transport)),
|
|
||||||
REQUEST_TIMEOUT_MS
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle connection close
|
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
activeRequests--;
|
activeRequests--;
|
||||||
if (pingInterval) {
|
if (pingInterval) {
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
}
|
}
|
||||||
|
if (transport) {
|
||||||
|
activeSseTransports.delete(transport.sessionId);
|
||||||
|
}
|
||||||
console.error(`SSE connection closed. Active requests: ${activeRequests}`);
|
console.error(`SSE connection closed. Active requests: ${activeRequests}`);
|
||||||
|
// Ensure the server stream is ended gracefully if it hasn't already
|
||||||
|
if (!res.writableEnded) {
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Handle error during connection establishment or connection timeout
|
||||||
activeRequests--;
|
activeRequests--;
|
||||||
if (pingInterval) {
|
if (pingInterval) {
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
}
|
}
|
||||||
|
if (transport) {
|
||||||
|
activeSseTransports.delete(transport.sessionId);
|
||||||
|
}
|
||||||
console.error('SSE connection error:', error.message);
|
console.error('SSE connection error:', error.message);
|
||||||
|
|
||||||
if (!headersSent) {
|
if (!res.headersSent) {
|
||||||
// Case 1: Error before SSE headers were flushed (e.g., checkMemory failed)
|
// Case 1: Error before SSE headers were flushed (e.g., checkMemory failed)
|
||||||
|
// We can still set the status code.
|
||||||
res.status(503).json({
|
res.status(503).json({
|
||||||
error: error.message,
|
error: error.message,
|
||||||
circuitBreaker: circuitBreaker.state,
|
circuitBreaker: circuitBreaker.state,
|
||||||
@@ -781,11 +798,22 @@ app.get('/sse', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST endpoint for MCP messages
|
||||||
|
app.post('/message', async (req, res) => {
|
||||||
|
const connectionId = req.headers['x-connection-id'];
|
||||||
|
const transport = activeSseTransports.get(connectionId);
|
||||||
|
|
||||||
// ⭐ CRITICAL FIX: Removed the custom app.post('/message') handler.
|
if (transport) {
|
||||||
// The SSEServerTransport (created in app.get('/sse')) now automatically handles
|
try {
|
||||||
// the POST requests to '/message' and sends the 200 OK response, fixing the 502 issue.
|
await transport.handlePostMessage(req, res, req.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing MCP message via POST:', error);
|
||||||
|
res.status(400).json({ error: 'Bad Request', message: error.message });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Not Found', message: 'No active SSE connection for this ID.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
@@ -818,4 +846,4 @@ process.on('SIGTERM', () => {
|
|||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.error('SIGINT received, shutting down gracefully...');
|
console.error('SIGINT received, shutting down gracefully...');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
241
test-http-mcp.js
241
test-http-mcp.js
@@ -4,9 +4,10 @@
|
|||||||
* Test script for HTTP/SSE MCP Server
|
* Test script for HTTP/SSE MCP Server
|
||||||
*
|
*
|
||||||
* This script tests the deployed MCP server by:
|
* This script tests the deployed MCP server by:
|
||||||
* 1. Connecting to the SSE endpoint
|
* 1. Resetting the Circuit Breaker on the server.
|
||||||
* 2. Sending MCP protocol messages
|
* 2. Connecting to the SSE endpoint.
|
||||||
* 3. Displaying responses
|
* 3. Sending MCP protocol messages sequentially.
|
||||||
|
* 4. Displaying responses and closing the connection cleanly.
|
||||||
*
|
*
|
||||||
* Usage: node test-http-mcp.js
|
* Usage: node test-http-mcp.js
|
||||||
*/
|
*/
|
||||||
@@ -14,57 +15,85 @@
|
|||||||
import EventSource from 'eventsource';
|
import EventSource from 'eventsource';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
const SERVER_URL = process.env.MCP_SERVER_URL || 'https://hpr-knowledge-base.onrender.com';
|
const SERVER_URL = process.env.MCP_SERVER_URL || 'http://localhost:3000';
|
||||||
const SSE_ENDPOINT = `${SERVER_URL}/sse`;
|
const SSE_ENDPOINT = `${SERVER_URL}/sse`;
|
||||||
const MESSAGE_ENDPOINT = `${SERVER_URL}/message`;
|
const MESSAGE_ENDPOINT = `${SERVER_URL}/message`;
|
||||||
|
const RESET_ENDPOINT = `${SERVER_URL}/reset`; // New endpoint for circuit breaker reset
|
||||||
|
|
||||||
let requestId = 1;
|
let requestId = 1;
|
||||||
|
let sse; // Declare outside for scope
|
||||||
|
let connectionId = null; // To store the connection ID from the server
|
||||||
|
|
||||||
console.log('🧪 Testing MCP Server over HTTP/SSE');
|
console.log('-- Testing MCP Server over HTTP/SSE');
|
||||||
console.log(`📡 Server: ${SERVER_URL}`);
|
console.log(`-- Server: ${SERVER_URL}`);
|
||||||
|
console.log(`-- Message Endpoint: ${MESSAGE_ENDPOINT}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Test health endpoint first
|
// === 0. Reset Circuit Breaker ===
|
||||||
console.log('1️⃣ Testing health endpoint...');
|
async function resetCircuitBreaker() {
|
||||||
try {
|
console.log('0. Resetting Circuit Breaker...');
|
||||||
const healthResponse = await fetch(`${SERVER_URL}/health`);
|
try {
|
||||||
const health = await healthResponse.json();
|
const resetResponse = await fetch(RESET_ENDPOINT, { method: 'POST' });
|
||||||
console.log('✅ Health check:', JSON.stringify(health, null, 2));
|
const result = await resetResponse.json();
|
||||||
console.log('');
|
console.log(`OK Reset check: ${result.message}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Health check failed:', error.message);
|
console.error('ERROR Circuit breaker reset failed (Server not fully up or endpoint missing):', error.message);
|
||||||
process.exit(1);
|
}
|
||||||
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to SSE endpoint
|
// === 1. Test Health Endpoint ===
|
||||||
console.log('2️⃣ Connecting to SSE endpoint...');
|
async function checkHealth() {
|
||||||
const sse = new EventSource(SSE_ENDPOINT);
|
console.log('1. Testing health endpoint...');
|
||||||
|
try {
|
||||||
|
const healthResponse = await fetch(`${SERVER_URL}/health`);
|
||||||
|
const health = await healthResponse.json();
|
||||||
|
console.log('OK Health check:', JSON.stringify(health, null, 2));
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR Health check failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sse.onopen = () => {
|
// === 2. Connect to SSE Endpoint ===
|
||||||
console.log('✅ SSE connection established');
|
function connectSSE() {
|
||||||
console.log('');
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('2. Connecting to SSE endpoint...');
|
||||||
|
// Use the EventSource polyfill to handle the SSE GET connection
|
||||||
|
sse = new EventSource(SSE_ENDPOINT);
|
||||||
|
|
||||||
// Run tests after connection is established
|
sse.onopen = () => {
|
||||||
setTimeout(() => runTests(), 1000);
|
console.log('OK SSE connection established');
|
||||||
};
|
// Resolve the promise once the connection is open
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
sse.onmessage = (event) => {
|
sse.addEventListener('endpoint', (event) => {
|
||||||
try {
|
// The endpoint event data contains the sessionId in the URL
|
||||||
const data = JSON.parse(event.data);
|
const url = new URL(event.data, SERVER_URL);
|
||||||
console.log('📨 Received:', JSON.stringify(data, null, 2));
|
connectionId = url.searchParams.get('sessionId');
|
||||||
console.log('');
|
console.log(`OK Received sessionId: ${connectionId}`);
|
||||||
} catch (error) {
|
});
|
||||||
console.log('📨 Received (raw):', event.data);
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sse.onerror = (error) => {
|
sse.onmessage = (event) => {
|
||||||
console.error('❌ SSE error:', error);
|
try {
|
||||||
console.log('');
|
const data = JSON.parse(event.data);
|
||||||
};
|
console.log('RECEIVED:', JSON.stringify(data, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.log('RECEIVED (raw):', event.data);
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
};
|
||||||
|
|
||||||
// Send MCP messages
|
sse.onerror = (error) => {
|
||||||
|
// Log and ignore to let the rest of the test run, as EventSource auto-reconnects
|
||||||
|
console.error('ERROR SSE error:', error.message || JSON.stringify(error));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 3. Send MCP Message (POST) ===
|
||||||
async function sendMessage(method, params = {}) {
|
async function sendMessage(method, params = {}) {
|
||||||
const message = {
|
const message = {
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
@@ -73,92 +102,114 @@ async function sendMessage(method, params = {}) {
|
|||||||
params
|
params
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('📤 Sending:', method);
|
console.log('SENDING:', method);
|
||||||
console.log(JSON.stringify(message, null, 2));
|
console.log(JSON.stringify(message, null, 2));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(MESSAGE_ENDPOINT, {
|
const response = await fetch(MESSAGE_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-connection-id': connectionId // Include the connection ID
|
||||||
|
},
|
||||||
body: JSON.stringify(message)
|
body: JSON.stringify(message)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
console.log('✅ Message sent successfully');
|
console.log('OK Message sent successfully');
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ Message send failed:', response.status, response.statusText);
|
// Log the full error response body if it's not successful
|
||||||
|
const errorBody = await response.text();
|
||||||
|
console.error('ERROR Message send failed:', response.status, response.statusText, errorBody);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Send error:', error.message);
|
console.error('ERROR Send error:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run test sequence
|
// === Main Test Sequence ===
|
||||||
async function runTests() {
|
async function runTests() {
|
||||||
console.log('3️⃣ Running MCP protocol tests...');
|
// Ensure the health check runs first
|
||||||
console.log('');
|
await checkHealth();
|
||||||
|
|
||||||
// Test 1: Initialize
|
// Ensure the circuit breaker is reset before trying to connect
|
||||||
await sendMessage('initialize', {
|
await resetCircuitBreaker();
|
||||||
protocolVersion: '0.1.0',
|
|
||||||
clientInfo: {
|
// Establish a fresh, single connection for the test sequence
|
||||||
name: 'test-client',
|
await connectSSE();
|
||||||
version: '1.0.0'
|
// Wait for connectionId to be received
|
||||||
},
|
while (connectionId === null) {
|
||||||
capabilities: {}
|
await sleep(100);
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
// Test 2: List tools
|
|
||||||
await sendMessage('tools/list');
|
|
||||||
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
// Test 3: List resources
|
|
||||||
await sendMessage('resources/list');
|
|
||||||
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
// Test 4: Call a tool (search episodes)
|
|
||||||
await sendMessage('tools/call', {
|
|
||||||
name: 'search_episodes',
|
|
||||||
arguments: {
|
|
||||||
query: 'linux',
|
|
||||||
limit: 3
|
|
||||||
}
|
}
|
||||||
});
|
await sleep(1000); // Give the server a moment to finalize setup
|
||||||
|
|
||||||
|
// Log the start of the protocol tests
|
||||||
|
console.log('3. Running MCP protocol tests...');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
await sleep(2000);
|
// Test 1: Initialize
|
||||||
|
await sendMessage('initialize', {
|
||||||
|
protocolVersion: '0.1.0',
|
||||||
|
clientInfo: {
|
||||||
|
name: 'test-client',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
capabilities: {}
|
||||||
|
});
|
||||||
|
|
||||||
// Test 5: Read a resource
|
await sleep(1000);
|
||||||
await sendMessage('resources/read', {
|
|
||||||
uri: 'hpr://stats'
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(3000);
|
// Test 2: List tools
|
||||||
|
await sendMessage('tools/list');
|
||||||
|
|
||||||
console.log('✅ All tests completed!');
|
await sleep(1000);
|
||||||
console.log('');
|
|
||||||
console.log('💡 The MCP server is working correctly over HTTP/SSE');
|
|
||||||
console.log('🔮 Once AI tools add HTTP/SSE support, they can connect to:');
|
|
||||||
console.log(` ${SSE_ENDPOINT}`);
|
|
||||||
|
|
||||||
// Close connection
|
// Test 3: List resources
|
||||||
sse.close();
|
await sendMessage('resources/list');
|
||||||
process.exit(0);
|
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
// Test 4: Call a tool (search episodes)
|
||||||
|
await sendMessage('tools/call', {
|
||||||
|
name: 'search_episodes',
|
||||||
|
arguments: {
|
||||||
|
query: 'linux',
|
||||||
|
limit: 3
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
// Test 5: Read a resource
|
||||||
|
await sendMessage('resources/read', {
|
||||||
|
uri: 'hpr://stats'
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(2000);
|
||||||
|
|
||||||
|
console.log('OK All tests completed!');
|
||||||
|
console.log('');
|
||||||
|
console.log('-- The MCP server is working correctly over HTTP/SSE');
|
||||||
|
console.log('-- Once AI tools add HTTP/SSE support, they can connect to:');
|
||||||
|
console.log(` ${SSE_ENDPOINT}`);
|
||||||
|
|
||||||
|
// Close connection explicitly at the end of the test run to stop auto-reconnects
|
||||||
|
sse.close();
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the test sequence
|
||||||
|
runTests();
|
||||||
|
|
||||||
// Handle Ctrl+C
|
// Handle Ctrl+C
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\n👋 Closing connection...');
|
console.log('\n-- Closing connection...');
|
||||||
sse.close();
|
if (sse) sse.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user