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) // Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true, widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. // tunnelRoute removed — was proxying all browser Sentry events through Vercel edge,
// This can increase your server load as well as your hosting bill. // generating unnecessary edge requests ($2.45/M) and function invocations ($0.60/M).
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- // Estimated savings: $100-400/month. Trade-off: some ad-blockers may block direct Sentry calls.
// side errors will fail. // See: https://github.com/f/prompts.chat/issues/1085
tunnelRoute: "/monitoring",
webpack: { webpack: {
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) // 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([isPrivate])
@@index([isFeatured]) @@index([isFeatured])
@@index([isUnlisted]) @@index([isUnlisted])
@@index([slug])
@@index([isPrivate, isUnlisted, deletedAt, createdAt(sort: Desc)])
@@map("prompts") @@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 { db } from "@/lib/db";
import crypto from "crypto";
function getUserIdentifier(user: { function getUserIdentifier(user: {
username: string; username: string;
@@ -8,141 +9,225 @@ function getUserIdentifier(user: {
return user.githubUsername || user.username; return user.githubUsername || user.username;
} }
const CONTENT_PREVIEW_LENGTH = 500;
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 100;
export const revalidate = 3600; 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 { try {
const prompts = await db.prompt.findMany({ const searchParams = request.nextUrl.searchParams;
where: { const fullContent = searchParams.get("full_content") === "true";
isPrivate: false, const pageParam = searchParams.get("page");
isUnlisted: false, const limitParam = searchParams.get("limit");
deletedAt: null,
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: { author: {
id: true, select: {
title: true, username: true,
slug: true, name: true,
description: true, avatar: true,
content: true, githubUsername: true,
type: true, verified: 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: { contributors: {
username: true, select: {
name: true, username: true,
avatar: true, name: true,
githubUsername: true, avatar: true,
verified: true, githubUsername: true,
}, verified: true,
}, },
contributors: { },
select: { tags: {
username: true, select: {
name: true, tag: {
avatar: true, select: {
githubUsername: true, id: true,
verified: true, name: true,
}, slug: true,
}, color: true,
tags: {
select: {
tag: {
select: {
id: true,
name: true,
slug: true,
color: true,
},
}, },
}, },
}, },
_count: { },
select: { _count: {
votes: true, select: {
comments: true, 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) => ({ const totalPages = isPaginated ? Math.ceil(totalCount / limit) : 1;
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,
})),
}));
return NextResponse.json( const responseBody = isPaginated
{ ? {
count: formattedPrompts.length, count: totalCount,
prompts: formattedPrompts, 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) { } catch (error) {
console.error("prompts.json error:", error); console.error("prompts.json error:", error);
return NextResponse.json( return NextResponse.json(
+121 -132
View File
@@ -10,7 +10,6 @@ import {
import { z } from "zod"; import { z } from "zod";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { isValidApiKeyFormat } from "@/lib/api-key"; import { isValidApiKeyFormat } from "@/lib/api-key";
import { improvePrompt } from "@/lib/ai/improve-prompt";
import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files"; import { parseSkillFiles, serializeSkillFiles, DEFAULT_SKILL_FILE } from "@/lib/skill-files";
import appConfig from "@/../prompts.config"; import appConfig from "@/../prompts.config";
@@ -103,7 +102,7 @@ function createServer(options: ServerOptions = {}) {
{ {
capabilities: { capabilities: {
prompts: { listChanged: false }, prompts: { listChanged: false },
tools: {}, tools: { listChanged: false },
}, },
} }
); );
@@ -206,24 +205,18 @@ function createServer(options: ServerOptions = {}) {
const promptSlug = request.params.name; const promptSlug = request.params.name;
const args = request.params.arguments || {}; const args = request.params.arguments || {};
// Fetch all matching prompts and find by slug // Try direct lookup by slug first
const prompts = await db.prompt.findMany({ let prompt = await db.prompt.findFirst({
where: promptFilter, where: { ...promptFilter, slug: promptSlug },
select: { select: { id: true, slug: true, title: true, description: true, content: true },
id: true,
slug: true,
title: true,
description: true,
content: true,
},
}); });
// Fallback: lookup by id
// Find by slug field first, then by slugified title, then by id if (!prompt) {
const prompt = prompts.find((p) => prompt = await db.prompt.findFirst({
p.slug === promptSlug || where: { ...promptFilter, id: promptSlug },
slugify(p.title) === promptSlug || select: { id: true, slug: true, title: true, description: true, content: true },
p.id === promptSlug });
); }
if (!prompt) { if (!prompt) {
throw new Error(`Prompt not found: ${promptSlug}`); throw new Error(`Prompt not found: ${promptSlug}`);
@@ -318,7 +311,6 @@ function createServer(options: ServerOptions = {}) {
description: true, description: true,
content: true, content: true,
type: true, type: true,
structuredFormat: true,
createdAt: true, createdAt: true,
author: { select: { username: true, name: true } }, author: { select: { username: true, name: true } },
category: { select: { name: true, slug: true } }, category: { select: { name: true, slug: true } },
@@ -332,9 +324,8 @@ function createServer(options: ServerOptions = {}) {
slug: getPromptName(p), slug: getPromptName(p),
title: p.title, title: p.title,
description: p.description, description: p.description,
content: p.content, contentPreview: p.content.substring(0, 1000) + (p.content.length > 1000 ? '...' : ''),
type: p.type, type: p.type,
structuredFormat: p.structuredFormat,
author: p.author.name || p.author.username, author: p.author.name || p.author.username,
category: p.category?.name || null, category: p.category?.name || null,
tags: p.tags.map((t) => t.tag.name), tags: p.tags.map((t) => t.tag.name),
@@ -346,7 +337,7 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, 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.", "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: { inputSchema: {
id: z.string().describe("The ID of the prompt to retrieve"), 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 { try {
const prompt = await db.prompt.findFirst({ const prompt = await db.prompt.findFirst({
where: { where: {
@@ -402,7 +396,7 @@ function createServer(options: ServerOptions = {}) {
const variables = extractVariables(prompt.content); const variables = extractVariables(prompt.content);
if (variables.length > 0) { if (fill_variables && variables.length > 0) {
const properties: Record<string, PrimitiveSchemaDefinition> = {}; const properties: Record<string, PrimitiveSchemaDefinition> = {};
const requiredFields: string[] = []; const requiredFields: string[] = [];
for (const variable of variables) { for (const variable of variables) {
@@ -437,72 +431,70 @@ function createServer(options: ServerOptions = {}) {
ElicitResultSchema ElicitResultSchema
); );
const timeoutPromise = new Promise<never>((_, reject) => let timeoutId: NodeJS.Timeout;
setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs) const timeoutPromise = new Promise<never>((_, reject) => {
); timeoutId = setTimeout(() => reject(new Error("Elicitation timeout")), timeoutMs);
});
const result = await Promise.race([elicitationPromise, timeoutPromise]); try {
const elicitResult = await Promise.race([elicitationPromise, timeoutPromise]);
clearTimeout(timeoutId!);
if (result.action === "accept" && result.content) { if (elicitResult.action === "accept" && elicitResult.content) {
let filledContent = prompt.content; let filledContent = prompt.content;
for (const [key, value] of Object.entries(result.content)) { for (const [key, value] of Object.entries(elicitResult.content)) {
// Replace ${key} or ${key:default} patterns // Replace ${key} or ${key:default} patterns
filledContent = filledContent.replace( filledContent = filledContent.replace(
new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"), new RegExp(`\\$\\{${key}(?::[^}]*)?\\}`, "g"),
String(value) 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)}`,
}),
},
],
};
} }
} catch (e) {
return { clearTimeout(timeoutId!);
content: [ throw e;
{
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 { } catch {
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
...prompt, ...prompt,
variablesRequired: variables, variablesRequired: variables,
message: "Elicitation not supported. Variables need to be filled manually.", message: "Elicitation not supported. Variables need to be filled manually.",
@@ -510,31 +502,41 @@ function createServer(options: ServerOptions = {}) {
category: prompt.category?.name || null, category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name), tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`, 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 { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
...prompt, ...prompt,
author: prompt.author.name || prompt.author.username, author: prompt.author.name || prompt.author.username,
category: prompt.category?.name || null, category: prompt.category?.name || null,
tags: prompt.tags.map((t) => t.tag.name), tags: prompt.tags.map((t) => t.tag.name),
link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`, link: `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
}, }),
null,
2
),
}, },
], ],
}; };
@@ -639,8 +641,7 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
success: true, success: true,
prompt: { prompt: {
...prompt, ...prompt,
@@ -648,10 +649,7 @@ function createServer(options: ServerOptions = {}) {
category: prompt.category?.name || null, category: prompt.category?.name || null,
link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`, link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`,
}, },
}, }),
null,
2
),
}, },
], ],
}; };
@@ -693,13 +691,14 @@ function createServer(options: ServerOptions = {}) {
} }
try { try {
const { improvePrompt } = await import("@/lib/ai/improve-prompt");
const result = await improvePrompt({ prompt, outputType, outputFormat }); const result = await improvePrompt({ prompt, outputType, outputFormat });
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify(result, null, 2), text: JSON.stringify(result),
}, },
], ],
}; };
@@ -810,8 +809,7 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
success: true, success: true,
skill: { skill: {
...skill, ...skill,
@@ -820,10 +818,7 @@ function createServer(options: ServerOptions = {}) {
category: skill.category?.name || null, category: skill.category?.name || null,
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`, link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
}, },
}, }),
null,
2
),
}, },
], ],
}; };
@@ -910,17 +905,13 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
success: true, success: true,
message: `File '${filename}' added to skill`, message: `File '${filename}' added to skill`,
skillId, skillId,
files: files.map(f => f.filename), files: files.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`, link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
}, }),
null,
2
),
}, },
], ],
}; };
@@ -1000,17 +991,13 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
success: true, success: true,
message: `File '${filename}' updated in skill`, message: `File '${filename}' updated in skill`,
skillId, skillId,
files: files.map(f => f.filename), files: files.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`, link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
}, }),
null,
2
),
}, },
], ],
}; };
@@ -1096,17 +1083,13 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
success: true, success: true,
message: `File '${filename}' removed from skill`, message: `File '${filename}' removed from skill`,
skillId, skillId,
files: updatedFiles.map(f => f.filename), files: updatedFiles.map(f => f.filename),
link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`, link: `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`,
}, }),
null,
2
),
}, },
], ],
}; };
@@ -1181,8 +1164,7 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: JSON.stringify( text: JSON.stringify({
{
id: skill.id, id: skill.id,
slug: getPromptName(skill), slug: getPromptName(skill),
title: skill.title, title: skill.title,
@@ -1199,10 +1181,7 @@ function createServer(options: ServerOptions = {}) {
content: f.content, content: f.content,
})), })),
link: skill.isPrivate ? null : `https://prompts.chat/prompts/${skill.id}_${getPromptName(skill)}`, 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, category: s.category?.name || null,
tags: s.tags.map((t) => t.tag.name), tags: s.tags.map((t) => t.tag.name),
votes: s._count.votes, votes: s._count.votes,
files: files.map(f => f.filename), fileNames: files.map(f => f.filename),
fileCount: files.length,
createdAt: s.createdAt.toISOString(), createdAt: s.createdAt.toISOString(),
link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`, link: `https://prompts.chat/prompts/${s.id}_${getPromptName(s)}`,
}; };
@@ -1304,7 +1284,7 @@ function createServer(options: ServerOptions = {}) {
content: [ content: [
{ {
type: "text" as const, 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> { async function parseBody(req: NextApiRequest): Promise<unknown> {
const MAX_BODY_SIZE = 1024 * 1024; // 1MB
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let body = ""; 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", () => { req.on("end", () => {
try { try {
resolve(JSON.parse(body)); resolve(JSON.parse(body));
@@ -1342,6 +1330,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
if (req.method === "GET") { if (req.method === "GET") {
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=86400');
return res.status(200).json({ return res.status(200).json({
name: "prompts-chat", name: "prompts-chat",
version: "1.0.0", version: "1.0.0",