@jieunmarslim/server-editable-slides 0.2.6 → 0.2.8

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 +125 -18
  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 {
@@ -499,7 +582,7 @@ async function startMcpServer() {
499
582
  // 1. Fast Synchronous Slide Conversion (Single Image)
500
583
  server.tool(
501
584
  '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.',
585
+ '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
586
  {
504
587
  filePath: z
505
588
  .string()
@@ -612,26 +695,32 @@ async function startMcpServer() {
612
695
  } catch (driveErr) {
613
696
  console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
614
697
  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
698
  }
620
699
  }
700
+ // Delete internal GCS signed URL so agent only uses live Google Slides URL or local file
701
+ delete parsed.downloadUrl;
702
+ }
621
703
  }
622
704
 
623
705
  if (parsed.presentationUrl) {
624
706
  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.',
707
+ 'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
708
+ `LIVE URL: ${parsed.presentationUrl}`,
709
+ 'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
710
+ '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
711
  ];
629
- } else if (parsed.downloadUrl || parsed.localPptxPath) {
712
+ } else if (format === 'pptx') {
630
713
  parsed.agentInstructions = [
631
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
714
+ 'TASK COMPLETE: PowerPoint file generated successfully.',
632
715
  parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
633
- parsed.driveNotice || null,
634
716
  ].filter(Boolean);
717
+ } else {
718
+ parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
719
+ parsed.agentInstructions = [
720
+ 'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
721
+ 'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
722
+ 'Tell the user directly: "구글 슬라이드로 자동 생성하려면 구글 드라이브 접근 권한이 필요합니다. 터미널에서 아래 명령어를 1회 실행해주세요:\n\n gcloud auth login --enable-gdrive-access"',
723
+ ];
635
724
  }
636
725
  result.content[0].text = JSON.stringify(parsed, null, 2);
637
726
  } catch {
@@ -699,7 +788,7 @@ async function startMcpServer() {
699
788
  try {
700
789
  const parsed = JSON.parse(startResult.content[0].text);
701
790
  if (parsed.jobId) {
702
- activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName });
791
+ activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName, format: format || 'slides' });
703
792
  parsed.agentInstructions = [
704
793
  `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
705
794
  'DO NOT write scripts or install packages while waiting.',
@@ -781,21 +870,39 @@ async function startMcpServer() {
781
870
  'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
782
871
  }
783
872
  }
873
+ delete parsed.downloadUrl;
784
874
  }
785
875
 
786
876
  if (parsed.presentationUrl) {
787
877
  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.',
878
+ 'TASK COMPLETE: Live Google Slides presentation created successfully in user\'s Google Drive!',
879
+ `LIVE URL: ${parsed.presentationUrl}`,
880
+ 'MANDATORY: Output ONLY the live Google Slides link directly (e.g. "👉 [Google Slides Link](' + parsed.presentationUrl + ')").',
881
+ '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
882
  ];
792
- } else {
883
+ } else if (jobInfo?.format === 'pptx') {
793
884
  parsed.agentInstructions = [
794
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
885
+ 'TASK COMPLETE: PowerPoint file generated successfully.',
795
886
  parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
796
- parsed.driveNotice || null,
797
887
  ].filter(Boolean);
888
+ } else {
889
+ parsed.error = 'GOOGLE_DRIVE_SCOPE_MISSING';
890
+ parsed.agentInstructions = [
891
+ 'CRITICAL ERROR: Failed to create live Google Slides presentation because your active gcloud account lacks the Google Drive scope.',
892
+ 'STRICT MANDATE: DO NOT tell the user how to manually import or open PowerPoint files.',
893
+ 'Tell the user directly: "구글 슬라이드로 자동 생성하려면 구글 드라이브 접근 권한이 필요합니다. 터미널에서 아래 명령어를 1회 실행해주세요:\n\n gcloud auth login --enable-gdrive-access"',
894
+ ];
798
895
  }
896
+ } else if (parsed.status === 'running') {
897
+ const percent =
898
+ parsed.progressPercent ??
899
+ (parsed.totalPages > 0 ? Math.round((parsed.pagesDone / parsed.totalPages) * 100) : 0);
900
+ parsed.progressSummary = `${parsed.pagesDone || 0}/${parsed.totalPages || 0} slides complete (${percent}%)`;
901
+ parsed.agentInstructions = [
902
+ `PROGRESS: ${parsed.pagesDone || 0} of ${parsed.totalPages || 0} slides completed (${percent}%).`,
903
+ `CURRENT STAGE: ${parsed.stage || 'Inpainting & Processing'}`,
904
+ '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".',
905
+ ];
799
906
  }
800
907
 
801
908
  result.content[0].text = JSON.stringify(parsed, null, 2);
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.8",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {