@jieunmarslim/server-editable-slides 0.2.6 → 0.2.9

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 +133 -45
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -73,7 +73,86 @@ async function checkDriveScope(token) {
73
73
  }
74
74
  }
75
75
 
76
+ async function uploadPptxResumable(pptxBuffer, title, token) {
77
+ const cleanTitle = (title || 'Presentation').replace(/\.pptx$/i, '');
78
+ const pptxContentType =
79
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
80
+
81
+ const initRes = await fetch(
82
+ 'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable',
83
+ {
84
+ method: 'POST',
85
+ headers: {
86
+ Authorization: `Bearer ${token}`,
87
+ 'Content-Type': 'application/json; charset=UTF-8',
88
+ 'X-Upload-Content-Type': pptxContentType,
89
+ 'X-Upload-Content-Length': String(pptxBuffer.length),
90
+ },
91
+ body: JSON.stringify({
92
+ name: cleanTitle,
93
+ mimeType: 'application/vnd.google-apps.presentation',
94
+ }),
95
+ },
96
+ );
97
+
98
+ if (!initRes.ok) {
99
+ const errText = await initRes.text();
100
+ let parsedMsg = errText;
101
+ try {
102
+ const j = JSON.parse(errText);
103
+ if (j.error && j.error.message) parsedMsg = j.error.message;
104
+ } catch {}
105
+ throw new Error(`Google Drive API Resumable Init error (${initRes.status}): ${parsedMsg}`);
106
+ }
107
+
108
+ const sessionUri = initRes.headers.get('location');
109
+ if (!sessionUri) {
110
+ throw new Error('Google Drive API did not return a resumable upload location header.');
111
+ }
112
+
113
+ const uploadRes = await fetch(sessionUri, {
114
+ method: 'PUT',
115
+ headers: {
116
+ 'Content-Length': String(pptxBuffer.length),
117
+ 'Content-Type': pptxContentType,
118
+ },
119
+ body: pptxBuffer,
120
+ });
121
+
122
+ if (!uploadRes.ok) {
123
+ const errText = await uploadRes.text();
124
+ let parsedMsg = errText;
125
+ try {
126
+ const j = JSON.parse(errText);
127
+ if (j.error && j.error.message) parsedMsg = j.error.message;
128
+ } catch {}
129
+ throw new Error(`Google Drive API Resumable Upload error (${uploadRes.status}): ${parsedMsg}`);
130
+ }
131
+
132
+ const fileData = await uploadRes.json();
133
+ return {
134
+ presentationId: fileData.id,
135
+ presentationUrl: `https://docs.google.com/presentation/d/${fileData.id}/edit`,
136
+ title: fileData.name,
137
+ };
138
+ }
139
+
76
140
  async function uploadPptxToGoogleDrive(pptxBuffer, title, token) {
141
+ // Google Slides has a hard 100MB limit for presentations converted to Google Slides format
142
+ const MAX_SLIDES_CONVERT_BYTES = 100 * 1024 * 1024;
143
+ if (pptxBuffer.length > MAX_SLIDES_CONVERT_BYTES) {
144
+ const mb = (pptxBuffer.length / (1024 * 1024)).toFixed(1);
145
+ throw new Error(
146
+ `Generated presentation deck is ${mb}MB, which exceeds Google Drive's maximum conversion limit for Google Slides (100MB).`,
147
+ );
148
+ }
149
+
150
+ // Files > 5MB require resumable upload to avoid HTTP 413 (Request Entity Too Large)
151
+ if (pptxBuffer.length > 5 * 1024 * 1024) {
152
+ return uploadPptxResumable(pptxBuffer, title, token);
153
+ }
154
+
155
+ // Small files (<= 5MB): fast single-request multipart upload with automatic resumable fallback
77
156
  const boundary = '-------EditNBLM' + Math.random().toString(36).substring(2);
78
157
  const delimiter = `\r\n--${boundary}\r\n`;
79
158
  const closeDelimiter = `\r\n--${boundary}--`;
@@ -109,6 +188,10 @@ async function uploadPptxToGoogleDrive(pptxBuffer, title, token) {
109
188
  );
110
189
 
111
190
  if (!res.ok) {
191
+ if (res.status === 413) {
192
+ // If multipart upload triggers 413, automatically fall back to resumable upload
193
+ return uploadPptxResumable(pptxBuffer, title, token);
194
+ }
112
195
  const errText = await res.text();
113
196
  let parsedMsg = errText;
114
197
  try {
@@ -434,27 +517,19 @@ async function assertEnvironmentOrExit() {
434
517
  const project = getProject();
435
518
 
436
519
  if (!account || !token) {
437
- console.error('\n[EditNBLM] ❌ Activation Failed: Google Cloud authentication required.');
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');
520
+ console.error('[EditNBLM] ❌ Error: Not authenticated. Run: gcloud auth login --enable-gdrive-access');
440
521
  process.exit(1);
441
522
  }
442
523
 
443
524
  // Verify Google Drive scope
444
525
  const hasDrive = await checkDriveScope(token);
445
526
  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');
450
- console.error('[EditNBLM] Run `npx -y @jieunmarslim/server-editable-slides doctor` to diagnose.\n');
527
+ console.error(`[EditNBLM] ❌ Error: Google Drive scope missing. Run: gcloud auth login --enable-gdrive-access`);
451
528
  process.exit(1);
452
529
  }
453
530
 
454
531
  if (!project) {
455
- console.error('\n[EditNBLM] ❌ Activation Failed: No GCP Project configured for GEAP billing.');
456
- console.error('[EditNBLM] Please run `gcloud config set project <PROJECT_ID>` or export EDITABLE_SLIDES_PROJECT.');
457
- console.error('[EditNBLM] Run `npx -y @jieunmarslim/server-editable-slides doctor` to diagnose.\n');
532
+ console.error('[EditNBLM] ❌ Error: No GCP project set. Run: gcloud config set project <PROJECT_ID>');
458
533
  process.exit(1);
459
534
  }
460
535
 
@@ -471,19 +546,16 @@ async function assertEnvironmentOrExit() {
471
546
  }
472
547
  } catch (err) {
473
548
  const stderr = err.stderr ? err.stderr.toString() : err.message;
474
- console.error(`\n[EditNBLM] ❌ Activation Failed: Account [${account}] does NOT have access to project [${project}].`);
475
549
  if (
476
550
  stderr.includes('does not have permission') ||
477
551
  stderr.includes('caller does not have permission')
478
552
  ) {
479
- console.error(`[EditNBLM] 👉 Solution 1: Log in with an account that has access: gcloud auth login --enable-gdrive-access`);
480
- console.error(`[EditNBLM] 👉 Solution 2: Switch to a project that [${account}] can access: gcloud config set project <PROJECT>`);
553
+ console.error(`[EditNBLM] Error: Account [${account}] lacks permission for project [${project}]. Run: gcloud config set project <PROJECT_ID>`);
481
554
  } else if (stderr.includes('not found') || stderr.includes('404')) {
482
- console.error(`[EditNBLM] 👉 Project [${project}] does not exist. Run: gcloud config set project <VALID_PROJECT_ID>`);
555
+ console.error(`[EditNBLM] Error: Project [${project}] not found. Run: gcloud config set project <VALID_PROJECT_ID>`);
483
556
  } else {
484
- console.error(`[EditNBLM] 👉 Details: ${stderr.trim()}`);
557
+ console.error(`[EditNBLM] Error: Cannot access project [${project}] (${stderr.trim().split('\n')[0]}).`);
485
558
  }
486
- console.error('[EditNBLM] Run `npx -y @jieunmarslim/server-editable-slides doctor` to diagnose.\n');
487
559
  process.exit(1);
488
560
  }
489
561
  }
@@ -499,7 +571,7 @@ async function startMcpServer() {
499
571
  // 1. Fast Synchronous Slide Conversion (Single Image)
500
572
  server.tool(
501
573
  'create_slides_from_image',
502
- 'Convert a single presentation slide image (PNG/JPG) synchronously into an editable deck. For multi-page PDFs or multi-slide documents, this tool automatically triggers background conversion to prevent client timeouts. DO NOT write code or install packages; present the output directly or use existing Google Drive MCP tools.',
574
+ 'Convert a single presentation slide image (PNG/JPG) or document synchronously into a live Google Slides presentation (docs.google.com/presentation/d/.../edit). For multi-page PDFs or multi-slide documents, this tool automatically triggers background conversion. Returns the direct live Google Slides URL.',
503
575
  {
504
576
  filePath: z
505
577
  .string()
@@ -612,26 +684,32 @@ async function startMcpServer() {
612
684
  } catch (driveErr) {
613
685
  console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
614
686
  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
687
  }
620
688
  }
689
+ // Delete internal GCS signed URL so agent only uses live Google Slides URL or local file
690
+ delete parsed.downloadUrl;
691
+ }
621
692
  }
622
693
 
623
694
  if (parsed.presentationUrl) {
624
695
  parsed.agentInstructions = [
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.',
696
+ 'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
697
+ `LIVE URL: ${parsed.presentationUrl}`,
698
+ 'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
699
+ 'STRICT MANDATE: DO NOT explain how to open files, DO NOT mention PowerPoint or local files, DO NOT give drag-and-drop tutorials. Output only the live link.',
628
700
  ];
629
- } else if (parsed.downloadUrl || parsed.localPptxPath) {
701
+ } else if (format === 'pptx') {
630
702
  parsed.agentInstructions = [
631
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
703
+ 'TASK COMPLETE: PowerPoint file generated successfully.',
632
704
  parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
633
- parsed.driveNotice || null,
634
705
  ].filter(Boolean);
706
+ } else {
707
+ parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
708
+ parsed.agentInstructions = [
709
+ 'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
710
+ 'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
711
+ 'Tell the user directly: "구글 슬라이드로 자동 생성하려면 구글 드라이브 접근 권한이 필요합니다. 터미널에서 아래 명령어를 1회 실행해주세요:\n\n gcloud auth login --enable-gdrive-access"',
712
+ ];
635
713
  }
636
714
  result.content[0].text = JSON.stringify(parsed, null, 2);
637
715
  } catch {
@@ -699,7 +777,7 @@ async function startMcpServer() {
699
777
  try {
700
778
  const parsed = JSON.parse(startResult.content[0].text);
701
779
  if (parsed.jobId) {
702
- activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName });
780
+ activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName, format: format || 'slides' });
703
781
  parsed.agentInstructions = [
704
782
  `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
705
783
  'DO NOT write scripts or install packages while waiting.',
@@ -781,21 +859,39 @@ async function startMcpServer() {
781
859
  'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
782
860
  }
783
861
  }
862
+ delete parsed.downloadUrl;
784
863
  }
785
864
 
786
865
  if (parsed.presentationUrl) {
787
866
  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.',
867
+ 'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
868
+ `LIVE URL: ${parsed.presentationUrl}`,
869
+ 'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
870
+ 'STRICT MANDATE: DO NOT explain how to open files, DO NOT mention PowerPoint or local files, DO NOT give drag-and-drop tutorials. Output only the live link.',
791
871
  ];
792
- } else {
872
+ } else if (jobInfo?.format === 'pptx') {
793
873
  parsed.agentInstructions = [
794
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
874
+ 'TASK COMPLETE: PowerPoint file generated successfully.',
795
875
  parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
796
- parsed.driveNotice || null,
797
876
  ].filter(Boolean);
877
+ } else {
878
+ parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
879
+ parsed.agentInstructions = [
880
+ 'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
881
+ 'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
882
+ 'Tell the user directly: "구글 슬라이드로 자동 생성하려면 구글 드라이브 접근 권한이 필요합니다. 터미널에서 아래 명령어를 1회 실행해주세요:\n\n gcloud auth login --enable-gdrive-access"',
883
+ ];
798
884
  }
885
+ } else if (parsed.status === 'running') {
886
+ const percent =
887
+ parsed.progressPercent ??
888
+ (parsed.totalPages > 0 ? Math.round((parsed.pagesDone / parsed.totalPages) * 100) : 0);
889
+ parsed.progressSummary = `${parsed.pagesDone || 0}/${parsed.totalPages || 0} slides complete (${percent}%)`;
890
+ parsed.agentInstructions = [
891
+ `PROGRESS: ${parsed.pagesDone || 0} of ${parsed.totalPages || 0} slides completed (${percent}%).`,
892
+ `CURRENT STAGE: ${parsed.stage || 'Inpainting & Processing'}`,
893
+ 'STRICT INSTRUCTION: Do NOT output duplicate messages to the user if the stage or slide count has not changed. Silently poll check_slide_conversion until status is "done".',
894
+ ];
799
895
  }
800
896
 
801
897
  result.content[0].text = JSON.stringify(parsed, null, 2);
@@ -881,16 +977,8 @@ async function startMcpServer() {
881
977
  );
882
978
 
883
979
  const transport = new StdioServerTransport();
884
- server.connect(transport).then(() => {
885
- const project = getProject();
886
- if (project) {
887
- console.error(`[EditNBLM] Ready. Connected to EditNBLM Cloud (Billing Project: ${project}).`);
888
- } else {
889
- console.error('[EditNBLM] ⚠️ Warning: No active GCP project detected for billing.');
890
- console.error('[EditNBLM] Run `npx @jieunmarslim/server-editable-slides doctor` to configure.');
891
- }
892
- }).catch((err) => {
893
- console.error('[EditNBLM] Fatal error:', err);
980
+ server.connect(transport).catch((err) => {
981
+ console.error(`[EditNBLM] Fatal error: ${err.message}`);
894
982
  process.exit(1);
895
983
  });
896
984
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jieunmarslim/server-editable-slides",
3
- "version": "0.2.6",
3
+ "version": "0.2.9",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {