@jieunmarslim/server-editable-slides 0.2.4 → 0.2.5

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 +255 -32
  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,32 @@ 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
+ console.error(` ⚠️ Scope Missing: Token lacks Google Drive access.`);
159
+ console.error(` 👉 To enable automatic Google Slides creation in your Drive, run:`);
160
+ console.error(` gcloud auth login --enable-gdrive-access`);
81
161
  }
82
162
 
83
163
  // 2. Check Project Configuration & Account Access Permissions
84
164
  let projectAccessOk = false;
85
165
  if (!project) {
86
166
  hasError = true;
87
- console.error(`\n [2/4] GEAP Billing & Quota Project:`);
167
+ console.error(`\n [3/5] GEAP Billing & Quota Project:`);
88
168
  console.error(` ✗ FAILED: No GCP project configured.`);
89
169
  console.error(` 👉 Please run: gcloud config set project YOUR_GCP_PROJECT_ID`);
90
170
  console.error(` 👉 Or export: export EDITABLE_SLIDES_PROJECT=YOUR_GCP_PROJECT_ID`);
@@ -99,7 +179,7 @@ async function runDoctor() {
99
179
  .trim();
100
180
  if (check === project) {
101
181
  projectAccessOk = true;
102
- console.error(`\n [2/4] Target GCP Project Accessibility:`);
182
+ console.error(`\n [3/5] Target GCP Project Accessibility:`);
103
183
  console.error(
104
184
  ` ✓ Verified: Account [${account}] has active access to project [${project}].`,
105
185
  );
@@ -107,7 +187,7 @@ async function runDoctor() {
107
187
  } catch (err) {
108
188
  hasError = true;
109
189
  const stderr = err.stderr ? err.stderr.toString() : err.message;
110
- console.error(`\n [2/4] Target GCP Project Accessibility:`);
190
+ console.error(`\n [3/5] Target GCP Project Accessibility:`);
111
191
  if (
112
192
  stderr.includes('does not have permission') ||
113
193
  stderr.includes('caller does not have permission')
@@ -118,7 +198,7 @@ async function runDoctor() {
118
198
  console.error(
119
199
  ` 👉 Solution 1: Log in with the account that has access to [${project}]:`,
120
200
  );
121
- console.error(` gcloud auth login`);
201
+ console.error(` gcloud auth login --enable-gdrive-access`);
122
202
  console.error(
123
203
  ` 👉 Solution 2: Switch to a project that [${account}] can access:`,
124
204
  );
@@ -140,7 +220,7 @@ async function runDoctor() {
140
220
  }
141
221
  }
142
222
 
143
- // 3. Verify One-Time IAM Delegation to EditNBLM Service Account
223
+ // 4. Verify One-Time IAM Delegation to EditNBLM Service Account
144
224
  if (projectAccessOk) {
145
225
  try {
146
226
  const bindings = execSync(
@@ -151,7 +231,7 @@ async function runDoctor() {
151
231
  'serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com',
152
232
  );
153
233
 
154
- console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
234
+ console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
155
235
  if (hasAiPlatform) {
156
236
  console.error(
157
237
  ` ✓ GRANTED: Service account has roles/aiplatform.user on [${project}].`,
@@ -164,28 +244,28 @@ async function runDoctor() {
164
244
  console.error(` 👉 Please run the delegation commands below.`);
165
245
  }
166
246
  } catch {
167
- console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
247
+ console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
168
248
  console.error(
169
249
  ` ⚠️ Could not read IAM policy directly. Ensure the delegation commands below were executed.`,
170
250
  );
171
251
  }
172
252
  } else {
173
- console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
253
+ console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
174
254
  console.error(` ⏸️ Skipped until project access is resolved.`);
175
255
  }
176
256
 
177
- // 4. Check Backend Connectivity
257
+ // 5. Check Backend Connectivity
178
258
  try {
179
259
  const res = await fetch(`${BASE_URL}/health`);
180
260
  if (res.ok) {
181
- console.error(`\n [4/4] EditNBLM Cloud Backend:`);
261
+ console.error(`\n [5/5] EditNBLM Cloud Backend:`);
182
262
  console.error(` ✓ Status: Connected (${BASE_URL})`);
183
263
  } else {
184
- console.error(`\n [4/4] EditNBLM Cloud Backend:`);
264
+ console.error(`\n [5/5] EditNBLM Cloud Backend:`);
185
265
  console.error(` ⚠️ Status: HTTP ${res.status}`);
186
266
  }
187
267
  } catch (err) {
188
- console.error(`\n [4/4] EditNBLM Cloud Backend:`);
268
+ console.error(`\n [5/5] EditNBLM Cloud Backend:`);
189
269
  console.error(` ✗ Status: Unreachable (${err.message})`);
190
270
  }
191
271
 
@@ -336,7 +416,7 @@ async function downloadPptxHelper(downloadUrl, targetPath) {
336
416
  if (res.ok) {
337
417
  const buf = Buffer.from(await res.arrayBuffer());
338
418
  await fs.promises.writeFile(targetPath, buf);
339
- return targetPath;
419
+ return { path: targetPath, buffer: buf };
340
420
  }
341
421
  } catch (err) {
342
422
  console.error(`[EditNBLM] Failed to download PPTX locally: ${err.message}`);
@@ -459,14 +539,18 @@ function startMcpServer() {
459
539
 
460
540
  const parsed = JSON.parse(startResult.content[0].text);
461
541
  if (parsed.jobId) {
462
- activeJobs.set(parsed.jobId, { filePath, fileName: originalName });
542
+ activeJobs.set(parsed.jobId, {
543
+ filePath,
544
+ fileName: originalName,
545
+ format: format || 'slides',
546
+ });
463
547
  parsed.note =
464
548
  'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
465
549
  'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
466
550
  'DO NOT write custom scripts or install packages.';
467
551
  parsed.agentInstructions = [
468
552
  `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.',
553
+ 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally and/or uploaded to Google Drive.',
470
554
  ];
471
555
  startResult.content[0].text = JSON.stringify(parsed, null, 2);
472
556
  }
@@ -491,16 +575,48 @@ function startMcpServer() {
491
575
  const parsed = JSON.parse(result.content[0].text);
492
576
  if (parsed.downloadUrl && filePath) {
493
577
  const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
494
- await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
578
+ const downloaded = await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
495
579
  parsed.localPptxPath = localPptxPath;
580
+
581
+ // If format is 'slides' (default), automatically upload to Google Drive if scope is granted
582
+ if (format !== 'pptx' && downloaded?.buffer) {
583
+ const token = getAuthToken();
584
+ const hasDrive = await checkDriveScope(token);
585
+ if (hasDrive) {
586
+ try {
587
+ console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
588
+ const driveResult = await uploadPptxToGoogleDrive(
589
+ downloaded.buffer,
590
+ path.basename(localPptxPath),
591
+ token,
592
+ );
593
+ parsed.presentationUrl = driveResult.presentationUrl;
594
+ parsed.presentationId = driveResult.presentationId;
595
+ parsed.googleSlidesCreated = true;
596
+ console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
597
+ } catch (driveErr) {
598
+ console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
599
+ parsed.driveError = driveErr.message;
600
+ }
601
+ } else {
602
+ parsed.driveNotice =
603
+ 'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
604
+ }
605
+ }
496
606
  }
497
- if (parsed.downloadUrl || parsed.localPptxPath) {
607
+
608
+ if (parsed.presentationUrl) {
498
609
  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."',
610
+ 'TASK FINISHED: Google Slides presentation created successfully in the user\'s Google Drive!',
611
+ `Live URL: ${parsed.presentationUrl}`,
612
+ '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
613
  ];
614
+ } else if (parsed.downloadUrl || parsed.localPptxPath) {
615
+ parsed.agentInstructions = [
616
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
617
+ parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
618
+ parsed.driveNotice || null,
619
+ ].filter(Boolean);
504
620
  }
505
621
  result.content[0].text = JSON.stringify(parsed, null, 2);
506
622
  } catch {
@@ -622,17 +738,49 @@ function startMcpServer() {
622
738
  destPath = path.resolve(process.cwd(), `${safeName}.pptx`);
623
739
  }
624
740
 
625
- await downloadPptxHelper(parsed.downloadUrl, destPath);
741
+ const downloaded = await downloadPptxHelper(parsed.downloadUrl, destPath);
626
742
  parsed.localPptxPath = destPath;
743
+
744
+ // If format is 'slides' (default), automatically upload to Google Drive if scope is granted
745
+ if (jobInfo?.format !== 'pptx' && downloaded?.buffer) {
746
+ const token = getAuthToken();
747
+ const hasDrive = await checkDriveScope(token);
748
+ if (hasDrive) {
749
+ try {
750
+ console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
751
+ const driveResult = await uploadPptxToGoogleDrive(
752
+ downloaded.buffer,
753
+ path.basename(destPath),
754
+ token,
755
+ );
756
+ parsed.presentationUrl = driveResult.presentationUrl;
757
+ parsed.presentationId = driveResult.presentationId;
758
+ parsed.googleSlidesCreated = true;
759
+ console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
760
+ } catch (driveErr) {
761
+ console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
762
+ parsed.driveError = driveErr.message;
763
+ }
764
+ } else {
765
+ parsed.driveNotice =
766
+ 'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
767
+ }
768
+ }
627
769
  }
628
770
 
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);
771
+ if (parsed.presentationUrl) {
772
+ parsed.agentInstructions = [
773
+ 'TASK FINISHED: Google Slides presentation created successfully in the user\'s Google Drive!',
774
+ `Live URL: ${parsed.presentationUrl}`,
775
+ '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.',
776
+ ];
777
+ } else {
778
+ parsed.agentInstructions = [
779
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
780
+ parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
781
+ parsed.driveNotice || null,
782
+ ].filter(Boolean);
783
+ }
636
784
  }
637
785
 
638
786
  result.content[0].text = JSON.stringify(parsed, null, 2);
@@ -642,6 +790,81 @@ function startMcpServer() {
642
790
  },
643
791
  );
644
792
 
793
+ // 4. Dedicated Google Drive Upload & Convert Tool
794
+ server.tool(
795
+ 'upload_to_google_drive',
796
+ '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.',
797
+ {
798
+ filePath: z
799
+ .string()
800
+ .describe('Local file path to the .pptx presentation file to upload'),
801
+ title: z
802
+ .string()
803
+ .optional()
804
+ .describe('Optional title for the Google Slides presentation'),
805
+ },
806
+ async ({ filePath, title }) => {
807
+ const resolvedPath = path.resolve(process.cwd(), filePath);
808
+ if (!fs.existsSync(resolvedPath)) {
809
+ return {
810
+ content: [
811
+ {
812
+ type: 'text',
813
+ text: JSON.stringify({ error: `File not found: ${filePath}` }),
814
+ },
815
+ ],
816
+ isError: true,
817
+ };
818
+ }
819
+
820
+ const token = getAuthToken();
821
+ const hasDrive = await checkDriveScope(token);
822
+ if (!hasDrive) {
823
+ return {
824
+ content: [
825
+ {
826
+ type: 'text',
827
+ text: JSON.stringify(
828
+ {
829
+ error:
830
+ 'Google Drive access scope is missing on the current token. ' +
831
+ 'Please run this one-time command in your terminal to enable Google Drive access: ' +
832
+ 'gcloud auth login --enable-gdrive-access',
833
+ },
834
+ null,
835
+ 2,
836
+ ),
837
+ },
838
+ ],
839
+ isError: true,
840
+ };
841
+ }
842
+
843
+ const pptxBuf = await fs.promises.readFile(resolvedPath);
844
+ const deckTitle = title || path.basename(resolvedPath).replace(/\.pptx$/i, '');
845
+ const driveResult = await uploadPptxToGoogleDrive(pptxBuf, deckTitle, token);
846
+
847
+ return {
848
+ content: [
849
+ {
850
+ type: 'text',
851
+ text: JSON.stringify(
852
+ {
853
+ success: true,
854
+ presentationUrl: driveResult.presentationUrl,
855
+ presentationId: driveResult.presentationId,
856
+ title: driveResult.title,
857
+ note: 'Successfully uploaded to Google Drive and converted to Google Slides.',
858
+ },
859
+ null,
860
+ 2,
861
+ ),
862
+ },
863
+ ],
864
+ };
865
+ },
866
+ );
867
+
645
868
  const transport = new StdioServerTransport();
646
869
  server.connect(transport).then(() => {
647
870
  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.5",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {