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 {
Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"`
Environments []Environment `json:"environments"`
Volumes []Volume `json:"volumes"`
Port int `json:"port" validate:"required"`
ContainerName string `json:"containerName"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
HostIP string `json:"hostIP"`
Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"`
Environments []Environment `json:"environments"`
Volumes []Volume `json:"volumes"`
Port int `json:"port" validate:"required"`
ContainerName string `json:"containerName"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
HostIP string `json:"hostIP"`
StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"`
}
type McpServerUpdate struct {

View file

@ -2,17 +2,19 @@ package model
type McpServer struct {
BaseModel
Name string `json:"name"`
DockerCompose string `json:"dockerCompose"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Message string `json:"message"`
Port int `json:"port"`
Status string `json:"status"`
Env string `json:"env"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
WebsiteID int `json:"websiteID"`
Dir string `json:"dir"`
HostIP string `json:"hostIP"`
Name string `json:"name"`
DockerCompose string `json:"dockerCompose"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Message string `json:"message"`
Port int `json:"port"`
Status string `json:"status"`
Env string `json:"env"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
WebsiteID int `json:"websiteID"`
Dir string `json:"dir"`
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 {
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))
if err != nil {
return err
@ -108,6 +113,8 @@ func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer.BaseURL = req.BaseURL
mcpServer.SsePath = req.SsePath
mcpServer.HostIP = req.HostIP
mcpServer.StreamableHttpPath = req.StreamableHttpPath
mcpServer.OutputTransport = req.OutputTransport
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
return err
}
@ -130,6 +137,11 @@ 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())
}
}()
servers, _ := mcpServerRepo.List()
for _, server := range servers {
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)
mcpServer := &model.McpServer{
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Command: create.Command,
Status: constant.StatusStarting,
BaseURL: create.BaseURL,
SsePath: create.SsePath,
Dir: mcpDir,
HostIP: create.HostIP,
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Command: create.Command,
Status: constant.StatusStarting,
BaseURL: create.BaseURL,
SsePath: create.SsePath,
Dir: mcpDir,
HostIP: create.HostIP,
StreamableHttpPath: create.StreamableHttpPath,
OutputTransport: create.OutputTransport,
}
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
return err
@ -524,6 +538,8 @@ func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env["BASE_URL"] = mcpServer.BaseURL
env["SSE_PATH"] = mcpServer.SsePath
env["HOST_IP"] = mcpServer.HostIP
env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath
env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport
envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr
return env

View file

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

View file

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

View file

@ -419,3 +419,16 @@ var AddMethodToAlertTask = &gormigrate.Migration{
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)
}
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;
protocol: string;
url: string;
outputTransport: string;
streamableHttpPath: string;
}
export interface McpServerSearch extends ReqPage {

View file

@ -706,6 +706,9 @@ const message = {
importMcpJsonError: 'mcpServers structure is incorrect',
bindDomainHelper:
'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: {

View file

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

View file

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

View file

@ -705,6 +705,9 @@ const message = {
importMcpJsonError: 'Struktur mcpServers tidak betul',
bindDomainHelper:
'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: {

View file

@ -701,6 +701,9 @@ const message = {
importMcpJsonError: 'A estrutura mcpServers está incorreta',
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',
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: {

View file

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

View file

@ -716,6 +716,9 @@ const message = {
importMcpJsonError: 'mcpServers yapısı yanlış',
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',
outputTransport: 'Çıktı Türü',
streamableHttpPath: 'Akış Yolu',
streamableHttpPathHelper: 'Örneğin: /mcp, diğer Sunucularla çakışmaması gerektiğine dikkat edin',
},
},
container: {

View file

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

View file

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

View file

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