@unboundai/tb-ppt-mgagent 0.1.0 → 0.2.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 +50 -4
  2. package/build/index.js +95 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +15,9 @@ Built against the official Managed Agents docs (session creation, session operat
15
15
 
16
16
  ## What's still an assumption
17
17
 
18
- Everything above matches the documented API and SDK method signatures verbatim (verified against platform.claude.com/docs/en/managed-agents/\* while building this). The one thing **not** confirmed against a real response is the exact `filename`/path convention the Files API returns for a file the agent wrote to its outputs directory — the code guesses by matching `/outputs?[\/]/i` against the filename and falling back to any `.pptx` file on the session if nothing matches that pattern. If a real run's file list looks different, adjust the filter in `get_tb_powerpoint_result` in `src/index.ts`.
18
+ Every SDK call in `src/index.ts` (`sessions.create`, `sessions.events.send`, `sessions.retrieve`, `sessions.events.list`, `files.list`, `files.download`) is checked against `@anthropic-ai/sdk`'s own `.d.ts`/source (installed version, not just the docs prose) — that's how a real bug was caught in 0.1.0: the docs' TypeScript sample for downloading a file shows `content.writeToFile(path)`, but that method doesn't exist on the installed SDK. `files.download()` actually resolves to a plain `Response`; 0.1.1 fixes this by reading it with `.arrayBuffer()` and writing the bytes directly.
19
+
20
+ The one thing still **not** confirmed against a real response is the exact `filename`/path convention the Files API returns for a file the agent wrote to its outputs directory — the code guesses by matching `/outputs?[\/]/i` against the filename and falling back to any `.pptx` file on the session if nothing matches that pattern. If a real run's file list looks different, adjust the filter in `get_tb_powerpoint_result` in `src/index.ts`.
19
21
 
20
22
  ## Setup
21
23
 
@@ -36,14 +38,39 @@ Everything above matches the documented API and SDK method signatures verbatim (
36
38
  - `ANTHROPIC_API_BASE` — optional, overrides the SDK's default API base URL.
37
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.
38
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
+ | `not_end_turn` | `get_tb_powerpoint_result` | idle, but `stop_reason` wasn't `end_turn`; includes `stopReason` |
53
+ | `no_output_file` | `get_tb_powerpoint_result` | finished cleanly but no `.pptx` found; includes `fileCount` |
54
+ | `success` | `get_tb_powerpoint_result` | file downloaded; includes `filename`, `downloadUrl` |
55
+ | `invalid_arguments` | either | zod validation failed; includes `details` |
56
+ | `error` | either | anything else threw; includes `message` |
57
+
58
+ All entries include `durationMs` except the initial `started` line. To see what's been run:
59
+
60
+ ```bash
61
+ tail -f logs/tasks.log
62
+ # or, for just failures:
63
+ grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
64
+ ```
39
65
 
40
- 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`):
66
+ 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:
41
67
 
42
68
  ```json
43
69
  "tb-ppt-mgagent": {
44
- "command": "node",
70
+ "command": "npx",
45
71
  "args": [
46
- "/path/to/tb-ppt-mgAgent/build/index.js"
72
+ "-y",
73
+ "@unboundai/tb-ppt-mgagent"
47
74
  ],
48
75
  "env": {
49
76
  "ANTHROPIC_API_KEY": "sk-ant-...",
@@ -55,6 +82,25 @@ Everything above matches the documented API and SDK method signatures verbatim (
55
82
  }
56
83
  ```
57
84
 
85
+ Or, from a local checkout of this repo:
86
+
87
+ ```json
88
+ "tb-ppt-mgagent": {
89
+ "command": "node",
90
+ "args": [
91
+ "/path/to/tb-ppt-mgAgent/build/index.js"
92
+ ],
93
+ "env": { "...": "same as above" }
94
+ }
95
+ ```
96
+
97
+ ### Publishing a new version
98
+
99
+ ```bash
100
+ npm version patch # or minor/major
101
+ npm publish # prepublishOnly rebuilds automatically; will prompt for a 2FA OTP
102
+ ```
103
+
58
104
  ## Development
59
105
 
60
106
  ```bash
package/build/index.js CHANGED
@@ -19,11 +19,13 @@
19
19
  * automatically for session endpoints. The Files API additionally takes an
20
20
  * explicit `betas` array on list/download, per the docs' own example.
21
21
  *
22
- * A couple of the beta SDK's exact TypeScript types for newer event/file
23
- * fields (stop_reason, processed_at, filename) aren't fully pinned down
24
- * here - those spots are narrowed with `as any` and a comment rather than
25
- * guessed field names baked into strict types, so a real API response can't
26
- * silently mismatch a wrong compile-time shape.
22
+ * Every call here is checked against @anthropic-ai/sdk's own .d.ts/source
23
+ * (v0.120.0, the latest published at the time of writing) rather than taken
24
+ * on faith from the docs prose - the docs' TypeScript sample for downloading
25
+ * a file shows `content.writeToFile(path)`, but that method does not exist
26
+ * on the installed SDK: `files.download()` resolves to a plain `Response`,
27
+ * read with `.arrayBuffer()` below. If a future SDK version reintroduces a
28
+ * writeToFile-style helper, this can simplify back down.
27
29
  *
28
30
  * Environment variables:
29
31
  * - ANTHROPIC_API_KEY: required. Anthropic API key with Managed Agents access.
@@ -35,6 +37,11 @@
35
37
  * - TB_PPT_PUBLIC_URL_BASE: required for retrieval. Base URL the synced
36
38
  * folder is served from (e.g. "https://storage.googleapis.com/bucket/prefix"),
37
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.
38
45
  */
39
46
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
40
47
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -43,7 +50,27 @@ import { z } from "zod";
43
50
  import Anthropic from "@anthropic-ai/sdk";
44
51
  import fs from "fs";
45
52
  import path from "path";
53
+ import { fileURLToPath } from "url";
46
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
+ }
47
74
  const API_KEY = process.env.ANTHROPIC_API_KEY;
48
75
  const AGENT_ID = process.env.ANTHROPIC_AGENT_ID;
49
76
  const ENVIRONMENT_ID = process.env.ANTHROPIC_ENVIRONMENT_ID;
@@ -166,9 +193,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
166
193
  });
167
194
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
168
195
  const { name, arguments: args } = request.params;
196
+ const startedAt = Date.now();
169
197
  try {
170
198
  if (name === "generate_tb_powerpoint") {
171
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
+ });
172
209
  const { apiKey, agentId, environmentId } = requireSessionConfig();
173
210
  const client = getClient(apiKey);
174
211
  const session = await client.beta.sessions.create({
@@ -189,6 +226,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
189
226
  { type: "user.message", content: [{ type: "text", text: messageText }] }
190
227
  ]
191
228
  });
