@unboundai/tb-ppt-mgagent 0.1.1 → 0.3.0

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 (3) hide show
  1. package/README.md +33 -0
  2. package/build/index.js +136 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,6 +38,39 @@ The one thing still **not** confirmed against a real response is the exact `file
38
38
  - `ANTHROPIC_API_BASE` — optional, overrides the SDK's default API base URL.
39
39
  - `TB_PPT_OUTPUT_DIR` — required for `get_tb_powerpoint_result`. Local folder that's synced to GCS by something outside this server; finished `.pptx` files are written here.
40
40
  - `TB_PPT_PUBLIC_URL_BASE` — required for `get_tb_powerpoint_result`. Base URL the synced folder is served from (e.g. `https://storage.googleapis.com/bucket/prefix`), used to build the returned download link.
41
+ - `TB_PPT_LOG_FILE` — optional. Path to the task log file. Defaults to `logs/tasks.log` next to this package.
42
+
43
+ ## Task logging
44
+
45
+ Every tool call writes one JSON-lines entry to `TB_PPT_LOG_FILE` (default `logs/tasks.log`) and mirrors it to stderr (which the MCP host's own log viewer already shows, as seen in the `[powerpoint-creator]` logs). Each line has a `timestamp`, `tool`, and `status`, plus whatever's relevant to that outcome:
46
+
47
+ | status | tool | meaning |
48
+ |---|---|---|
49
+ | `started` | either | call received, with its key arguments (outline preview/length, or sessionId) |
50
+ | `success` | `generate_tb_powerpoint` | session created; includes `sessionId` |
51
+ | `not_ready` | `get_tb_powerpoint_result` | session isn't `idle` yet; includes `sessionStatus` |
52
+ | `agent_message` | `get_tb_powerpoint_result` | the agent's own most recent `agent.message` text, up to 500 chars, in `agentMessagePreview` — this is what the agent actually said, not our derived status. Logged once the session is idle, before the stop-reason check, so it's there even on `not_end_turn` |
53
+ | `not_end_turn` | `get_tb_powerpoint_result` | idle, but `stop_reason` wasn't `end_turn`; includes `stopReason` |
54
+ | `listing_files` | `get_tb_powerpoint_result` | about to call Files API `list` for this session |
55
+ | `files_listed` | `get_tb_powerpoint_result` | includes `fileCount`, every `.pptx` candidate found (`pptxCandidates`, each with `id`/`filename`/`sizeBytes`), and which one was `chosenFileId` |
56
+ | `no_output_file` | `get_tb_powerpoint_result` | finished cleanly but no `.pptx` found; includes `fileCount` |
57
+ | `download_started` | `get_tb_powerpoint_result` | about to call Files API `download`; includes `fileId`, `filename`, `expectedSizeBytes` |
58
+ | `download_completed` | `get_tb_powerpoint_result` | bytes received; includes `downloadedBytes` |
59
+ | `saving_file` | `get_tb_powerpoint_result` | about to write to `destPath` inside `outputDir` |
60
+ | `file_saved` | `get_tb_powerpoint_result` | write confirmed via `fs.statSync`; includes `savedBytes` and `bytesMatch` (should be `true`) |
61
+ | `success` | `get_tb_powerpoint_result` | fully done; includes `filename`, `destPath`, `downloadUrl` |
62
+ | `invalid_arguments` | either | zod validation failed; includes `details` |
63
+ | `error` | either | anything else threw; includes `message` |
64
+
65
+ The agent's actual final message is also returned to the caller as `agentMessage` on every `get_tb_powerpoint_result` response (not just logged), so a not-quite-right run can be diagnosed from the tool's own output, not just the log file.
66
+
67
+ All entries include `durationMs` except the intermediate progress lines (`agent_message`, `listing_files`, `files_listed`, `download_started`, `download_completed`, `saving_file`) — those are pure checkpoints, timed by their surrounding `started`/`success`/`error` lines instead. To see what's been run:
68
+
69
+ ```bash
70
+ tail -f logs/tasks.log
71
+ # or, for just failures:
72
+ grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
73
+ ```
41
74
 
42
75
  4. **Register with your MCP client** (Claude Desktop / TypingMind's local MCP connector — same pattern as this repo's siblings, e.g. `tb-gcs-assembly-mcp`). Published to npm as [`@unboundai/tb-ppt-mgagent`](https://www.npmjs.com/package/@unboundai/tb-ppt-mgagent), so `npx` can run it without a local checkout:
43
76
 
package/build/index.js CHANGED
@@ -37,6 +37,11 @@
37
37
  * - TB_PPT_PUBLIC_URL_BASE: required for retrieval. Base URL the synced
38
38
  * folder is served from (e.g. "https://storage.googleapis.com/bucket/prefix"),
39
39
  * used to build the download link returned to the caller.
40
+ * - TB_PPT_LOG_FILE: optional. Path to a JSON-lines task log (one call per
41
+ * line: tool, status, timing, and relevant ids). Defaults to logs/tasks.log
42
+ * next to this package. Every entry is also mirrored to stderr, which the
43
+ * MCP host's own log viewer already picks up - the file exists so history
44
+ * survives a host restart and can be grepped/tailed directly.
40
45
  */
41
46
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
42
47
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -45,7 +50,27 @@ import { z } from "zod";
45
50
  import Anthropic from "@anthropic-ai/sdk";
46
51
  import fs from "fs";
47
52
  import path from "path";
53
+ import { fileURLToPath } from "url";
48
54
  const MANAGED_AGENTS_BETA = "managed-agents-2026-04-01";
55
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
56
+ const LOG_FILE = process.env.TB_PPT_LOG_FILE || path.join(__dirname, "..", "logs", "tasks.log");
57
+ /**
58
+ * Records one line of task history: a JSON object to LOG_FILE, and a short
59
+ * human-readable echo to stderr (stdout is reserved for MCP's own JSON-RPC
60
+ * traffic in stdio mode - never write task logs there).
61
+ */
62
+ function logTask(event) {
63
+ const record = { timestamp: new Date().toISOString(), ...event };
64
+ const line = JSON.stringify(record);
65
+ try {
66
+ fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
67
+ fs.appendFileSync(LOG_FILE, line + "\n");
68
+ }
69
+ catch (err) {
70
+ console.error(`Failed to write task log to ${LOG_FILE}: ${err.message}`);
71
+ }
72
+ console.error(`[task] ${line}`);
73
+ }
49
74
  const API_KEY = process.env.ANTHROPIC_API_KEY;
50
75
  const AGENT_ID = process.env.ANTHROPIC_AGENT_ID;
51
76
  const ENVIRONMENT_ID = process.env.ANTHROPIC_ENVIRONMENT_ID;
@@ -168,9 +193,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
168
193
  });
