fix: align MCP server schema with memento-note + per-request user isolation
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Remove `embedding` column from MCP Note model (dropped by migration 20260425120000) - Add missing columns: trashedAt, dismissedFromRecent, contentUpdatedAt, cardSizeMode - Add NoteEmbedding model and Label.notebook relation - Use AsyncLocalStorage to pass authenticated userId from API key to tool handlers - Enable SSE mode and auth in docker-compose for N8N integration Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,8 @@ services:
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento}
|
||||
- NODE_ENV=production
|
||||
- APP_BASE_URL=http://memento-note:3000
|
||||
- MCP_MODE=sse
|
||||
- MCP_REQUIRE_AUTH=true
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -30,6 +30,7 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { registerTools } from './tools.js';
|
||||
import { validateApiKey, resolveUser } from './auth.js';
|
||||
import { requestContext } from './request-context.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -143,6 +144,7 @@ app.use(async (req, res, next) => {
|
||||
req.userSession = {
|
||||
id: randomUUID(),
|
||||
name: 'Static API Key User',
|
||||
userId: process.env.USER_ID || null,
|
||||
connectedAt: new Date().toISOString(),
|
||||
lastSeen: new Date().toISOString(),
|
||||
requestCount: 0,
|
||||
@@ -231,7 +233,6 @@ const server = new Server(
|
||||
);
|
||||
|
||||
registerTools(server, prisma, {
|
||||
userId: process.env.USER_ID || null,
|
||||
appBaseUrl,
|
||||
});
|
||||
|
||||
@@ -314,8 +315,12 @@ app.all('/mcp', async (req, res) => {
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
// Pass authenticated userId to tool handlers via AsyncLocalStorage
|
||||
const ctx = { userId: req.userSession?.userId || null };
|
||||
await requestContext.run(ctx, async () => {
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
});
|
||||
|
||||
// Legacy /sse redirect for backward compat
|
||||
app.all('/sse', async (req, res) => {
|
||||
@@ -341,7 +346,7 @@ Sessions: http://localhost:${PORT}/sessions
|
||||
|
||||
Database: ${databaseUrl}
|
||||
App URL: ${appBaseUrl}
|
||||
User filter: ${process.env.USER_ID || 'none (all data)'}
|
||||
User filter: per-request (from auth)
|
||||
Auth: ${process.env.MCP_REQUIRE_AUTH === 'true' ? 'ENABLED' : 'DISABLED (dev mode)'}
|
||||
Timeout: ${REQUEST_TIMEOUT}ms
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ model Note {
|
||||
color String @default("default")
|
||||
isPinned Boolean @default(false)
|
||||
isArchived Boolean @default(false)
|
||||
trashedAt DateTime?
|
||||
type String @default("text")
|
||||
dismissedFromRecent Boolean @default(false)
|
||||
checkItems String?
|
||||
labels String?
|
||||
images String?
|
||||
@@ -27,19 +29,30 @@ model Note {
|
||||
reminderLocation String?
|
||||
isMarkdown Boolean @default(false)
|
||||
size String @default("small")
|
||||
embedding String?
|
||||
sharedWith String?
|
||||
userId String?
|
||||
order Int @default(0)
|
||||
notebookId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
contentUpdatedAt DateTime @default(now())
|
||||
autoGenerated Boolean?
|
||||
aiProvider String?
|
||||
aiConfidence Int?
|
||||
language String?
|
||||
languageConfidence Float?
|
||||
lastAiAnalysis DateTime?
|
||||
noteEmbedding NoteEmbedding?
|
||||
}
|
||||
|
||||
model NoteEmbedding {
|
||||
id String @id @default(cuid())
|
||||
noteId String @unique
|
||||
embedding String
|
||||
createdAt DateTime @default(now())
|
||||
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([noteId])
|
||||
}
|
||||
|
||||
model Notebook {
|
||||
@@ -51,6 +64,7 @@ model Notebook {
|
||||
userId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
labels Label[]
|
||||
}
|
||||
|
||||
model Label {
|
||||
@@ -61,6 +75,11 @@ model Label {
|
||||
userId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([notebookId, name])
|
||||
@@index([notebookId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -72,6 +91,7 @@ model User {
|
||||
role String @default("USER")
|
||||
image String?
|
||||
theme String @default("light")
|
||||
cardSizeMode String @default("variable")
|
||||
resetToken String? @unique
|
||||
resetTokenExpiry DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -170,7 +190,8 @@ model UserAISettings {
|
||||
preferredLanguage String @default("auto")
|
||||
fontSize String @default("medium")
|
||||
demoMode Boolean @default(false)
|
||||
showRecentNotes Boolean @default(false)
|
||||
showRecentNotes Boolean @default(true)
|
||||
notesViewMode String @default("masonry")
|
||||
emailNotifications Boolean @default(false)
|
||||
desktopNotifications Boolean @default(false)
|
||||
anonymousAnalytics Boolean @default(false)
|
||||
|
||||
7
mcp-server/request-context.js
Normal file
7
mcp-server/request-context.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
/**
|
||||
* Per-request context storage for passing authenticated userId
|
||||
* from Express middleware to MCP tool handlers.
|
||||
*/
|
||||
export const requestContext = new AsyncLocalStorage();
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { requestContext } from './request-context.js';
|
||||
|
||||
// ─── Configuration ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -603,22 +604,30 @@ const toolDefinitions = [
|
||||
* @param {string} [options.appBaseUrl] - Optional base URL of the Next.js app for AI API calls
|
||||
*/
|
||||
export function registerTools(server, prisma, options = {}) {
|
||||
const { userId = null, appBaseUrl = null } = options;
|
||||
const { appBaseUrl = null } = options;
|
||||
|
||||
// Resolve userId: if not provided, auto-detect the first user
|
||||
let resolvedUserId = userId;
|
||||
let userIdPromise = null;
|
||||
// Resolve userId per-request from AsyncLocalStorage (set by auth middleware)
|
||||
const getResolvedUserId = () => {
|
||||
const store = requestContext.getStore();
|
||||
return store?.userId || null;
|
||||
};
|
||||
|
||||
// Fallback: auto-detect first user when no auth context
|
||||
let fallbackUserId = null;
|
||||
let fallbackPromise = null;
|
||||
|
||||
const ensureUserId = async () => {
|
||||
if (resolvedUserId) return resolvedUserId;
|
||||
if (userIdPromise) return userIdPromise;
|
||||
const fromContext = getResolvedUserId();
|
||||
if (fromContext) return fromContext;
|
||||
if (fallbackUserId) return fallbackUserId;
|
||||
if (fallbackPromise) return fallbackPromise;
|
||||
|
||||
userIdPromise = prisma.user.findFirst({ select: { id: true } }).then(u => {
|
||||
if (u) resolvedUserId = u.id;
|
||||
return resolvedUserId;
|
||||
fallbackPromise = prisma.user.findFirst({ select: { id: true } }).then(u => {
|
||||
if (u) fallbackUserId = u.id;
|
||||
return fallbackUserId;
|
||||
});
|
||||
|
||||
return userIdPromise;
|
||||
return fallbackPromise;
|
||||
};
|
||||
|
||||
// ── List Tools ────────────────────────────────────────────────────────────
|
||||
@@ -629,6 +638,7 @@ export function registerTools(server, prisma, options = {}) {
|
||||
// ── Call Tools ────────────────────────────────────────────────────────────
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
const resolvedUserId = getResolvedUserId();
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
|
||||
Reference in New Issue
Block a user