mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-09 06:43:08 +00:00
Merge branch 'main' into kimbo128/main - resolve conflicts
This commit is contained in:
@@ -11,6 +11,8 @@ import { ConsoleManager } from './utils/console-manager';
|
||||
import { logger } from './utils/logger';
|
||||
import { readFileSync } from 'fs';
|
||||
import dotenv from 'dotenv';
|
||||
import { getStartupBaseUrl, formatEndpointUrls, detectBaseUrl } from './utils/url-detector';
|
||||
import { PROJECT_VERSION } from './utils/version';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -244,12 +246,44 @@ export class SingleSessionHTTPServer {
|
||||
next();
|
||||
});
|
||||
|
||||
// Root endpoint with API information
|
||||
app.get('/', (req, res) => {
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const baseUrl = detectBaseUrl(req, host, port);
|
||||
const endpoints = formatEndpointUrls(baseUrl);
|
||||
|
||||
res.json({
|
||||
name: 'n8n Documentation MCP Server',
|
||||
version: PROJECT_VERSION,
|
||||
description: 'Model Context Protocol server providing comprehensive n8n node documentation and workflow management',
|
||||
endpoints: {
|
||||
health: {
|
||||
url: endpoints.health,
|
||||
method: 'GET',
|
||||
description: 'Health check and status information'
|
||||
},
|
||||
mcp: {
|
||||
url: endpoints.mcp,
|
||||
method: 'GET/POST',
|
||||
description: 'MCP endpoint - GET for info, POST for JSON-RPC'
|
||||
}
|
||||
},
|
||||
authentication: {
|
||||
type: 'Bearer Token',
|
||||
header: 'Authorization: Bearer <token>',
|
||||
required_for: ['POST /mcp']
|
||||
},
|
||||
documentation: 'https://github.com/czlonkowski/n8n-mcp'
|
||||
});
|
||||
});
|
||||
|
||||
// Health check endpoint (no body parsing needed for GET)
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
mode: 'single-session',
|
||||
version: '2.3.2',
|
||||
version: PROJECT_VERSION,
|
||||
uptime: Math.floor(process.uptime()),
|
||||
sessionActive: !!this.session,
|
||||
sessionAge: this.session
|
||||
@@ -264,6 +298,35 @@ export class SingleSessionHTTPServer {
|
||||
});
|
||||
});
|
||||
|
||||
// MCP information endpoint (no auth required for discovery)
|
||||
app.get('/mcp', (req, res) => {
|
||||
res.json({
|
||||
description: 'n8n Documentation MCP Server',
|
||||
version: PROJECT_VERSION,
|
||||
endpoints: {
|
||||
mcp: {
|
||||
method: 'POST',
|
||||
path: '/mcp',
|
||||
description: 'Main MCP JSON-RPC endpoint',
|
||||
authentication: 'Bearer token required'
|
||||
},
|
||||
health: {
|
||||
method: 'GET',
|
||||
path: '/health',
|
||||
description: 'Health check endpoint',
|
||||
authentication: 'None'
|
||||
},
|
||||
root: {
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
description: 'API information',
|
||||
authentication: 'None'
|
||||
}
|
||||
},
|
||||
documentation: 'https://github.com/czlonkowski/n8n-mcp'
|
||||
});
|
||||
});
|
||||
|
||||
// Main MCP endpoint with authentication
|
||||
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
// Enhanced authentication check with specific logging
|
||||
@@ -361,9 +424,14 @@ export class SingleSessionHTTPServer {
|
||||
|
||||
this.expressServer = app.listen(port, host, () => {
|
||||
logger.info(`n8n MCP Single-Session HTTP Server started`, { port, host });
|
||||
|
||||
// Detect the base URL using our utility
|
||||
const baseUrl = getStartupBaseUrl(host, port);
|
||||
const endpoints = formatEndpointUrls(baseUrl);
|
||||
|
||||
console.log(`n8n MCP Single-Session HTTP Server running on ${host}:${port}`);
|
||||
console.log(`Health check: http://localhost:${port}/health`);
|
||||
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
||||
console.log(`Health check: ${endpoints.health}`);
|
||||
console.log(`MCP endpoint: ${endpoints.mcp}`);
|
||||
console.log('\nPress Ctrl+C to stop the server');
|
||||
|
||||
// Start periodic warning timer if using default token
|
||||
@@ -375,6 +443,12 @@ export class SingleSessionHTTPServer {
|
||||
}
|
||||
}, 300000); // Every 5 minutes
|
||||
}
|
||||
|
||||
if (process.env.BASE_URL || process.env.PUBLIC_URL) {
|
||||
console.log(`\nPublic URL configured: ${baseUrl}`);
|
||||
} else if (process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
|
||||
console.log(`\nNote: TRUST_PROXY is enabled. URLs will be auto-detected from proxy headers.`);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle server errors
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PROJECT_VERSION } from './utils/version';
|
||||
import { isN8nApiConfigured } from './config/n8n-api';
|
||||
import dotenv from 'dotenv';
|
||||
import { readFileSync } from 'fs';
|
||||
import { getStartupBaseUrl, formatEndpointUrls, detectBaseUrl } from './utils/url-detector';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -159,6 +160,38 @@ export async function startFixedHTTPServer() {
|
||||
const mcpServer = new N8NDocumentationMCPServer();
|
||||
logger.info('Created persistent MCP server instance');
|
||||
|
||||
// Root endpoint with API information
|
||||
app.get('/', (req, res) => {
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
const host = process.env.HOST || '0.0.0.0';
|
||||
const baseUrl = detectBaseUrl(req, host, port);
|
||||
const endpoints = formatEndpointUrls(baseUrl);
|
||||
|
||||
res.json({
|
||||
name: 'n8n Documentation MCP Server',
|
||||
version: PROJECT_VERSION,
|
||||
description: 'Model Context Protocol server providing comprehensive n8n node documentation and workflow management',
|
||||
endpoints: {
|
||||
health: {
|
||||
url: endpoints.health,
|
||||
method: 'GET',
|
||||
description: 'Health check and status information'
|
||||
},
|
||||
mcp: {
|
||||
url: endpoints.mcp,
|
||||
method: 'GET/POST',
|
||||
description: 'MCP endpoint - GET for info, POST for JSON-RPC'
|
||||
}
|
||||
},
|
||||
authentication: {
|
||||
type: 'Bearer Token',
|
||||
header: 'Authorization: Bearer <token>',
|
||||
required_for: ['POST /mcp']
|
||||
},
|
||||
documentation: 'https://github.com/czlonkowski/n8n-mcp'
|
||||
});
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
@@ -195,6 +228,35 @@ export async function startFixedHTTPServer() {
|
||||
}
|
||||
});
|
||||
|
||||
// MCP information endpoint (no auth required for discovery)
|
||||
app.get('/mcp', (req, res) => {
|
||||
res.json({
|
||||
description: 'n8n Documentation MCP Server',
|
||||
version: PROJECT_VERSION,
|
||||
endpoints: {
|
||||
mcp: {
|
||||
method: 'POST',
|
||||
path: '/mcp',
|
||||
description: 'Main MCP JSON-RPC endpoint',
|
||||
authentication: 'Bearer token required'
|
||||
},
|
||||
health: {
|
||||
method: 'GET',
|
||||
path: '/health',
|
||||
description: 'Health check endpoint',
|
||||
authentication: 'None'
|
||||
},
|
||||
root: {
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
description: 'API information',
|
||||
authentication: 'None'
|
||||
}
|
||||
},
|
||||
documentation: 'https://github.com/czlonkowski/n8n-mcp'
|
||||
});
|
||||
});
|
||||
|
||||
// Main MCP endpoint - handle each request with custom transport handling
|
||||
app.post('/mcp', async (req: express.Request, res: express.Response): Promise<void> => {
|
||||
const startTime = Date.now();
|
||||
@@ -428,9 +490,14 @@ export async function startFixedHTTPServer() {
|
||||
|
||||
expressServer = app.listen(port, host, () => {
|
||||
logger.info(`n8n MCP Fixed HTTP Server started`, { port, host });
|
||||
|
||||
// Detect the base URL using our utility
|
||||
const baseUrl = getStartupBaseUrl(host, port);
|
||||
const endpoints = formatEndpointUrls(baseUrl);
|
||||
|
||||
console.log(`n8n MCP Fixed HTTP Server running on ${host}:${port}`);
|
||||
console.log(`Health check: http://localhost:${port}/health`);
|
||||
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
||||
console.log(`Health check: ${endpoints.health}`);
|
||||
console.log(`MCP endpoint: ${endpoints.mcp}`);
|
||||
console.log('\nPress Ctrl+C to stop the server');
|
||||
|
||||
// Start periodic warning timer if using default token
|
||||
@@ -442,6 +509,12 @@ export async function startFixedHTTPServer() {
|
||||
}
|
||||
}, 300000); // Every 5 minutes
|
||||
}
|
||||
|
||||
if (process.env.BASE_URL || process.env.PUBLIC_URL) {
|
||||
console.log(`\nPublic URL configured: ${baseUrl}`);
|
||||
} else if (process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
|
||||
console.log(`\nNote: TRUST_PROXY is enabled. URLs will be auto-detected from proxy headers.`);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
|
||||
@@ -15,7 +15,8 @@ import * as path from 'path';
|
||||
async function rebuild() {
|
||||
console.log('🔄 Rebuilding n8n node database...\n');
|
||||
|
||||
const db = await createDatabaseAdapter('./data/nodes.db');
|
||||
const dbPath = process.env.NODE_DB_PATH || './data/nodes.db';
|
||||
const db = await createDatabaseAdapter(dbPath);
|
||||
const loader = new N8nNodeLoader();
|
||||
const parser = new NodeParser();
|
||||
const mapper = new DocsMapper();
|
||||
|
||||
165
src/scripts/test-issue-45-fix.ts
Normal file
165
src/scripts/test-issue-45-fix.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test for Issue #45 Fix: Partial Update Tool Validation/Execution Discrepancy
|
||||
*
|
||||
* This test verifies that the cleanWorkflowForUpdate function no longer adds
|
||||
* default settings to workflows during updates, which was causing the n8n API
|
||||
* to reject requests with "settings must NOT have additional properties".
|
||||
*/
|
||||
|
||||
import { config } from 'dotenv';
|
||||
import { logger } from '../utils/logger';
|
||||
import { cleanWorkflowForUpdate, cleanWorkflowForCreate } from '../services/n8n-validation';
|
||||
import { Workflow } from '../types/n8n-api';
|
||||
|
||||
// Load environment variables
|
||||
config();
|
||||
|
||||
function testCleanWorkflowFunctions() {
|
||||
logger.info('Testing Issue #45 Fix: cleanWorkflowForUpdate should not add default settings\n');
|
||||
|
||||
// Test 1: cleanWorkflowForUpdate with workflow without settings
|
||||
logger.info('=== Test 1: cleanWorkflowForUpdate without settings ===');
|
||||
const workflowWithoutSettings: Workflow = {
|
||||
id: 'test-123',
|
||||
name: 'Test Workflow',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
versionId: 'version-123'
|
||||
};
|
||||
|
||||
const cleanedUpdate = cleanWorkflowForUpdate(workflowWithoutSettings);
|
||||
|
||||
if ('settings' in cleanedUpdate) {
|
||||
logger.error('❌ FAIL: cleanWorkflowForUpdate added settings when it should not have');
|
||||
logger.error(' Found settings:', JSON.stringify(cleanedUpdate.settings));
|
||||
} else {
|
||||
logger.info('✅ PASS: cleanWorkflowForUpdate did not add settings');
|
||||
}
|
||||
|
||||
// Test 2: cleanWorkflowForUpdate with existing settings
|
||||
logger.info('\n=== Test 2: cleanWorkflowForUpdate with existing settings ===');
|
||||
const workflowWithSettings: Workflow = {
|
||||
...workflowWithoutSettings,
|
||||
settings: {
|
||||
executionOrder: 'v1',
|
||||
saveDataErrorExecution: 'none',
|
||||
saveDataSuccessExecution: 'none',
|
||||
saveManualExecutions: false,
|
||||
saveExecutionProgress: false
|
||||
}
|
||||
};
|
||||
|
||||
const cleanedUpdate2 = cleanWorkflowForUpdate(workflowWithSettings);
|
||||
|
||||
if ('settings' in cleanedUpdate2) {
|
||||
const settingsMatch = JSON.stringify(cleanedUpdate2.settings) === JSON.stringify(workflowWithSettings.settings);
|
||||
if (settingsMatch) {
|
||||
logger.info('✅ PASS: cleanWorkflowForUpdate preserved existing settings without modification');
|
||||
} else {
|
||||
logger.error('❌ FAIL: cleanWorkflowForUpdate modified existing settings');
|
||||
logger.error(' Original:', JSON.stringify(workflowWithSettings.settings));
|
||||
logger.error(' Cleaned:', JSON.stringify(cleanedUpdate2.settings));
|
||||
}
|
||||
} else {
|
||||
logger.error('❌ FAIL: cleanWorkflowForUpdate removed existing settings');
|
||||
}
|
||||
|
||||
// Test 3: cleanWorkflowForUpdate with partial settings
|
||||
logger.info('\n=== Test 3: cleanWorkflowForUpdate with partial settings ===');
|
||||
const workflowWithPartialSettings: Workflow = {
|
||||
...workflowWithoutSettings,
|
||||
settings: {
|
||||
executionOrder: 'v1'
|
||||
// Missing other default properties
|
||||
}
|
||||
};
|
||||
|
||||
const cleanedUpdate3 = cleanWorkflowForUpdate(workflowWithPartialSettings);
|
||||
|
||||
if ('settings' in cleanedUpdate3) {
|
||||
const settingsKeys = cleanedUpdate3.settings ? Object.keys(cleanedUpdate3.settings) : [];
|
||||
const hasOnlyExecutionOrder = settingsKeys.length === 1 &&
|
||||
cleanedUpdate3.settings?.executionOrder === 'v1';
|
||||
if (hasOnlyExecutionOrder) {
|
||||
logger.info('✅ PASS: cleanWorkflowForUpdate preserved partial settings without adding defaults');
|
||||
} else {
|
||||
logger.error('❌ FAIL: cleanWorkflowForUpdate added default properties to partial settings');
|
||||
logger.error(' Original keys:', Object.keys(workflowWithPartialSettings.settings || {}));
|
||||
logger.error(' Cleaned keys:', settingsKeys);
|
||||
}
|
||||
} else {
|
||||
logger.error('❌ FAIL: cleanWorkflowForUpdate removed partial settings');
|
||||
}
|
||||
|
||||
// Test 4: Verify cleanWorkflowForCreate still adds defaults
|
||||
logger.info('\n=== Test 4: cleanWorkflowForCreate should add default settings ===');
|
||||
const newWorkflow = {
|
||||
name: 'New Workflow',
|
||||
nodes: [],
|
||||
connections: {}
|
||||
};
|
||||
|
||||
const cleanedCreate = cleanWorkflowForCreate(newWorkflow);
|
||||
|
||||
if ('settings' in cleanedCreate && cleanedCreate.settings) {
|
||||
const hasDefaults =
|
||||
cleanedCreate.settings.executionOrder === 'v1' &&
|
||||
cleanedCreate.settings.saveDataErrorExecution === 'all' &&
|
||||
cleanedCreate.settings.saveDataSuccessExecution === 'all' &&
|
||||
cleanedCreate.settings.saveManualExecutions === true &&
|
||||
cleanedCreate.settings.saveExecutionProgress === true;
|
||||
|
||||
if (hasDefaults) {
|
||||
logger.info('✅ PASS: cleanWorkflowForCreate correctly adds default settings');
|
||||
} else {
|
||||
logger.error('❌ FAIL: cleanWorkflowForCreate added settings but not with correct defaults');
|
||||
logger.error(' Settings:', JSON.stringify(cleanedCreate.settings));
|
||||
}
|
||||
} else {
|
||||
logger.error('❌ FAIL: cleanWorkflowForCreate did not add default settings');
|
||||
}
|
||||
|
||||
// Test 5: Verify read-only fields are removed
|
||||
logger.info('\n=== Test 5: cleanWorkflowForUpdate removes read-only fields ===');
|
||||
const workflowWithReadOnly: any = {
|
||||
...workflowWithoutSettings,
|
||||
staticData: { some: 'data' },
|
||||
pinData: { node1: 'data' },
|
||||
tags: ['tag1', 'tag2'],
|
||||
isArchived: true,
|
||||
usedCredentials: ['cred1'],
|
||||
sharedWithProjects: ['proj1'],
|
||||
triggerCount: 5,
|
||||
shared: true,
|
||||
active: true
|
||||
};
|
||||
|
||||
const cleanedReadOnly = cleanWorkflowForUpdate(workflowWithReadOnly);
|
||||
|
||||
const removedFields = [
|
||||
'id', 'createdAt', 'updatedAt', 'versionId', 'meta',
|
||||
'staticData', 'pinData', 'tags', 'isArchived',
|
||||
'usedCredentials', 'sharedWithProjects', 'triggerCount',
|
||||
'shared', 'active'
|
||||
];
|
||||
|
||||
const hasRemovedFields = removedFields.some(field => field in cleanedReadOnly);
|
||||
|
||||
if (!hasRemovedFields) {
|
||||
logger.info('✅ PASS: cleanWorkflowForUpdate correctly removed all read-only fields');
|
||||
} else {
|
||||
const foundFields = removedFields.filter(field => field in cleanedReadOnly);
|
||||
logger.error('❌ FAIL: cleanWorkflowForUpdate did not remove these fields:', foundFields);
|
||||
}
|
||||
|
||||
logger.info('\n=== Test Summary ===');
|
||||
logger.info('All tests completed. The fix ensures that cleanWorkflowForUpdate only removes fields');
|
||||
logger.info('without adding default settings, preventing the n8n API validation error.');
|
||||
}
|
||||
|
||||
// Run the tests
|
||||
testCleanWorkflowFunctions();
|
||||
@@ -93,6 +93,19 @@ export function cleanWorkflowForCreate(workflow: Partial<Workflow>): Partial<Wor
|
||||
return cleanedWorkflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean workflow data for update operations.
|
||||
*
|
||||
* This function removes read-only and computed fields that should not be sent
|
||||
* in API update requests. It does NOT add any default values or new fields.
|
||||
*
|
||||
* Note: Unlike cleanWorkflowForCreate, this function does not add default settings.
|
||||
* The n8n API will reject update requests that include properties not present in
|
||||
* the original workflow ("settings must NOT have additional properties" error).
|
||||
*
|
||||
* @param workflow - The workflow object to clean
|
||||
* @returns A cleaned partial workflow suitable for API updates
|
||||
*/
|
||||
export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
|
||||
const {
|
||||
// Remove read-only/computed fields
|
||||
@@ -116,11 +129,6 @@ export function cleanWorkflowForUpdate(workflow: Workflow): Partial<Workflow> {
|
||||
...cleanedWorkflow
|
||||
} = workflow as any;
|
||||
|
||||
// Ensure settings are present
|
||||
if (!cleanedWorkflow.settings) {
|
||||
cleanedWorkflow.settings = defaultWorkflowSettings;
|
||||
}
|
||||
|
||||
return cleanedWorkflow;
|
||||
}
|
||||
|
||||
|
||||
111
src/utils/url-detector.ts
Normal file
111
src/utils/url-detector.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Request } from 'express';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Validates a hostname to prevent header injection attacks
|
||||
*/
|
||||
function isValidHostname(host: string): boolean {
|
||||
// Allow alphanumeric, dots, hyphens, and optional port
|
||||
return /^[a-zA-Z0-9.-]+(:[0-9]+)?$/.test(host) && host.length < 256;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a URL string
|
||||
*/
|
||||
function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// Only allow http and https protocols
|
||||
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the base URL for the server, considering:
|
||||
* 1. Explicitly configured BASE_URL or PUBLIC_URL
|
||||
* 2. Proxy headers (X-Forwarded-Proto, X-Forwarded-Host)
|
||||
* 3. Host and port configuration
|
||||
*/
|
||||
export function detectBaseUrl(req: Request | null, host: string, port: number): string {
|
||||
try {
|
||||
// 1. Check for explicitly configured URL
|
||||
const configuredUrl = process.env.BASE_URL || process.env.PUBLIC_URL;
|
||||
if (configuredUrl) {
|
||||
if (isValidUrl(configuredUrl)) {
|
||||
logger.debug('Using configured BASE_URL/PUBLIC_URL', { url: configuredUrl });
|
||||
return configuredUrl.replace(/\/$/, ''); // Remove trailing slash
|
||||
} else {
|
||||
logger.warn('Invalid BASE_URL/PUBLIC_URL configured, falling back to auto-detection', { url: configuredUrl });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If we have a request, try to detect from proxy headers
|
||||
if (req && process.env.TRUST_PROXY && Number(process.env.TRUST_PROXY) > 0) {
|
||||
const proto = req.get('X-Forwarded-Proto') || req.protocol || 'http';
|
||||
const forwardedHost = req.get('X-Forwarded-Host');
|
||||
const hostHeader = req.get('Host');
|
||||
|
||||
const detectedHost = forwardedHost || hostHeader;
|
||||
if (detectedHost && isValidHostname(detectedHost)) {
|
||||
const baseUrl = `${proto}://${detectedHost}`;
|
||||
logger.debug('Detected URL from proxy headers', {
|
||||
proto,
|
||||
forwardedHost,
|
||||
hostHeader,
|
||||
baseUrl
|
||||
});
|
||||
return baseUrl;
|
||||
} else if (detectedHost) {
|
||||
logger.warn('Invalid hostname detected in proxy headers, using fallback', { detectedHost });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to configured host and port
|
||||
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
||||
const protocol = 'http'; // Default to http for local bindings
|
||||
|
||||
// Don't show standard ports (for http only in this fallback case)
|
||||
const needsPort = port !== 80;
|
||||
const baseUrl = needsPort ?
|
||||
`${protocol}://${displayHost}:${port}` :
|
||||
`${protocol}://${displayHost}`;
|
||||
|
||||
logger.debug('Using fallback URL from host/port', {
|
||||
host,
|
||||
displayHost,
|
||||
port,
|
||||
baseUrl
|
||||
});
|
||||
|
||||
return baseUrl;
|
||||
} catch (error) {
|
||||
logger.error('Error detecting base URL, using fallback', error);
|
||||
// Safe fallback
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base URL for console display during startup
|
||||
* This is used when we don't have a request object yet
|
||||
*/
|
||||
export function getStartupBaseUrl(host: string, port: number): string {
|
||||
return detectBaseUrl(null, host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats endpoint URLs for display
|
||||
*/
|
||||
export function formatEndpointUrls(baseUrl: string): {
|
||||
health: string;
|
||||
mcp: string;
|
||||
root: string;
|
||||
} {
|
||||
return {
|
||||
health: `${baseUrl}/health`,
|
||||
mcp: `${baseUrl}/mcp`,
|
||||
root: baseUrl
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user