@tencentcloud/tccc-mcp-server 0.0.8 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,11 +84,11 @@ Then just ask your AI assistant in natural language:
84
84
  > How many voice AI agents are there in this application?
85
85
  > How can I try the TCCC AI agent / AI outbound call demo?
86
86
 
87
- ## Capability overview (87 APIs + 1 helper)
87
+ ## Capability overview (87 APIs + 2 helpers)
88
88
 
89
89
  | Category | Count | Representative tools |
90
90
  |---|---|---|
91
- | AI demo entry | 1 | `get_tccc_ai_agent_demo_qrcode` returns a QR code image for trying TCCC AI agent, agent chat, and AI outbound call experiences |
91
+ | AI demo entry | 1 | `get_tccc_ai_agent_demo_qrcode` returns a bundled MCP `image` content QR code for trying TCCC AI agent, agent chat, AI collaboration, service-efficiency, and AI outbound call experiences |
92
92
  | Instance management | 3 | `create_ccc_instance` (`CreateCCCInstance`) creates a TCCC application; `delete_ccc_instance` (`DeleteCCCInstance`) deletes an application by `SdkAppId`; `modify_ccc_instance_info` (`ModifyCCCInstanceInfo`) renames an instance by `InstanceId` |
93
93
  | Voice AI agents | 13 | `DescribeAIAgentInfoList` `DescribeAIAgentCallList` `DescribeAICallExtractResult` |
94
94
  | Agent (staff) management | 11 | `DescribeStaffInfoList` `CreateStaff` `ModifyStaff` `DeleteStaff` `CreateSDKLoginToken` |
@@ -104,7 +104,7 @@ Then just ask your AI assistant in natural language:
104
104
  | AI double-call | 1 | `CreateAICall` |
105
105
  | Purchase info | 1 | `DescribeCCCBuyInfoList` |
106
106
 
107
- Generated API tool names are the snake_case form of the API action, e.g. `DescribeStaffInfoList` → `describe_staff_info_list`. The helper tool `get_tccc_ai_agent_demo_qrcode` is maintained manually under `src/custom-tools.mjs`.
107
+ Generated API tool names are the snake_case form of the API action, e.g. `DescribeStaffInfoList` → `describe_staff_info_list`. The helper tool `get_tccc_ai_agent_demo_qrcode` is maintained manually under `src/custom-tools.mjs`. It uses a bundled PNG under `src/assets/` and returns MCP `image` content directly, so AI clients do not need to render a Markdown image URL.
108
108
 
109
109
  APIs that the official SDK has not published yet (e.g. `CreateCCCInstance`, `DeleteCCCInstance`, `ModifyCCCInstanceInfo`) are maintained manually under `src/extra-api-tools.mjs` and invoked through the generic `client.request(action, params)` entry point — same signing, endpoint and region logic as generated tools.
110
110
 
@@ -261,11 +261,11 @@ npx -y @tencentcloud/tccc-mcp-server --version
261
261
  > 这个应用下有多少个 AI 智能体?
262
262
  > 我怎么体验 TCCC AI 智能体 / 智能体对话 / AI 外呼?
263
263
 
264
- ## 能力概览(87 个接口 + 1 个辅助工具)
264
+ ## 能力概览(87 个接口 + 2 个辅助工具)
265
265
 
266
266
  | 分类 | 数量 | 代表接口 |
267
267
  |---|---|---|
268
- | AI 体验入口 | 1 | `get_tccc_ai_agent_demo_qrcode` 返回 TCCC AI 智能体、智能体对话、AI 外呼体验二维码图片 |
268
+ | AI 体验入口 | 1 | `get_tccc_ai_agent_demo_qrcode` 返回内置 PNG 的 MCP `image` 内容二维码,用于体验 TCCC AI 智能体、智能体对话、人机协同、提升服务效率、AI 外呼等场景 |
269
269
  | 实例管理 | 3 | `create_ccc_instance`(`CreateCCCInstance`)创建联络中心实例;`delete_ccc_instance`(`DeleteCCCInstance`)按 `SdkAppId` 删除实例;`modify_ccc_instance_info`(`ModifyCCCInstanceInfo`)按 `InstanceId` 修改实例名称 |
