mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat: successfully install
This commit is contained in:
@@ -31,6 +31,8 @@ class MCPRouterGroup(group.RouterGroup):
|
||||
@self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(server_name: str) -> str:
|
||||
"""获取、更新或删除MCP服务器配置"""
|
||||
from urllib.parse import unquote
|
||||
server_name = unquote(server_name)
|
||||
|
||||
server_data = await self.ap.mcp_service.get_mcp_server_by_name(server_name)
|
||||
if server_data is None:
|
||||
@@ -57,6 +59,8 @@ class MCPRouterGroup(group.RouterGroup):
|
||||
@self.route('/servers/<server_name>/test', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _(server_name: str) -> str:
|
||||
"""测试MCP服务器连接"""
|
||||
from urllib.parse import unquote
|
||||
server_name = unquote(server_name)
|
||||
server_data = await quart.request.json
|
||||
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
||||
return self.success(data={'task_id': task_id})
|
||||
|
||||
@@ -202,12 +202,150 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _install_mcp_from_marketplace(
|
||||
self,
|
||||
mcp_data: dict[str, Any],
|
||||
task_context: taskmgr.TaskContext | None = None,
|
||||
):
|
||||
"""Install an MCP server from marketplace data."""
|
||||
from ..entity.persistence import mcp as persistence_mcp
|
||||
import uuid
|
||||
|
||||
config = mcp_data.get('config', {})
|
||||
url = config.get('url', '')
|
||||
# Use __ instead of / to avoid URL routing issues with slashes
|
||||
name = f"{mcp_data.get('author', '')}__{mcp_data.get('name', '')}"
|
||||
|
||||
# Determine mode from URL
|
||||
if 'sse' in url.lower():
|
||||
mode = 'sse'
|
||||
elif url.startswith('http'):
|
||||
mode = 'http'
|
||||
else:
|
||||
mode = 'stdio'
|
||||
|
||||
# Build extra_args from config
|
||||
extra_args = {
|
||||
'url': url,
|
||||
'timeout': config.get('timeout', 30),
|
||||
'sse_read_timeout': config.get('sse_read_timeout', 300),
|
||||
}
|
||||
|
||||
# Check if MCP server already exists
|
||||
existing = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.name == name)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
self.ap.logger.info(f'MCP server {name} already exists, skipping installation')
|
||||
return
|
||||
|
||||
# Create MCP server record
|
||||
server_uuid = str(uuid.uuid4())
|
||||
server_data = {
|
||||
'uuid': server_uuid,
|
||||
'name': name,
|
||||
'enable': True,
|
||||
'mode': mode,
|
||||
'extra_args': extra_args,
|
||||
}
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.insert(persistence_mcp.MCPServer).values(server_data)
|
||||
)
|
||||
|
||||
# Start the MCP server
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_mcp.MCPServer).where(persistence_mcp.MCPServer.uuid == server_uuid)
|
||||
)
|
||||
server_entity = result.first()
|
||||
if server_entity:
|
||||
server_config = self.ap.persistence_mgr.serialize_model(persistence_mcp.MCPServer, server_entity)
|
||||
if self.ap.tool_mgr.mcp_tool_loader:
|
||||
mcp_task = asyncio.create_task(
|
||||
self.ap.tool_mgr.mcp_tool_loader.host_mcp_server(server_config)
|
||||
)
|
||||
self.ap.tool_mgr.mcp_tool_loader._hosted_mcp_tasks.append(mcp_task)
|
||||
|
||||
self.ap.logger.info(f'Installed MCP server {name} from marketplace')
|
||||
|
||||
async def _install_skill_from_zip(
|
||||
self,
|
||||
file_bytes: bytes,
|
||||
filename: str,
|
||||
task_context: taskmgr.TaskContext | None = None,
|
||||
):
|
||||
"""Install a skill from marketplace ZIP data."""
|
||||
from ..api.http.service.skill import SkillService
|
||||
|
||||
skill_service = SkillService(self.ap)
|
||||
|
||||
self.ap.logger.info(f'Installing skill from marketplace ZIP ({len(file_bytes)} bytes)')
|
||||
|
||||
# Install from ZIP using skill service
|
||||
result = await skill_service.install_from_zip_upload(
|
||||
file_bytes=file_bytes,
|
||||
filename=filename + '.zip',
|
||||
)
|
||||
self.ap.logger.info(f'Skill installed successfully: {result}')
|
||||
|
||||
async def install_plugin(
|
||||
self,
|
||||
install_source: PluginInstallSource,
|
||||
install_info: dict[str, Any],
|
||||
task_context: taskmgr.TaskContext | None = None,
|
||||
):
|
||||
if install_source == PluginInstallSource.MARKETPLACE:
|
||||
# Handle marketplace plugin/mcp/skill installation
|
||||
plugin_author = install_info.get('plugin_author', '')
|
||||
plugin_name = install_info.get('plugin_name', '')
|
||||
space_url = self.ap.instance_config.data.get('space', {}).get('url', 'https://space.langbot.app').rstrip('/')
|
||||
|
||||
# Try MCP endpoint first
|
||||
async with httpx.AsyncClient(trust_env=True, timeout=15) as client:
|
||||
mcp_resp = await client.get(f'{space_url}/api/v1/marketplace/mcps/{plugin_author}/{plugin_name}')
|
||||
if mcp_resp.status_code == 200:
|
||||
mcp_data = mcp_resp.json().get('data', {}).get('mcp', {})
|
||||
if mcp_data.get('config'):
|
||||
# It's an MCP - create server locally
|
||||
self.ap.logger.info(f'Installing MCP from marketplace: {plugin_author}/{plugin_name}')
|
||||
if task_context:
|
||||
task_context.set_current_action('installing mcp server')
|
||||
await self._install_mcp_from_marketplace(mcp_data, task_context)
|
||||
return
|
||||
else:
|
||||
raise Exception(f'MCP {plugin_author}/{plugin_name} has no config')
|
||||
elif mcp_resp.status_code == 404:
|
||||
# Try skill endpoint - download ZIP and install
|
||||
self.ap.logger.info(f'Installing skill from marketplace: {plugin_author}/{plugin_name}')
|
||||
if task_context:
|
||||
task_context.set_current_action('installing skill from marketplace')
|
||||
|
||||
# Get skill detail to find version
|
||||
skill_resp = await client.get(f'{space_url}/api/v1/marketplace/skills/{plugin_author}/{plugin_name}')
|
||||
if skill_resp.status_code == 200:
|
||||
# Download the skill ZIP (no version needed - uses latest)
|
||||
if task_context:
|
||||
task_context.set_current_action('downloading skill package')
|
||||
|
||||
download_resp = await client.get(
|
||||
f'{space_url}/api/v1/marketplace/skills/download/{plugin_author}/{plugin_name}'
|
||||
)
|
||||
if download_resp.status_code != 200:
|
||||
raise Exception(f'Failed to download skill {plugin_author}/{plugin_name}: {download_resp.status_code}')
|
||||
|
||||
file_bytes = download_resp.content
|
||||
file_size = len(file_bytes)
|
||||
self.ap.logger.info(f'Downloaded skill ZIP ({file_size} bytes)')
|
||||
|
||||
# Install skill from ZIP using skill service
|
||||
await self._install_skill_from_zip(file_bytes, f'{plugin_author}-{plugin_name}', task_context)
|
||||
return
|
||||
else:
|
||||
raise Exception(f'Skill {plugin_author}/{plugin_name} not found in marketplace')
|
||||
else:
|
||||
mcp_resp.raise_for_status()
|
||||
raise Exception(f'Failed to get MCP {plugin_author}/{plugin_name}')
|
||||
|
||||
if install_source == PluginInstallSource.LOCAL:
|
||||
# transfer file before install
|
||||
file_bytes = install_info['plugin_file']
|
||||
|
||||
@@ -233,8 +233,8 @@ export function SidebarDataProvider({
|
||||
const resp = await httpClient.getMCPServers();
|
||||
setMCPServers(
|
||||
resp.servers.map((server) => ({
|
||||
id: server.name,
|
||||
name: server.name,
|
||||
id: server.name, // Keep __ for API calls
|
||||
name: server.name.replace(/__/g, '/'), // Display with / for readability
|
||||
enabled: server.enable,
|
||||
runtimeStatus: server.runtime_info?.status,
|
||||
})),
|
||||
|
||||
@@ -39,7 +39,9 @@ export default function MCPDetailContent({ id }: { id: string }) {
|
||||
setDetailEntityName(t('mcp.createServer'));
|
||||
} else {
|
||||
const server = mcpServers.find((s) => s.id === id);
|
||||
setDetailEntityName(server?.name ?? id);
|
||||
// Convert __ back to / for display (since / is used as separator in stored names)
|
||||
const displayName = (server?.name ?? id).replace(/__/g, '/');
|
||||
setDetailEntityName(displayName);
|
||||
}
|
||||
return () => setDetailEntityName(null);
|
||||
}, [id, isCreateMode, mcpServers, setDetailEntityName, t]);
|
||||
|
||||
@@ -309,7 +309,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
const server = resp.server ?? resp;
|
||||
|
||||
const formValues: FormValues = {
|
||||
name: server.name,
|
||||
name: server.name.replace(/__/g, '/'), // Convert __ back to / for display
|
||||
mode: server.mode,
|
||||
url: '',
|
||||
command: '',
|
||||
|
||||
@@ -33,7 +33,7 @@ import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComp
|
||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { PluginV4, PluginV4Status } from '@/app/infra/entities/plugin';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { toast } from 'sonner';
|
||||
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
|
||||
@@ -489,13 +489,44 @@ function MarketPageContent({
|
||||
|
||||
// 处理安装插件
|
||||
const handleInstallPlugin = useCallback(
|
||||
async (author: string, pluginName: string) => {
|
||||
async (cardVO: PluginMarketCardVO) => {
|
||||
try {
|
||||
// Fetch full plugin details to get PluginV4 object
|
||||
if (cardVO.type === 'mcp' || cardVO.type === 'skill') {
|
||||
// For MCP and Skill, directly pass the data - backend will fetch from Space
|
||||
const pluginV4: PluginV4 = {
|
||||
id: 0,
|
||||
plugin_id: `${cardVO.author}/${cardVO.pluginName}`,
|
||||
mcp_id: cardVO.type === 'mcp' ? `${cardVO.author}/${cardVO.pluginName}` : undefined,
|
||||
skill_id: cardVO.type === 'skill' ? `${cardVO.author}/${cardVO.pluginName}` : undefined,
|
||||
author: cardVO.author,
|
||||
name: cardVO.pluginName,
|
||||
label: { en_US: cardVO.label, zh_Hans: cardVO.label },
|
||||
description: { en_US: cardVO.description, zh_Hans: cardVO.description },
|
||||
icon: cardVO.iconURL,
|
||||
repository: cardVO.githubURL,
|
||||
tags: cardVO.tags || [],
|
||||
install_count: cardVO.installCount,
|
||||
latest_version: cardVO.version,
|
||||
components: cardVO.components || {},
|
||||
status: PluginV4Status.Live,
|
||||
type: cardVO.type,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
installPlugin(pluginV4);
|
||||
return;
|
||||
}
|
||||
|
||||
// For plugin type, fetch full details via API
|
||||
const response = await getCloudServiceClientSync().getPluginDetail(
|
||||
author,
|
||||
pluginName,
|
||||
cardVO.author,
|
||||
cardVO.pluginName,
|
||||
);
|
||||
if (!response?.plugin) {
|
||||
console.error('Failed to install plugin: plugin not found', { author: cardVO.author, pluginName: cardVO.pluginName });
|
||||
toast.error(t('market.installFailed'));
|
||||
return;
|
||||
}
|
||||
const pluginV4: PluginV4 = response.plugin;
|
||||
|
||||
// Call the install function passed from parent
|
||||
@@ -505,7 +536,7 @@ function MarketPageContent({
|
||||
toast.error(t('market.installFailed'));
|
||||
}
|
||||
},
|
||||
[plugins, installPlugin, t],
|
||||
[installPlugin, t],
|
||||
);
|
||||
|
||||
// 清理定时器
|
||||
|
||||
@@ -50,7 +50,7 @@ function RecommendationListRow({
|
||||
}: {
|
||||
list: RecommendationList;
|
||||
tagNames: Record<string, string>;
|
||||
onInstall: (author: string, pluginName: string) => void;
|
||||
onInstall: (cardVO: PluginMarketCardVO) => void;
|
||||
isLast: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -162,7 +162,7 @@ export function RecommendationLists({
|
||||
}: {
|
||||
lists: RecommendationList[];
|
||||
tagNames: Record<string, string>;
|
||||
onInstall: (author: string, pluginName: string) => void;
|
||||
onInstall: (cardVO: PluginMarketCardVO) => void;
|
||||
}) {
|
||||
if (!lists || lists.length === 0) return null;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function PluginMarketCardComponent({
|
||||
tagNames = {},
|
||||
}: {
|
||||
cardVO: PluginMarketCardVO;
|
||||
onInstall?: (author: string, pluginName: string) => void;
|
||||
onInstall?: (cardVO: PluginMarketCardVO) => void;
|
||||
tagNames?: Record<string, string>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -244,7 +244,7 @@ export default function PluginMarketCardComponent({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onInstall) {
|
||||
onInstall(cardVO.author, cardVO.pluginName);
|
||||
onInstall(cardVO);
|
||||
}
|
||||
}}
|
||||
className={`bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg shadow-sm flex items-center gap-2 transition-all duration-200 ${
|
||||
|
||||
@@ -804,7 +804,7 @@ export class BackendClient extends BaseHttpClient {
|
||||
}
|
||||
|
||||
public getMCPServer(serverName: string): Promise<ApiRespMCPServer> {
|
||||
return this.get(`/api/v1/mcp/servers/${serverName}`);
|
||||
return this.get(`/api/v1/mcp/servers/${encodeURIComponent(serverName)}`);
|
||||
}
|
||||
|
||||
public createMCPServer(server: MCPServer): Promise<AsyncTaskCreatedResp> {
|
||||
@@ -815,18 +815,18 @@ export class BackendClient extends BaseHttpClient {
|
||||
serverName: string,
|
||||
server: Partial<MCPServer>,
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.put(`/api/v1/mcp/servers/${serverName}`, server);
|
||||
return this.put(`/api/v1/mcp/servers/${encodeURIComponent(serverName)}`, server);
|
||||
}
|
||||
|
||||
public deleteMCPServer(serverName: string): Promise<AsyncTaskCreatedResp> {
|
||||
return this.delete(`/api/v1/mcp/servers/${serverName}`);
|
||||
return this.delete(`/api/v1/mcp/servers/${encodeURIComponent(serverName)}`);
|
||||
}
|
||||
|
||||
public toggleMCPServer(
|
||||
serverName: string,
|
||||
target_enabled: boolean,
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.put(`/api/v1/mcp/servers/${serverName}`, {
|
||||
return this.put(`/api/v1/mcp/servers/${encodeURIComponent(serverName)}`, {
|
||||
enable: target_enabled,
|
||||
});
|
||||
}
|
||||
@@ -835,7 +835,7 @@ export class BackendClient extends BaseHttpClient {
|
||||
serverName: string,
|
||||
serverData: object,
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.post(`/api/v1/mcp/servers/${serverName}/test`, serverData);
|
||||
return this.post(`/api/v1/mcp/servers/${encodeURIComponent(serverName)}/test`, serverData);
|
||||
}
|
||||
|
||||
public installMCPServerFromGithub(
|
||||
|
||||
Reference in New Issue
Block a user