Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Cyrano/src/http-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ console.error('[HTTP Bridge] Starting module load...');
// ESSENTIAL IMPORTS ONLY - These must load before server starts
// ============================================================================

import express from 'express';
import express, { Request, Response, NextFunction } from 'express';
console.error('[HTTP Bridge] Express imported');
import cors, { CorsOptions } from 'cors';
console.error('[HTTP Bridge] CORS imported');
Expand Down Expand Up @@ -1242,7 +1242,8 @@ app.get('/mcp/tools/info', async (req, res) => {
});

// Catch-all for unknown /mcp routes - always return JSON
app.all('/mcp/*', (req, res) => {
// Using '/mcp/*path' (named wildcard) for compatibility with path-to-regexp v8+
app.all('/mcp/*path', (req, res) => {
res.status(404).json({ isError: true, content: [{ text: `Unknown MCP route: ${req.path}` }] });
});

Expand Down Expand Up @@ -1460,6 +1461,21 @@ app.use('/api', onboardingRoutes);
// Mount beta portal routes
app.use('/api/beta', betaRoutes);

// ============================================================================
// GLOBAL ERROR HANDLER
// ============================================================================

// Must be registered after all routes. Catches any unhandled errors and
// returns a JSON response so tests never receive HTML/DOCTYPE error pages.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error('[HTTP Bridge] Unhandled error:', err.message);
res.status(500).json({
isError: true,
content: [{ type: 'text', text: err.message || 'Internal server error' }],
Comment on lines +1470 to +1475
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This global error handler always responds with HTTP 500, which will incorrectly convert known Express errors (e.g., invalid JSON body parsing sets err.status=400) into 500s. Preserve status when available (err.status/err.statusCode) and sanitize the user-facing message (there’s an existing error-sanitizer utility). Also consider scoping the MCP-shaped { isError, content } response to /mcp routes so /api endpoints don’t unexpectedly return MCP error payloads on unhandled exceptions.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error('[HTTP Bridge] Unhandled error:', err.message);
res.status(500).json({
isError: true,
content: [{ type: 'text', text: err.message || 'Internal server error' }],
function sanitizeErrorMessage(err: Error, status: number): string {
// For client errors, it's generally safe to surface the message (validation, bad input, etc.)
if (status >= 400 && status < 500 && err.message) {
return err.message;
}
// For server errors, avoid leaking internal details
return 'Internal server error';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error('[HTTP Bridge] Unhandled error:', err.message);
const anyErr = err as any;
let status: number | undefined;
if (typeof anyErr.statusCode === 'number') {
status = anyErr.statusCode;
} else if (typeof anyErr.status === 'number') {
status = anyErr.status;
}
if (!status || status < 400 || status > 599) {
status = 500;
}
const isMcpRoute = req.path.startsWith('/mcp');
const message = sanitizeErrorMessage(err, status);
if (isMcpRoute) {
return res.status(status).json({
isError: true,
content: [{ type: 'text', text: message }],
});
}
return res.status(status).json({
error: {
message,
},

Copilot uses AI. Check for mistakes.
});
});

// ============================================================================
// SERVER STARTUP
// ============================================================================
Expand Down
5 changes: 3 additions & 2 deletions Cyrano/tests/mcp-compliance/http-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@
});
});

// Wait a bit for server to be fully ready
await new Promise(resolve => setTimeout(resolve, 100));
// Wait for server to be fully ready before issuing test requests
// Increased from 100ms to 500ms to avoid race condition on slow CI runners
await new Promise(resolve => setTimeout(resolve, 500));

// Get CSRF token for POST requests
try {
Expand Down Expand Up @@ -89,7 +90,7 @@
const errorText = await response.text();
console.error('Error response:', errorText);
}
expect(response.status).toBe(200);

Check failure on line 93 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should respond to GET /mcp/tools

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:93:31

Check failure on line 93 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should respond to GET /mcp/tools

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:93:31

Check failure on line 93 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should respond to GET /mcp/tools

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:93:31
const data = await response.json();
expect(data).toHaveProperty('tools');
expect(Array.isArray(data.tools)).toBe(true);
Expand All @@ -99,13 +100,13 @@
const response = await fetch(`${baseUrl}/mcp/tools`);
const data = await response.json();
// Should have 32+ tools (updated from original 17)
expect(data.tools.length).toBeGreaterThanOrEqual(32);

Check failure on line 103 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should list all registered tools

TypeError: Cannot read properties of undefined (reading 'length') ❯ tests/mcp-compliance/http-bridge.test.ts:103:25

Check failure on line 103 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should list all registered tools

TypeError: Cannot read properties of undefined (reading 'length') ❯ tests/mcp-compliance/http-bridge.test.ts:103:25

Check failure on line 103 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should list all registered tools

TypeError: Cannot read properties of undefined (reading 'length') ❯ tests/mcp-compliance/http-bridge.test.ts:103:25
});

