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:
Arvin Xu
2025-04-23 00:26:33 +08:00
committed by GitHub
parent c1844933ac
commit 35129bb7c6
43 changed files with 1278 additions and 126 deletions
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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",
+33 -2
View File
@@ -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",
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": "플러그인 작성자",
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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",
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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": {
+33 -2
View File
@@ -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",
+33 -2
View File
@@ -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": "插件的作者",
+33 -2
View File
@@ -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": {
+4 -1
View File
@@ -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 />;
+2 -1
View File
@@ -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>
);
+43 -34
View File
@@ -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>
+3 -1
View File
@@ -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}>
+8 -2
View File
@@ -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
@@ -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) {
+2
View File
@@ -0,0 +1,2 @@
export * from './client';
export * from './types';
+27
View File
@@ -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;
+34 -3
View File
@@ -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: '插件的作者',
+2
View File
@@ -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,
});
+79
View File
@@ -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);
}),
});
+157
View File
@@ -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();
+25
View File
@@ -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();
+46 -2
View File
@@ -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
View File
@@ -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;
}