feat(mcp): add stdio env support and env editor component
This commit is contained in:
parent
2666b27fcb
commit
c1653a59c0
@ -658,6 +658,7 @@ class MigrationRunner:
|
|||||||
url=sqlite_server.url,
|
url=sqlite_server.url,
|
||||||
command=sqlite_server.command,
|
command=sqlite_server.command,
|
||||||
args=sqlite_server.args,
|
args=sqlite_server.args,
|
||||||
|
env=getattr(sqlite_server, "env", None),
|
||||||
headers=sqlite_server.headers,
|
headers=sqlite_server.headers,
|
||||||
timeout=sqlite_server.timeout,
|
timeout=sqlite_server.timeout,
|
||||||
sse_read_timeout=sqlite_server.sse_read_timeout,
|
sse_read_timeout=sqlite_server.sse_read_timeout,
|
||||||
|
|||||||
@ -532,6 +532,7 @@ async def migrate_mcp_servers(sqlite_reader: SQLiteReader, dry_run: bool, execut
|
|||||||
url=sqlite_server.url,
|
url=sqlite_server.url,
|
||||||
command=sqlite_server.command,
|
command=sqlite_server.command,
|
||||||
args=sqlite_server.args,
|
args=sqlite_server.args,
|
||||||
|
env=getattr(sqlite_server, "env", None),
|
||||||
headers=sqlite_server.headers,
|
headers=sqlite_server.headers,
|
||||||
timeout=sqlite_server.timeout,
|
timeout=sqlite_server.timeout,
|
||||||
sse_read_timeout=sqlite_server.sse_read_timeout,
|
sse_read_timeout=sqlite_server.sse_read_timeout,
|
||||||
|
|||||||
@ -33,6 +33,7 @@ class CreateMcpServerRequest(BaseModel):
|
|||||||
url: str | None = Field(None, description="服务器 URL(sse/streamable_http)")
|
url: str | None = Field(None, description="服务器 URL(sse/streamable_http)")
|
||||||
command: str | None = Field(None, description="命令(stdio)")
|
command: str | None = Field(None, description="命令(stdio)")
|
||||||
args: list | None = Field(None, description="命令参数数组(stdio)")
|
args: list | None = Field(None, description="命令参数数组(stdio)")
|
||||||
|
env: dict | None = Field(None, description="环境变量(stdio)")
|
||||||
description: str | None = Field(None, description="描述")
|
description: str | None = Field(None, description="描述")
|
||||||
headers: dict | None = Field(None, description="HTTP 请求头")
|
headers: dict | None = Field(None, description="HTTP 请求头")
|
||||||
timeout: int | None = Field(None, description="HTTP 超时时间(秒)")
|
timeout: int | None = Field(None, description="HTTP 超时时间(秒)")
|
||||||
@ -46,6 +47,7 @@ class UpdateMcpServerRequest(BaseModel):
|
|||||||
url: str | None = Field(None, description="服务器 URL")
|
url: str | None = Field(None, description="服务器 URL")
|
||||||
command: str | None = Field(None, description="命令(stdio)")
|
command: str | None = Field(None, description="命令(stdio)")
|
||||||
args: list | None = Field(None, description="命令参数数组(stdio)")
|
args: list | None = Field(None, description="命令参数数组(stdio)")
|
||||||
|
env: dict | None = Field(None, description="环境变量(stdio)")
|
||||||
description: str | None = Field(None, description="描述")
|
description: str | None = Field(None, description="描述")
|
||||||
headers: dict | None = Field(None, description="HTTP 请求头")
|
headers: dict | None = Field(None, description="HTTP 请求头")
|
||||||
timeout: int | None = Field(None, description="HTTP 超时时间(秒)")
|
timeout: int | None = Field(None, description="HTTP 超时时间(秒)")
|
||||||
@ -112,6 +114,7 @@ async def create_mcp_server_route(
|
|||||||
url=request.url,
|
url=request.url,
|
||||||
command=request.command,
|
command=request.command,
|
||||||
args=request.args,
|
args=request.args,
|
||||||
|
env=request.env,
|
||||||
description=request.description,
|
description=request.description,
|
||||||
headers=request.headers,
|
headers=request.headers,
|
||||||
timeout=request.timeout,
|
timeout=request.timeout,
|
||||||
@ -159,6 +162,11 @@ async def update_mcp_server_route(
|
|||||||
raise HTTPException(status_code=400, detail=f"传输类型必须是 {', '.join(valid_transports)} 之一")
|
raise HTTPException(status_code=400, detail=f"传输类型必须是 {', '.join(valid_transports)} 之一")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
fields_set = getattr(request, "model_fields_set", getattr(request, "__fields_set__", set()))
|
||||||
|
update_kwargs = {}
|
||||||
|
if "env" in fields_set:
|
||||||
|
update_kwargs["env"] = request.env
|
||||||
|
|
||||||
server = await update_mcp_server(
|
server = await update_mcp_server(
|
||||||
db,
|
db,
|
||||||
name=name,
|
name=name,
|
||||||
@ -173,6 +181,7 @@ async def update_mcp_server_route(
|
|||||||
tags=request.tags,
|
tags=request.tags,
|
||||||
icon=request.icon,
|
icon=request.icon,
|
||||||
updated_by=current_user.username,
|
updated_by=current_user.username,
|
||||||
|
**update_kwargs,
|
||||||
)
|
)
|
||||||
return {"success": True, "data": server.to_dict()}
|
return {"success": True, "data": server.to_dict()}
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
|
|||||||
@ -35,6 +35,7 @@ _mcp_tools_stats: dict[str, dict[str, int]] = {}
|
|||||||
|
|
||||||
# MCP Server configurations (Runtime cache, loaded from DB)
|
# MCP Server configurations (Runtime cache, loaded from DB)
|
||||||
MCP_SERVERS: dict[str, dict[str, Any]] = {}
|
MCP_SERVERS: dict[str, dict[str, Any]] = {}
|
||||||
|
_UNSET = object()
|
||||||
|
|
||||||
# Default MCP Server configurations (Imported to DB on first run)
|
# Default MCP Server configurations (Imported to DB on first run)
|
||||||
_DEFAULT_MCP_SERVERS = {
|
_DEFAULT_MCP_SERVERS = {
|
||||||
@ -130,6 +131,7 @@ async def init_mcp_servers() -> None:
|
|||||||
url=config.get("url"),
|
url=config.get("url"),
|
||||||
command=config.get("command"),
|
command=config.get("command"),
|
||||||
args=config.get("args"),
|
args=config.get("args"),
|
||||||
|
env=config.get("env"),
|
||||||
headers=config.get("headers"),
|
headers=config.get("headers"),
|
||||||
timeout=config.get("timeout"),
|
timeout=config.get("timeout"),
|
||||||
sse_read_timeout=config.get("sse_read_timeout"),
|
sse_read_timeout=config.get("sse_read_timeout"),
|
||||||
@ -155,6 +157,7 @@ async def init_mcp_servers() -> None:
|
|||||||
url=config.get("url"),
|
url=config.get("url"),
|
||||||
command=config.get("command"),
|
command=config.get("command"),
|
||||||
args=config.get("args"),
|
args=config.get("args"),
|
||||||
|
env=config.get("env"),
|
||||||
headers=config.get("headers"),
|
headers=config.get("headers"),
|
||||||
timeout=config.get("timeout"),
|
timeout=config.get("timeout"),
|
||||||
sse_read_timeout=config.get("sse_read_timeout"),
|
sse_read_timeout=config.get("sse_read_timeout"),
|
||||||
@ -368,6 +371,7 @@ async def create_mcp_server(
|
|||||||
url: str = None,
|
url: str = None,
|
||||||
command: str = None,
|
command: str = None,
|
||||||
args: list = None,
|
args: list = None,
|
||||||
|
env: dict = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
headers: dict = None,
|
headers: dict = None,
|
||||||
timeout: int = None,
|
timeout: int = None,
|
||||||
@ -389,6 +393,7 @@ async def create_mcp_server(
|
|||||||
url=url,
|
url=url,
|
||||||
command=command,
|
command=command,
|
||||||
args=args,
|
args=args,
|
||||||
|
env=env,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
sse_read_timeout=sse_read_timeout,
|
sse_read_timeout=sse_read_timeout,
|
||||||
@ -417,6 +422,7 @@ async def update_mcp_server(
|
|||||||
url: str = None,
|
url: str = None,
|
||||||
command: str = None,
|
command: str = None,
|
||||||
args: list = None,
|
args: list = None,
|
||||||
|
env: Any = _UNSET,
|
||||||
headers: dict = None,
|
headers: dict = None,
|
||||||
timeout: int = None,
|
timeout: int = None,
|
||||||
sse_read_timeout: int = None,
|
sse_read_timeout: int = None,
|
||||||
@ -439,6 +445,8 @@ async def update_mcp_server(
|
|||||||
server.command = command
|
server.command = command
|
||||||
if args is not None:
|
if args is not None:
|
||||||
server.args = args
|
server.args = args
|
||||||
|
if env is not _UNSET:
|
||||||
|
server.env = env
|
||||||
if headers is not None:
|
if headers is not None:
|
||||||
server.headers = headers
|
server.headers = headers
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
|
|||||||
@ -341,6 +341,7 @@ class MCPServer(Base):
|
|||||||
url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)")
|
url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)")
|
||||||
command = Column(String(500), nullable=True, comment="命令(stdio)")
|
command = Column(String(500), nullable=True, comment="命令(stdio)")
|
||||||
args = Column(JSON, nullable=True, comment="命令参数数组(stdio)")
|
args = Column(JSON, nullable=True, comment="命令参数数组(stdio)")
|
||||||
|
env = Column(JSON, nullable=True, comment="环境变量(stdio)")
|
||||||
headers = Column(JSON, nullable=True, comment="HTTP 请求头")
|
headers = Column(JSON, nullable=True, comment="HTTP 请求头")
|
||||||
timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)")
|
timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)")
|
||||||
sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)")
|
sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)")
|
||||||
@ -369,6 +370,7 @@ class MCPServer(Base):
|
|||||||
"url": self.url,
|
"url": self.url,
|
||||||
"command": self.command,
|
"command": self.command,
|
||||||
"args": self.args or [],
|
"args": self.args or [],
|
||||||
|
"env": self.env or {},
|
||||||
"headers": self.headers or {},
|
"headers": self.headers or {},
|
||||||
"timeout": self.timeout,
|
"timeout": self.timeout,
|
||||||
"sse_read_timeout": self.sse_read_timeout,
|
"sse_read_timeout": self.sse_read_timeout,
|
||||||
@ -402,6 +404,14 @@ class MCPServer(Base):
|
|||||||
config["args"] = json.loads(self.args)
|
config["args"] = json.loads(self.args)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
if self.transport == "stdio" and self.env:
|
||||||
|
if isinstance(self.env, dict):
|
||||||
|
config["env"] = self.env
|
||||||
|
elif isinstance(self.env, str):
|
||||||
|
try:
|
||||||
|
config["env"] = json.loads(self.env)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
# headers 只用于 sse/streamable_http 传输类型
|
# headers 只用于 sse/streamable_http 传输类型
|
||||||
if self.transport in ("sse", "streamable_http") and self.headers:
|
if self.transport in ("sse", "streamable_http") and self.headers:
|
||||||
if isinstance(self.headers, dict):
|
if isinstance(self.headers, dict):
|
||||||
|
|||||||
@ -171,6 +171,7 @@ class PostgresManager(metaclass=SingletonMeta):
|
|||||||
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS tool_dependencies JSONB DEFAULT '[]'::jsonb",
|
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS tool_dependencies JSONB DEFAULT '[]'::jsonb",
|
||||||
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS mcp_dependencies JSONB DEFAULT '[]'::jsonb",
|
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS mcp_dependencies JSONB DEFAULT '[]'::jsonb",
|
||||||
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS skill_dependencies JSONB DEFAULT '[]'::jsonb",
|
"ALTER TABLE IF EXISTS skills ADD COLUMN IF NOT EXISTS skill_dependencies JSONB DEFAULT '[]'::jsonb",
|
||||||
|
"ALTER TABLE IF EXISTS mcp_servers ADD COLUMN IF NOT EXISTS env JSONB",
|
||||||
]
|
]
|
||||||
async with self.async_engine.begin() as conn:
|
async with self.async_engine.begin() as conn:
|
||||||
for stmt in stmts:
|
for stmt in stmts:
|
||||||
|
|||||||
@ -416,6 +416,7 @@ class MCPServer(Base):
|
|||||||
url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)")
|
url = Column(String(500), nullable=True, comment="服务器 URL(sse/streamable_http)")
|
||||||
command = Column(String(500), nullable=True, comment="命令(stdio)")
|
command = Column(String(500), nullable=True, comment="命令(stdio)")
|
||||||
args = Column(JSON, nullable=True, comment="命令参数数组(stdio)")
|
args = Column(JSON, nullable=True, comment="命令参数数组(stdio)")
|
||||||
|
env = Column(JSON, nullable=True, comment="环境变量(stdio)")
|
||||||
headers = Column(JSON, nullable=True, comment="HTTP 请求头")
|
headers = Column(JSON, nullable=True, comment="HTTP 请求头")
|
||||||
timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)")
|
timeout = Column(Integer, nullable=True, comment="HTTP 超时时间(秒)")
|
||||||
sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)")
|
sse_read_timeout = Column(Integer, nullable=True, comment="SSE 读取超时(秒)")
|
||||||
@ -444,6 +445,7 @@ class MCPServer(Base):
|
|||||||
"url": self.url,
|
"url": self.url,
|
||||||
"command": self.command,
|
"command": self.command,
|
||||||
"args": self.args or [],
|
"args": self.args or [],
|
||||||
|
"env": self.env or {},
|
||||||
"headers": self.headers or {},
|
"headers": self.headers or {},
|
||||||
"timeout": self.timeout,
|
"timeout": self.timeout,
|
||||||
"sse_read_timeout": self.sse_read_timeout,
|
"sse_read_timeout": self.sse_read_timeout,
|
||||||
@ -475,6 +477,14 @@ class MCPServer(Base):
|
|||||||
config["args"] = json.loads(self.args)
|
config["args"] = json.loads(self.args)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
if self.transport == "stdio" and self.env:
|
||||||
|
if isinstance(self.env, dict):
|
||||||
|
config["env"] = self.env
|
||||||
|
elif isinstance(self.env, str):
|
||||||
|
try:
|
||||||
|
config["env"] = json.loads(self.env)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
# headers 只用于 sse/streamable_http 传输类型
|
# headers 只用于 sse/streamable_http 传输类型
|
||||||
if self.transport in ("sse", "streamable_http") and self.headers:
|
if self.transport in ("sse", "streamable_http") and self.headers:
|
||||||
if isinstance(self.headers, dict):
|
if isinstance(self.headers, dict):
|
||||||
|
|||||||
145
web/src/components/McpEnvEditor.vue
Normal file
145
web/src/components/McpEnvEditor.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<template>
|
||||||
|
<div class="env-editor">
|
||||||
|
<div v-for="(row, index) in rows" :key="index" class="env-row">
|
||||||
|
<a-input v-model:value="row.key" placeholder="Key" class="env-key-input" />
|
||||||
|
<a-input v-model:value="row.value" placeholder="Value" class="env-value-input" />
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
@click="removeRow(index)"
|
||||||
|
:disabled="rows.length === 1"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-button size="small" @click="addRow">
|
||||||
|
<template #icon><PlusOutlined /></template>
|
||||||
|
添加变量
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const rows = ref([{ key: '', value: '' }])
|
||||||
|
const syncingFromObject = ref(false)
|
||||||
|
|
||||||
|
const objectToRows = (envObj) => {
|
||||||
|
if (!envObj || typeof envObj !== 'object') {
|
||||||
|
return [{ key: '', value: '' }]
|
||||||
|
}
|
||||||
|
const entries = Object.entries(envObj)
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return [{ key: '', value: '' }]
|
||||||
|
}
|
||||||
|
return entries.map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: value == null ? '' : String(value)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeEnvObject = (value) => {
|
||||||
|
if (value == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsToObject = (rowsValue) => {
|
||||||
|
const entries = rowsValue
|
||||||
|
.map((row) => ({
|
||||||
|
key: row.key.trim(),
|
||||||
|
value: row.value
|
||||||
|
}))
|
||||||
|
.filter((row) => row.key)
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return Object.fromEntries(entries.map((row) => [row.key, row.value]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRow = () => {
|
||||||
|
rows.value.push({ key: '', value: '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeRow = (index) => {
|
||||||
|
if (rows.value.length === 1) {
|
||||||
|
rows.value[0].key = ''
|
||||||
|
rows.value[0].value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value) => {
|
||||||
|
syncingFromObject.value = true
|
||||||
|
const normalized = normalizeEnvObject(value)
|
||||||
|
if (!normalized) {
|
||||||
|
rows.value = [{ key: '', value: '' }]
|
||||||
|
} else {
|
||||||
|
rows.value = objectToRows(normalized)
|
||||||
|
}
|
||||||
|
syncingFromObject.value = false
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
rows,
|
||||||
|
(value) => {
|
||||||
|
if (syncingFromObject.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const obj = rowsToObject(value)
|
||||||
|
emit('update:modelValue', obj)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.env-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.env-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.env-key-input,
|
||||||
|
.env-value-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -78,6 +78,10 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-item" v-if="server.env && Object.keys(server.env).length > 0">
|
||||||
|
<label>环境变量</label>
|
||||||
|
<pre class="headers-pre">{{ JSON.stringify(server.env, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="info-item" v-if="server.description">
|
<div class="info-item" v-if="server.description">
|
||||||
|
|||||||
@ -214,7 +214,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- StdIO 类型 -->
|
<!-- StdIO 类型 -->
|
||||||
<template v-if="form.transport === 'stdio'">
|
<template v-if="isStdioTransport">
|
||||||
<a-form-item label="命令" required class="form-item">
|
<a-form-item label="命令" required class="form-item">
|
||||||
<a-input v-model:value="form.command" placeholder="例如:npx 或 /path/to/server" />
|
<a-input v-model:value="form.command" placeholder="例如:npx 或 /path/to/server" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@ -227,6 +227,10 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="环境变量" class="form-item">
|
||||||
|
<McpEnvEditor v-model="form.env" />
|
||||||
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<a-form-item label="标签" class="form-item">
|
<a-form-item label="标签" class="form-item">
|
||||||
@ -283,6 +287,7 @@ import {
|
|||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { mcpApi } from '@/apis/mcp_api'
|
import { mcpApi } from '@/apis/mcp_api'
|
||||||
import McpServerDetailModal from './McpServerDetailModal.vue'
|
import McpServerDetailModal from './McpServerDetailModal.vue'
|
||||||
|
import McpEnvEditor from './McpEnvEditor.vue'
|
||||||
|
|
||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -304,6 +309,7 @@ const form = reactive({
|
|||||||
url: '',
|
url: '',
|
||||||
command: '',
|
command: '',
|
||||||
args: [],
|
args: [],
|
||||||
|
env: null,
|
||||||
headersText: '',
|
headersText: '',
|
||||||
timeout: null,
|
timeout: null,
|
||||||
sse_read_timeout: null,
|
sse_read_timeout: null,
|
||||||
@ -321,6 +327,10 @@ const httpCount = computed(
|
|||||||
)
|
)
|
||||||
const sseCount = computed(() => servers.value.filter((s) => s.transport === 'sse').length)
|
const sseCount = computed(() => servers.value.filter((s) => s.transport === 'sse').length)
|
||||||
const stdioCount = computed(() => servers.value.filter((s) => s.transport === 'stdio').length)
|
const stdioCount = computed(() => servers.value.filter((s) => s.transport === 'stdio').length)
|
||||||
|
const envEditorKey = computed(() => `${form.name}-${form.transport}`)
|
||||||
|
const isStdioTransport = computed(
|
||||||
|
() => String(form.transport || '').trim().toLowerCase() === 'stdio'
|
||||||
|
)
|
||||||
|
|
||||||
// 获取服务器列表
|
// 获取服务器列表
|
||||||
const fetchServers = async () => {
|
const fetchServers = async () => {
|
||||||
@ -352,6 +362,7 @@ const showAddModal = () => {
|
|||||||
url: '',
|
url: '',
|
||||||
command: '',
|
command: '',
|
||||||
args: [],
|
args: [],
|
||||||
|
env: null,
|
||||||
headersText: '',
|
headersText: '',
|
||||||
timeout: null,
|
timeout: null,
|
||||||
sse_read_timeout: null,
|
sse_read_timeout: null,
|
||||||
@ -362,8 +373,7 @@ const showAddModal = () => {
|
|||||||
formModalVisible.value = true
|
formModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示编辑模态框
|
const applyServerToForm = (server) => {
|
||||||
const showEditModal = (server) => {
|
|
||||||
editMode.value = true
|
editMode.value = true
|
||||||
formMode.value = 'form'
|
formMode.value = 'form'
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
@ -373,6 +383,7 @@ const showEditModal = (server) => {
|
|||||||
url: server.url || '',
|
url: server.url || '',
|
||||||
command: server.command || '',
|
command: server.command || '',
|
||||||
args: server.args || [],
|
args: server.args || [],
|
||||||
|
env: server.env || null,
|
||||||
headersText: server.headers ? JSON.stringify(server.headers, null, 2) : '',
|
headersText: server.headers ? JSON.stringify(server.headers, null, 2) : '',
|
||||||
timeout: server.timeout,
|
timeout: server.timeout,
|
||||||
sse_read_timeout: server.sse_read_timeout,
|
sse_read_timeout: server.sse_read_timeout,
|
||||||
@ -382,6 +393,20 @@ const showEditModal = (server) => {
|
|||||||
formModalVisible.value = true
|
formModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示编辑模态框
|
||||||
|
const showEditModal = async (server) => {
|
||||||
|
try {
|
||||||
|
const result = await mcpApi.getMcpServer(server.name)
|
||||||
|
if (result.success && result.data) {
|
||||||
|
applyServerToForm(result.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取服务器详情失败,回退使用列表数据:', err)
|
||||||
|
}
|
||||||
|
applyServerToForm(server)
|
||||||
|
}
|
||||||
|
|
||||||
// 显示详情模态框
|
// 显示详情模态框
|
||||||
const showDetailModal = (server) => {
|
const showDetailModal = (server) => {
|
||||||
selectedServer.value = server
|
selectedServer.value = server
|
||||||
@ -420,6 +445,7 @@ const handleFormSubmit = async () => {
|
|||||||
url: form.url || null,
|
url: form.url || null,
|
||||||
command: form.command || null,
|
command: form.command || null,
|
||||||
args: form.args.length > 0 ? form.args : null,
|
args: form.args.length > 0 ? form.args : null,
|
||||||
|
env: form.env,
|
||||||
headers,
|
headers,
|
||||||
timeout: form.timeout || null,
|
timeout: form.timeout || null,
|
||||||
sse_read_timeout: form.sse_read_timeout || null,
|
sse_read_timeout: form.sse_read_timeout || null,
|
||||||
@ -577,6 +603,7 @@ const parseJsonToForm = () => {
|
|||||||
url: obj.url || '',
|
url: obj.url || '',
|
||||||
command: obj.command || '',
|
command: obj.command || '',
|
||||||
args: obj.args || [],
|
args: obj.args || [],
|
||||||
|
env: obj.env || null,
|
||||||
headersText: obj.headers ? JSON.stringify(obj.headers, null, 2) : '',
|
headersText: obj.headers ? JSON.stringify(obj.headers, null, 2) : '',
|
||||||
timeout: obj.timeout || null,
|
timeout: obj.timeout || null,
|
||||||
sse_read_timeout: obj.sse_read_timeout || null,
|
sse_read_timeout: obj.sse_read_timeout || null,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user