@jieunmarslim/server-editable-slides 0.2.4 → 0.2.6

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 (2) hide show
  1. package/index.js +276 -38
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -58,6 +58,74 @@ function getAccount() {
58
58
  return null;
59
59
  }
60
60
 
61
+ async function checkDriveScope(token) {
62
+ if (!token) return false;
63
+ try {
64
+ const res = await fetch(
65
+ `https://oauth2.googleapis.com/tokeninfo?access_token=${token}`,
66
+ );
67
+ if (!res.ok) return false;
68
+ const data = await res.json();
69
+ const scopes = (data.scope || '').split(' ');
70
+ return scopes.some((s) => s.includes('drive'));
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ async function uploadPptxToGoogleDrive(pptxBuffer, title, token) {
77
+ const boundary = '-------EditNBLM' + Math.random().toString(36).substring(2);
78
+ const delimiter = `\r\n--${boundary}\r\n`;
79
+ const closeDelimiter = `\r\n--${boundary}--`;
80
+
81
+ const metadata = {
82
+ name: (title || 'Presentation').replace(/\.pptx$/i, ''),
83
+ mimeType: 'application/vnd.google-apps.presentation',
84
+ };
85
+
86
+ const metadataHeader =
87
+ 'Content-Type: application/json; charset=UTF-8\r\n\r\n' +
88
+ JSON.stringify(metadata);
89
+ const mediaHeader =
90
+ 'Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation\r\n\r\n';
91
+
92
+ const body = Buffer.concat([
93
+ Buffer.from(delimiter + metadataHeader + delimiter + mediaHeader),
94
+ pptxBuffer,
95
+ Buffer.from(closeDelimiter),
96
+ ]);
97
+
98
+ const res = await fetch(
99
+ 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
100
+ {
101
+ method: 'POST',
102
+ headers: {
103
+ Authorization: `Bearer ${token}`,
104
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
105
+ 'Content-Length': String(body.length),
106
+ },
107
+ body: body,
108
+ },
109
+ );
110
+
111
+ if (!res.ok) {
112
+ const errText = await res.text();
113
+ let parsedMsg = errText;
114
+ try {
115
+ const j = JSON.parse(errText);
116
+ if (j.error && j.error.message) parsedMsg = j.error.message;
117
+ } catch {}
118
+ throw new Error(`Google Drive API error (${res.status}): ${parsedMsg}`);
119
+ }
120
+
121
+ const fileData = await res.json();
122
+ return {
123
+ presentationId: fileData.id,
124
+ presentationUrl: `https://docs.google.com/presentation/d/${fileData.id}/edit`,
125
+ title: fileData.name,
126
+ };
127
+ }
128
+
61
129
  async function runDoctor() {
62
130
  const account = getAccount();
63
131
  const token = getAuthToken();
@@ -71,20 +139,33 @@ async function runDoctor() {
71
139
 
72
140
  // 1. Check Auth
73
141
  if (account && token) {
74
- console.error(` [1/4] Google Cloud Authentication:`);
142
+ console.error(` [1/5] Google Cloud Authentication:`);
75
143
  console.error(` ✓ Authenticated: ${account}`);
76
144
  } else {
77
145
  hasError = true;
78
- console.error(` [1/4] Google Cloud Authentication:`);
146
+ console.error(` [1/5] Google Cloud Authentication:`);
79
147
  console.error(` ✗ FAILED: Not authenticated or token expired.`);
80
- console.error(` 👉 Please run: gcloud auth login`);
148
+ console.error(` 👉 Please run: gcloud auth login --enable-gdrive-access`);
149
+ }
150
+
151
+ // 2. Check Google Drive Integration & Scope
152
+ const hasDrive = await checkDriveScope(token);
153
+ console.error(`\n [2/5] Google Drive Integration & Scope:`);
154
+ if (hasDrive) {
155
+ console.error(` ✓ Active: Google Drive access granted.`);
156
+ console.error(` ✓ Presentations will automatically convert into live Google Slides.`);
157
+ } else {
158
+ hasError = true;
159
+ console.error(` ✗ FAILED: Token lacks Google Drive access.`);
160
+ console.error(` 👉 To enable automatic Google Slides creation in your Drive, run:`);
161
+ console.error(` gcloud auth login --enable-gdrive-access`);
81
162
  }
82
163
 
83
164
  // 2. Check Project Configuration & Account Access Permissions
84
165
  let projectAccessOk = false;
85
166
  if (!project) {
86
167
  hasError = true;
87
- console.error(`\n [2/4] GEAP Billing & Quota Project:`);
168
+ console.error(`\n [3/5] GEAP Billing & Quota Project:`);
88
169
  console.error(` ✗ FAILED: No GCP project configured.`);
89
170
  console.error(` 👉 Please run: gcloud config set project YOUR_GCP_PROJECT_ID`);
90
171
  console.error(` 👉 Or export: export EDITABLE_SLIDES_PROJECT=YOUR_GCP_PROJECT_ID`);
@@ -99,7 +180,7 @@ async function runDoctor() {
99
180
  .trim();
100
181
  if (check === project) {
101
182
  projectAccessOk = true;
102
- console.error(`\n [2/4] Target GCP Project Accessibility:`);
183
+ console.error(`\n [3/5] Target GCP Project Accessibility:`);
103
184
  console.error(
104
185
  ` ✓ Verified: Account [${account}] has active access to project [${project}].`,
105
186
  );
@@ -107,7 +188,7 @@ async function runDoctor() {
107
188
  } catch (err) {
108
189
  hasError = true;
109
190
  const stderr = err.stderr ? err.stderr.toString() : err.message;
110
- console.error(`\n [2/4] Target GCP Project Accessibility:`);
191
+ console.error(`\n [3/5] Target GCP Project Accessibility:`);
111
192
  if (
112
193
  stderr.includes('does not have permission') ||
113
194
  stderr.includes('caller does not have permission')
@@ -118,7 +199,7 @@ async function runDoctor() {
118
199
  console.error(
119
200
  ` 👉 Solution 1: Log in with the account that has access to [${project}]:`,
120
201
  );
121
- console.error(` gcloud auth login`);
202
+ console.error(` gcloud auth login --enable-gdrive-access`);
122
203
  console.error(
123
204
  ` 👉 Solution 2: Switch to a project that [${account}] can access:`,
124
205
  );
@@ -140,7 +221,7 @@ async function runDoctor() {
140
221
  }
141
222
  }
142
223
 
143
- // 3. Verify One-Time IAM Delegation to EditNBLM Service Account
224
+ // 4. Verify One-Time IAM Delegation to EditNBLM Service Account
144
225
  if (projectAccessOk) {
145
226
  try {
146
227
  const bindings = execSync(
@@ -151,7 +232,7 @@ async function runDoctor() {
151
232
  'serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com',
152
233
  );
153
234
 
154
- console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
235
+ console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
155
236
  if (hasAiPlatform) {
156
237
  console.error(
157
238
  ` ✓ GRANTED: Service account has roles/aiplatform.user on [${project}].`,
@@ -164,28 +245,28 @@ async function runDoctor() {
164
245
  console.error(` 👉 Please run the delegation commands below.`);
165
246
  }
166
247
  } catch {
167
- console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
248
+ console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
168
249
  console.error(
169
250
  ` ⚠️ Could not read IAM policy directly. Ensure the delegation commands below were executed.`,
170
251
  );
171
252
  }
172
253
  } else {
173
- console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
254
+ console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
174
255
  console.error(` ⏸️ Skipped until project access is resolved.`);
175
256
  }
176
257
 
177
- // 4. Check Backend Connectivity
258
+ // 5. Check Backend Connectivity
178
259
  try {
179
260
  const res = await fetch(`${BASE_URL}/health`);
180
261
  if (res.ok) {
181
- console.error(`\n [4/4] EditNBLM Cloud Backend:`);
262
+ console.error(`\n [5/5] EditNBLM Cloud Backend:`);
182
263
  console.error(` ✓ Status: Connected (${BASE_URL})`);
183
264
  } else {
184
- console.error(`\n [4/4] EditNBLM Cloud Backend:`);
265
+ console.error(`\n [5/5] EditNBLM Cloud Backend:`);
185
266
  console.error(` ⚠️ Status: HTTP ${res.status}`);
186
267
  }
187
268
  } catch (err) {
188
- console.error(`\n [4/4] EditNBLM Cloud Backend:`);
269
+ console.error(`\n [5/5] EditNBLM Cloud Backend:`);
189
270
  console.error(` ✗ Status: Unreachable (${err.message})`);
190
271
  }
191
272
 
@@ -225,7 +306,10 @@ if (
225
306
  ) {
226
307
  runDoctor();
227
308
  } else {
228
- startMcpServer();
309
+ startMcpServer().catch((err) => {
310
+ console.error('[EditNBLM] Fatal error:', err);
311
+ process.exit(1);
312
+ });
229
313
  }
230
314
 
231
315
  async function callRemoteMcp(method, params, apiKey) {
@@ -336,7 +420,7 @@ async function downloadPptxHelper(downloadUrl, targetPath) {
336
420
  if (res.ok) {
337
421
  const buf = Buffer.from(await res.arrayBuffer());
338
422
  await fs.promises.writeFile(targetPath, buf);
339
- return targetPath;
423
+ return { path: targetPath, buffer: buf };
340
424
  }
341
425
  } catch (err) {
342
426
  console.error(`[EditNBLM] Failed to download PPTX locally: ${err.message}`);
@@ -344,14 +428,25 @@ async function downloadPptxHelper(downloadUrl, targetPath) {
344
428
  return null;
345
429
  }
346
430
 
347
- function assertEnvironmentOrExit() {
431
+ async function assertEnvironmentOrExit() {
348
432
  const account = getAccount();
349
433
  const token = getAuthToken();
350
434
  const project = getProject();
351
435
 
352
436
  if (!account || !token) {
353
437
  console.error('\n[EditNBLM] ❌ Activation Failed: Google Cloud authentication required.');
354
- console.error('[EditNBLM] Please run `gcloud auth login` before activating this MCP server.');
438
+ console.error('[EditNBLM] Please run `gcloud auth login --enable-gdrive-access` before activating this MCP server.');
439
+ console.error('[EditNBLM] Run `npx -y @jieunmarslim/server-editable-slides doctor` to diagnose.\n');
440
+ process.exit(1);
441
+ }
442
+
443
+ // Verify Google Drive scope
444
+ const hasDrive = await checkDriveScope(token);
445
+ if (!hasDrive) {
446
+ console.error(`\n[EditNBLM] ❌ Activation Failed: Account [${account}] lacks Google Drive access.`);
447
+ console.error('[EditNBLM] EditNBLM MCP requires Google Drive scope to automatically convert presentations into live Google Slides.');
448
+ console.error('[EditNBLM] 👉 Please run in your terminal:');
449
+ console.error('[EditNBLM] gcloud auth login --enable-gdrive-access\n');
355
450
  console.error('[EditNBLM] Run `npx -y @jieunmarslim/server-editable-slides doctor` to diagnose.\n');
356
451
  process.exit(1);
357
452
  }
@@ -381,7 +476,7 @@ function assertEnvironmentOrExit() {
381
476
  stderr.includes('does not have permission') ||
382
477
  stderr.includes('caller does not have permission')
383
478
  ) {
384
- console.error(`[EditNBLM] 👉 Solution 1: Log in with an account that has access: gcloud auth login`);
479
+ console.error(`[EditNBLM] 👉 Solution 1: Log in with an account that has access: gcloud auth login --enable-gdrive-access`);
385
480
  console.error(`[EditNBLM] 👉 Solution 2: Switch to a project that [${account}] can access: gcloud config set project <PROJECT>`);
386
481
  } else if (stderr.includes('not found') || stderr.includes('404')) {
387
482
  console.error(`[EditNBLM] 👉 Project [${project}] does not exist. Run: gcloud config set project <VALID_PROJECT_ID>`);
@@ -393,8 +488,8 @@ function assertEnvironmentOrExit() {
393
488
  }
394
489
  }
395
490
 
396
- function startMcpServer() {
397
- assertEnvironmentOrExit();
491
+ async function startMcpServer() {
492
+ await assertEnvironmentOrExit();
398
493
 
399
494
  const server = new McpServer({
400
495
  name: 'editable-slides',
@@ -459,14 +554,18 @@ function startMcpServer() {
459
554
 
460
555
  const parsed = JSON.parse(startResult.content[0].text);
461
556
  if (parsed.jobId) {
462
- activeJobs.set(parsed.jobId, { filePath, fileName: originalName });
557
+ activeJobs.set(parsed.jobId, {
558
+ filePath,
559
+ fileName: originalName,
560
+ format: format || 'slides',
561
+ });
463
562
  parsed.note =
464
563
  'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
465
564
  'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
466
565
  'DO NOT write custom scripts or install packages.';
467
566
  parsed.agentInstructions = [
468
567
  `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
469
- 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally.',
568
+ 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally and/or uploaded to Google Drive.',
470
569
  ];
471
570
  startResult.content[0].text = JSON.stringify(parsed, null, 2);
472
571
  }
@@ -491,16 +590,48 @@ function startMcpServer() {
491
590
  const parsed = JSON.parse(result.content[0].text);
492
591
  if (parsed.downloadUrl && filePath) {
493
592
  const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
494
- await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
593
+ const downloaded = await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
495
594
  parsed.localPptxPath = localPptxPath;
595
+
596
+ // If format is 'slides' (default), automatically upload to Google Drive if scope is granted
597
+ if (format !== 'pptx' && downloaded?.buffer) {
598
+ const token = getAuthToken();
599
+ const hasDrive = await checkDriveScope(token);
600
+ if (hasDrive) {
601
+ try {
602
+ console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
603
+ const driveResult = await uploadPptxToGoogleDrive(
604
+ downloaded.buffer,
605
+ path.basename(localPptxPath),
606
+ token,
607
+ );
608
+ parsed.presentationUrl = driveResult.presentationUrl;
609
+ parsed.presentationId = driveResult.presentationId;
610
+ parsed.googleSlidesCreated = true;
611
+ console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
612
+ } catch (driveErr) {
613
+ console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
614
+ parsed.driveError = driveErr.message;
615
+ }
616
+ } else {
617
+ parsed.driveNotice =
618
+ 'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
619
+ }
620
+ }
496
621
  }
497
- if (parsed.downloadUrl || parsed.localPptxPath) {
622
+
623
+ if (parsed.presentationUrl) {
498
624
  parsed.agentInstructions = [
499
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
500
- 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert or upload this file.',
501
- '1. If you already have an active Google Drive MCP tool (e.g. drive.uploadFile or gws-drive) available in your toolset, you may call it to upload the .pptx file with Google Slides conversion.',
502
- '2. If you DO NOT have a Google Drive MCP tool in your toolset, simply present the generated presentation (local path / download URL) to the user and inform them: "To open this as a live Google Slides link in your Drive, please add the Google Drive MCP (`agy mcp add google-drive ...`) or drag the .pptx file into Google Drive."',
625
+ 'TASK FINISHED: Google Slides presentation created successfully in the user\'s Google Drive!',
626
+ `Live URL: ${parsed.presentationUrl}`,
627
+ 'STRICT MANDATE FOR AGENT: Present the live Google Slides link directly to the user so they can click and open it in Chrome. DO NOT write custom scripts.',
503
628
  ];
629
+ } else if (parsed.downloadUrl || parsed.localPptxPath) {
630
+ parsed.agentInstructions = [
631
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
632
+ parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
633
+ parsed.driveNotice || null,
634
+ ].filter(Boolean);
504
635
  }
505
636
  result.content[0].text = JSON.stringify(parsed, null, 2);
506
637
  } catch {
@@ -622,17 +753,49 @@ function startMcpServer() {
622
753
  destPath = path.resolve(process.cwd(), `${safeName}.pptx`);
623
754
  }
624
755
 
625
- await downloadPptxHelper(parsed.downloadUrl, destPath);
756
+ const downloaded = await downloadPptxHelper(parsed.downloadUrl, destPath);
626
757
  parsed.localPptxPath = destPath;
758
+
759
+ // If format is 'slides' (default), automatically upload to Google Drive if scope is granted
760
+ if (jobInfo?.format !== 'pptx' && downloaded?.buffer) {
761
+ const token = getAuthToken();
762
+ const hasDrive = await checkDriveScope(token);
763
+ if (hasDrive) {
764
+ try {
765
+ console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
766
+ const driveResult = await uploadPptxToGoogleDrive(
767
+ downloaded.buffer,
768
+ path.basename(destPath),
769
+ token,
770
+ );
771
+ parsed.presentationUrl = driveResult.presentationUrl;
772
+ parsed.presentationId = driveResult.presentationId;
773
+ parsed.googleSlidesCreated = true;
774
+ console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
775
+ } catch (driveErr) {
776
+ console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
777
+ parsed.driveError = driveErr.message;
778
+ }
779
+ } else {
780
+ parsed.driveNotice =
781
+ 'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
782
+ }
783
+ }
627
784
  }
628
785
 
629
- parsed.agentInstructions = [
630
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
631
- parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
632
- 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert this file.',
633
- '1. If you have an active Google Drive MCP tool (e.g. drive.uploadFile or gws-drive), call it now to upload the .pptx file with Google Slides conversion.',
634
- '2. If you DO NOT have a Google Drive MCP tool, present the local file path and download URL to the user directly.',
635
- ].filter(Boolean);
786
+ if (parsed.presentationUrl) {
787
+ parsed.agentInstructions = [
788
+ 'TASK FINISHED: Google Slides presentation created successfully in the user\'s Google Drive!',
789
+ `Live URL: ${parsed.presentationUrl}`,
790
+ 'STRICT MANDATE FOR AGENT: Present the live Google Slides link directly to the user so they can click and open it in Chrome. DO NOT write custom scripts.',
791
+ ];
792
+ } else {
793
+ parsed.agentInstructions = [
794
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
795
+ parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
796
+ parsed.driveNotice || null,
797
+ ].filter(Boolean);
798
+ }
636
799
  }
637
800
 
638
801
  result.content[0].text = JSON.stringify(parsed, null, 2);
@@ -642,6 +805,81 @@ function startMcpServer() {
642
805
  },
643
806
  );
644
807
 
808
+ // 4. Dedicated Google Drive Upload & Convert Tool
809
+ server.tool(
810
+ 'upload_to_google_drive',
811
+ 'Upload a local presentation (.pptx) file to Google Drive and convert it into a native, live Google Slides presentation. Returns the direct Google Slides URL (https://docs.google.com/presentation/d/.../edit). Requires gcloud auth login --enable-gdrive-access.',
812
+ {
813
+ filePath: z
814
+ .string()
815
+ .describe('Local file path to the .pptx presentation file to upload'),
816
+ title: z
817
+ .string()
818
+ .optional()
819
+ .describe('Optional title for the Google Slides presentation'),
820
+ },
821
+ async ({ filePath, title }) => {
822
+ const resolvedPath = path.resolve(process.cwd(), filePath);
823
+ if (!fs.existsSync(resolvedPath)) {
824
+ return {
825
+ content: [
826
+ {
827
+ type: 'text',
828
+ text: JSON.stringify({ error: `File not found: ${filePath}` }),
829
+ },
830
+ ],
831
+ isError: true,
832
+ };
833
+ }
834
+
835
+ const token = getAuthToken();
836
+ const hasDrive = await checkDriveScope(token);
837
+ if (!hasDrive) {
838
+ return {
839
+ content: [
840
+ {
841
+ type: 'text',
842
+ text: JSON.stringify(
843
+ {
844
+ error:
845
+ 'Google Drive access scope is missing on the current token. ' +
846
+ 'Please run this one-time command in your terminal to enable Google Drive access: ' +
847
+ 'gcloud auth login --enable-gdrive-access',
848
+ },
849
+ null,
850
+ 2,
851
+ ),
852
+ },
853
+ ],
854
+ isError: true,
855
+ };
856
+ }
857
+
858
+ const pptxBuf = await fs.promises.readFile(resolvedPath);
859
+ const deckTitle = title || path.basename(resolvedPath).replace(/\.pptx$/i, '');
860
+ const driveResult = await uploadPptxToGoogleDrive(pptxBuf, deckTitle, token);
861
+
862
+ return {
863
+ content: [
864
+ {
865
+ type: 'text',
866
+ text: JSON.stringify(
867
+ {
868
+ success: true,
869
+ presentationUrl: driveResult.presentationUrl,
870
+ presentationId: driveResult.presentationId,
871
+ title: driveResult.title,
872
+ note: 'Successfully uploaded to Google Drive and converted to Google Slides.',
873
+ },
874
+ null,
875
+ 2,
876
+ ),
877
+ },
878
+ ],
879
+ };
880
+ },
881
+ );
882
+
645
883
  const transport = new StdioServerTransport();
646
884
  server.connect(transport).then(() => {
647
885
  const project = getProject();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jieunmarslim/server-editable-slides",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {