refactor: remove dangerous, AI proxy, and API key tools from MCP server
- Remove delete_all_notes, 11 AI proxy tools, 3 API key management tools - Clean up unused imports (auth helpers, resolveUser, clearAuthCaches) - Remove fetchWithTimeout helper and appBaseUrl option (no longer needed) - Update tool counts in health endpoint and startup banner (37 → 22 tools) - Keep only CRUD notes/notebooks/labels + reminders + search/export/import Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -232,9 +232,7 @@ const server = new Server(
|
||||
},
|
||||
);
|
||||
|
||||
registerTools(server, prisma, {
|
||||
appBaseUrl,
|
||||
});
|
||||
registerTools(server, prisma);
|
||||
|
||||
// ── HTTP Endpoints ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -252,13 +250,11 @@ app.get('/', (req, res) => {
|
||||
method: 'x-api-key or x-user-id header',
|
||||
},
|
||||
tools: {
|
||||
notes: 12,
|
||||
notes: 11,
|
||||
notebooks: 6,
|
||||
labels: 4,
|
||||
ai: 11,
|
||||
reminders: 1,
|
||||
apiKeys: 3,
|
||||
total: 37,
|
||||
total: 22,
|
||||
},
|
||||
performance: {
|
||||
optimizations: [
|
||||
@@ -358,11 +354,11 @@ Performance Optimizations:
|
||||
✅ Request timeout handling
|
||||
✅ Session cleanup
|
||||
|
||||
Tools (37 total):
|
||||
Notes (12):
|
||||
Tools (22 total):
|
||||
Notes (11):
|
||||
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
|
||||
search_notes, move_note, toggle_pin, toggle_archive,
|
||||
export_notes, import_notes
|
||||
|
||||
Notebooks (6):
|
||||
create_notebook, get_notebooks, get_notebook, update_notebook,
|
||||
@@ -371,18 +367,9 @@ Tools (37 total):
|
||||
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
|
||||
`);
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
* - Connection pooling
|
||||
*/
|
||||
|
||||
import { generateApiKey, listApiKeys, revokeApiKey, resolveUser, validateApiKey, clearAuthCaches } from './auth.js';
|
||||
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
@@ -22,36 +20,11 @@ import { requestContext } from './request-context.js';
|
||||
|
||||
// ─── Configuration ─────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_TIMEOUT = 10000; // 10 seconds for HTTP requests
|
||||
const DEFAULT_SEARCH_LIMIT = 50;
|
||||
const DEFAULT_NOTES_LIMIT = 100;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch with timeout wrapper
|
||||
* Prevents hanging on slow/unresponsive endpoints
|
||||
*/
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseNote(dbNote) {
|
||||
if (!dbNote) return null;
|
||||
return {
|
||||
@@ -207,11 +180,6 @@ const toolDefinitions = [
|
||||
required: ['id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'delete_all_notes',
|
||||
description: 'Delete ALL notes, labels, and notebooks for the configured user. Use with caution.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'search_notes',
|
||||
description: 'Search notes by keyword in title, content, and labels. Returns lightweight format.',
|
||||
@@ -406,145 +374,6 @@ const toolDefinitions = [
|
||||
},
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// AI TOOLS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
{
|
||||
name: 'generate_title_suggestions',
|
||||
description: 'Use AI to generate 3 title suggestions for a note based on its content.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Note content (at least 10 words recommended)' },
|
||||
},
|
||||
required: ['content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reformulate_text',
|
||||
description: 'Use AI to reformulate text: clarify meaning, shorten, or improve writing style.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Text to reformulate' },
|
||||
option: { type: 'string', enum: ['clarify', 'shorten', 'improve'], description: 'Reformulation mode' },
|
||||
},
|
||||
required: ['text', 'option'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'generate_tags',
|
||||
description: 'Use AI to generate tags/labels for content. Optionally contextual to a notebook\'s existing labels.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: { type: 'string', description: 'Content to analyze' },
|
||||
notebookId: { type: 'string', description: 'Notebook ID for contextual tagging (uses existing labels)' },
|
||||
language: { type: 'string', description: 'Language hint (e.g. "en", "fr")', default: 'en' },
|
||||
},
|
||||
required: ['content'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'suggest_notebook',
|
||||
description: 'Use AI to suggest which notebook a note should belong to based on its content.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteContent: { type: 'string', description: 'Note content (at least 20 words recommended)' },
|
||||
language: { type: 'string', default: 'en' },
|
||||
},
|
||||
required: ['noteContent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_notebook_summary',
|
||||
description: 'Generate an AI summary of all notes in a notebook.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
notebookId: { type: 'string', description: 'Notebook ID' },
|
||||
language: { type: 'string', default: 'en' },
|
||||
},
|
||||
required: ['notebookId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'get_memory_echo',
|
||||
description: 'Get the next Memory Echo insight — AI-discovered connections between your notes based on semantic similarity.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'get_note_connections',
|
||||
description: 'Get all semantically related notes (Memory Echo connections) for a specific note. Supports pagination.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', description: 'Note ID' },
|
||||
page: { type: 'number', description: 'Page number (default 1)', default: 1 },
|
||||
limit: { type: 'number', description: 'Results per page (default 10, max 50)', default: 10 },
|
||||
},
|
||||
required: ['noteId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dismiss_connection',
|
||||
description: 'Dismiss a Memory Echo connection between two notes.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteId: { type: 'string', description: 'First note ID' },
|
||||
connectedNoteId: { type: 'string', description: 'Connected note ID to dismiss' },
|
||||
},
|
||||
required: ['noteId', 'connectedNoteId'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fuse_notes',
|
||||
description: 'Use AI to merge/fuse multiple notes into a single unified note.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
noteIds: {
|
||||
type: 'array',
|
||||
description: 'Array of note IDs to fuse (minimum 2)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
prompt: { type: 'string', description: 'Optional instructions for the fusion' },
|
||||
},
|
||||
required: ['noteIds'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'batch_organize',
|
||||
description: 'Create an AI-powered organization plan to move inbox notes into appropriate notebooks, or apply a previously created plan.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: { type: 'string', enum: ['create_plan', 'apply_plan'], description: 'Create a plan or apply it' },
|
||||
language: { type: 'string', default: 'en' },
|
||||
plan: { type: 'object', description: 'The plan object (required for apply_plan)' },
|
||||
selectedNoteIds: { type: 'array', items: { type: 'string' }, description: 'Note IDs to move (required for apply_plan)' },
|
||||
},
|
||||
required: ['action'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'suggest_auto_labels',
|
||||
description: 'Use AI to suggest new labels for a notebook based on its notes\' content (requires 15+ notes), or create selected labels.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: { type: 'string', enum: ['suggest', 'create'], description: 'Suggest labels or create selected ones' },
|
||||
notebookId: { type: 'string', description: 'Notebook ID' },
|
||||
language: { type: 'string', default: 'en' },
|
||||
selectedLabels: { type: 'array', items: { type: 'string' }, description: 'Label names to create (for create action)' },
|
||||
suggestions: { type: 'object', description: 'Suggestions object from suggest step (for create action)' },
|
||||
},
|
||||
required: ['action', 'notebookId'],
|
||||
},
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// REMINDER TOOLS
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -553,43 +382,6 @@ const toolDefinitions = [
|
||||
description: 'Get all notes with due reminders that haven\'t been processed yet. Designed for cron/automation use.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// API KEY MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
{
|
||||
name: 'generate_api_key',
|
||||
description: 'Generate a new API key for a user. The raw key is only shown once — store it securely.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Human-readable name for this key (e.g. "N8N Automation")' },
|
||||
userId: { type: 'string', description: 'User ID to link this key to. If omitted, uses the default user.' },
|
||||
userEmail: { type: 'string', description: 'Alternatively, identify the user by email.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'list_api_keys',
|
||||
description: 'List all API keys (without revealing the actual keys). Can filter by user.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', description: 'Filter keys by user ID' },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'revoke_api_key',
|
||||
description: 'Revoke an API key by its short ID. The key will no longer be usable.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
shortId: { type: 'string', description: 'The short ID of the key (e.g. "a1b2c3d4")' },
|
||||
},
|
||||
required: ['shortId'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Tool Handlers ──────────────────────────────────────────────────────────
|
||||
@@ -599,12 +391,8 @@ const toolDefinitions = [
|
||||
*
|
||||
* @param {import('@modelcontextprotocol/sdk/server/index.js').Server} server
|
||||
* @param {import('@prisma/client').PrismaClient} prisma
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.userId] - Optional user ID for multi-user filtering
|
||||
* @param {string} [options.appBaseUrl] - Optional base URL of the Next.js app for AI API calls
|
||||
*/
|
||||
export function registerTools(server, prisma, options = {}) {
|
||||
const { appBaseUrl = null } = options;
|
||||
export function registerTools(server, prisma) {
|
||||
|
||||
// Resolve userId per-request from AsyncLocalStorage (set by auth middleware)
|
||||
const getResolvedUserId = () => {
|
||||
@@ -733,23 +521,6 @@ export function registerTools(server, prisma, options = {}) {
|
||||
return textResult({ success: true, message: 'Note deleted' });
|
||||
}
|
||||
|
||||
case 'delete_all_notes': {
|
||||
const where = {};
|
||||
if (resolvedUserId) where.userId = resolvedUserId;
|
||||
|
||||
const [deletedNotes] = await prisma.$transaction([
|
||||
prisma.note.deleteMany({ where }),
|
||||
resolvedUserId
|
||||
? prisma.label.deleteMany({ where: { notebook: { userId: resolvedUserId } } })
|
||||
: prisma.label.deleteMany({}),
|
||||
resolvedUserId
|
||||
? prisma.notebook.deleteMany({ where: { userId: resolvedUserId } })
|
||||
: prisma.notebook.deleteMany({}),
|
||||
]);
|
||||
|
||||
return textResult({ success: true, deletedNotes: deletedNotes.count });
|
||||
}
|
||||
|
||||
case 'search_notes': {
|
||||
const where = {
|
||||
isArchived: args.includeArchived || false,
|
||||
@@ -1170,165 +941,6 @@ export function registerTools(server, prisma, options = {}) {
|
||||
return textResult({ success: true, message: 'Label deleted' });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// AI TOOLS - OPTIMIZED with timeout
|
||||
// ═══════════════════════════════════════════════════════
|
||||
case 'generate_title_suggestions': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/title-suggestions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: args.content }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Title suggestion failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'reformulate_text': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/reformulate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: args.text, option: args.option }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Reformulation failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'generate_tags': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/tags`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: args.content, notebookId: args.notebookId, language: args.language || 'en' }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Tag generation failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'suggest_notebook': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/suggest-notebook`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ noteContent: args.noteContent, language: args.language || 'en' }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Notebook suggestion failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'get_notebook_summary': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/notebook-summary`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ notebookId: args.notebookId, language: args.language || 'en' }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Summary generation failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'get_memory_echo': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await resp.json();
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'get_note_connections': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const params = new URLSearchParams({ noteId: args.noteId, page: String(args.page || 1), limit: String(Math.min(args.limit || 10, 50)) });
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/connections?${params}`);
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Connection fetch failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'dismiss_connection': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/dismiss`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ noteId: args.noteId, connectedNoteId: args.connectedNoteId }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Dismiss failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'fuse_notes': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
if (!args.noteIds || args.noteIds.length < 2) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, 'At least 2 note IDs required');
|
||||
}
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/fusion`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ noteIds: args.noteIds, prompt: args.prompt }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Fusion failed');
|
||||
return textResult(data);
|
||||
}
|
||||
|
||||
case 'batch_organize': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
if (args.action === 'create_plan') {
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/batch-organize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ language: args.language || 'en' }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Plan creation failed');
|
||||
return textResult(data);
|
||||
} else if (args.action === 'apply_plan') {
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/batch-organize`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ plan: args.plan, selectedNoteIds: args.selectedNoteIds }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Plan application failed');
|
||||
return textResult(data);
|
||||
} else {
|
||||
throw new McpError(ErrorCode.InvalidRequest, 'Invalid action. Use "create_plan" or "apply_plan"');
|
||||
}
|
||||
}
|
||||
|
||||
case 'suggest_auto_labels': {
|
||||
if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features');
|
||||
if (args.action === 'suggest') {
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/auto-labels`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ notebookId: args.notebookId, language: args.language || 'en' }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Label suggestion failed');
|
||||
return textResult(data);
|
||||
} else if (args.action === 'create') {
|
||||
const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/auto-labels`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ suggestions: args.suggestions, selectedLabels: args.selectedLabels }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Label creation failed');
|
||||
return textResult(data);
|
||||
} else {
|
||||
throw new McpError(ErrorCode.InvalidRequest, 'Invalid action. Use "suggest" or "create"');
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// REMINDERS
|
||||
// ═══════════════════════════════════════════════════════
|
||||
@@ -1357,41 +969,6 @@ export function registerTools(server, prisma, options = {}) {
|
||||
return textResult({ success: true, count: reminders.length, reminders });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// API KEY MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════
|
||||
case 'generate_api_key': {
|
||||
// Resolve target user
|
||||
let targetUserId = args?.userId || resolvedUserId;
|
||||
if (!targetUserId && args?.userEmail) {
|
||||
const user = await resolveUser(prisma, args.userEmail);
|
||||
if (user) targetUserId = user.id;
|
||||
}
|
||||
if (!targetUserId) targetUserId = await ensureUserId();
|
||||
|
||||
const result = await generateApiKey(prisma, {
|
||||
name: args?.name,
|
||||
userId: targetUserId,
|
||||
});
|
||||
|
||||
return textResult({
|
||||
warning: 'Store this raw key securely — it will NOT be shown again.',
|
||||
rawKey: result.rawKey,
|
||||
info: result.info,
|
||||
});
|
||||
}
|
||||
|
||||
case 'list_api_keys': {
|
||||
const keys = await listApiKeys(prisma, { userId: args?.userId });
|
||||
return textResult(keys);
|
||||
}
|
||||
|
||||
case 'revoke_api_key': {
|
||||
const revoked = await revokeApiKey(prisma, args.shortId);
|
||||
if (!revoked) throw new McpError(ErrorCode.InvalidRequest, 'Key not found or already revoked');
|
||||
return textResult({ success: true, message: `Key ${args.shortId} revoked` });
|
||||
}
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||
}
|
||||
@@ -1400,7 +977,4 @@ export function registerTools(server, prisma, options = {}) {
|
||||
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export clear caches function for testing
|
||||
export { clearAuthCaches };
|
||||
}
|
||||
Reference in New Issue
Block a user