@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.
- package/index.js +133 -45
- 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('
|
|
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(
|
|
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('
|
|
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]
|
|
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]
|
|
555
|
+
console.error(`[EditNBLM] ❌ Error: Project [${project}] not found. Run: gcloud config set project <VALID_PROJECT_ID>`);
|
|
483
556
|
} else {
|
|
484
|
-
console.error(`[EditNBLM]
|
|
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
|
|
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
|
|
626
|
-
`
|
|
627
|
-
'
|
|
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 (
|
|
701
|
+
} else if (format === 'pptx') {
|
|
630
702
|
parsed.agentInstructions = [
|
|
631
|
-
'TASK
|
|
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
|
|
789
|
-
`
|
|
790
|
-
'
|
|
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
|
|
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).
|
|
885
|
-
|
|
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
|
}
|