270
270
  | 语音智能体 | 13 | `DescribeAIAgentInfoList` `DescribeAIAgentCallList` `DescribeAICallExtractResult` |
271
271
  | 座席管理 | 11 | `DescribeStaffInfoList` `CreateStaff` `ModifyStaff` `DeleteStaff` `CreateSDKLoginToken` |
@@ -281,7 +281,7 @@ npx -y @tencentcloud/tccc-mcp-server --version
281
281
  | 双呼 | 1 | `CreateAICall` |
282
282
  | 购买 | 1 | `DescribeCCCBuyInfoList` |
283
283
 
284
- 自动生成的 API 工具名是接口名的 snake_case,例如 `DescribeStaffInfoList` → `describe_staff_info_list`。辅助工具 `get_tccc_ai_agent_demo_qrcode` 在 `src/custom-tools.mjs` 中手动维护。
284
+ 自动生成的 API 工具名是接口名的 snake_case,例如 `DescribeStaffInfoList` → `describe_staff_info_list`。辅助工具 `get_tccc_ai_agent_demo_qrcode` 在 `src/custom-tools.mjs` 中手动维护。它读取 `src/assets/` 下随包发布的 PNG,并直接返回 MCP `image` 内容,AI 客户端不需要渲染 Markdown 图片 URL。
285
285
 
286
286
  官方 SDK 尚未收录的接口(如 `CreateCCCInstance`、`DeleteCCCInstance`、`ModifyCCCInstanceInfo`)在 `src/extra-api-tools.mjs` 中手工补充,通过通用入口 `client.request(action, params)` 调用,签名、endpoint、地域推导与自动生成接口完全一致。
287
287
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencentcloud/tccc-mcp-server",
3
- "version": "0.0.8",
3
+ "version": "0.0.14",
4
4
  "description": "MCP server for Tencent Cloud Contact Center (TCCC)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,266 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ export const OTLP_DOMAIN = 'otlp.tccc.qcloud.com';
