feat: Mcp Server support streamableHttp (#9761)

Refs https://github.com/1Panel-dev/1Panel/issues/8482
This commit is contained in:
CityFun 2025-07-30 18:54:51 +08:00 committed by GitHub
parent 78f280c88f
commit cab42b3c7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 130 additions and 32 deletions

View file

@ -9,15 +9,17 @@ type McpServerSearch struct {
} }
type McpServerCreate struct { type McpServerCreate struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"` Command string `json:"command" validate:"required"`
Environments []Environment `json:"environments"` Environments []Environment `json:"environments"`
Volumes []Volume `json:"volumes"` Volumes []Volume `json:"volumes"`
Port int `json:"port" validate:"required"` Port int `json:"port" validate:"required"`
ContainerName string `json:"containerName"` ContainerName string `json:"containerName"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"` SsePath string `json:"ssePath"`
HostIP string `json:"hostIP"` HostIP string `json:"hostIP"`
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"`
} }
type McpServerUpdate struct { type McpServerUpdate struct {

View file

@ -2,17 +2,19 @@ package model
type McpServer struct { type McpServer struct {
BaseModel BaseModel
Name string `json:"name"` Name string `json:"name"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
Command string `json:"command"` Command string `json:"command"`
ContainerName string `json:"containerName"` ContainerName string `json:"containerName"`
Message string `json:"message"` Message string `json:"message"`
Port int `json:"port"` Port int `json:"port"`
Status string `json:"status"` Status string `json:"status"`
Env string `json:"env"` Env string `json:"env"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"` SsePath string `json:"ssePath"`
WebsiteID int `json:"websiteID"` WebsiteID int `json:"websiteID"`
Dir string `json:"dir"` Dir string `json:"dir"`
HostIP string `json:"hostIP"` HostIP string `json:"hostIP"`
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"`
} }

View file

@ -86,6 +86,11 @@ func (m McpServerService) Page(req request.McpServerSearch) response.McpServersR
} }
func (m McpServerService) Update(req request.McpServerUpdate) error { 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())
}
}()
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID)) mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
@ -108,6 +113,8 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer.BaseURL = req.BaseURL mcpServer.BaseURL = req.BaseURL
mcpServer.SsePath = req.SsePath mcpServer.SsePath = req.SsePath
mcpServer.HostIP = req.HostIP mcpServer.HostIP = req.HostIP
mcpServer.StreamableHttpPath = req.StreamableHttpPath
mcpServer.OutputTransport = req.OutputTransport
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil { if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
return err return err
} }
@ -130,6 +137,11 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
} }
func (m McpServerService) Create(create request.McpServerCreate) 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())
}
}()
servers, _ := mcpServerRepo.List() servers, _ := mcpServerRepo.List()
for _, server := range servers { for _, server := range servers {
if server.Port == create.Port { if server.Port == create.Port {
@ -154,15 +166,17 @@ func (m McpServerService) Create(create request.McpServerCreate) error {
} }
mcpDir := path.Join(global.Dir.McpDir, create.Name) mcpDir := path.Join(global.Dir.McpDir, create.Name)
mcpServer := &model.McpServer{ mcpServer := &model.McpServer{
Name: create.Name, Name: create.Name,
ContainerName: create.ContainerName, ContainerName: create.ContainerName,
Port: create.Port, Port: create.Port,
Command: create.Command, Command: create.Command,
Status: constant.StatusStarting, Status: constant.StatusStarting,
BaseURL: create.BaseURL, BaseURL: create.BaseURL,
SsePath: create.SsePath, SsePath: create.SsePath,
Dir: mcpDir, Dir: mcpDir,
HostIP: create.HostIP, HostIP: create.HostIP,
StreamableHttpPath: create.StreamableHttpPath,
OutputTransport: create.OutputTransport,
} }
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil { if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
return err return err
@ -524,6 +538,8 @@ func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env["BASE_URL"] = mcpServer.BaseURL env["BASE_URL"] = mcpServer.BaseURL
env["SSE_PATH"] = mcpServer.SsePath env["SSE_PATH"] = mcpServer.SsePath
env["HOST_IP"] = mcpServer.HostIP env["HOST_IP"] = mcpServer.HostIP
env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath
env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport
envStr, _ := gotenv.Marshal(env) envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr mcpServer.Env = envStr
return env return env

View file

@ -7,9 +7,11 @@ services:
- "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}" - "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}"
command: [ command: [
"--stdio", "${COMMAND}", "--stdio", "${COMMAND}",
"--outputTransport","${OUTPUT_TRANSPORT}",
"--port", "${PANEL_APP_PORT_HTTP}", "--port", "${PANEL_APP_PORT_HTTP}",
"--baseUrl", "${BASE_URL}", "--baseUrl", "${BASE_URL}",
"--ssePath", "${SSE_PATH}", "--ssePath", "${SSE_PATH}",
"--streamableHttpPath", "${STREAMABLE_HTTP_PATH}",
"--messagePath", "${SSE_PATH}/messages" "--messagePath", "${SSE_PATH}/messages"
] ]
networks: networks:

View file

@ -33,6 +33,7 @@ func InitAgentDB() {
migrations.InitAlertConfig, migrations.InitAlertConfig,
migrations.AddMethodToAlertLog, migrations.AddMethodToAlertLog,
migrations.AddMethodToAlertTask, migrations.AddMethodToAlertTask,
migrations.UpdateMcpServer,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View file

@ -419,3 +419,16 @@ var AddMethodToAlertTask = &gormigrate.Migration{
return nil return nil
}, },
} }
var UpdateMcpServer = &gormigrate.Migration{
ID: "20250729-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("output_transport", "sse").Error; err != nil {
return err
}
return nil
},
}

