File size: 22,472 Bytes
7dc28be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
// src/server.ts
import { FastMCP, UserError } from 'fastmcp';
import { z } from 'zod';
import { google, docs_v1 } from 'googleapis';
import { authorize } from './auth.js';
import { OAuth2Client } from 'google-auth-library';

// --- Helper function for hex color validation (basic) ---
const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
const validateHexColor = (color: string) => hexColorRegex.test(color);

// --- Helper function for Hex to RGB conversion ---
/**
 * Converts a hex color string to a Google Docs API RgbColor object.
 * @param hex - The hex color string (e.g., "#FF0000", "#F00", "FF0000").
 * @returns A Google Docs API RgbColor object or null if invalid.
 */
function hexToRgbColor(hex: string): docs_v1.Schema$RgbColor | null {
  if (!hex) return null;
  let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;

  // Expand shorthand form (e.g. "F00") to full form (e.g. "FF0000")
  if (hexClean.length === 3) {
    hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
  }

  if (hexClean.length !== 6) {
    return null; // Invalid length
  }

  const bigint = parseInt(hexClean, 16);
  if (isNaN(bigint)) {
      return null; // Invalid hex characters
  }

  // Extract RGB values and normalize to 0.0 - 1.0 range
  const r = ((bigint >> 16) & 255) / 255;
  const g = ((bigint >> 8) & 255) / 255;
  const b = (bigint & 255) / 255;

  return { red: r, green: g, blue: b };
}

// --- Zod Schema for the formatText tool ---
// const FormatTextParameters = z.object({
//   documentId: z.string().describe('The ID of the Google Document.'),
//   startIndex: z.number().int().min(1).describe('The starting index of the text range (inclusive, starts from 1).'),
//   endIndex: z.number().int().min(1).describe('The ending index of the text range (inclusive).'),
//   // Optional Formatting Parameters (SHARED)
//   bold: z.boolean().optional().describe('Apply bold formatting.'),
//   italic: z.boolean().optional().describe('Apply italic formatting.'),
//   underline: z.boolean().optional().describe('Apply underline formatting.'),
//   strikethrough: z.boolean().optional().describe('Apply strikethrough formatting.'),
//   fontSize: z.number().min(1).optional().describe('Set font size (in points, e.g., 12).'),
//   fontFamily: z.string().optional().describe('Set font family (e.g., "Arial", "Times New Roman").'),
//   foregroundColor: z.string()
//     .refine(validateHexColor, { message: "Invalid hex color format (e.g., #FF0000 or #F00)" })
//     .optional()
//     .describe('Set text color using hex format (e.g., "#FF0000").'),
//   backgroundColor: z.string()
//     .refine(validateHexColor, { message: "Invalid hex color format (e.g., #00FF00 or #0F0)" })
//     .optional()
//     .describe('Set text background color using hex format (e.g., "#FFFF00").'),
//   linkUrl: z.string().url().optional().describe('Make the text a hyperlink pointing to this URL.')
// })
// .refine(data => data.endIndex >= data.startIndex, {
//   message: "endIndex must be greater than or equal to startIndex",
//   path: ["endIndex"],
// })
// .refine(data => Object.keys(data).some(key => !['documentId', 'startIndex', 'endIndex'].includes(key) && data[key as keyof typeof data] !== undefined), {
//     message: "At least one formatting option (bold, italic, fontSize, etc.) must be provided."
// });

// --- Define the TypeScript type based on the schema ---
// type FormatTextArgs = z.infer<typeof FormatTextParameters>;