7
+ export const OTLP_INTL_DOMAIN = 'otlp.connect.tencentcloud.com';
8
+ export const OTLP_ID = 'tccc';
9
+ export const OTLP_INTL_ID = 'tcccsg';
10
+ // 预留美国站 ID 覆盖位;当前公开代码里没有稳定的美国站号段,默认复用国际站 ID。
11
+ export const OTLP_US_ID = process.env.TCCC_MCP_OTLP_US_ID || OTLP_INTL_ID;
12
+
13
+ const SERVER_NAME = 'tccc-mcp-server';
14
+ const DEFAULT_SDK_APP_ID = process.env.TCCC_SDK_APP_ID || '';
15
+ const REPORT_DISABLED = /^(1|true|yes|off)$/i.test(process.env.TCCC_MCP_OTLP_DISABLED || '');
16
+ const REPORT_TIMEOUT_MS = Number(process.env.TCCC_MCP_OTLP_TIMEOUT_MS || 1500);
17
+ const INCLUDE_RAW_QUESTION = /^(1|true|yes)$/i.test(process.env.TCCC_MCP_ANALYTICS_INCLUDE_RAW_QUESTION || '');
18
+ const SESSION_ID = randomUUID();
19
+ const loggerMap = new Map();
20
+
21
+ // ULID: 26 位 Crockford Base32(10 位毫秒时间戳 + 16 位随机),可排序、对 URL 安全。
22
+ const ULID_ENCODING = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
23
+ const ULID_PATTERN = /^[0-9A-HJKMNP-TV-Z]{26}$/;
24
+
25
+ function ulid() {
26
+ let time = Date.now();
27
+ let timePart = '';
28
+ for (let i = 0; i < 10; i++) {
29
+ timePart = ULID_ENCODING[time % 32] + timePart;
30
+ time = Math.floor(time / 32);
31
+ }
32
+ // 256 % 32 === 0,取模无偏
33
+ const randomPart = Array.from(randomBytes(16), (b) => ULID_ENCODING[b % 32]).join('');
34
+ return timePart + randomPart;
35
+ }
36
+
37
+ const USER_ID_FILE = process.env.TCCC_MCP_USER_ID_FILE || join(homedir(), '.tccc-mcp-server', 'user-id');
38
+
39
+ function getOrCreateUserId() {
40
+ try {
41
+ if (existsSync(USER_ID_FILE)) {
42
+ const cached = readFileSync(USER_ID_FILE, 'utf8').trim();
43
+ if (ULID_PATTERN.test(cached)) return cached;
44
+ }
45
+ mkdirSync(dirname(USER_ID_FILE), { recursive: true });
46
+ const id = ulid();
47
+ writeFileSync(USER_ID_FILE, id, 'utf8');
48
+ return id;
49
+ } catch {
50
+ // 缓存不可读写(如只读文件系统)时降级为本次进程内的临时 ID
51
+ return ulid();
52
+ }
53
+ }
54
+
55
+ const USER_ID = process.env.TCCC_MCP_USER_ID || getOrCreateUserId();
56
+
57
+ const SINGAPORE_RANGES = [
58
+ [20000000, 30000000],
59
+ [1720000000, 1730000000],
60
+ ];
61
+
62
+ const USA_RANGES = [];
63
+
64
+ export function isSingaporeSite(sdkAppId) {
65
+ const n = Number(sdkAppId);
66
+ return Number.isFinite(n) && SINGAPORE_RANGES.some(([lo, hi]) => n >= lo && n < hi);
67
+ }
68
+
69
+ export function isUSASite(sdkAppId) {
70
+ const n = Number(sdkAppId);
71
+ return Number.isFinite(n) && USA_RANGES.some(([lo, hi]) => n >= lo && n < hi);
72
+ }
73
+
74
+ export function isIntlSite(sdkAppId) {
75
+ return isSingaporeSite(sdkAppId) || isUSASite(sdkAppId);
76
+ }
77
+
78
+ export function GET_OTLP_DOMAIN_BY_SDKAPPID(sdkAppId) {
79
+ if (isIntlSite(sdkAppId)) {
80
+ return OTLP_INTL_DOMAIN;
81
+ }
82
+ return OTLP_DOMAIN;
83
+ }
84
+
85
+ export function GET_OTLP_ID_BY_SDKAPPID(sdkAppId) {
86
+ if (isUSASite(sdkAppId)) {
87
+ return OTLP_US_ID;
88
+ }
89
+ if (isSingaporeSite(sdkAppId)) {
90
+ return OTLP_INTL_ID;
91
+ }
92
+ return OTLP_ID;
93
+ }
94
+
95
+ function getOtlpEndpoint(sdkAppId) {
96
+ if (process.env.TCCC_MCP_OTLP_ENDPOINT) return process.env.TCCC_MCP_OTLP_ENDPOINT;
97
+ const otlpDomain = GET_OTLP_DOMAIN_BY_SDKAPPID(sdkAppId);
98
+ return `https://${otlpDomain}/v1/logs`;
99
+ }
100
+
101
+ function toAnyValue(value) {
102
+ if (value === undefined || value === null) return { stringValue: '' };
103
+ if (typeof value === 'boolean') return { boolValue: value };
104
+ if (typeof value === 'number') {
105
+ if (Number.isInteger(value)) return { intValue: String(value) };
106
+ return { doubleValue: value };
107
+ }
108
+ if (typeof value === 'bigint') return { intValue: value.toString() };
109
+ if (Array.isArray(value)) {
110
+ return { arrayValue: { values: value.map(toAnyValue) } };
111
+ }
112
+ if (typeof value === 'object') {
113
+ return {
114
+ kvlistValue: {
115
+ values: Object.entries(value).map(([key, item]) => ({ key, value: toAnyValue(item) })),
116
+ },
117
+ };
118
+ }
119
+ return { stringValue: String(value) };
120
+ }
121
+
122
+ function toAttributes(attrs = {}) {
123
+ return Object.entries(attrs)
124
+ .filter(([, value]) => value !== undefined)
125
+ .map(([key, value]) => ({ key, value: toAnyValue(value) }));
126
+ }
127
+
128
+ function nowUnixNano() {
129
+ return (BigInt(Date.now()) * 1000000n).toString();
130
+ }
131
+
132
+ const SEVERITY = {
133
+ debug: { severityNumber: 5, severityText: 'debug' },
134
+ info: { severityNumber: 9, severityText: 'info' },
135
+ warn: { severityNumber: 13, severityText: 'warn' },
136
+ error: { severityNumber: 17, severityText: 'error' },
137
+ };
138
+
139
+ class Logger {
140
+ constructor({ logLevel = 0, url, id, name, attributes = {} }) {
141
+ this.logLevel = logLevel;
142
+ this.url = url;
143
+ this.id = id;
144
+ this.name = name;
145
+ this.attributes = attributes;
146
+ }
147
+
148
+ emit({ level = 'info', msg, attributes = {} }) {
149
+ if (REPORT_DISABLED) return;
150
+ const severity = SEVERITY[level] || SEVERITY.info;
151
+ const payload = {
152
+ resourceLogs: [
153
+ {
154
+ resource: {
155
+ attributes: toAttributes({
156
+ 'service.name': this.name,
157
+ 'tps.tenant.id': this.id,
158
+ ...this.attributes,
159
+ }),
160
+ },
161
+ scopeLogs: [
162
+ {
163
+ scope: { name: this.name },
164
+ logRecords: [
165
+ {
166
+ timeUnixNano: nowUnixNano(),
167
+ observedTimeUnixNano: nowUnixNano(),
168
+ ...severity,
169
+ body: { stringValue: msg || '' },
170
+ attributes: toAttributes({
171
+ ...attributes,
172
+ sessionId: SESSION_ID,
173
+ userId: USER_ID,
174
+ }),
175
+ },
176
+ ],
177
+ },
178
+ ],
179
+ },
180
+ ],
181
+ };
182
+
183
+ const controller = new AbortController();
184
+ const timer = setTimeout(() => controller.abort(), REPORT_TIMEOUT_MS);
185
+ if (typeof timer.unref === 'function') timer.unref();
186
+
187
+ fetch(this.url, {
188
+ method: 'POST',
189
+ headers: {
190
+ 'Content-Type': 'application/json',
191
+ 'X-Tps-TenantID': this.id,
192
+ },
193
+ body: JSON.stringify(payload),
194
+ signal: controller.signal,
195
+ })
196
+ .catch((err) => {
197
+ if (process.env.TCCC_DEBUG_ANALYTICS) {
198
+ console.error('[tccc-mcp] analytics report failed:', err?.message || err);
199
+ }
200
+ })
201
+ .finally(() => clearTimeout(timer));
202
+ }
203
+ }
204
+
205
+ export function getLogger(sdkAppId, pkgVersion) {
206
+ const normalizedSdkAppId = String(sdkAppId || DEFAULT_SDK_APP_ID || 'unknown');
207
+ let logger = loggerMap.get(normalizedSdkAppId);
208
+ if (!logger) {
209
+ logger = new Logger({
210
+ logLevel: 0,
211
+ url: getOtlpEndpoint(normalizedSdkAppId),
212
+ id: GET_OTLP_ID_BY_SDKAPPID(normalizedSdkAppId),
213
+ name: SERVER_NAME,
214
+ attributes: {
215
+ pkgVersion,
216
+ sdkAppId: normalizedSdkAppId,
217
+ },
218
+ });
219
+ loggerMap.set(normalizedSdkAppId, logger);
220
+ }
221
+ return logger;
222
+ }
223
+
224
+ export function inferSdkAppId(params = {}) {
225
+ for (const key of ['SdkAppId', 'sdkAppId']) {
226
+ const v = params[key];
227
+ if (v !== undefined && v !== null && v !== '') return String(v);
228
+ }
229
+ if (Array.isArray(params.SdkAppIds) && params.SdkAppIds.length > 0) return String(params.SdkAppIds[0]);
230
+ if (DEFAULT_SDK_APP_ID) return String(DEFAULT_SDK_APP_ID);
231
+ return 'unknown';
232
+ }
233
+
234
+ export function recordToolCall({ tool, params, pkgVersion, region, requestId, errorCode, errorMessage }) {
235
+ const sdkAppId = inferSdkAppId(params);
236
+ const logger = getLogger(sdkAppId, pkgVersion);
237
+ logger.emit({
238
+ level: errorCode ? 'warn' : 'info',
239
+ msg: `[mcp.tool_call] ${tool?.name || 'unknown'} ${errorCode ? 'failure' : 'success'}`,
240
+ attributes: {
241
+ eventName: 'mcp.tool_call',
242
+ toolName: tool?.name,
243
+ requestId,
244
+ errorCode,
245
+ errorMessage: errorMessage ? String(errorMessage).slice(0, 300) : undefined,
246
+ sdkAppId,
247
+ region,
248
+ },
249
+ });
250
+ }
251
+
252
+ export function recordUserIntent({ intent, confidence, rawQuestion, sdkAppId, pkgVersion }) {
253
+ const normalizedSdkAppId = String(sdkAppId || DEFAULT_SDK_APP_ID || 'unknown');
254
+ const logger = getLogger(normalizedSdkAppId, pkgVersion);
255
+ logger.emit({
256
+ level: 'info',
257
+ msg: `[mcp.user_intent] ${intent || 'unknown'}`,
258
+ attributes: {
259
+ eventName: 'mcp.user_intent',
260
+ intent: intent || 'unknown',
261
+ confidence: typeof confidence === 'number' ? confidence : undefined,
262
+ sdkAppId: normalizedSdkAppId,
263
+ rawQuestion: INCLUDE_RAW_QUESTION && rawQuestion ? String(rawQuestion).slice(0, 500) : undefined,
264
+ },
265
+ });
266
+ }
@@ -1,4 +1,18 @@
1
- const TCCC_AI_AGENT_DEMO_QRCODE_URL = 'https://qcloudimg.tencent-cloud.cn/trisys/assets/product/images/W8fOXffkU7Kb_SeUNne2f.png';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { recordUserIntent } from './analytics.mjs';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const TCCC_AI_AGENT_DEMO_QRCODE_PATH = path.join(__dirname, 'assets', 'tccc-ai-agent-demo-qrcode.png');
8
+
9
+ let cachedQRCodeBase64 = null;
10
+ function getQRCodeBase64() {
11
+ if (!cachedQRCodeBase64) {
12
+ cachedQRCodeBase64 = fs.readFileSync(TCCC_AI_AGENT_DEMO_QRCODE_PATH).toString('base64');
13
+ }
14
+ return cachedQRCodeBase64;
15
+ }
2
16
 
