# 编辑MCP服务

`POST` `/api/agent/finx/updateMcpService`

编辑指定 MCP 类型 Finx 服务的配置（名称、展示名、Logo、描述、mcpConfig）。

**更新语义**：mcpConfig 中除 auth 以外的字段全量覆盖原配置；auth 与库中原配置按合并语义处理（未修改的凭证由后端用库中真实值回填，详见 mcpConfig 参数说明）。允许修改 serviceName 并会做唯一性校验（排除自身）。

**保存时会做连通性测试**：后端会对合并后的真实配置执行一次 listTools，失败直接报错、修改不生效。

**建议做法**：先调用 getServiceDetail 拿到当前完整配置（凭证为脱敏值），在其上修改后整体回传；未修改的凭证字段保留脱敏值原样回传即可，星号不会被写入库。用户各自的凭证仍由 saveAuthCredential 独立管理。

## 请求参数

### 请求体（JSON）

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| agentId | integer | 是 | 智能体ID，需与该服务归属的智能体一致，否则报错 |
| id | integer | 是 | 服务ID，由 listServices / getServiceDetail 返回；必须是 MCP 类型服务的 ID，类型不匹配会报错 |
| serviceName | string | 是 | 服务标识名，创建后作为 finx 命令中调用该服务的名字。必须以英文字母或下划线开头，只能包含字母、数字、下划线和连字符，长度不超过 64；在同一智能体内以及与平台全局服务之间都不能重名，重名会直接报错。允许修改，会做唯一性校验（排除自身） |
| displayName | string | 否 | 服务中文名（前端展示用，可为空，为空时展示服务标识名），长度不超过64 |
| logoUrl | string | 否 | 服务Logo URL（前端展示用，可为空），长度不超过512 |
| description | string | 否 | 服务描述，长度不超过 512。**智能体依赖这段描述判断什么场景该调用该服务**，请写清能力范围、覆盖的数据品类与适用问题场景，描述越准确调用时机越准 |
| mcpConfig | object | 是 | 除 auth 外的字段整体覆盖原配置，auth 按下方合并语义处理。建议先调用 getServiceDetail 拿到当前完整配置，在其上修改后回传，避免字段丢失。

**MCP服务配置（JSON对象）**
- serverUrl：MCP Server 的接入地址（必填），如 `https://mcp.example.com/sse`
- transportType：传输协议，可选值 SSE、STREAMABLE_HTTP，不传时默认 SSE
- requestTimeoutSeconds：单次请求超时时间（秒），不传时默认 30
- description：配置备注（选填，仅作记录，不影响调用）
- auth：鉴权配置，字段说明见下方
- testVariables：连通性测试用的临时变量值（选填），仅当配置中使用了 `${...}` 占位符时才需要

**保存时的行为**
- 后端会用本配置真实连接 MCP Server 执行一次 listTools，连不通会直接返回错误原因、服务不会被创建
- 若 auth.requireUserProvided=true，创建期没有可用凭证，会跳过连通性测试，此时**必须显式填写 description**（autoGenerateDescription 会被忽略）

**运行时变量占位符（进阶用法）**
- serverUrl、auth.credential、auth.customHeaders 的值、auth.queryParams 的值支持 `${ctx.xxx}`（发起运行时由调用方传入）与 `${sys.xxx}`（系统注入，如 `${sys.agentId}`）
- 使用了占位符时，请在 testVariables 中给出本次连通性测试用的值，key 需带命名空间前缀，如 `{"ctx.union_id":"u-123"}`；该值只用于保存时的测试，运行时取真实上下文值

**完整示例**
- 无鉴权：`{"serverUrl":"https://mcp.example.com/sse","transportType":"SSE","requestTimeoutSeconds":30}`
- 平台统一 Bearer 凭证：`{"serverUrl":"https://mcp.example.com/mcp","transportType":"STREAMABLE_HTTP","requestTimeoutSeconds":30,"auth":{"type":"BEARER","credential":"my-token"}}`
- 用户各自提供凭证：`{"serverUrl":"https://mcp.example.com/sse","transportType":"SSE","auth":{"type":"CUSTOM","customHeaders":{"X-App-Secret":""},"requireUserProvided":true}}`

**auth（鉴权配置，选填。不传或 type=NONE 表示该服务无需鉴权）**
- type：鉴权类型。**请求侧键名是 `type`；响应侧（getServiceDetail、getAuthSchema）回显时叫 `authType`，两者不要混用**。可选值：
  - NONE：无需认证
  - API_KEY：API 密钥放在请求头
  - BEARER：Bearer 令牌
  - BASIC：HTTP Basic 认证
  - CUSTOM：一个或多个自定义请求头
  - CUSTOM_HTTP_TOKEN：先调用一个 HTTP 接口动态换取令牌
  - OAUTH：OAuth 2.1 按用户授权（平台预置数据源专用，见下文）
- headerName：API_KEY 使用的请求头名，不传时默认 `X-API-Key`
- tokenPrefix：BEARER 使用的令牌前缀，不传时默认 `Bearer`（BASIC 固定使用 `Basic`，此字段对它无效）
- credential：主凭证值，API_KEY / BEARER / BASIC 使用。BEARER 只填令牌本身、**不要带 `Bearer ` 前缀**；BASIC 填 `base64(用户名:密码)` 的结果
- customHeaders：自定义请求头键值对，CUSTOM 使用，运行时按原样作为请求头发送
- queryParams：URL 查询参数键值对，**可与任意 type 叠加**，运行时拼接到请求 URL 上（如 `?apiKey=xxx`）
- requireUserProvided：凭证由谁提供，不传时默认 false
  - false（平台统一凭证）：在 auth 里直接写真实凭证值，该服务的所有用户共用这一份凭证
  - true（用户各自提供）：auth 只作为模板，仅声明字段名、凭证值留空串；每个用户后续通过 saveAuthCredential 提交自己的凭证
