feat: Mcp Server support uvx (#10261)

Refs https://github.com/1Panel-dev/1Panel/issues/10118
This commit is contained in:
CityFun 2025-09-04 15:21:33 +08:00 committed by GitHub
parent ac4962cf19
commit 23876ed186
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 80 additions and 25 deletions

View file

@ -20,6 +20,7 @@ type McpServerCreate struct {
HostIP string `json:"hostIP"`
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"`
Type string `json:"type" validate:"required"`
}
type McpServerUpdate struct {

View file

@ -17,4 +17,5 @@ type McpServer struct {
HostIP string `json:"hostIP"`
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"`
Type string `json:"type"`
}

View file

@ -59,7 +59,11 @@ func (m McpServerService) Page(req request.McpServerSearch) response.McpServersR
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
}
project, _ := docker.GetComposeProject(item.Name, path.Join(global.Dir.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
project, err := docker.GetComposeProject(item.Name, path.Join(global.Dir.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
if err != nil {
global.LOG.Errorf("get mcp compose project error: %s", err.Error())
continue
}
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
@ -86,11 +90,7 @@ func (m McpServerService) Page(req request.McpServerSearch) response.McpServersR
}
func (m McpServerService) Update(req request.McpServerUpdate) error {
go func() {
if err := docker.PullImage("supercorp/supergateway:latest"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
}()
go pullImage(req.Type)
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return err
@ -115,6 +115,7 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer.HostIP = req.HostIP
mcpServer.StreamableHttpPath = req.StreamableHttpPath
mcpServer.OutputTransport = req.OutputTransport
mcpServer.Type = req.Type
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
return err
}
@ -137,11 +138,7 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
}
func (m McpServerService) Create(create request.McpServerCreate) error {
go func() {
if err := docker.PullImage("supercorp/supergateway:latest"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
}()
go pullImage(create.Type)
servers, _ := mcpServerRepo.List()
for _, server := range servers {
if server.Port == create.Port {
@ -177,6 +174,7 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
HostIP: create.HostIP,
StreamableHttpPath: create.StreamableHttpPath,
OutputTransport: create.OutputTransport,
Type: create.Type,
}
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
return err
@ -590,6 +588,11 @@ func handleCreateParams(mcpServer *model.McpServer, environments []request.Envir
}
serviceValue["volumes"] = volumeList
}
if mcpServer.Type == "npx" {
serviceValue["image"] = "supercorp/supergateway:latest"
} else {
serviceValue["image"] = "supercorp/supergateway:uvx"
}
services[mcpServer.Name] = serviceValue
composeByte, err := yaml.Marshal(composeMap)
@ -641,7 +644,7 @@ func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
case "restarting":
mcpServer.Status = constant.StatusRestarting
default:
if mcpServer.Status != constant.StatusBuilding {
if mcpServer.Status != constant.StatusStarting {
mcpServer.Status = constant.StatusStopped
}
}
@ -656,3 +659,15 @@ func GetWebsiteID() uint {
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
return uint(websiteIDUint)
}
func pullImage(imageType string) {
if imageType == "npx" {
if err := docker.PullImage("supercorp/supergateway:latest"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
} else {
if err := docker.PullImage("supercorp/supergateway:uvx"); err != nil {
global.LOG.Errorf("docker pull mcp image error: %s", err.Error())
}
}
}

View file

@ -38,6 +38,7 @@ func InitAgentDB() {
migrations.AddColumnToAlert,
migrations.UpdateWebsiteSSL,
migrations.AddQuickJump,
migrations.UpdateMcpServerAddType,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -496,3 +496,16 @@ var AddQuickJump = &gormigrate.Migration{
return nil
},
}
var UpdateMcpServerAddType = &gormigrate.Migration{
ID: "20250904-update-mcp-server",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.McpServer{}); err != nil {
return err
}
if err := tx.Model(&model.McpServer{}).Where("1=1").Update("type", "npx").Error; err != nil {
return err
}
return nil
},
}

View file

@ -139,6 +139,7 @@ export namespace AI {
url: string;
outputTransport: string;
streamableHttpPath: string;
type: string;
}
export interface McpServerSearch extends ReqPage {

View file

@ -696,7 +696,6 @@ const message = {
server: 'MCP Server',
create: 'Add MCP Server',
edit: 'Edit MCP Server',
commandHelper: 'For example: npx -y {0}',
baseUrl: 'External Access Path',
baseUrlHelper: 'For example: http://192.168.1.2:8000',
ssePath: 'SSE Path',
@ -717,6 +716,8 @@ const message = {
outputTransport: 'Output Type',
streamableHttpPath: 'Streaming Path',
streamableHttpPathHelper: 'For example: /mcp, note that it should not overlap with other Servers',
npxHelper: 'Suitable for mcp started with npx or binary',
uvxHelper: 'Suitable for mcp started with uvx',
},
},
container: {

View file

@ -684,7 +684,6 @@ const message = {
server: 'MCP サーバー',
create: 'サーバーを追加',
edit: 'サーバーを編集',
commandHelper: ': npx -y {0}',
baseUrl: '外部アクセスパス',
baseUrlHelper: ': http://192.168.1.2:8000',
ssePath: 'SSE パス',
@ -705,6 +704,8 @@ const message = {
outputTransport: '出力タイプ',
streamableHttpPath: 'ストリーミングパス',
streamableHttpPathHelper: '/mcp他のサーバーと重複しないように注意してください',
npxHelper: 'npx またはバイナリで起動する mcp に適しています',
uvxHelper: 'uvx で起動する mcp に適しています',
},
},
container: {

View file

@ -680,7 +680,6 @@ const message = {
server: 'MCP サーバー',
create: 'サーバーを追加',
edit: 'サーバーを編集',
commandHelper: ': npx -y {0}',
baseUrl: '外部アクセスパス',
baseUrlHelper: ': http://192.168.1.2:8000',
ssePath: 'SSE パス',
@ -701,6 +700,8 @@ const message = {
outputTransport: '출력 유형',
streamableHttpPath: '스트리밍 경로',
streamableHttpPathHelper: ': /mcp, 다른 서버와 중복되지 않도록 주의하세요',
npxHelper: 'npx 또는 바이너리로 시작하는 mcp에 적합',
uvxHelper: 'uvx로 시작하는 mcp에 적합',
},
},
container: {

View file

@ -697,7 +697,6 @@ const message = {
server: 'Pelayan MCP',
create: 'Tambah Pelayan',
edit: 'Edit Pelayan',
commandHelper: 'Contoh: npx -y {0}',
baseUrl: 'Laluan Akses Luar',
baseUrlHelper: 'Contoh: http://192.168.1.2:8000',
ssePath: 'Laluan SSE',
@ -718,6 +717,8 @@ const message = {
outputTransport: 'Jenis Output',
streamableHttpPath: 'Laluan Streaming',
streamableHttpPathHelper: 'Contoh: /mcp, elakkan daripada bertindan dengan pelayan lain',
npxHelper: 'Sesuai untuk mcp yang dimulakan dengan npx atau binari',
uvxHelper: 'Sesuai untuk mcp yang dimulakan dengan uvx',
},
},
container: {

View file

@ -693,7 +693,6 @@ const message = {
server: 'Servidor MCP',
create: 'Adicionar Servidor',
edit: 'Editar Servidor',
commandHelper: 'Por exemplo: npx -y {0}',
baseUrl: 'Caminho de Acesso Externo',
baseUrlHelper: 'Por exemplo: http://192.168.1.2:8000',
ssePath: 'Caminho SSE',
@ -714,6 +713,8 @@ const message = {
outputTransport: 'Tipo de Saída',
streamableHttpPath: 'Caminho de Streaming',
streamableHttpPathHelper: 'Por exemplo: /mcp, certifique-se de que não se sobreponha a outros Servidores',
npxHelper: 'Adequado para mcp iniciado com npx ou binário',
uvxHelper: 'Adequado para mcp iniciado com uvx',
},
},
container: {

View file

@ -690,7 +690,6 @@ const message = {
server: 'Сервер MCP',
create: 'Добавить сервер',
edit: 'Редактировать сервер',
commandHelper: 'Например: npx -y {0}',
baseUrl: 'Внешний путь доступа',
baseUrlHelper: 'Например: http://192.168.1.2:8000',
ssePath: 'Путь SSE',
@ -711,6 +710,8 @@ const message = {
outputTransport: 'Тип вывода',
streamableHttpPath: 'Путь потоковой передачи',
streamableHttpPathHelper: 'Например: /mcp, обратите внимание, чтобы не перекрывать другие серверы',
npxHelper: 'Подходит для mcp, запущенного с помощью npx или бинарного файла',
uvxHelper: 'Подходит для mcp, запущенного с помощью uvx',
},
},
container: {

View file

@ -706,7 +706,6 @@ const message = {
server: 'MCP Sunucusu',
create: 'MCP Sunucusu Ekle',
edit: 'MCP Sunucusunu Düzenle',
commandHelper: 'Örneğin: npx -y {0}',
baseUrl: 'Harici Erişim Yolu',
baseUrlHelper: 'Örneğin: http://192.168.1.2:8000',
ssePath: 'SSE Yolu',
@ -727,6 +726,8 @@ const message = {
outputTransport: 'Çıktı Türü',
streamableHttpPath: 'Akış Yolu',
streamableHttpPathHelper: 'Örneğin: /mcp, diğer Sunucularla çakışmaması gerektiğine dikkat edin',
npxHelper: 'npx veya ikili dosya ile başlatılan mcp için uygundur',
uvxHelper: 'uvx ile başlatılan mcp için uygundur',
},
},
container: {

View file

@ -671,7 +671,6 @@ const message = {
server: 'MCP Server',
create: '创建 MCP Server',
edit: '編輯 MCP Server',
commandHelper: '例如npx -y {0}',
baseUrl: '外部訪問路徑',
baseUrlHelper: '例如http://192.168.1.2:8000',
ssePath: 'SSE 路徑',
@ -691,6 +690,8 @@ const message = {
outputTransport: '輸出類型',
streamableHttpPath: '流式傳輸路徑',
streamableHttpPathHelper: '例如/mcp, 注意不要與其他 Server 重複',
npxHelper: '適合 npx 或者 二進制啟動的 mcp',
uvxHelper: '適合 uvx 啟動的 mcp',
},
},
container: {

View file

@ -670,7 +670,6 @@ const message = {
server: 'MCP Server',
create: '创建 MCP Server',
edit: '编辑 MCP Server',
commandHelper: '例如npx -y {0}',
baseUrl: '外部访问路径',
baseUrlHelper: '例如http://192.168.1.1:8000',
ssePath: 'SSE 路径',
@ -690,6 +689,8 @@ const message = {
outputTransport: '输出类型',
streamableHttpPath: '流式传输路径',
streamableHttpPathHelper: '例如/mcp, 注意不要与其他 Server 重复',
npxHelper: '适合 npx 或者 二进制启动的 mcp',
uvxHelper: '适合 uvx 启动的 mcp',
},
},
container: {

View file

@ -22,16 +22,27 @@
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="mcpServer.name" :disabled="mode == 'edit'" />
</el-form-item>
<el-form-item :label="$t('commons.table.type')" prop="type">
<el-select v-model="mcpServer.type">
<el-option label="npx" value="npx" />
<el-option label="uvx" value="uvx" />
</el-select>
<span class="input-help">
{{ $t('aiTools.mcp.' + mcpServer.type + 'Helper') }}
</span>
</el-form-item>
<el-form-item :label="$t('runtime.runScript')" prop="command">
<el-input
v-model="mcpServer.command"
type="textarea"
:rows="3"
:placeholder="$t('aiTools.mcp.commandPlaceHolder')"
:placeholder="
$t('ssl.commonNameHelper') +
(mcpServer.type == 'npx'
? ' npx -y @modelcontextprotocol/server-github'
: ' uvx mcp-server-fetch')
"
></el-input>
<span class="input-help">
{{ $t('aiTools.mcp.commandHelper', ['@modelcontextprotocol/server-github']) }}
</span>
</el-form-item>
<div>
<el-text>{{ $t('aiTools.mcp.environment') }}</el-text>
@ -160,6 +171,7 @@ const newMcpServer = () => {
url: '',
outputTransport: 'sse',
streamableHttpPath: '',
type: 'npx',
};
};
const em = defineEmits(['close']);
@ -175,6 +187,7 @@ const rules = ref({
value: [Rules.requiredInput],
outputTransport: [Rules.requiredSelect],
streamableHttpPath: [Rules.requiredInput],
type: [Rules.requiredSelect],
});
const hasWebsite = ref(false);
@ -201,6 +214,7 @@ const acceptParams = async (params: AI.McpServer) => {
mcpServer.value.protocol = parts[0];
mcpServer.value.url = parts[1];
mcpServer.value.outputTransport = mcpServer.value.outputTransport || 'sse';
mcpServer.value.type = mcpServer.value.type || 'npx';
} else {
mcpServer.value = newMcpServer();
if (params.port) {