// --- Zod Schema for the NEW formatMatchingText tool ---
const FormatMatchingTextParameters = z.object({
  documentId: z.string().describe('The ID of the Google Document.'),
  textToFind: z.string().min(1).describe('The exact text string to find and format.'),
  matchInstance: z.number().int().min(1).optional().default(1).describe('Which instance of the text to format (1st, 2nd, etc.). Defaults to 1.'),
  // Re-use optional Formatting Parameters (SHARED)
  bold: z.boolean().optional().describe('Apply bold formatting.'),
  italic: z.boolean().optional().describe('Apply italic formatting.'),
  underline: z.boolean().optional().describe('Apply underline formatting.'),
  strikethrough: z.boolean().optional().describe('Apply strikethrough formatting.'),
  fontSize: z.number().min(1).optional().describe('Set font size (in points, e.g., 12).'),
  fontFamily: z.string().optional().describe('Set font family (e.g., "Arial", "Times New Roman").'),
  foregroundColor: z.string()
    .refine(validateHexColor, { message: "Invalid hex color format (e.g., #FF0000 or #F00)" })
    .optional()
    .describe('Set text color using hex format (e.g., "#FF0000").'),
  backgroundColor: z.string()
    .refine(validateHexColor, { message: "Invalid hex color format (e.g., #00FF00 or #0F0)" })
    .optional()
    .describe('Set text background color using hex format (e.g., "#FFFF00").'),
  linkUrl: z.string().url().optional().describe('Make the text a hyperlink pointing to this URL.')
})
.refine(data => Object.keys(data).some(key => !['documentId', 'textToFind', 'matchInstance'].includes(key) && data[key as keyof typeof data] !== undefined), {
    message: "At least one formatting option (bold, italic, fontSize, etc.) must be provided."
});

// --- Define the TypeScript type based on the new schema ---
type FormatMatchingTextArgs = z.infer<typeof FormatMatchingTextParameters>;

// --- Helper function to build TextStyle and fields mask (reusable) ---
function buildTextStyleAndFields(args: Omit<FormatMatchingTextArgs, 'documentId' | 'textToFind' | 'matchInstance'>): { textStyle: docs_v1.Schema$TextStyle, fields: string[] } {
    const textStyle: docs_v1.Schema$TextStyle = {};
    const fieldsToUpdate: string[] = [];

    if (args.bold !== undefined) { textStyle.bold = args.bold; fieldsToUpdate.push('bold'); }
    if (args.italic !== undefined) { textStyle.italic = args.italic; fieldsToUpdate.push('italic'); }
    if (args.underline !== undefined) { textStyle.underline = args.underline; fieldsToUpdate.push('underline'); }
    if (args.strikethrough !== undefined) { textStyle.strikethrough = args.strikethrough; fieldsToUpdate.push('strikethrough'); }
    if (args.fontSize !== undefined) {
        textStyle.fontSize = { magnitude: args.fontSize, unit: 'PT' };
        fieldsToUpdate.push('fontSize');
    }
    if (args.fontFamily !== undefined) {
        textStyle.weightedFontFamily = { fontFamily: args.fontFamily };
        fieldsToUpdate.push('weightedFontFamily');
    }
    if (args.foregroundColor !== undefined) {
        const rgbColor = hexToRgbColor(args.foregroundColor);
        if (!rgbColor) throw new UserError(`Invalid foreground hex color format: ${args.foregroundColor}`);
        textStyle.foregroundColor = { color: { rgbColor: rgbColor } };
        fieldsToUpdate.push('foregroundColor');
    }
    if (args.backgroundColor !== undefined) {
        const rgbColor = hexToRgbColor(args.backgroundColor);
        if (!rgbColor) throw new UserError(`Invalid background hex color format: ${args.backgroundColor}`);
        textStyle.backgroundColor = { color: { rgbColor: rgbColor } };
        fieldsToUpdate.push('backgroundColor');
    }
    if (args.linkUrl !== undefined) {
        textStyle.link = { url: args.linkUrl };
        fieldsToUpdate.push('link');
    }

    if (fieldsToUpdate.length === 0) {
        // This should ideally be caught by Zod refine, but defensive check
        throw new UserError("No formatting options were specified.");
    }

    return { textStyle, fields: fieldsToUpdate };
}

let authClient: OAuth2Client | null = null;
let googleDocs: docs_v1.Docs | null = null;

async function initializeGoogleClient() {
  if (googleDocs) return { authClient, googleDocs };
  if (authClient === null && googleDocs === null) {
    try {
      console.error("Attempting to authorize Google API client...");
      const client = await authorize();
      if (client) {
        authClient = client;
        googleDocs = google.docs({ version: 'v1', auth: authClient });
        console.error("Google API client authorized successfully.");
      } else {
        console.error("FATAL: Authorization returned null or undefined client.");
        authClient = null;
        googleDocs = null;
      }
    } catch (error) {
      console.error("FATAL: Failed to initialize Google API client:", error);
      authClient = null;
      googleDocs = null;
    }
  }
  return { authClient, googleDocs };
}