- tokenFetchConfig：动态换取令牌的配置，仅 type=CUSTOM_HTTP_TOKEN 时需要，子字段：
  - method：换取令牌的请求方法，不传时默认 POST
  - url：换取令牌的接口地址（必填）
  - headers：换取令牌时携带的请求头，客户端凭证放在这里（如 `{"Authorization":"Basic xxx"}`）
  - body：换取令牌的请求体字符串（如 `grant_type=client_credentials`）
  - tokenField：从响应 JSON 中取令牌的字段名，不传时默认 `access_token`
  - expiresInField：从响应 JSON 中取有效期（秒）的字段名，不传时默认 `expires_in`
  - targetHeaderName：把令牌注入业务请求时使用的请求头名，不传时默认 `Authorization`
  - targetTokenPrefix：注入令牌时使用的前缀，不传时默认 `Bearer`
  - refreshBufferSeconds：在令牌到期前多少秒主动刷新，不传时默认 60

**各 type 的 auth 写法示例**
- NONE：`{"type":"NONE"}`
- API_KEY（默认请求头 X-API-Key）：`{"type":"API_KEY","credential":"sk-abcdef123456"}`
- API_KEY（自定义请求头名）：`{"type":"API_KEY","headerName":"X-Custom-Key","credential":"sk-abcdef123456"}`
- BEARER：`{"type":"BEARER","credential":"eyJhbGciOiJIUzI1NiJ9.payload.sig"}`
- BASIC：`{"type":"BASIC","credential":"dXNlcjpwYXNz"}`（`dXNlcjpwYXNz` 即 base64 编码后的 `user:pass`）
- CUSTOM（多个自定义头）：`{"type":"CUSTOM","customHeaders":{"X-App-Id":"123","X-App-Secret":"abc"}}`
- CUSTOM_HTTP_TOKEN：`{"type":"CUSTOM_HTTP_TOKEN","tokenFetchConfig":{"method":"POST","url":"https://auth.example.com/oauth2/token","headers":{"Authorization":"Basic dXNlcjpwYXNz","Content-Type":"application/x-www-form-urlencoded"},"body":"grant_type=client_credentials","tokenField":"access_token","expiresInField":"expires_in","targetHeaderName":"Authorization","targetTokenPrefix":"Bearer","refreshBufferSeconds":60}}`
- 用户各自提供凭证（只声明字段名、值留空）：`{"type":"CUSTOM","customHeaders":{"X-App-Secret":""},"requireUserProvided":true}`
- 鉴权信息放 URL 参数（与 BEARER 叠加）：`{"type":"BEARER","credential":"my-token","queryParams":{"appId":"123"}}`
- OAUTH：仅用于平台预置的「按用户授权」数据源，需平台侧先完成 OAuth 客户端注册，通过本接口自助创建服务时暂不支持自行配置，如有此类需求请联系平台

**凭证安全**
- credential、customHeaders 与 queryParams 的值落库前会加密存储；getServiceDetail 只回显含 `***` 的脱敏值，不会返回明文
- 编辑服务时把脱敏值原样回传即表示「保留原值」，星号不会被写入库；要真正修改就传新的明文值

**编辑时 auth 的合并语义（本接口特有）**
- 整个 auth 不传：完整保留原鉴权配置
- auth.type=NONE：明确清除鉴权配置
- credential：不传或传脱敏值（含 `***`）= 保留原值；传新的明文 = 替换；传空字符串 = 明确清空
- customHeaders / queryParams：**请求中出现的 key 集合即最终保留的集合**，原有 key 未出现即被删除；某个 key 的值不传或传脱敏值 = 保留该 key 原值，传空字符串 = 清空
- customHeaderKeys / queryParamKeys（字符串数组，选填）：用于「只修改其中一个 key」的场景声明最终 key 集合。key 在数组内但不在 customHeaders / queryParams 中 = 保留原值；key 不在数组内 = 删除。之所以需要这两个字段，是因为多数客户端的 JSON 序列化会丢弃 null 值，无法用 `{"key":null}` 表达「保留原值」。不传数组时按上一条语义处理
- headerName / tokenPrefix / requireUserProvided / tokenFetchConfig：不传时保留原值
- 示例（只改 X-App-Secret，保留 X-App-Id 原值，并删除原有的其他自定义头）：`{"type":"CUSTOM","customHeaders":{"X-App-Secret":"new-secret"},"customHeaderKeys":["X-App-Id","X-App-Secret"]}` |

## 响应

### 200

| 参数名 | 类型 | 说明 |
|--------|------|------|
| httpStatusCode | integer | HTTP状态码 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| success | boolean | 是否成功 |
| requestId | string | 上游请求ID，用于跨系统链路追踪排查问题 |
| errCode | string | 上游错误码，前端可据此做特殊UI处理 |

### 400

| 参数名 | 类型 | 说明 |
|--------|------|------|
| httpStatusCode | integer | HTTP状态码 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| success | boolean | 是否成功 |
| requestId | string | 上游请求ID，用于跨系统链路追踪排查问题 |
| errCode | string | 上游错误码，前端可据此做特殊UI处理 |

### 500

| 参数名 | 类型 | 说明 |
|--------|------|------|
| httpStatusCode | integer | HTTP状态码 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| success | boolean | 是否成功 |
| requestId | string | 上游请求ID，用于跨系统链路追踪排查问题 |
| errCode | string | 上游错误码，前端可据此做特殊UI处理 |

