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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ NODE_ENV=development
CIPHER_LOG_LEVEL=info
REDACT_SECRETS=true

# API prefix for routes
# CIPHER_API_PREFIX=""

# ====================
# Storage Configuration
# ====================
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ RUN pnpm install --frozen-lockfile

# Copy source and build
COPY . .
RUN pnpm run build
# Remove the build step (we build UI on host)
# RUN pnpm run build

# Clean up and prepare production node_modules
RUN pnpm prune --prod && \
Expand All @@ -51,6 +52,10 @@ COPY --from=builder --chown=cipher:cipher /app/dist ./dist
COPY --from=builder --chown=cipher:cipher /app/node_modules ./node_modules
COPY --from=builder --chown=cipher:cipher /app/package.json ./
COPY --from=builder --chown=cipher:cipher /app/memAgent ./memAgent
# Copy prebuilt Next.js standalone UI (from host build)
COPY ./dist/src/app/ui/.next/standalone ./ui
COPY ./dist/src/app/ui/.next/static ./ui/.next/static
COPY ./dist/src/app/ui/public ./ui/public

# Create a minimal .env file for Docker (environment variables will be passed via docker)
RUN echo "# Docker environment - variables passed via docker run" > .env
Expand Down
26 changes: 3 additions & 23 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
version: '3.8'

services:
cipher-api:
build: .
ports:
- '3000:3000'
environment:
- PORT=3000
- NODE_ENV=production
- CIPHER_MULTI_BACKEND=1
- CIPHER_PG_URL=postgres://username:password@postgres:5432/cipherdb # Update with your actual credentials
- CIPHER_WAL_FLUSH_INTERVAL=5000 # Optional: flush interval in ms
env_file:
- .env
command:
[
'sh',
'-c',
'node dist/src/app/index.cjs --mode api --port 3000 --host 0.0.0.0 --agent /app/memAgent/cipher.yml',
'node dist/src/app/index.cjs --mode api --port 3000 --host 0.0.0.0 --agent /app/memAgent/cipher.yml --api-prefix "" --mcp-transport-type sse',
]
volumes:
- ./memAgent:/app/memAgent:ro # Mount custom agent config
- cipher-data:/app/.cipher # Persist application data
- ./memAgent:/app/memAgent:ro
- cipher-data:/app/.cipher
restart: unless-stopped
healthcheck:
test:
Expand All @@ -35,18 +27,6 @@ services:
timeout: 10s
retries: 3
start_period: 40s
postgres:
image: postgres:15
restart: unless-stopped
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- '5432:5432'
volumes:
- pgdata:/var/lib/postgresql/data

volumes:
cipher-data:
pgdata:
83 changes: 70 additions & 13 deletions src/app/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ export interface ApiServerConfig {
// WebSocket configuration
enableWebSocket?: boolean;
webSocketConfig?: WebSocketConfig;
// API prefix configuration
apiPrefix?: string;
}

export class ApiServer {
private app: Application;
private agent: MemAgent;
private config: ApiServerConfig;
private apiPrefix: string;
private mcpServer?: McpServer;
private activeMcpSseTransports: Map<string, SSEServerTransport> = new Map();

Expand All @@ -62,6 +65,10 @@ export class ApiServer {
constructor(agent: MemAgent, config: ApiServerConfig) {
this.agent = agent;
this.config = config;

// Validate and set API prefix
this.apiPrefix = this.validateAndNormalizeApiPrefix(config.apiPrefix);

this.app = express();
this.setupMiddleware();
this.setupRoutes();
Expand All @@ -70,6 +77,49 @@ export class ApiServer {
// Note: MCP setup is now handled in start() method to properly handle async operations
}

/**
* Validate and normalize API prefix configuration
*/
private validateAndNormalizeApiPrefix(prefix?: string): string {
// Default to '/api' for backward compatibility
if (prefix === undefined) {
return '/api';
}

// Allow empty string to disable prefix
if (prefix === '') {
return '';
}

// Validate prefix format
if (typeof prefix !== 'string') {
throw new Error('API prefix must be a string');
}

// Ensure prefix starts with '/' if not empty
if (!prefix.startsWith('/')) {
prefix = '/' + prefix;
}

// Remove trailing slash to normalize
if (prefix.endsWith('/') && prefix !== '/') {
prefix = prefix.slice(0, -1);
}

logger.info(`[API Server] Using API prefix: '${prefix || '(none)'}'`);
return prefix;
}

/**
* Helper method to construct API route paths
*/
private buildApiRoute(route: string): string {
if (!this.apiPrefix) {
return route;
}
return `${this.apiPrefix}${route}`;
}

private async setupMcpServer(
transportType: 'stdio' | 'sse' | 'http',
_port?: number
Expand Down Expand Up @@ -258,8 +308,10 @@ export class ApiServer {
}
});

const mcpSseRoute = this.apiPrefix ? `${this.apiPrefix}/mcp/sse` : '/mcp/sse';
const mcpPostRoute = this.apiPrefix ? `${this.apiPrefix}/mcp` : '/mcp';
logger.info(
'[API Server] MCP SSE (GET /mcp/sse) and POST (/mcp?sessionId=...) routes registered.'
`[API Server] MCP SSE (GET ${mcpSseRoute}) and POST (${mcpPostRoute}?sessionId=...) routes registered.`
);
}

Expand Down Expand Up @@ -463,7 +515,10 @@ export class ApiServer {
standardHeaders: true,
legacyHeaders: false,
});
this.app.use('/api/', limiter);
// Apply rate limiting to API routes if prefix is configured
if (this.apiPrefix) {
this.app.use(`${this.apiPrefix}/`, limiter);
}