const server = new FastMCP({
  name: 'Google Docs MCP Server',
  version: '1.0.0',
});

// Tool: Read Google Doc
server.addTool({
  name: 'readGoogleDoc',
  description: 'Reads the content of a specific Google Document.',
  parameters: z.object({
    documentId: z.string().describe('The ID of the Google Document (from the URL).'),
  }),
  execute: async (args, { log }) => {
    const { googleDocs: docs } = await initializeGoogleClient();
    if (!docs) throw new UserError("Google Docs client not initialized.");

    log.info(`Reading Google Doc: ${args.documentId}`);
    try {
      const res = await docs.documents.get({
        documentId: args.documentId,
        fields: 'body(content)',
      });
      log.info(`Fetched doc: ${args.documentId}`);

      let textContent = '';
      res.data.body?.content?.forEach(element => {
        element.paragraph?.elements?.forEach(pe => {
          textContent += pe.textRun?.content || '';
        });
      });

      if (!textContent.trim()) return "Document found, but appears empty.";

      const maxLength = 2000;
      const truncatedContent = textContent.length > maxLength ? textContent.substring(0, maxLength) + '... [truncated]' : textContent;
      return `Content:\n---\n${truncatedContent}`;
    } catch (error: any) {
      log.error(`Error reading doc ${args.documentId}: ${error.message}`);
       if (error.code === 404) throw new UserError(`Doc not found (ID: ${args.documentId}).`);
       if (error.code === 403) throw new UserError(`Permission denied for doc (ID: ${args.documentId}).`);
       throw new UserError(`Failed to read doc: ${error.message}`);
    }
  },
});

// Tool: Append to Google Doc
server.addTool({
  name: 'appendToGoogleDoc',
  description: 'Appends text to the end of a specific Google Document.',
  parameters: z.object({
    documentId: z.string().describe('The ID of the Google Document.'),
    textToAppend: z.string().describe('The text to add.'),
  }),
  execute: async (args, { log }) => {
    const { googleDocs: docs } = await initializeGoogleClient();
     if (!docs) throw new UserError("Google Docs client not initialized.");

    log.info(`Appending to Google Doc: ${args.documentId}`);
    try {
      const docInfo = await docs.documents.get({ documentId: args.documentId, fields: 'body(content)' });
      let endIndex = 1;
      if (docInfo.data.body?.content) {
        const lastElement = docInfo.data.body.content[docInfo.data.body.content.length - 1];
        if (lastElement?.endIndex) endIndex = lastElement.endIndex - 1;
      }
      const textToInsert = (endIndex > 1 && !args.textToAppend.startsWith('\n') ? '\n' : '') + args.textToAppend;

      await docs.documents.batchUpdate({
        documentId: args.documentId,
        requestBody: { requests: [{ insertText: { location: { index: endIndex }, text: textToInsert } }] },
      });

      log.info(`Successfully appended to doc: ${args.documentId}`);
      return `Successfully appended text to document ${args.documentId}.`;
    } catch (error: any) {
      log.error(`Error editing doc ${args.documentId}: ${error.message}`);
      if (error.code === 404) throw new UserError(`Doc not found (ID: ${args.documentId}).`);
      if (error.code === 403) throw new UserError(`Permission denied for doc (ID: ${args.documentId}).`);
      throw new UserError(`Failed to edit doc: ${error.message}`);
    }
  },
});

// --- Add the formatMatchingText tool ---
server.addTool({
    name: 'formatMatchingText',
    description: 'Finds specific text within a Google Document and applies character formatting (bold, italics, color, etc.) to the specified instance.',
    parameters: FormatMatchingTextParameters, // Use the new Zod schema
    execute: async (args: FormatMatchingTextArgs, { log }) => {
        const { googleDocs: docs } = await initializeGoogleClient();
        if (!docs) {
          throw new UserError("Google Docs client is not initialized. Authentication might have failed.");
        }

        log.info(`Attempting to find text "${args.textToFind}" (instance ${args.matchInstance}) in doc: ${args.documentId} and format it.`);

        // 1. Get the document content to find the text range
        let docContent: docs_v1.Schema$Document;
        try {
            const res = await docs.documents.get({
                documentId: args.documentId,
                // Request fields needed to reconstruct text and find indices
                fields: 'body(content(paragraph(elements(startIndex,endIndex,textRun(content)))))',
            });
            docContent = res.data;
            if (!docContent.body?.content) {
                throw new UserError(`Document body or content is empty or inaccessible (ID: ${args.documentId}).`);
            }
            log.info(`Fetched doc content for searching: ${args.documentId}`);
        } catch (error: any) {
            log.error(`Error retrieving doc ${args.documentId} for search: ${error.message}`);
            if (error.code === 404) throw new UserError(`Doc not found (ID: ${args.documentId}).`);
            if (error.code === 403) throw new UserError(`Permission denied for doc (ID: ${args.documentId}).`);
            throw new UserError(`Failed to retrieve doc for searching: ${error.message}`);
        }

        // 2. Find the Nth instance of the text and its range
        let fullText = '';
        const textSegments: { text: string, start: number, end: number }[] = [];
        docContent.body.content.forEach(element => {
            element.paragraph?.elements?.forEach(pe => {
                if (pe.textRun?.content && pe.startIndex && pe.endIndex) {
                    // Handle potential line breaks within content
                    const content = pe.textRun.content;
                    fullText += content;
                    textSegments.push({
                        text: content,
                        start: pe.startIndex,
                        end: pe.endIndex
                    });
                }
            });
        });

        let startIndex = -1;
        let endIndex = -1;
        let foundCount = 0;
        let searchStartIndex = 0;

        while (foundCount < args.matchInstance) {
            const currentIndex = fullText.indexOf(args.textToFind, searchStartIndex);
            if (currentIndex === -1) {
                // Text not found anymore
                break;
            }
            foundCount++;
            if (foundCount === args.matchInstance) {
                // Found the start of the Nth match in the *reconstructed* string.
                // Map this back to the API's startIndex/endIndex.
                const targetStartInFullText = currentIndex;
                const targetEndInFullText = currentIndex + args.textToFind.length;
                let currentPosInFullText = 0;

                for (const seg of textSegments) {
                    const segStartInFullText = currentPosInFullText;
                    // Length of segment text might differ from index range if it contains newlines etc.
                    const segTextLength = seg.text.length;
                    const segEndInFullText = segStartInFullText + segTextLength;

                     // Check if the target *starts* within this segment's text span
                    if (startIndex === -1 && targetStartInFullText >= segStartInFullText && targetStartInFullText < segEndInFullText) {
                        // Calculate the API start index relative to the segment's start index
                        startIndex = seg.start + (targetStartInFullText - segStartInFullText);
                    }

                    // Check if the target *ends* within this segment's text span
                     if (targetEndInFullText > segStartInFullText && targetEndInFullText <= segEndInFullText) {
                        // Calculate the API end index relative to the segment's start index
                        endIndex = seg.start + (targetEndInFullText - segStartInFullText);
                        break; // Found the end, we have the full range
                    }

                    currentPosInFullText = segEndInFullText;
                }

                if (startIndex === -1 || endIndex === -1) {
                     log.warn(`Could not accurately map indices for match ${foundCount} of "${args.textToFind}". Start found at ${targetStartInFullText}, End at ${targetEndInFullText}. Resetting.`);
                     // Reset if we couldn't map indices correctly for this match
                     startIndex = -1;
                     endIndex = -1;
                     // Don't break the outer loop, let it try searching again
                }
            }
            // Continue searching after the start of the current match to find subsequent occurrences
            searchStartIndex = currentIndex + 1;
        }


        if (startIndex === -1 || endIndex === -1) {
          throw new UserError(`Could not find instance ${args.matchInstance} of the text "${args.textToFind}" in document ${args.documentId}. Found ${foundCount} total instance(s).`);
        }

        log.info(`Found text "${args.textToFind}" (instance ${args.matchInstance}) at mapped range: ${startIndex}-${endIndex}`);

        // 3. Build the TextStyle object and fields mask
        const { textStyle, fields } = buildTextStyleAndFields(args);


        // 4. Build the UpdateTextStyleRequest
        const updateTextStyleRequest: docs_v1.Schema$UpdateTextStyleRequest = {
            range: {
                // API uses segmentId, but omitting it defaults to the document BODY
                startIndex: startIndex, // Use the calculated start index
                endIndex: endIndex,     // Use the calculated end index
            },
            textStyle: textStyle,
            fields: fields.join(','), // Crucial: Tells API which fields to update
        };

        // 5. Send the batchUpdate request
        try {
            await docs.documents.batchUpdate({
                documentId: args.documentId,
                requestBody: {
                    requests: [{ updateTextStyle: updateTextStyleRequest }],
                },
            });
            log.info(`Successfully formatted text in doc: ${args.documentId}, range: ${startIndex}-${endIndex}`);
            return `Successfully applied formatting to instance ${args.matchInstance} of "${args.textToFind}".`;
        } catch (error: any) {
            log.error(`Error formatting text in doc ${args.documentId}: ${error.message}`);
            // Consider more specific error handling based on API response if needed
            throw new UserError(`Failed to apply formatting: ${error.message}`);
        }
    },
});

