@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.
- package/README.md +50 -4
- package/build/index.js +95 -14
- 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
|
-
|
|
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": "
|
|
70
|
+
"command": "npx",
|
|
45
71
|
"args": [
|
|
46
|
-
"
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": {
|