mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-09 23:17:21 +08:00
feat: Mcp Server support streamableHttp (#9761)
Refs https://github.com/1Panel-dev/1Panel/issues/8482
This commit is contained in:
parent
78f280c88f
commit
cab42b3c7d
18 changed files with 130 additions and 32 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -692,6 +692,9 @@ const message = {
|
||||||
importMcpJsonError: 'mcpServers 構造が正しくありません',
|
importMcpJsonError: 'mcpServers 構造が正しくありません',
|
||||||
bindDomainHelper:
|
bindDomainHelper:
|
||||||
'ウェブサイトをバインドした後、インストールされたすべての MCP サーバーのアクセスアドレスを変更し、ポートへの外部アクセスを閉じます',
|
'ウェブサイトをバインドした後、インストールされたすべての MCP サーバーのアクセスアドレスを変更し、ポートへの外部アクセスを閉じます',
|
||||||
|
outputTransport: '出力タイプ',
|
||||||
|
streamableHttpPath: 'ストリーミングパス',
|
||||||
|
streamableHttpPathHelper: '例:/mcp、他のサーバーと重複しないように注意してください',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
|
|
|
@ -688,6 +688,9 @@ const message = {
|
||||||
importMcpJsonError: 'mcpServers 構造が正しくありません',
|
importMcpJsonError: 'mcpServers 構造が正しくありません',
|
||||||
bindDomainHelper:
|
bindDomainHelper:
|
||||||
'웹사이트를 바인딩한 후, 설치된 모든 MCP 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다',
|
'웹사이트를 바인딩한 후, 설치된 모든 MCP 서버의 접근 주소를 수정하고 포트의 외부 접근을 닫습니다',
|
||||||
|
outputTransport: '출력 유형',
|
||||||
|
streamableHttpPath: '스트리밍 경로',
|
||||||
|
streamableHttpPathHelper: '예: /mcp, 다른 서버와 중복되지 않도록 주의하세요',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -698,6 +698,9 @@ const message = {
|
||||||
importMcpJsonError: 'Структура mcpServers некорректна',
|
importMcpJsonError: 'Структура mcpServers некорректна',
|
||||||
bindDomainHelper:
|
bindDomainHelper:
|
||||||
'После привязки веб-сайта он изменит адрес доступа для всех установленных серверов MCP и закроет внешний доступ к портам',
|
'После привязки веб-сайта он изменит адрес доступа для всех установленных серверов MCP и закроет внешний доступ к портам',
|
||||||
|
outputTransport: 'Тип вывода',
|
||||||
|
streamableHttpPath: 'Путь потоковой передачи',
|
||||||
|
streamableHttpPathHelper: 'Например: /mcp, обратите внимание, чтобы не перекрывать другие серверы',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue