@tencentcloud/tccc-mcp-server 0.0.1

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/src/index.mjs ADDED
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TCCC MCP Server
4
+ *
5
+ * 把腾讯云联络中心(TCCC)API 3.0 的 84 个接口暴露为 MCP 工具。
6
+ * 工具定义由 src/generate.mjs 从官方 SDK 类型定义自动生成,SDK 升级后重跑即可。
7
+ *
8
+ * 环境变量:
9
+ * TENCENTCLOUD_SECRET_ID 必填
10
+ * TENCENTCLOUD_SECRET_KEY 必填
11
+ * TCCC_SDK_APP_ID 可选,缺省的 SdkAppId(多数接口必填,配了就不用每次传)
12
+ * TCCC_REGION 可选,显式指定则始终使用;否则按 SdkAppId 号段自动推导:
13
+ * [20000000, 30000000) 与 [1720000000, 1730000000) 为新加坡站
14
+ * → ap-singapore,其余为中国站 → ap-guangzhou
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
20
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
+ import {
22
+ CallToolRequestSchema,
23
+ ListToolsRequestSchema,
24
+ } from '@modelcontextprotocol/sdk/types.js';
25
+ import tencentcloud from 'tencentcloud-sdk-nodejs-ccc';
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+ const TOOLS = JSON.parse(fs.readFileSync(path.join(__dirname, 'tools.json'), 'utf8'));
29
+ const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
30
+
31
+ // ---- CLI 参数:只做 --help / --version,其余一律走 stdio JSON-RPC ----
32
+ const argv = process.argv.slice(2);
33
+ if (argv.includes('--help') || argv.includes('-h')) {
34
+ process.stdout.write(`${PKG.name} v${PKG.version}
35
+
36
+ 把腾讯云联络中心(TCCC)API 3.0 的 ${TOOLS.length} 个接口暴露为 MCP 工具。
37
+
38
+ 环境变量:
39
+ TENCENTCLOUD_SECRET_ID 必填 腾讯云 SecretId(AKID 开头)
40
+ TENCENTCLOUD_SECRET_KEY 必填 腾讯云 SecretKey
41
+ TCCC_SDK_APP_ID 选填 缺省 SdkAppId,配了就不用每次调用都传
42
+ TCCC_REGION 选填 显式指定则始终使用;未指定时按 SdkAppId 号段
43
+ 自动推导(新加坡站号段 → ap-singapore,
44
+ 其余 → ap-guangzhou)
45
+
46
+ 用法:
47
+ npx -y ${PKG.name}
48
+
49
+ 获取密钥:https://console.cloud.tencent.com/cam/capi
50
+ 获取 SdkAppId:https://console.cloud.tencent.com/ccc
51
+ `);
52
+ process.exit(0);
53
+ }
54
+ if (argv.includes('--version') || argv.includes('-v')) {
55
+ process.stdout.write(`${PKG.version}\n`);
56
+ process.exit(0);
57
+ }
58
+
59
+ const SECRET_ID = process.env.TENCENTCLOUD_SECRET_ID;
60
+ const SECRET_KEY = process.env.TENCENTCLOUD_SECRET_KEY;
61
+ const SDK_APP_ID = process.env.TCCC_SDK_APP_ID;
62
+ const REGION_ENV = process.env.TCCC_REGION;
63
+
64
+ // 按 SdkAppId 号段推导地域:[20000000, 30000000) 与 [1720000000, 1730000000)
65
+ // 是新加坡站 → ap-singapore;其余号段是中国站 → ap-guangzhou。
66
+ const SINGAPORE_RANGES = [
67
+ [20000000, 30000000],
68
+ [1720000000, 1730000000],
69
+ ];
70
+ function regionFromSdkAppId(id) {
71
+ if (id === null || id === undefined || id === '') return null;
72
+ const n = Number(id);
73
+ // Number(null)/Number('')/Number(false) 均为 0,必须排除,否则会误判为中国站
74
+ if (!Number.isFinite(n) || n <= 0) return null;
75
+ if (SINGAPORE_RANGES.some(([lo, hi]) => n >= lo && n < hi)) return 'ap-singapore';
76
+ return 'ap-guangzhou';
77
+ }
78
+
79
+ const DEFAULT_REGION = REGION_ENV || (SDK_APP_ID ? regionFromSdkAppId(SDK_APP_ID) : null) || 'ap-guangzhou';
80
+
81
+ // 客户端按 region 缓存:SdkAppId 是每次调用可变的参数,
82
+ // 不同应用的站区不同,必须按本次调用实际生效的 SdkAppId 动态选 region。
83
+ const CccClient = tencentcloud.ccc.v20200210.Client;
84
+ const clientCache = new Map();
85
+ function getClient(region) {
86
+ if (!clientCache.has(region)) {
87
+ clientCache.set(region, new CccClient({
88
+ credential: { secretId: SECRET_ID, secretKey: SECRET_KEY },
89
+ region,
90
+ profile: { httpProfile: { endpoint: 'ccc.tencentcloudapi.com', reqTimeout: 30 } },
91
+ }));
92
+ }
93
+ return clientCache.get(region);
94
+ }
95
+
96
+ if (!SECRET_ID || !SECRET_KEY) {
97
+ const miss = [!SECRET_ID && 'TENCENTCLOUD_SECRET_ID', !SECRET_KEY && 'TENCENTCLOUD_SECRET_KEY'].filter(Boolean);
98
+ console.error(
99
+ `[tccc-mcp] 启动失败:缺少环境变量 ${miss.join(' / ')}\n` +
100
+ `\n` +
101
+ `请在 MCP 配置的 env 里补上,例如:\n` +
102
+ ` "env": {\n` +
103
+ ` "TENCENTCLOUD_SECRET_ID": "AKID...",\n` +
104
+ ` "TENCENTCLOUD_SECRET_KEY": "...",\n` +
105
+ ` "TCCC_SDK_APP_ID": "1400000000"\n` +
106
+ ` }\n` +
107
+ `\n` +
108
+ `密钥获取:https://console.cloud.tencent.com/cam/capi\n` +
109
+ `注意:SecretId 以 AKID 开头,SecretKey 是 32 位随机串,两者不要填反。`
110
+ );
111
+ process.exit(1);
112
+ }
113
+
114
+ // SdkAppId 未显式传入时,用环境变量兜底,省得每个调用都问一遍。
115
+ // 必须校验该接口的 schema 里真的有 SdkAppId 字段——像 DescribeCCCBuyInfoList 这种
116
+ // 只有 SdkAppIds(复数)的接口,注入 SdkAppId 会被服务端拒绝(UnknownParameter)。
117
+ function withDefaults(tool, args) {
118
+ const out = { ...args };
119
+ const hasSdkAppId = !!tool.inputSchema?.properties?.SdkAppId;
120
+ if (hasSdkAppId && out.SdkAppId === undefined && SDK_APP_ID) {
121
+ out.SdkAppId = Number(SDK_APP_ID);
122
+ }
123
+ return out;
124
+ }
125
+
126
+ const server = new Server(
127
+ { name: PKG.name, version: PKG.version },
128
+ { capabilities: { tools: {} } }
129
+ );
130
+
131
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
132
+ tools: TOOLS.map((t) => ({
133
+ name: t.name,
134
+ description: t.description,
135
+ inputSchema: t.inputSchema,
136
+ })),
137
+ }));
138
+
139
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
140
+ const { name, arguments: args = {} } = req.params;
141
+ const tool = TOOLS.find((t) => t.name === name);
142
+ if (!tool) throw new Error(`未知工具: ${name}`);
143
+
144
+ const params = withDefaults(tool, args);
145
+
146
+ // 地域决策:显式 TCCC_REGION 环境变量 > 按本次调用生效的 SdkAppId 推导 > 启动默认值。
147
+ // 这样同一会话里混合调用中国站/新加坡站应用时,各自走对的地域。
148
+ const effectiveAppId = tool.inputSchema?.properties?.SdkAppId ? params.SdkAppId : undefined;
149
+ const region =
150
+ REGION_ENV ||
151
+ (effectiveAppId !== undefined ? regionFromSdkAppId(effectiveAppId) : null) ||
152
+ DEFAULT_REGION;
153
+ if (process.env.TCCC_DEBUG) {
154
+ console.error(`[tccc-mcp] ${tool.action} region=${region} SdkAppId=${effectiveAppId ?? '(无)'}`);
155
+ }
156
+
157
+ // 必填参数本地校验,避免白跑一次网络请求
158
+ const missing = (tool.inputSchema.required || []).filter((k) => params[k] === undefined);
159
+ if (missing.length) {
160
+ return {
161
+ isError: true,
162
+ content: [
163
+ {
164
+ type: 'text',
165
+ text: `缺少必填参数: ${missing.join(', ')}\n接口: ${tool.action}(${
166
+ tool.inputSchema.properties[missing[0]]?.description || ''
167
+ })`,
168
+ },
169
+ ],
170
+ };
171
+ }
172
+
173
+ try {
174
+ const res = await getClient(region)[tool.action](params);
175
+ return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }] };
176
+ } catch (err) {
177
+ const detail = [err?.code, err?.message, err?.requestId].filter(Boolean).join(' | ');
178
+ return {
179
+ isError: true,
180
+ content: [{ type: 'text', text: `调用 ${tool.action} 失败: ${detail || err}` }],
181
+ };
182
+ }
183
+ });
184
+
185
+ // 兜住所有未处理异常:stdio 下进程一崩,客户端只会看到"连接已关闭",无从排查。
186
+ for (const sig of ['uncaughtException', 'unhandledRejection']) {
187
+ process.on(sig, (err) => {
188
+ console.error(`[tccc-mcp] ${sig}:`, err?.stack || err);
189
+ });
190
+ }
191
+
192
+ const transport = new StdioServerTransport();
193
+ await server.connect(transport);
194
+ console.error(
195
+ `[tccc-mcp] 就绪 v${PKG.version}:${TOOLS.length} 个工具,region=${
196
+ REGION_ENV
197
+ ? `${REGION_ENV}(显式指定,覆盖自动推导)`
198
+ : SDK_APP_ID
199
+ ? `${DEFAULT_REGION}(按 TCCC_SDK_APP_ID=${SDK_APP_ID} 推导,调用时若传入其他号段的 SdkAppId 会自动切换)`
200
+ : `${DEFAULT_REGION}(未配置 TCCC_SDK_APP_ID 时的默认值,调用时按实际 SdkAppId 推导)`
201
+ }${SDK_APP_ID ? '' : '(未配置 TCCC_SDK_APP_ID,调用时需显式传 SdkAppId)'}`
202
+ );