@sunnoy/wecom 2.1.0 → 2.2.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.
Files changed (43) hide show
  1. package/README.md +6 -2
  2. package/index.js +2 -0
  3. package/openclaw.plugin.json +3 -0
  4. package/package.json +5 -3
  5. package/skills/wecom-contact-lookup/SKILL.md +167 -0
  6. package/skills/wecom-doc-manager/SKILL.md +106 -0
  7. package/skills/wecom-doc-manager/references/api-create-doc.md +56 -0
  8. package/skills/wecom-doc-manager/references/api-edit-doc-content.md +68 -0
  9. package/skills/wecom-doc-manager/references/api-export-document.md +88 -0
  10. package/skills/wecom-edit-todo/SKILL.md +254 -0
  11. package/skills/wecom-get-todo-detail/SKILL.md +148 -0
  12. package/skills/wecom-get-todo-list/SKILL.md +132 -0
  13. package/skills/wecom-meeting-create/SKILL.md +163 -0
  14. package/skills/wecom-meeting-create/references/example-full.md +30 -0
  15. package/skills/wecom-meeting-create/references/example-reminder.md +46 -0
  16. package/skills/wecom-meeting-create/references/example-security.md +22 -0
  17. package/skills/wecom-meeting-manage/SKILL.md +141 -0
  18. package/skills/wecom-meeting-query/SKILL.md +335 -0
  19. package/skills/wecom-preflight/SKILL.md +103 -0
  20. package/skills/wecom-schedule/SKILL.md +164 -0
  21. package/skills/wecom-schedule/references/api-check-availability.md +56 -0
  22. package/skills/wecom-schedule/references/api-create-schedule.md +38 -0
  23. package/skills/wecom-schedule/references/api-get-schedule-detail.md +81 -0
  24. package/skills/wecom-schedule/references/api-update-schedule.md +30 -0
  25. package/skills/wecom-schedule/references/ref-reminders.md +24 -0
  26. package/skills/wecom-smartsheet-data/SKILL.md +76 -0
  27. package/skills/wecom-smartsheet-data/references/api-get-records.md +61 -0
  28. package/skills/wecom-smartsheet-data/references/cell-value-formats.md +120 -0
  29. package/skills/wecom-smartsheet-schema/SKILL.md +96 -0
  30. package/skills/wecom-smartsheet-schema/references/field-types.md +43 -0
  31. package/wecom/accounts.js +1 -0
  32. package/wecom/callback-inbound.js +133 -33
  33. package/wecom/channel-plugin.js +107 -125
  34. package/wecom/constants.js +83 -3
  35. package/wecom/mcp-config.js +146 -0
  36. package/wecom/mcp-tool.js +660 -0
  37. package/wecom/media-uploader.js +208 -0
  38. package/wecom/openclaw-compat.js +302 -0
  39. package/wecom/reqid-store.js +146 -0
  40. package/wecom/target.js +3 -2
  41. package/wecom/workspace-template.js +107 -21
  42. package/wecom/ws-monitor.js +778 -328
  43. package/image-processor.js +0 -175