it('should return tools in correct format', async () => {
const response = await fetch(`${baseUrl}/mcp/tools`);
const data = await response.json();
const firstTool = data.tools[0];

Check failure on line 109 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should return tools in correct format

TypeError: Cannot read properties of undefined (reading '0') ❯ tests/mcp-compliance/http-bridge.test.ts:109:30

Check failure on line 109 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should return tools in correct format

TypeError: Cannot read properties of undefined (reading '0') ❯ tests/mcp-compliance/http-bridge.test.ts:109:30

Check failure on line 109 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should return tools in correct format

TypeError: Cannot read properties of undefined (reading '0') ❯ tests/mcp-compliance/http-bridge.test.ts:109:30
expect(firstTool).toHaveProperty('name');
expect(firstTool).toHaveProperty('description');
expect(firstTool).toHaveProperty('inputSchema');
Expand All @@ -124,7 +125,7 @@
input: {}
})
});
expect(response.status).toBe(200);

Check failure on line 128 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should respond to POST /mcp/execute

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:128:31

Check failure on line 128 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should respond to POST /mcp/execute

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:128:31

Check failure on line 128 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > REST API Compliance > should respond to POST /mcp/execute

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:128:31
const data = await response.json();
expect(data).toHaveProperty('content');
});
Expand Down Expand Up @@ -176,7 +177,7 @@
});
const data = await response.json();
expect(data.isError).toBe(true);
expect(data.content[0].text).toContain('not found');

Check failure on line 180 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Error Handling > should handle invalid tool names

AssertionError: expected 'Cannot set property query of #<Incomi…' to contain 'not found' Expected: "not found" Received: "Cannot set property query of #<IncomingMessage> which has only a getter" ❯ tests/mcp-compliance/http-bridge.test.ts:180:36

Check failure on line 180 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Error Handling > should handle invalid tool names

AssertionError: expected 'Cannot set property query of #<Incomi…' to contain 'not found' Expected: "not found" Received: "Cannot set property query of #<IncomingMessage> which has only a getter" ❯ tests/mcp-compliance/http-bridge.test.ts:180:36

Check failure on line 180 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Error Handling > should handle invalid tool names

AssertionError: expected 'Cannot set property query of #<Incomi…' to contain 'not found' Expected: "not found" Received: "Cannot set property query of #<IncomingMessage> which has only a getter" ❯ tests/mcp-compliance/http-bridge.test.ts:180:36
});

it('should handle invalid input schemas', async () => {
Expand Down Expand Up @@ -220,7 +221,7 @@
it('should expose chronometric_module via HTTP', async () => {
const response = await fetch(`${baseUrl}/mcp/tools`);
const data = await response.json();
const chronometricModule = data.tools.find((t: any) => t.name === 'chronometric_module');

Check failure on line 224 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Module Exposure > should expose chronometric_module via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:224:45

Check failure on line 224 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Module Exposure > should expose chronometric_module via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:224:45

Check failure on line 224 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Module Exposure > should expose chronometric_module via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:224:45
expect(chronometricModule).toBeDefined();
});

Expand All @@ -240,7 +241,7 @@
}
})
});
expect(response.status).toBe(200);

Check failure on line 244 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Module Exposure > should execute chronometric_module

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:244:31

Check failure on line 244 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Module Exposure > should execute chronometric_module

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:244:31

Check failure on line 244 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Module Exposure > should execute chronometric_module

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ tests/mcp-compliance/http-bridge.test.ts:244:31
const data = await response.json();
expect(data).toHaveProperty('content');
});
Expand All @@ -250,21 +251,21 @@
it('should expose mae_engine via HTTP', async () => {
const response = await fetch(`${baseUrl}/mcp/tools`);
const data = await response.json();
const maeEngine = data.tools.find((t: any) => t.name === 'mae_engine');

Check failure on line 254 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose mae_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:254:36

Check failure on line 254 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose mae_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:254:36

Check failure on line 254 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose mae_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:254:36
expect(maeEngine).toBeDefined();
});

it('should expose goodcounsel_engine via HTTP', async () => {
const response = await fetch(`${baseUrl}/mcp/tools`);
const data = await response.json();
const goodcounselEngine = data.tools.find((t: any) => t.name === 'goodcounsel_engine');

Check failure on line 261 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose goodcounsel_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:261:44

Check failure on line 261 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose goodcounsel_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:261:44

Check failure on line 261 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose goodcounsel_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:261:44
expect(goodcounselEngine).toBeDefined();
});

it('should expose potemkin_engine via HTTP', async () => {
const response = await fetch(`${baseUrl}/mcp/tools`);
const data = await response.json();
const potemkinEngine = data.tools.find((t: any) => t.name === 'potemkin_engine');

Check failure on line 268 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose potemkin_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:268:41

Check failure on line 268 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose potemkin_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:268:41

Check failure on line 268 in Cyrano/tests/mcp-compliance/http-bridge.test.ts

View workflow job for this annotation

GitHub Actions / Quality Gates

tests/mcp-compliance/http-bridge.test.ts > MCP HTTP Bridge Compliance > Engine Exposure > should expose potemkin_engine via HTTP

TypeError: Cannot read properties of undefined (reading 'find') ❯ tests/mcp-compliance/http-bridge.test.ts:268:41
expect(potemkinEngine).toBeDefined();
});

Expand Down
Loading