View file

@ -353,3 +353,15 @@ func logProcess(progress map[string]interface{}, task *task.Task) {
} }
_ = setLog(id, progressStr, task) _ = setLog(id, progressStr, task)
} }
func PullImage(imageName string) error {
cli, err := NewDockerClient()
if err != nil {
return err
}
defer cli.Close()
if _, err := cli.ImagePull(context.Background(), imageName, image.PullOptions{}); err != nil {
return err
}
return nil
}

View file

@ -137,6 +137,8 @@ export namespace AI {
hostIP: string; hostIP: string;
protocol: string; protocol: string;
url: string; url: string;
outputTransport: string;
streamableHttpPath: string;
} }
export interface McpServerSearch extends ReqPage { export interface McpServerSearch extends ReqPage {

View file

@ -706,6 +706,9 @@ const message = {
importMcpJsonError: 'mcpServers structure is incorrect', importMcpJsonError: 'mcpServers structure is incorrect',
bindDomainHelper: bindDomainHelper:
'After binding the website, it will modify the access address of all installed MCP Servers and close external access to the ports', 'After binding the website, it will modify the access address of all installed MCP Servers and close external access to the ports',
outputTransport: 'Output Type',
streamableHttpPath: 'Streaming Path',
streamableHttpPathHelper: 'For example: /mcp, note that it should not overlap with other Servers',
}, },
}, },
container: { container: {

View file

@ -692,6 +692,9 @@ const message = {
importMcpJsonError: 'mcpServers 構造が正しくありません', importMcpJsonError: 'mcpServers 構造が正しくありません',
bindDomainHelper: bindDomainHelper:
'ウェブサイトをバインドした後インストールされたすべての MCP サーバーのアクセスアドレスを変更しポートへの外部アクセスを閉じます', 'ウェブサイトをバインドした後インストールされたすべての MCP サーバーのアクセスアドレスを変更しポートへの外部アクセスを閉じます',
outputTransport: '出力タイプ',
streamableHttpPath: 'ストリーミングパス',
streamableHttpPathHelper: '/mcp他のサーバーと重複しないように注意してください',
}, },
}, },
container: { container: {

View file

@ -688,6 +688,9 @@ const message = {
importMcpJsonError: 'mcpServers 構造が正しくありません', importMcpJsonError: 'mcpServers 構造が正しくありません',
bindDomainHelper: bindDomainHelper:
'웹사이트를 바인딩한 , 설치된 모든 MCP 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다', '웹사이트를 바인딩한 , 설치된 모든 MCP 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다',
outputTransport: '출력 유형',
streamableHttpPath: '스트리밍 경로',
streamableHttpPathHelper: ': /mcp, 다른 서버와 중복되지 않도록 주의하세요',
}, },
}, },
container: { container: {

View file

@ -705,6 +705,9 @@ const message = {
importMcpJsonError: 'Struktur mcpServers tidak betul', importMcpJsonError: 'Struktur mcpServers tidak betul',
bindDomainHelper: bindDomainHelper:
'Setelah mengikat laman web, ia akan mengubah alamat akses semua Pelayan MCP yang dipasang dan menutup akses luaran ke pelabuhan', 'Setelah mengikat laman web, ia akan mengubah alamat akses semua Pelayan MCP yang dipasang dan menutup akses luaran ke pelabuhan',
outputTransport: 'Jenis Output',
streamableHttpPath: 'Laluan Streaming',
streamableHttpPathHelper: 'Contoh: /mcp, elakkan daripada bertindan dengan pelayan lain',
}, },
}, },
container: { container: {

View file

@ -701,6 +701,9 @@ const message = {
importMcpJsonError: 'A estrutura mcpServers está incorreta', importMcpJsonError: 'A estrutura mcpServers está incorreta',
bindDomainHelper: bindDomainHelper:
'Após vincular o site, ele modificará o endereço de acesso de todos os servidores MCP instalados e fechará o acesso externo às portas', 'Após vincular o site, ele modificará o endereço de acesso de todos os servidores MCP instalados e fechará o acesso externo às portas',
outputTransport: 'Tipo de Saída',
streamableHttpPath: 'Caminho de Streaming',
streamableHttpPathHelper: 'Por exemplo: /mcp, certifique-se de que não se sobreponha a outros Servidores',
}, },
}, },
container: { container: {

View file

@ -698,6 +698,9 @@ const message = {
importMcpJsonError: 'Структура mcpServers некорректна', importMcpJsonError: 'Структура mcpServers некорректна',
bindDomainHelper: bindDomainHelper:
'После привязки веб-сайта он изменит адрес доступа для всех установленных серверов MCP и закроет внешний доступ к портам', 'После привязки веб-сайта он изменит адрес доступа для всех установленных серверов MCP и закроет внешний доступ к портам',
outputTransport: 'Тип вывода',
streamableHttpPath: 'Путь потоковой передачи',
streamableHttpPathHelper: 'Например: /mcp, обратите внимание, чтобы не перекрывать другие серверы',
}, },
}, },
container: { container: {

View file

@ -716,6 +716,9 @@ const message = {
importMcpJsonError: 'mcpServers yapısı yanlış', importMcpJsonError: 'mcpServers yapısı yanlış',
bindDomainHelper: bindDomainHelper:
'Web sitesini bağladıktan sonra, kurulu tüm MCP Sunucularının erişim adresini değiştirecek ve portlara harici erişimi kapatacaktır', 'Web sitesini bağladıktan sonra, kurulu tüm MCP Sunucularının erişim adresini değiştirecek ve portlara harici erişimi kapatacaktır',
outputTransport: 'Çıktı Türü',
streamableHttpPath: 'Akış Yolu',
streamableHttpPathHelper: 'Örneğin: /mcp, diğer Sunucularla çakışmaması gerektiğine dikkat edin',
}, },
}, },
container: { container: {

View file

@ -680,6 +680,9 @@ const message = {
importMcpJson: '導入 MCP Server配置', importMcpJson: '導入 MCP Server配置',
importMcpJsonError: 'mcpServers 結構不正確', importMcpJsonError: 'mcpServers 結構不正確',
bindDomainHelper: '綁定網站之後會修改所有已安裝 MCP Server 的訪問地址並關閉端口的外部訪問', bindDomainHelper: '綁定網站之後會修改所有已安裝 MCP Server 的訪問地址並關閉端口的外部訪問',
outputTransport: '輸出類型',
streamableHttpPath: '流式傳輸路徑',
streamableHttpPathHelper: '例如/mcp, 注意不要與其他 Server 重複',
}, },
}, },
container: { container: {

View file

@ -679,6 +679,9 @@ const message = {
importMcpJson: '导入 MCP Server 配置', importMcpJson: '导入 MCP Server 配置',
importMcpJsonError: 'mcpServers 结构不正确', importMcpJsonError: 'mcpServers 结构不正确',
bindDomainHelper: '绑定网站之后会修改所有已安装 MCP Server 的访问地址并关闭端口的外部访问', bindDomainHelper: '绑定网站之后会修改所有已安装 MCP Server 的访问地址并关闭端口的外部访问',
outputTransport: '输出类型',
streamableHttpPath: '流式传输路径',
streamableHttpPathHelper: '例如/mcp, 注意不要与其他 Server 重复',
}, },
}, },
container: { container: {

View file

@ -91,12 +91,28 @@
<el-form-item :label="$t('app.containerName')" prop="containerName"> <el-form-item :label="$t('app.containerName')" prop="containerName">
<el-input v-model.trim="mcpServer.containerName"></el-input> <el-input v-model.trim="mcpServer.containerName"></el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('aiTools.mcp.ssePath')" prop="ssePath"> <el-form-item :label="$t('aiTools.mcp.outputTransport')" prop="outputTransport">
<el-select v-model="mcpServer.outputTransport">
<el-option label="sse" value="sse" />
<el-option label="streamableHttp" value="streamableHttp" />
</el-select>
</el-form-item>
<el-form-item :label="$t('aiTools.mcp.ssePath')" prop="ssePath" v-if="mcpServer.outputTransport === 'sse'">
<el-input v-model.trim="mcpServer.ssePath"></el-input> <el-input v-model.trim="mcpServer.ssePath"></el-input>
<span class="input-help"> <span class="input-help">
{{ $t('aiTools.mcp.ssePathHelper') }} {{ $t('aiTools.mcp.ssePathHelper') }}
</span> </span>
</el-form-item> </el-form-item>
<el-form-item
:label="$t('aiTools.mcp.streamableHttpPath')"
prop="streamableHttpPath"
v-if="mcpServer.outputTransport === 'streamableHttp'"
>
<el-input v-model.trim="mcpServer.streamableHttpPath"></el-input>
<span class="input-help">
{{ $t('aiTools.mcp.streamableHttpPathHelper') }}
</span>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span> <span>
@ -142,6 +158,8 @@ const newMcpServer = () => {
hostIP: '127.0.0.1', hostIP: '127.0.0.1',
protocol: 'http://', protocol: 'http://',
url: '', url: '',
outputTransport: 'sse',
streamableHttpPath: '',
}; };
}; };
const em = defineEmits(['close']); const em = defineEmits(['close']);
@ -155,6 +173,8 @@ const rules = ref({
ssePath: [Rules.requiredInput], ssePath: [Rules.requiredInput],
key: [Rules.requiredInput], key: [Rules.requiredInput],
value: [Rules.requiredInput], value: [Rules.requiredInput],
outputTransport: [Rules.requiredSelect],
streamableHttpPath: [Rules.requiredInput],
}); });
const hasWebsite = ref(false); const hasWebsite = ref(false);
@ -180,6 +200,7 @@ const acceptParams = async (params: AI.McpServer) => {
const parts = mcpServer.value.baseUrl.split(/(https?:\/\/)/).filter(Boolean); const parts = mcpServer.value.baseUrl.split(/(https?:\/\/)/).filter(Boolean);
mcpServer.value.protocol = parts[0]; mcpServer.value.protocol = parts[0];
mcpServer.value.url = parts[1]; mcpServer.value.url = parts[1];
mcpServer.value.outputTransport = mcpServer.value.outputTransport || 'sse';
} else { } else {
mcpServer.value = newMcpServer(); mcpServer.value = newMcpServer();
if (params.port) { if (params.port) {