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:
Basil Hosmer 2025-09-11 23:52:39 -04:00
commit 40773bd051
No known key found for this signature in database

View file

@ -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"
}
});
}
}
}