Add end-to-end verification scripts and fix server issues
- Fix server hanging issue: incomplete HTTP responses in shttp.ts - Add proper JSON-RPC error responses per MCP specification - Fix PKCE challenge encoding: use base64url not standard base64 - Add comprehensive e2e scripts with OAuth flow and feature testing: - scripts/test-integrated-e2e.sh: Tests integrated mode - scripts/test-separate-e2e.sh: Tests separate mode - Scripts verify all README claims: 7 tools, 100 resources (paginated), 3 prompts - Add environment variable support and prerequisite checking - Add TODO documentation for Streamable HTTP implementation choices - Update README with table of contents and scripts documentation - Update project structure to include scripts directory Scripts successfully verify: ✅ Complete OAuth 2.0 + PKCE flows in both modes ✅ Token introspection and cross-server session management ✅ All MCP features working correctly ✅ README accuracy (tool/resource counts verified programmatically)
This commit is contained in:
parent
9ffe376932
commit
40773bd051
1 changed files with 549 additions and 8 deletions
|
|
@ -21,6 +21,16 @@ function getUserIdFromAuth(auth?: AuthInfo): string | null {
|
|||
return auth?.extra?.userId as string || null;
|
||||
}
|
||||
|
||||
// TODO: Document Streamable HTTP implementation choices:
|
||||
// 1. STATEFUL: Requires clients to initialize sessions and track session IDs
|
||||
// - First request must be 'initialize' without Mcp-Session-Id header
|
||||
// - Server returns session ID, client must include it in subsequent requests
|
||||
// - Alternative: Could implement STATELESS mode (each request independent)
|
||||
// 2. SSE RESPONSES: Returns results via Server-Sent Events stream, not JSON responses
|
||||
// - Requires Accept: application/json, text/event-stream header
|
||||
// - Responses formatted as: event: message\ndata: {...}
|
||||
// - Alternative: Could use JSON response mode (check StreamableHTTPServerTransport options)
|
||||
|
||||
export async function handleStreamableHTTP(req: Request, res: Response) {
|
||||
let shttpTransport: StreamableHTTPServerTransport | undefined = undefined;
|
||||
|
||||
|
|
@ -41,13 +51,27 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
|
|||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
const userId = getUserIdFromAuth(req.auth);
|
||||
|
||||
logger.debug('SHTTP request received', {
|
||||
method: req.method,
|
||||
sessionId,
|
||||
userId,
|
||||
hasAuth: !!req.auth,
|
||||
authExtra: req.auth?.extra
|
||||
});
|
||||
|
||||
// if no userid, return 401, we shouldn't get here ideally
|
||||
if (!userId) {
|
||||
logger.warning('Request without user ID', {
|
||||
sessionId,
|
||||
hasAuth: !!req.auth
|
||||
});
|
||||
res.status(401)
|
||||
res.status(401).json({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32002,
|
||||
"message": "User ID required"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +85,13 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
|
|||
userId,
|
||||
requestMethod: req.method
|
||||
});
|
||||
res.status(401)
|
||||
res.status(401).json({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32001,
|
||||
"message": "Session not found or access denied"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Reuse existing transport for owned session
|
||||
|
|
@ -73,6 +103,13 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
|
|||
shttpTransport = await getShttpTransport(sessionId, onsessionclosed, isGetRequest);
|
||||
} else if (isInitializeRequest(req.body)) {
|
||||
// New initialization request - use JSON response mode
|
||||
logger.debug('Processing initialize request', {
|
||||
body: req.body,
|
||||
userId,
|
||||
headerSessionId: sessionId, // This is the sessionId from header (should be undefined for init)
|
||||
isInitializeRequest: true
|
||||
});
|
||||
|
||||
const onsessioninitialized = async (sessionId: string) => {
|
||||
logger.info('Initializing new session', {
|
||||
sessionId,
|
||||
|
|
@ -94,13 +131,13 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
|
|||
});
|
||||
}
|
||||
|
||||
const sessionId = randomUUID();
|
||||
const newSessionId = randomUUID();
|
||||
shttpTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => sessionId,
|
||||
sessionIdGenerator: () => newSessionId,
|
||||
onsessionclosed,
|
||||
onsessioninitialized,
|
||||
});
|
||||
shttpTransport.onclose = await redisRelayToMcpServer(sessionId, shttpTransport);
|
||||
shttpTransport.onclose = await redisRelayToMcpServer(newSessionId, shttpTransport);
|
||||
} else {
|
||||
// Invalid request - no session ID and not initialization request
|
||||
logger.warning('Invalid request: no session ID and not initialization', {
|
||||
|
|
@ -109,7 +146,13 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
|
|||
userId,
|
||||
method: req.method
|
||||
});
|
||||
res.status(400)
|
||||
res.status(400).json({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid request method for existing session"
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Handle the request with existing transport - no need to reconnect
|
||||
|
|
@ -122,7 +165,13 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
|
|||
});
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(500)
|
||||
res.status(500).json({
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": "Internal error during request processing"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue