From 424eae5656c507d7bf4111fddcf6f0cc3294b4ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fatih=20Kadir=20Ak=C4=B1n?= Date: Tue, 16 Dec 2025 23:48:24 +0300 Subject: [PATCH] feat(messages): add API key generation feature in multiple languages --- messages/ar.json | 23 +- messages/de.json | 23 +- messages/el.json | 23 +- messages/en.json | 23 +- messages/es.json | 23 +- messages/fr.json | 23 +- messages/he.json | 23 +- messages/it.json | 23 +- messages/ja.json | 23 +- messages/ko.json | 23 +- messages/pt.json | 23 +- messages/ru.json | 23 +- messages/tr.json | 23 +- messages/zh.json | 23 +- .../migration.sql | 6 + prisma/schema.prisma | 30 ++- src/app/api/user/api-key/route.ts | 84 ++++++ src/app/docs/api/page.tsx | 159 ++++++++++- src/app/settings/page.tsx | 11 +- src/components/mcp/mcp-config-tabs.tsx | 106 ++++++-- src/components/mcp/mcp-server-popup.tsx | 37 ++- src/components/settings/api-key-settings.tsx | 243 +++++++++++++++++ src/lib/api-key.ts | 24 ++ src/pages/api/mcp.ts | 252 ++++++++++++++++-- 24 files changed, 1195 insertions(+), 79 deletions(-) create mode 100644 prisma/migrations/20251216195800_add_api_key_and_mcp_settings/migration.sql create mode 100644 src/app/api/user/api-key/route.ts create mode 100644 src/components/settings/api-key-settings.tsx create mode 100644 src/lib/api-key.ts diff --git a/messages/ar.json b/messages/ar.json index 8ee66d3b..8defe5c4 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -769,6 +769,27 @@ "categories": "الفئات", "categoryPlaceholder": "إضافة رمز الفئة...", "tags": "العلامات", - "tagPlaceholder": "إضافة رمز العلامة..." + "tagPlaceholder": "إضافة رمز العلامة...", + "generateApiKey": "أنشئ مفتاح API لحفظ الأوامر عبر MCP" + }, + "apiKey": { + "title": "مفتاح API لـ MCP", + "description": "أنشئ مفتاح API لحفظ الأوامر عبر MCP والوصول إلى أوامرك الخاصة.", + "yourApiKey": "مفتاح API الخاص بك", + "keyWarning": "احتفظ بهذا المفتاح سرياً. يمكن لأي شخص لديه هذا المفتاح الوصول إلى أوامرك الخاصة وإنشاء أوامر نيابة عنك.", + "noApiKey": "لم تقم بإنشاء مفتاح API بعد.", + "generate": "إنشاء مفتاح API", + "regenerate": "إعادة الإنشاء", + "revoke": "إلغاء", + "regenerateTitle": "إعادة إنشاء مفتاح API؟", + "regenerateDescription": "سيؤدي هذا إلى إبطال مفتاح API الحالي. ستحتاج عملاء MCP الذين يستخدمون المفتاح القديم إلى التحديث.", + "revokeTitle": "إلغاء مفتاح API؟", + "revokeDescription": "سيؤدي هذا إلى حذف مفتاح API الخاص بك نهائياً. لن تتمكن من استخدام ميزات MCP التي تتطلب المصادقة حتى تنشئ مفتاحاً جديداً.", + "keyGenerated": "تم إنشاء مفتاح API بنجاح", + "keyRegenerated": "تم إعادة إنشاء مفتاح API بنجاح", + "keyRevoked": "تم إلغاء مفتاح API", + "publicByDefault": "أوامر عامة افتراضياً", + "publicByDefaultDescription": "عند حفظ الأوامر عبر MCP، اجعلها عامة افتراضياً بدلاً من خاصة.", + "settingUpdated": "تم تحديث الإعداد" } } diff --git a/messages/de.json b/messages/de.json index 235f05bf..4c9e7ce5 100644 --- a/messages/de.json +++ b/messages/de.json @@ -769,6 +769,27 @@ "categories": "Kategorien", "categoryPlaceholder": "Kategorie-Slug hinzufügen...", "tags": "Tags", - "tagPlaceholder": "Tag-Slug hinzufügen..." + "tagPlaceholder": "Tag-Slug hinzufügen...", + "generateApiKey": "API-Schlüssel generieren, um Prompts über MCP zu speichern" + }, + "apiKey": { + "title": "MCP API-Schlüssel", + "description": "Generieren Sie einen API-Schlüssel, um Prompts über MCP zu speichern und auf Ihre privaten Prompts zuzugreifen.", + "yourApiKey": "Ihr API-Schlüssel", + "keyWarning": "Halten Sie diesen Schlüssel geheim. Jeder mit diesem Schlüssel kann auf Ihre privaten Prompts zugreifen und Prompts in Ihrem Namen erstellen.", + "noApiKey": "Sie haben noch keinen API-Schlüssel generiert.", + "generate": "API-Schlüssel generieren", + "regenerate": "Neu generieren", + "revoke": "Widerrufen", + "regenerateTitle": "API-Schlüssel neu generieren?", + "regenerateDescription": "Dies macht Ihren aktuellen API-Schlüssel ungültig. MCP-Clients, die den alten Schlüssel verwenden, müssen aktualisiert werden.", + "revokeTitle": "API-Schlüssel widerrufen?", + "revokeDescription": "Dies löscht Ihren API-Schlüssel dauerhaft. Sie können MCP-Funktionen, die eine Authentifizierung erfordern, nicht mehr verwenden, bis Sie einen neuen Schlüssel generieren.", + "keyGenerated": "API-Schlüssel erfolgreich generiert", + "keyRegenerated": "API-Schlüssel erfolgreich neu generiert", + "keyRevoked": "API-Schlüssel widerrufen", + "publicByDefault": "Prompts standardmäßig öffentlich", + "publicByDefaultDescription": "Beim Speichern von Prompts über MCP standardmäßig öffentlich statt privat machen.", + "settingUpdated": "Einstellung aktualisiert" } } diff --git a/messages/el.json b/messages/el.json index 10e7e49b..fc807358 100644 --- a/messages/el.json +++ b/messages/el.json @@ -769,6 +769,27 @@ "categories": "Κατηγορίες", "categoryPlaceholder": "Προσθέστε slug κατηγορίας...", "tags": "Ετικέτες", - "tagPlaceholder": "Προσθέστε slug ετικέτας..." + "tagPlaceholder": "Προσθέστε slug ετικέτας...", + "generateApiKey": "Δημιουργήστε κλειδί API για αποθήκευση prompts μέσω MCP" + }, + "apiKey": { + "title": "Κλειδί API MCP", + "description": "Δημιουργήστε ένα κλειδί API για αποθήκευση prompts μέσω MCP και πρόσβαση στα ιδιωτικά σας prompts.", + "yourApiKey": "Το Κλειδί API σας", + "keyWarning": "Κρατήστε αυτό το κλειδί μυστικό. Οποιοσδήποτε με αυτό το κλειδί μπορεί να έχει πρόσβαση στα ιδιωτικά σας prompts και να δημιουργήσει prompts εκ μέρους σας.", + "noApiKey": "Δεν έχετε δημιουργήσει ακόμα κλειδί API.", + "generate": "Δημιουργία Κλειδιού API", + "regenerate": "Αναδημιουργία", + "revoke": "Ανάκληση", + "regenerateTitle": "Αναδημιουργία Κλειδιού API;", + "regenerateDescription": "Αυτό θα ακυρώσει το τρέχον κλειδί API σας. Οι MCP clients που χρησιμοποιούν το παλιό κλειδί θα χρειαστούν ενημέρωση.", + "revokeTitle": "Ανάκληση Κλειδιού API;", + "revokeDescription": "Αυτό θα διαγράψει μόνιμα το κλειδί API σας. Δεν θα μπορείτε να χρησιμοποιήσετε λειτουργίες MCP που απαιτούν πιστοποίηση μέχρι να δημιουργήσετε νέο κλειδί.", + "keyGenerated": "Το κλειδί API δημιουργήθηκε επιτυχώς", + "keyRegenerated": "Το κλειδί API αναδημιουργήθηκε επιτυχώς", + "keyRevoked": "Το κλειδί API ανακλήθηκε", + "publicByDefault": "Δημόσια prompts από προεπιλογή", + "publicByDefaultDescription": "Κατά την αποθήκευση prompts μέσω MCP, να γίνονται δημόσια από προεπιλογή αντί για ιδιωτικά.", + "settingUpdated": "Η ρύθμιση ενημερώθηκε" } } diff --git a/messages/en.json b/messages/en.json index 5fd7401f..d568e70b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -769,6 +769,27 @@ "categories": "Categories", "categoryPlaceholder": "Add category slug...", "tags": "Tags", - "tagPlaceholder": "Add tag slug..." + "tagPlaceholder": "Add tag slug...", + "generateApiKey": "Generate API key to save prompts via MCP" + }, + "apiKey": { + "title": "MCP API Key", + "description": "Generate an API key to save prompts via MCP and access your private prompts.", + "yourApiKey": "Your API Key", + "keyWarning": "Keep this key secret. Anyone with this key can access your private prompts and create prompts on your behalf.", + "noApiKey": "You haven't generated an API key yet.", + "generate": "Generate API Key", + "regenerate": "Regenerate", + "revoke": "Revoke", + "regenerateTitle": "Regenerate API Key?", + "regenerateDescription": "This will invalidate your current API key. Any MCP clients using the old key will need to be updated.", + "revokeTitle": "Revoke API Key?", + "revokeDescription": "This will permanently delete your API key. You won't be able to use MCP features that require authentication until you generate a new key.", + "keyGenerated": "API key generated successfully", + "keyRegenerated": "API key regenerated successfully", + "keyRevoked": "API key revoked", + "publicByDefault": "Public prompts by default", + "publicByDefaultDescription": "When saving prompts via MCP, make them public by default instead of private.", + "settingUpdated": "Setting updated" } } diff --git a/messages/es.json b/messages/es.json index f48e515f..75b615d1 100644 --- a/messages/es.json +++ b/messages/es.json @@ -769,6 +769,27 @@ "categories": "Categorías", "categoryPlaceholder": "Añadir slug de categoría...", "tags": "Etiquetas", - "tagPlaceholder": "Añadir slug de etiqueta..." + "tagPlaceholder": "Añadir slug de etiqueta...", + "generateApiKey": "Genera una clave API para guardar prompts vía MCP" + }, + "apiKey": { + "title": "Clave API MCP", + "description": "Genera una clave API para guardar prompts vía MCP y acceder a tus prompts privados.", + "yourApiKey": "Tu Clave API", + "keyWarning": "Mantén esta clave en secreto. Cualquiera con esta clave puede acceder a tus prompts privados y crear prompts en tu nombre.", + "noApiKey": "Aún no has generado una clave API.", + "generate": "Generar Clave API", + "regenerate": "Regenerar", + "revoke": "Revocar", + "regenerateTitle": "¿Regenerar Clave API?", + "regenerateDescription": "Esto invalidará tu clave API actual. Los clientes MCP que usen la clave antigua necesitarán ser actualizados.", + "revokeTitle": "¿Revocar Clave API?", + "revokeDescription": "Esto eliminará permanentemente tu clave API. No podrás usar las funciones MCP que requieren autenticación hasta que generes una nueva clave.", + "keyGenerated": "Clave API generada exitosamente", + "keyRegenerated": "Clave API regenerada exitosamente", + "keyRevoked": "Clave API revocada", + "publicByDefault": "Prompts públicos por defecto", + "publicByDefaultDescription": "Al guardar prompts vía MCP, hacerlos públicos por defecto en lugar de privados.", + "settingUpdated": "Configuración actualizada" } } diff --git a/messages/fr.json b/messages/fr.json index 7f79bfc6..4102b02c 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -769,6 +769,27 @@ "categories": "Catégories", "categoryPlaceholder": "Ajouter un slug de catégorie...", "tags": "Tags", - "tagPlaceholder": "Ajouter un slug de tag..." + "tagPlaceholder": "Ajouter un slug de tag...", + "generateApiKey": "Générez une clé API pour enregistrer des prompts via MCP" + }, + "apiKey": { + "title": "Clé API MCP", + "description": "Générez une clé API pour enregistrer des prompts via MCP et accéder à vos prompts privés.", + "yourApiKey": "Votre Clé API", + "keyWarning": "Gardez cette clé secrète. Toute personne possédant cette clé peut accéder à vos prompts privés et créer des prompts en votre nom.", + "noApiKey": "Vous n'avez pas encore généré de clé API.", + "generate": "Générer une Clé API", + "regenerate": "Régénérer", + "revoke": "Révoquer", + "regenerateTitle": "Régénérer la Clé API ?", + "regenerateDescription": "Cela invalidera votre clé API actuelle. Les clients MCP utilisant l'ancienne clé devront être mis à jour.", + "revokeTitle": "Révoquer la Clé API ?", + "revokeDescription": "Cela supprimera définitivement votre clé API. Vous ne pourrez pas utiliser les fonctionnalités MCP nécessitant une authentification jusqu'à ce que vous génériez une nouvelle clé.", + "keyGenerated": "Clé API générée avec succès", + "keyRegenerated": "Clé API régénérée avec succès", + "keyRevoked": "Clé API révoquée", + "publicByDefault": "Prompts publics par défaut", + "publicByDefaultDescription": "Lors de l'enregistrement de prompts via MCP, les rendre publics par défaut au lieu de privés.", + "settingUpdated": "Paramètre mis à jour" } } diff --git a/messages/he.json b/messages/he.json index dc7a59b3..357c53ce 100644 --- a/messages/he.json +++ b/messages/he.json @@ -769,6 +769,27 @@ "categories": "קטגוריות", "categoryPlaceholder": "הוסף slug קטגוריה...", "tags": "תגיות", - "tagPlaceholder": "הוסף slug תגית..." + "tagPlaceholder": "הוסף slug תגית...", + "generateApiKey": "צור מפתח API לשמירת פרומפטים דרך MCP" + }, + "apiKey": { + "title": "מפתח API ל-MCP", + "description": "צור מפתח API לשמירת פרומפטים דרך MCP וגישה לפרומפטים הפרטיים שלך.", + "yourApiKey": "מפתח ה-API שלך", + "keyWarning": "שמור על מפתח זה בסוד. כל מי שיש לו מפתח זה יכול לגשת לפרומפטים הפרטיים שלך וליצור פרומפטים בשמך.", + "noApiKey": "עדיין לא יצרת מפתח API.", + "generate": "צור מפתח API", + "regenerate": "צור מחדש", + "revoke": "בטל", + "regenerateTitle": "ליצור מחדש מפתח API?", + "regenerateDescription": "פעולה זו תבטל את מפתח ה-API הנוכחי שלך. לקוחות MCP המשתמשים במפתח הישן יצטרכו להתעדכן.", + "revokeTitle": "לבטל מפתח API?", + "revokeDescription": "פעולה זו תמחק לצמיתות את מפתח ה-API שלך. לא תוכל להשתמש בתכונות MCP הדורשות אימות עד שתיצור מפתח חדש.", + "keyGenerated": "מפתח API נוצר בהצלחה", + "keyRegenerated": "מפתח API נוצר מחדש בהצלחה", + "keyRevoked": "מפתח API בוטל", + "publicByDefault": "פרומפטים ציבוריים כברירת מחדל", + "publicByDefaultDescription": "בעת שמירת פרומפטים דרך MCP, הפוך אותם לציבוריים כברירת מחדל במקום פרטיים.", + "settingUpdated": "ההגדרה עודכנה" } } diff --git a/messages/it.json b/messages/it.json index 6f607623..114917e3 100644 --- a/messages/it.json +++ b/messages/it.json @@ -769,6 +769,27 @@ "categories": "Categorie", "categoryPlaceholder": "Aggiungi slug categoria...", "tags": "Tag", - "tagPlaceholder": "Aggiungi slug tag..." + "tagPlaceholder": "Aggiungi slug tag...", + "generateApiKey": "Genera una chiave API per salvare prompt tramite MCP" + }, + "apiKey": { + "title": "Chiave API MCP", + "description": "Genera una chiave API per salvare prompt tramite MCP e accedere ai tuoi prompt privati.", + "yourApiKey": "La Tua Chiave API", + "keyWarning": "Mantieni questa chiave segreta. Chiunque abbia questa chiave può accedere ai tuoi prompt privati e creare prompt a tuo nome.", + "noApiKey": "Non hai ancora generato una chiave API.", + "generate": "Genera Chiave API", + "regenerate": "Rigenera", + "revoke": "Revoca", + "regenerateTitle": "Rigenerare la Chiave API?", + "regenerateDescription": "Questo invaliderà la tua chiave API attuale. I client MCP che usano la vecchia chiave dovranno essere aggiornati.", + "revokeTitle": "Revocare la Chiave API?", + "revokeDescription": "Questo eliminerà permanentemente la tua chiave API. Non potrai usare le funzionalità MCP che richiedono autenticazione fino a quando non genererai una nuova chiave.", + "keyGenerated": "Chiave API generata con successo", + "keyRegenerated": "Chiave API rigenerata con successo", + "keyRevoked": "Chiave API revocata", + "publicByDefault": "Prompt pubblici per impostazione predefinita", + "publicByDefaultDescription": "Quando salvi prompt tramite MCP, rendili pubblici per impostazione predefinita invece che privati.", + "settingUpdated": "Impostazione aggiornata" } } diff --git a/messages/ja.json b/messages/ja.json index 924d4dbe..651fdee0 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -769,6 +769,27 @@ "categories": "カテゴリー", "categoryPlaceholder": "カテゴリースラッグを追加...", "tags": "タグ", - "tagPlaceholder": "タグスラッグを追加..." + "tagPlaceholder": "タグスラッグを追加...", + "generateApiKey": "MCP経由でプロンプトを保存するためのAPIキーを生成" + }, + "apiKey": { + "title": "MCP APIキー", + "description": "MCP経由でプロンプトを保存し、プライベートプロンプトにアクセスするためのAPIキーを生成します。", + "yourApiKey": "あなたのAPIキー", + "keyWarning": "このキーは秘密にしてください。このキーを持っている人は、あなたのプライベートプロンプトにアクセスし、あなたの名前でプロンプトを作成できます。", + "noApiKey": "まだAPIキーを生成していません。", + "generate": "APIキーを生成", + "regenerate": "再生成", + "revoke": "取り消し", + "regenerateTitle": "APIキーを再生成しますか?", + "regenerateDescription": "これにより現在のAPIキーが無効になります。古いキーを使用しているMCPクライアントは更新が必要です。", + "revokeTitle": "APIキーを取り消しますか?", + "revokeDescription": "これによりAPIキーが永久に削除されます。新しいキーを生成するまで、認証が必要なMCP機能は使用できなくなります。", + "keyGenerated": "APIキーが正常に生成されました", + "keyRegenerated": "APIキーが正常に再生成されました", + "keyRevoked": "APIキーが取り消されました", + "publicByDefault": "デフォルトで公開プロンプト", + "publicByDefaultDescription": "MCP経由でプロンプトを保存する際、プライベートではなくデフォルトで公開にします。", + "settingUpdated": "設定が更新されました" } } diff --git a/messages/ko.json b/messages/ko.json index 196ce348..e01e0342 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -769,6 +769,27 @@ "categories": "카테고리", "categoryPlaceholder": "카테고리 슬러그 추가...", "tags": "태그", - "tagPlaceholder": "태그 슬러그 추가..." + "tagPlaceholder": "태그 슬러그 추가...", + "generateApiKey": "MCP를 통해 프롬프트를 저장하려면 API 키를 생성하세요" + }, + "apiKey": { + "title": "MCP API 키", + "description": "MCP를 통해 프롬프트를 저장하고 비공개 프롬프트에 접근하려면 API 키를 생성하세요.", + "yourApiKey": "API 키", + "keyWarning": "이 키를 비밀로 유지하세요. 이 키를 가진 사람은 비공개 프롬프트에 접근하고 대신 프롬프트를 만들 수 있습니다.", + "noApiKey": "아직 API 키를 생성하지 않았습니다.", + "generate": "API 키 생성", + "regenerate": "재생성", + "revoke": "취소", + "regenerateTitle": "API 키를 재생성하시겠습니까?", + "regenerateDescription": "현재 API 키가 무효화됩니다. 이전 키를 사용하는 MCP 클라이언트는 업데이트가 필요합니다.", + "revokeTitle": "API 키를 취소하시겠습니까?", + "revokeDescription": "API 키가 영구적으로 삭제됩니다. 새 키를 생성할 때까지 인증이 필요한 MCP 기능을 사용할 수 없습니다.", + "keyGenerated": "API 키가 성공적으로 생성되었습니다", + "keyRegenerated": "API 키가 성공적으로 재생성되었습니다", + "keyRevoked": "API 키가 취소되었습니다", + "publicByDefault": "기본적으로 공개 프롬프트", + "publicByDefaultDescription": "MCP를 통해 프롬프트를 저장할 때 비공개 대신 기본적으로 공개로 설정합니다.", + "settingUpdated": "설정이 업데이트되었습니다" } } diff --git a/messages/pt.json b/messages/pt.json index 233309d1..110f7d47 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -769,6 +769,27 @@ "categories": "Categorias", "categoryPlaceholder": "Adicionar slug da categoria...", "tags": "Tags", - "tagPlaceholder": "Adicionar slug da tag..." + "tagPlaceholder": "Adicionar slug da tag...", + "generateApiKey": "Gere uma chave API para salvar prompts via MCP" + }, + "apiKey": { + "title": "Chave API MCP", + "description": "Gere uma chave API para salvar prompts via MCP e acessar seus prompts privados.", + "yourApiKey": "Sua Chave API", + "keyWarning": "Mantenha esta chave em segredo. Qualquer pessoa com esta chave pode acessar seus prompts privados e criar prompts em seu nome.", + "noApiKey": "Você ainda não gerou uma chave API.", + "generate": "Gerar Chave API", + "regenerate": "Regenerar", + "revoke": "Revogar", + "regenerateTitle": "Regenerar Chave API?", + "regenerateDescription": "Isso invalidará sua chave API atual. Os clientes MCP que usam a chave antiga precisarão ser atualizados.", + "revokeTitle": "Revogar Chave API?", + "revokeDescription": "Isso excluirá permanentemente sua chave API. Você não poderá usar recursos MCP que requerem autenticação até gerar uma nova chave.", + "keyGenerated": "Chave API gerada com sucesso", + "keyRegenerated": "Chave API regenerada com sucesso", + "keyRevoked": "Chave API revogada", + "publicByDefault": "Prompts públicos por padrão", + "publicByDefaultDescription": "Ao salvar prompts via MCP, torná-los públicos por padrão em vez de privados.", + "settingUpdated": "Configuração atualizada" } } diff --git a/messages/ru.json b/messages/ru.json index aabe5295..93e8d0af 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -769,6 +769,27 @@ "categories": "Категории", "categoryPlaceholder": "Добавить slug категории...", "tags": "Теги", - "tagPlaceholder": "Добавить slug тега..." + "tagPlaceholder": "Добавить slug тега...", + "generateApiKey": "Сгенерируйте API-ключ для сохранения промптов через MCP" + }, + "apiKey": { + "title": "MCP API-ключ", + "description": "Сгенерируйте API-ключ для сохранения промптов через MCP и доступа к вашим приватным промптам.", + "yourApiKey": "Ваш API-ключ", + "keyWarning": "Держите этот ключ в секрете. Любой, у кого есть этот ключ, может получить доступ к вашим приватным промптам и создавать промпты от вашего имени.", + "noApiKey": "Вы еще не сгенерировали API-ключ.", + "generate": "Сгенерировать API-ключ", + "regenerate": "Перегенерировать", + "revoke": "Отозвать", + "regenerateTitle": "Перегенерировать API-ключ?", + "regenerateDescription": "Это сделает ваш текущий API-ключ недействительным. MCP-клиенты, использующие старый ключ, потребуют обновления.", + "revokeTitle": "Отозвать API-ключ?", + "revokeDescription": "Это навсегда удалит ваш API-ключ. Вы не сможете использовать функции MCP, требующие аутентификации, пока не сгенерируете новый ключ.", + "keyGenerated": "API-ключ успешно сгенерирован", + "keyRegenerated": "API-ключ успешно перегенерирован", + "keyRevoked": "API-ключ отозван", + "publicByDefault": "Публичные промпты по умолчанию", + "publicByDefaultDescription": "При сохранении промптов через MCP делать их публичными по умолчанию вместо приватных.", + "settingUpdated": "Настройка обновлена" } } diff --git a/messages/tr.json b/messages/tr.json index ec7db3b3..3fc0c3e0 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -769,6 +769,27 @@ "categories": "Kategoriler", "categoryPlaceholder": "Kategori slug ekle...", "tags": "Etiketler", - "tagPlaceholder": "Etiket slug ekle..." + "tagPlaceholder": "Etiket slug ekle...", + "generateApiKey": "MCP ile prompt kaydetmek için API anahtarı oluşturun" + }, + "apiKey": { + "title": "MCP API Anahtarı", + "description": "MCP ile prompt kaydetmek ve özel promptlarınıza erişmek için bir API anahtarı oluşturun.", + "yourApiKey": "API Anahtarınız", + "keyWarning": "Bu anahtarı gizli tutun. Bu anahtara sahip olan herkes özel promptlarınıza erişebilir ve sizin adınıza prompt oluşturabilir.", + "noApiKey": "Henüz bir API anahtarı oluşturmadınız.", + "generate": "API Anahtarı Oluştur", + "regenerate": "Yeniden Oluştur", + "revoke": "İptal Et", + "regenerateTitle": "API Anahtarını Yeniden Oluştur?", + "regenerateDescription": "Bu, mevcut API anahtarınızı geçersiz kılacak. Eski anahtarı kullanan MCP istemcilerinin güncellenmesi gerekecek.", + "revokeTitle": "API Anahtarını İptal Et?", + "revokeDescription": "Bu, API anahtarınızı kalıcı olarak silecek. Yeni bir anahtar oluşturana kadar kimlik doğrulaması gerektiren MCP özelliklerini kullanamazsınız.", + "keyGenerated": "API anahtarı başarıyla oluşturuldu", + "keyRegenerated": "API anahtarı başarıyla yeniden oluşturuldu", + "keyRevoked": "API anahtarı iptal edildi", + "publicByDefault": "Varsayılan olarak herkese açık promptlar", + "publicByDefaultDescription": "MCP ile prompt kaydederken, varsayılan olarak özel yerine herkese açık yap.", + "settingUpdated": "Ayar güncellendi" } } diff --git a/messages/zh.json b/messages/zh.json index 2cbf7dbf..7a50988a 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -769,6 +769,27 @@ "categories": "分类", "categoryPlaceholder": "添加分类标识...", "tags": "标签", - "tagPlaceholder": "添加标签标识..." + "tagPlaceholder": "添加标签标识...", + "generateApiKey": "生成 API 密钥以通过 MCP 保存提示词" + }, + "apiKey": { + "title": "MCP API 密钥", + "description": "生成 API 密钥以通过 MCP 保存提示词并访问您的私有提示词。", + "yourApiKey": "您的 API 密钥", + "keyWarning": "请保密此密钥。任何拥有此密钥的人都可以访问您的私有提示词并以您的名义创建提示词。", + "noApiKey": "您尚未生成 API 密钥。", + "generate": "生成 API 密钥", + "regenerate": "重新生成", + "revoke": "撤销", + "regenerateTitle": "重新生成 API 密钥?", + "regenerateDescription": "这将使您当前的 API 密钥失效。使用旧密钥的 MCP 客户端需要更新。", + "revokeTitle": "撤销 API 密钥?", + "revokeDescription": "这将永久删除您的 API 密钥。在生成新密钥之前,您将无法使用需要身份验证的 MCP 功能。", + "keyGenerated": "API 密钥生成成功", + "keyRegenerated": "API 密钥重新生成成功", + "keyRevoked": "API 密钥已撤销", + "publicByDefault": "默认公开提示词", + "publicByDefaultDescription": "通过 MCP 保存提示词时,默认设为公开而非私有。", + "settingUpdated": "设置已更新" } } diff --git a/prisma/migrations/20251216195800_add_api_key_and_mcp_settings/migration.sql b/prisma/migrations/20251216195800_add_api_key_and_mcp_settings/migration.sql new file mode 100644 index 00000000..12c6d4a3 --- /dev/null +++ b/prisma/migrations/20251216195800_add_api_key_and_mcp_settings/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "apiKey" TEXT; +ALTER TABLE "users" ADD COLUMN "mcpPromptsPublicByDefault" BOOLEAN NOT NULL DEFAULT false; + +-- CreateIndex +CREATE UNIQUE INDEX "users_apiKey_key" ON "users"("apiKey"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3469b34f..68a59a61 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,20 +8,22 @@ datasource db { } model User { - id String @id @default(cuid()) - email String @unique - username String @unique - name String? - password String? - avatar String? - role UserRole @default(USER) - locale String @default("en") - emailVerified DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - verified Boolean @default(false) - githubUsername String? - accounts Account[] + id String @id @default(cuid()) + email String @unique + username String @unique + name String? + password String? + avatar String? + role UserRole @default(USER) + locale String @default("en") + emailVerified DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + verified Boolean @default(false) + githubUsername String? + apiKey String? @unique + mcpPromptsPublicByDefault Boolean @default(false) + accounts Account[] subscriptions CategorySubscription[] changeRequests ChangeRequest[] pinnedPrompts PinnedPrompt[] diff --git a/src/app/api/user/api-key/route.ts b/src/app/api/user/api-key/route.ts new file mode 100644 index 00000000..f7d2f8aa --- /dev/null +++ b/src/app/api/user/api-key/route.ts @@ -0,0 +1,84 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { db } from "@/lib/db"; +import { generateApiKey } from "@/lib/api-key"; + +export async function GET() { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const user = await db.user.findUnique({ + where: { id: session.user.id }, + select: { + apiKey: true, + mcpPromptsPublicByDefault: true, + }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + return NextResponse.json({ + hasApiKey: !!user.apiKey, + apiKey: user.apiKey, + mcpPromptsPublicByDefault: user.mcpPromptsPublicByDefault, + }); +} + +export async function POST() { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const apiKey = generateApiKey(); + + await db.user.update({ + where: { id: session.user.id }, + data: { apiKey }, + }); + + return NextResponse.json({ apiKey }); +} + +export async function DELETE() { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + await db.user.update({ + where: { id: session.user.id }, + data: { apiKey: null }, + }); + + return NextResponse.json({ success: true }); +} + +export async function PATCH(request: Request) { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const { mcpPromptsPublicByDefault } = body; + + if (typeof mcpPromptsPublicByDefault !== "boolean") { + return NextResponse.json({ error: "Invalid request" }, { status: 400 }); + } + + await db.user.update({ + where: { id: session.user.id }, + data: { mcpPromptsPublicByDefault }, + }); + + return NextResponse.json({ success: true }); +} diff --git a/src/app/docs/api/page.tsx b/src/app/docs/api/page.tsx index 46275c60..90e71477 100644 --- a/src/app/docs/api/page.tsx +++ b/src/app/docs/api/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { headers } from "next/headers"; -import { Code, Zap, Terminal, Search, Box } from "lucide-react"; +import { Code, Zap, Terminal, Search, Box, Key, Save } from "lucide-react"; import { Table, TableBody, @@ -70,6 +70,69 @@ export default async function ApiDocsPage() {

+ {/* Authentication */} +
+

+ + Authentication +

+

+ Most API features work without authentication. However, to save prompts via MCP or access your private prompts, + you need to authenticate using an API key. +

+ +
+
+

Generate an API Key

+

+ Go to{" "} + + Settings + + {" "}to generate your API key. Keys start with pchat_. +

+
+
+

Using the API Key

+

+ Pass your API key via the PROMPTS_API_KEY header. +

+
+
+ +
+
{`# Remote: HTTP transport with headers
+"prompts-chat": {
+  "url": "${baseUrl}/api/mcp",
+  "headers": {
+    "PROMPTS_API_KEY": "pchat_your_api_key_here"
+  }
+}
+
+# Local: stdio transport with environment variable
+"prompts-chat": {
+  "command": "npx",
+  "args": ["-y", "@fkadev/prompts.chat-mcp"],
+  "env": {
+    "PROMPTS_API_KEY": "pchat_your_api_key_here"
+  }
+}
+
+# Or via curl (remote)
+curl -X POST ${baseUrl}/api/mcp \\
+  -H "Content-Type: application/json" \\
+  -H "PROMPTS_API_KEY: pchat_your_api_key_here" \\
+  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'`}
+
+ +

+ Remote (HTTP) sends requests to prompts.chat with the API key in headers. + Local (stdio) runs the MCP server locally via npx with the API key as an environment variable. + With authentication, you can use the save_prompt tool + and search results will include your private prompts. +

+
+ {/* MCP Prompts */}

@@ -249,6 +312,100 @@ curl -X POST ${baseUrl}/api/mcp \\

+ + {/* save_prompt Tool */} +
+

+ + save_prompt + Requires Auth +

+

+ Save a new prompt to your account. Requires API key authentication. Prompts are private by default + unless you've changed the default in your settings. +

+ +
+ + + + Parameter + Type + Required + Description + + + + + title + string + Yes + Title of the prompt (max 200 chars) + + + content + string + Yes + The prompt content. Can include variables like {"${var}"} + + + description + string + No + Optional description (max 500 chars) + + + tags + string[] + No + Array of tag names (max 10) + + + category + string + No + Category slug + + + isPrivate + boolean + No + Override default privacy setting + + + type + string + No + + TEXT (default), STRUCTURED, IMAGE, VIDEO, AUDIO + + + +
+
+ +
+
{`# Save a prompt via MCP
+curl -X POST ${baseUrl}/api/mcp \\
+  -H "Content-Type: application/json" \\
+  -H "PROMPTS_API_KEY: pchat_your_api_key_here" \\
+  -d '{
+    "jsonrpc": "2.0",
+    "id": 1,
+    "method": "tools/call",
+    "params": {
+      "name": "save_prompt",
+      "arguments": {
+        "title": "My Code Review Prompt",
+        "content": "Review this code for \${language} best practices:\\n\\n\${code}",
+        "description": "A helpful code review assistant",
+        "tags": ["coding", "review"],
+        "isPrivate": false
+      }
+    }
+  }'`}
+
+

{/* MCP Protocol Example */} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 6ac37525..b38dcf40 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -3,6 +3,7 @@ import { getTranslations } from "next-intl/server"; import { auth } from "@/lib/auth"; import { db } from "@/lib/db"; import { ProfileForm } from "@/components/settings/profile-form"; +import { ApiKeySettings } from "@/components/settings/api-key-settings"; export default async function SettingsPage() { const session = await auth(); @@ -20,6 +21,8 @@ export default async function SettingsPage() { username: true, email: true, avatar: true, + apiKey: true, + mcpPromptsPublicByDefault: true, }, }); @@ -36,7 +39,13 @@ export default async function SettingsPage() {

- +
+ + +
); } diff --git a/src/components/mcp/mcp-config-tabs.tsx b/src/components/mcp/mcp-config-tabs.tsx index 96748fee..c38aa41b 100644 --- a/src/components/mcp/mcp-config-tabs.tsx +++ b/src/components/mcp/mcp-config-tabs.tsx @@ -18,6 +18,8 @@ interface McpConfigTabsProps { onModeChange?: (mode: "remote" | "local") => void; /** Hide the mode toggle (when controlled externally) */ hideModeToggle?: boolean; + /** API key for authenticated access */ + apiKey?: string | null; } const CLIENT_LABELS: Record = { @@ -31,68 +33,97 @@ const CLIENT_LABELS: Record = { const NPM_PACKAGE = "@fkadev/prompts.chat-mcp"; -function getConfig(client: Client, mode: Mode, mcpUrl: string): string { +function getConfig(client: Client, mode: Mode, mcpUrl: string, apiKey?: string | null): string { const packageName = NPM_PACKAGE; switch (client) { case "cursor": if (mode === "remote") { - return JSON.stringify({ + const config: Record = { mcpServers: { "prompts.chat": { url: mcpUrl, + ...(apiKey && { headers: { "PROMPTS_API_KEY": apiKey } }), }, }, - }, null, 2); + }; + return JSON.stringify(config, null, 2); } else { - return JSON.stringify({ + const config: Record = { mcpServers: { "prompts.chat": { command: "npx", args: ["-y", packageName], + ...(apiKey && { env: { "PROMPTS_API_KEY": apiKey } }), }, }, - }, null, 2); + }; + return JSON.stringify(config, null, 2); } case "claude-code": if (mode === "remote") { + if (apiKey) { + return `claude mcp add --transport http prompts.chat ${mcpUrl} --header "PROMPTS_API_KEY: ${apiKey}"`; + } return `claude mcp add --transport http prompts.chat ${mcpUrl}`; } else { + if (apiKey) { + return `PROMPTS_API_KEY=${apiKey} claude mcp add prompts.chat -- npx -y ${packageName}`; + } return `claude mcp add prompts.chat -- npx -y ${packageName}`; } case "vscode": if (mode === "remote") { - return JSON.stringify({ + const config: Record = { mcp: { servers: { "prompts.chat": { type: "http", url: mcpUrl, + ...(apiKey && { headers: { "PROMPTS_API_KEY": apiKey } }), }, }, }, - }, null, 2); + }; + return JSON.stringify(config, null, 2); } else { - return JSON.stringify({ + const config: Record = { mcp: { servers: { "prompts.chat": { type: "stdio", command: "npx", args: ["-y", packageName], + ...(apiKey && { env: { "PROMPTS_API_KEY": apiKey } }), }, }, }, - }, null, 2); + }; + return JSON.stringify(config, null, 2); } case "codex": if (mode === "remote") { + if (apiKey) { + return `[mcp_servers.prompts_chat] +url = "${mcpUrl}" + +[mcp_servers.prompts_chat.headers] +PROMPTS_API_KEY = "${apiKey}"`; + } return `[mcp_servers.prompts_chat] url = "${mcpUrl}"`; } else { + if (apiKey) { + return `[mcp_servers.prompts_chat] +command = "npx" +args = ["-y", "${packageName}"] + +[mcp_servers.prompts_chat.env] +PROMPTS_API_KEY = "${apiKey}"`; + } return `[mcp_servers.prompts_chat] command = "npx" args = ["-y", "${packageName}"]`; @@ -100,28 +131,38 @@ args = ["-y", "${packageName}"]`; case "windsurf": if (mode === "remote") { - return JSON.stringify({ + const config: Record = { mcpServers: { "prompts.chat": { serverUrl: mcpUrl, + ...(apiKey && { headers: { "PROMPTS_API_KEY": apiKey } }), }, }, - }, null, 2); + }; + return JSON.stringify(config, null, 2); } else { - return JSON.stringify({ + const config: Record = { mcpServers: { "prompts.chat": { command: "npx", args: ["-y", packageName], + ...(apiKey && { env: { "PROMPTS_API_KEY": apiKey } }), }, }, - }, null, 2); + }; + return JSON.stringify(config, null, 2); } case "gemini": if (mode === "remote") { + if (apiKey) { + return `PROMPTS_API_KEY=${apiKey} gemini mcp add prompts.chat --transport sse ${mcpUrl}`; + } return `gemini mcp add prompts.chat --transport sse ${mcpUrl}`; } else { + if (apiKey) { + return `PROMPTS_API_KEY=${apiKey} gemini mcp add prompts.chat -- npx -y ${packageName}`; + } return `gemini mcp add prompts.chat -- npx -y ${packageName}`; } @@ -130,10 +171,11 @@ args = ["-y", "${packageName}"]`; } } -export function McpConfigTabs({ baseUrl, queryParams, className, mode, onModeChange, hideModeToggle }: McpConfigTabsProps) { +export function McpConfigTabs({ baseUrl, queryParams, className, mode, onModeChange, hideModeToggle, apiKey }: McpConfigTabsProps) { const [selectedClient, setSelectedClient] = useState("vscode"); const [internalMode, setInternalMode] = useState("remote"); const [copied, setCopied] = useState(false); + const [showApiKey, setShowApiKey] = useState(false); const selectedMode = mode ?? internalMode; const handleModeChange = (newMode: Mode) => { @@ -147,7 +189,14 @@ export function McpConfigTabs({ baseUrl, queryParams, className, mode, onModeCha const base = baseUrl || (typeof window !== "undefined" ? window.location.origin : "https://prompts.chat"); const mcpUrl = queryParams ? `${base}/api/mcp?${queryParams}` : `${base}/api/mcp`; - const config = getConfig(selectedClient, selectedMode, mcpUrl); + // Full config with actual API key (for copying) + const config = getConfig(selectedClient, selectedMode, mcpUrl, apiKey); + + // Display config: show full key if revealed, otherwise show placeholder + const displayApiKey = apiKey + ? (showApiKey ? apiKey : "") + : null; + const displayConfig = getConfig(selectedClient, selectedMode, mcpUrl, displayApiKey); const handleCopy = async () => { await navigator.clipboard.writeText(config); @@ -207,8 +256,31 @@ export function McpConfigTabs({ baseUrl, queryParams, className, mode, onModeCha {/* Config Display */}
-
-
{config}
+
+
+            {apiKey && !showApiKey ? (
+              <>
+                {displayConfig.split("").map((part, i, arr) => (
+                  
+                    {part}
+                    {i < arr.length - 1 && (
+                       setShowApiKey(true)}
+                        className="text-primary underline cursor-pointer hover:text-primary/80"
+                      >
+                        {""}
+                      
+                    )}
+                  
+                ))}
+              
+            ) : (
+              displayConfig
+            )}
+          
+ +
+

{t("keyWarning")}

+
+ +
+
+ +

+ {t("publicByDefaultDescription")} +

+
+ +
+ +
+ + + + + + + {t("regenerateTitle")} + + {t("regenerateDescription")} + + + + {tCommon("cancel")} + + {t("regenerate")} + + + + + + + + + + + + {t("revokeTitle")} + + {t("revokeDescription")} + + + + {tCommon("cancel")} + + {t("revoke")} + + + + +
+ + ) : ( +
+

+ {t("noApiKey")} +

+ +
+ )} + + + ); +} diff --git a/src/lib/api-key.ts b/src/lib/api-key.ts new file mode 100644 index 00000000..956c29b0 --- /dev/null +++ b/src/lib/api-key.ts @@ -0,0 +1,24 @@ +import { randomBytes } from "crypto"; + +const API_KEY_PREFIX = "pchat_"; +const API_KEY_LENGTH = 32; + +/** + * Generate a secure API key with the pchat_ prefix + */ +export function generateApiKey(): string { + const randomPart = randomBytes(API_KEY_LENGTH).toString("hex"); + return `${API_KEY_PREFIX}${randomPart}`; +} + +/** + * Validate that an API key has the correct format + */ +export function isValidApiKeyFormat(apiKey: string): boolean { + if (!apiKey.startsWith(API_KEY_PREFIX)) { + return false; + } + const randomPart = apiKey.slice(API_KEY_PREFIX.length); + // Should be 64 hex characters (32 bytes) + return /^[a-f0-9]{64}$/.test(randomPart); +} diff --git a/src/pages/api/mcp.ts b/src/pages/api/mcp.ts index c0a6a31a..6c55e21a 100644 --- a/src/pages/api/mcp.ts +++ b/src/pages/api/mcp.ts @@ -9,6 +9,30 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { db } from "@/lib/db"; +import { isValidApiKeyFormat } from "@/lib/api-key"; + +interface AuthenticatedUser { + id: string; + username: string; + mcpPromptsPublicByDefault: boolean; +} + +async function authenticateApiKey(apiKey: string | null): Promise { + if (!apiKey || !isValidApiKeyFormat(apiKey)) { + return null; + } + + const user = await db.user.findUnique({ + where: { apiKey }, + select: { + id: true, + username: true, + mcpPromptsPublicByDefault: true, + }, + }); + + return user; +} interface ExtractedVariable { name: string; @@ -64,6 +88,7 @@ interface ServerOptions { categories?: string[]; tags?: string[]; users?: string[]; + authenticatedUser?: AuthenticatedUser | null; } function createServer(options: ServerOptions = {}) { @@ -75,36 +100,63 @@ function createServer(options: ServerOptions = {}) { { capabilities: { prompts: { listChanged: false }, + tools: {}, }, } ); + const { authenticatedUser } = options; + // Build category/tag filter for prompts - const promptFilter: Record = { - isPrivate: false, - isUnlisted: false, - deletedAt: null, + // If authenticated user is present and no specific users filter, include their private prompts + const buildPromptFilter = (includeOwnPrivate: boolean = true): Record => { + const baseFilter: Record = { + isUnlisted: false, + deletedAt: null, + }; + + // Handle visibility: public prompts OR authenticated user's own prompts + if (authenticatedUser && includeOwnPrivate) { + // If users filter includes the authenticated user (or no users filter), include their private prompts + const usersFilter = options.users && options.users.length > 0 ? options.users : null; + const includeAuthUserPrivate = !usersFilter || usersFilter.includes(authenticatedUser.username); + + if (includeAuthUserPrivate) { + baseFilter.OR = [ + { isPrivate: false }, + { isPrivate: true, authorId: authenticatedUser.id }, + ]; + } else { + baseFilter.isPrivate = false; + } + } else { + baseFilter.isPrivate = false; + } + + if (options.categories && options.categories.length > 0) { + baseFilter.category = { + slug: { in: options.categories }, + }; + } + + if (options.tags && options.tags.length > 0) { + baseFilter.tags = { + some: { + tag: { slug: { in: options.tags } }, + }, + }; + } + + if (options.users && options.users.length > 0) { + baseFilter.author = { + username: { in: options.users }, + }; + } + + return baseFilter; }; - if (options.categories && options.categories.length > 0) { - promptFilter.category = { - slug: { in: options.categories }, - }; - } - - if (options.tags && options.tags.length > 0) { - promptFilter.tags = { - some: { - tag: { slug: { in: options.tags } }, - }, - }; - } - - if (options.users && options.users.length > 0) { - promptFilter.author = { - username: { in: options.users }, - }; - } + const promptFilter = buildPromptFilter(); // Dynamic MCP Prompts - expose database prompts as MCP prompts server.server.setRequestHandler(ListPromptsRequestSchema, async (request) => { @@ -225,13 +277,26 @@ function createServer(options: ServerOptions = {}) { async ({ query, limit = 10, type, category, tag }) => { try { const where: Record = { - isPrivate: false, isUnlisted: false, deletedAt: null, - OR: [ - { title: { contains: query, mode: "insensitive" } }, - { description: { contains: query, mode: "insensitive" } }, - { content: { contains: query, mode: "insensitive" } }, + AND: [ + // Search filter + { + OR: [ + { title: { contains: query, mode: "insensitive" } }, + { description: { contains: query, mode: "insensitive" } }, + { content: { contains: query, mode: "insensitive" } }, + ], + }, + // Visibility filter: public OR user's own private prompts + authenticatedUser + ? { + OR: [ + { isPrivate: false }, + { isPrivate: true, authorId: authenticatedUser.id }, + ], + } + : { isPrivate: false }, ], }; @@ -480,6 +545,123 @@ function createServer(options: ServerOptions = {}) { } ); + // Save prompt tool - requires authentication + server.registerTool( + "save_prompt", + { + title: "Save Prompt", + description: + "Save a new prompt to your prompts.chat account. Requires API key authentication. Prompts are private by default unless configured otherwise in settings.", + inputSchema: { + title: z.string().min(1).max(200).describe("Title of the prompt"), + content: z.string().min(1).describe("The prompt content. Can include variables like ${variable} or ${variable:default}"), + description: z.string().max(500).optional().describe("Optional description of the prompt"), + tags: z.array(z.string()).max(10).optional().describe("Optional array of tag names (will be created if they don't exist)"), + category: z.string().optional().describe("Optional category slug"), + isPrivate: z.boolean().optional().describe("Whether the prompt is private (default: uses your account setting)"), + type: z.enum(["TEXT", "STRUCTURED", "IMAGE", "VIDEO", "AUDIO"]).optional().describe("Prompt type (default: TEXT)"), + structuredFormat: z.enum(["JSON", "YAML"]).optional().describe("Format for structured prompts"), + }, + }, + async ({ title, content, description, tags, category, isPrivate, type, structuredFormat }) => { + if (!authenticatedUser) { + return { + content: [{ type: "text" as const, text: JSON.stringify({ error: "Authentication required. Please provide an API key." }) }], + isError: true, + }; + } + + try { + // Determine privacy setting + const shouldBePrivate = isPrivate !== undefined ? isPrivate : !authenticatedUser.mcpPromptsPublicByDefault; + + // Find or create tags + const tagConnections: { tag: { connect: { id: string } } }[] = []; + if (tags && tags.length > 0) { + for (const tagName of tags) { + const tagSlug = slugify(tagName); + if (!tagSlug) continue; + + let tag = await db.tag.findUnique({ where: { slug: tagSlug } }); + if (!tag) { + tag = await db.tag.create({ + data: { + name: tagName, + slug: tagSlug, + }, + }); + } + tagConnections.push({ tag: { connect: { id: tag.id } } }); + } + } + + // Find category if provided + let categoryId: string | undefined; + if (category) { + const cat = await db.category.findUnique({ where: { slug: category } }); + if (cat) categoryId = cat.id; + } + + // Create the prompt + const prompt = await db.prompt.create({ + data: { + title, + slug: slugify(title), + content, + description: description || null, + isPrivate: shouldBePrivate, + type: type || "TEXT", + structuredFormat: type === "STRUCTURED" ? (structuredFormat || "JSON") : null, + authorId: authenticatedUser.id, + categoryId: categoryId || null, + tags: { + create: tagConnections, + }, + }, + select: { + id: true, + slug: true, + title: true, + description: true, + content: true, + isPrivate: true, + type: true, + createdAt: true, + tags: { select: { tag: { select: { name: true, slug: true } } } }, + category: { select: { name: true, slug: true } }, + }, + }); + + return { + content: [ + { + type: "text" as const, + text: JSON.stringify( + { + success: true, + prompt: { + ...prompt, + tags: prompt.tags.map((t) => t.tag.name), + category: prompt.category?.name || null, + link: prompt.isPrivate ? null : `https://prompts.chat/prompts/${prompt.id}_${getPromptName(prompt)}`, + }, + }, + null, + 2 + ), + }, + ], + }; + } catch (error) { + console.error("MCP save_prompt error:", error); + return { + content: [{ type: "text" as const, text: JSON.stringify({ error: "Failed to save prompt" }) }], + isError: true, + }; + } + } + ); + return server; } @@ -518,6 +700,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) name: "get_prompt", description: "Get a prompt by ID with variable elicitation support.", }, + { + name: "save_prompt", + description: "Save a new prompt (requires API key authentication).", + }, ], prompts: { description: "All public prompts are available as MCP prompts. Use prompts/list to browse and prompts/get to retrieve with variable substitution.", @@ -545,7 +731,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const tagsParam = url.searchParams.get("tags"); const usersParam = url.searchParams.get("users"); - const serverOptions: ServerOptions = {}; + // Extract API key from PROMPTS_API_KEY header or query parameter + const apiKeyHeader = req.headers["prompts_api_key"] || req.headers["prompts-api-key"]; + const apiKeyParam = url.searchParams.get("api_key"); + const apiKey = (Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader) || apiKeyParam; + + // Authenticate user if API key is provided + const authenticatedUser = await authenticateApiKey(apiKey); + + const serverOptions: ServerOptions = { authenticatedUser }; if (categoriesParam) { serverOptions.categories = categoriesParam.split(",").map((c) => c.trim()); }