169
194
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
170
195
  const { name, arguments: args } = request.params;
196
+ const startedAt = Date.now();
171
197
  try {
172
198
  if (name === "generate_tb_powerpoint") {
173
199
  const { outline, deckType, clientName, filename } = GenerateTbPowerpointSchema.parse(args);
200
+ logTask({
201
+ tool: name,
202
+ status: "started",
203
+ outlinePreview: outline.slice(0, 120),
204
+ outlineLength: outline.length,
205
+ deckType: deckType ?? null,
206
+ clientName: clientName ?? null,
207
+ filename: filename ?? null
208
+ });
174
209
  const { apiKey, agentId, environmentId } = requireSessionConfig();
175
210
  const client = getClient(apiKey);
176
211
  const session = await client.beta.sessions.create({
@@ -191,6 +226,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
191
226
  { type: "user.message", content: [{ type: "text", text: messageText }] }
192
227
  ]
193
228
  });
229
+ logTask({
230
+ tool: name,
231
+ status: "success",
232
+ sessionId: session.id,
233
+ durationMs: Date.now() - startedAt
234
+ });
194
235
  return {
195
236
  content: [{
196
237
  type: "text",
@@ -204,10 +245,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
204
245
  }
205
246
  if (name === "get_tb_powerpoint_result") {
206
247
  const { sessionId } = GetTbPowerpointResultSchema.parse(args);
248
+ logTask({ tool: name, status: "started", sessionId });
207
249
  const apiKey = requireApiKey();
208
250
  const client = getClient(apiKey);
209
251
  const session = await client.beta.sessions.retrieve(sessionId);
210
252
  if (session.status !== "idle") {
253
+ logTask({
254
+ tool: name,
255
+ status: "not_ready",
256
+ sessionId,
257
+ sessionStatus: session.status,
258
+ durationMs: Date.now() - startedAt
259
+ });
211
260
  return {
212
261
  content: [{
213
262
  type: "text",
@@ -219,17 +268,44 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
219
268
  }]
220
269
  };
221
270
  }
222
- // Find the most recent session.status_idle event to read its stop_reason.
271
+ // Find the most recent session.status_idle event to read its stop_reason, and
272
+ // the most recent agent.message event so we can log/return what the agent
273
+ // actually said (not just our own derived status).
223
274
  let latestIdleEvent = null;
275
+ let latestAgentMessage = null;
224
276
  for await (const event of client.beta.sessions.events.list(sessionId)) {
225
277
  if (event.type === "session.status_idle") {
226
278
  if (!latestIdleEvent || event.processed_at > latestIdleEvent.processed_at) {
227
279
  latestIdleEvent = event;
228
280
  }
229
281
  }
282
+ else if (event.type === "agent.message") {
283
+ const text = event.content
284
+ .filter((block) => block.type === "text")
285
+ .map((block) => block.text)
286
+ .join("\n")
287
+ .trim();
288
+ if (text && (!latestAgentMessage || event.processed_at > latestAgentMessage.processed_at)) {
289
+ latestAgentMessage = { processed_at: event.processed_at, text };
290
+ }
291
+ }
230
292
  }
293
+ const agentMessagePreview = latestAgentMessage ? latestAgentMessage.text.slice(0, 500) : null;
294
+ logTask({
295
+ tool: name,
296
+ status: "agent_message",
297
+ sessionId,
298
+ agentMessagePreview
299
+ });
231
300
  const stopReasonType = latestIdleEvent?.stop_reason.type;
232
301
  if (stopReasonType !== "end_turn") {
302
+ logTask({
303
+ tool: name,
304
+ status: "not_end_turn",
305
+ sessionId,
306
+ stopReason: stopReasonType ?? null,
307
+ durationMs: Date.now() - startedAt
308
+ });
233
309
  return {
234
310
  content: [{
235
311
  type: "text",
@@ -237,12 +313,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
237
313
  ready: false,
238
314
  status: session.status,
239
315
  stopReason: stopReasonType ?? null,
316
+ agentMessage: agentMessagePreview,
240
317
  message: `Session is idle but its stop reason is "${stopReasonType ?? "unknown"}", not "end_turn" - not fetching files yet.`
241
318
  }, null, 2)
242
319
  }]
243
320
  };
244
321
  }
245
322
  // Session finished cleanly - look for a .pptx output file.
323
+ logTask({ tool: name, status: "listing_files", sessionId });
246
324
  const files = await client.beta.files.list({
247
325
  scope_id: sessionId,
248
326
  betas: [MANAGED_AGENTS_BETA]
@@ -251,7 +329,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
251
329
  const pptxFiles = fileList.filter((f) => /\.pptx$/i.test(f.filename));
252
330
  const outputPptxFiles = pptxFiles.filter((f) => /outputs?[\\/]/i.test(f.filename));
253
331
  const chosen = outputPptxFiles[0] ?? pptxFiles[0];
332
+ logTask({
333
+ tool: name,
334
+ status: "files_listed",
335
+ sessionId,
336
+ fileCount: fileList.length,
337
+ pptxCandidates: pptxFiles.map((f) => ({ id: f.id, filename: f.filename, sizeBytes: f.size_bytes })),
338
+ chosenFileId: chosen?.id ?? null
339
+ });
254
340
  if (!chosen) {
341
+ logTask({
342
+ tool: name,
343
+ status: "no_output_file",
344
+ sessionId,
345
+ fileCount: fileList.length,
346
+ durationMs: Date.now() - startedAt
347
+ });
255
348
  return {
256
349
  content: [{
257
350
  type: "text",
@@ -259,6 +352,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
259
352
  ready: true,
260
353
  status: "idle",
261
354
  stopReason: "end_turn",
355
+ agentMessage: agentMessagePreview,
262
356
  message: "Session finished but no .pptx output file was found.",
263
357
  sessionFiles: fileList.map((f) => ({ id: f.id, filename: f.filename }))
264
358
  }, null, 2)
@@ -268,15 +362,50 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
268
362
  const { outputDir, publicUrlBase } = requireOutputConfig();
269
363
  const safeName = path.basename(chosen.filename || `${sessionId}.pptx`);
270
364
  const destPath = path.join(outputDir, safeName);
365
+ logTask({
366
+ tool: name,
367
+ status: "download_started",
368
+ sessionId,
369
+ fileId: chosen.id,
370
+ filename: safeName,
371
+ expectedSizeBytes: chosen.size_bytes
372
+ });
271
373
  // files.download() resolves to a plain Response on this SDK version - no
272
374
  // writeToFile() helper (see file header). Read the bytes and write them ourselves.
273
375
  const response = await client.beta.files.download(chosen.id, {
274
376
  betas: [MANAGED_AGENTS_BETA]
275
377
  });
276
378
  const arrayBuffer = await response.arrayBuffer();
379
+ const downloadedBytes = arrayBuffer.byteLength;
380
+ logTask({
381
+ tool: name,
382
+ status: "download_completed",
383
+ sessionId,
384
+ fileId: chosen.id,
385
+ downloadedBytes
386
+ });
387
+ logTask({ tool: name, status: "saving_file", sessionId, destPath, outputDir });
277
388
  fs.mkdirSync(outputDir, { recursive: true });
278
389
  fs.writeFileSync(destPath, Buffer.from(arrayBuffer));
390
+ const savedBytes = fs.statSync(destPath).size;
391
+ logTask({
392
+ tool: name,
393
+ status: "file_saved",
394
+ sessionId,
395
+ destPath,
396
+ savedBytes,
397
+ bytesMatch: savedBytes === downloadedBytes
398
+ });
279
399
  const downloadUrl = `${publicUrlBase.replace(/\/$/, "")}/${encodeURIComponent(safeName)}`;
400
+ logTask({
401
+ tool: name,
402
+ status: "success",
403
+ sessionId,
404
+ filename: safeName,
405
+ destPath,
406
+ downloadUrl,
407
+ durationMs: Date.now() - startedAt
408
+ });
280
409
  return {
281
410
  content: [{
282
411
  type: "text",
@@ -284,6 +413,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
284
413
  ready: true,
285
414
  status: "idle",
286
415
  stopReason: "end_turn",
416
+ agentMessage: agentMessagePreview,
287
417
  filename: safeName,
288
418
  savedTo: destPath,
289
419
  downloadUrl
@@ -294,17 +424,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
294
424
  throw new Error(`Unknown tool: ${name}`);
295
425
  }
296
426
  catch (error) {
427
+ const durationMs = Date.now() - startedAt;
297
428
  if (error instanceof z.ZodError) {
429
+ const details = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
430
+ logTask({ tool: name, status: "invalid_arguments", details, durationMs });
298
431
  return {
299
432
  content: [{
300
433
  type: "text",
301
434
  text: JSON.stringify({
302
435
  error: "Invalid arguments",
303
- details: error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")
436
+ details
304
437
  }, null, 2)
305
438
  }]
306
439
  };
307
440
  }
441
+ logTask({ tool: name, status: "error", message: error.message, durationMs });
308
442
  return {
309
443
  content: [{
310
444
  type: "text",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundai/tb-ppt-mgagent",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server that hands a presentation outline to the Traffic Builders PowerPoint Managed Agent, then retrieves the finished .pptx",
5
5
  "main": "build/index.js",
6
6
  "bin": {