@jieunmarslim/server-editable-slides 0.2.0 β†’ 0.2.2

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 +439 -234
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -43,23 +43,212 @@ function getProject() {
43
43
  .trim();
44
44
  if (p && p !== '(unset)') return p;
45
45
  } catch {}
46
- throw new Error(
47
- 'No GCP project detected for Gemini Enterprise Agent Platform (GEAP) billing. Set your project via `gcloud config set project <PROJECT_ID>` or export EDITABLE_SLIDES_PROJECT.',
48
- );
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/4] Google Cloud Authentication:`);
75
+ console.error(` βœ“ Authenticated: ${account}`);
76
+ } else {
77
+ hasError = true;
78
+ console.error(` [1/4] 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 Configuration & Account Access Permissions
84
+ let projectAccessOk = false;
85
+ if (!project) {
86
+ hasError = true;
87
+ console.error(`\n [2/4] GEAP Billing & Quota Project:`);
88
+ console.error(` βœ— FAILED: No GCP project configured.`);
89
+ console.error(` πŸ‘‰ Please run: gcloud config set project YOUR_GCP_PROJECT_ID`);
90
+ console.error(` πŸ‘‰ Or export: export EDITABLE_SLIDES_PROJECT=YOUR_GCP_PROJECT_ID`);
91
+ } else {
92
+ try {
93
+ // Actually verify that the logged-in account has access to this project
94
+ const check = execSync(
95
+ `gcloud projects describe ${project} --format="value(projectId)"`,
96
+ { stdio: ['ignore', 'pipe', 'pipe'] },
97
+ )
98
+ .toString()
99
+ .trim();
100
+ if (check === project) {
101
+ projectAccessOk = true;
102
+ console.error(`\n [2/4] Target GCP Project Accessibility:`);
103
+ console.error(
104
+ ` βœ“ Verified: Account [${account}] has active access to project [${project}].`,
105
+ );
106
+ }
107
+ } catch (err) {
108
+ hasError = true;
109
+ const stderr = err.stderr ? err.stderr.toString() : err.message;
110
+ console.error(`\n [2/4] Target GCP Project Accessibility:`);
111
+ if (
112
+ stderr.includes('does not have permission') ||
113
+ stderr.includes('caller does not have permission')
114
+ ) {
115
+ console.error(
116
+ ` βœ— ACCESS DENIED: Account [${account}] does NOT have permission to access project [${project}].`,
117
+ );
118
+ console.error(
119
+ ` πŸ‘‰ Solution 1: Switch to an account that has access to [${project}]:`,
120
+ );
121
+ console.error(` gcloud config set account <ACCOUNT_WITH_ACCESS>`);
122
+ console.error(
123
+ ` πŸ‘‰ Solution 2: Switch to a project that [${account}] can access:`,
124
+ );
125
+ console.error(
126
+ ` gcloud config set project <ACCESSIBLE_PROJECT_ID>`,
127
+ );
128
+ } else if (stderr.includes('not found') || stderr.includes('404')) {
129
+ console.error(
130
+ ` βœ— NOT FOUND: Project [${project}] does not exist or was deleted.`,
131
+ );
132
+ console.error(
133
+ ` πŸ‘‰ Please run: gcloud config set project <VALID_PROJECT_ID>`,
134
+ );
135
+ } else {
136
+ console.error(
137
+ ` βœ— FAILED: Unable to verify project access (${stderr.trim()}).`,
138
+ );
139
+ }
140
+ }
141
+ }
142
+
143
+ // 3. Verify One-Time IAM Delegation to EditNBLM Service Account
144
+ if (projectAccessOk) {
145
+ try {
146
+ const bindings = execSync(
147
+ `gcloud projects get-iam-policy ${project} --flatten="bindings[].members" --filter="bindings.role:roles/aiplatform.user" --format="value(bindings.members)"`,
148
+ { stdio: ['ignore', 'pipe', 'ignore'] },
149
+ ).toString();
150
+ const hasAiPlatform = bindings.includes(
151
+ 'serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com',
152
+ );
153
+
154
+ console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
155
+ if (hasAiPlatform) {
156
+ console.error(
157
+ ` βœ“ GRANTED: Service account has roles/aiplatform.user on [${project}].`,
158
+ );
159
+ } else {
160
+ hasError = true;
161
+ console.error(
162
+ ` βœ— MISSING: editnblm-mcp-runtime has NOT been granted roles/aiplatform.user on [${project}].`,
163
+ );
164
+ console.error(` πŸ‘‰ Please run the delegation commands below.`);
165
+ }
166
+ } catch {
167
+ console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
168
+ console.error(
169
+ ` ⚠️ Could not read IAM policy directly. Ensure the delegation commands below were executed.`,
170
+ );
171
+ }
172
+ } else {
173
+ console.error(`\n [3/4] GEAP Quota & IAM Delegation:`);
174
+ console.error(` ⏸️ Skipped until project access is resolved.`);
175
+ }
176
+
177
+ // 4. Check Backend Connectivity
178
+ try {
179
+ const res = await fetch(`${BASE_URL}/health`);
180
+ if (res.ok) {
181
+ console.error(`\n [4/4] EditNBLM Cloud Backend:`);
182
+ console.error(` βœ“ Status: Connected (${BASE_URL})`);
183
+ } else {
184
+ console.error(`\n [4/4] EditNBLM Cloud Backend:`);
185
+ console.error(` ⚠️ Status: HTTP ${res.status}`);
186
+ }
187
+ } catch (err) {
188
+ console.error(`\n [4/4] EditNBLM Cloud Backend:`);
189
+ console.error(` βœ— Status: Unreachable (${err.message})`);
190
+ }
191
+
192
+ // 5. One-Time IAM Delegation command helper
193
+ console.error('\n------------------------------------------------------------------------');
194
+ console.error(' πŸ“‹ One-Time IAM Delegation Commands for your Project:');
195
+ console.error('------------------------------------------------------------------------');
196
+ const targetProject = project || 'YOUR_GCP_PROJECT_ID';
197
+ console.error(`gcloud projects add-iam-policy-binding ${targetProject} \\`);
198
+ console.error(' --member="serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com" \\');
199
+ console.error(' --role="roles/aiplatform.user"\n');
200
+ console.error(`gcloud projects add-iam-policy-binding ${targetProject} \\`);
201
+ console.error(' --member="serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com" \\');
202
+ console.error(' --role="roles/serviceusage.serviceUsageConsumer"');
203
+ console.error('========================================================================\n');
204
+
205
+ if (!hasError) {
206
+ console.error('πŸŽ‰ All checks PASSED! Your environment is ready to use EditNBLM MCP.\n');
207
+ process.exit(0);
208
+ } else {
209
+ console.error('⚠️ Setup incomplete. Please fix the items marked with βœ— above.\n');
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ // If invoked from an interactive terminal directly or with doctor/auth/check args, run diagnostic
215
+ const args = process.argv.slice(2);
216
+ if (
217
+ args.includes('doctor') ||
218
+ args.includes('auth') ||
219
+ args.includes('check') ||
220
+ args.includes('--check') ||
221
+ args.includes('--doctor') ||
222
+ args.includes('-h') ||
223
+ args.includes('--help') ||
224
+ (process.stdin.isTTY && !args.includes('--stdio'))
225
+ ) {
226
+ runDoctor();
227
+ } else {
228
+ startMcpServer();
49
229
  }
50
230
 
51
231
  async function callRemoteMcp(method, params, apiKey) {
52
232
  const token = apiKey || getAuthToken();
233
+ if (!token) {
234
+ throw new Error(
235
+ '[EditNBLM Auth Required] Google Cloud credentials not found. Run `gcloud auth login` or set EDITABLE_SLIDES_API_KEY.',
236
+ );
237
+ }
238
+
53
239
  const project = getProject();
54
- const url = `${BASE_URL}/mcp/${project}`;
240
+ if (!project) {
241
+ throw new Error(
242
+ '[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>.',
243
+ );
244
+ }
55
245
 
246
+ const url = `${BASE_URL}/mcp/${project}`;
56
247
  const headers = {
57
248
  'Content-Type': 'application/json',
58
249
  Accept: 'application/json, text/event-stream',
250
+ Authorization: `Bearer ${token}`,
59
251
  };
60
- if (token) {
61
- headers['Authorization'] = `Bearer ${token}`;
62
- }
63
252
 
64
253
  const res = await fetch(url, {
65
254
  method: 'POST',
@@ -82,7 +271,18 @@ async function callRemoteMcp(method, params, apiKey) {
82
271
  const data = JSON.parse(jsonStr);
83
272
 
84
273
  if (data.error) {
85
- throw new Error(data.error.message || JSON.stringify(data.error));
274
+ const msg = data.error.message || JSON.stringify(data.error);
275
+ if (
276
+ msg.includes('aiplatform.endpoints.predict') ||
277
+ (msg.includes('Permission') && msg.includes('denied'))
278
+ ) {
279
+ throw new Error(
280
+ `[EditNBLM IAM Delegation Required] Project (${project}) has not delegated roles/aiplatform.user to the EditNBLM service account.\n` +
281
+ `Run in terminal:\n` +
282
+ `gcloud projects add-iam-policy-binding ${project} --member="serviceAccount:editnblm-mcp-runtime@editnblm-in-ge-2036.iam.gserviceaccount.com" --role="roles/aiplatform.user"`,
283
+ );
284
+ }
285
+ throw new Error(msg);
86
286
  }
87
287
  return data.result;
88
288
  }
@@ -144,259 +344,264 @@ async function downloadPptxHelper(downloadUrl, targetPath) {
144
344
  return null;
145
345
  }
146
346
 
147
- const server = new McpServer({
148
- name: 'editable-slides',
149
- version: '1.0.0',
150
- });
151
-
152
- // 1. Fast Synchronous Slide Conversion (Single Image)
153
- server.tool(
154
- 'create_slides_from_image',
155
- '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.',
156
- {
157
- filePath: z
158
- .string()
159
- .optional()
160
- .describe('Local file path to the slide image or PDF (e.g. /path/to/slide.png)'),
161
- fileUrl: z
162
- .string()
163
- .optional()
164
- .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
165
- format: z
166
- .enum(['slides', 'pptx'])
167
- .optional()
168
- .describe("Output format: 'slides' (default, Google Slides URL) or 'pptx'"),
169
- apiKey: z
170
- .string()
171
- .optional()
172
- .describe('Optional API key or access token for authentication'),
173
- },
174
- async ({ filePath, fileUrl, format, apiKey }) => {
175
- let targetUrl = fileUrl;
176
- let originalName = fileUrl ? path.basename(fileUrl) : 'presentation';
177
- let isPdf = false;
178
-
179
- if (filePath && !targetUrl) {
180
- const uploaded = await uploadLocalFile(filePath, apiKey);
181
- targetUrl = uploaded.fileUrl;
182
- originalName = uploaded.fileName;
183
- isPdf = uploaded.isPdf;
184
- } else if (targetUrl && targetUrl.toLowerCase().endsWith('.pdf')) {
185
- isPdf = true;
186
- }
347
+ function startMcpServer() {
348
+ const server = new McpServer({
349
+ name: 'editable-slides',
350
+ version: '1.0.0',
351
+ });
187
352
 
188
- if (!targetUrl) {
189
- throw new Error('Either filePath or fileUrl must be provided.');
190
- }
353
+ // 1. Fast Synchronous Slide Conversion (Single Image)
354
+ server.tool(
355
+ 'create_slides_from_image',
356
+ '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.',
357
+ {
358
+ filePath: z
359
+ .string()
360
+ .optional()
361
+ .describe('Local file path to the slide image or PDF (e.g. /path/to/slide.png)'),
362
+ fileUrl: z
363
+ .string()
364
+ .optional()
365
+ .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
366
+ format: z
367
+ .enum(['slides', 'pptx'])
368
+ .optional()
369
+ .describe("Output format: 'slides' (default, Google Slides URL) or 'pptx'"),
370
+ apiKey: z
371
+ .string()
372
+ .optional()
373
+ .describe('Optional API key or access token for authentication'),
374
+ },
375
+ async ({ filePath, fileUrl, format, apiKey }) => {
376
+ let targetUrl = fileUrl;
377
+ let originalName = fileUrl ? path.basename(fileUrl) : 'presentation';
378
+ let isPdf = false;
379
+
380
+ if (filePath && !targetUrl) {
381
+ const uploaded = await uploadLocalFile(filePath, apiKey);
382
+ targetUrl = uploaded.fileUrl;
383
+ originalName = uploaded.fileName;
384
+ isPdf = uploaded.isPdf;
385
+ } else if (targetUrl && targetUrl.toLowerCase().endsWith('.pdf')) {
386
+ isPdf = true;
387
+ }
191
388
 
192
- // If document is a multi-page PDF, route to async pipeline to prevent 120s MCP timeout
193
- if (isPdf) {
194
- console.error('[EditNBLM] Multi-page PDF detected. Starting background pipeline to prevent timeout...');
195
- const startResult = await callRemoteMcp(
389
+ if (!targetUrl) {
390
+ throw new Error('Either filePath or fileUrl must be provided.');
391
+ }
392
+
393
+ // If document is a multi-page PDF, route to async pipeline to prevent 120s MCP timeout
394
+ if (isPdf) {
395
+ console.error('[EditNBLM] Multi-page PDF detected. Starting background pipeline to prevent timeout...');
396
+ const startResult = await callRemoteMcp(
397
+ 'tools/call',
398
+ {
399
+ name: 'start_slide_conversion',
400
+ arguments: {
401
+ fileUrl: targetUrl,
402
+ fileName: originalName,
403
+ format: format || 'slides',
404
+ },
405
+ },
406
+ apiKey,
407
+ );
408
+
409
+ const parsed = JSON.parse(startResult.content[0].text);
410
+ if (parsed.jobId) {
411
+ activeJobs.set(parsed.jobId, { filePath, fileName: originalName });
412
+ parsed.note =
413
+ 'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
414
+ 'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
415
+ 'DO NOT write custom scripts or install packages.';
416
+ parsed.agentInstructions = [
417
+ `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
418
+ 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally.',
419
+ ];
420
+ startResult.content[0].text = JSON.stringify(parsed, null, 2);
421
+ }
422
+ return startResult;
423
+ }
424
+
425
+ // Synchronous single-image conversion
426
+ console.error(`[EditNBLM] Converting image to ${format || 'slides'}...`);
427
+ const result = await callRemoteMcp(
196
428
  'tools/call',
197
429
  {
198
- name: 'start_slide_conversion',
430
+ name: 'create_slides_from_image',
199
431
  arguments: {
200
432
  fileUrl: targetUrl,
201
- fileName: originalName,
202
433
  format: format || 'slides',
203
434
  },
204
435
  },
205
436
  apiKey,
206
437
  );
207
438
 
208
- const parsed = JSON.parse(startResult.content[0].text);
209
- if (parsed.jobId) {
210
- activeJobs.set(parsed.jobId, { filePath, fileName: originalName });
211
- parsed.note =
212
- 'Multi-page PDF detected. High-performance conversion started in background to prevent client timeouts. ' +
213
- 'STRICT MANDATE FOR AGENT: Immediately call `check_slide_conversion` with this jobId to track progress until done. ' +
214
- 'DO NOT write custom scripts or install packages.';
215
- parsed.agentInstructions = [
216
- `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
217
- 'When check_slide_conversion returns status "done", the finished presentation will be automatically saved locally.',
218
- ];
219
- startResult.content[0].text = JSON.stringify(parsed, null, 2);
439
+ try {
440
+ const parsed = JSON.parse(result.content[0].text);
441
+ if (parsed.downloadUrl && filePath) {
442
+ const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
443
+ await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
444
+ parsed.localPptxPath = localPptxPath;
445
+ }
446
+ if (parsed.downloadUrl || parsed.localPptxPath) {
447
+ parsed.agentInstructions = [
448
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
449
+ 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert or upload this file.',
450
+ '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.',
451
+ '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."',
452
+ ];
453
+ }
454
+ result.content[0].text = JSON.stringify(parsed, null, 2);
455
+ } catch {
456
+ // Retain original result
220
457
  }
221
- return startResult;
222
- }
223
458
 
224
- // Synchronous single-image conversion
225
- console.error(`[EditNBLM] Converting image to ${format || 'slides'}...`);
226
- const result = await callRemoteMcp(
227
- 'tools/call',
228
- {
229
- name: 'create_slides_from_image',
230
- arguments: {
231
- fileUrl: targetUrl,
232
- format: format || 'slides',
233
- },
234
- },
235
- apiKey,
236
- );
459
+ return result;
460
+ },
461
+ );
237
462
 
238
- try {
239
- const parsed = JSON.parse(result.content[0].text);
240
- if (parsed.downloadUrl && filePath) {
241
- const localPptxPath = filePath.replace(/\.[^/.]+$/, '') + '.pptx';
242
- await downloadPptxHelper(parsed.downloadUrl, localPptxPath);
243
- parsed.localPptxPath = localPptxPath;
463
+ // 2. Start Asynchronous Slide Conversion (for PDFs & heavy decks)
464
+ server.tool(
465
+ 'start_slide_conversion',
466
+ '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.',
467
+ {
468
+ filePath: z
469
+ .string()
470
+ .optional()
471
+ .describe('Local file path to the slide image or PDF (e.g. /path/to/asdf.pdf)'),
472
+ fileUrl: z
473
+ .string()
474
+ .optional()
475
+ .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
476
+ fileName: z
477
+ .string()
478
+ .optional()
479
+ .describe('Original file name if known'),
480
+ format: z
481
+ .enum(['slides', 'pptx'])
482
+ .optional()
483
+ .describe("What to produce: 'slides' for a Google Slides deck (default), or 'pptx'"),
484
+ apiKey: z
485
+ .string()
486
+ .optional()
487
+ .describe('Optional API key or access token for authentication'),
488
+ },
489
+ async ({ filePath, fileUrl, fileName, format, apiKey }) => {
490
+ let targetUrl = fileUrl;
491
+ let targetFileName = fileName;
492
+
493
+ if (filePath && !targetUrl) {
494
+ const uploaded = await uploadLocalFile(filePath, apiKey);
495
+ targetUrl = uploaded.fileUrl;
496
+ targetFileName = targetFileName || uploaded.fileName;
244
497
  }
245
- if (parsed.downloadUrl || parsed.localPptxPath) {
246
- parsed.agentInstructions = [
247
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
248
- 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert or upload this file.',
249
- '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.',
250
- '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."',
251
- ];
498
+
499
+ if (!targetUrl) {
500
+ throw new Error('Either filePath or fileUrl must be provided.');
252
501
  }
253
- result.content[0].text = JSON.stringify(parsed, null, 2);
254
- } catch {
255
- // Retain original result
256
- }
257
502
 
258
- return result;
259
- },
260
- );
261
-
262
- // 2. Start Asynchronous Slide Conversion (for PDFs & heavy decks)
263
- server.tool(
264
- 'start_slide_conversion',
265
- '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.',
266
- {
267
- filePath: z
268
- .string()
269
- .optional()
270
- .describe('Local file path to the slide image or PDF (e.g. /path/to/asdf.pdf)'),
271
- fileUrl: z
272
- .string()
273
- .optional()
274
- .describe('Public HTTP(S) or gs:// URL of the slide image or PDF'),
275
- fileName: z
276
- .string()
277
- .optional()
278
- .describe('Original file name if known'),
279
- format: z
280
- .enum(['slides', 'pptx'])
281
- .optional()
282
- .describe("What to produce: 'slides' for a Google Slides deck (default), or 'pptx'"),
283
- apiKey: z
284
- .string()
285
- .optional()
286
- .describe('Optional API key or access token for authentication'),
287
- },
288
- async ({ filePath, fileUrl, fileName, format, apiKey }) => {
289
- let targetUrl = fileUrl;
290
- let targetFileName = fileName;
291
-
292
- if (filePath && !targetUrl) {
293
- const uploaded = await uploadLocalFile(filePath, apiKey);
294
- targetUrl = uploaded.fileUrl;
295
- targetFileName = targetFileName || uploaded.fileName;
296
- }
503
+ console.error(`[EditNBLM] Starting background conversion for "${targetFileName || targetUrl}"...`);
504
+ const startResult = await callRemoteMcp(
505
+ 'tools/call',
506
+ {
507
+ name: 'start_slide_conversion',
508
+ arguments: {
509
+ fileUrl: targetUrl,
510
+ fileName: targetFileName,
511
+ format: format || 'slides',
512
+ },
513
+ },
514
+ apiKey,
515
+ );
297
516
 
298
- if (!targetUrl) {
299
- throw new Error('Either filePath or fileUrl must be provided.');
300
- }
517
+ try {
518
+ const parsed = JSON.parse(startResult.content[0].text);
519
+ if (parsed.jobId) {
520
+ activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName });
521
+ parsed.agentInstructions = [
522
+ `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
523
+ 'DO NOT write scripts or install packages while waiting.',
524
+ ];
525
+ startResult.content[0].text = JSON.stringify(parsed, null, 2);
526
+ }
527
+ } catch {}
301
528
 
302
- console.error(`[EditNBLM] Starting background conversion for "${targetFileName || targetUrl}"...`);
303
- const startResult = await callRemoteMcp(
304
- 'tools/call',
305
- {
306
- name: 'start_slide_conversion',
307
- arguments: {
308
- fileUrl: targetUrl,
309
- fileName: targetFileName,
310
- format: format || 'slides',
311
- },
312
- },
313
- apiKey,
314
- );
529
+ return startResult;
530
+ },
531
+ );
315
532
 
316
- try {
317
- const parsed = JSON.parse(startResult.content[0].text);
318
- if (parsed.jobId) {
319
- activeJobs.set(parsed.jobId, { filePath, fileName: targetFileName });
320
- parsed.agentInstructions = [
321
- `Call check_slide_conversion with jobId: "${parsed.jobId}" to follow page-by-page progress.`,
322
- 'DO NOT write scripts or install packages while waiting.',
323
- ];
324
- startResult.content[0].text = JSON.stringify(parsed, null, 2);
325
- }
326
- } catch {}
327
-
328
- return startResult;
329
- },
330
- );
331
-
332
- // 3. Check Slide Conversion Progress & Auto-Download
333
- server.tool(
334
- 'check_slide_conversion',
335
- '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.',
336
- {
337
- jobId: z
338
- .string()
339
- .describe('The jobId returned by start_slide_conversion or create_slides_from_image'),
340
- sinceUpdatedAt: z
341
- .number()
342
- .optional()
343
- .describe('updatedAt from previous check to wait for new progress updates'),
344
- apiKey: z
345
- .string()
346
- .optional()
347
- .describe('Optional API key or access token for authentication'),
348
- },
349
- async ({ jobId, sinceUpdatedAt, apiKey }) => {
350
- const result = await callRemoteMcp(
351
- 'tools/call',
352
- {
353
- name: 'check_slide_conversion',
354
- arguments: { jobId, sinceUpdatedAt },
355
- },
356
- apiKey,
357
- );
533
+ // 3. Check Slide Conversion Progress & Auto-Download
534
+ server.tool(
535
+ 'check_slide_conversion',
536
+ '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.',
537
+ {
538
+ jobId: z
539
+ .string()
540
+ .describe('The jobId returned by start_slide_conversion or create_slides_from_image'),
541
+ sinceUpdatedAt: z
542
+ .number()
543
+ .optional()
544
+ .describe('updatedAt from previous check to wait for new progress updates'),
545
+ apiKey: z
546
+ .string()
547
+ .optional()
548
+ .describe('Optional API key or access token for authentication'),
549
+ },
550
+ async ({ jobId, sinceUpdatedAt, apiKey }) => {
551
+ const result = await callRemoteMcp(
552
+ 'tools/call',
553
+ {
554
+ name: 'check_slide_conversion',
555
+ arguments: { jobId, sinceUpdatedAt },
556
+ },
557
+ apiKey,
558
+ );
358
559
 
359
- try {
360
- const parsed = JSON.parse(result.content[0].text);
361
- const jobInfo = activeJobs.get(jobId);
362
-
363
- if (parsed.status === 'done') {
364
- if (parsed.downloadUrl) {
365
- let destPath = null;
366
- if (jobInfo?.filePath) {
367
- destPath = jobInfo.filePath.replace(/\.[^/.]+$/, '') + '.pptx';
368
- } else {
369
- const safeName = (jobInfo?.fileName || 'presentation').replace(/\.[^/.]+$/, '');
370
- destPath = path.resolve(process.cwd(), `${safeName}.pptx`);
560
+ try {
561
+ const parsed = JSON.parse(result.content[0].text);
562
+ const jobInfo = activeJobs.get(jobId);
563
+
564
+ if (parsed.status === 'done') {
565
+ if (parsed.downloadUrl) {
566
+ let destPath = null;
567
+ if (jobInfo?.filePath) {
568
+ destPath = jobInfo.filePath.replace(/\.[^/.]+$/, '') + '.pptx';
569
+ } else {
570
+ const safeName = (jobInfo?.fileName || 'presentation').replace(/\.[^/.]+$/, '');
571
+ destPath = path.resolve(process.cwd(), `${safeName}.pptx`);
572
+ }
573
+
574
+ await downloadPptxHelper(parsed.downloadUrl, destPath);
575
+ parsed.localPptxPath = destPath;
371
576
  }
372
577
 
373
- await downloadPptxHelper(parsed.downloadUrl, destPath);
374
- parsed.localPptxPath = destPath;
578
+ parsed.agentInstructions = [
579
+ 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
580
+ parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
581
+ 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert this file.',
582
+ '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.',
583
+ '2. If you DO NOT have a Google Drive MCP tool, present the local file path and download URL to the user directly.',
584
+ ].filter(Boolean);
375
585
  }
376
586
 
377
- parsed.agentInstructions = [
378
- 'TASK FINISHED: The presentation has been successfully generated and saved as an editable PowerPoint (.pptx) file.',
379
- parsed.localPptxPath ? `Local file: ${parsed.localPptxPath}` : null,
380
- 'STRICT MANDATE FOR AGENT: DO NOT write custom scripts, install Python/Node packages, or execute terminal commands to convert this file.',
381
- '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.',
382
- '2. If you DO NOT have a Google Drive MCP tool, present the local file path and download URL to the user directly.',
383
- ].filter(Boolean);
384
- }
385
-
386
- result.content[0].text = JSON.stringify(parsed, null, 2);
387
- } catch {}
587
+ result.content[0].text = JSON.stringify(parsed, null, 2);
588
+ } catch {}
388
589
 
389
- return result;
390
- },
391
- );
590
+ return result;
591
+ },
592
+ );
392
593
 
393
- async function main() {
394
594
  const transport = new StdioServerTransport();
395
- await server.connect(transport);
396
- console.error('[EditNBLM] Ready and connected to EditNBLM Cloud.');
595
+ server.connect(transport).then(() => {
596
+ const project = getProject();
597
+ if (project) {
598
+ console.error(`[EditNBLM] Ready. Connected to EditNBLM Cloud (Billing Project: ${project}).`);
599
+ } else {
600
+ console.error('[EditNBLM] ⚠️ Warning: No active GCP project detected for billing.');
601
+ console.error('[EditNBLM] Run `npx @jieunmarslim/server-editable-slides doctor` to configure.');
602
+ }
603
+ }).catch((err) => {
604
+ console.error('[EditNBLM] Fatal error:', err);
605
+ process.exit(1);
606
+ });
397
607
  }
398
-
399
- main().catch((err) => {
400
- console.error('[EditNBLM] Fatal error:', err);
401
- process.exit(1);
402
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jieunmarslim/server-editable-slides",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Model Context Protocol (MCP) client for Editable Slides Cloud",
5
5
  "type": "module",
6
6
  "bin": {