slack-max-api-mcp 1.0.2

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.
@@ -0,0 +1,718 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
6
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
7
+ const { z } = require("zod");
8
+
9
+ const SERVER_NAME = "slack-max-api-mcp";
10
+ const SERVER_VERSION = "2.0.0";
11
+
12
+ const SLACK_API_BASE_URL = process.env.SLACK_API_BASE_URL || "https://slack.com/api";
13
+
14
+ const CATALOG_PATH =
15
+ process.env.SLACK_CATALOG_PATH || path.join(process.cwd(), "data", "slack-catalog.json");
16
+ const METHOD_TOOL_PREFIX = process.env.SLACK_METHOD_TOOL_PREFIX || "slack_method";
17
+ const ENABLE_METHOD_TOOLS = process.env.SLACK_ENABLE_METHOD_TOOLS !== "false";
18
+ const MAX_METHOD_TOOLS = Number(process.env.SLACK_MAX_METHOD_TOOLS || 0);
19
+ const ENV_EXAMPLE_PATH = path.join(process.cwd(), ".env.example");
20
+
21
+ function parseSimpleEnvFile(filePath) {
22
+ if (!fs.existsSync(filePath)) return {};
23
+
24
+ const out = {};
25
+ const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
26
+ for (const line of lines) {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith("#")) continue;
29
+ const eqIndex = trimmed.indexOf("=");
30
+ if (eqIndex <= 0) continue;
31
+ const key = trimmed.slice(0, eqIndex).trim();
32
+ const value = trimmed.slice(eqIndex + 1).trim();
33
+ if (!key) continue;
34
+ out[key] = value;
35
+ }
36
+ return out;
37
+ }
38
+
39
+ const ENV_EXAMPLE_VALUES = parseSimpleEnvFile(ENV_EXAMPLE_PATH);
40
+ const FIXED_BOT_TOKEN = ENV_EXAMPLE_VALUES.SLACK_BOT_TOKEN || "";
41
+ const FIXED_USER_TOKEN = ENV_EXAMPLE_VALUES.SLACK_USER_TOKEN || "";
42
+ const FIXED_GENERIC_TOKEN = ENV_EXAMPLE_VALUES.SLACK_TOKEN || "";
43
+ const DEFAULT_SLACK_TOKEN =
44
+ process.env.SLACK_BOT_TOKEN ||
45
+ process.env.SLACK_USER_TOKEN ||
46
+ process.env.SLACK_TOKEN ||
47
+ FIXED_BOT_TOKEN ||
48
+ FIXED_USER_TOKEN ||
49
+ FIXED_GENERIC_TOKEN;
50
+
51
+ function requireSlackToken(tokenOverride) {
52
+ const token = tokenOverride || DEFAULT_SLACK_TOKEN;
53
+ if (!token) {
54
+ throw new Error(
55
+ "Slack token is missing. Set SLACK_BOT_TOKEN/SLACK_USER_TOKEN/SLACK_TOKEN or fill .env.example."
56
+ );
57
+ }
58
+ return token;
59
+ }
60
+
61
+ function toUrlEncodedBody(params = {}) {
62
+ const search = new URLSearchParams();
63
+ for (const [key, value] of Object.entries(params)) {
64
+ if (value === undefined || value === null) continue;
65
+
66
+ if (Array.isArray(value) || (typeof value === "object" && !(value instanceof Date))) {
67
+ search.append(key, JSON.stringify(value));
68
+ continue;
69
+ }
70
+
71
+ search.append(key, String(value));
72
+ }
73
+ return search.toString();
74
+ }
75
+
76
+ function parseJsonMaybe(value) {
77
+ if (typeof value !== "string") return value;
78
+ try {
79
+ return JSON.parse(value);
80
+ } catch {
81
+ return value;
82
+ }
83
+ }
84
+
85
+ function toRecordObject(value) {
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
87
+ return value;
88
+ }
89
+
90
+ async function callSlackApi(method, params = {}, tokenOverride) {
91
+ const token = requireSlackToken(tokenOverride);
92
+ const url = `${SLACK_API_BASE_URL.replace(/\/+$/, "")}/${method}`;
93
+
94
+ const response = await fetch(url, {
95
+ method: "POST",
96
+ headers: {
97
+ Authorization: `Bearer ${token}`,
98
+ "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
99
+ },
100
+ body: toUrlEncodedBody(params),
101
+ });
102
+
103
+ const text = await response.text();
104
+ let data;
105
+ try {
106
+ data = JSON.parse(text);
107
+ } catch {
108
+ throw new Error(`Slack API returned non-JSON for ${method} (HTTP ${response.status}).`);
109
+ }
110
+
111
+ if (!response.ok) {
112
+ throw new Error(`Slack API HTTP ${response.status} for ${method}: ${data.error || "unknown_error"}`);
113
+ }
114
+
115
+ if (!data.ok) {
116
+ throw new Error(`Slack method ${method} failed: ${data.error || "unknown_error"}`);
117
+ }
118
+
119
+ return data;
120
+ }
121
+
122
+ function successResult(payload) {
123
+ return {
124
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
125
+ structuredContent: payload,
126
+ };
127
+ }
128
+
129
+ function errorResult(error) {
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ return {
132
+ isError: true,
133
+ content: [{ type: "text", text: message }],
134
+ };
135
+ }
136
+
137
+ async function safeToolRun(executor) {
138
+ try {
139
+ const result = await executor();
140
+ return successResult(result);
141
+ } catch (error) {
142
+ return errorResult(error);
143
+ }
144
+ }
145
+
146
+ function loadCatalog() {
147
+ if (!fs.existsSync(CATALOG_PATH)) {
148
+ return { methods: [], scopes: [], totals: {} };
149
+ }
150
+
151
+ try {
152
+ const raw = fs.readFileSync(CATALOG_PATH, "utf8");
153
+ const parsed = JSON.parse(raw);
154
+ return parsed;
155
+ } catch (error) {
156
+ throw new Error(`Failed to load catalog at ${CATALOG_PATH}: ${error}`);
157
+ }
158
+ }
159
+
160
+ function toolNameFromMethod(method, usedNames) {
161
+ const base = `${METHOD_TOOL_PREFIX}_${method.replace(/[^a-zA-Z0-9]/g, "_")}`;
162
+ if (!usedNames.has(base)) {
163
+ usedNames.add(base);
164
+ return base;
165
+ }
166
+
167
+ let idx = 2;
168
+ while (usedNames.has(`${base}_${idx}`)) idx += 1;
169
+ const name = `${base}_${idx}`;
170
+ usedNames.add(name);
171
+ return name;
172
+ }
173
+
174
+ function registerCoreTools(server) {
175
+ server.registerTool(
176
+ "slack_api_call",
177
+ {
178
+ description: "Call any Slack Web API method directly.",
179
+ inputSchema: {
180
+ method: z
181
+ .string()
182
+ .min(3)
183
+ .regex(/^[a-z][a-zA-Z0-9_.]+$/, "Method must look like chat.postMessage"),
184
+ params: z.record(z.string(), z.any()).optional(),
185
+ token_override: z.string().optional(),
186
+ },
187
+ },
188
+ async ({ method, params, token_override }) =>
189
+ safeToolRun(async () => {
190
+ const data = await callSlackApi(method, params || {}, token_override);
191
+ return { method, data };
192
+ })
193
+ );
194
+
195
+ server.registerTool(
196
+ "slack_http_api_call",
197
+ {
198
+ description:
199
+ "Generic HTTP call for Slack APIs outside standard Web API methods (SCIM/Audit/Legal Holds).",
200
+ inputSchema: {
201
+ url: z.string().url(),
202
+ http_method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
203
+ query: z.record(z.string(), z.any()).optional(),
204
+ json_body: z.record(z.string(), z.any()).optional(),
205
+ form_body: z.record(z.string(), z.any()).optional(),
206
+ headers: z.record(z.string(), z.string()).optional(),
207
+ token_override: z.string().optional(),
208
+ },
209
+ },
210
+ async ({ url, http_method, query, json_body, form_body, headers, token_override }) =>
211
+ safeToolRun(async () => {
212
+ const token = requireSlackToken(token_override);
213
+ const method = http_method || "GET";
214
+
215
+ const endpoint = new URL(url);
216
+ for (const [k, v] of Object.entries(toRecordObject(query))) {
217
+ if (v === undefined || v === null) continue;
218
+ endpoint.searchParams.set(k, String(v));
219
+ }
220
+
221
+ const reqHeaders = {
222
+ Authorization: `Bearer ${token}`,
223
+ ...(headers || {}),
224
+ };
225
+
226
+ let body;
227
+ if (form_body && Object.keys(form_body).length > 0) {
228
+ reqHeaders["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8";
229
+ body = toUrlEncodedBody(form_body);
230
+ } else if (json_body && Object.keys(json_body).length > 0) {
231
+ reqHeaders["Content-Type"] = "application/json; charset=utf-8";
232
+ body = JSON.stringify(json_body);
233
+ }
234
+
235
+ const res = await fetch(endpoint.toString(), {
236
+ method,
237
+ headers: reqHeaders,
238
+ body,
239
+ });
240
+
241
+ const text = await res.text();
242
+ let parsedBody = text;
243
+ try {
244
+ parsedBody = JSON.parse(text);
245
+ } catch {
246
+ // Keep plain text when response is not JSON.
247
+ }
248
+
249
+ return {
250
+ url: endpoint.toString(),
251
+ status: res.status,
252
+ ok: res.ok,
253
+ headers: Object.fromEntries(res.headers.entries()),
254
+ body: parsedBody,
255
+ };
256
+ })
257
+ );
258
+
259
+ server.registerTool(
260
+ "search_messages_files",
261
+ {
262
+ description:
263
+ "Search messages and files. Uses search.messages and search.files and returns both.",
264
+ inputSchema: {
265
+ query: z.string().min(1),
266
+ count: z.number().int().min(1).max(100).optional(),
267
+ page: z.number().int().min(1).optional(),
268
+ sort: z.enum(["score", "timestamp"]).optional(),
269
+ sort_dir: z.enum(["asc", "desc"]).optional(),
270
+ include_messages: z.boolean().optional(),
271
+ include_files: z.boolean().optional(),
272
+ token_override: z.string().optional(),
273
+ },
274
+ },
275
+ async ({
276
+ query,
277
+ count,
278
+ page,
279
+ sort,
280
+ sort_dir,
281
+ include_messages,
282
+ include_files,
283
+ token_override,
284
+ }) =>
285
+ safeToolRun(async () => {
286
+ const shouldSearchMessages = include_messages !== false;
287
+ const shouldSearchFiles = include_files !== false;
288
+ const sharedParams = {
289
+ query,
290
+ count: count ?? 20,
291
+ page: page ?? 1,
292
+ sort,
293
+ sort_dir,
294
+ };
295
+
296
+ let messages = null;
297
+ let files = null;
298
+
299
+ if (shouldSearchMessages) {
300
+ messages = await callSlackApi("search.messages", sharedParams, token_override);
301
+ }
302
+ if (shouldSearchFiles) {
303
+ files = await callSlackApi("search.files", sharedParams, token_override);
304
+ }
305
+
306
+ return {
307
+ query,
308
+ messages: messages ? messages.messages : null,
309
+ files: files ? files.files : null,
310
+ };
311
+ })
312
+ );
313
+
314
+ server.registerTool(
315
+ "search_users",
316
+ {
317
+ description:
318
+ "Find users by partial match on id/name/display_name/email using users.list and local filtering.",
319
+ inputSchema: {
320
+ query: z.string().optional(),
321
+ limit: z.number().int().min(1).max(1000).optional(),
322
+ include_locale: z.boolean().optional(),
323
+ include_deleted: z.boolean().optional(),
324
+ include_bots: z.boolean().optional(),
325
+ cursor: z.string().optional(),
326
+ token_override: z.string().optional(),
327
+ },
328
+ },
329
+ async ({
330
+ query,
331
+ limit,
332
+ include_locale,
333
+ include_deleted,
334
+ include_bots,
335
+ cursor,
336
+ token_override,
337
+ }) =>
338
+ safeToolRun(async () => {
339
+ const listData = await callSlackApi(
340
+ "users.list",
341
+ {
342
+ limit: limit ?? 200,
343
+ include_locale: include_locale ?? true,
344
+ cursor,
345
+ },
346
+ token_override
347
+ );
348
+
349
+ const normalizedQuery = query ? query.toLowerCase() : null;
350
+ let users = Array.isArray(listData.members) ? listData.members : [];
351
+
352
+ if (include_deleted !== true) {
353
+ users = users.filter((u) => !u.deleted);
354
+ }
355
+ if (include_bots !== true) {
356
+ users = users.filter((u) => !u.is_bot && !u.is_app_user);
357
+ }
358
+ if (normalizedQuery) {
359
+ users = users.filter((u) => {
360
+ const candidates = [
361
+ u.id,
362
+ u.name,
363
+ u.real_name,
364
+ u.profile?.display_name,
365
+ u.profile?.real_name,
366
+ u.profile?.email,
367
+ ]
368
+ .filter((v) => typeof v === "string")
369
+ .map((v) => v.toLowerCase());
370
+ return candidates.some((value) => value.includes(normalizedQuery));
371
+ });
372
+ }
373
+
374
+ return {
375
+ users,
376
+ next_cursor: listData.response_metadata?.next_cursor || null,
377
+ count: users.length,
378
+ };
379
+ })
380
+ );
381
+
382
+ server.registerTool(
383
+ "search_channels",
384
+ {
385
+ description:
386
+ "Find channels by partial match on name/topic/purpose using conversations.list and local filtering.",
387
+ inputSchema: {
388
+ query: z.string().optional(),
389
+ types: z.string().optional(),
390
+ exclude_archived: z.boolean().optional(),
391
+ limit: z.number().int().min(1).max(1000).optional(),
392
+ cursor: z.string().optional(),
393
+ token_override: z.string().optional(),
394
+ },
395
+ },
396
+ async ({ query, types, exclude_archived, limit, cursor, token_override }) =>
397
+ safeToolRun(async () => {
398
+ const data = await callSlackApi(
399
+ "conversations.list",
400
+ {
401
+ types: types || "public_channel,private_channel",
402
+ exclude_archived: exclude_archived ?? true,
403
+ limit: limit ?? 200,
404
+ cursor,
405
+ },
406
+ token_override
407
+ );
408
+
409
+ let channels = Array.isArray(data.channels) ? data.channels : [];
410
+ if (query) {
411
+ const normalizedQuery = query.toLowerCase();
412
+ channels = channels.filter((channel) => {
413
+ const candidates = [channel.id, channel.name, channel.purpose?.value, channel.topic?.value]
414
+ .filter((v) => typeof v === "string")
415
+ .map((v) => v.toLowerCase());
416
+ return candidates.some((value) => value.includes(normalizedQuery));
417
+ });
418
+ }
419
+
420
+ return {
421
+ channels,
422
+ next_cursor: data.response_metadata?.next_cursor || null,
423
+ count: channels.length,
424
+ };
425
+ })
426
+ );
427
+
428
+ server.registerTool(
429
+ "send_message",
430
+ {
431
+ description: "Send a message using chat.postMessage.",
432
+ inputSchema: {
433
+ channel: z.string().min(1),
434
+ text: z.string().min(1),
435
+ thread_ts: z.string().optional(),
436
+ blocks: z.any().optional(),
437
+ mrkdwn: z.boolean().optional(),
438
+ unfurl_links: z.boolean().optional(),
439
+ unfurl_media: z.boolean().optional(),
440
+ token_override: z.string().optional(),
441
+ },
442
+ },
443
+ async ({ channel, text, thread_ts, blocks, mrkdwn, unfurl_links, unfurl_media, token_override }) =>
444
+ safeToolRun(async () => {
445
+ const data = await callSlackApi(
446
+ "chat.postMessage",
447
+ {
448
+ channel,
449
+ text,
450
+ thread_ts,
451
+ blocks: parseJsonMaybe(blocks),
452
+ mrkdwn,
453
+ unfurl_links,
454
+ unfurl_media,
455
+ },
456
+ token_override
457
+ );
458
+
459
+ return {
460
+ channel: data.channel,
461
+ ts: data.ts,
462
+ message: data.message,
463
+ };
464
+ })
465
+ );
466
+
467
+ server.registerTool(
468
+ "read_channel",
469
+ {
470
+ description: "Read channel history with conversations.history.",
471
+ inputSchema: {
472
+ channel: z.string().min(1),
473
+ limit: z.number().int().min(1).max(1000).optional(),
474
+ cursor: z.string().optional(),
475
+ oldest: z.string().optional(),
476
+ latest: z.string().optional(),
477
+ inclusive: z.boolean().optional(),
478
+ token_override: z.string().optional(),
479
+ },
480
+ },
481
+ async ({ channel, limit, cursor, oldest, latest, inclusive, token_override }) =>
482
+ safeToolRun(async () => {
483
+ const data = await callSlackApi(
484
+ "conversations.history",
485
+ {
486
+ channel,
487
+ limit: limit ?? 50,
488
+ cursor,
489
+ oldest,
490
+ latest,
491
+ inclusive,
492
+ },
493
+ token_override
494
+ );
495
+
496
+ return {
497
+ channel,
498
+ messages: data.messages || [],
499
+ has_more: Boolean(data.has_more),
500
+ next_cursor: data.response_metadata?.next_cursor || null,
501
+ };
502
+ })
503
+ );
504
+
505
+ server.registerTool(
506
+ "read_thread",
507
+ {
508
+ description: "Read a thread using conversations.replies.",
509
+ inputSchema: {
510
+ channel: z.string().min(1),
511
+ thread_ts: z.string().min(1),
512
+ limit: z.number().int().min(1).max(1000).optional(),
513
+ cursor: z.string().optional(),
514
+ oldest: z.string().optional(),
515
+ latest: z.string().optional(),
516
+ inclusive: z.boolean().optional(),
517
+ token_override: z.string().optional(),
518
+ },
519
+ },
520
+ async ({ channel, thread_ts, limit, cursor, oldest, latest, inclusive, token_override }) =>
521
+ safeToolRun(async () => {
522
+ const data = await callSlackApi(
523
+ "conversations.replies",
524
+ {
525
+ channel,
526
+ ts: thread_ts,
527
+ limit: limit ?? 50,
528
+ cursor,
529
+ oldest,
530
+ latest,
531
+ inclusive,
532
+ },
533
+ token_override
534
+ );
535
+
536
+ return {
537
+ channel,
538
+ thread_ts,
539
+ messages: data.messages || [],
540
+ has_more: Boolean(data.has_more),
541
+ next_cursor: data.response_metadata?.next_cursor || null,
542
+ };
543
+ })
544
+ );
545
+
546
+ server.registerTool(
547
+ "create_canvas",
548
+ {
549
+ description: "Create a canvas using canvases.create. Pass Slack params in `params`.",
550
+ inputSchema: {
551
+ params: z.record(z.string(), z.any()),
552
+ token_override: z.string().optional(),
553
+ },
554
+ },
555
+ async ({ params, token_override }) =>
556
+ safeToolRun(async () => {
557
+ const data = await callSlackApi("canvases.create", params, token_override);
558
+ return data;
559
+ })
560
+ );
561
+
562
+ server.registerTool(
563
+ "update_canvas",
564
+ {
565
+ description: "Update a canvas using canvases.edit. Pass Slack params in `params`.",
566
+ inputSchema: {
567
+ params: z.record(z.string(), z.any()),
568
+ token_override: z.string().optional(),
569
+ },
570
+ },
571
+ async ({ params, token_override }) =>
572
+ safeToolRun(async () => {
573
+ const data = await callSlackApi("canvases.edit", params, token_override);
574
+ return data;
575
+ })
576
+ );
577
+
578
+ server.registerTool(
579
+ "read_canvas",
580
+ {
581
+ description: "Read canvas content using canvases.sections.lookup. Pass Slack params in `params`.",
582
+ inputSchema: {
583
+ params: z.record(z.string(), z.any()),
584
+ token_override: z.string().optional(),
585
+ },
586
+ },
587
+ async ({ params, token_override }) =>
588
+ safeToolRun(async () => {
589
+ const data = await callSlackApi("canvases.sections.lookup", params, token_override);
590
+ return data;
591
+ })
592
+ );
593
+
594
+ server.registerTool(
595
+ "read_user_profile",
596
+ {
597
+ description: "Read user info/profile using users.info plus users.profile.get (best effort).",
598
+ inputSchema: {
599
+ user: z.string().min(1),
600
+ include_locale: z.boolean().optional(),
601
+ include_labels: z.boolean().optional(),
602
+ token_override: z.string().optional(),
603
+ },
604
+ },
605
+ async ({ user, include_locale, include_labels, token_override }) =>
606
+ safeToolRun(async () => {
607
+ const info = await callSlackApi(
608
+ "users.info",
609
+ { user, include_locale: include_locale ?? true },
610
+ token_override
611
+ );
612
+
613
+ let profile = null;
614
+ try {
615
+ const profileData = await callSlackApi(
616
+ "users.profile.get",
617
+ { user, include_labels: include_labels ?? true },
618
+ token_override
619
+ );
620
+ profile = profileData.profile || null;
621
+ } catch {
622
+ profile = info.user?.profile || null;
623
+ }
624
+
625
+ return { user: info.user || null, profile };
626
+ })
627
+ );
628
+ }
629
+
630
+ function registerCatalogMethodTools(server, catalog) {
631
+ if (!ENABLE_METHOD_TOOLS) return { registered: 0 };
632
+
633
+ const methods = Array.isArray(catalog.methods) ? catalog.methods : [];
634
+ const limited = MAX_METHOD_TOOLS > 0 ? methods.slice(0, MAX_METHOD_TOOLS) : methods;
635
+
636
+ const usedNames = new Set();
637
+ let registered = 0;
638
+
639
+ for (const methodInfo of limited) {
640
+ const method = methodInfo?.method;
641
+ if (!method || typeof method !== "string") continue;
642
+
643
+ const toolName = toolNameFromMethod(method, usedNames);
644
+ const descriptionParts = [
645
+ `Slack Web API method wrapper for ${method}.`,
646
+ methodInfo.description ? `Official: ${methodInfo.description}` : "",
647
+ Array.isArray(methodInfo.scopes) && methodInfo.scopes.length
648
+ ? `Scopes: ${methodInfo.scopes.join(", ")}`
649
+ : "",
650
+ ].filter(Boolean);
651
+
652
+ server.registerTool(
653
+ toolName,
654
+ {
655
+ description: descriptionParts.join(" "),
656
+ inputSchema: {
657
+ params: z.record(z.string(), z.any()).optional(),
658
+ token_override: z.string().optional(),
659
+ },
660
+ },
661
+ async ({ params, token_override }) =>
662
+ safeToolRun(async () => {
663
+ const data = await callSlackApi(method, params || {}, token_override);
664
+ return { method, data };
665
+ })
666
+ );
667
+ registered += 1;
668
+ }
669
+
670
+ server.registerTool(
671
+ "slack_method_tools_info",
672
+ {
673
+ description: "Return summary for catalog-driven method tools currently loaded.",
674
+ inputSchema: {},
675
+ },
676
+ async () =>
677
+ safeToolRun(async () => ({
678
+ catalog_path: CATALOG_PATH,
679
+ method_tools_enabled: ENABLE_METHOD_TOOLS,
680
+ max_method_tools: MAX_METHOD_TOOLS,
681
+ methods_in_catalog: methods.length,
682
+ method_tools_registered: registered,
683
+ method_tool_prefix: METHOD_TOOL_PREFIX,
684
+ }))
685
+ );
686
+
687
+ return { registered };
688
+ }
689
+
690
+ async function main() {
691
+ const server = new McpServer(
692
+ { name: SERVER_NAME, version: SERVER_VERSION },
693
+ { capabilities: { logging: {} } }
694
+ );
695
+
696
+ registerCoreTools(server);
697
+ const catalog = loadCatalog();
698
+ const methodStats = registerCatalogMethodTools(server, catalog);
699
+
700
+ const transport = new StdioServerTransport();
701
+ await server.connect(transport);
702
+
703
+ const catalogCount =
704
+ catalog && catalog.totals && typeof catalog.totals.methods === "number"
705
+ ? catalog.totals.methods
706
+ : Array.isArray(catalog.methods)
707
+ ? catalog.methods.length
708
+ : 0;
709
+
710
+ console.error(
711
+ `[${SERVER_NAME}] connected via stdio | catalog_methods=${catalogCount} | method_tools_registered=${methodStats.registered}`
712
+ );
713
+ }
714
+
715
+ main().catch((error) => {
716
+ console.error(`[${SERVER_NAME}] fatal error:`, error);
717
+ process.exit(1);
718
+ });