feat(messages): add API key generation feature in multiple languages

This commit is contained in:
Fatih Kadir Akın
2025-12-16 23:48:24 +03:00
parent 4d0d80f8b9
commit 424eae5656
24 changed files with 1195 additions and 79 deletions
+22 -1
View File
@@ -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": "تم تحديث الإعداد"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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": "Η ρύθμιση ενημερώθηκε"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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": "ההגדרה עודכנה"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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": "設定が更新されました"
}
}
+22 -1
View File
@@ -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": "설정이 업데이트되었습니다"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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": "Настройка обновлена"
}
}
+22 -1
View File
@@ -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"
}
}
+22 -1
View File
@@ -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": "设置已更新"
}
}
@@ -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");
+16 -14
View File
@@ -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[]
+84
View File
@@ -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 });
}
+158 -1
View File
@@ -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() {
</p>
</section>
{/* Authentication */}
<section className="space-y-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<Key className="h-5 w-5" />
Authentication
</h2>
<p className="text-muted-foreground">
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.
</p>
<div className="bg-muted/50 rounded-lg p-4 text-sm space-y-3">
<div>
<p className="font-medium">Generate an API Key</p>
<p className="text-muted-foreground">
Go to{" "}
<Link href="/settings" className="underline hover:text-foreground">
Settings
</Link>
{" "}to generate your API key. Keys start with <code className="bg-muted px-1.5 py-0.5 rounded">pchat_</code>.
</p>
</div>
<div>
<p className="font-medium">Using the API Key</p>
<p className="text-muted-foreground">
Pass your API key via the <code className="bg-muted px-1.5 py-0.5 rounded">PROMPTS_API_KEY</code> header.
</p>
</div>
</div>
<div className="bg-muted rounded-lg p-4 font-mono text-sm overflow-x-auto">
<pre>{`# 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"}'`}</pre>
</div>
<p className="text-muted-foreground text-sm">
<strong>Remote (HTTP)</strong> sends requests to prompts.chat with the API key in headers.
<strong> Local (stdio)</strong> runs the MCP server locally via npx with the API key as an environment variable.
With authentication, you can use the <code className="bg-muted px-1.5 py-0.5 rounded">save_prompt</code> tool
and search results will include your private prompts.
</p>
</section>
{/* MCP Prompts */}
<section className="space-y-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
@@ -249,6 +312,100 @@ curl -X POST ${baseUrl}/api/mcp \\
</p>
</div>
</div>
{/* save_prompt Tool */}
<div className="space-y-4">
<h3 className="font-medium flex items-center gap-2">
<Save className="h-4 w-4" />
save_prompt
<span className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">Requires Auth</span>
</h3>
<p className="text-muted-foreground">
Save a new prompt to your account. Requires API key authentication. Prompts are private by default
unless you&apos;ve changed the default in your settings.
</p>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[140px]">Parameter</TableHead>
<TableHead className="w-[100px]">Type</TableHead>
<TableHead className="w-[80px]">Required</TableHead>
<TableHead>Description</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono text-xs">title</TableCell>
<TableCell className="text-muted-foreground text-xs">string</TableCell>
<TableCell className="text-xs">Yes</TableCell>
<TableCell className="text-muted-foreground text-sm">Title of the prompt (max 200 chars)</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">content</TableCell>
<TableCell className="text-muted-foreground text-xs">string</TableCell>
<TableCell className="text-xs">Yes</TableCell>
<TableCell className="text-muted-foreground text-sm">The prompt content. Can include variables like {"${var}"}</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">description</TableCell>
<TableCell className="text-muted-foreground text-xs">string</TableCell>
<TableCell className="text-xs">No</TableCell>
<TableCell className="text-muted-foreground text-sm">Optional description (max 500 chars)</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">tags</TableCell>
<TableCell className="text-muted-foreground text-xs">string[]</TableCell>
<TableCell className="text-xs">No</TableCell>
<TableCell className="text-muted-foreground text-sm">Array of tag names (max 10)</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">category</TableCell>
<TableCell className="text-muted-foreground text-xs">string</TableCell>
<TableCell className="text-xs">No</TableCell>
<TableCell className="text-muted-foreground text-sm">Category slug</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">isPrivate</TableCell>
<TableCell className="text-muted-foreground text-xs">boolean</TableCell>
<TableCell className="text-xs">No</TableCell>
<TableCell className="text-muted-foreground text-sm">Override default privacy setting</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-mono text-xs">type</TableCell>
<TableCell className="text-muted-foreground text-xs">string</TableCell>
<TableCell className="text-xs">No</TableCell>
<TableCell className="text-muted-foreground text-sm">
<code className="text-xs">TEXT</code> (default), <code className="text-xs">STRUCTURED</code>, <code className="text-xs">IMAGE</code>, <code className="text-xs">VIDEO</code>, <code className="text-xs">AUDIO</code>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div className="bg-muted rounded-lg p-4 font-mono text-sm overflow-x-auto">
<pre>{`# 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
}
}
}'`}</pre>
</div>
</div>
</section>
{/* MCP Protocol Example */}
+10 -1
View File
@@ -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() {
</p>
</div>
<ProfileForm user={user} />
<div className="space-y-6">
<ProfileForm user={user} />
<ApiKeySettings
initialApiKey={user.apiKey}
initialPublicByDefault={user.mcpPromptsPublicByDefault}
/>
</div>
</div>
);
}
+89 -17
View File
@@ -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<Client, string> = {
@@ -31,68 +33,97 @@ const CLIENT_LABELS: Record<Client, string> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<Client>("vscode");
const [internalMode, setInternalMode] = useState<Mode>("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 : "<click to reveal>")
: 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 */}
<div className="relative">
<div dir="ltr" className="bg-muted rounded-md p-2 font-mono text-[11px] overflow-x-auto text-left">
<pre className="whitespace-pre">{config}</pre>
<div
dir="ltr"
className="bg-muted rounded-md p-2 font-mono text-[11px] overflow-x-auto text-left"
>
<pre className="whitespace-pre">
{apiKey && !showApiKey ? (
<>
{displayConfig.split("<click to reveal>").map((part, i, arr) => (
<span key={i}>
{part}
{i < arr.length - 1 && (
<span
onClick={() => setShowApiKey(true)}
className="text-primary underline cursor-pointer hover:text-primary/80"
>
{"<click to reveal>"}
</span>
)}
</span>
))}
</>
) : (
displayConfig
)}
</pre>
</div>
<Button
variant="ghost"
+34 -3
View File
@@ -1,8 +1,10 @@
"use client";
import { useState, useMemo } from "react";
import { useState, useMemo, useEffect } from "react";
import { useTranslations } from "next-intl";
import { Plus, X, ChevronDown } from "lucide-react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { Plus, X, ChevronDown, Key } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -50,6 +52,7 @@ export function McpServerPopup({
baseUrl,
}: McpServerPopupProps) {
const t = useTranslations("mcp");
const { data: session } = useSession();
const [filtersOpen, setFiltersOpen] = useState(false);
const [mcpMode, setMcpMode] = useState<"remote" | "local">("remote");
const [users, setUsers] = useState<string[]>(initialUsers);
@@ -58,6 +61,23 @@ export function McpServerPopup({
const [userInput, setUserInput] = useState("");
const [categoryInput, setCategoryInput] = useState("");
const [tagInput, setTagInput] = useState("");
const [apiKey, setApiKey] = useState<string | null>(null);
// Fetch API key when user is logged in
useEffect(() => {
if (session?.user) {
fetch("/api/user/api-key")
.then((res) => res.json())
.then((data) => {
if (data.apiKey) {
setApiKey(data.apiKey);
}
})
.catch(() => {
// Ignore errors
});
}
}, [session?.user]);
// Build query params for MCP URL
const queryParams = useMemo(() => {
@@ -152,7 +172,18 @@ export function McpServerPopup({
</p>
{/* Config Tabs */}
<McpConfigTabs baseUrl={baseUrl} queryParams={queryParams || undefined} mode={mcpMode} hideModeToggle />
<McpConfigTabs baseUrl={baseUrl} queryParams={queryParams || undefined} mode={mcpMode} hideModeToggle apiKey={apiKey} />
{/* API Key Link */}
{session?.user && !apiKey && (
<Link
href="/settings"
className="flex items-center gap-1.5 text-[11px] text-primary hover:underline"
>
<Key className="h-3 w-3" />
{t("generateApiKey")}
</Link>
)}
{/* Collapsible Filters */}
<div className="border-t pt-2">
@@ -0,0 +1,243 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
import { Copy, Eye, EyeOff, RefreshCw, Trash2, Key, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";
interface ApiKeySettingsProps {
initialApiKey: string | null;
initialPublicByDefault: boolean;
}
export function ApiKeySettings({
initialApiKey,
initialPublicByDefault,
}: ApiKeySettingsProps) {
const t = useTranslations("apiKey");
const tCommon = useTranslations("common");
const [apiKey, setApiKey] = useState<string | null>(initialApiKey);
const [showKey, setShowKey] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [publicByDefault, setPublicByDefault] = useState(initialPublicByDefault);
const generateKey = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/user/api-key", {
method: "POST",
});
if (!response.ok) throw new Error("Failed to generate API key");
const data = await response.json();
setApiKey(data.apiKey);
setShowKey(true);
toast.success(t("keyGenerated"));
} catch {
toast.error(tCommon("error"));
} finally {
setIsLoading(false);
}
};
const regenerateKey = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/user/api-key", {
method: "POST",
});
if (!response.ok) throw new Error("Failed to regenerate API key");
const data = await response.json();
setApiKey(data.apiKey);
setShowKey(true);
toast.success(t("keyRegenerated"));
} catch {
toast.error(tCommon("error"));
} finally {
setIsLoading(false);
}
};
const revokeKey = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/user/api-key", {
method: "DELETE",
});
if (!response.ok) throw new Error("Failed to revoke API key");
setApiKey(null);
setShowKey(false);
toast.success(t("keyRevoked"));
} catch {
toast.error(tCommon("error"));
} finally {
setIsLoading(false);
}
};
const updatePublicDefault = async (value: boolean) => {
try {
const response = await fetch("/api/user/api-key", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mcpPromptsPublicByDefault: value }),
});
if (!response.ok) throw new Error("Failed to update setting");
setPublicByDefault(value);
toast.success(t("settingUpdated"));
} catch {
toast.error(tCommon("error"));
}
};
const copyToClipboard = () => {
if (apiKey) {
navigator.clipboard.writeText(apiKey);
toast.success(tCommon("copied"));
}
};
const maskedKey = apiKey
? `${apiKey.slice(0, 10)}${"•".repeat(32)}${apiKey.slice(-8)}`
: "";
return (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Key className="h-4 w-4" />
{t("title")}
</CardTitle>
<CardDescription>{t("description")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{apiKey ? (
<>
<div className="space-y-2">
<Label>{t("yourApiKey")}</Label>
<div className="flex items-center gap-2">
<code className="flex-1 bg-muted px-3 py-2 rounded-md text-sm font-mono overflow-hidden text-ellipsis">
{showKey ? apiKey : maskedKey}
</code>
<Button
variant="outline"
size="icon"
onClick={() => setShowKey(!showKey)}
>
{showKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
<Button variant="outline" size="icon" onClick={copyToClipboard}>
<Copy className="h-4 w-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">{t("keyWarning")}</p>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label htmlFor="public-default">{t("publicByDefault")}</Label>
<p className="text-xs text-muted-foreground">
{t("publicByDefaultDescription")}
</p>
</div>
<Switch
id="public-default"
checked={publicByDefault}
onCheckedChange={updatePublicDefault}
/>
</div>
<div className="flex gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" disabled={isLoading}>
<RefreshCw className="h-4 w-4 mr-2" />
{t("regenerate")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("regenerateTitle")}</AlertDialogTitle>
<AlertDialogDescription>
{t("regenerateDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={regenerateKey}>
{t("regenerate")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" disabled={isLoading} className="text-white">
<Trash2 className="h-4 w-4 mr-2" />
{t("revoke")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("revokeTitle")}</AlertDialogTitle>
<AlertDialogDescription>
{t("revokeDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={revokeKey}
className="bg-destructive text-white hover:bg-destructive/90"
>
{t("revoke")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</>
) : (
<div className="text-center py-4">
<p className="text-sm text-muted-foreground mb-4">
{t("noApiKey")}
</p>
<Button onClick={generateKey} disabled={isLoading}>
{isLoading ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Key className="h-4 w-4 mr-2" />
)}
{t("generate")}
</Button>
</div>
)}
</CardContent>
</Card>
);
}
+24
View File
@@ -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);
}
+223 -29
View File
@@ -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<AuthenticatedUser | null> {
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<string, unknown> = {
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<string, unknown> => {
const baseFilter: Record<string, unknown> = {
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<string, unknown> = {
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());
}