@@ -0,0 +1,660 @@
1
+ import { generateReqId } from "@wecom/aibot-node-sdk";
2
+ import { logger } from "../logger.js";
3
+ import { resolveDefaultAccountId } from "./accounts.js";
4
+ import { getOpenclawConfig, streamContext } from "./state.js";
5
+ import { getWsClient } from "./ws-state.js";
6
+
7
+ const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
8
+ const MCP_CONFIG_FETCH_TIMEOUT_MS = 15_000;
9
+ const HTTP_REQUEST_TIMEOUT_MS = 30_000;
10
+ const UNSUPPORTED_BIZ_TYPE_ERRCODE = 846609;
11
+ const PLUGIN_VERSION = "1.0.12";
12
+
13
+ const CACHE_CLEAR_ERROR_CODES = new Set([-32001, -32002, -32003]);
14
+ const BIZ_CACHE_CLEAR_ERROR_CODES = new Set([850002]);
15
+ const GEMINI_UNSUPPORTED_KEYWORDS = new Set([
16
+ "patternProperties",
17
+ "additionalProperties",
18
+ "$schema",
19
+ "$id",
20
+ "$ref",
21
+ "$defs",
22
+ "definitions",
23
+ "examples",
24
+ "minLength",
25
+ "maxLength",
26
+ "minimum",
27
+ "maximum",
28
+ "multipleOf",
29
+ "pattern",
30
+ "format",
31
+ "minItems",
32
+ "maxItems",
33
+ "uniqueItems",
34
+ "minProperties",
35
+ "maxProperties",
36
+ ]);
37
+
38
+ const mcpConfigCache = new Map();
39
+ const mcpSessionCache = new Map();
40
+ const statelessSessions = new Set();
41
+ const inflightInitRequests = new Map();
42
+
43
+ class McpRpcError extends Error {
44
+ constructor(code, message, data) {
45
+ super(message);
46
+ this.code = code;
47
+ this.data = data;
48
+ this.name = "McpRpcError";
49
+ }
50
+ }
51
+
52
+ class McpHttpError extends Error {
53
+ constructor(statusCode, message) {
54
+ super(message);
55
+ this.statusCode = statusCode;
56
+ this.name = "McpHttpError";
57
+ }
58
+ }
59
+
60
+ function createErrcodeError(errcode, errmsg, extra = {}) {
61
+ const error = new Error(errmsg ?? `Error code: ${errcode}`);
62
+ error.errcode = errcode;
63
+ error.errmsg = errmsg;
64
+ Object.assign(error, extra);
65
+ return error;
66
+ }
67
+
68
+ function normalizeUnsupportedBizTypePayload(payload, category) {
69
+ const errcode = Number(payload?.errcode);
70
+ const rawMessage =
71
+ typeof payload?.error === "string"
72
+ ? payload.error
73
+ : typeof payload?.errmsg === "string"
74
+ ? payload.errmsg
75
+ : "";
76
+ if (errcode !== UNSUPPORTED_BIZ_TYPE_ERRCODE && !/unsupported mcp biz type/i.test(rawMessage)) {
77
+ return payload;
78
+ }
79
+
80
+ return {
81
+ error: `WeCom MCP category "${category}" is not enabled for the current bot/runtime.`,
82
+ errcode: UNSUPPORTED_BIZ_TYPE_ERRCODE,
83
+ category,
84
+ unsupportedCategory: true,
85
+ details:
86
+ rawMessage ||
87
+ `unsupported mcp biz type for category "${category}"`,
88
+ next_action:
89
+ `Stop retrying category "${category}" with alternate read/list/find paths. ` +
90
+ `Ask an administrator to enable the "${category}" MCP category for this bot.`,
91
+ };
92
+ }
93
+
94
+ function normalizeBizResult(category, result) {
95
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
96
+ return result;
97
+ }
98
+ return normalizeUnsupportedBizTypePayload(result, category);
99
+ }
100
+
101
+ function withTimeout(promise, timeoutMs, message) {
102
+ if (!timeoutMs || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
103
+ return promise;
104
+ }
105
+
106
+ let timer = null;
107
+ const timeout = new Promise((_, reject) => {
108
+ timer = setTimeout(() => reject(new Error(message ?? `Timed out after ${timeoutMs}ms`)), timeoutMs);
109
+ });
110
+
111
+ promise.catch(() => {});
112
+
113
+ return Promise.race([promise, timeout]).finally(() => {
114
+ if (timer) {
115
+ clearTimeout(timer);
116
+ }
117
+ });
118
+ }
119
+
120
+ function buildCacheKey(accountId, category) {
121
+ return `${accountId}:${category}`;
122
+ }
123
+
124
+ function resolveCurrentAccountId() {
125
+ const contextualAccountId = streamContext.getStore()?.accountId;
126
+ if (contextualAccountId) {
127
+ return contextualAccountId;
128
+ }
129
+ return resolveDefaultAccountId(getOpenclawConfig());
130
+ }
131
+
132
+ function getConnectedWsClient(accountId) {
133
+ const wsClient = getWsClient(accountId);
134
+ if (!wsClient?.isConnected) {
135
+ throw new Error(`WS client is not connected for account ${accountId}`);
136
+ }
137
+ return wsClient;
138
+ }
139
+
140
+ async function fetchMcpConfig(accountId, category) {
141
+ const wsClient = getConnectedWsClient(accountId);
142
+ const reqId = generateReqId("mcp_config");
143
+ const response = await withTimeout(
144
+ wsClient.reply(
145
+ { headers: { req_id: reqId } },
146
+ { biz_type: category, plugin_version: PLUGIN_VERSION },
147
+ MCP_GET_CONFIG_CMD,
148
+ ),
149
+ MCP_CONFIG_FETCH_TIMEOUT_MS,
150
+ `MCP config fetch for "${category}" timed out after ${MCP_CONFIG_FETCH_TIMEOUT_MS}ms`,
151
+ );
152
+
153
+ if (response?.errcode !== undefined && response.errcode !== 0) {
154
+ throw createErrcodeError(
155
+ response.errcode,
156
+ response.errmsg ?? `MCP config request failed for category "${category}"`,
157
+ { category },
158
+ );
159
+ }
160
+
161
+ const body = response?.body;
162
+ if (!body?.url) {
163
+ throw new Error(`MCP config response missing url field for category "${category}"`);
164
+ }
165
+
166
+ return body;
167
+ }
168
+
169
+ async function getMcpConfig(accountId, category) {
170
+ const key = buildCacheKey(accountId, category);
171
+ const cached = mcpConfigCache.get(key);
172
+ if (cached) {
173
+ return cached;
174
+ }
175
+
176
+ const config = await fetchMcpConfig(accountId, category);
177
+ mcpConfigCache.set(key, config);
178
+ return config;
179
+ }
180
+
181
+ async function getMcpUrl(accountId, category) {
182
+ const config = await getMcpConfig(accountId, category);
183
+ return config.url;
184
+ }
185
+
186
+ async function sendRawJsonRpc(url, session, body) {
187
+ const controller = new AbortController();
188
+ const timeoutId = setTimeout(() => controller.abort(), HTTP_REQUEST_TIMEOUT_MS);
189
+ const headers = {
190
+ "Content-Type": "application/json",
191
+ Accept: "application/json, text/event-stream",
192
+ };
193
+
194
+ if (session.sessionId) {
195
+ headers["Mcp-Session-Id"] = session.sessionId;
196
+ }
197
+
198
+ let response;
199
+ try {
200
+ response = await fetch(url, {
201
+ method: "POST",
202
+ headers,
203
+ body: JSON.stringify(body),
204
+ signal: controller.signal,
205
+ });
206
+ } catch (error) {
207
+ if (error instanceof DOMException && error.name === "AbortError") {
208
+ throw new Error(`MCP request timed out after ${HTTP_REQUEST_TIMEOUT_MS}ms`);
209
+ }
210
+ throw new Error(`MCP network request failed: ${error instanceof Error ? error.message : String(error)}`);
211
+ } finally {
212
+ clearTimeout(timeoutId);
213
+ }
214
+
215
+ const newSessionId = response.headers.get("mcp-session-id");
216
+ if (!response.ok) {
217
+ throw new McpHttpError(response.status, `MCP HTTP request failed: ${response.status} ${response.statusText}`);
218
+ }
219
+
220
+ const contentLength = response.headers.get("content-length");
221
+ if (response.status === 204 || contentLength === "0") {
222
+ return { rpcResult: undefined, newSessionId };
223
+ }
224
+
225
+ const contentType = response.headers.get("content-type") ?? "";
226
+ if (contentType.includes("text/event-stream")) {
227
+ return {
228
+ rpcResult: await parseSseResponse(response),
229
+ newSessionId,
230
+ };
231
+ }
232
+
233
+ const text = await response.text();
234
+ if (!text.trim()) {
235
+ return { rpcResult: undefined, newSessionId };
236
+ }
237
+
238
+ const rpc = JSON.parse(text);
239
+ if (rpc.error) {
240
+ throw new McpRpcError(rpc.error.code, `MCP RPC error [${rpc.error.code}]: ${rpc.error.message}`, rpc.error.data);
241
+ }
242
+
243
+ return { rpcResult: rpc.result, newSessionId };
244
+ }
245
+
246
+ async function initializeSession(accountId, category, url) {
247
+ const session = { sessionId: null, initialized: false, stateless: false };
248
+ const initializeBody = {
249
+ jsonrpc: "2.0",
250
+ id: generateReqId("mcp_init"),
251
+ method: "initialize",
252
+ params: {
253
+ protocolVersion: "2025-03-26",
254
+ capabilities: {},
255
+ clientInfo: { name: "wecom_mcp", version: "1.0.0" },
256
+ },
257
+ };
258
+
259
+ const { newSessionId: initSessionId } = await sendRawJsonRpc(url, session, initializeBody);
260
+ if (initSessionId) {
261
+ session.sessionId = initSessionId;
262
+ }
263
+
264
+ const key = buildCacheKey(accountId, category);
265
+ if (!session.sessionId) {
266
+ session.stateless = true;
267
+ session.initialized = true;
268
+ statelessSessions.add(key);
269
+ mcpSessionCache.set(key, session);
270
+ return session;
271
+ }
272
+
273
+ const { newSessionId: notifySessionId } = await sendRawJsonRpc(url, session, {
274
+ jsonrpc: "2.0",
275
+ method: "notifications/initialized",
276
+ });
277
+
278
+ if (notifySessionId) {
279
+ session.sessionId = notifySessionId;
280
+ }
281
+
282
+ session.initialized = true;
283
+ mcpSessionCache.set(key, session);
284
+ return session;
285
+ }
286
+
287
+ async function getOrCreateSession(accountId, category, url) {
288
+ const key = buildCacheKey(accountId, category);
289
+ if (statelessSessions.has(key)) {
290
+ const cached = mcpSessionCache.get(key);
291
+ if (cached) {
292
+ return cached;
293
+ }
294
+ }
295
+
296
+ const cached = mcpSessionCache.get(key);
297
+ if (cached?.initialized) {
298
+ return cached;
299
+ }
300
+
301
+ const inflight = inflightInitRequests.get(key);
302
+ if (inflight) {
303
+ return inflight;
304
+ }
305
+
306
+ const request = initializeSession(accountId, category, url).finally(() => {
307
+ inflightInitRequests.delete(key);
308
+ });
309
+ inflightInitRequests.set(key, request);
310
+ return request;
311
+ }
312
+
313
+ async function rebuildSession(accountId, category, url) {
314
+ const key = buildCacheKey(accountId, category);
315
+ const inflight = inflightInitRequests.get(key);
316
+ if (inflight) {
317
+ return inflight;
318
+ }
319
+
320
+ const request = initializeSession(accountId, category, url).finally(() => {
321
+ inflightInitRequests.delete(key);
322
+ });
323
+ inflightInitRequests.set(key, request);
324
+ return request;
325
+ }
326
+
327
+ function clearCategoryCache(accountId, category) {
328
+ const key = buildCacheKey(accountId, category);
329
+ mcpConfigCache.delete(key);
330
+ mcpSessionCache.delete(key);
331
+ statelessSessions.delete(key);
332
+ inflightInitRequests.delete(key);
333
+ }
334
+
335
+ async function sendJsonRpc(accountId, category, method, params) {
336
+ const url = await getMcpUrl(accountId, category);
337
+ const body = {
338
+ jsonrpc: "2.0",
339
+ id: generateReqId("mcp_rpc"),
340
+ method,
341
+ ...(params !== undefined ? { params } : {}),
342
+ };
343
+
344
+ let session = await getOrCreateSession(accountId, category, url);
345
+ try {
346
+ const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body);
347
+ if (newSessionId) {
348
+ session.sessionId = newSessionId;
349
+ }
350
+ return rpcResult;
351
+ } catch (error) {
352
+ if (error instanceof McpRpcError && CACHE_CLEAR_ERROR_CODES.has(error.code)) {
353
+ clearCategoryCache(accountId, category);
354
+ }
355
+
356
+ if (session.stateless) {
357
+ throw error;
358
+ }
359
+
360
+ if (error instanceof McpHttpError && error.statusCode === 404) {
361
+ const key = buildCacheKey(accountId, category);
362
+ mcpSessionCache.delete(key);
363
+ session = await rebuildSession(accountId, category, url);
364
+ const { rpcResult, newSessionId } = await sendRawJsonRpc(url, session, body);
365
+ if (newSessionId) {
366
+ session.sessionId = newSessionId;
367
+ }
368
+ return rpcResult;
369
+ }
370
+
371
+ logger.error(`[wecom_mcp] RPC failed for ${accountId}/${category}/${method}: ${error.message}`);
372
+ throw error;
373
+ }
374
+ }
375
+
376
+ async function parseSseResponse(response) {
377
+ const text = await response.text();
378
+ const lines = text.split("\n");
379
+ let currentDataParts = [];
380
+ let lastEventData = "";
381
+
382
+ for (const line of lines) {
383
+ if (line.startsWith("data: ")) {
384
+ currentDataParts.push(line.slice(6));
385
+ continue;
386
+ }
387
+ if (line.startsWith("data:")) {
388
+ currentDataParts.push(line.slice(5));
389
+ continue;
390
+ }
391
+ if (line.trim() === "" && currentDataParts.length > 0) {
392
+ lastEventData = currentDataParts.join("\n").trim();
393
+ currentDataParts = [];
394
+ }
395
+ }
396
+
397
+ if (currentDataParts.length > 0) {
398
+ lastEventData = currentDataParts.join("\n").trim();
399
+ }
400
+
401
+ if (!lastEventData) {
402
+ throw new Error("SSE response did not contain usable data");
403
+ }
404
+
405
+ try {
406
+ const rpc = JSON.parse(lastEventData);
407
+ if (rpc.error) {
408
+ throw new McpRpcError(rpc.error.code, `MCP RPC error [${rpc.error.code}]: ${rpc.error.message}`, rpc.error.data);
409
+ }
410
+ return rpc.result;
411
+ } catch (error) {
412
+ if (error instanceof SyntaxError) {
413
+ throw new Error(`Failed to parse SSE response: ${lastEventData.slice(0, 200)}`);
414
+ }
415
+ throw error;
416
+ }
417
+ }
418
+
419
+ function cleanSchemaForGemini(schema) {
420
+ if (!schema || typeof schema !== "object") {
421
+ return schema;
422
+ }
423
+ if (Array.isArray(schema)) {
424
+ return schema.map(cleanSchemaForGemini);
425
+ }
426
+
427
+ const defs = {
428
+ ...(schema.$defs && typeof schema.$defs === "object" ? schema.$defs : {}),
429
+ ...(schema.definitions && typeof schema.definitions === "object" ? schema.definitions : {}),
430
+ };
431
+
432
+ return cleanWithDefs(schema, defs, new Set());
433
+ }
434
+
435
+ function cleanWithDefs(schema, defs, refStack) {
436
+ if (!schema || typeof schema !== "object") {
437
+ return schema;
438
+ }
439
+ if (Array.isArray(schema)) {
440
+ return schema.map((item) => cleanWithDefs(item, defs, refStack));
441
+ }
442
+
443
+ if (schema.$defs && typeof schema.$defs === "object") {
444
+ Object.assign(defs, schema.$defs);
445
+ }
446
+ if (schema.definitions && typeof schema.definitions === "object") {
447
+ Object.assign(defs, schema.definitions);
448
+ }
449
+
450
+ if (typeof schema.$ref === "string") {
451
+ const ref = schema.$ref;
452
+ if (refStack.has(ref)) {
453
+ return {};
454
+ }
455
+ const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
456
+ if (match?.[1] && defs[match[1]]) {
457
+ const nextStack = new Set(refStack);
458
+ nextStack.add(ref);
459
+ return cleanWithDefs(defs[match[1]], defs, nextStack);
460
+ }
461
+ return {};
462
+ }
463
+
464
+ const cleaned = {};
465
+ for (const [key, value] of Object.entries(schema)) {
466
+ if (GEMINI_UNSUPPORTED_KEYWORDS.has(key)) {
467
+ continue;
468
+ }
469
+ if (key === "const") {
470
+ cleaned.enum = [value];
471
+ continue;
472
+ }
473
+ if (key === "properties" && value && typeof value === "object" && !Array.isArray(value)) {
474
+ cleaned[key] = Object.fromEntries(
475
+ Object.entries(value).map(([entryKey, entryValue]) => [entryKey, cleanWithDefs(entryValue, defs, refStack)]),
476
+ );
477
+ continue;
478
+ }
479
+ if (key === "items" && value) {
480
+ cleaned[key] = Array.isArray(value)
481
+ ? value.map((item) => cleanWithDefs(item, defs, refStack))
482
+ : cleanWithDefs(value, defs, refStack);
483
+ continue;
484
+ }
485
+ if ((key === "anyOf" || key === "oneOf" || key === "allOf") && Array.isArray(value)) {
486
+ const nonNullVariants = value.filter((variant) => {
487
+ if (!variant || typeof variant !== "object") {
488
+ return true;
489
+ }
490
+ return variant.type !== "null";
491
+ });
492
+ if (nonNullVariants.length === 1) {
493
+ const single = cleanWithDefs(nonNullVariants[0], defs, refStack);
494
+ if (single && typeof single === "object" && !Array.isArray(single)) {
495
+ Object.assign(cleaned, single);
496
+ }
497
+ } else {
498
+ cleaned[key] = nonNullVariants.map((variant) => cleanWithDefs(variant, defs, refStack));
499
+ }
500
+ continue;
501
+ }
502
+
503
+ cleaned[key] = value;
504
+ }
505
+
506
+ return cleaned;
507
+ }
508
+
509
+ function parseArgs(args) {
510
+ if (!args) {
511
+ return {};
512
+ }
513
+ if (typeof args === "object") {
514
+ return args;
515
+ }
516
+ try {
517
+ return JSON.parse(args);
518
+ } catch (error) {
519
+ const detail = error instanceof SyntaxError ? error.message : String(error);
520
+ throw new Error(`args is not valid JSON: ${args} (${detail})`);
521
+ }
522
+ }
523
+
524
+ const textResult = (data) => ({
525
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
526
+ });
527
+
528
+ const errorResult = (error, category) => {
529
+ if (error && typeof error === "object" && "errcode" in error) {
530
+ return textResult(normalizeUnsupportedBizTypePayload({
531
+ error: error.errmsg ?? `Error code: ${error.errcode}`,
532
+ errcode: error.errcode,
533
+ }, error.category ?? category));
534
+ }
535
+ return textResult({
536
+ error: error instanceof Error ? error.message : String(error),
537
+ });
538
+ };
539
+
540
+ function checkBizErrorAndClearCache(accountId, category, result) {
541
+ if (!result || typeof result !== "object" || !Array.isArray(result.content)) {
542
+ return;
543
+ }
544
+
545
+ for (const item of result.content) {
546
+ if (item?.type !== "text" || !item.text) {
547
+ continue;
548
+ }
549
+ try {
550
+ const parsed = JSON.parse(item.text);
551
+ if (typeof parsed.errcode === "number" && BIZ_CACHE_CLEAR_ERROR_CODES.has(parsed.errcode)) {
552
+ clearCategoryCache(accountId, category);
553
+ return;
554
+ }
555
+ } catch {
556
+ // Ignore non-JSON text payloads.
557
+ }
558
+ }
559
+ }
560
+
561
+ async function handleList(accountId, category) {
562
+ const result = await sendJsonRpc(accountId, category, "tools/list");
563
+ const normalized = normalizeBizResult(category, result);
564
+ if (normalized !== result) {
565
+ return normalized;
566
+ }
567
+ const tools = result?.tools ?? [];
568
+ if (tools.length === 0) {
569
+ return { message: `No tools available under category "${category}"`, tools: [] };
570
+ }
571
+
572
+ return {
573
+ accountId,
574
+ category,
575
+ count: tools.length,
576
+ tools: tools.map((tool) => ({
577
+ name: tool.name,
578
+ description: tool.description ?? "",
579
+ inputSchema: tool.inputSchema ? cleanSchemaForGemini(tool.inputSchema) : undefined,
580
+ })),
581
+ };
582
+ }
583
+
584
+ async function handleCall(accountId, category, method, args) {
585
+ const result = await sendJsonRpc(accountId, category, "tools/call", {
586
+ name: method,
587
+ arguments: args,
588
+ });
589
+ checkBizErrorAndClearCache(accountId, category, result);
590
+ return normalizeBizResult(category, result);
591
+ }
592
+
593
+ export function createWeComMcpTool() {
594
+ return {
595
+ name: "wecom_mcp",
596
+ label: "WeCom MCP Tool",
597
+ description: [
598
+ "Calls WeCom MCP servers over Streamable HTTP.",
599
+ "Supported actions:",
600
+ " - list: list tools under a category",
601
+ " - call: call one tool under a category",
602
+ "",
603
+ "Examples:",
604
+ " wecom_mcp list contact",
605
+ " wecom_mcp call schedule create_schedule '{\"schedule\": {...}}'",
606
+ ].join("\n"),
607
+ parameters: {
608
+ type: "object",
609
+ properties: {
610
+ action: {
611
+ type: "string",
612
+ enum: ["list", "call"],
613
+ description: "Operation type.",
614
+ },
615
+ category: {
616
+ type: "string",
617
+ description: "WeCom MCP category, such as doc, contact, schedule, todo, meeting.",
618
+ },
619
+ method: {
620
+ type: "string",
621
+ description: "Tool method name when action=call.",
622
+ },
623
+ args: {
624
+ type: ["string", "object"],
625
+ description: "Tool arguments as an object or JSON string when action=call.",
626
+ },
627
+ },
628
+ required: ["action", "category"],
629
+ },
630
+ async execute(_toolCallId, params) {
631
+ const accountId = resolveCurrentAccountId();
632
+ try {
633
+ switch (params.action) {
634
+ case "list":
635
+ return textResult(await handleList(accountId, params.category));
636
+ case "call":
637
+ if (!params.method) {
638
+ return textResult({ error: "method is required when action=call" });
639
+ }
640
+ return textResult(await handleCall(accountId, params.category, params.method, parseArgs(params.args)));
641
+ default:
642
+ return textResult({ error: `Unknown action: ${String(params.action)}` });
643
+ }
644
+ } catch (error) {
645
+ return errorResult(error, params.category);
646
+ }
647
+ },
648
+ };
649
+ }
650
+
651
+ export const mcpToolTesting = {
652
+ cleanSchemaForGemini,
653
+ parseArgs,
654
+ resetCaches() {
655
+ mcpConfigCache.clear();
656
+ mcpSessionCache.clear();
657
+ statelessSessions.clear();
658
+ inflightInitRequests.clear();
659
+ },
660
+ };