// Tool: Format Text (existing, keep for index-based formatting if needed)
// server.addTool({
//   name: 'formatText',
//   description: 'Applies character formatting (bold, italics, font size, color, link, etc.) to a specific text range in a Google Document using start/end indices.',
//   parameters: FormatTextParameters, // Use the original Zod schema
//   execute: async (args: FormatTextArgs, { log }) => {
//     const { googleDocs: docs } = await initializeGoogleClient();
//     if (!docs) {
//       throw new UserError("Google Docs client is not initialized. Authentication might have failed.");
//     }
//
//     log.info(`Attempting to format text in doc: ${args.documentId}, range: ${args.startIndex}-${args.endIndex}`);
//
//     // 1. Build the TextStyle object and fields mask
//     const { textStyle, fields } = buildTextStyleAndFields(args);
//
//     // 2. Build the UpdateTextStyleRequest
//     const updateTextStyleRequest: docs_v1.Schema$UpdateTextStyleRequest = {
//       range: {
//         startIndex: args.startIndex,
//         endIndex: args.endIndex,
//       },
//       textStyle: textStyle,
//       fields: fields.join(','),
//     };
//
//     // 3. Send the batchUpdate request
//     try {
//       await docs.documents.batchUpdate({
//         documentId: args.documentId,
//         requestBody: {
//           requests: [{ updateTextStyle: updateTextStyleRequest }],
//         },
//       });
//       log.info(`Successfully formatted text in doc: ${args.documentId}, range: ${args.startIndex}-${args.endIndex}`);
//       return `Successfully applied formatting to range ${args.startIndex}-${args.endIndex}.`;
//     } catch (error: any) {
//       log.error(`Error formatting text in doc ${args.documentId}: ${error.message}`);
//        if (error.code === 404) throw new UserError(`Doc not found (ID: ${args.documentId}).`);
//        if (error.code === 403) throw new UserError(`Permission denied for doc (ID: ${args.documentId}).`);
//       throw new UserError(`Failed to format text: ${error.message}`);
//     }
//   },
// });

// Start the Server (Modified to avoid server.config issue)
async function startServer() {
  await initializeGoogleClient(); // Authorize before starting listeners
  console.error("Starting MCP server...");
  try {
      const configToUse = {
        // Choose one transport:
         transportType: "stdio" as const,
        //  transportType: "sse" as const,
        //  sse: {                       // <-- COMMENT OUT or DELETE SSE config
        //    endpoint: "/sse" as const,
        //    port: 8080,
        //  },
      };

      server.start(configToUse); // Start the server with stdio config

      // Adjust logging (optional, but good practice)
      console.error(`MCP Server running using ${configToUse.transportType}.`);
      if (configToUse.transportType === 'stdio') {
          console.error("Awaiting MCP client connection via stdio...");
      }
      // Removed SSE-specific logging

  } catch(startError) {
      console.error("Error occurred during server.start():", startError);
      throw startError; // Re-throw to be caught by the outer catch
  }
}

// Call the modified startServer function
startServer().catch(err => {
    console.error("Server failed to start:", err);
    process.exit(1);
});