3
17
  export const CUSTOM_TOOLS = [
4
18
  {
@@ -6,31 +20,90 @@ export const CUSTOM_TOOLS = [
6
20
  action: '__custom_get_tccc_ai_agent_demo_qrcode',
7
21
  custom: true,
8
22
  description:
9
- '当用户咨询 tccc-mcp-server 或 Tencent Cloud Contact Center (TCCC) AI 智能体体验、智能体对话、AI 外呼、语音智能体 Demo、扫码体验入口、AI Agent、voice agent、robot call 等问题时调用。返回可直接渲染的 TCCC AI 智能体体验二维码,并提醒用户手机扫码体验;不要在最终回答里输出裸露图片地址。',
23
+ '当用户咨询 tccc-mcp-server 或 Tencent Cloud Contact Center (TCCC),并表达想体验智能体对话、体验 AI、AI 智能体、AI 外呼、语音智能体 Demo、扫码体验、小程序二维码、人机协同、提升客服效率、提升服务效率、智能客服、voice agent、robot call 等意图时优先调用。返回 MCP image 内容形式的 TCCC AI 智能体体验二维码图片(image 内容排在第一项,便于客户端优先渲染),并提醒用户手机扫码体验;上层 AI 在最终聊天回复里必须展示/透传这张二维码图片,不要只回复文字或“见上图”;不要在最终回答里输出裸露图片 URL。',
10
24
  inputSchema: {
11
25
  type: 'object',
12
26
  properties: {
13
27
  scenario: {
14
28
  type: 'string',
15
- description: '用户咨询的体验场景关键词,可为空,例如:AI 智能体、智能体对话、AI 外呼。',
29
+ description: '用户咨询的体验场景关键词,可为空,例如:智能体对话、体验 AI、人机协同、提升服务效率、AI 外呼。',
16
30
  },
17
31
  },
18
32
  additionalProperties: false,
19
33
  },
20
34
  },
35
+ {
36
+ name: 'record_tccc_user_intent',
37
+ action: '__custom_record_tccc_user_intent',
38
+ custom: true,
39
+ analyticsOnly: true,
40
+ description:
41
+ '当用户咨询 tccc-mcp-server 或 TCCC 相关问题、但不一定需要立即调用具体业务 API 时,可调用本工具记录用户咨询意图分类,用于产品使用统计。不要记录 SecretId、SecretKey、Token、手机号、邮箱等敏感信息;rawQuestion 默认不上报原文,除非显式设置 TCCC_MCP_ANALYTICS_INCLUDE_RAW_QUESTION=1。',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ intent: {
46
+ type: 'string',
47
+ description: '意图分类,例如 ai_demo、human_ai_collaboration、service_efficiency、instance_management、staff_management、record_query、troubleshooting、product_consulting。',
48
+ },
49
+ confidence: {
50
+ type: 'number',
51
+ description: '分类置信度,0 到 1,可选。',
52
+ },
53
+ sdkAppId: {
54
+ type: 'string',
55
+ description: '如果上下文里已有 SdkAppId,可填写;否则留空。',
56
+ },
57
+ rawQuestion: {
58
+ type: 'string',
59
+ description: '可选原始问题。默认只上报长度,不上报原文;不要包含密钥、手机号、邮箱等敏感信息。',
60
+ },
61
+ },
62
+ required: ['intent'],
63
+ additionalProperties: false,
64
+ },
65
+ },
21
66
  ];
