CEMS provides an MCP (Model Context Protocol) wrapper that exposes the Python REST API as MCP tools and resources.
Architecture
The MCP wrapper is a lightweight Express.js server that:
Handles MCP Streamable HTTP transport
Proxies all tool calls to the Python REST API
Runs stateless (no session management)
Returns JSON responses (no SSE streaming)
Services
cems-server Port 8765 - Python REST API (Starlette + uvicorn)
cems-mcp Port 8766 - MCP wrapper (Express.js + TypeScript)
postgres Port 5432 - PostgreSQL with pgvector extension
MCP Wrapper Implementation
Server Setup
// From mcp-wrapper/src/index.ts:52
function createMcpServer ( authHeaders : { authorization ?: string ; teamId ?: string }) {
const server = new McpServer ({
name: "cems-mcp-wrapper" ,
version: "1.0.0" ,
});
// Helper to get auth headers (always from request, no session caching)
const getAuthHeaders = () => authHeaders ;
// Register tools and resources...
return server ;
}
Transport Configuration
// From mcp-wrapper/src/index.ts:415
const transport = new StreamableHTTPServerTransport ({
sessionIdGenerator: undefined , // Stateless mode - no session tracking
enableJsonResponse: true , // JSON response, no SSE streaming
});
Why stateless?
Simpler deployment (no session state to manage)
Works with any HTTP client (no WebSocket/SSE required)
Auth headers extracted from each request
No cleanup needed (no sessions to expire)
The MCP wrapper exposes 6 tools:
memory_add
Store a memory in personal or shared namespace.
// From mcp-wrapper/src/index.ts:62
server . registerTool (
"memory_add" ,
{
title: "Add Memory" ,
description: "Store a memory. Set infer=false for bulk imports (faster)." ,
inputSchema: {
content: z . string (). describe ( "What to remember" ),
scope: z . enum ([ "personal" , "shared" ]). default ( "personal" ),
category: z . string (). default ( "general" ),
tags: z . array ( z . string ()). default ([]),
infer: z . boolean (). default ( true ). describe ( "Use LLM for fact extraction" ),
source_ref: z . string (). optional (). describe ( "Project reference" ),
},
},
async ( args ) => {
// Proxy to Python API
}
);
Usage:
await mcpClient . callTool ( "memory_add" , {
content: "I prefer pytest over unittest for Python testing" ,
scope: "personal" ,
category: "preference" ,
tags: [ "python" , "testing" ],
});
memory_search
Search memories using the full retrieval pipeline.
// From mcp-wrapper/src/index.ts:100
server . registerTool (
"memory_search" ,
{
title: "Search Memories" ,
description: "Search using unified 5-stage retrieval pipeline" ,
inputSchema: {
query: z . string (). describe ( "What to search for" ),
scope: z . enum ([ "personal" , "shared" , "both" ]). default ( "both" ),
max_results: z . number (). default ( 10 ). describe ( "Max results (1-20)" ),
max_tokens: z . number (). default ( 4000 ). describe ( "Token budget" ),
enable_graph: z . boolean (). default ( true ),
enable_query_synthesis: z . boolean (). default ( true ),
raw: z . boolean (). default ( false ). describe ( "Debug mode" ),
project: z . string (). optional (),
},
},
async ( args ) => {
// Proxy to Python API
}
);
Usage:
const results = await mcpClient . callTool ( "memory_search" , {
query: "Python testing preferences" ,
scope: "personal" ,
max_results: 5 ,
project: "myorg/myrepo" ,
});
memory_get
Retrieve full memory document by ID.
// From mcp-wrapper/src/index.ts:168
server . registerTool (
"memory_get" ,
{
title: "Get Memory" ,
description: "Retrieve full document by ID (after search returns truncated)" ,
inputSchema: {
memory_id: z . string (). describe ( "ID of memory to retrieve" ),
},
},
async ( args ) => {
// Proxy to Python API
}
);
memory_forget
Delete or archive a memory.
// From mcp-wrapper/src/index.ts:200
server . registerTool (
"memory_forget" ,
{
title: "Forget Memory" ,
description: "Delete or archive a memory" ,
inputSchema: {
memory_id: z . string (). describe ( "ID of memory to forget" ),
hard_delete: z . boolean (). default ( false ),
},
},
async ( args ) => {
// Proxy to Python API
}
);
memory_update
Update existing memory content.
// From mcp-wrapper/src/index.ts:234
server . registerTool (
"memory_update" ,
{
title: "Update Memory" ,
description: "Update an existing memory's content" ,
inputSchema: {
memory_id: z . string (). describe ( "ID of memory to update" ),
content: z . string (). describe ( "New content" ),
},
},
async ( args ) => {
// Proxy to Python API
}
);
memory_maintenance
Run maintenance jobs on-demand.
// From mcp-wrapper/src/index.ts:266
server . registerTool (
"memory_maintenance" ,
{
title: "Run Maintenance" ,
description: "Run memory maintenance jobs" ,
inputSchema: {
job_type: z . enum ([ "consolidation" , "summarization" , "reindex" , "all" ])
. default ( "consolidation" ),
},
},
async ( args ) => {
// Proxy to Python API
}
);
Usage:
await mcpClient . callTool ( "memory_maintenance" , {
job_type: "consolidation"
});
MCP Resources
The MCP wrapper exposes 3 resources:
memory_status
URI: memory://status
Description: Current status of the memory system
// From mcp-wrapper/src/index.ts:299
server . registerResource (
"memory_status" ,
"memory://status" ,
{
title: "Memory System Status" ,
description: "Current status of the memory system" ,
mimeType: "application/json" ,
},
async ( uri ) => {
// Fetch from Python API /api/memory/status
}
);
memory_personal_summary
URI: memory://personal/summary
Description: Summary of personal memories
// From mcp-wrapper/src/index.ts:333
server . registerResource (
"memory_personal_summary" ,
"memory://personal/summary" ,
{
title: "Personal Memory Summary" ,
mimeType: "application/json" ,
},
async ( uri ) => {
// Fetch from Python API /api/memory/summary/personal
}
);
memory_shared_summary
URI: memory://shared/summary
Description: Summary of shared team memories
// From mcp-wrapper/src/index.ts:367
server . registerResource (
"memory_shared_summary" ,
"memory://shared/summary" ,
{
title: "Shared Memory Summary" ,
mimeType: "application/json" ,
},
async ( uri ) => {
// Fetch from Python API /api/memory/summary/shared
}
);
Client Configuration
Cursor
Add to ~/.cursor/mcp.json:
{
"mcpServers" : {
"cems" : {
"url" : "http://localhost:8766/mcp" ,
"transport" : "streamable-http" ,
"headers" : {
"Authorization" : "Bearer YOUR_API_KEY"
}
}
}
}
Goose
Add to ~/.config/goose/config.yaml:
mcp :
servers :
cems :
url : http://localhost:8766/mcp
transport : streamable-http
headers :
Authorization : Bearer YOUR_API_KEY
Codex
Add to ~/.codex/config.toml:
[[ mcp . servers ]]
name = "cems"
url = "http://localhost:8766/mcp"
transport = "streamable-http"
[ mcp . servers . headers ]
Authorization = "Bearer YOUR_API_KEY"
Custom MCP Client
Use any MCP SDK:
import { MCPClient } from "@modelcontextprotocol/sdk/client" ;
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp" ;
const transport = new StreamableHTTPClientTransport ({
url: "http://localhost:8766/mcp" ,
headers: {
Authorization: `Bearer ${ apiKey } ` ,
},
});
const client = new MCPClient ({
name: "my-app" ,
version: "1.0.0" ,
});
await client . connect ( transport );
// Use tools
const result = await client . callTool ( "memory_search" , {
query: "Python testing" ,
});
Authentication
The MCP wrapper extracts auth headers from each request:
// From mcp-wrapper/src/index.ts:409
const authHeaders = {
authorization: req . headers . authorization as string | undefined ,
teamId: req . headers [ "x-team-id" ] as string | undefined ,
};
Required headers:
Authorization: Bearer <API_KEY> - User API key
x-team-id: <TEAM_ID> - Optional team ID for shared memories
Auth flow:
MCP client sends request with Authorization header
MCP wrapper extracts header
Wrapper forwards header to Python API
Python API validates API key and returns user context
Deployment
Docker Compose
The MCP wrapper is deployed alongside the Python server:
# From docker-compose.yml
services :
cems-server :
build : .
ports :
- "8765:8765"
environment :
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
cems-mcp :
build :
context : ./mcp-wrapper
ports :
- "8766:8766"
environment :
- PYTHON_API_URL=http://cems-server:8765
- PORT=8766
Build & Run
# Start all services
docker compose up -d
# Check MCP wrapper health
curl http://localhost:8766/health
# Check Python API health
curl http://localhost:8765/health
Environment Variables
PYTHON_API_URL
string
default: "http://cems-server:8765"
URL of Python REST API server
Port for MCP wrapper to listen on
Health Checks
MCP Wrapper Health
curl http://localhost:8766/health
Response:
{
"status" : "healthy" ,
"service" : "cems-mcp-wrapper" ,
"python_api" : "healthy"
}
Ping Endpoint
Ultra-lightweight heartbeat check:
curl http://localhost:8766/ping
Response:
Stateless Mode
The MCP wrapper runs in stateless mode:
GET /mcp → 405 Method Not Allowed
// From mcp-wrapper/src/index.ts:445
app . get ( "/mcp" , ( _req , res ) => {
res . status ( 405 )
. set ( 'Allow' , 'POST' )
. send ( 'Method Not Allowed - This is a stateless MCP server. Use POST requests only.' );
});
DELETE /mcp → 405 Method Not Allowed
// From mcp-wrapper/src/index.ts:450
app . delete ( "/mcp" , ( _req , res ) => {
res . status ( 405 )
. set ( 'Allow' , 'POST' )
. send ( 'Method Not Allowed - No session management.' );
});
Why?
MCP spec: stateless servers MUST return 405 for GET/DELETE
Signals to clients: no SSE streaming, no session management
All state in auth headers (no cookies, no sessions)
Troubleshooting
MCP wrapper returns 500 Internal Server Error
Cause: Python API is unreachable or returning errors.Fix:
Check Python API health: curl http://localhost:8765/health
Check Docker network: docker compose ps
Check logs: docker compose logs cems-mcp
Verify PYTHON_API_URL env var points to correct host
MCP client can't connect to wrapper
Cause: Port not exposed or firewall blocking.Fix:
Check port is exposed: docker compose ps (should show 8766:8766)
Test with curl: curl http://localhost:8766/ping
Check firewall: sudo ufw status
If remote: use SSH tunnel: ssh -L 8766:localhost:8766 user@host
Cause: Invalid or missing API key.Fix:
Check API key in client config: cat ~/.cursor/mcp.json
Verify key is valid: cems health --api-key YOUR_KEY
Check Authorization header is being sent
Regenerate key: cems admin reset-key USER_ID
Tools not appearing in MCP client
How do I debug MCP requests?
Enable logging in wrapper: Edit mcp-wrapper/src/index.ts to log requests: app . post ( "/mcp" , async ( req , res ) => {
console . log ( "MCP Request:" , JSON . stringify ( req . body , null , 2 ));
// ...
});
Rebuild and restart: docker compose build cems-mcp
docker compose up -d cems-mcp
docker compose logs -f cems-mcp
Next Steps
Retrieval Tuning Optimize search parameters for better results
Troubleshooting Debug common MCP and API issues