@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.
- package/index.js +255 -32
- 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/
|
|
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/
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
//
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
//
|
|
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 [
|
|
261
|
+
console.error(`\n [5/5] EditNBLM Cloud Backend:`);
|
|
182
262
|
console.error(` ✓ Status: Connected (${BASE_URL})`);
|
|
183
263
|
} else {
|
|
184
|
-
console.error(`\n [
|
|
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 [
|
|
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, {
|
|
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
|
-
|
|
607
|
+
|
|
608
|
+
if (parsed.presentationUrl) {
|
|
498
609
|
parsed.agentInstructions = [
|
|
499
|
-
'TASK FINISHED:
|
|
500
|
-
|
|
501
|
-
'
|
|
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.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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();
|