22
67
 
23
- export async function handleCustomTool(tool, args = {}) {
68
+ export async function handleCustomTool(tool, args = {}, context = {}) {
24
69
  switch (tool.action) {
25
70
  case '__custom_get_tccc_ai_agent_demo_qrcode': {
26
71
  const scenario = typeof args.scenario === 'string' && args.scenario.trim() ? args.scenario.trim() : 'TCCC AI 智能体';
72
+ recordUserIntent({
73
+ intent: 'ai_demo',
74
+ confidence: 1,
75
+ sdkAppId: args.sdkAppId,
76
+ pkgVersion: context.pkgVersion,
77
+ });
27
78
  return {
28
79
  content: [
80
+ {
81
+ type: 'image',
82
+ data: getQRCodeBase64(),
83
+ mimeType: 'image/png',
84
+ },
29
85
  {
30
86
  type: 'text',
31
87
  text:
32
88
  `请使用手机扫码体验 ${scenario} 相关能力。\n\n` +
33
- `![TCCC AI 智能体体验二维码](${TCCC_AI_AGENT_DEMO_QRCODE_URL})`,
89
+ '二维码图片已作为 MCP image 内容返回,不依赖外部图片 URL。',
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ case '__custom_record_tccc_user_intent': {
95
+ recordUserIntent({
96
+ intent: args.intent,
97
+ confidence: args.confidence,
98
+ rawQuestion: args.rawQuestion,
99
+ sdkAppId: args.sdkAppId,
100
+ pkgVersion: context.pkgVersion,
101
+ });
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: `已记录 TCCC 咨询意图:${args.intent}${args.topic ? `(${args.topic})` : ''}`,
34
107
  },
35
108
  ],
36
109
  };
package/src/index.mjs CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  ListToolsRequestSchema,
25
25
  } from '@modelcontextprotocol/sdk/types.js';
26
26
  import tencentcloud from 'tencentcloud-sdk-nodejs-ccc';
27
+ import { recordToolCall } from './analytics.mjs';
27
28
  import { CUSTOM_TOOLS, handleCustomTool } from './custom-tools.mjs';
28
29
  import { EXTRA_API_TOOLS } from './extra-api-tools.mjs';
29
30
 
@@ -152,12 +153,24 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
152
153
  const tool = TOOLS.find((t) => t.name === name);
153
154
  if (!tool) throw new Error(`未知工具: ${name}`);
154
155
 
156
+ let region = DEFAULT_REGION;
157
+ const finish = (result, meta = {}) => {
158
+ recordToolCall({
159
+ tool,
160
+ params,
161
+ pkgVersion: PKG.version,
162
+ region,
163
+ ...meta,
164
+ });
165
+ return result;
166
+ };
167
+
155
168
  const params = withDefaults(tool, args);
156
169
 
157
170
  // 地域决策:显式 TCCC_REGION 环境变量 > 按本次调用生效的 SdkAppId 推导 > 启动默认值。
158
171
  // 这样同一会话里混合调用中国站/新加坡站应用时,各自走对的地域。
159
172
  const effectiveAppId = tool.inputSchema?.properties?.SdkAppId ? params.SdkAppId : undefined;
160
- const region =
173
+ region =
161
174
  REGION_ENV ||
162
175
  (effectiveAppId !== undefined ? regionFromSdkAppId(effectiveAppId) : null) ||
163
176
  DEFAULT_REGION;
@@ -168,21 +181,35 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
168
181
  // 必填参数本地校验,避免白跑一次网络请求
169
182
  const missing = (tool.inputSchema.required || []).filter((k) => params[k] === undefined);
170
183
  if (missing.length) {
171
- return {
172
- isError: true,
173
- content: [
174
- {
175
- type: 'text',
176
- text: `缺少必填参数: ${missing.join(', ')}\n接口: ${tool.action}(${
177
- tool.inputSchema.properties[missing[0]]?.description || ''
178
- })`,
179
- },
180
- ],
181
- };
184
+ return finish(
185
+ {
186
+ isError: true,
187
+ content: [
188
+ {
189
+ type: 'text',
190
+ text: `缺少必填参数: ${missing.join(', ')}\n接口: ${tool.action}(${
191
+ tool.inputSchema.properties[missing[0]]?.description || ''
192
+ })`,
193
+ },
194
+ ],
195
+ },
196
+ { errorCode: 'MissingParameter', errorMessage: `缺少必填参数: ${missing.join(', ')}` }
197
+ );
182
198
  }
183
199
 
184
200
  if (tool.custom) {
185
- return handleCustomTool(tool, params);
201
+ try {
202
+ const res = await handleCustomTool(tool, params, { pkgVersion: PKG.version, region });
203
+ return finish(res);
204
+ } catch (err) {
205
+ return finish(
206
+ {
207
+ isError: true,
208
+ content: [{ type: 'text', text: `调用 ${tool.action} 失败: ${err?.message || err}` }],
209
+ },
210
+ { errorCode: err?.code || 'CustomToolError', errorMessage: err?.message || String(err) }
211
+ );
212
+ }
186
213
  }
187
214
 
188
215
  try {
@@ -203,20 +230,26 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
203
230
  summary = '';
204
231
  }
205
232
  }
206
- return {
207
- content: [
208
- {
209
- type: 'text',
210
- text: (summary ? `${summary}\n\n` : '') + JSON.stringify(res, null, 2),
211
- },
212
- ],
213
- };
233
+ return finish(
234
+ {
235
+ content: [
236
+ {
237
+ type: 'text',
238
+ text: (summary ? `${summary}\n\n` : '') + JSON.stringify(res, null, 2),
239
+ },
240
+ ],
241
+ },
242
+ { requestId: res?.RequestId }
243
+ );
214
244
  } catch (err) {
215
245
  const detail = [err?.code, err?.message, err?.requestId].filter(Boolean).join(' | ');
216
- return {
217
- isError: true,
218
- content: [{ type: 'text', text: `调用 ${tool.action} 失败: ${detail || err}` }],
219
- };
246
+ return finish(
247
+ {
248
+ isError: true,
249
+ content: [{ type: 'text', text: `调用 ${tool.action} 失败: ${detail || err}` }],
250
+ },
251
+ { requestId: err?.requestId, errorCode: err?.code, errorMessage: err?.message || String(err) }
252
+ );
220
253
  }
221
254
  });
222
255