Files
Keep/mcp-server/index-sse.js
Sepehr Ramezani fa7e166f3e feat: add reminders page, BMad skills upgrade, MCP server refactor
- Add reminders page with navigation support
- Upgrade BMad builder module to skills-based architecture
- Refactor MCP server: extract tools and auth into separate modules
- Add connections cache, custom AI provider support
- Update prisma schema and generated client
- Various UI/UX improvements and i18n updates
- Add service worker for PWA support

Made-with: Cursor
2026-04-13 21:02:53 +02:00

312 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* Memento MCP Server - Streamable HTTP Transport
*
* For remote access (N8N, automation tools, etc.). Runs on Express.
*
* Environment variables:
* PORT - Server port (default: 3001)
* DATABASE_URL - Prisma database URL (default: ../../keep-notes/prisma/dev.db)
* USER_ID - Optional user ID to filter data
* APP_BASE_URL - Optional Next.js app URL for AI features (default: http://localhost:3000)
* MCP_REQUIRE_AUTH - Set to 'true' to require x-api-key or x-user-id header
* MCP_API_KEY - Static API key for authentication (when MCP_REQUIRE_AUTH=true)
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { PrismaClient } from '../keep-notes/prisma/client-generated/index.js';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { randomUUID } from 'crypto';
import express from 'express';
import cors from 'cors';
import { registerTools } from './tools.js';
import { validateApiKey, resolveUser } from './auth.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
// Database path - auto-detect relative to project
const defaultDbPath = join(__dirname, '..', 'keep-notes', 'prisma', 'dev.db');
const databaseUrl = process.env.DATABASE_URL || `file:${defaultDbPath}`;
const prisma = new PrismaClient({
datasources: {
db: { url: databaseUrl },
},
});
const appBaseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
// ── Auth Middleware ──────────────────────────────────────────────────────────
const userSessions = {};
app.use(async (req, res, next) => {
// Dev mode: no auth required
if (process.env.MCP_REQUIRE_AUTH !== 'true') {
req.userSession = { id: 'dev-user', name: 'Development User', isAuth: false };
return next();
}
const apiKey = req.headers['x-api-key'];
const headerUserId = req.headers['x-user-id'];
if (!apiKey && !headerUserId) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide x-api-key header (recommended) or x-user-id header',
});
}
// ── Method 1: API Key (recommended) ──────────────────────────────
if (apiKey) {
// Check DB-stored API keys first
const keyUser = await validateApiKey(prisma, apiKey);
if (keyUser) {
const sessionKey = `key:${keyUser.apiKeyId}`;
if (userSessions[sessionKey]) {
req.userSession = userSessions[sessionKey];
req.userSession.lastSeen = new Date().toISOString();
} else {
req.userSession = {
id: randomUUID(),
name: `${keyUser.userName} (${keyUser.apiKeyName})`,
userId: keyUser.userId,
userName: keyUser.userName,
apiKeyId: keyUser.apiKeyId,
connectedAt: new Date().toISOString(),
lastSeen: new Date().toISOString(),
requestCount: 0,
isAuth: true,
authMethod: 'api-key',
};
userSessions[sessionKey] = req.userSession;
}
return next();
}
// Fallback: static env var key
if (process.env.MCP_API_KEY && apiKey === process.env.MCP_API_KEY) {
const sessionKey = `static:${apiKey.substring(0, 8)}`;
if (userSessions[sessionKey]) {
req.userSession = userSessions[sessionKey];
req.userSession.lastSeen = new Date().toISOString();
} else {
req.userSession = {
id: randomUUID(),
name: 'Static API Key User',
connectedAt: new Date().toISOString(),
lastSeen: new Date().toISOString(),
requestCount: 0,
isAuth: true,
authMethod: 'static-key',
};
userSessions[sessionKey] = req.userSession;
}
return next();
}
return res.status(401).json({ error: 'Invalid API key' });
}
// ── Method 2: User ID header (validate against DB) ──────────────
if (headerUserId) {
const user = await resolveUser(prisma, headerUserId);
if (!user) {
return res.status(401).json({ error: 'User not found', message: `No user matching: ${headerUserId}` });
}
const sessionKey = `user:${user.id}`;
if (userSessions[sessionKey]) {
req.userSession = userSessions[sessionKey];
req.userSession.lastSeen = new Date().toISOString();
} else {
req.userSession = {
id: randomUUID(),
name: user.name,
userId: user.id,
userName: user.name,
userEmail: user.email,
userRole: user.role,
connectedAt: new Date().toISOString(),
lastSeen: new Date().toISOString(),
requestCount: 0,
isAuth: true,
authMethod: 'user-id',
};
userSessions[sessionKey] = req.userSession;
}
return next();
}
return res.status(401).json({ error: 'Authentication failed' });
});
// ── Request Logging ─────────────────────────────────────────────────────────
app.use((req, res, next) => {
if (req.userSession) {
req.userSession.requestCount = (req.userSession.requestCount || 0) + 1;
console.log(`[${req.userSession.id.substring(0, 8)}] ${req.method} ${req.path}`);
}
next();
});
// ── MCP Server Setup ────────────────────────────────────────────────────────
const server = new Server(
{
name: 'memento-mcp-server',
version: '3.0.0',
},
{
capabilities: { tools: {} },
},
);
registerTools(server, prisma, {
userId: process.env.USER_ID || null,
appBaseUrl,
});
// ── HTTP Endpoints ──────────────────────────────────────────────────────────
const transports = {};
// Health check
app.get('/', (req, res) => {
res.json({
name: 'Memento MCP Server',
version: '3.0.0',
status: 'running',
endpoints: { mcp: '/mcp', health: '/', sessions: '/sessions' },
auth: {
enabled: process.env.MCP_REQUIRE_AUTH === 'true',
method: 'x-api-key or x-user-id header',
},
tools: {
notes: 12,
notebooks: 6,
labels: 4,
ai: 11,
reminders: 1,
apiKeys: 3,
total: 37,
},
});
});
// Session status
app.get('/sessions', (req, res) => {
const sessions = Object.values(userSessions).map((s) => ({
id: s.id,
name: s.name,
connectedAt: s.connectedAt,
lastSeen: s.lastSeen,
requestCount: s.requestCount || 0,
}));
res.json({ activeUsers: sessions.length, sessions });
});
// MCP endpoint - Streamable HTTP
app.all('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
let transport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
console.log(`Session initialized: ${id}`);
transports[id] = transport;
},
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(`Session closed: ${sid}`);
delete transports[sid];
}
};
await server.connect(transport);
}
await transport.handleRequest(req, res, req.body);
});
// Legacy /sse redirect for backward compat
app.all('/sse', async (req, res) => {
// Redirect to /mcp
req.url = '/mcp';
return app._router.handle(req, res, () => {
res.status(404).json({ error: 'Not found' });
});
});
// ── Start Server ────────────────────────────────────────────────────────────
app.listen(PORT, '0.0.0.0', () => {
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ Memento MCP Server v3.0.0 (Streamable HTTP) ║
╚═══════════════════════════════════════════════════════════════╝
Server: http://localhost:${PORT}
MCP: http://localhost:${PORT}/mcp
Health: http://localhost:${PORT}/
Sessions: http://localhost:${PORT}/sessions
Database: ${databaseUrl}
App URL: ${appBaseUrl}
User filter: ${process.env.USER_ID || 'none (all data)'}
Auth: ${process.env.MCP_REQUIRE_AUTH === 'true' ? 'ENABLED' : 'DISABLED (dev mode)'}
Tools (37 total):
Notes (12):
create_note, get_notes, get_note, update_note, delete_note,
delete_all_notes, search_notes, move_note, toggle_pin,
toggle_archive, export_notes, import_notes
Notebooks (6):
create_notebook, get_notebooks, get_notebook, update_notebook,
delete_notebook, reorder_notebooks
Labels (4):
create_label, get_labels, update_label, delete_label
AI (11):
generate_title_suggestions, reformulate_text, generate_tags,
suggest_notebook, get_notebook_summary, get_memory_echo,
get_note_connections, dismiss_connection, fuse_notes,
batch_organize, suggest_auto_labels
Reminders (1):
get_due_reminders
API Key Management (3):
generate_api_key, list_api_keys, revoke_api_key
N8N config: SSE endpoint http://YOUR_IP:${PORT}/mcp
Headers: x-api-key or x-user-id
`);
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down MCP server...');
await prisma.$disconnect();
process.exit(0);
});