// Body parsing middleware
this.app.use(express.json({ limit: '10mb' })); // Support for image data
Expand Down Expand Up @@ -520,16 +575,16 @@ export class ApiServer {
});

// API routes
this.app.use('/api/message', createMessageRoutes(this.agent));
this.app.use('/api/sessions', createSessionRoutes(this.agent));
this.app.use('/api/mcp', createMcpRoutes(this.agent));
this.app.use('/api/llm', createLlmRoutes(this.agent));
this.app.use('/api/config', createConfigRoutes(this.agent));
this.app.use('/api/search', createSearchRoutes(this.agent));
this.app.use('/api/webhooks', createWebhookRoutes(this.agent));
this.app.use(this.buildApiRoute('/message'), createMessageRoutes(this.agent));
this.app.use(this.buildApiRoute('/sessions'), createSessionRoutes(this.agent));
this.app.use(this.buildApiRoute('/mcp'), createMcpRoutes(this.agent));
this.app.use(this.buildApiRoute('/llm'), createLlmRoutes(this.agent));
this.app.use(this.buildApiRoute('/config'), createConfigRoutes(this.agent));
this.app.use(this.buildApiRoute('/search'), createSearchRoutes(this.agent));
this.app.use(this.buildApiRoute('/webhooks'), createWebhookRoutes(this.agent));

