@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 +6 -6
- package/package.json +1 -1
- package/src/analytics.mjs +266 -0
- package/src/assets/tccc-ai-agent-demo-qrcode.png +0 -0
- package/src/custom-tools.mjs +78 -5
- package/src/index.mjs +58 -25
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 +
|
|
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
|
|
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 个接口 +
|
|
264
|
+
## 能力概览(87 个接口 + 2 个辅助工具)
|
|
265
265
|
|
|
266
266
|
| 分类 | 数量 | 代表接口 |
|
|
267
267
|
|---|---|---|
|
|
268
|
-
| AI 体验入口 | 1 | `get_tccc_ai_agent_demo_qrcode`
|
|
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
|
@@ -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
|
+
}
|
|
Binary file
|
package/src/custom-tools.mjs
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
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)
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
|