perf: optimize MCP handler DB queries and response sizes

- Remove content field from prompts/list DB query (was fetching
  2-10KB per prompt just to extract variables, never returned)
- Drop arguments from prompts/list response (optional per spec,
  clients get variables when they call GetPrompt)
- Replace 500-row slug fallback scan with indexed title query
- Reduce search_prompts contentPreview from 1000 to 300 chars
- Add in-memory auth cache (5-min TTL) for warm function instances
This commit is contained in:
John Pham
2026-03-30 16:26:30 -07:00
parent 9c714ffaed
commit 09aca8cf54
+16 -13
View File
@@ -25,11 +25,20 @@ interface AuthenticatedUser {
mcpPromptsPublicByDefault: boolean;
}
// In-memory auth cache for warm function instances (5-min TTL)
const authCache = new Map<string, { user: AuthenticatedUser | null; expiry: number }>();
const AUTH_CACHE_TTL = 5 * 60 * 1000;
async function authenticateApiKey(apiKey: string | null): Promise<AuthenticatedUser | null> {
if (!apiKey || !isValidApiKeyFormat(apiKey)) {
return null;
}
const cached = authCache.get(apiKey);
if (cached && Date.now() < cached.expiry) {
return cached.user;
}
const user = await db.user.findUnique({
where: { apiKey },
select: {
@@ -39,6 +48,7 @@ async function authenticateApiKey(apiKey: string | null): Promise<AuthenticatedU
},
});
authCache.set(apiKey, { user, expiry: Date.now() + AUTH_CACHE_TTL });
return user;
}
@@ -182,7 +192,6 @@ function createServer(options: ServerOptions = {}) {
slug: true,
title: true,
description: true,
content: true,
},
});
@@ -191,16 +200,10 @@ function createServer(options: ServerOptions = {}) {
return {
prompts: results.map((p) => {
const variables = extractVariables(p.content);
return {
name: getPromptName(p),
title: p.title,
description: p.description || undefined,
arguments: variables.map((v) => ({
name: v.name,
description: v.defaultValue ? `Default: ${v.defaultValue}` : undefined,
required: !v.defaultValue,
})),
};
}),
nextCursor: hasMore ? String(page + 1) : undefined,
@@ -225,15 +228,15 @@ function createServer(options: ServerOptions = {}) {
select: promptSelect,
});
}
// Fallback: lookup by slugified title (for prompts without stored slug)
// Fallback: lookup by title for prompts without stored slug
// Uses indexed DB query instead of loading 500 rows into memory
// TODO: Backfill slug column for all existing prompts so this fallback can be removed
if (!prompt) {
const unslugged = await db.prompt.findMany({
where: { ...promptFilter, slug: null },
const titleGuess = promptSlug.replace(/-/g, " ");
prompt = await db.prompt.findFirst({
where: { ...promptFilter, slug: null, title: { contains: titleGuess, mode: "insensitive" } },
select: promptSelect,
take: 500,
});
prompt = unslugged.find((p) => slugify(p.title) === promptSlug) || null;
}
if (!prompt) {
@@ -342,7 +345,7 @@ function createServer(options: ServerOptions = {}) {
slug: getPromptName(p),
title: p.title,
description: p.description,
contentPreview: p.content.substring(0, 1000) + (p.content.length > 1000 ? '...' : ''),
contentPreview: p.content.substring(0, 300) + (p.content.length > 300 ? '...' : ''),
type: p.type,
author: p.author.name || p.author.username,
category: p.category?.name || null,