@jieunmarslim/server-editable-slides 0.2.4 → 0.2.6
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 +276 -38
- 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,33 @@ 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
|
+
hasError = true;
|
|
159
|
+
console.error(` ✗ FAILED: Token lacks Google Drive access.`);
|
|
160
|
+
console.error(` 👉 To enable automatic Google Slides creation in your Drive, run:`);
|
|
161
|
+
console.error(` gcloud auth login --enable-gdrive-access`);
|
|
81
162
|
}
|
|
82
163
|
|
|
83
164
|
// 2. Check Project Configuration & Account Access Permissions
|
|
84
165
|
let projectAccessOk = false;
|
|
85
166
|
if (!project) {
|
|
86
167
|
hasError = true;
|
|
87
|
-
console.error(`\n [
|
|
168
|
+
console.error(`\n [3/5] GEAP Billing & Quota Project:`);
|
|
88
169
|
console.error(` ✗ FAILED: No GCP project configured.`);
|
|
89
170
|
console.error(` 👉 Please run: gcloud config set project YOUR_GCP_PROJECT_ID`);
|
|
90
171
|
console.error(` 👉 Or export: export EDITABLE_SLIDES_PROJECT=YOUR_GCP_PROJECT_ID`);
|
|
@@ -99,7 +180,7 @@ async function runDoctor() {
|
|
|
99
180
|
.trim();
|
|
100
181
|
if (check === project) {
|
|
101
182
|
projectAccessOk = true;
|
|
102
|
-
console.error(`\n [
|
|
183
|
+
console.error(`\n [3/5] Target GCP Project Accessibility:`);
|
|
103
184
|
console.error(
|
|
104
185
|
` ✓ Verified: Account [${account}] has active access to project [${project}].`,
|
|
105
186
|
);
|
|
@@ -107,7 +188,7 @@ async function runDoctor() {
|
|
|
107
188
|
} catch (err) {
|
|
108
189
|
hasError = true;
|
|
109
190
|
const stderr = err.stderr ? err.stderr.toString() : err.message;
|
|
110
|
-
console.error(`\n [
|
|
191
|
+
console.error(`\n [3/5] Target GCP Project Accessibility:`);
|
|
111
192
|
if (
|
|
112
193
|
stderr.includes('does not have permission') ||
|
|
113
194
|
stderr.includes('caller does not have permission')
|
|
@@ -118,7 +199,7 @@ async function runDoctor() {
|
|
|
118
199
|
console.error(
|
|
119
200
|
` 👉 Solution 1: Log in with the account that has access to [${project}]:`,
|
|
120
201
|
);
|
|
121
|
-
console.error(` gcloud auth login`);
|
|
202
|
+
console.error(` gcloud auth login --enable-gdrive-access`);
|
|
122
203
|
console.error(
|
|
123
204
|
` 👉 Solution 2: Switch to a project that [${account}] can access:`,
|
|
124
205
|
);
|
|
@@ -140,7 +221,7 @@ async function runDoctor() {
|
|
|
140
221
|
}
|
|
141
222
|
}
|
|
142
223
|
|
|
143
|
-
//
|
|
224
|
+
// 4. Verify One-Time IAM Delegation to EditNBLM Service Account
|
|
144
225
|
if (projectAccessOk) {
|
|
145
226
|
try {
|
|
146
227
|
const bindings = execSync(
|
|
@@ -151,7 +232,7 @@ async function runDoctor() {
|
|
|
151
232
|
'serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com',
|
|
152
233
|
);
|
|
153
234
|
|
|
154
|
-
console.error(`\n [
|
|
235
|
+
console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
|
|
155
236
|
if (hasAiPlatform) {
|
|
156
237
|
console.error(
|
|
157
238
|
` ✓ GRANTED: Service account has roles/aiplatform.user on [${project}].`,
|
|
@@ -164,28 +245,28 @@ async function runDoctor() {
|
|
|
164
245
|
console.error(` 👉 Please run the delegation commands below.`);
|
|
165
246
|
}
|
|
166
247
|
} catch {
|
|
167
|
-
console.error(`\n [
|
|
248
|
+
console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
|
|
168
249
|
console.error(
|
|
169
250
|
` ⚠️ Could not read IAM policy directly. Ensure the delegation commands below were executed.`,
|
|
170
251
|
);
|
|
171
252
|
}
|
|
172
253
|
} else {
|
|
173
|
-
console.error(`\n [
|
|
254
|
+
console.error(`\n [4/5] GEAP Quota & IAM Delegation:`);
|
|
174
255
|
console.error(` ⏸️ Skipped until project access is resolved.`);
|
|
175
256
|
}
|
|
176
257
|
|
|
177
|
-
//
|
|
258
|
+
// 5. Check Backend Connectivity
|
|
178
259
|
try {
|
|
179
260
|
const res = await fetch(`${BASE_URL}/health`);
|
|
180
261
|
if (res.ok) {
|
|
181
|
-
console.error(`\n [
|
|
262
|
+
console.error(`\n [5/5] EditNBLM Cloud Backend:`);
|
|
182
263
|
console.error(` ✓ Status: Connected (${BASE_URL})`);
|
|
183
264
|
} else {
|
|
184
|
-
console.error(`\n [
|
|
265
|
+
console.error(`\n [5/5] EditNBLM Cloud Backend:`);
|
|
185
266
|
console.error(` ⚠️ Status: HTTP ${res.status}`);
|
|
186
267
|
}
|
|
187
268
|
} catch (err) {
|
|
188
|
-
console.error(`\n [
|
|
269
|
+
console.error(`\n [5/5] EditNBLM Cloud Backend:`);
|
|
189
270
|
console.error(` ✗ Status: Unreachable (${err.message})`);
|
|
190
271
|
}
|
|
191
272
|
|
|
@@ -225,7 +306,10 @@ if (
|
|
|
225
306
|
) {
|
|
226
307
|
runDoctor();
|
|
227
308
|
} else {
|
|
228
|
-
startMcpServer()
|
|
309
|
+
startMcpServer().catch((err) => {
|
|
310
|
+
console.error('[EditNBLM] Fatal error:', err);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
|
229
313
|
}
|
|
230
314
|
|
|
231
315
|
async function callRemoteMcp(method, params, apiKey) {
|
|
@@ -336,7 +420,7 @@ async function downloadPptxHelper(downloadUrl, targetPath) {
|
|
|
336
420
|
if (res.ok) {
|
|
337
421
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
338
422
|
await fs.promises.writeFile(targetPath, buf);
|
|
339
|
-
return targetPath;
|
|
423
|
+
return { path: targetPath, buffer: buf };
|
|
340
424
|
}
|
|
341
425
|
} catch (err) {
|
|
342
426
|
console.error(`[EditNBLM] Failed to download PPTX locally: ${err.message}`);
|
|
@@ -344,14 +428,25 @@ async function downloadPptxHelper(downloadUrl, targetPath) {
|
|
|
344
428
|
return null;
|
|
345
429
|
}
|
|
346
430
|
|
|
347
|
-
function assertEnvironmentOrExit() {
|
|
431
|
+
async function assertEnvironmentOrExit() {
|
|
348
432
|
const account = getAccount();
|
|
349
433
|
const token = getAuthToken();
|
|
350
434
|
const project = getProject();
|
|
351
435
|
|
|
352
436
|
if (!account || !token) {
|
|
353
437
|
console.error('\n[EditNBLM] ❌ Activation Failed: Google Cloud authentication required.');
|
|
354
|
-
console.error('[EditNBLM] Please run `gcloud auth login` before activating this MCP server.');
|
|
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');
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Verify Google Drive scope
|
|
444
|
+
const hasDrive = await checkDriveScope(token);
|
|
445
|
+
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');
|
|
355
450
|
console.error('[EditNBLM] Run `npx -y @jieunmarslim/server-editable-slides doctor` to diagnose.\n');
|
|
356
451
|
process.exit(1);
|
|
357
452
|
}
|
|
@@ -381,7 +476,7 @@ function assertEnvironmentOrExit() {
|
|
|
381
476
|
stderr.includes('does not have permission') ||
|
|
382
477
|
stderr.includes('caller does not have permission')
|
|
383
478
|
) {
|
|
384
|
-
console.error(`[EditNBLM] 👉 Solution 1: Log in with an account that has access: gcloud auth login`);
|
|
479
|
+
console.error(`[EditNBLM] 👉 Solution 1: Log in with an account that has access: gcloud auth login --enable-gdrive-access`);
|
|
385
480
|
console.error(`[EditNBLM] 👉 Solution 2: Switch to a project that [${account}] can access: gcloud config set project <PROJECT>`);
|
|
386
481
|
} else if (stderr.includes('not found') || stderr.includes('404')) {
|
|
387
482
|
console.error(`[EditNBLM] 👉 Project [${project}] does not exist. Run: gcloud config set project <VALID_PROJECT_ID>`);
|
|
@@ -393,8 +488,8 @@ function assertEnvironmentOrExit() {
|
|
|
393
488
|
}
|
|
394
489
|
}
|
|
395
490
|
|
|
396
|
-
function startMcpServer() {
|
|
397
|
-
assertEnvironmentOrExit();
|
|
491
|
+
async function startMcpServer() {
|
|
492
|
+
await assertEnvironmentOrExit();
|
|
398
493
|
|
|
399
494
|
const server = new McpServer({
|
|
400
495
|
name: 'editable-slides',
|
|
@@ -459,14 +554,18 @@ function startMcpServer() {
|
|
|
459
554
|
|
|
460
555
|
const parsed = JSON.parse(startResult.content[0].text);
|
|
461
556
|
if (parsed.jobId) {
|
|
462
|
-
activeJobs.set(parsed.jobId, {
|
|
557
|
+
activeJobs.set(parsed.jobId, {
|
|
558
|
+
filePath,
|
|
559
|
+
fileName: originalName,
|
|
560
|
+
format: format || 'slides',
|
|
561
|
+
});
|
|
463
562
|
parsed.note =
|
|
464
563
|
'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
|
|
465
564
|
'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
|
|
466
565
|
'DO NOT write custom scripts or install packages.';
|
|
467
566
|
parsed.agentInstructions = [
|
|
468
567
|
`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.',
|
|
568
|
+
'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally and/or uploaded to Google Drive.',
|
|
470
569
|
];
|
|
471
570
|
startResult.content[0].text = JSON.stringify(parsed, null, 2);
|
|
472
571
|
}
|
|
@@ -491,16 +590,48 @@ function startMcpServer() {
|
|
|
491
590
|
const parsed = JSON.parse(result.content[0].text);
|
|
492
591
|
if (parsed.downloadUrl && filePath) {
|
|
493
592
|
const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
|
|
494
|
-
await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
|
|
593
|
+
const downloaded = await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
|
|
495
594
|
parsed.localPptxPath = localPptxPath;
|
|
595
|
+
|
|
596
|
+
// If format is 'slides' (default), automatically upload to Google Drive if scope is granted
|
|
597
|
+
if (format !== 'pptx' && downloaded?.buffer) {
|
|
598
|
+
const token = getAuthToken();
|
|
599
|
+
const hasDrive = await checkDriveScope(token);
|
|
600
|
+
if (hasDrive) {
|
|
601
|
+
try {
|
|
602
|
+
console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
|
|
603
|
+
const driveResult = await uploadPptxToGoogleDrive(
|
|
604
|
+
downloaded.buffer,
|
|
605
|
+
path.basename(localPptxPath),
|
|
606
|
+
token,
|
|
607
|
+
);
|
|
608
|
+
parsed.presentationUrl = driveResult.presentationUrl;
|
|
609
|
+
parsed.presentationId = driveResult.presentationId;
|
|
610
|
+
parsed.googleSlidesCreated = true;
|
|
611
|
+
console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
|
|
612
|
+
} catch (driveErr) {
|
|
613
|
+
console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
|
|
614
|
+
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
|
+
}
|
|
620
|
+
}
|
|
496
621
|
}
|
|
497
|
-
|
|
622
|
+
|
|
623
|
+
if (parsed.presentationUrl) {
|
|
498
624
|
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."',
|
|
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.',
|
|
503
628
|
];
|
|
629
|
+
} else if (parsed.downloadUrl || parsed.localPptxPath) {
|
|
630
|
+
parsed.agentInstructions = [
|
|
631
|
+
'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
|
|
632
|
+
parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
|
|
633
|
+
parsed.driveNotice || null,
|
|
634
|
+
].filter(Boolean);
|
|
504
635
|
}
|
|
505
636
|
result.content[0].text = JSON.stringify(parsed, null, 2);
|
|
506
637
|
} catch {
|
|
@@ -622,17 +753,49 @@ function startMcpServer() {
|
|
|
622
753
|
destPath = path.resolve(process.cwd(), `${safeName}.pptx`);
|
|
623
754
|
}
|
|
624
755
|
|
|
625
|
-
await downloadPptxHelper(parsed.downloadUrl, destPath);
|
|
756
|
+
const downloaded = await downloadPptxHelper(parsed.downloadUrl, destPath);
|
|
626
757
|
parsed.localPptxPath = destPath;
|
|
758
|
+
|
|
759
|
+
// If format is 'slides' (default), automatically upload to Google Drive if scope is granted
|
|
760
|
+
if (jobInfo?.format !== 'pptx' && downloaded?.buffer) {
|
|
761
|
+
const token = getAuthToken();
|
|
762
|
+
const hasDrive = await checkDriveScope(token);
|
|
763
|
+
if (hasDrive) {
|
|
764
|
+
try {
|
|
765
|
+
console.error('[EditNBLM] 🚀 Creating live Google Slides in your personal Google Drive...');
|
|
766
|
+
const driveResult = await uploadPptxToGoogleDrive(
|
|
767
|
+
downloaded.buffer,
|
|
768
|
+
path.basename(destPath),
|
|
769
|
+
token,
|
|
770
|
+
);
|
|
771
|
+
parsed.presentationUrl = driveResult.presentationUrl;
|
|
772
|
+
parsed.presentationId = driveResult.presentationId;
|
|
773
|
+
parsed.googleSlidesCreated = true;
|
|
774
|
+
console.error(`[EditNBLM] ✅ Live Google Slides: ${driveResult.presentationUrl}`);
|
|
775
|
+
} catch (driveErr) {
|
|
776
|
+
console.warn(`[EditNBLM] ⚠️ Google Drive upload: ${driveErr.message}`);
|
|
777
|
+
parsed.driveError = driveErr.message;
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
parsed.driveNotice =
|
|
781
|
+
'Tip: To have presentations automatically created as live Google Slides in your Google Drive, run once: gcloud auth login --enable-gdrive-access';
|
|
782
|
+
}
|
|
783
|
+
}
|
|
627
784
|
}
|
|
628
785
|
|
|
629
|
-
parsed.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
786
|
+
if (parsed.presentationUrl) {
|
|
787
|
+
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.',
|
|
791
|
+
];
|
|
792
|
+
} else {
|
|
793
|
+
parsed.agentInstructions = [
|
|
794
|
+
'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
|
|
795
|
+
parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
|
|
796
|
+
parsed.driveNotice || null,
|
|
797
|
+
].filter(Boolean);
|
|
798
|
+
}
|
|
636
799
|
}
|
|
637
800
|
|
|
638
801
|
result.content[0].text = JSON.stringify(parsed, null, 2);
|
|
@@ -642,6 +805,81 @@ function startMcpServer() {
|
|
|
642
805
|
},
|
|
643
806
|
);
|
|
644
807
|
|
|
808
|
+
// 4. Dedicated Google Drive Upload & Convert Tool
|
|
809
|
+
server.tool(
|
|
810
|
+
'upload_to_google_drive',
|
|
811
|
+
'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.',
|
|
812
|
+
{
|
|
813
|
+
filePath: z
|
|
814
|
+
.string()
|
|
815
|
+
.describe('Local file path to the .pptx presentation file to upload'),
|
|
816
|
+
title: z
|
|
817
|
+
.string()
|
|
818
|
+
.optional()
|
|
819
|
+
.describe('Optional title for the Google Slides presentation'),
|
|
820
|
+
},
|
|
821
|
+
async ({ filePath, title }) => {
|
|
822
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
823
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
824
|
+
return {
|
|
825
|
+
content: [
|
|
826
|
+
{
|
|
827
|
+
type: 'text',
|
|
828
|
+
text: JSON.stringify({ error: `File not found: ${filePath}` }),
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
isError: true,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const token = getAuthToken();
|
|
836
|
+
const hasDrive = await checkDriveScope(token);
|
|
837
|
+
if (!hasDrive) {
|
|
838
|
+
return {
|
|
839
|
+
content: [
|
|
840
|
+
{
|
|
841
|
+
type: 'text',
|
|
842
|
+
text: JSON.stringify(
|
|
843
|
+
{
|
|
844
|
+
error:
|
|
845
|
+
'Google Drive access scope is missing on the current token. ' +
|
|
846
|
+
'Please run this one-time command in your terminal to enable Google Drive access: ' +
|
|
847
|
+
'gcloud auth login --enable-gdrive-access',
|
|
848
|
+
},
|
|
849
|
+
null,
|
|
850
|
+
2,
|
|
851
|
+
),
|
|
852
|
+
},
|
|
853
|
+
],
|
|
854
|
+
isError: true,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const pptxBuf = await fs.promises.readFile(resolvedPath);
|
|
859
|
+
const deckTitle = title || path.basename(resolvedPath).replace(/\.pptx$/i, '');
|
|
860
|
+
const driveResult = await uploadPptxToGoogleDrive(pptxBuf, deckTitle, token);
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
content: [
|
|
864
|
+
{
|
|
865
|
+
type: 'text',
|
|
866
|
+
text: JSON.stringify(
|
|
867
|
+
{
|
|
868
|
+
success: true,
|
|
869
|
+
presentationUrl: driveResult.presentationUrl,
|
|
870
|
+
presentationId: driveResult.presentationId,
|
|
871
|
+
title: driveResult.title,
|
|
872
|
+
note: 'Successfully uploaded to Google Drive and converted to Google Slides.',
|
|
873
|
+
},
|
|
874
|
+
null,
|
|
875
|
+
2,
|
|
876
|
+
),
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
};
|
|
880
|
+
},
|
|
881
|
+
);
|
|
882
|
+
|
|
645
883
|
const transport = new StdioServerTransport();
|
|
646
884
|
server.connect(transport).then(() => {
|
|
647
885
|
const project = getProject();
|