229
+ logTask({
230
+ tool: name,
231
+ status: "success",
232
+ sessionId: session.id,
233
+ durationMs: Date.now() - startedAt
234
+ });
192
235
  return {
193
236
  content: [{
194
237
  type: "text",
@@ -202,10 +245,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
202
245
  }
203
246
  if (name === "get_tb_powerpoint_result") {
204
247
  const { sessionId } = GetTbPowerpointResultSchema.parse(args);
248
+ logTask({ tool: name, status: "started", sessionId });
205
249
  const apiKey = requireApiKey();
206
250
  const client = getClient(apiKey);
207
251
  const session = await client.beta.sessions.retrieve(sessionId);
208
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
+ });
209
260
  return {
210
261
  content: [{
211
262
  type: "text",
@@ -218,17 +269,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
218
269
  };
219
270
  }
220
271
  // Find the most recent session.status_idle event to read its stop_reason.
221
- // (Its exact field shape isn't pinned down in our TS types yet - see file header.)
222
272
  let latestIdleEvent = null;
223
273
  for await (const event of client.beta.sessions.events.list(sessionId)) {
224
274
  if (event.type === "session.status_idle") {
225
- if (!latestIdleEvent || (event.processed_at ?? "") > (latestIdleEvent.processed_at ?? "")) {
275
+ if (!latestIdleEvent || event.processed_at > latestIdleEvent.processed_at) {
226
276
  latestIdleEvent = event;
227
277
  }
228
278
  }
229
279
  }
230
- const stopReasonType = latestIdleEvent?.stop_reason?.type;
280
+ const stopReasonType = latestIdleEvent?.stop_reason.type;
231
281
  if (stopReasonType !== "end_turn") {
282
+ logTask({
283
+ tool: name,
284
+ status: "not_end_turn",
285
+ sessionId,
286
+ stopReason: stopReasonType ?? null,
287
+ durationMs: Date.now() - startedAt
288
+ });
232
289
  return {
233
290
  content: [{
234
291
  type: "text",
@@ -246,11 +303,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
246
303
  scope_id: sessionId,
247
304
  betas: [MANAGED_AGENTS_BETA]
248
305
  });
249
- const fileList = files.data ?? [];
250
- const pptxFiles = fileList.filter((f) => /\.pptx$/i.test(f.filename ?? ""));
251
- const outputPptxFiles = pptxFiles.filter((f) => /outputs?[\\/]/i.test(f.filename ?? ""));
306
+ const fileList = files.data;
307
+ const pptxFiles = fileList.filter((f) => /\.pptx$/i.test(f.filename));
308
+ const outputPptxFiles = pptxFiles.filter((f) => /outputs?[\\/]/i.test(f.filename));
252
309
  const chosen = outputPptxFiles[0] ?? pptxFiles[0];
253
310
  if (!chosen) {
311
+ logTask({
312
+ tool: name,
313
+ status: "no_output_file",
314
+ sessionId,
315
+ fileCount: fileList.length,
316
+ durationMs: Date.now() - startedAt
317
+ });
254
318
  return {
255
319
  content: [{
256
320
  type: "text",
@@ -267,10 +331,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
267
331
  const { outputDir, publicUrlBase } = requireOutputConfig();
268
332
  const safeName = path.basename(chosen.filename || `${sessionId}.pptx`);
269
333
  const destPath = path.join(outputDir, safeName);
334
+ // files.download() resolves to a plain Response on this SDK version - no
335
+ // writeToFile() helper (see file header). Read the bytes and write them ourselves.
336
+ const response = await client.beta.files.download(chosen.id, {
337
+ betas: [MANAGED_AGENTS_BETA]
338
+ });
339
+ const arrayBuffer = await response.arrayBuffer();
270
340
  fs.mkdirSync(outputDir, { recursive: true });
271
- const content = await client.beta.files.download(chosen.id);
272
- await content.writeToFile(destPath);
341
+ fs.writeFileSync(destPath, Buffer.from(arrayBuffer));
273
342
  const downloadUrl = `${publicUrlBase.replace(/\/$/, "")}/${encodeURIComponent(safeName)}`;
343
+ logTask({
344
+ tool: name,
345
+ status: "success",
346
+ sessionId,
347
+ filename: safeName,
348
+ downloadUrl,
349
+ durationMs: Date.now() - startedAt
350
+ });
274
351
  return {
275
352
  content: [{
276
353
  type: "text",
@@ -288,17 +365,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
288
365
  throw new Error(`Unknown tool: ${name}`);
289
366
  }
290
367
  catch (error) {
368
+ const durationMs = Date.now() - startedAt;
291
369
  if (error instanceof z.ZodError) {
370
+ const details = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
371
+ logTask({ tool: name, status: "invalid_arguments", details, durationMs });
292
372
  return {
293
373
  content: [{
294
374
  type: "text",
295
375
  text: JSON.stringify({
296
376
  error: "Invalid arguments",
297
- details: error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ")
377
+ details
298
378
  }, null, 2)
299
379
  }]
300
380
  };
301
381
  }
382
+ logTask({ tool: name, status: "error", message: error.message, durationMs });
302
383
  return {
303
384
  content: [{
304
385
  type: "text",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundai/tb-ppt-mgagent",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": {