@unboundai/tb-ppt-mgagent 0.2.0 → 0.4.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 +62 -36
- package/build/index.js +124 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# Traffic Builders PowerPoint MCP Server
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server that hands a presentation outline to Anthropic's hosted **Managed Agent** for building Traffic Builders-branded PowerPoints, then retrieves the finished file once it's done.
|
|
3
|
+
A Model Context Protocol (MCP) server that hands a presentation outline to Anthropic's hosted **Managed Agent** for building Traffic Builders-branded PowerPoints, then retrieves the finished file once it's done.
|
|
4
|
+
|
|
5
|
+
Brand assets (the design brief, logos, Outfit font) are **not** assumed to already exist in the agent's environment. A Managed Agents "deployment" (scheduled/cron runs) was investigated as a way to pin a session to a specific pre-configured setup, but it only re-sends a fixed baked-in message on a schedule — it offers no extra asset-targeting capability over a plain session. So instead, this server uploads each brand asset once via the Files API (a one-time step, see [Setup](#setup)) and attaches them explicitly to **every** session it creates via the `resources` array, mounted into the sandbox for the agent to read.
|
|
4
6
|
|
|
5
7
|
Built against the official Managed Agents docs (session creation, session operations, events & streaming, files, event/type reference) using the official `@anthropic-ai/sdk` client, which sets the `managed-agents-2026-04-01` beta header automatically for session endpoints.
|
|
6
8
|
|
|
7
9
|
## Available tools
|
|
8
10
|
|
|
9
|
-
- **`generate_tb_powerpoint(outline, deckType?, clientName?, filename?)`** — creates a session against the configured agent + environment, sends the outline as the first message, returns `{ success, sessionId, message }`. Does not wait for the deck to finish.
|
|
11
|
+
- **`generate_tb_powerpoint(outline, deckType?, clientName?, filename?)`** — creates a session against the configured agent + environment, attaches whichever brand asset `resources` are configured (see Setup), sends the outline as the first message, returns `{ success, sessionId, message }`. Does not wait for the deck to finish.
|
|
10
12
|
|
|
11
13
|
- **`get_tb_powerpoint_result(sessionId)`** — checks on a generation started above:
|
|
12
14
|
1. Retrieves the session. If its `status` isn't `idle`, returns `{ ready: false, status }` — still running, try again later.
|
|
@@ -15,7 +17,7 @@ Built against the official Managed Agents docs (session creation, session operat
|
|
|
15
17
|
|
|
16
18
|
## What's still an assumption
|
|
17
19
|
|
|
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.
|
|
20
|
+
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. The `resources` shape (`{ type: "file", file_id, mount_path }`) is likewise checked against the SDK source, not just the docs.
|
|
19
21
|
|
|
20
22
|
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`.
|
|
21
23
|
|
|
@@ -31,7 +33,13 @@ The one thing still **not** confirmed against a real response is the exact `file
|
|
|
31
33
|
npm run build
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
3. **
|
|
36
|
+
3. **Upload the brand assets** (one time, or again whenever an asset changes):
|
|
37
|
+
```
|
|
38
|
+
.\uploadAssets.ps1 -ApiKey "sk-ant-..."
|
|
39
|
+
```
|
|
40
|
+
Uploads `TrafficBuilders-PPT-Handoff.md`, all five `logos/*.png`, and `fonts/Outfit-VariableFont_wght.ttf` to the Files API, and prints a `file_id` per asset along with the matching env var name. These are just config, not code — plug them into step 5 below and there's nothing else to change or republish.
|
|
41
|
+
|
|
42
|
+
4. **Configure environment variables**:
|
|
35
43
|
- `ANTHROPIC_API_KEY` — Anthropic API key with Managed Agents access.
|
|
36
44
|
- `ANTHROPIC_AGENT_ID` — e.g. `agent_01H8osDSMeEtGcEHT1ajzuin`.
|
|
37
45
|
- `ANTHROPIC_ENVIRONMENT_ID` — e.g. `env_01VHzZmqBababsghFjQoAGFx`.
|
|
@@ -39,31 +47,9 @@ The one thing still **not** confirmed against a real response is the exact `file
|
|
|
39
47
|
- `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
48
|
- `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
49
|
- `TB_PPT_LOG_FILE` — optional. Path to the task log file. Defaults to `logs/tasks.log` next to this package.
|
|
50
|
+
- `TB_DESIGN_BRIEF_FILE_ID`, `TB_LOGO_HORIZONTAL_FILE_ID`, `TB_LOGO_HORIZONTAL_WHITE_FILE_ID`, `TB_LOGO_ICON_FILE_ID`, `TB_LOGO_ICON_WHITE_FILE_ID`, `TB_LOGO_ICON_TAGLINE_FILE_ID`, `TB_FONT_FILE_ID` — all optional but strongly recommended, from step 3. Any left unset are just skipped (with a warning in the log) rather than failing the call — the deck still gets built, just without that asset available to the agent.
|
|
42
51
|
|
|
43
|
-
|
|
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
|
-
```
|
|
65
|
-
|
|
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:
|
|
52
|
+
5. **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:
|
|
67
53
|
|
|
68
54
|
```json
|
|
69
55
|
"tb-ppt-mgagent": {
|
|
@@ -77,7 +63,14 @@ grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
|
|
|
77
63
|
"ANTHROPIC_AGENT_ID": "agent_...",
|
|
78
64
|
"ANTHROPIC_ENVIRONMENT_ID": "env_...",
|
|
79
65
|
"TB_PPT_OUTPUT_DIR": "/path/to/gcs-synced-folder",
|
|
80
|
-
"TB_PPT_PUBLIC_URL_BASE": "https://storage.googleapis.com/your-bucket/your-prefix"
|
|
66
|
+
"TB_PPT_PUBLIC_URL_BASE": "https://storage.googleapis.com/your-bucket/your-prefix",
|
|
67
|
+
"TB_DESIGN_BRIEF_FILE_ID": "file_...",
|
|
68
|
+
"TB_LOGO_HORIZONTAL_FILE_ID": "file_...",
|
|
69
|
+
"TB_LOGO_HORIZONTAL_WHITE_FILE_ID": "file_...",
|
|
70
|
+
"TB_LOGO_ICON_FILE_ID": "file_...",
|
|
71
|
+
"TB_LOGO_ICON_WHITE_FILE_ID": "file_...",
|
|
72
|
+
"TB_LOGO_ICON_TAGLINE_FILE_ID": "file_...",
|
|
73
|
+
"TB_FONT_FILE_ID": "file_..."
|
|
81
74
|
}
|
|
82
75
|
}
|
|
83
76
|
```
|
|
@@ -94,12 +87,45 @@ grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
|
|
|
94
87
|
}
|
|
95
88
|
```
|
|
96
89
|
|
|
97
|
-
|
|
90
|
+
### Publishing a new version
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
```bash
|
|
93
|
+
npm version patch # or minor/major
|
|
94
|
+
npm publish # prepublishOnly rebuilds automatically; will prompt for a 2FA OTP
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Task logging
|
|
98
|
+
|
|
99
|
+
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:
|
|
100
|
+
|
|
101
|
+
| status | tool | meaning |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `started` | either | call received, with its key arguments (outline preview/length, or sessionId) |
|
|
104
|
+
| `attaching_assets` | `generate_tb_powerpoint` | which brand asset `resources` are being attached (`attached`, a list of mount paths) and which env vars are unset (`missing`) |
|
|
105
|
+
| `success` | `generate_tb_powerpoint` | session created; includes `sessionId` and `assetsAttached` count |
|
|
106
|
+
| `not_ready` | `get_tb_powerpoint_result` | session isn't `idle` yet; includes `sessionStatus` |
|
|
107
|
+
| `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` |
|
|
108
|
+
| `not_end_turn` | `get_tb_powerpoint_result` | idle, but `stop_reason` wasn't `end_turn`; includes `stopReason` |
|
|
109
|
+
| `listing_files` | `get_tb_powerpoint_result` | about to call Files API `list` for this session |
|
|
110
|
+
| `files_listed` | `get_tb_powerpoint_result` | includes `fileCount`, every `.pptx` candidate found (`pptxCandidates`, each with `id`/`filename`/`sizeBytes`), and which one was `chosenFileId` |
|
|
111
|
+
| `no_output_file` | `get_tb_powerpoint_result` | finished cleanly but no `.pptx` found; includes `fileCount` |
|
|
112
|
+
| `download_started` | `get_tb_powerpoint_result` | about to call Files API `download`; includes `fileId`, `filename`, `expectedSizeBytes` |
|
|
113
|
+
| `download_completed` | `get_tb_powerpoint_result` | bytes received; includes `downloadedBytes` |
|
|
114
|
+
| `saving_file` | `get_tb_powerpoint_result` | about to write to `destPath` inside `outputDir` |
|
|
115
|
+
| `file_saved` | `get_tb_powerpoint_result` | write confirmed via `fs.statSync`; includes `savedBytes` and `bytesMatch` (should be `true`) |
|
|
116
|
+
| `success` | `get_tb_powerpoint_result` | fully done; includes `filename`, `destPath`, `downloadUrl` |
|
|
117
|
+
| `invalid_arguments` | either | zod validation failed; includes `details` |
|
|
118
|
+
| `error` | either | anything else threw; includes `message` |
|
|
119
|
+
|
|
120
|
+
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.
|
|
121
|
+
|
|
122
|
+
All entries include `durationMs` except the intermediate progress lines (`attaching_assets`, `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:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
tail -f logs/tasks.log
|
|
126
|
+
# or, for just failures:
|
|
127
|
+
grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
|
|
128
|
+
```
|
|
103
129
|
|
|
104
130
|
## Development
|
|
105
131
|
|
|
@@ -108,8 +134,8 @@ grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
|
|
|
108
134
|
npm run dev
|
|
109
135
|
```
|
|
110
136
|
|
|
111
|
-
Both tools were smoke-tested end-to-end over the real MCP protocol (`tools/list`, and calling each with a dummy API key to confirm the SDK calls actually reach `api.anthropic.com` and get a real `401 authentication_error` back rather than failing on a wrong method/shape before the network call).
|
|
137
|
+
Both tools were smoke-tested end-to-end over the real MCP protocol (`tools/list`, and calling each with a dummy API key to confirm the SDK calls actually reach `api.anthropic.com` and get a real `401 authentication_error` back rather than failing on a wrong method/shape before the network call) — including with asset env vars set, to confirm the `resources` array builds correctly.
|
|
112
138
|
|
|
113
139
|
## Design brief reference
|
|
114
140
|
|
|
115
|
-
`TrafficBuilders-PPT-Handoff.md`, `logos/`, and `fonts/` in this repo are the source material
|
|
141
|
+
`TrafficBuilders-PPT-Handoff.md`, `logos/`, and `fonts/` in this repo are the source material — pulled from the "Traffic Builders Design System" Claude Design project (colors, type scale, the six slide archetypes, brand logo PNGs, Outfit variable font) — that `uploadAssets.ps1` uploads and every session attaches via `resources`. Update the files here first if the design system changes, then re-run `uploadAssets.ps1` and update the env vars with the new file_ids.
|
package/build/index.js
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
* Traffic Builders PowerPoint MCP Server
|
|
4
4
|
*
|
|
5
5
|
* Hands a presentation outline to Anthropic's hosted Managed Agent that
|
|
6
|
-
* builds Traffic Builders-branded PowerPoints
|
|
7
|
-
* brief are already provisioned in that agent's environment - this server
|
|
8
|
-
* does not upload any brand files itself), then later retrieves the
|
|
6
|
+
* builds Traffic Builders-branded PowerPoints, then later retrieves the
|
|
9
7
|
* finished .pptx once the agent's turn has ended.
|
|
10
8
|
*
|
|
9
|
+
* Brand assets (design brief, logos, font) are NOT assumed to already exist
|
|
10
|
+
* in the agent's environment - a "deployment" in the Managed Agents API is
|
|
11
|
+
* only a fixed/scheduled way to re-run the same baked-in message and offers
|
|
12
|
+
* no extra asset-targeting capability over a plain session, so instead this
|
|
13
|
+
* server attaches each asset to every session explicitly via the `resources`
|
|
14
|
+
* array (mounted through the Files API - see uploadAssets.ps1 for the
|
|
15
|
+
* one-time upload step that produces the file_ids below).
|
|
16
|
+
*
|
|
11
17
|
* Built against the documented Managed Agents API:
|
|
12
18
|
* https://platform.claude.com/docs/en/managed-agents/sessions
|
|
13
19
|
* https://platform.claude.com/docs/en/managed-agents/session-operations
|
|
@@ -42,6 +48,15 @@
|
|
|
42
48
|
* next to this package. Every entry is also mirrored to stderr, which the
|
|
43
49
|
* MCP host's own log viewer already picks up - the file exists so history
|
|
44
50
|
* survives a host restart and can be grepped/tailed directly.
|
|
51
|
+
* - TB_DESIGN_BRIEF_FILE_ID, TB_LOGO_HORIZONTAL_FILE_ID,
|
|
52
|
+
* TB_LOGO_HORIZONTAL_WHITE_FILE_ID, TB_LOGO_ICON_FILE_ID,
|
|
53
|
+
* TB_LOGO_ICON_WHITE_FILE_ID, TB_LOGO_ICON_TAGLINE_FILE_ID,
|
|
54
|
+
* TB_FONT_FILE_ID: all optional but strongly recommended. Files API
|
|
55
|
+
* file_ids (from uploadAssets.ps1) for the design brief, each logo
|
|
56
|
+
* variant, and the Outfit font. Any that are set get mounted into every
|
|
57
|
+
* session via `resources`; any left unset are just skipped (with a
|
|
58
|
+
* warning), so the agent falls back to approximating the brand from the
|
|
59
|
+
* prompt text alone.
|
|
45
60
|
*/
|
|
46
61
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
47
62
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -87,6 +102,26 @@ if (!OUTPUT_DIR)
|
|
|
87
102
|
console.error("Warning: TB_PPT_OUTPUT_DIR environment variable is not set (needed for get_tb_powerpoint_result)");
|
|
88
103
|
if (!PUBLIC_URL_BASE)
|
|
89
104
|
console.error("Warning: TB_PPT_PUBLIC_URL_BASE environment variable is not set (needed for get_tb_powerpoint_result)");
|
|
105
|
+
/**
|
|
106
|
+
* Brand assets mounted into every session via `resources`. `mountPath` is
|
|
107
|
+
* relative to the session's uploads root - see file header - so the actual
|
|
108
|
+
* sandbox path is `/mnt/session/uploads${mountPath}`. Any entry whose env
|
|
109
|
+
* var isn't set is skipped (not an error): the deck still gets built, just
|
|
110
|
+
* without that asset available to the agent.
|
|
111
|
+
*/
|
|
112
|
+
const ASSET_RESOURCES = [
|
|
113
|
+
{ envVar: "TB_DESIGN_BRIEF_FILE_ID", mountPath: "/design/TrafficBuilders-PPT-Handoff.md", label: "design brief" },
|
|
114
|
+
{ envVar: "TB_LOGO_HORIZONTAL_FILE_ID", mountPath: "/design/logos/logo-horizontal.png", label: "logo-horizontal.png (full-color, light backgrounds)" },
|
|
115
|
+
{ envVar: "TB_LOGO_HORIZONTAL_WHITE_FILE_ID", mountPath: "/design/logos/logo-horizontal-white.png", label: "logo-horizontal-white.png (reversed, brand-blue backgrounds)" },
|
|
116
|
+
{ envVar: "TB_LOGO_ICON_FILE_ID", mountPath: "/design/logos/logo-icon.png", label: "logo-icon.png (full-color square mark)" },
|
|
117
|
+
{ envVar: "TB_LOGO_ICON_WHITE_FILE_ID", mountPath: "/design/logos/logo-icon-white.png", label: "logo-icon-white.png (reversed square mark)" },
|
|
118
|
+
{ envVar: "TB_LOGO_ICON_TAGLINE_FILE_ID", mountPath: "/design/logos/logo-icon-tagline.png", label: "logo-icon-tagline.png (icon + tagline lockup)" },
|
|
119
|
+
{ envVar: "TB_FONT_FILE_ID", mountPath: "/design/fonts/Outfit-VariableFont_wght.ttf", label: "Outfit variable font (weights 300-900)" }
|
|
120
|
+
];
|
|
121
|
+
const missingAssetEnvVars = ASSET_RESOURCES.filter((a) => !(process.env[a.envVar] || "").trim());
|
|
122
|
+
if (missingAssetEnvVars.length > 0) {
|
|
123
|
+
console.error(`Warning: brand asset file_ids not configured, decks will be built without them: ${missingAssetEnvVars.map((a) => a.envVar).join(", ")}`);
|
|
124
|
+
}
|
|
90
125
|
function requireSessionConfig() {
|
|
91
126
|
if (!API_KEY)
|
|
92
127
|
throw new Error("ANTHROPIC_API_KEY is not configured on this MCP server.");
|
|
@@ -208,9 +243,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
208
243
|
});
|
|
209
244
|
const { apiKey, agentId, environmentId } = requireSessionConfig();
|
|
210
245
|
const client = getClient(apiKey);
|
|
246
|
+
const attachedAssets = ASSET_RESOURCES
|
|
247
|
+
.map((a) => ({ ...a, fileId: (process.env[a.envVar] || "").trim() }))
|
|
248
|
+
.filter((a) => a.fileId);
|
|
249
|
+
logTask({
|
|
250
|
+
tool: name,
|
|
251
|
+
status: "attaching_assets",
|
|
252
|
+
attached: attachedAssets.map((a) => a.mountPath),
|
|
253
|
+
missing: missingAssetEnvVars.map((a) => a.envVar)
|
|
254
|
+
});
|
|
211
255
|
const session = await client.beta.sessions.create({
|
|
212
256
|
agent: agentId,
|
|
213
|
-
environment_id: environmentId
|
|
257
|
+
environment_id: environmentId,
|
|
258
|
+
...(attachedAssets.length > 0
|
|
259
|
+
? {
|
|
260
|
+
resources: attachedAssets.map((a) => ({
|
|
261
|
+
type: "file",
|
|
262
|
+
file_id: a.fileId,
|
|
263
|
+
mount_path: a.mountPath
|
|
264
|
+
}))
|
|
265
|
+
}
|
|
266
|
+
: {})
|
|
214
267
|
});
|
|
215
268
|
const lines = [];
|
|
216
269
|
if (deckType)
|
|
@@ -220,7 +273,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
220
273
|
if (filename)
|
|
221
274
|
lines.push(`Save the resulting file as "${filename}.pptx".`);
|
|
222
275
|
const meta = lines.length ? `${lines.join("\n")}\n\n` : "";
|
|
223
|
-
const
|
|
276
|
+
const assetsNote = attachedAssets.length > 0
|
|
277
|
+
? `## Attached brand assets\nThese files are mounted in your sandbox:\n${attachedAssets
|
|
278
|
+
.map((a) => `- /mnt/session/uploads${a.mountPath} - ${a.label}`)
|
|
279
|
+
.join("\n")}\n\nRead the design brief first and follow it exactly: brand colors, type scale, and the six slide archetypes it defines. Place the correct logo variant per slide background (full-color logo on white/light backgrounds, the reversed white logo on brand-blue backgrounds). Embed/register the attached font file and use it for all text - ExtraBold (800) for headings, Regular (400) for body - instead of a fallback typeface.\n\n---\n\n`
|
|
280
|
+
: "";
|
|
281
|
+
const messageText = `Build a Traffic Builders PowerPoint from the following outline.\n\n${assetsNote}${meta}## Outline\n${outline}`;
|
|
224
282
|
await client.beta.sessions.events.send(session.id, {
|
|
225
283
|
events: [
|
|
226
284
|
{ type: "user.message", content: [{ type: "text", text: messageText }] }
|
|
@@ -230,6 +288,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
230
288
|
tool: name,
|
|
231
289
|
status: "success",
|
|
232
290
|
sessionId: session.id,
|
|
291
|
+
assetsAttached: attachedAssets.length,
|
|
233
292
|
durationMs: Date.now() - startedAt
|
|
234
293
|
});
|
|
235
294
|
return {
|
|
@@ -268,15 +327,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
268
327
|
}]
|
|
269
328
|
};
|
|
270
329
|
}
|
|
271
|
-
// Find the most recent session.status_idle event to read its stop_reason
|
|
330
|
+
// Find the most recent session.status_idle event to read its stop_reason, and
|
|
331
|
+
// the most recent agent.message event so we can log/return what the agent
|
|
332
|
+
// actually said (not just our own derived status).
|
|
272
333
|
let latestIdleEvent = null;
|
|
334
|
+
let latestAgentMessage = null;
|
|
273
335
|
for await (const event of client.beta.sessions.events.list(sessionId)) {
|
|
274
336
|
if (event.type === "session.status_idle") {
|
|
275
337
|
if (!latestIdleEvent || event.processed_at > latestIdleEvent.processed_at) {
|
|
276
338
|
latestIdleEvent = event;
|
|
277
339
|
}
|
|
278
340
|
}
|
|
341
|
+
else if (event.type === "agent.message") {
|
|
342
|
+
const text = event.content
|
|
343
|
+
.filter((block) => block.type === "text")
|
|
344
|
+
.map((block) => block.text)
|
|
345
|
+
.join("\n")
|
|
346
|
+
.trim();
|
|
347
|
+
if (text && (!latestAgentMessage || event.processed_at > latestAgentMessage.processed_at)) {
|
|
348
|
+
latestAgentMessage = { processed_at: event.processed_at, text };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
279
351
|
}
|
|
352
|
+
const agentMessagePreview = latestAgentMessage ? latestAgentMessage.text.slice(0, 500) : null;
|
|
353
|
+
logTask({
|
|
354
|
+
tool: name,
|
|
355
|
+
status: "agent_message",
|
|
356
|
+
sessionId,
|
|
357
|
+
agentMessagePreview
|
|
358
|
+
});
|
|
280
359
|
const stopReasonType = latestIdleEvent?.stop_reason.type;
|
|
281
360
|
if (stopReasonType !== "end_turn") {
|
|
282
361
|
logTask({
|
|
@@ -293,12 +372,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
293
372
|
ready: false,
|
|
294
373
|
status: session.status,
|
|
295
374
|
stopReason: stopReasonType ?? null,
|
|
375
|
+
agentMessage: agentMessagePreview,
|
|
296
376
|
message: `Session is idle but its stop reason is "${stopReasonType ?? "unknown"}", not "end_turn" - not fetching files yet.`
|
|
297
377
|
}, null, 2)
|
|
298
378
|
}]
|
|
299
379
|
};
|
|
300
380
|
}
|
|
301
381
|
// Session finished cleanly - look for a .pptx output file.
|
|
382
|
+
logTask({ tool: name, status: "listing_files", sessionId });
|
|
302
383
|
const files = await client.beta.files.list({
|
|
303
384
|
scope_id: sessionId,
|
|
304
385
|
betas: [MANAGED_AGENTS_BETA]
|
|
@@ -307,6 +388,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
307
388
|
const pptxFiles = fileList.filter((f) => /\.pptx$/i.test(f.filename));
|
|
308
389
|
const outputPptxFiles = pptxFiles.filter((f) => /outputs?[\\/]/i.test(f.filename));
|
|
309
390
|
const chosen = outputPptxFiles[0] ?? pptxFiles[0];
|
|
391
|
+
logTask({
|
|
392
|
+
tool: name,
|
|
393
|
+
status: "files_listed",
|
|
394
|
+
sessionId,
|
|
395
|
+
fileCount: fileList.length,
|
|
396
|
+
pptxCandidates: pptxFiles.map((f) => ({ id: f.id, filename: f.filename, sizeBytes: f.size_bytes })),
|
|
397
|
+
chosenFileId: chosen?.id ?? null
|
|
398
|
+
});
|
|
310
399
|
if (!chosen) {
|
|
311
400
|
logTask({
|
|
312
401
|
tool: name,
|
|
@@ -322,6 +411,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
322
411
|
ready: true,
|
|
323
412
|
status: "idle",
|
|
324
413
|
stopReason: "end_turn",
|
|
414
|
+
agentMessage: agentMessagePreview,
|
|
325
415
|
message: "Session finished but no .pptx output file was found.",
|
|
326
416
|
sessionFiles: fileList.map((f) => ({ id: f.id, filename: f.filename }))
|
|
327
417
|
}, null, 2)
|
|
@@ -331,20 +421,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
331
421
|
const { outputDir, publicUrlBase } = requireOutputConfig();
|
|
332
422
|
const safeName = path.basename(chosen.filename || `${sessionId}.pptx`);
|
|
333
423
|
const destPath = path.join(outputDir, safeName);
|
|
424
|
+
logTask({
|
|
425
|
+
tool: name,
|
|
426
|
+
status: "download_started",
|
|
427
|
+
sessionId,
|
|
428
|
+
fileId: chosen.id,
|
|
429
|
+
filename: safeName,
|
|
430
|
+
expectedSizeBytes: chosen.size_bytes
|
|
431
|
+
});
|
|
334
432
|
// files.download() resolves to a plain Response on this SDK version - no
|
|
335
433
|
// writeToFile() helper (see file header). Read the bytes and write them ourselves.
|
|
336
434
|
const response = await client.beta.files.download(chosen.id, {
|
|
337
435
|
betas: [MANAGED_AGENTS_BETA]
|
|
338
436
|
});
|
|
339
437
|
const arrayBuffer = await response.arrayBuffer();
|
|
438
|
+
const downloadedBytes = arrayBuffer.byteLength;
|
|
439
|
+
logTask({
|
|
440
|
+
tool: name,
|
|
441
|
+
status: "download_completed",
|
|
442
|
+
sessionId,
|
|
443
|
+
fileId: chosen.id,
|
|
444
|
+
downloadedBytes
|
|
445
|
+
});
|
|
446
|
+
logTask({ tool: name, status: "saving_file", sessionId, destPath, outputDir });
|
|
340
447
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
341
448
|
fs.writeFileSync(destPath, Buffer.from(arrayBuffer));
|
|
449
|
+
const savedBytes = fs.statSync(destPath).size;
|
|
450
|
+
logTask({
|
|
451
|
+
tool: name,
|
|
452
|
+
status: "file_saved",
|
|
453
|
+
sessionId,
|
|
454
|
+
destPath,
|
|
455
|
+
savedBytes,
|
|
456
|
+
bytesMatch: savedBytes === downloadedBytes
|
|
457
|
+
});
|
|
342
458
|
const downloadUrl = `${publicUrlBase.replace(/\/$/, "")}/${encodeURIComponent(safeName)}`;
|
|
343
459
|
logTask({
|
|
344
460
|
tool: name,
|
|
345
461
|
status: "success",
|
|
346
462
|
sessionId,
|
|
347
463
|
filename: safeName,
|
|
464
|
+
destPath,
|
|
348
465
|
downloadUrl,
|
|
349
466
|
durationMs: Date.now() - startedAt
|
|
350
467
|
});
|
|
@@ -355,6 +472,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
355
472
|
ready: true,
|
|
356
473
|
status: "idle",
|
|
357
474
|
stopReason: "end_turn",
|
|
475
|
+
agentMessage: agentMessagePreview,
|
|
358
476
|
filename: safeName,
|
|
359
477
|
savedTo: destPath,
|
|
360
478
|
downloadUrl
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboundai/tb-ppt-mgagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|