mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
✨ feat: support Streamable HTTP MCP server (#7511)
* add mcp * feat support streamable http * revert * update locale * improve style * improve experience
This commit is contained in:
+33
-2
@@ -34,9 +34,40 @@
|
||||
"desc": "العلامة المميزة للبرنامج المساعد",
|
||||
"label": "المعرف"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "قائمة المعلمات المرسلة إلى أمر STDIO",
|
||||
"label": "معلمات الأمر",
|
||||
"placeholder": "على سبيل المثال: --port 8080 --debug",
|
||||
"tooltip": "اضغط على Enter بعد إدخال المعلمات أو استخدم الفاصلة/المسافة للفصل"
|
||||
},
|
||||
"command": {
|
||||
"desc": "الملف القابل للتنفيذ أو البرنامج النصي لبدء مكون MCP STDIO",
|
||||
"label": "الأمر",
|
||||
"placeholder": "على سبيل المثال: python main.py أو /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "أدخل عنوان خادم HTTP القابل للبث MCP الخاص بك",
|
||||
"label": "عنوان URL لنقطة نهاية MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "حدد اسمًا لمكون MCP الخاص بك، يجب أن يكون باستخدام أحرف إنجليزية",
|
||||
"invalid": "يمكنك إدخال أحرف إنجليزية، أرقام، والرمزين - و _ فقط",
|
||||
"label": "اسم مكون MCP",
|
||||
"placeholder": "على سبيل المثال: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "اختر طريقة الاتصال لمكون MCP، النسخة على الويب تدعم فقط HTTP القابل للبث",
|
||||
"label": "نوع مكون MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "أدخل عنوان نقطة نهاية مكون HTTP MCP الخاص بك",
|
||||
"label": "عنوان URL لنقطة نهاية HTTP"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "تكوين بصري",
|
||||
"local-tooltip": "غير مدعوم مؤقتًا",
|
||||
"mcp": "مكون MCP",
|
||||
"mcpExp": "تجريبي",
|
||||
"url": "رابط عبر الإنترنت"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Уникалният идентификатор на плъгина",
|
||||
"label": "Идентификатор"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Списък с параметри, предадени на STDIO командата",
|
||||
"label": "Командни параметри",
|
||||
"placeholder": "Например: --port 8080 --debug",
|
||||
"tooltip": "Натиснете Enter след въвеждане на параметри или използвайте запетая/интервал за разделяне"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Изпълним файл или скрипт за стартиране на MCP STDIO плъгина",
|
||||
"label": "Команда",
|
||||
"placeholder": "Например: python main.py или /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Въведете адреса на вашия MCP Streamable HTTP сървър",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Определете име за вашия MCP плъгин, трябва да използвате английски символи",
|
||||
"invalid": "Можете да въвеждате само английски символи, цифри, - и _",
|
||||
"label": "Име на MCP плъгин",
|
||||
"placeholder": "Например: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Изберете начина на комуникация на MCP плъгина, уеб версията поддържа само Streamable HTTP",
|
||||
"label": "Тип на MCP плъгин"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Въведете адреса на вашия MCP HTTP плъгин",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Визуална конфигурация",
|
||||
"local-tooltip": "Визуалната конфигурация не се поддържа в момента",
|
||||
"mcp": "MCP плъгин",
|
||||
"mcpExp": "Експериментален",
|
||||
"url": "Онлайн връзка"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"label": "Kennung"
|
||||
},
|
||||
"mode": {
|
||||
"local": "Visuelle Konfiguration",
|
||||
"local-tooltip": "Visuelle Konfiguration vorübergehend nicht unterstützt",
|
||||
"mcp": "MCP-Plugin",
|
||||
"mcpExp": "Experimentell",
|
||||
"url": "Online-Link"
|
||||
},
|
||||
"name": {
|
||||
@@ -45,6 +45,37 @@
|
||||
"placeholder": "Suchmaschine"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Liste der Parameter, die an den STDIO-Befehl übergeben werden",
|
||||
"label": "Befehlsparameter",
|
||||
"placeholder": "z.B.: --port 8080 --debug",
|
||||
"tooltip": "Drücken Sie die Eingabetaste oder verwenden Sie Kommas/Leerzeichen zur Trennung der Eingabeparameter"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Die ausführbare Datei oder das Skript zum Starten des MCP STDIO-Plugins",
|
||||
"label": "Befehl",
|
||||
"placeholder": "z.B.: python main.py oder /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Geben Sie die Adresse Ihres MCP Streamable HTTP Servers ein",
|
||||
"label": "MCP Endpoint-URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Geben Sie Ihrem MCP-Plugin einen Namen, der englische Zeichen verwenden muss",
|
||||
"invalid": "Es dürfen nur englische Zeichen, Zahlen, - und _ verwendet werden",
|
||||
"label": "MCP-Plugin-Name",
|
||||
"placeholder": "z.B.: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Wählen Sie die Kommunikationsart des MCP-Plugins, die Webversion unterstützt nur Streamable HTTP",
|
||||
"label": "MCP-Plugin-Typ"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Geben Sie die Endpoint-Adresse Ihres MCP HTTP-Plugins ein",
|
||||
"label": "HTTP Endpoint-URL"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"author": {
|
||||
"desc": "Autor des Plugins",
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"label": "Identifier"
|
||||
},
|
||||
"mode": {
|
||||
"local": "Visual Configuration",
|
||||
"local-tooltip": "Visual configuration is not supported at the moment",
|
||||
"mcp": "MCP Plugin",
|
||||
"mcpExp": "Experimental",
|
||||
"url": "Online Link"
|
||||
},
|
||||
"name": {
|
||||
@@ -45,6 +45,37 @@
|
||||
"placeholder": "Search Engine"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "List of parameters to pass to the STDIO command",
|
||||
"label": "Command Parameters",
|
||||
"placeholder": "e.g., --port 8080 --debug",
|
||||
"tooltip": "Press Enter after entering parameters or separate with commas/spaces"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Executable file or script to start the MCP STDIO plugin",
|
||||
"label": "Command",
|
||||
"placeholder": "e.g., python main.py or /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Enter the address of your MCP Streamable HTTP Server",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Specify a name for your MCP plugin, using English characters",
|
||||
"invalid": "Only English letters, numbers, - and _ are allowed",
|
||||
"label": "MCP Plugin Name",
|
||||
"placeholder": "e.g., my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Select the communication method for the MCP plugin; the web version only supports Streamable HTTP",
|
||||
"label": "MCP Plugin Type"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Enter the Endpoint address of your MCP HTTP plugin",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"author": {
|
||||
"desc": "The author of the plugin",
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Identificador único del complemento",
|
||||
"label": "Identificador"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Lista de parámetros a pasar al comando STDIO",
|
||||
"label": "Parámetros del comando",
|
||||
"placeholder": "Por ejemplo: --port 8080 --debug",
|
||||
"tooltip": "Presiona enter después de ingresar los parámetros o usa comas/espacios para separarlos"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Archivo ejecutable o script para iniciar el plugin MCP STDIO",
|
||||
"label": "Comando",
|
||||
"placeholder": "Por ejemplo: python main.py o /ruta/al/ejecutable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Ingresa la dirección de tu servidor HTTP Streamable MCP",
|
||||
"label": "URL del Endpoint MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Especifica un nombre para tu plugin MCP, debe usar caracteres en inglés",
|
||||
"invalid": "Solo se pueden ingresar caracteres en inglés, números, y los símbolos - y _",
|
||||
"label": "Nombre del plugin MCP",
|
||||
"placeholder": "Por ejemplo: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Selecciona el método de comunicación del plugin MCP, la versión web solo soporta HTTP Streamable",
|
||||
"label": "Tipo de plugin MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Ingresa la dirección del Endpoint de tu plugin HTTP MCP",
|
||||
"label": "URL del Endpoint HTTP"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Configuración visual",
|
||||
"local-tooltip": "La configuración visual no está disponible temporalmente",
|
||||
"mcp": "Plugin MCP",
|
||||
"mcpExp": "Experimental",
|
||||
"url": "Enlace en línea"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "شناسهی یکتای افزونه",
|
||||
"label": "شناسه"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "لیست پارامترهای منتقل شده به دستور STDIO",
|
||||
"label": "پارامترهای دستور",
|
||||
"placeholder": "برای مثال: --port 8080 --debug",
|
||||
"tooltip": "پس از وارد کردن پارامترها، کلید Enter را فشار دهید یا از کاما/فاصله برای جداسازی استفاده کنید"
|
||||
},
|
||||
"command": {
|
||||
"desc": "فایل اجرایی یا اسکریپتی که برای راهاندازی افزونه MCP STDIO استفاده میشود",
|
||||
"label": "دستور",
|
||||
"placeholder": "برای مثال: python main.py یا /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "آدرس سرور HTTP Streamable MCP خود را وارد کنید",
|
||||
"label": "آدرس URL Endpoint MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "برای افزونه MCP خود یک نام مشخص کنید، باید از کاراکترهای انگلیسی استفاده کنید",
|
||||
"invalid": "فقط میتوانید از کاراکترهای انگلیسی، اعداد، و دو علامت - و _ استفاده کنید",
|
||||
"label": "نام افزونه MCP",
|
||||
"placeholder": "برای مثال: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "روش ارتباطی افزونه MCP را انتخاب کنید، نسخه وب فقط از Streamable HTTP پشتیبانی میکند",
|
||||
"label": "نوع افزونه MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "آدرس Endpoint افزونه HTTP MCP خود را وارد کنید",
|
||||
"label": "آدرس URL Endpoint HTTP"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "پیکربندی بصری",
|
||||
"local-tooltip": "پیکربندی بصری در حال حاضر پشتیبانی نمیشود",
|
||||
"mcp": "افزونه MCP",
|
||||
"mcpExp": "تجربی",
|
||||
"url": "لینک آنلاین"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Identifiant unique du plugin",
|
||||
"label": "Identifiant"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Liste des paramètres à passer à la commande STDIO",
|
||||
"label": "Paramètres de commande",
|
||||
"placeholder": "Par exemple : --port 8080 --debug",
|
||||
"tooltip": "Appuyez sur Entrée après avoir saisi les paramètres ou utilisez une virgule/un espace pour les séparer"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Fichier exécutable ou script utilisé pour démarrer le plugin MCP STDIO",
|
||||
"label": "Commande",
|
||||
"placeholder": "Par exemple : python main.py ou /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Entrez l'adresse de votre serveur HTTP Streamable MCP",
|
||||
"label": "URL de l'endpoint MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Donnez un nom à votre plugin MCP, doit utiliser des caractères anglais",
|
||||
"invalid": "Vous ne pouvez entrer que des caractères anglais, des chiffres, - et _",
|
||||
"label": "Nom du plugin MCP",
|
||||
"placeholder": "Par exemple : mon-plugin-mcp"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Choisissez le mode de communication du plugin MCP, la version web ne prend en charge que HTTP Streamable",
|
||||
"label": "Type de plugin MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Entrez l'adresse de l'endpoint de votre plugin HTTP MCP",
|
||||
"label": "URL de l'endpoint HTTP"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Configuration visuelle",
|
||||
"local-tooltip": "Configuration visuelle non prise en charge pour le moment",
|
||||
"mcp": "Plugin MCP",
|
||||
"mcpExp": "Expérimental",
|
||||
"url": "Lien en ligne"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Identificatore univoco del plugin",
|
||||
"label": "Identificatore"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Elenco di parametri da passare al comando STDIO",
|
||||
"label": "Parametri del comando",
|
||||
"placeholder": "Ad esempio: --port 8080 --debug",
|
||||
"tooltip": "Premi invio dopo aver inserito i parametri o separali con una virgola/spazio"
|
||||
},
|
||||
"command": {
|
||||
"desc": "File eseguibile o script per avviare il plugin MCP STDIO",
|
||||
"label": "Comando",
|
||||
"placeholder": "Ad esempio: python main.py o /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Inserisci l'indirizzo del tuo server HTTP Streamable MCP",
|
||||
"label": "URL Endpoint MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Assegna un nome al tuo plugin MCP, deve utilizzare caratteri inglesi",
|
||||
"invalid": "Puoi inserire solo caratteri inglesi, numeri, e i simboli - e _",
|
||||
"label": "Nome del plugin MCP",
|
||||
"placeholder": "Ad esempio: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Seleziona il metodo di comunicazione del plugin MCP, la versione web supporta solo Streamable HTTP",
|
||||
"label": "Tipo di plugin MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Inserisci l'indirizzo Endpoint del tuo plugin HTTP MCP",
|
||||
"label": "URL Endpoint HTTP"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Configurazione visuale",
|
||||
"local-tooltip": "Configurazione visuale non supportata al momento",
|
||||
"mcp": "Plugin MCP",
|
||||
"mcpExp": "Sperimentale",
|
||||
"url": "Collegamento online"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "プラグインの一意の識別子",
|
||||
"label": "識別子"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "STDIO コマンドに渡すパラメータのリスト",
|
||||
"label": "コマンドパラメータ",
|
||||
"placeholder": "例:--port 8080 --debug",
|
||||
"tooltip": "パラメータを入力したら Enter を押すか、カンマ/スペースで区切ってください"
|
||||
},
|
||||
"command": {
|
||||
"desc": "MCP STDIO プラグインを起動するための実行可能ファイルまたはスクリプト",
|
||||
"label": "コマンド",
|
||||
"placeholder": "例:python main.py または /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "あなたの MCP ストリーミング HTTP サーバーのアドレスを入力してください",
|
||||
"label": "MCP エンドポイント URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "あなたの MCP プラグインに名前を指定してください。英字を使用する必要があります",
|
||||
"invalid": "英字、数字、- および _ のみ入力できます",
|
||||
"label": "MCP プラグイン名",
|
||||
"placeholder": "例:my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "MCP プラグインの通信方法を選択してください。ウェブ版はストリーミング HTTP のみサポートしています",
|
||||
"label": "MCP プラグインタイプ"
|
||||
},
|
||||
"url": {
|
||||
"desc": "あなたの MCP HTTP プラグインのエンドポイントアドレスを入力してください",
|
||||
"label": "HTTP エンドポイント URL"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "ビジュアル設定",
|
||||
"local-tooltip": "ビジュアル設定は一時的にサポートされていません",
|
||||
"mcp": "MCP プラグイン",
|
||||
"mcpExp": "実験的",
|
||||
"url": "オンラインリンク"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"label": "식별자"
|
||||
},
|
||||
"mode": {
|
||||
"local": "시각적 구성",
|
||||
"local-tooltip": "시각적 구성은 일시적으로 지원되지 않습니다.",
|
||||
"mcp": "MCP 플러그인",
|
||||
"mcpExp": "실험적",
|
||||
"url": "온라인 링크"
|
||||
},
|
||||
"name": {
|
||||
@@ -45,6 +45,37 @@
|
||||
"placeholder": "검색 엔진"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "STDIO 명령에 전달할 매개변수 목록",
|
||||
"label": "명령 매개변수",
|
||||
"placeholder": "예: --port 8080 --debug",
|
||||
"tooltip": "매개변수를 입력한 후 Enter 키를 누르거나 쉼표/공백으로 구분하세요"
|
||||
},
|
||||
"command": {
|
||||
"desc": "MCP STDIO 플러그인을 시작하는 실행 파일 또는 스크립트",
|
||||
"label": "명령",
|
||||
"placeholder": "예: python main.py 또는 /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "당신의 MCP 스트리밍 HTTP 서버 주소를 입력하세요",
|
||||
"label": "MCP 엔드포인트 URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "당신의 MCP 플러그인에 이름을 지정하세요, 영어 문자 사용 필요",
|
||||
"invalid": "영어 문자, 숫자, - 및 _ 기호만 입력할 수 있습니다",
|
||||
"label": "MCP 플러그인 이름",
|
||||
"placeholder": "예: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "MCP 플러그인의 통신 방식을 선택하세요, 웹 버전은 스트리밍 HTTP만 지원합니다",
|
||||
"label": "MCP 플러그인 유형"
|
||||
},
|
||||
"url": {
|
||||
"desc": "당신의 MCP HTTP 플러그인의 엔드포인트 주소를 입력하세요",
|
||||
"label": "HTTP 엔드포인트 URL"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"author": {
|
||||
"desc": "플러그인 작성자",
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "De unieke identificatie van de plug-in",
|
||||
"label": "Identificatie"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Lijst van parameters die aan het STDIO-commando worden doorgegeven",
|
||||
"label": "Commando-parameters",
|
||||
"placeholder": "Bijvoorbeeld: --port 8080 --debug",
|
||||
"tooltip": "Druk op enter of gebruik komma's/spaties om parameters te scheiden"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Uitvoerbaar bestand of script om de MCP STDIO-plugin te starten",
|
||||
"label": "Commando",
|
||||
"placeholder": "Bijvoorbeeld: python main.py of /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Voer het adres van je MCP Streamable HTTP Server in",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Geef je MCP-plugin een naam, gebruik Engelse karakters",
|
||||
"invalid": "Alleen Engelse karakters, cijfers, - en _ zijn toegestaan",
|
||||
"label": "Naam van de MCP-plugin",
|
||||
"placeholder": "Bijvoorbeeld: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Kies de communicatiemethode voor de MCP-plugin, de webversie ondersteunt alleen Streamable HTTP",
|
||||
"label": "Type MCP-plugin"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Voer het Endpoint-adres van je MCP HTTP-plugin in",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Visuele configuratie",
|
||||
"local-tooltip": "Visuele configuratie wordt op dit moment niet ondersteund",
|
||||
"mcp": "MCP-plugin",
|
||||
"mcpExp": "Experimenteel",
|
||||
"url": "Online link"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"label": "Identyfikator"
|
||||
},
|
||||
"mode": {
|
||||
"local": "Konfiguracja wizualna",
|
||||
"local-tooltip": "Konfiguracja wizualna nie jest obecnie obsługiwana",
|
||||
"mcp": "Wtyczka MCP",
|
||||
"mcpExp": "Eksperymentalna",
|
||||
"url": "Link online"
|
||||
},
|
||||
"name": {
|
||||
@@ -45,6 +45,37 @@
|
||||
"placeholder": "Wyszukiwarka"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Lista argumentów przekazywanych do polecenia STDIO",
|
||||
"label": "Argumenty polecenia",
|
||||
"placeholder": "Na przykład: --port 8080 --debug",
|
||||
"tooltip": "Naciśnij Enter po wprowadzeniu argumentów lub użyj przecinka/spacji jako separatora"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Wykonywalny plik lub skrypt do uruchomienia wtyczki MCP STDIO",
|
||||
"label": "Polecenie",
|
||||
"placeholder": "Na przykład: python main.py lub /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Wprowadź adres swojego serwera MCP Streamable HTTP",
|
||||
"label": "URL punktu końcowego MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Nadaj nazwę swojej wtyczce MCP, używając znaków angielskich",
|
||||
"invalid": "Można wprowadzać tylko znaki angielskie, cyfry, oraz symbole - i _",
|
||||
"label": "Nazwa wtyczki MCP",
|
||||
"placeholder": "Na przykład: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Wybierz sposób komunikacji wtyczki MCP, wersja przeglądarkowa obsługuje tylko Streamable HTTP",
|
||||
"label": "Typ wtyczki MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Wprowadź adres punktu końcowego swojej wtyczki MCP HTTP",
|
||||
"label": "URL punktu końcowego HTTP"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"author": {
|
||||
"desc": "Autor wtyczki",
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Identificador único do plugin",
|
||||
"label": "Identificador"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Lista de parâmetros a serem passados para o comando STDIO",
|
||||
"label": "Parâmetros do comando",
|
||||
"placeholder": "Por exemplo: --port 8080 --debug",
|
||||
"tooltip": "Pressione Enter após inserir os parâmetros ou use vírgula/espaço para separar"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Arquivo executável ou script usado para iniciar o plugin MCP STDIO",
|
||||
"label": "Comando",
|
||||
"placeholder": "Por exemplo: python main.py ou /caminho/para/executável"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Insira o endereço do seu Servidor HTTP Streamable MCP",
|
||||
"label": "URL do Endpoint MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Dê um nome ao seu plugin MCP, deve usar caracteres em inglês",
|
||||
"invalid": "Somente caracteres em inglês, números, - e _ são permitidos",
|
||||
"label": "Nome do plugin MCP",
|
||||
"placeholder": "Por exemplo: meu-plugin-mcp"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Escolha o método de comunicação do plugin MCP, a versão web suporta apenas HTTP Streamable",
|
||||
"label": "Tipo de plugin MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Insira o endereço do Endpoint do seu plugin HTTP MCP",
|
||||
"label": "URL do Endpoint HTTP"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Configuração Visual",
|
||||
"local-tooltip": "Configuração visual não suportada temporariamente",
|
||||
"mcp": "Plugin MCP",
|
||||
"mcpExp": "Experimental",
|
||||
"url": "Link Online"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Уникальный идентификатор плагина",
|
||||
"label": "Идентификатор"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Список параметров, передаваемых в команду STDIO",
|
||||
"label": "Параметры команды",
|
||||
"placeholder": "Например: --port 8080 --debug",
|
||||
"tooltip": "Нажмите Enter после ввода параметров или используйте запятую/пробел для разделения"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Исполняемый файл или скрипт для запуска MCP STDIO плагина",
|
||||
"label": "Команда",
|
||||
"placeholder": "Например: python main.py или /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Введите адрес вашего MCP Streamable HTTP сервера",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Укажите имя для вашего MCP плагина, необходимо использовать английские символы",
|
||||
"invalid": "Можно вводить только английские буквы, цифры, символы - и _",
|
||||
"label": "Имя MCP плагина",
|
||||
"placeholder": "Например: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Выберите способ связи для MCP плагина, веб-версия поддерживает только Streamable HTTP",
|
||||
"label": "Тип MCP плагина"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Введите адрес Endpoint вашего MCP HTTP плагина",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Локальная настройка",
|
||||
"local-tooltip": "Локальная настройка временно недоступна",
|
||||
"mcp": "MCP плагин",
|
||||
"mcpExp": "Экспериментальный",
|
||||
"url": "Ссылка онлайн"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "Eklenti için benzersiz tanımlayıcı",
|
||||
"label": "Tanımlayıcı"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "STDIO komutuna iletilen parametreler listesi",
|
||||
"label": "Komut Parametreleri",
|
||||
"placeholder": "Örneğin: --port 8080 --debug",
|
||||
"tooltip": "Parametreleri girdikten sonra enter tuşuna basın veya virgül/boşluk ile ayırın"
|
||||
},
|
||||
"command": {
|
||||
"desc": "MCP STDIO eklentisini başlatmak için kullanılacak yürütülebilir dosya veya betik",
|
||||
"label": "Komut",
|
||||
"placeholder": "Örneğin: python main.py veya /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "MCP Streamable HTTP Sunucunuzun adresini girin",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "MCP eklentinize bir ad verin, İngilizce karakterler kullanmalısınız",
|
||||
"invalid": "Sadece İngilizce karakterler, rakamlar, - ve _ bu iki sembolü girebilirsiniz",
|
||||
"label": "MCP Eklenti Adı",
|
||||
"placeholder": "Örneğin: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "MCP eklentisinin iletişim yöntemini seçin, web sürümü yalnızca Streamable HTTP'yi destekler",
|
||||
"label": "MCP Eklenti Türü"
|
||||
},
|
||||
"url": {
|
||||
"desc": "MCP HTTP eklentinizin Endpoint adresini girin",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "Yapılandırma",
|
||||
"local-tooltip": "Yapılandırma geçici olarak desteklenmiyor",
|
||||
"mcp": "MCP Eklentisi",
|
||||
"mcpExp": "Deneysel",
|
||||
"url": "Çevrimiçi Bağlantı"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"label": "Định danh"
|
||||
},
|
||||
"mode": {
|
||||
"local": "Cấu hình trực quan",
|
||||
"local-tooltip": "Tạm thời không hỗ trợ cấu hình trực quan",
|
||||
"mcp": "MCP Plugin",
|
||||
"mcpExp": "Thí nghiệm",
|
||||
"url": "Liên kết trực tuyến"
|
||||
},
|
||||
"name": {
|
||||
@@ -45,6 +45,37 @@
|
||||
"placeholder": "Tìm kiếm công cụ tìm kiếm"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "Danh sách các tham số được truyền cho lệnh STDIO",
|
||||
"label": "Tham số lệnh",
|
||||
"placeholder": "Ví dụ: --port 8080 --debug",
|
||||
"tooltip": "Nhấn Enter sau khi nhập tham số hoặc sử dụng dấu phẩy/khoảng trắng để phân tách"
|
||||
},
|
||||
"command": {
|
||||
"desc": "Tệp thực thi hoặc kịch bản để khởi động plugin MCP STDIO",
|
||||
"label": "Lệnh",
|
||||
"placeholder": "Ví dụ: python main.py hoặc /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "Nhập địa chỉ của máy chủ HTTP Streamable MCP của bạn",
|
||||
"label": "URL Điểm cuối MCP"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "Chỉ định một tên cho plugin MCP của bạn, cần sử dụng ký tự tiếng Anh",
|
||||
"invalid": "Chỉ có thể nhập ký tự tiếng Anh, số, và hai ký hiệu - và _",
|
||||
"label": "Tên plugin MCP",
|
||||
"placeholder": "Ví dụ: my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "Chọn phương thức giao tiếp của plugin MCP, phiên bản web chỉ hỗ trợ Streamable HTTP",
|
||||
"label": "Loại plugin MCP"
|
||||
},
|
||||
"url": {
|
||||
"desc": "Nhập địa chỉ Điểm cuối HTTP của plugin MCP của bạn",
|
||||
"label": "URL Điểm cuối HTTP"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"author": {
|
||||
"desc": "Tác giả của plugin",
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"label": "标识符"
|
||||
},
|
||||
"mode": {
|
||||
"local": "可视化配置",
|
||||
"local-tooltip": "暂时不支持可视化配置",
|
||||
"mcp": "MCP 插件",
|
||||
"mcpExp": "实验性",
|
||||
"url": "在线链接"
|
||||
},
|
||||
"name": {
|
||||
@@ -45,6 +45,37 @@
|
||||
"placeholder": "搜索引擎"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "传递给 STDIO 命令的参数列表",
|
||||
"label": "命令参数",
|
||||
"placeholder": "例如:--port 8080 --debug",
|
||||
"tooltip": "输入参数后按回车或使用逗号/空格分隔"
|
||||
},
|
||||
"command": {
|
||||
"desc": "用于启动 MCP STDIO 插件的可执行文件或脚本",
|
||||
"label": "命令",
|
||||
"placeholder": "例如:python main.py 或 /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "输入你的 MCP Streamable HTTP Server 的地址",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "为你的 MCP 插件指定一个名称,需要使用英文字符",
|
||||
"invalid": "只能输入英文字符、数字 、- 和_ 这两个符号",
|
||||
"label": "MCP 插件名称",
|
||||
"placeholder": "例如:my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP",
|
||||
"label": "MCP 插件类型"
|
||||
},
|
||||
"url": {
|
||||
"desc": "输入你的 MCP HTTP 插件的 Endpoint 地址",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"author": {
|
||||
"desc": "插件的作者",
|
||||
|
||||
@@ -34,9 +34,40 @@
|
||||
"desc": "外掛的唯一識別碼",
|
||||
"label": "識別碼"
|
||||
},
|
||||
"mcp": {
|
||||
"args": {
|
||||
"desc": "傳遞給 STDIO 命令的參數列表",
|
||||
"label": "命令參數",
|
||||
"placeholder": "例如:--port 8080 --debug",
|
||||
"tooltip": "輸入參數後按回車或使用逗號/空格分隔"
|
||||
},
|
||||
"command": {
|
||||
"desc": "用於啟動 MCP STDIO 插件的可執行文件或腳本",
|
||||
"label": "命令",
|
||||
"placeholder": "例如:python main.py 或 /path/to/executable"
|
||||
},
|
||||
"endpoint": {
|
||||
"desc": "輸入你的 MCP Streamable HTTP Server 的地址",
|
||||
"label": "MCP Endpoint URL"
|
||||
},
|
||||
"identifier": {
|
||||
"desc": "為你的 MCP 插件指定一個名稱,需要使用英文字符",
|
||||
"invalid": "只能輸入英文字符、數字、- 和_ 這兩個符號",
|
||||
"label": "MCP 插件名稱",
|
||||
"placeholder": "例如:my-mcp-plugin"
|
||||
},
|
||||
"type": {
|
||||
"desc": "選擇 MCP 插件的通信方式,網頁版只支持 Streamable HTTP",
|
||||
"label": "MCP 插件類型"
|
||||
},
|
||||
"url": {
|
||||
"desc": "輸入你的 MCP HTTP 插件的 Endpoint 地址",
|
||||
"label": "HTTP Endpoint URL"
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"local": "視覺設定",
|
||||
"local-tooltip": "目前不支援視覺設定",
|
||||
"mcp": "MCP 插件",
|
||||
"mcpExp": "實驗性",
|
||||
"url": "線上連結"
|
||||
},
|
||||
"name": {
|
||||
|
||||
@@ -13,7 +13,10 @@ const ManifestPreviewer = memo<PluginManifestPreviewerProps>(
|
||||
<Popover
|
||||
arrow={false}
|
||||
content={
|
||||
<Highlighter language={'json'} style={{ maxHeight: 600, maxWidth: 400 }}>
|
||||
<Highlighter
|
||||
language={'json'}
|
||||
style={{ maxHeight: 600, maxWidth: 400, overflow: 'scroll' }}
|
||||
>
|
||||
{JSON.stringify(manifest, null, 2)}
|
||||
</Highlighter>
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PluginStore from '@/features/PluginStore';
|
||||
import PluginAvatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
||||
import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
|
||||
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
||||
import { useWorkspaceModal } from '@/hooks/useWorkspaceModal';
|
||||
@@ -67,7 +68,7 @@ const DropdownMenu = memo<PropsWithChildren>(({ children }) => {
|
||||
children: [
|
||||
...list.map((item) => ({
|
||||
icon: item.meta?.avatar ? (
|
||||
<Avatar avatar={pluginHelpers.getPluginAvatar(item.meta)} size={24} />
|
||||
<PluginAvatar avatar={pluginHelpers.getPluginAvatar(item.meta)} size={24} />
|
||||
) : (
|
||||
<Icon icon={ToyBrick} size={{ fontSize: 16 }} style={{ padding: 4 }} />
|
||||
),
|
||||
|
||||
@@ -24,7 +24,13 @@ const Usage = memo<UsageProps>(({ model, metadata, provider }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} className={styles.container} horizontal justify={'space-between'}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.container}
|
||||
gap={12}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
>
|
||||
<Center gap={4} horizontal style={{ fontSize: 12 }}>
|
||||
<ModelIcon model={model as string} type={'mono'} />
|
||||
{model}
|
||||
|
||||
@@ -75,7 +75,7 @@ const CustomRender = memo<CustomRenderProps>(
|
||||
useEffect(() => {
|
||||
if (!plugin?.type || loading) return;
|
||||
|
||||
setShowPluginRender(plugin?.type !== 'default');
|
||||
setShowPluginRender(!['default', 'mcp'].includes(plugin?.type));
|
||||
}, [plugin?.type, loading]);
|
||||
|
||||
if (loading) return <Arguments arguments={requestArgs} shine />;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Avatar, Icon } from '@lobehub/ui';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { LucideToyBrick } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Avatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { ActionIcon, FormItem } from '@lobehub/ui';
|
||||
import { Form, FormInstance, Input, Radio, Select } from 'antd';
|
||||
import { FileCode, RotateCwIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import ManifestPreviewer from '@/components/ManifestPreviewer';
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { mcpService } from '@/services/mcp';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
import { PluginInstallError } from '@/types/tool/plugin';
|
||||
|
||||
interface MCPManifestFormProps {
|
||||
form: FormInstance;
|
||||
isEditMode?: boolean;
|
||||
}
|
||||
|
||||
const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const mcpType = Form.useWatch(['customParams', 'mcp', 'type'], form);
|
||||
const [manifest, setManifest] = useState<LobeChatPluginManifest>();
|
||||
const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
|
||||
|
||||
const HTTP_URL_KEY = ['customParams', 'mcp', 'url'];
|
||||
return (
|
||||
<Form form={form} layout={'vertical'}>
|
||||
<Flexbox gap={16}>
|
||||
<Form.Item
|
||||
extra={t('dev.mcp.identifier.desc')}
|
||||
label={t('dev.mcp.identifier.label')}
|
||||
name={'identifier'}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
message: t('dev.mcp.identifier.invalid'),
|
||||
pattern: /^[\w-]+$/,
|
||||
},
|
||||
// 编辑模式下,不进行重复校验
|
||||
isEditMode
|
||||
? {}
|
||||
: {
|
||||
message: t('dev.meta.identifier.errorDuplicate'),
|
||||
validator: async () => {
|
||||
const id = form.getFieldValue('identifier');
|
||||
if (!id) return true;
|
||||
|
||||
if (pluginIds.includes(id)) {
|
||||
throw new Error('Duplicate');
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('dev.mcp.identifier.placeholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
extra={t('dev.mcp.type.desc')}
|
||||
initialValue={'http'}
|
||||
label={t('dev.mcp.type.label')}
|
||||
name={['customParams', 'mcp', 'type']}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={'http'}>Streamable HTTP</Radio>
|
||||
<Radio disabled={!isDesktop} value={'stdio'}>
|
||||
STDIO
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{mcpType === 'http' && (
|
||||
<Form.Item
|
||||
extra={
|
||||
<Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
|
||||
{t('dev.mcp.url.desc')}
|
||||
{manifest && (
|
||||
<ManifestPreviewer manifest={manifest}>
|
||||
<ActionIcon
|
||||
icon={FileCode}
|
||||
size={'small'}
|
||||
title={t('dev.meta.manifest.preview')}
|
||||
/>
|
||||
</ManifestPreviewer>
|
||||
)}
|
||||
</Flexbox>
|
||||
}
|
||||
hasFeedback
|
||||
label={t('dev.mcp.url.label')}
|
||||
name={HTTP_URL_KEY}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ type: 'url' },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (!value) return true;
|
||||
|
||||
try {
|
||||
const data = await mcpService.getStreamableMcpServerManifest(
|
||||
form.getFieldValue('identifier'),
|
||||
value,
|
||||
);
|
||||
setManifest(data);
|
||||
|
||||
form.setFieldsValue({ identifier: data.identifier, manifest: data });
|
||||
} catch (error) {
|
||||
const err = error as PluginInstallError;
|
||||
throw t(`error.${err.message}`, { error: err.cause! });
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="https://mcp.higress.ai/mcp-github/xxxxx"
|
||||
suffix={
|
||||
<ActionIcon
|
||||
icon={RotateCwIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
form.validateFields([HTTP_URL_KEY]);
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('dev.meta.manifest.refresh')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{mcpType === 'stdio' && (
|
||||
<>
|
||||
<Form.Item
|
||||
extra={t('dev.mcp.command.desc')}
|
||||
label={t('dev.mcp.command.label')}
|
||||
name={['mcp', 'command']}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input placeholder={t('dev.mcp.command.placeholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
extra={t('dev.mcp.args.desc')}
|
||||
label={t('dev.mcp.args.label')}
|
||||
name={['mcp', 'args']}
|
||||
tooltip={t('dev.mcp.args.tooltip')}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={t('dev.mcp.args.placeholder')}
|
||||
tokenSeparators={[',', ' ']}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<FormItem name={'manifest'} noStyle />
|
||||
</Flexbox>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCPManifestForm;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Avatar, Form } from '@lobehub/ui';
|
||||
import { Form } from '@lobehub/ui';
|
||||
import { Form as AForm, Card, FormInstance } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PluginAvatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
||||
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
||||
import { pluginHelpers } from '@/store/tool';
|
||||
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
||||
@@ -15,7 +16,7 @@ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
||||
const meta = plugin?.manifest?.meta;
|
||||
|
||||
const items = {
|
||||
avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(meta)} style={{ flex: 'none' }} />,
|
||||
avatar: <PluginAvatar avatar={pluginHelpers.getPluginAvatar(meta)} />,
|
||||
desc: pluginHelpers.getPluginDesc(meta) || 'Plugin Description',
|
||||
label: (
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
@@ -27,7 +28,7 @@ const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card bodyStyle={{ padding: '0 16px' }} size={'small'} title={t('dev.preview.card')}>
|
||||
<Card size={'small'} styles={{ body: { padding: '0 16px' } }} title={t('dev.preview.card')}>
|
||||
<Form.Item {...items} colon={false} style={{ alignItems: 'center', marginBottom: 0 }} />
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Alert, Icon, Modal, Tooltip } from '@lobehub/ui';
|
||||
import { App, Button, Form, Popconfirm, Segmented } from 'antd';
|
||||
import { Alert, Icon, Modal } from '@lobehub/ui';
|
||||
import { App, Button, Form, Popconfirm, Segmented, Tag } from 'antd';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { MoveUpRight } from 'lucide-react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
@@ -9,6 +9,7 @@ import { Flexbox } from 'react-layout-kit';
|
||||
import { WIKI_PLUGIN_GUIDE } from '@/const/url';
|
||||
import { LobeToolCustomPlugin } from '@/types/tool/plugin';
|
||||
|
||||
import MCPManifestForm from './MCPManifestForm';
|
||||
import PluginPreview from './PluginPreview';
|
||||
import UrlManifestForm from './UrlManifestForm';
|
||||
|
||||
@@ -25,7 +26,7 @@ interface DevModalProps {
|
||||
const DevModal = memo<DevModalProps>(
|
||||
({ open, mode = 'create', value, onValueChange, onSave, onOpenChange, onDelete }) => {
|
||||
const isEditMode = mode === 'edit';
|
||||
const [configMode, setConfigMode] = useState<'url' | 'local'>('url');
|
||||
const [configMode, setConfigMode] = useState<'url' | 'mcp'>('mcp');
|
||||
const { t } = useTranslation('plugin');
|
||||
const { message } = App.useApp();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -118,49 +119,57 @@ const DevModal = memo<DevModalProps>(
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
message={
|
||||
<Trans i18nKey={'dev.modalDesc'} ns={'plugin'}>
|
||||
添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考:
|
||||
<a
|
||||
href={WIKI_PLUGIN_GUIDE}
|
||||
rel="noreferrer"
|
||||
style={{ paddingInline: 8 }}
|
||||
target={'_blank'}
|
||||
>
|
||||
文档
|
||||
</a>
|
||||
<Icon icon={MoveUpRight} />
|
||||
</Trans>
|
||||
}
|
||||
showIcon
|
||||
type={'info'}
|
||||
/>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(e) => {
|
||||
setConfigMode(e as any);
|
||||
setConfigMode(e as 'url' | 'mcp');
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: (
|
||||
<Flexbox align={'center'} gap={4} horizontal justify={'center'}>
|
||||
{t('dev.manifest.mode.mcp')}
|
||||
<div>
|
||||
<Tag bordered={false} color={'warning'}>
|
||||
{t('dev.manifest.mode.mcpExp')}
|
||||
</Tag>
|
||||
</div>
|
||||
</Flexbox>
|
||||
),
|
||||
value: 'mcp',
|
||||
},
|
||||
{
|
||||
label: t('dev.manifest.mode.url'),
|
||||
value: 'url',
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
label: (
|
||||
<Tooltip title={t('dev.manifest.mode.local-tooltip')}>
|
||||
{t('dev.manifest.mode.local')}
|
||||
</Tooltip>
|
||||
),
|
||||
value: 'local',
|
||||
},
|
||||
]}
|
||||
value={configMode}
|
||||
/>
|
||||
|
||||
{configMode === 'url' ? (
|
||||
<UrlManifestForm form={form} isEditMode={mode === 'edit'} />
|
||||
) : null}
|
||||
{configMode === 'url' && (
|
||||
<>
|
||||
<Alert
|
||||
message={
|
||||
<Trans i18nKey={'dev.modalDesc'} ns={'plugin'}>
|
||||
添加自定义插件后,可用于插件开发验证,也可直接在会话中使用。插件开发文档请参考:
|
||||
<a
|
||||
href={WIKI_PLUGIN_GUIDE}
|
||||
rel="noreferrer"
|
||||
style={{ paddingInline: 8 }}
|
||||
target={'_blank'}
|
||||
>
|
||||
文档
|
||||
</a>
|
||||
<Icon icon={MoveUpRight} />
|
||||
</Trans>
|
||||
}
|
||||
showIcon
|
||||
type={'info'}
|
||||
/>
|
||||
<UrlManifestForm form={form} isEditMode={mode === 'edit'} />
|
||||
</>
|
||||
)}
|
||||
{configMode === 'mcp' && <MCPManifestForm form={form} />}
|
||||
<PluginPreview form={form} />
|
||||
</Flexbox>
|
||||
</Modal>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { forwardRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DevModal from '@/features/PluginDevModal';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
|
||||
const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
||||
@@ -15,6 +16,7 @@ const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
||||
s.installCustomPlugin,
|
||||
s.updateNewCustomPlugin,
|
||||
]);
|
||||
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -26,7 +28,7 @@ const AddPluginButton = forwardRef<HTMLButtonElement>((props, ref) => {
|
||||
onOpenChange={setModal}
|
||||
onSave={async (devPlugin) => {
|
||||
await installCustomPlugin(devPlugin);
|
||||
// toggleAgentPlugin(devPlugin.identifier);
|
||||
await togglePlugin(devPlugin.identifier);
|
||||
}}
|
||||
onValueChange={updateNewDevPlugin}
|
||||
open={showModal}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PluginDetailModal from '@/features/PluginDetailModal';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useServerConfigStore } from '@/store/serverConfig';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
|
||||
@@ -31,6 +32,7 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const [open, setOpen] = useState(false);
|
||||
const plugin = useToolStore(pluginSelectors.getToolManifestById(identifier));
|
||||
const togglePlugin = useAgentStore((s) => s.togglePlugin);
|
||||
const { modal } = App.useApp();
|
||||
const [tab, setTab] = useState('info');
|
||||
const hasSettings = pluginHelpers.isSettingSchemaNonEmpty(plugin?.settings);
|
||||
@@ -89,8 +91,9 @@ const Actions = memo<ActionsProps>(({ identifier, type }) => {
|
||||
) : (
|
||||
<Button
|
||||
loading={installing}
|
||||
onClick={() => {
|
||||
installPlugin(identifier);
|
||||
onClick={async () => {
|
||||
await installPlugin(identifier);
|
||||
await togglePlugin(identifier);
|
||||
}}
|
||||
size={mobile ? 'small' : undefined}
|
||||
type={'primary'}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MCP } from '@lobehub/icons';
|
||||
import { Avatar } from '@lobehub/ui';
|
||||
import { CSSProperties, memo } from 'react';
|
||||
|
||||
interface PluginAvatarProps {
|
||||
alt?: string;
|
||||
avatar?: string;
|
||||
size?: number;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const PluginAvatar = memo<PluginAvatarProps>(({ avatar, style, size, alt }) => {
|
||||
return avatar === 'MCP_AVATAR' ? (
|
||||
<MCP.Avatar size={size ? size * 0.8 : 36} />
|
||||
) : (
|
||||
<Avatar
|
||||
alt={alt}
|
||||
avatar={avatar}
|
||||
size={size}
|
||||
style={{ flex: 'none', overflow: 'hidden', ...style }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginAvatar;
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Avatar, Tooltip } from '@lobehub/ui';
|
||||
import { Tooltip } from '@lobehub/ui';
|
||||
import { Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PluginTag from '@/features/PluginStore/PluginItem/PluginTag';
|
||||
import { InstallPluginMeta } from '@/types/tool/plugin';
|
||||
|
||||
import Actions from './Action';
|
||||
import PluginAvatar from './PluginAvatar';
|
||||
import PluginTag from './PluginTag';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
@@ -51,7 +52,7 @@ const PluginItem = memo<InstallPluginMeta>(({ identifier, homepage, author, type
|
||||
horizontal
|
||||
style={{ overflow: 'hidden', position: 'relative' }}
|
||||
>
|
||||
<Avatar avatar={meta.avatar} style={{ flex: 'none', overflow: 'hidden' }} />
|
||||
<PluginAvatar avatar={meta.avatar} />
|
||||
<Flexbox flex={1} gap={4} style={{ overflow: 'hidden', position: 'relative' }}>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
<Tooltip title={identifier}>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { LucideToyBrick } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { Center } from 'react-layout-kit';
|
||||
|
||||
import Avatar from '@/features/PluginStore/PluginItem/PluginAvatar';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
@@ -30,7 +32,11 @@ const PluginTag = memo<PluginTagProps>(({ plugins }) => {
|
||||
const avatar = isDeprecated ? '♻️' : pluginHelpers.getPluginAvatar(item?.meta);
|
||||
|
||||
return {
|
||||
icon: <Avatar avatar={avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />,
|
||||
icon: (
|
||||
<Center style={{ minWidth: 24 }}>
|
||||
<Avatar avatar={avatar} size={24} />
|
||||
</Center>
|
||||
),
|
||||
key: id,
|
||||
label: (
|
||||
<PluginStatus
|
||||
|
||||
+2
-2
@@ -36,10 +36,10 @@ describe('MCPClient', () => {
|
||||
const result = await mcpClient.listTools();
|
||||
|
||||
// Check exact length if no other tools are expected
|
||||
expect(result.tools).toHaveLength(3);
|
||||
expect(result).toHaveLength(3);
|
||||
|
||||
// Expect the tools defined in mock-sdk-server.ts
|
||||
expect(result.tools).toMatchSnapshot();
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call the "echo" tool via stdio', async () => {
|
||||
@@ -4,54 +4,34 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
||||
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.d.ts';
|
||||
import debug from 'debug';
|
||||
|
||||
import { MCPClientParams, McpTool } from './types';
|
||||
|
||||
const log = debug('lobe-mcp:client');
|
||||
|
||||
interface MCPConnectionBase {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'http' | 'stdio';
|
||||
}
|
||||
|
||||
interface HttpMCPConnection extends MCPConnectionBase {
|
||||
type: 'http';
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface StdioMCPConnection extends MCPConnectionBase {
|
||||
args: string[];
|
||||
command: string;
|
||||
type: 'stdio';
|
||||
}
|
||||
type MCPConnection = HttpMCPConnection | StdioMCPConnection;
|
||||
|
||||
export class MCPClient {
|
||||
private mcp: Client;
|
||||
private transport: Transport;
|
||||
|
||||
constructor(connection: MCPConnection) {
|
||||
log('Creating MCPClient with connection: %O', connection);
|
||||
constructor(params: MCPClientParams) {
|
||||
log('Creating MCPClient with connection: %O', params);
|
||||
this.mcp = new Client({ name: 'lobehub-mcp-client', version: '1.0.0' });
|
||||
|
||||
switch (connection.type) {
|
||||
switch (params.type) {
|
||||
case 'http': {
|
||||
log('Using HTTP transport with url: %s', connection.url);
|
||||
this.transport = new StreamableHTTPClientTransport(new URL(connection.url));
|
||||
log('Using HTTP transport with url: %s', params.url);
|
||||
this.transport = new StreamableHTTPClientTransport(new URL(params.url));
|
||||
break;
|
||||
}
|
||||
case 'stdio': {
|
||||
log(
|
||||
'Using Stdio transport with command: %s and args: %O',
|
||||
connection.command,
|
||||
connection.args,
|
||||
);
|
||||
log('Using Stdio transport with command: %s and args: %O', params.command, params.args);
|
||||
this.transport = new StdioClientTransport({
|
||||
args: connection.args,
|
||||
command: connection.command,
|
||||
args: params.args,
|
||||
command: params.command,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error(`Unsupported MCP connection type: ${(connection as any).type}`);
|
||||
const err = new Error(`Unsupported MCP connection type: ${(params as any).type}`);
|
||||
log('Error creating client: %O', err);
|
||||
throw err;
|
||||
}
|
||||
@@ -64,11 +44,27 @@ export class MCPClient {
|
||||
log('MCP connection initialized.');
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
log('Disconnecting MCP connection...');
|
||||
// Assuming the mcp client has a disconnect method
|
||||
if (this.mcp && typeof (this.mcp as any).disconnect === 'function') {
|
||||
await (this.mcp as any).disconnect();
|
||||
log('MCP connection disconnected.');
|
||||
} else {
|
||||
log('MCP client does not have a disconnect method or is not initialized.');
|
||||
// Depending on the transport, we might need specific cleanup
|
||||
if (this.transport && typeof (this.transport as any).close === 'function') {
|
||||
(this.transport as any).close();
|
||||
log('Transport closed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async listTools() {
|
||||
log('Listing tools...');
|
||||
const tools = await this.mcp.listTools();
|
||||
const { tools } = await this.mcp.listTools();
|
||||
log('Listed tools: %O', tools);
|
||||
return tools;
|
||||
return tools as McpTool[];
|
||||
}
|
||||
|
||||
async callTool(toolName: string, args: any) {
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './client';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,27 @@
|
||||
interface InputSchema {
|
||||
[k: string]: unknown;
|
||||
|
||||
properties?: unknown | null;
|
||||
type: 'object';
|
||||
}
|
||||
|
||||
export interface McpTool {
|
||||
description: string;
|
||||
inputSchema: InputSchema;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface HttpMCPClientParams {
|
||||
name: string;
|
||||
type: 'http';
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface StdioMCPParams {
|
||||
args: string[];
|
||||
command: string;
|
||||
name: string;
|
||||
type: 'stdio';
|
||||
}
|
||||
|
||||
export type MCPClientParams = HttpMCPClientParams | StdioMCPParams;
|
||||
@@ -35,9 +35,9 @@ export default {
|
||||
label: '标识符',
|
||||
},
|
||||
mode: {
|
||||
'local': '可视化配置',
|
||||
'local-tooltip': '暂时不支持可视化配置',
|
||||
'url': '在线链接',
|
||||
mcp: 'MCP 插件',
|
||||
mcpExp: '实验性',
|
||||
url: '在线链接',
|
||||
},
|
||||
name: {
|
||||
desc: '插件标题',
|
||||
@@ -45,6 +45,37 @@ export default {
|
||||
placeholder: '搜索引擎',
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
args: {
|
||||
desc: '传递给 STDIO 命令的参数列表',
|
||||
label: '命令参数',
|
||||
placeholder: '例如:--port 8080 --debug',
|
||||
tooltip: '输入参数后按回车或使用逗号/空格分隔',
|
||||
},
|
||||
command: {
|
||||
desc: '用于启动 MCP STDIO 插件的可执行文件或脚本',
|
||||
label: '命令',
|
||||
placeholder: '例如:python main.py 或 /path/to/executable',
|
||||
},
|
||||
endpoint: {
|
||||
desc: '输入你的 MCP Streamable HTTP Server 的地址',
|
||||
label: 'MCP Endpoint URL',
|
||||
},
|
||||
identifier: {
|
||||
desc: '为你的 MCP 插件指定一个名称,需要使用英文字符',
|
||||
invalid: '只能输入英文字符、数字 、- 和_ 这两个符号',
|
||||
label: 'MCP 插件名称',
|
||||
placeholder: '例如:my-mcp-plugin',
|
||||
},
|
||||
type: {
|
||||
desc: '选择 MCP 插件的通信方式,网页版只支持 Streamable HTTP',
|
||||
label: 'MCP 插件类型',
|
||||
},
|
||||
url: {
|
||||
desc: '输入你的 MCP HTTP 插件的 Endpoint 地址',
|
||||
label: 'HTTP Endpoint URL',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
author: {
|
||||
desc: '插件的作者',
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
||||
|
||||
import { mcpRouter } from './mcp';
|
||||
import { searchRouter } from './search';
|
||||
|
||||
export const toolsRouter = router({
|
||||
healthcheck: publicProcedure.query(() => "i'm live!"),
|
||||
mcp: mcpRouter,
|
||||
search: searchRouter,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isDesktop, isServerMode } from '@/const/version';
|
||||
import { passwordProcedure } from '@/libs/trpc/edge';
|
||||
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
||||
import { mcpService } from '@/server/services/mcp';
|
||||
|
||||
// Define Zod schemas for MCP Client parameters
|
||||
const httpParamsSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
type: z.literal('http'),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
const stdioParamsSchema = z.object({
|
||||
args: z.array(z.string()).optional().default([]),
|
||||
command: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
type: z.literal('stdio'),
|
||||
});
|
||||
|
||||
// Union schema for MCPClientParams
|
||||
const mcpClientParamsSchema = z.union([httpParamsSchema, stdioParamsSchema]);
|
||||
|
||||
const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) => {
|
||||
if (params.type === 'stdio' && !isDesktop) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Stdio MCP type is not supported in web environment.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mcpProcedure = isServerMode ? authedProcedure : passwordProcedure;
|
||||
|
||||
export const mcpRouter = router({
|
||||
getStreamableMcpServerManifest: mcpProcedure
|
||||
.input(
|
||||
z.object({
|
||||
identifier: z.string(),
|
||||
url: z.string().url(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await mcpService.getStreamableMcpServerManifest(input.identifier, input.url);
|
||||
}),
|
||||
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||
// --- MCP Interaction ---
|
||||
// listTools now accepts MCPClientParams directly
|
||||
listTools: mcpProcedure
|
||||
.input(mcpClientParamsSchema) // Use the unified schema
|
||||
.query(async ({ input }) => {
|
||||
// Stdio check can be done here or rely on the service/client layer
|
||||
checkStdioEnvironment(input);
|
||||
|
||||
// Pass the validated MCPClientParams to the service
|
||||
return await mcpService.listTools(input);
|
||||
}),
|
||||
|
||||
// callTool now accepts MCPClientParams, toolName, and args
|
||||
callTool: mcpProcedure
|
||||
.input(
|
||||
z.object({
|
||||
params: mcpClientParamsSchema, // Use the unified schema for client params
|
||||
args: z.any(), // Arguments for the tool call
|
||||
toolName: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
// Stdio check can be done here or rely on the service/client layer
|
||||
checkStdioEnvironment(input.params);
|
||||
|
||||
// Pass the validated params, toolName, and args to the service
|
||||
const data = await mcpService.callTool(input.params, input.toolName, input.args);
|
||||
|
||||
return JSON.stringify(data);
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import debug from 'debug';
|
||||
|
||||
import { MCPClient, MCPClientParams } from '@/libs/mcp';
|
||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||
|
||||
const log = debug('lobe-mcp:service');
|
||||
|
||||
// Removed MCPConnection interface as it's no longer needed
|
||||
|
||||
class MCPService {
|
||||
// Store instances of the custom MCPClient, keyed by serialized MCPClientParams
|
||||
private clients: Map<string, MCPClient> = new Map();
|
||||
|
||||
constructor() {
|
||||
log('MCPService initialized');
|
||||
}
|
||||
|
||||
// --- MCP Interaction ---
|
||||
|
||||
// listTools now accepts MCPClientParams
|
||||
async listTools(params: MCPClientParams): Promise<LobeChatPluginApi[]> {
|
||||
const client = await this.getClient(params); // Get client using params
|
||||
log(`Listing tools using client for params: %O`, params);
|
||||
|
||||
try {
|
||||
const result = await client.listTools();
|
||||
log(`Tools listed successfully for params: %O, result count: %d`, params, result.length);
|
||||
return result.map<LobeChatPluginApi>((item) => ({
|
||||
// Assuming identifier is the unique name/id
|
||||
description: item.description,
|
||||
name: item.name,
|
||||
parameters: item.inputSchema as PluginSchema,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`Error listing tools for params %O:`, params, error);
|
||||
// Propagate a TRPCError for better handling upstream
|
||||
throw new TRPCError({
|
||||
cause: error,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Error listing tools from MCP server: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// callTool now accepts MCPClientParams, toolName, and args
|
||||
async callTool(params: MCPClientParams, toolName: string, argsStr: any): Promise<any> {
|
||||
const client = await this.getClient(params); // Get client using params
|
||||
|
||||
const args = safeParseJSON(argsStr);
|
||||
|
||||
log(`Calling tool "${toolName}" using client for params: %O with args: %O`, params, args);
|
||||
|
||||
try {
|
||||
// Delegate the call to the MCPClient instance
|
||||
const result = await client.callTool(toolName, args); // Pass args directly
|
||||
log(`Tool "${toolName}" called successfully for params: %O, result: %O`, params, result);
|
||||
const { content, isError } = result;
|
||||
if (!isError) return content;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
|
||||
// Propagate a TRPCError
|
||||
throw new TRPCError({
|
||||
cause: error,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Error calling tool "${toolName}" on MCP server: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider adding methods for managing the client lifecycle if needed,
|
||||
// e.g., explicitly closing clients on shutdown or after inactivity,
|
||||
// although for serverless, on-demand creation/retrieval might be sufficient.
|
||||
|
||||
// TODO: Implement methods like listResources, getResource, listPrompts, getPrompt if needed,
|
||||
// following the pattern of accepting MCPClientParams.
|
||||
|
||||
// --- Client Management (Replaces Connection Management) ---
|
||||
|
||||
// Private method to get or initialize a client based on parameters
|
||||
private async getClient(params: MCPClientParams): Promise<MCPClient> {
|
||||
const key = this.serializeParams(params); // Use custom serialization
|
||||
log(`Attempting to get client for key: ${key} (params: %O)`, params);
|
||||
|
||||
if (this.clients.has(key)) {
|
||||
log(`Returning cached client for key: ${key}`);
|
||||
return this.clients.get(key)!;
|
||||
}
|
||||
|
||||
log(`No cached client found for key: ${key}. Initializing new client.`);
|
||||
try {
|
||||
// Ensure stdio is only attempted in desktop/server environments within the client itself
|
||||
// or add a check here if MCPClient doesn't handle it.
|
||||
// Example check (adjust based on where environment check is best handled):
|
||||
// if (params.type === 'stdio' && typeof window !== 'undefined') {
|
||||
// throw new Error('Stdio MCP type is not supported in browser environment.');
|
||||
// }
|
||||
|
||||
const client = new MCPClient(params);
|
||||
await client.initialize(); // Initialization logic should be within MCPClient
|
||||
this.clients.set(key, client);
|
||||
log(`New client initialized and cached for key: ${key}`);
|
||||
return client;
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize MCP client for key ${key}:`, error);
|
||||
// Do not cache failed initializations
|
||||
throw new TRPCError({
|
||||
cause: error,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: `Failed to initialize MCP client: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Custom serialization function to ensure consistent keys
|
||||
private serializeParams(params: MCPClientParams): string {
|
||||
const sortedKeys = Object.keys(params).sort();
|
||||
const sortedParams: Record<string, any> = {};
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const value = (params as any)[key];
|
||||
// Sort the 'args' array if it exists
|
||||
if (key === 'args' && Array.isArray(value)) {
|
||||
sortedParams[key] = JSON.stringify(key);
|
||||
} else {
|
||||
sortedParams[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(sortedParams);
|
||||
}
|
||||
|
||||
async getStreamableMcpServerManifest(
|
||||
identifier: string,
|
||||
url: string,
|
||||
): Promise<LobeChatPluginManifest> {
|
||||
const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
|
||||
|
||||
return {
|
||||
api: tools,
|
||||
identifier,
|
||||
meta: {
|
||||
avatar: 'MCP_AVATAR',
|
||||
description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
|
||||
title: identifier,
|
||||
},
|
||||
// TODO: temporary
|
||||
type: 'mcp' as any,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const mcpService = new MCPService();
|
||||
@@ -0,0 +1,25 @@
|
||||
import { toolsClient } from '@/libs/trpc/client';
|
||||
import { getToolStoreState } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
|
||||
import { ChatToolPayload } from '@/types/message';
|
||||
|
||||
class MCPService {
|
||||
async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
|
||||
const s = getToolStoreState();
|
||||
const { identifier, arguments: args, apiName } = payload;
|
||||
const plugin = pluginSelectors.getCustomPluginById(identifier)(s);
|
||||
|
||||
if (!plugin) return;
|
||||
|
||||
return toolsClient.mcp.callTool.mutate(
|
||||
{ args, params: { ...plugin.customParams?.mcp, name: identifier } as any, toolName: apiName },
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
|
||||
async getStreamableMcpServerManifest(identifier: string, url: string) {
|
||||
return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpService = new MCPService();
|
||||
@@ -8,6 +8,7 @@ import { StateCreator } from 'zustand/vanilla';
|
||||
import { LOADING_FLAT } from '@/const/message';
|
||||
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
||||
import { chatService } from '@/services/chat';
|
||||
import { mcpService } from '@/services/mcp';
|
||||
import { messageService } from '@/services/message';
|
||||
import { ChatStore } from '@/store/chat/store';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
@@ -41,6 +42,7 @@ export interface ChatPluginAction {
|
||||
invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
|
||||
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
|
||||
invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
||||
invokeMCPTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
||||
|
||||
invokeStandaloneTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
||||
|
||||
@@ -271,7 +273,7 @@ export const chatPlugin: StateCreator<
|
||||
// trigger the plugin call
|
||||
const data = await get().internal_invokeDifferentTypePlugin(id, payload);
|
||||
|
||||
if ((payload.type === 'default' || payload.type === 'builtin') && data) {
|
||||
if (data && !['markdown', 'standalone'].includes(payload.type)) {
|
||||
shouldCreateMessage = true;
|
||||
latestToolId = id;
|
||||
}
|
||||
@@ -328,7 +330,9 @@ export const chatPlugin: StateCreator<
|
||||
|
||||
const updateAssistantMessage = async () => {
|
||||
if (!assistantMessage) return;
|
||||
await messageService.updateMessage(assistantMessage!.id, { tools: assistantMessage?.tools });
|
||||
await messageService.updateMessage(assistantMessage!.id, {
|
||||
tools: assistantMessage?.tools,
|
||||
});
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
@@ -438,11 +442,51 @@ export const chatPlugin: StateCreator<
|
||||
return await get().invokeBuiltinTool(id, payload);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
case 'mcp': {
|
||||
return await get().invokeMCPTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
default: {
|
||||
return await get().invokeDefaultTypePlugin(id, payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
invokeMCPTypePlugin: async (id, payload) => {
|
||||
const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
|
||||
get();
|
||||
let data: string = '';
|
||||
|
||||
try {
|
||||
const abortController = internal_togglePluginApiCalling(
|
||||
true,
|
||||
id,
|
||||
n('fetchPlugin/start') as string,
|
||||
);
|
||||
|
||||
const result = await mcpService.invokeMcpToolCall(payload, {
|
||||
signal: abortController?.signal,
|
||||
});
|
||||
if (!!result) data = result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
const err = error as Error;
|
||||
|
||||
// ignore the aborted request error
|
||||
if (!err.message.includes('The user aborted a request.')) {
|
||||
await messageService.updateMessageError(id, error as any);
|
||||
await refreshMessages();
|
||||
}
|
||||
}
|
||||
|
||||
internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
|
||||
// 如果报错则结束了
|
||||
if (!data) return;
|
||||
|
||||
await internal_updateMessageContent(id, data);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
internal_togglePluginApiCalling: (loading, id, action) => {
|
||||
return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
|
||||
|
||||
@@ -9,6 +9,15 @@ export interface CustomPluginParams {
|
||||
enableSettings?: boolean;
|
||||
manifestMode?: 'local' | 'url';
|
||||
manifestUrl?: string;
|
||||
/**
|
||||
* 临时方案,后续需要做一次大重构
|
||||
*/
|
||||
mcp?: {
|
||||
args?: string[];
|
||||
command?: string;
|
||||
type: 'http' | 'stdio';
|
||||
url?: string;
|
||||
};
|
||||
useProxy?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user