// Legacy endpoint for MCP server connection
this.app.post('/api/connect-server', (req: Request, res: Response) => {
this.app.post(this.buildApiRoute('/connect-server'), (req: Request, res: Response) => {
// Forward to MCP routes
req.url = '/servers';
createMcpRoutes(this.agent)(req, res, () => {});
Expand All @@ -553,7 +608,7 @@ export class ApiServer {
capabilities: ['conversation', 'memory', 'tools', 'mcp', 'websocket', 'streaming'],
endpoints: {
base: `${req.protocol}://${req.get('host')}`,
api: `${req.protocol}://${req.get('host')}/api`,
api: `${req.protocol}://${req.get('host')}${this.apiPrefix || ''}`,
websocket: `ws://${req.get('host')}/ws`,
health: `${req.protocol}://${req.get('host')}/health`,
},
Expand Down Expand Up @@ -583,7 +638,7 @@ export class ApiServer {
});

// Global reset endpoint
this.app.post('/api/reset', async (req: Request, res: Response) => {
this.app.post(this.buildApiRoute('/reset'), async (req: Request, res: Response) => {
try {
const { sessionId } = req.body;

Expand Down Expand Up @@ -724,8 +779,10 @@ export class ApiServer {
'green'
);
if (this.config.mcpTransportType) {
const mcpSseEndpoint = this.apiPrefix ? `${this.apiPrefix}/mcp/sse` : '/mcp/sse';
const mcpEndpoint = this.apiPrefix ? `${this.apiPrefix}/mcp` : '/mcp';
logger.info(
`[API Server] MCP SSE endpoints available at /mcp/sse and /mcp`,
`[API Server] MCP SSE endpoints available at ${mcpSseEndpoint} and ${mcpEndpoint}`,
null,
'green'
);
Expand Down
17 changes: 17 additions & 0 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ program
.option('--port <port>', 'Port for API server (only used with --mode api or ui)', '3001')
.option('--ui-port <port>', 'Port for UI server (only used with --mode ui)', '3000')
.option('--host <host>', 'Host for API server (only used with --mode api or ui)', 'localhost')
.option(
'--api-prefix <prefix>',
'API prefix for routes (default: /api, use empty string to disable)',
'/api'
)
.option(
'--mcp-transport-type <type>',
'MCP transport type (stdio, sse, streamable-http)',
Expand Down Expand Up @@ -320,6 +325,11 @@ program
const host = options.host || 'localhost';
const mcpTransportType = options.mcpTransportType || undefined; // Pass through from CLI options
const mcpPort = options.mcpPort ? parseInt(options.mcpPort, 10) : undefined; // Pass through from CLI options
// Handle API prefix from environment variable or CLI option
const apiPrefix =
process.env.CIPHER_API_PREFIX !== undefined
? process.env.CIPHER_API_PREFIX
: options.apiPrefix;

logger.info(`Starting API server on ${host}:${port}`, null, 'green');

Expand All @@ -338,6 +348,7 @@ program
heartbeatInterval: 30000, // 30 seconds
enableCompression: true,
},
apiPrefix, // Add API prefix configuration
...(mcpTransportType && { mcpTransportType }), // Only include if defined
...(mcpPort !== undefined && { mcpPort }), // Only include if defined
});
Expand Down Expand Up @@ -365,6 +376,11 @@ program
const host = options.host || 'localhost';
const mcpTransportType = options.mcpTransportType || undefined;
const mcpPort = options.mcpPort ? parseInt(options.mcpPort, 10) : undefined;
// Handle API prefix from environment variable or CLI option
const apiPrefix =
process.env.CIPHER_API_PREFIX !== undefined
? process.env.CIPHER_API_PREFIX
: options.apiPrefix;

logger.info(
`Starting UI mode - API server on ${host}:${apiPort}, UI server on ${host}:${uiPort}`,
Expand All @@ -388,6 +404,7 @@ program
heartbeatInterval: 30000, // 30 seconds
enableCompression: true,
},
apiPrefix, // Add API prefix configuration
...(mcpTransportType && { mcpTransportType }),
...(mcpPort !== undefined && { mcpPort }),
});
Expand Down
12 changes: 8 additions & 4 deletions src/core/brain/tools/unified-tool-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,24 @@
*/
private isEmbeddingRelatedTool(toolName: string): boolean {
const embeddingToolPatterns = [
'extract_and_operate_memory',
'search_memory',
'search_reasoning',
'store_reasoning_memory',
'extract_and_operate_memory',
'extract_reasoning_steps',
'evaluate_reasoning',
'memory_operation',
'knowledge_search',
'vector_search',
'embedding',
'similarity',
'cipher_extract_and_operate_memory',
'cipher_search_memory',
'cipher_search_reasoning_patterns',
'cipher_store_reasoning_memory',
'cipher_extract_and_operate_memory',
// Workspace memory tools
'cipher_extract_reasoning_steps',
'cipher_evaluate_reasoning',
'cipher_search_reasoning_patterns',
// Workspace memory tools still need embeddings
'cipher_workspace_search',
'cipher_workspace_store',
'workspace_search',
Expand Down Expand Up @@ -517,7 +521,7 @@
return !!tool;
}
return false;
} catch (error) {

Check warning on line 524 in src/core/brain/tools/unified-tool-manager.ts

View workflow job for this annotation

GitHub Actions / ESLint

'error' is defined but never used
return false;
}
}
Expand Down
27 changes: 20 additions & 7 deletions src/core/utils/service-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,13 +847,26 @@ export async function createAgentServices(
};
}
} else {
// API Mode: Similar to CLI for now
unifiedToolManagerConfig = {
enableInternalTools: true,
enableMcpTools: true,
conflictResolution: 'prefix-internal',
mode: 'api',
};
// API Mode: Respect MCP_SERVER_MODE like MCP mode does
const mcpServerMode = process.env.MCP_SERVER_MODE || 'default';

if (mcpServerMode === 'aggregator') {
// Aggregator mode: Use aggregator mode for unified tool manager to expose all tools
unifiedToolManagerConfig = {
enableInternalTools: true,
enableMcpTools: true,
conflictResolution: 'prefix-internal',
mode: 'aggregator', // Aggregator mode exposes all tools without filtering
};
} else {
// Default API mode: Similar to CLI
unifiedToolManagerConfig = {
enableInternalTools: true,
enableMcpTools: true,
conflictResolution: 'prefix-internal',
mode: 'api',
};
}
}

const unifiedToolManager = new UnifiedToolManager(
Expand Down
Loading