# 创建MCP服务

`POST` `/api/agent/finx/createMcpService`

为指定智能体创建一个 MCP 类型的 Finx 服务。服务创建后，其工具由远端 MCP Server 动态提供，无需再调用 createTool。

**保存时会做连通性测试**：后端会用传入的 mcpConfig 真实连接 MCP Server 执行一次 listTools，连不通就直接报错、服务不会被创建；需要看参数校验、连接测试、描述生成、持久化各阶段进度时，可用流式版本 `/createMcpService/stream`。

**与凭证管理的分工**：平台统一凭证直接写在 mcpConfig.auth 里；若要求每个用户用自己的凭证，则在 auth 里只声明字段名、值留空并置 requireUserProvided=true，用户后续通过 getAuthSchema + saveAuthCredential 各自提交。

身份说明：当您通过API访问本系统时，请求头中必须传递 X-Principal-Id 以标识操作主体；当您通过页面访问本系统时，无需传递。

## 请求参数

### 请求头参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| X-Principal-Id | integer | 否 | 身份主体ID（Long类型）。通过API访问时必须传递以标识操作主体；通过页面访问时无需传递。可通过「身份主体」模块的创建接口获取。 |

### 请求体（JSON）

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| agentId | integer | 是 | 智能体ID，服务创建后归属该智能体 |
| serviceName | string | 是 | 服务标识名，创建后作为 finx 命令中调用该服务的名字。必须以英文字母或下划线开头，只能包含字母、数字、下划线和连字符，长度不超过 64；在同一智能体内以及与平台全局服务之间都不能重名，重名会直接报错 |
| displayName | string | 否 | 服务中文名（前端展示用，可为空，为空时展示服务标识名），长度不超过64 |
| logoUrl | string | 否 | 服务Logo URL（前端展示用，可为空），长度不超过512 |
| description | string | 否 | 服务描述，长度不超过 512。**智能体依赖这段描述判断什么场景该调用该服务**，请写清能力范围、覆盖的数据品类与适用问题场景，描述越准确调用时机越准 |
| mcpConfig | object | 是 | **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 只回显含 `***` 的脱敏值，不会返回明文
- 编辑服务时把脱敏值原样回传即表示「保留原值」，星号不会被写入库；要真正修改就传新的明文值 |
| autoGenerateDescription | boolean | 否 | 是否自动生成服务描述，不传时默认 false。开启后系统会连接 MCP Server 获取工具列表，再调用大模型生成概括性描述并覆盖 description。注意：当 mcpConfig.auth.requireUserProvided=true（用户各自提供凭证）时，创建期无凭证可连接，本开关会被忽略，此时必须自行填写 description |

## 响应

### 200

| 参数名 | 类型 | 说明 |
|--------|------|------|
| httpStatusCode | integer | HTTP状态码 |
| message | string | 响应消息 |
| data | integer | 响应数据 |
| 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处理 |

