@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.
- package/README.md +33 -0
- package/build/index.js +136 -2
- 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
|
|
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.
|
|
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": {
|