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:
+4
-5
@@ -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.)
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user