perf: reduce Vercel costs — 12 optimizations across MCP endpoint, prompts.json, and infra

Addresses $6,478/month Vercel bill with 12 targeted fixes based on forensic
bill analysis and 7-expert review. See #1085 for full analysis.

MCP endpoint (src/pages/api/mcp.ts):
- Fix GetPromptRequestSchema P0 full table scan (findMany→findFirst)
- Remove all JSON pretty-printing (13 occurrences, ~30% payload reduction)
- Add Cache-Control headers to GET discovery endpoint
- Set tools.listChanged=false to prevent client polling
- Add 1MB body size limit to parseBody (DoS protection)
- Make elicitation opt-in via fill_variables param (saves 10s CPU/request)
- Fix timer leak in elicitation setTimeout
- Cap search content at 1,000 chars (contentPreview)
- Remove structuredFormat from search results
- Dynamic import for improve_prompt (reduces cold start)
- Optimize search_skills to return fileNames only

prompts.json:
- Add pagination (?page=&limit=) with backward compatibility
- Add ETag support for 304 Not-Modified responses
- Truncate content to 500-char preview by default

Infrastructure:
- Remove Sentry tunnelRoute (was proxying all browser events through edge)
- Add slug + composite database indexes for MCP query patterns

Closes #1085

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mert Koseoglu
2026-03-19 21:04:34 +03:00
parent cd1715e703
commit 1dbdb88717
4 changed files with 335 additions and 260 deletions
+4 -5
View File
@@ -82,11 +82,10 @@ export default withSentryConfig(withMDX(withNextIntl(nextConfig)), {
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// tunnelRoute removed — was proxying all browser Sentry events through Vercel edge,
// generating unnecessary edge requests ($2.45/M) and function invocations ($0.60/M).
// Estimated savings: $100-400/month. Trade-off: some ad-blockers may block direct Sentry calls.
// See: https://github.com/f/prompts.chat/issues/1085
webpack: {
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
+2
View File
@@ -139,6 +139,8 @@ model Prompt {
@@index([isPrivate])
@@index([isFeatured])
@@index([isUnlisted])
@@index([slug])
@@index([isPrivate, isUnlisted, deletedAt, createdAt(sort: Desc)])
@@map("prompts")
}
+206 -121
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import crypto from "crypto";
function getUserIdentifier(user: {
username: string;
@@ -8,141 +9,225 @@ function getUserIdentifier(user: {
return user.githubUsername || user.username;
}
const CONTENT_PREVIEW_LENGTH = 500;
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 100;
export const revalidate = 3600;
export async function GET() {
function generateETag(count: number, latestUpdatedAt: Date | null): string {
const raw = `${count}-${latestUpdatedAt?.toISOString() ?? "none"}`;
const hash = crypto.createHash("md5").update(raw).digest("hex");
return `"${hash}"`;
}
export async function GET(request: NextRequest) {
try {
const prompts = await db.prompt.findMany({
where: {
isPrivate: false,
isUnlisted: false,
deletedAt: null,
const searchParams = request.nextUrl.searchParams;
const fullContent = searchParams.get("full_content") === "true";
const pageParam = searchParams.get("page");
const limitParam = searchParams.get("limit");
const isPaginated = pageParam !== null || limitParam !== null;
const page = Math.max(1, parseInt(pageParam ?? "1", 10) || 1);
const limit = Math.min(
MAX_LIMIT,
Math.max(1, parseInt(limitParam ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT)
);
const whereClause = {
isPrivate: false,
isUnlisted: false,
deletedAt: null,
};
// Fetch total count and latest updatedAt for ETag generation
const [totalCount, latestPrompt] = await Promise.all([
db.prompt.count({ where: whereClause }),
db.prompt.findFirst({
where: whereClause,
orderBy: { updatedAt: "desc" },
select: { updatedAt: true },
}),
]);
// Generate and check ETag for conditional requests
const etag = generateETag(totalCount, latestPrompt?.updatedAt ?? null);
const ifNoneMatch = request.headers.get("If-None-Match");
if (ifNoneMatch && ifNoneMatch === etag) {
return new NextResponse(null, {
status: 304,
headers: {
ETag: etag,
"Cache-Control":
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
},
});
}
const selectClause = {
id: true,
title: true,
slug: true,
description: true,
content: true,
type: true,
mediaUrl: true,
viewCount: true,
createdAt: true,
updatedAt: true,
structuredFormat: true,
isFeatured: true,
featuredAt: true,
requiresMediaUpload: true,
requiredMediaType: true,
requiredMediaCount: true,
category: {
select: {
id: true,
name: true,
slug: true,
icon: true,
},
},
select: {
id: true,
title: true,
slug: true,
description: true,
content: true,
type: true,
mediaUrl: true,
viewCount: true,
createdAt: true,
updatedAt: true,
structuredFormat: true,
isFeatured: true,
featuredAt: true,
requiresMediaUpload: true,
requiredMediaType: true,
requiredMediaCount: true,
category: {
select: {
id: true,
name: true,
slug: true,
icon: true,
},
author: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
author: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
},
contributors: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
contributors: {
select: {
username: true,
name: true,
avatar: true,
githubUsername: true,
verified: true,
},
},
tags: {
select: {
tag: {
select: {
id: true,
name: true,
slug: true,
color: true,
},
},
tags: {
select: {
tag: {
select: {
id: true,
name: true,
slug: true,
color: true,
},
},
},
_count: {
select: {
votes: true,
comments: true,
},
},
_count: {
select: {
votes: true,
comments: true,
},
},
orderBy: { createdAt: "desc" },
} as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryOptions: any = {
where: whereClause,
select: selectClause,
orderBy: { createdAt: "desc" as const },
};
if (isPaginated) {
queryOptions.skip = (page - 1) * limit;
queryOptions.take = limit;
}
const prompts = await db.prompt.findMany(queryOptions);
const includeFullContent = fullContent;
const formattedPrompts = prompts.map((prompt) => {
const content = prompt.content ?? "";
const contentPreview =
content.length > CONTENT_PREVIEW_LENGTH
? content.slice(0, CONTENT_PREVIEW_LENGTH) + "..."
: content;
return {
id: prompt.id,
title: prompt.title,
slug: prompt.slug,
description: prompt.description,
...(includeFullContent
? { content, contentPreview }
: { contentPreview }),
type: prompt.type,
structuredFormat: prompt.structuredFormat,
mediaUrl: prompt.mediaUrl,
viewCount: prompt.viewCount,
voteCount: prompt._count.votes,
commentCount: prompt._count.comments,
isFeatured: prompt.isFeatured,
featuredAt: prompt.featuredAt,
requiresMediaUpload: prompt.requiresMediaUpload,
requiredMediaType: prompt.requiredMediaType,
requiredMediaCount: prompt.requiredMediaCount,
createdAt: prompt.createdAt,
updatedAt: prompt.updatedAt,
category: prompt.category
? {
id: prompt.category.id,
name: prompt.category.name,
slug: prompt.category.slug,
icon: prompt.category.icon,
}
: null,
author: {
username: prompt.author.username,
name: prompt.author.name,
avatar: prompt.author.avatar,
identifier: getUserIdentifier(prompt.author),
verified: prompt.author.verified,
},
contributors: prompt.contributors.map((c) => ({
username: c.username,
name: c.name,
avatar: c.avatar,
identifier: getUserIdentifier(c),
verified: c.verified,
})),
tags: prompt.tags.map((pt) => ({
id: pt.tag.id,
name: pt.tag.name,
slug: pt.tag.slug,
color: pt.tag.color,
})),
};
});
const formattedPrompts = prompts.map((prompt) => ({
id: prompt.id,
title: prompt.title,
slug: prompt.slug,
description: prompt.description,
content: prompt.content,
type: prompt.type,
structuredFormat: prompt.structuredFormat,
mediaUrl: prompt.mediaUrl,
viewCount: prompt.viewCount,
voteCount: prompt._count.votes,
commentCount: prompt._count.comments,
isFeatured: prompt.isFeatured,
featuredAt: prompt.featuredAt,
requiresMediaUpload: prompt.requiresMediaUpload,
requiredMediaType: prompt.requiredMediaType,
requiredMediaCount: prompt.requiredMediaCount,
createdAt: prompt.createdAt,
updatedAt: prompt.updatedAt,
category: prompt.category
? {
id: prompt.category.id,
name: prompt.category.name,
slug: prompt.category.slug,
icon: prompt.category.icon,
}
: null,
author: {
username: prompt.author.username,
name: prompt.author.name,
avatar: prompt.author.avatar,
identifier: getUserIdentifier(prompt.author),
verified: prompt.author.verified,
},
contributors: prompt.contributors.map((c) => ({
username: c.username,
name: c.name,
avatar: c.avatar,
identifier: getUserIdentifier(c),
verified: c.verified,
})),
tags: prompt.tags.map((pt) => ({
id: pt.tag.id,
name: pt.tag.name,
slug: pt.tag.slug,
color: pt.tag.color,
})),
}));
const totalPages = isPaginated ? Math.ceil(totalCount / limit) : 1;
return NextResponse.json(
{
count: formattedPrompts.length,
prompts: formattedPrompts,
const responseBody = isPaginated
? {
count: totalCount,
page,
limit,
totalPages,
hasMore: page < totalPages,
prompts: formattedPrompts,
}
: {
count: formattedPrompts.length,
prompts: formattedPrompts,
};
return NextResponse.json(responseBody, {
headers: {
"Cache-Control":
"public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
ETag: etag,
},
{
headers: {
"Cache-Control": "public, max-age=3600",
},
}
);
});
} catch (error) {
console.error("prompts.json error:", error);
return NextResponse.json(
+123 -134
View File
@@ -10,7 +10,6 @@ import {
import { z } from "zod";
import { db } from "@/lib/db";
import { isValidApiKeyFormat } from "@/lib/api-key";
import { improvePrompt } from "@/lib/ai/improve-prompt";
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
import appConfig from "@/../prompts.config";
@@ -103,7 +102,7 @@ function createServer(options: ServerOptions = {}) {
{
capabilities: {
prompts: { listChanged: false },
tools: {},
tools: { listChanged: false },
},
}
);
@@ -206,24 +205,18 @@ function createServer(options: ServerOptions = {}) {
const promptSlug = request.params.name;
const args = request.params.arguments || {};
// Fetch all matching prompts and find by slug
const prompts = await db.prompt.findMany({
where: promptFilter,
select: {
id: true,
slug: true,
title: true,
description: true,
content: true,
},
// Try direct lookup by slug first
let prompt = await db.prompt.findFirst({
where: { ...promptFilter, slug: promptSlug },
select: { id: true, slug: true, title: true, description: true, content: true },
});
// Find by slug field first, then by slugified title, then by id
const prompt = prompts.find((p) =>
p.slug === promptSlug ||
slugify(p.title) === promptSlug ||
p.id === promptSlug
);
// Fallback: lookup by id
if (!prompt) {
prompt = await db.prompt.findFirst({
where: { ...promptFilter, id: promptSlug },
select: { id: true, slug: true, title: true, description: true, content: true },
});
}
if (!prompt) {
throw new Error(`Prompt not found: ${promptSlug}`);
@@ -318,7 +311,6 @@ function createServer(options: ServerOptions = {}) {
description: true,
content: true,
type: true,
structuredFormat: true,
createdAt: true,
author: { select: { username: true, name: true } },
category: { select: { name: true, slug: true } },
@@ -332,9 +324,8 @@ function createServer(options: ServerOptions = {}) {
slug: getPromptName(p),
title: p.title,
description: p.description,
content: p.content,
contentPreview: p.content.substring(0, 1000) + (p.content.length > 1000 ? '...' : ''),
type: p.type,
structuredFormat: p.structuredFormat,
author: p.author.name || p.author.username,
category: p.category?.name || null,
tags: p.tags.map((t) => t.tag.name),
@@ -346,7 +337,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify({ query, count: results.length, prompts: results }, null, 2),
text: JSON.stringify({ query, count: results.length, prompts: results }),
},
],
};
@@ -368,9 +359,12 @@ function createServer(options: ServerOptions = {}) {
"Get a prompt by ID and optionally fill in its variables. If the prompt contains template variables (like {{variable}}), you will be asked to provide values for them.",
inputSchema: {
id: z.string().describe("The ID of the prompt to retrieve"),
fill_variables: z.boolean().default(false).describe(
"If true and the prompt has template variables, triggers interactive variable filling. Default false — returns raw prompt with variable metadata."
),
},
},
async ({ id }, extra) => {
async ({ id, fill_variables }, extra) => {
try {
const prompt = await db.prompt.findFirst({
where: {
@@ -402,7 +396,7 @@ function createServer(options: ServerOptions = {}) {
const variables = extractVariables(prompt.content);
if (variables.length > 0) {
if (fill_variables && variables.length > 0) {
const properties: Record<string, PrimitiveSchemaDefinition> = {};
const requiredFields: string[] = [];
for (const variable of variables) {
@@ -436,73 +430,71 @@ function createServer(options: ServerOptions = {}) {
},
ElicitResultSchema
);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs)
);
const result = await Promise.race([elicitationPromise, timeoutPromise]);
if (result.action === "accept" && result.content) {
let filledContent = prompt.content;
for (const [key, value] of Object.entries(result.content)) {
// Replace ${key} or ${key:default} patterns
filledContent = filledContent.replace(
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
String(value)
);
let timeoutId: NodeJS.Timeout;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs);
});
try {
const elicitResult = await Promise.race([elicitationPromise, timeoutPromise]);
clearTimeout(timeoutId!);
if (elicitResult.action === "accept" && elicitResult.content) {
let filledContent = prompt.content;
for (const [key, value] of Object.entries(elicitResult.content)) {
// Replace ${key} or ${key:default} patterns
filledContent = filledContent.replace(
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
String(value)
);
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
...prompt,
content: filledContent,
originalContent: prompt.content,
variables: elicitResult.content,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
}),
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: JSON.stringify({
...prompt,
variablesRequired: variables,
message: "User declined to provide variable values. Returning original prompt.",
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
}),
},
],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...prompt,
content: filledContent,
originalContent: prompt.content,
variables: result.content,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
...prompt,
variablesRequired: variables,
message: "User declined to provide variable values. Returning original prompt.",
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
},
],
};
} catch (e) {
clearTimeout(timeoutId!);
throw e;
}
} catch {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
...prompt,
variablesRequired: variables,
message: "Elicitation not supported. Variables need to be filled manually.",
@@ -510,31 +502,41 @@ function createServer(options: ServerOptions = {}) {
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
}),
},
],
};
}
} else if (variables.length > 0) {
// Return prompt with variable metadata, no timeout
return {
content: [{ type: "text" as const, text: JSON.stringify({
id: prompt.id,
slug: getPromptName(prompt),
title: prompt.title,
description: prompt.description,
content: prompt.content,
type: prompt.type,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
variables: variables.map(v => ({ name: v.name, defaultValue: v.defaultValue })),
hint: "This prompt has template variables. Call get_prompt with fill_variables=true to fill them interactively.",
}) }],
};
}
return {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
...prompt,
author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
null,
2
),
}),
},
],
};
@@ -639,8 +641,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
prompt: {
...prompt,
@@ -648,10 +649,7 @@ function createServer(options: ServerOptions = {}) {
category: prompt.category?.name || null,
link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
},
},
null,
2
),
}),
},
],
};
@@ -693,13 +691,14 @@ function createServer(options: ServerOptions = {}) {
}
try {
const { improvePrompt } = await import("@/lib/ai/improve-prompt");
const result = await improvePrompt({ prompt, outputType, outputFormat });
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
text: JSON.stringify(result),
},
],
};
@@ -810,8 +809,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
skill: {
...skill,
@@ -820,10 +818,7 @@ function createServer(options: ServerOptions = {}) {
category: skill.category?.name || null,
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
},
null,
2
),
}),
},
],
};
@@ -910,17 +905,13 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
message: `File '${filename}' added to skill`,
skillId,
files: files.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1000,17 +991,13 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
message: `File '${filename}' updated in skill`,
skillId,
files: files.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1096,17 +1083,13 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
success: true,
message: `File '${filename}' removed from skill`,
skillId,
files: updatedFiles.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1181,8 +1164,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify(
{
text: JSON.stringify({
id: skill.id,
slug: getPromptName(skill),
title: skill.title,
@@ -1199,10 +1181,7 @@ function createServer(options: ServerOptions = {}) {
content: f.content,
})),
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
},
null,
2
),
}),
},
],
};
@@ -1294,7 +1273,8 @@ function createServer(options: ServerOptions = {}) {
category: s.category?.name || null,
tags: s.tags.map((t) => t.tag.name),
votes: s._count.votes,
files: files.map(f => f.filename),
fileNames: files.map(f => f.filename),
fileCount: files.length,
createdAt: s.createdAt.toISOString(),
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
};
@@ -1304,7 +1284,7 @@ function createServer(options: ServerOptions = {}) {
content: [
{
type: "text" as const,
text: JSON.stringify({ query, count: results.length, skills: results }, null, 2),
text: JSON.stringify({ query, count: results.length, skills: results }),
},
],
};
@@ -1322,9 +1302,17 @@ function createServer(options: ServerOptions = {}) {
}
async function parseBody(req: NextApiRequest): Promise<unknown> {
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("data", (chunk) => {
body += chunk;
if (body.length > MAX_BODY_SIZE) {
req.destroy();
reject(new Error("Body too large"));
return;
}
});
req.on("end", () => {
try {
resolve(JSON.parse(body));
@@ -1342,6 +1330,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
if (req.method === "GET") {
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
return res.status(200).json({
name: "prompts-chat",
version: "1.0.0",