@jieunmarslim/server-editable-slides 0.1.3 → 0.2.1

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 +428 -117
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -13,6 +13,9 @@ const BASE_URL =
13
13
  process.env.EDITABLE_SLIDES_URL ||
14
14
  'https://editable-slides-mcp-1056428002550.us-central1.run.app';
15
15
 
16
+ // In-memory mapping of active conversion jobs to local file origins
17
+ const activeJobs = new Map();
18
+
16
19
  function getAuthToken() {
17
20
  if (process.env.EDITABLE_SLIDES_API_KEY) {
18
21
  return process.env.EDITABLE_SLIDES_API_KEY;
@@ -29,21 +32,141 @@ function getAuthToken() {
29
32
  }
30
33
 
31
34
  function getProject() {
32
- return process.env.EDITABLE_SLIDES_PROJECT || 'editnblm-in-ge-2036';
35
+ if (process.env.EDITABLE_SLIDES_PROJECT) {
36
+ return process.env.EDITABLE_SLIDES_PROJECT;
37
+ }
38
+ try {
39
+ const p = execSync('gcloud config get-value project', {
40
+ stdio: ['ignore', 'pipe', 'ignore'],
41
+ })
42
+ .toString()
43
+ .trim();
44
+ if (p && p !== '(unset)') return p;
45
+ } catch {}
46
+ return null;
47
+ }
48
+
49
+ function getAccount() {
50
+ try {
51
+ const a = execSync('gcloud config get-value account', {
52
+ stdio: ['ignore', 'pipe', 'ignore'],
53
+ })
54
+ .toString()
55
+ .trim();
56
+ if (a && a !== '(unset)') return a;
57
+ } catch {}
58
+ return null;
59
+ }
60
+
61
+ async function runDoctor() {
62
+ const account = getAccount();
63
+ const token = getAuthToken();
64
+ const project = getProject();
65
+
66
+ console.error('\n========================================================================');
67
+ console.error(' 🔍 EditNBLM MCP Pre-flight Diagnostic & Doctor');
68
+ console.error('========================================================================\n');
69
+
70
+ let hasError = false;
71
+
72
+ // 1. Check Auth
73
+ if (account && token) {
74
+ console.error(` [1/3] Google Cloud Authentication:`);
75
+ console.error(` ✓ Authenticated: ${account}`);
76
+ } else {
77
+ hasError = true;
78
+ console.error(` [1/3] Google Cloud Authentication:`);
79
+ console.error(` ✗ FAILED: Not authenticated or token expired.`);
80
+ console.error(` 👉 Please run: gcloud auth login`);
81
+ }
82
+
83
+ // 2. Check Project
84
+ if (project) {
85
+ console.error(`\n [2/3] GEAP Billing & Quota Project:`);
86
+ console.error(` ✓ Target Project ID: ${project}`);
87
+ } else {
88
+ hasError = true;
89
+ console.error(`\n [2/3] GEAP Billing & Quota Project:`);
90
+ console.error(` ✗ FAILED: No GCP project configured for billing.`);
91
+ console.error(` 👉 Please run: gcloud config set project YOUR_GCP_PROJECT_ID`);
92
+ console.error(` 👉 Or export: export EDITABLE_SLIDES_PROJECT=YOUR_GCP_PROJECT_ID`);
93
+ }
94
+
95
+ // 3. Check Backend Connectivity
96
+ try {
97
+ const res = await fetch(`${BASE_URL}/health`);
98
+ if (res.ok) {
99
+ console.error(`\n [3/3] EditNBLM Cloud Backend:`);
100
+ console.error(` ✓ Status: Connected (${BASE_URL})`);
101
+ } else {
102
+ console.error(`\n [3/3] EditNBLM Cloud Backend:`);
103
+ console.error(` ⚠️ Status: HTTP ${res.status}`);
104
+ }
105
+ } catch (err) {
106
+ console.error(`\n [3/3] EditNBLM Cloud Backend:`);
107
+ console.error(` ✗ Status: Unreachable (${err.message})`);
108
+ }
109
+
110
+ // 4. One-Time IAM Delegation command helper
111
+ console.error('\n------------------------------------------------------------------------');
112
+ console.error(' 📋 One-Time IAM Delegation Commands for your Project:');
113
+ console.error('------------------------------------------------------------------------');
114
+ const targetProject = project || 'YOUR_GCP_PROJECT_ID';
115
+ console.error(`gcloud projects add-iam-policy-binding ${targetProject} \\`);
116
+ console.error(' --member="serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com" \\');
117
+ console.error(' --role="roles/aiplatform.user"\n');
118
+ console.error(`gcloud projects add-iam-policy-binding ${targetProject} \\`);
119
+ console.error(' --member="serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com" \\');
120
+ console.error(' --role="roles/serviceusage.serviceUsageConsumer"');
121
+ console.error('========================================================================\n');
122
+
123
+ if (!hasError) {
124
+ console.error('🎉 Pre-flight check PASSED! Your environment is ready to use EditNBLM MCP.\n');
125
+ process.exit(0);
126
+ } else {
127
+ console.error('⚠️ Setup incomplete. Please fix the items marked with ✗ above.\n');
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ // If invoked from an interactive terminal directly or with doctor/auth/check args, run diagnostic
133
+ const args = process.argv.slice(2);
134
+ if (
135
+ args.includes('doctor') ||
136
+ args.includes('auth') ||
137
+ args.includes('check') ||
138
+ args.includes('--check') ||
139
+ args.includes('--doctor') ||
140
+ args.includes('-h') ||
141
+ args.includes('--help') ||
142
+ (process.stdin.isTTY && !args.includes('--stdio'))
143
+ ) {
144
+ runDoctor();
145
+ } else {
146
+ startMcpServer();
33
147
  }
34
148
 
35
149
  async function callRemoteMcp(method, params, apiKey) {
36
150
  const token = apiKey || getAuthToken();
151
+ if (!token) {
152
+ throw new Error(
153
+ '[EditNBLM Auth Required] Google Cloud credentials not found. Run `gcloud auth login` or set EDITABLE_SLIDES_API_KEY.',
154
+ );
155
+ }
156
+
37
157
  const project = getProject();
38
- const url = `${BASE_URL}/mcp/${project}`;
158
+ if (!project) {
159
+ throw new Error(
160
+ '[EditNBLM Setup Required] No GCP project detected for Gemini Enterprise Agent Platform (GEAP) billing. Run `gcloud config set project <PROJECT_ID>` or export EDITABLE_SLIDES_PROJECT=<PROJECT_ID>.',
161
+ );
162
+ }
39
163
 
164
+ const url = `${BASE_URL}/mcp/${project}`;
40
165
  const headers = {
41
166
  'Content-Type': 'application/json',
42
167
  Accept: 'application/json, text/event-stream',
168
+ Authorization: `Bearer ${token}`,
43
169
  };
44
- if (token) {
45
- headers['Authorization'] = `Bearer ${token}`;
46
- }
47
170
 
48
171
  const res = await fetch(url, {
49
172
  method: 'POST',
@@ -71,133 +194,321 @@ async function callRemoteMcp(method, params, apiKey) {
71
194
  return data.result;
72
195
  }
73
196
 
74
- const server = new McpServer({
75
- name: 'editable-slides',
76
- version: '1.0.0',
77
- });
78
-
79
- server.tool(
80
- 'create_slides_from_image',
81
- 'Convert a presentation slide image or PDF into an editable presentation deck. Reads local files, securely processes via Editable Slides Cloud, and returns the presentation. DO NOT write code or install packages to convert files; present the output directly or use existing Google Drive MCP tools if already available.',
82
- {
83
- filePath: z
84
- .string()
85
- .optional()
86
- .describe('Local file path to the slide image or PDF (e.g. /path/to/slide.png)'),
87
- fileUrl: z
88
- .string()
89
- .optional()
90
- .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
91
- format: z
92
- .enum(['slides', 'pptx'])
93
- .optional()
94
- .describe("Output format: 'slides' (default, Google Slides URL) or 'pptx'"),
95
- apiKey: z
96
- .string()
97
- .optional()
98
- .describe('Optional API key or access token for authentication'),
99
- },
100
- async ({ filePath, fileUrl, format, apiKey }) => {
101
- let targetUrl = fileUrl;
102
-
103
- if (filePath && !targetUrl) {
104
- if (!fs.existsSync(filePath)) {
105
- throw new Error(`File not found: ${filePath}`);
197
+ async function uploadLocalFile(filePath, apiKey) {
198
+ if (!fs.existsSync(filePath)) {
199
+ throw new Error(`File not found: ${filePath}`);
200
+ }
201
+ const fileName = path.basename(filePath);
202
+ const ext = path.extname(filePath).toLowerCase();
203
+ const mimeType =
204
+ ext === '.pdf'
205
+ ? 'application/pdf'
206
+ : ext === '.jpg' || ext === '.jpeg'
207
+ ? 'image/jpeg'
208
+ : 'image/png';
209
+ const fileBytes = await fs.promises.readFile(filePath);
210
+
211
+ console.error(`[EditNBLM] Requesting secure upload URL for "${fileName}"...`);
212
+ const uploadResp = await callRemoteMcp(
213
+ 'tools/call',
214
+ {
215
+ name: 'request_upload_url',
216
+ arguments: { fileName, mimeType },
217
+ },
218
+ apiKey,
219
+ );
220
+
221
+ const parsed = JSON.parse(uploadResp.content[0].text);
222
+ const { uploadUrl, fileUrl: uploadedFileUrl } = parsed;
223
+
224
+ console.error(
225
+ `[EditNBLM] Uploading document (${(fileBytes.length / 1024).toFixed(1)} KB)...`,
226
+ );
227
+ const putRes = await fetch(uploadUrl, {
228
+ method: 'PUT',
229
+ headers: { 'Content-Type': mimeType },
230
+ body: fileBytes,
231
+ });
232
+
233
+ if (!putRes.ok) {
234
+ throw new Error(`Upload failed: ${putRes.status} ${putRes.statusText}`);
235
+ }
236
+ return { fileName, fileUrl: uploadedFileUrl, isPdf: ext === '.pdf' };
237
+ }
238
+
239
+ async function downloadPptxHelper(downloadUrl, targetPath) {
240
+ try {
241
+ console.error(`[EditNBLM] Saving PowerPoint file to "${targetPath}"...`);
242
+ const res = await fetch(downloadUrl);
243
+ if (res.ok) {
244
+ const buf = Buffer.from(await res.arrayBuffer());
245
+ await fs.promises.writeFile(targetPath, buf);
246
+ return targetPath;
247
+ }
248
+ } catch (err) {
249
+ console.error(`[EditNBLM] Failed to download PPTX locally: ${err.message}`);
250
+ }
251
+ return null;
252
+ }
253
+
254
+ function startMcpServer() {
255
+ const server = new McpServer({
256
+ name: 'editable-slides',
257
+ version: '1.0.0',
258
+ });
259
+
260
+ // 1. Fast Synchronous Slide Conversion (Single Image)
261
+ server.tool(
262
+ 'create_slides_from_image',
263
+ '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.',
264
+ {
265
+ filePath: z
266
+ .string()
267
+ .optional()
268
+ .describe('Local file path to the slide image or PDF (e.g. /path/to/slide.png)'),
269
+ fileUrl: z
270
+ .string()
271
+ .optional()
272
+ .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
273
+ format: z
274
+ .enum(['slides', 'pptx'])
275
+ .optional()
276
+ .describe("Output format: 'slides' (default, Google Slides URL) or 'pptx'"),
277
+ apiKey: z
278
+ .string()
279
+ .optional()
280
+ .describe('Optional API key or access token for authentication'),
281
+ },
282
+ async ({ filePath, fileUrl, format, apiKey }) => {
283
+ let targetUrl = fileUrl;
284
+ let originalName = fileUrl ? path.basename(fileUrl) : 'presentation';
285
+ let isPdf = false;
286
+
287
+ if (filePath && !targetUrl) {
288
+ const uploaded = await uploadLocalFile(filePath, apiKey);
289
+ targetUrl = uploaded.fileUrl;
290
+ originalName = uploaded.fileName;
291
+ isPdf = uploaded.isPdf;
292
+ } else if (targetUrl && targetUrl.toLowerCase().endsWith('.pdf')) {
293
+ isPdf = true;
294
+ }
295
+
296
+ if (!targetUrl) {
297
+ throw new Error('Either filePath or fileUrl must be provided.');
298
+ }
299
+
300
+ // If document is a multi-page PDF, route to async pipeline to prevent 120s MCP timeout
301
+ if (isPdf) {
302
+ console.error('[EditNBLM] Multi-page PDF detected. Starting background pipeline to prevent timeout...');
303
+ const startResult = await callRemoteMcp(
304
+ 'tools/call',
305
+ {
306
+ name: 'start_slide_conversion',
307
+ arguments: {
308
+ fileUrl: targetUrl,
309
+ fileName: originalName,
310
+ format: format || 'slides',
311
+ },
312
+ },
313
+ apiKey,
314
+ );
315
+
316
+ const parsed = JSON.parse(startResult.content[0].text);
317
+ if (parsed.jobId) {
318
+ activeJobs.set(parsed.jobId, { filePath, fileName: originalName });
319
+ parsed.note =
320
+ 'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
321
+ 'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
322
+ 'DO NOT write custom scripts or install packages.';
323
+ parsed.agentInstructions = [
324
+ `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
325
+ 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally.',
326
+ ];
327
+ startResult.content[0].text = JSON.stringify(parsed, null, 2);
328
+ }
329
+ return startResult;
106
330
  }
107
- const fileName = path.basename(filePath);
108
- const ext = path.extname(filePath).toLowerCase();
109
- const mimeType =
110
- ext === '.pdf'
111
- ? 'application/pdf'
112
- : ext === '.jpg' || ext === '.jpeg'
113
- ? 'image/jpeg'
114
- : 'image/png';
115
- const fileBytes = await fs.promises.readFile(filePath);
116
-
117
- console.error(`[EditableSlides] Requesting secure upload URL for "${fileName}"...`);
118
- const uploadResp = await callRemoteMcp(
331
+
332
+ // Synchronous single-image conversion
333
+ console.error(`[EditNBLM] Converting image to ${format || 'slides'}...`);
334
+ const result = await callRemoteMcp(
119
335
  'tools/call',
120
336
  {
121
- name: 'request_upload_url',
122
- arguments: { fileName, mimeType },
337
+ name: 'create_slides_from_image',
338
+ arguments: {
339
+ fileUrl: targetUrl,
340
+ format: format || 'slides',
341
+ },
123
342
  },
124
343
  apiKey,
125
344
  );
126
345
 
127
- const parsed = JSON.parse(uploadResp.content[0].text);
128
- const { uploadUrl, fileUrl: uploadedFileUrl } = parsed;
346
+ try {
347
+ const parsed = JSON.parse(result.content[0].text);
348
+ if (parsed.downloadUrl && filePath) {
349
+ const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
350
+ await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
351
+ parsed.localPptxPath = localPptxPath;
352
+ }
353
+ if (parsed.downloadUrl || parsed.localPptxPath) {
354
+ parsed.agentInstructions = [
355
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
356
+ 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert or upload this file.',
357
+ '1. If you already have an active Google Drive MCP tool (e.g. drive.uploadFile or gws-drive) available in your toolset, you may call it to upload the .pptx file with Google Slides conversion.',
358
+ '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."',
359
+ ];
360
+ }
361
+ result.content[0].text = JSON.stringify(parsed, null, 2);
362
+ } catch {
363
+ // Retain original result
364
+ }
129
365
 
130
- console.error(
131
- `[EditableSlides] Uploading image (${(fileBytes.length / 1024).toFixed(1)} KB)...`,
132
- );
133
- const putRes = await fetch(uploadUrl, {
134
- method: 'PUT',
135
- headers: { 'Content-Type': mimeType },
136
- body: fileBytes,
137
- });
138
-
139
- if (!putRes.ok) {
140
- throw new Error(`Upload failed: ${putRes.status} ${putRes.statusText}`);
366
+ return result;
367
+ },
368
+ );
369
+
370
+ // 2. Start Asynchronous Slide Conversion (for PDFs & heavy decks)
371
+ server.tool(
372
+ 'start_slide_conversion',
373
+ 'Begin converting an image or multi-page PDF (e.g. 15-page presentation) into an editable presentation deck. Accepts a local filePath or remote fileUrl. Returns a jobId immediately in ~1 second without client timeouts. Follow up by calling check_slide_conversion with the returned jobId.',
374
+ {
375
+ filePath: z
376
+ .string()
377
+ .optional()
378
+ .describe('Local file path to the slide image or PDF (e.g. /path/to/asdf.pdf)'),
379
+ fileUrl: z
380
+ .string()
381
+ .optional()
382
+ .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
383
+ fileName: z
384
+ .string()
385
+ .optional()
386
+ .describe('Original file name if known'),
387
+ format: z
388
+ .enum(['slides', 'pptx'])
389
+ .optional()
390
+ .describe("What to produce: 'slides' for a Google Slides deck (default), or 'pptx'"),
391
+ apiKey: z
392
+ .string()
393
+ .optional()
394
+ .describe('Optional API key or access token for authentication'),
395
+ },
396
+ async ({ filePath, fileUrl, fileName, format, apiKey }) => {
397
+ let targetUrl = fileUrl;
398
+ let targetFileName = fileName;
399
+
400
+ if (filePath && !targetUrl) {
401
+ const uploaded = await uploadLocalFile(filePath, apiKey);
402
+ targetUrl = uploaded.fileUrl;
403
+ targetFileName = targetFileName || uploaded.fileName;
141
404
  }
142
- targetUrl = uploadedFileUrl;
143
- }
144
405
 
145
- if (!targetUrl) {
146
- throw new Error('Either filePath or fileUrl must be provided.');
147
- }
406
+ if (!targetUrl) {
407
+ throw new Error('Either filePath or fileUrl must be provided.');
408
+ }
148
409
 
149
- console.error(`[EditableSlides] Converting to ${format || 'slides'}...`);
150
- const result = await callRemoteMcp(
151
- 'tools/call',
152
- {
153
- name: 'create_slides_from_image',
154
- arguments: {
155
- fileUrl: targetUrl,
156
- format: format || 'slides',
410
+ console.error(`[EditNBLM] Starting background conversion for "${targetFileName || targetUrl}"...`);
411
+ const startResult = await callRemoteMcp(
412
+ 'tools/call',
413
+ {
414
+ name: 'start_slide_conversion',
415
+ arguments: {
416
+ fileUrl: targetUrl,
417
+ fileName: targetFileName,
418
+ format: format || 'slides',
419
+ },
157
420
  },
158
- },
159
- apiKey,
160
- );
421
+ apiKey,
422
+ );
161
423
 
162
- // If a PPTX download URL was returned and the caller used a local filePath,
163
- // automatically download the .pptx directly to the local disk and guide the
164
- // agent to use Google Drive MCP to convert it into a live Google Slide.
165
- try {
166
- const parsed = JSON.parse(result.content[0].text);
167
- if (parsed.downloadUrl && filePath) {
168
- const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
169
- console.error(`[EditableSlides] Saving PowerPoint file to "${localPptxPath}"...`);
170
- const pptxRes = await fetch(parsed.downloadUrl);
171
- if (pptxRes.ok) {
172
- const buf = Buffer.from(await pptxRes.arrayBuffer());
173
- await fs.promises.writeFile(localPptxPath, buf);
174
- parsed.localPptxPath = localPptxPath;
424
+ try {
425
+ const parsed = JSON.parse(startResult.content[0].text);
426
+ if (parsed.jobId) {
427
+ activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName });
428
+ parsed.agentInstructions = [
429
+ `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
430
+ 'DO NOT write scripts or install packages while waiting.',
431
+ ];
432
+ startResult.content[0].text = JSON.stringify(parsed, null, 2);
175
433
  }
176
- }
177
- if (parsed.downloadUrl || parsed.localPptxPath) {
178
- parsed.agentInstructions = [
179
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
180
- 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert or upload this file.',
181
- '1. If you already have an active Google Drive MCP tool (e.g. drive.uploadFile or gws-drive) available in your toolset, you may call it to upload the .pptx file with Google Slides conversion.',
182
- '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."',
183
- ];
184
- }
185
- result.content[0].text = JSON.stringify(parsed, null, 2);
186
- } catch {
187
- // Keep original result text if parsing failed
188
- }
434
+ } catch {}
435
+
436
+ return startResult;
437
+ },
438
+ );
439
+
440
+ // 3. Check Slide Conversion Progress & Auto-Download
441
+ server.tool(
442
+ 'check_slide_conversion',
443
+ 'Report the progress of a conversion started by start_slide_conversion. Call this repeatedly until status is "done". When complete, automatically downloads the editable PowerPoint (.pptx) file to local disk.',
444
+ {
445
+ jobId: z
446
+ .string()
447
+ .describe('The jobId returned by start_slide_conversion or create_slides_from_image'),
448
+ sinceUpdatedAt: z
449
+ .number()
450
+ .optional()
451
+ .describe('updatedAt from previous check to wait for new progress updates'),
452
+ apiKey: z
453
+ .string()
454
+ .optional()
455
+ .describe('Optional API key or access token for authentication'),
456
+ },
457
+ async ({ jobId, sinceUpdatedAt, apiKey }) => {
458
+ const result = await callRemoteMcp(
459
+ 'tools/call',
460
+ {
461
+ name: 'check_slide_conversion',
462
+ arguments: { jobId, sinceUpdatedAt },
463
+ },
464
+ apiKey,
465
+ );
466
+
467
+ try {
468
+ const parsed = JSON.parse(result.content[0].text);
469
+ const jobInfo = activeJobs.get(jobId);
470
+
471
+ if (parsed.status === 'done') {
472
+ if (parsed.downloadUrl) {
473
+ let destPath = null;
474
+ if (jobInfo?.filePath) {
475
+ destPath = jobInfo.filePath.replace(/\.[^/.]+$/, '') + '.pptx';
476
+ } else {
477
+ const safeName = (jobInfo?.fileName || 'presentation').replace(/\.[^/.]+$/, '');
478
+ destPath = path.resolve(process.cwd(), `${safeName}.pptx`);
479
+ }
480
+
481
+ await downloadPptxHelper(parsed.downloadUrl, destPath);
482
+ parsed.localPptxPath = destPath;
483
+ }
189
484
 
190
- return result;
191
- },
192
- );
485
+ parsed.agentInstructions = [
486
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
487
+ parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
488
+ 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert this file.',
489
+ '1. If you have an active Google Drive MCP tool (e.g. drive.uploadFile or gws-drive), call it now to upload the .pptx file with Google Slides conversion.',
490
+ '2. If you DO NOT have a Google Drive MCP tool, present the local file path and download URL to the user directly.',
491
+ ].filter(Boolean);
492
+ }
493
+
494
+ result.content[0].text = JSON.stringify(parsed, null, 2);
495
+ } catch {}
496
+
497
+ return result;
498
+ },
499
+ );
193
500
 
194
- async function main() {
195
501
  const transport = new StdioServerTransport();
196
- await server.connect(transport);
197
- console.error('[EditableSlides] Ready and connected to Editable Slides Cloud.');
502
+ server.connect(transport).then(() => {
503
+ const project = getProject();
504
+ if (project) {
505
+ console.error(`[EditNBLM] Ready. Connected to EditNBLM Cloud (Billing Project: ${project}).`);
506
+ } else {
507
+ console.error('[EditNBLM] ⚠️ Warning: No active GCP project detected for billing.');
508
+ console.error('[EditNBLM] Run `npx @jieunmarslim/server-editable-slides doctor` to configure.');
509
+ }
510
+ }).catch((err) => {
511
+ console.error('[EditNBLM] Fatal error:', err);
512
+ process.exit(1);
513
+ });
198
514
  }
199
-
200
- main().catch((err) => {
201
- console.error('[EditableSlides] Fatal error:', err);
202
- process.exit(1);
203
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jieunmarslim/server-editable-slides",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {