@unboundai/tb-ppt-mgagent 0.6.0 → 0.7.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 +187 -186
- package/build/index.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,186 +1,187 @@
|
|
|
1
|
-
# Traffic Builders PowerPoint MCP Server
|
|
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.
|
|
4
|
-
|
|
5
|
-
Brand assets (the design brief, logos, Outfit
|
|
6
|
-
|
|
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.
|
|
8
|
-
|
|
9
|
-
## Available tools
|
|
10
|
-
|
|
11
|
-
- **`generate_tb_powerpoint(outline, deckType?, clientName?, filename?, attachments?)`** — creates a session against the configured agent + environment, attaches whichever brand asset `resources` are configured (see Setup), uploads any chat attachments to the Files API and mounts them too, sends the outline as the first message, returns `{ success, sessionId, message, attachmentsAttached, imageWarnings? }`. Does not wait for the deck to finish. `attachments` is an optional array (max 10) of `{ path? | url? | data? | fileId?, filename?, mimeType?, placement? }` — any file type (images to embed, PDFs/spreadsheets/documents as source material). `images` still works as a deprecated alias. See [Chat attachments](#chat-attachments).
|
|
12
|
-
|
|
13
|
-
- **`get_tb_powerpoint_result(sessionId)`** — checks on a generation started above:
|
|
14
|
-
1. Retrieves the session. If its `status` isn't `idle`, returns `{ ready: false, status }` — still running, try again later.
|
|
15
|
-
2. If idle, finds the most recent `session.status_idle` event and checks its `stop_reason.type`. Only proceeds if it's `end_turn` (a clean finish) — a different stop reason (e.g. a budget cap) also returns `{ ready: false, ... }` rather than treating the session as done.
|
|
16
|
-
3. Lists files scoped to the session, filters for a `.pptx` filename (preferring one whose path looks like it's under an outputs directory), downloads it via the Files API into `TB_PPT_OUTPUT_DIR` (the local folder synced to GCS outside this server), and returns a download link built from `TB_PPT_PUBLIC_URL_BASE`.
|
|
17
|
-
|
|
18
|
-
## Chat attachments
|
|
19
|
-
|
|
20
|
-
An MCP tool call cannot see chat attachments by itself, so the calling model passes a reference:
|
|
21
|
-
|
|
22
|
-
- **`path`** (preferred in Claude Desktop / Claude Code): the attachment is stored on disk and the model knows its absolute path. The server reads the file itself, so no bytes travel through the model.
|
|
23
|
-
- **`url`**: a directly fetchable URL (server-side `fetch`).
|
|
24
|
-
- **`data`**: base64 bytes, as a last resort (costs tokens).
|
|
25
|
-
- **`fileId`**: a file already in the Files API, mounted without re-uploading.
|
|
26
|
-
|
|
27
|
-
Each file is uploaded to the Anthropic Files API and mounted at `/mnt/session/uploads/user-attachments/<n>-<name>`; the first message tells the agent to insert images at the given `placement` and to use other files as source material. A source that fails (bad path, HTTP error) is skipped and reported in `imageWarnings` instead of failing the whole call.
|
|
28
|
-
|
|
29
|
-
Optional hardening: set `TB_PPT_ATTACHMENT_ROOTS` to one or more folders (separated by `;` on Windows, `:` elsewhere); `path` attachments outside them are refused. Unset means any readable local file is allowed.
|
|
30
|
-
|
|
31
|
-
### TypingMind (URL flow)
|
|
32
|
-
|
|
33
|
-
A plain MCP tool call (which is what `generate_tb_powerpoint` is) has no visibility into chat attachments at all — TypingMind only exposes `resources.userMessage.attachments` to a **JavaScript-implementation regular plugin** with the `read_user_message` permission, never to a native/remote MCP tool. So getting an attached image into a deck is a two-step chain:
|
|
34
|
-
|
|
35
|
-
1. **A small companion TypingMind plugin surfaces the attachment.** In TypingMind's Admin Panel → Plugins → Create New Plugin, set up a plugin like this (a "Regular Plugin", separate from this MCP server's registration):
|
|
36
|
-
- **Name:** `get_chat_attachments`
|
|
37
|
-
- **Description** (used for tool selection): "Returns the direct URL, MIME type, and filename of any file/image the user attached to their most recent message in this chat. Call this whenever the user references an attachment (e.g. 'insert this logo', 'use this screenshot') so its URL can be passed to another tool such as generate_tb_powerpoint's `images` parameter."
|
|
38
|
-
- **Parameters:** none
|
|
39
|
-
- **Permissions:** `read_user_message`
|
|
40
|
-
- **Implementation type:** JavaScript Code
|
|
41
|
-
```javascript
|
|
42
|
-
async function get_chat_attachments(params, userSettings, resources) {
|
|
43
|
-
const attachments = (resources && resources.userMessage && resources.userMessage.attachments) || [];
|
|
44
|
-
if (attachments.length === 0) {
|
|
45
|
-
return JSON.stringify({ attachments: [], message: "No attachments found on the user's most recent message." });
|
|
46
|
-
}
|
|
47
|
-
return JSON.stringify({
|
|
48
|
-
attachments: attachments.map(function (a) {
|
|
49
|
-
return { url: a.url, type: a.type, name: a.name };
|
|
50
|
-
})
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
- **Plugin Context** (recommended, injected into the system prompt): "When the user says they attached/added/pasted an image or file and references it (e.g. 'zet dit logo op de titelslide'), call get_chat_attachments first to get its URL, then pass that URL into generate_tb_powerpoint's `images` array along with a `placement` describing what the user asked for, in their own words."
|
|
55
|
-
|
|
56
|
-
2. **The model chains the two tools itself.** With both plugins active in the same chat, the flow the model follows is: user attaches an image and says where it goes → model calls `get_chat_attachments` → gets back `{ url, type, name }` → calls `generate_tb_powerpoint` with `images: [{ url, mimeType: type, filename: name, placement: "<what the user asked for>" }]`. This server then fetches that URL itself (a plain server-side fetch — no browser/CORS involved), uploads the bytes to the Files API exactly like the brand logos, and mounts it into the session at `/user-images/image-N.ext`, telling the agent in the message text where to insert it.
|
|
57
|
-
|
|
58
|
-
**One real unknown, worth testing before relying on this:** it isn't confirmed whether the `url` TypingMind's attachment object returns is a plain, publicly-fetchable link, or one scoped to TypingMind's own browser session (which this server's server-side `fetch()` couldn't reach). Test it once by attaching an image, calling `get_chat_attachments`, and trying to fetch the returned URL independently (e.g. `curl -I <url>`) from outside TypingMind. **If that fails**, the fallback is already built in: `images` entries also accept `data` (base64 image bytes) instead of `url` — change the plugin's JS code above to fetch the attachment's `url` itself (`fetch(a.url).then(r => r.arrayBuffer())`, browser-side, where TypingMind's own session auth applies) and return base64-encoded `data` instead of `url`; no MCP-server code change is needed either way.
|
|
59
|
-
|
|
60
|
-
## What's still an assumption
|
|
61
|
-
|
|
62
|
-
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.
|
|
63
|
-
|
|
64
|
-
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`.
|
|
65
|
-
|
|
66
|
-
`generate_tb_powerpoint`'s image-forwarding path (`client.beta.files.upload(...)`, via the SDK's `toFile()` helper) is checked against the SDK source the same way, and is exercised by the same `resources` mechanism already used for brand assets — but the TypingMind-side attachment flow that's meant to feed it a URL has not been run end-to-end yet; see [Sending an attached image from TypingMind](#sending-an-attached-image-from-typingmind) for the one open question there.
|
|
67
|
-
|
|
68
|
-
## Setup
|
|
69
|
-
|
|
70
|
-
1. **Install dependencies**:
|
|
71
|
-
```
|
|
72
|
-
npm install
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
2. **Build the project**:
|
|
76
|
-
```
|
|
77
|
-
npm run build
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
3. **Upload the brand assets** (one time, or again whenever an asset changes):
|
|
81
|
-
```
|
|
82
|
-
.\uploadAssets.ps1 -ApiKey "sk-ant-..."
|
|
83
|
-
```
|
|
84
|
-
Uploads `TrafficBuilders-PPT-Handoff.md`, all five `logos/*.png`, and `fonts
|
|
85
|
-
|
|
86
|
-
4. **Configure environment variables**:
|
|
87
|
-
- `ANTHROPIC_API_KEY` — Anthropic API key with Managed Agents access.
|
|
88
|
-
- `ANTHROPIC_AGENT_ID` — e.g. `agent_01H8osDSMeEtGcEHT1ajzuin`.
|
|
89
|
-
- `ANTHROPIC_ENVIRONMENT_ID` — e.g. `env_01VHzZmqBababsghFjQoAGFx`.
|
|
90
|
-
- `ANTHROPIC_API_BASE` — optional, overrides the SDK's default API base URL.
|
|
91
|
-
- `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.
|
|
92
|
-
- `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.
|
|
93
|
-
- `TB_PPT_LOG_FILE` — optional. Path to the task log file. Defaults to `logs/tasks.log` next to this package.
|
|
94
|
-
- `TB_PPT_ATTACHMENT_ROOTS` — optional. Folders that `path` attachments must live in.
|
|
95
|
-
- `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.
|
|
96
|
-
|
|
97
|
-
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:
|
|
98
|
-
|
|
99
|
-
```json
|
|
100
|
-
"tb-ppt-mgagent": {
|
|
101
|
-
"command": "npx",
|
|
102
|
-
"args": [
|
|
103
|
-
"-y",
|
|
104
|
-
"@unboundai/tb-ppt-mgagent"
|
|
105
|
-
],
|
|
106
|
-
"env": {
|
|
107
|
-
"ANTHROPIC_API_KEY": "sk-ant-...",
|
|
108
|
-
"ANTHROPIC_AGENT_ID": "agent_...",
|
|
109
|
-
"ANTHROPIC_ENVIRONMENT_ID": "env_...",
|
|
110
|
-
"TB_PPT_OUTPUT_DIR": "/path/to/gcs-synced-folder",
|
|
111
|
-
"TB_PPT_PUBLIC_URL_BASE": "https://storage.googleapis.com/your-bucket/your-prefix",
|
|
112
|
-
"TB_DESIGN_BRIEF_FILE_ID": "file_...",
|
|
113
|
-
"TB_LOGO_HORIZONTAL_FILE_ID": "file_...",
|
|
114
|
-
"TB_LOGO_HORIZONTAL_WHITE_FILE_ID": "file_...",
|
|
115
|
-
"TB_LOGO_ICON_FILE_ID": "file_...",
|
|
116
|
-
"TB_LOGO_ICON_WHITE_FILE_ID": "file_...",
|
|
117
|
-
"TB_LOGO_ICON_TAGLINE_FILE_ID": "file_...",
|
|
118
|
-
"TB_FONT_FILE_ID": "file_..."
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
npm
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1
|
+
# Traffic Builders PowerPoint MCP Server
|
|
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.
|
|
4
|
+
|
|
5
|
+
Brand assets (the design brief, logos, Outfit and Figtree fonts) 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.
|
|
6
|
+
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Available tools
|
|
10
|
+
|
|
11
|
+
- **`generate_tb_powerpoint(outline, deckType?, clientName?, filename?, attachments?)`** — creates a session against the configured agent + environment, attaches whichever brand asset `resources` are configured (see Setup), uploads any chat attachments to the Files API and mounts them too, sends the outline as the first message, returns `{ success, sessionId, message, attachmentsAttached, imageWarnings? }`. Does not wait for the deck to finish. `attachments` is an optional array (max 10) of `{ path? | url? | data? | fileId?, filename?, mimeType?, placement? }` — any file type (images to embed, PDFs/spreadsheets/documents as source material). `images` still works as a deprecated alias. See [Chat attachments](#chat-attachments).
|
|
12
|
+
|
|
13
|
+
- **`get_tb_powerpoint_result(sessionId)`** — checks on a generation started above:
|
|
14
|
+
1. Retrieves the session. If its `status` isn't `idle`, returns `{ ready: false, status }` — still running, try again later.
|
|
15
|
+
2. If idle, finds the most recent `session.status_idle` event and checks its `stop_reason.type`. Only proceeds if it's `end_turn` (a clean finish) — a different stop reason (e.g. a budget cap) also returns `{ ready: false, ... }` rather than treating the session as done.
|
|
16
|
+
3. Lists files scoped to the session, filters for a `.pptx` filename (preferring one whose path looks like it's under an outputs directory), downloads it via the Files API into `TB_PPT_OUTPUT_DIR` (the local folder synced to GCS outside this server), and returns a download link built from `TB_PPT_PUBLIC_URL_BASE`.
|
|
17
|
+
|
|
18
|
+
## Chat attachments
|
|
19
|
+
|
|
20
|
+
An MCP tool call cannot see chat attachments by itself, so the calling model passes a reference:
|
|
21
|
+
|
|
22
|
+
- **`path`** (preferred in Claude Desktop / Claude Code): the attachment is stored on disk and the model knows its absolute path. The server reads the file itself, so no bytes travel through the model.
|
|
23
|
+
- **`url`**: a directly fetchable URL (server-side `fetch`).
|
|
24
|
+
- **`data`**: base64 bytes, as a last resort (costs tokens).
|
|
25
|
+
- **`fileId`**: a file already in the Files API, mounted without re-uploading.
|
|
26
|
+
|
|
27
|
+
Each file is uploaded to the Anthropic Files API and mounted at `/mnt/session/uploads/user-attachments/<n>-<name>`; the first message tells the agent to insert images at the given `placement` and to use other files as source material. A source that fails (bad path, HTTP error) is skipped and reported in `imageWarnings` instead of failing the whole call.
|
|
28
|
+
|
|
29
|
+
Optional hardening: set `TB_PPT_ATTACHMENT_ROOTS` to one or more folders (separated by `;` on Windows, `:` elsewhere); `path` attachments outside them are refused. Unset means any readable local file is allowed.
|
|
30
|
+
|
|
31
|
+
### TypingMind (URL flow)
|
|
32
|
+
|
|
33
|
+
A plain MCP tool call (which is what `generate_tb_powerpoint` is) has no visibility into chat attachments at all — TypingMind only exposes `resources.userMessage.attachments` to a **JavaScript-implementation regular plugin** with the `read_user_message` permission, never to a native/remote MCP tool. So getting an attached image into a deck is a two-step chain:
|
|
34
|
+
|
|
35
|
+
1. **A small companion TypingMind plugin surfaces the attachment.** In TypingMind's Admin Panel → Plugins → Create New Plugin, set up a plugin like this (a "Regular Plugin", separate from this MCP server's registration):
|
|
36
|
+
- **Name:** `get_chat_attachments`
|
|
37
|
+
- **Description** (used for tool selection): "Returns the direct URL, MIME type, and filename of any file/image the user attached to their most recent message in this chat. Call this whenever the user references an attachment (e.g. 'insert this logo', 'use this screenshot') so its URL can be passed to another tool such as generate_tb_powerpoint's `images` parameter."
|
|
38
|
+
- **Parameters:** none
|
|
39
|
+
- **Permissions:** `read_user_message`
|
|
40
|
+
- **Implementation type:** JavaScript Code
|
|
41
|
+
```javascript
|
|
42
|
+
async function get_chat_attachments(params, userSettings, resources) {
|
|
43
|
+
const attachments = (resources && resources.userMessage && resources.userMessage.attachments) || [];
|
|
44
|
+
if (attachments.length === 0) {
|
|
45
|
+
return JSON.stringify({ attachments: [], message: "No attachments found on the user's most recent message." });
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
attachments: attachments.map(function (a) {
|
|
49
|
+
return { url: a.url, type: a.type, name: a.name };
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
- **Plugin Context** (recommended, injected into the system prompt): "When the user says they attached/added/pasted an image or file and references it (e.g. 'zet dit logo op de titelslide'), call get_chat_attachments first to get its URL, then pass that URL into generate_tb_powerpoint's `images` array along with a `placement` describing what the user asked for, in their own words."
|
|
55
|
+
|
|
56
|
+
2. **The model chains the two tools itself.** With both plugins active in the same chat, the flow the model follows is: user attaches an image and says where it goes → model calls `get_chat_attachments` → gets back `{ url, type, name }` → calls `generate_tb_powerpoint` with `images: [{ url, mimeType: type, filename: name, placement: "<what the user asked for>" }]`. This server then fetches that URL itself (a plain server-side fetch — no browser/CORS involved), uploads the bytes to the Files API exactly like the brand logos, and mounts it into the session at `/user-images/image-N.ext`, telling the agent in the message text where to insert it.
|
|
57
|
+
|
|
58
|
+
**One real unknown, worth testing before relying on this:** it isn't confirmed whether the `url` TypingMind's attachment object returns is a plain, publicly-fetchable link, or one scoped to TypingMind's own browser session (which this server's server-side `fetch()` couldn't reach). Test it once by attaching an image, calling `get_chat_attachments`, and trying to fetch the returned URL independently (e.g. `curl -I <url>`) from outside TypingMind. **If that fails**, the fallback is already built in: `images` entries also accept `data` (base64 image bytes) instead of `url` — change the plugin's JS code above to fetch the attachment's `url` itself (`fetch(a.url).then(r => r.arrayBuffer())`, browser-side, where TypingMind's own session auth applies) and return base64-encoded `data` instead of `url`; no MCP-server code change is needed either way.
|
|
59
|
+
|
|
60
|
+
## What's still an assumption
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
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`.
|
|
65
|
+
|
|
66
|
+
`generate_tb_powerpoint`'s image-forwarding path (`client.beta.files.upload(...)`, via the SDK's `toFile()` helper) is checked against the SDK source the same way, and is exercised by the same `resources` mechanism already used for brand assets — but the TypingMind-side attachment flow that's meant to feed it a URL has not been run end-to-end yet; see [Sending an attached image from TypingMind](#sending-an-attached-image-from-typingmind) for the one open question there.
|
|
67
|
+
|
|
68
|
+
## Setup
|
|
69
|
+
|
|
70
|
+
1. **Install dependencies**:
|
|
71
|
+
```
|
|
72
|
+
npm install
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. **Build the project**:
|
|
76
|
+
```
|
|
77
|
+
npm run build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
3. **Upload the brand assets** (one time, or again whenever an asset changes):
|
|
81
|
+
```
|
|
82
|
+
.\uploadAssets.ps1 -ApiKey "sk-ant-..."
|
|
83
|
+
```
|
|
84
|
+
Uploads `TrafficBuilders-PPT-Handoff.md`, all five `logos/*.png`, and both `fonts/*-VariableFont_wght.ttf` (Outfit, Figtree) 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.
|
|
85
|
+
|
|
86
|
+
4. **Configure environment variables**:
|
|
87
|
+
- `ANTHROPIC_API_KEY` — Anthropic API key with Managed Agents access.
|
|
88
|
+
- `ANTHROPIC_AGENT_ID` — e.g. `agent_01H8osDSMeEtGcEHT1ajzuin`.
|
|
89
|
+
- `ANTHROPIC_ENVIRONMENT_ID` — e.g. `env_01VHzZmqBababsghFjQoAGFx`.
|
|
90
|
+
- `ANTHROPIC_API_BASE` — optional, overrides the SDK's default API base URL.
|
|
91
|
+
- `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.
|
|
92
|
+
- `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.
|
|
93
|
+
- `TB_PPT_LOG_FILE` — optional. Path to the task log file. Defaults to `logs/tasks.log` next to this package.
|
|
94
|
+
- `TB_PPT_ATTACHMENT_ROOTS` — optional. Folders that `path` attachments must live in.
|
|
95
|
+
- `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`, `TB_FONT_BODY_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.
|
|
96
|
+
|
|
97
|
+
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:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
"tb-ppt-mgagent": {
|
|
101
|
+
"command": "npx",
|
|
102
|
+
"args": [
|
|
103
|
+
"-y",
|
|
104
|
+
"@unboundai/tb-ppt-mgagent"
|
|
105
|
+
],
|
|
106
|
+
"env": {
|
|
107
|
+
"ANTHROPIC_API_KEY": "sk-ant-...",
|
|
108
|
+
"ANTHROPIC_AGENT_ID": "agent_...",
|
|
109
|
+
"ANTHROPIC_ENVIRONMENT_ID": "env_...",
|
|
110
|
+
"TB_PPT_OUTPUT_DIR": "/path/to/gcs-synced-folder",
|
|
111
|
+
"TB_PPT_PUBLIC_URL_BASE": "https://storage.googleapis.com/your-bucket/your-prefix",
|
|
112
|
+
"TB_DESIGN_BRIEF_FILE_ID": "file_...",
|
|
113
|
+
"TB_LOGO_HORIZONTAL_FILE_ID": "file_...",
|
|
114
|
+
"TB_LOGO_HORIZONTAL_WHITE_FILE_ID": "file_...",
|
|
115
|
+
"TB_LOGO_ICON_FILE_ID": "file_...",
|
|
116
|
+
"TB_LOGO_ICON_WHITE_FILE_ID": "file_...",
|
|
117
|
+
"TB_LOGO_ICON_TAGLINE_FILE_ID": "file_...",
|
|
118
|
+
"TB_FONT_FILE_ID": "file_...",
|
|
119
|
+
"TB_FONT_BODY_FILE_ID": "file_..."
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Or, from a local checkout of this repo:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
"tb-ppt-mgagent": {
|
|
128
|
+
"command": "node",
|
|
129
|
+
"args": [
|
|
130
|
+
"/path/to/tb-ppt-mgAgent/build/index.js"
|
|
131
|
+
],
|
|
132
|
+
"env": { "...": "same as above" }
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Publishing a new version
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm version patch # or minor/major
|
|
140
|
+
npm publish # prepublishOnly rebuilds automatically; will prompt for a 2FA OTP
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Task logging
|
|
144
|
+
|
|
145
|
+
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:
|
|
146
|
+
|
|
147
|
+
| status | tool | meaning |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `started` | either | call received, with its key arguments (outline preview/length, or sessionId) |
|
|
150
|
+
| `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`) |
|
|
151
|
+
| `success` | `generate_tb_powerpoint` | session created; includes `sessionId` and `assetsAttached` count |
|
|
152
|
+
| `not_ready` | `get_tb_powerpoint_result` | session isn't `idle` yet; includes `sessionStatus` |
|
|
153
|
+
| `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` |
|
|
154
|
+
| `not_end_turn` | `get_tb_powerpoint_result` | idle, but `stop_reason` wasn't `end_turn`; includes `stopReason` |
|
|
155
|
+
| `listing_files` | `get_tb_powerpoint_result` | about to call Files API `list` for this session |
|
|
156
|
+
| `files_listed` | `get_tb_powerpoint_result` | includes `fileCount`, every `.pptx` candidate found (`pptxCandidates`, each with `id`/`filename`/`sizeBytes`), and which one was `chosenFileId` |
|
|
157
|
+
| `no_output_file` | `get_tb_powerpoint_result` | finished cleanly but no `.pptx` found; includes `fileCount` |
|
|
158
|
+
| `download_started` | `get_tb_powerpoint_result` | about to call Files API `download`; includes `fileId`, `filename`, `expectedSizeBytes` |
|
|
159
|
+
| `download_completed` | `get_tb_powerpoint_result` | bytes received; includes `downloadedBytes` |
|
|
160
|
+
| `saving_file` | `get_tb_powerpoint_result` | about to write to `destPath` inside `outputDir` |
|
|
161
|
+
| `file_saved` | `get_tb_powerpoint_result` | write confirmed via `fs.statSync`; includes `savedBytes` and `bytesMatch` (should be `true`) |
|
|
162
|
+
| `success` | `get_tb_powerpoint_result` | fully done; includes `filename`, `destPath`, `downloadUrl` |
|
|
163
|
+
| `invalid_arguments` | either | zod validation failed; includes `details` |
|
|
164
|
+
| `error` | either | anything else threw; includes `message` |
|
|
165
|
+
|
|
166
|
+
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.
|
|
167
|
+
|
|
168
|
+
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:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
tail -f logs/tasks.log
|
|
172
|
+
# or, for just failures:
|
|
173
|
+
grep -E '"status":"(error|not_end_turn|no_output_file)"' logs/tasks.log
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Watch mode
|
|
180
|
+
npm run dev
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
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.
|
|
184
|
+
|
|
185
|
+
## Design brief reference
|
|
186
|
+
|
|
187
|
+
`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 21 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. No slide-image assets are attached separately — the handoff brief embeds a literal HTML/CSS reference snippet per archetype, which is enough on its own (see its closing section for when a visual PNG reference would be worth adding).
|
package/build/index.js
CHANGED
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
* - TB_DESIGN_BRIEF_FILE_ID, TB_LOGO_HORIZONTAL_FILE_ID,
|
|
52
52
|
* TB_LOGO_HORIZONTAL_WHITE_FILE_ID, TB_LOGO_ICON_FILE_ID,
|
|
53
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
|
|
54
|
+
* TB_FONT_FILE_ID (Outfit), TB_FONT_BODY_FILE_ID (Figtree): all optional but strongly recommended. Files API
|
|
55
55
|
* file_ids (from uploadAssets.ps1) for the design brief, each logo
|
|
56
|
-
* variant, and the Outfit
|
|
56
|
+
* variant, and the Outfit and Figtree fonts. Any that are set get mounted into every
|
|
57
57
|
* session via `resources`; any left unset are just skipped (with a
|
|
58
58
|
* warning), so the agent falls back to approximating the brand from the
|
|
59
59
|
* prompt text alone.
|
|
@@ -129,7 +129,8 @@ const ASSET_RESOURCES = [
|
|
|
129
129
|
{ envVar: "TB_LOGO_ICON_FILE_ID", mountPath: "/design/logos/logo-icon.png", label: "logo-icon.png (full-color square mark)" },
|
|
130
130
|
{ envVar: "TB_LOGO_ICON_WHITE_FILE_ID", mountPath: "/design/logos/logo-icon-white.png", label: "logo-icon-white.png (reversed square mark)" },
|
|
131
131
|
{ envVar: "TB_LOGO_ICON_TAGLINE_FILE_ID", mountPath: "/design/logos/logo-icon-tagline.png", label: "logo-icon-tagline.png (icon + tagline lockup)" },
|
|
132
|
-
{ envVar: "TB_FONT_FILE_ID", mountPath: "/design/fonts/Outfit-VariableFont_wght.ttf", label: "Outfit variable font (weights 300-900)" }
|
|
132
|
+
{ envVar: "TB_FONT_FILE_ID", mountPath: "/design/fonts/Outfit-VariableFont_wght.ttf", label: "Outfit variable font (weights 300-900) - headings, numbers, eyebrows" },
|
|
133
|
+
{ envVar: "TB_FONT_BODY_FILE_ID", mountPath: "/design/fonts/Figtree-VariableFont_wght.ttf", label: "Figtree variable font (weights 300-900) - body copy and labels" }
|
|
133
134
|
];
|
|
134
135
|
const missingAssetEnvVars = ASSET_RESOURCES.filter((a) => !(process.env[a.envVar] || "").trim());
|
|
135
136
|
if (missingAssetEnvVars.length > 0) {
|
|
@@ -486,7 +487,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
486
487
|
const assetsNote = attachedAssets.length > 0
|
|
487
488
|
? `## Attached brand assets\nThese files are mounted in your sandbox:\n${attachedAssets
|
|
488
489
|
.map((a) => `- /mnt/session/uploads${a.mountPath} - ${a.label}`)
|
|
489
|
-
.join("\n")}\n\nRead the design brief first and follow it exactly: brand colors, type scale, and the
|
|
490
|
+
.join("\n")}\n\nRead the design brief first and follow it exactly: brand colors, type scale, and the 21 slide archetypes it defines. Choose each slide's archetype by the shape of its content using the brief's section 7 table, and obey its variation rules (card-block slides are the exception, never the default; never two in a row). Follow its logo lockup rule precisely - it is not a simple per-background swap: the wordmark logo appears ONLY on Title and Section-divider slides (top-left), while a small icon mark appears bottom-right on EVERY slide regardless of archetype; use the white logo variants on brand-blue backgrounds and the full-color variants on white backgrounds. Embed/register BOTH attached font files: Outfit (ExtraBold 800) for headings, big numbers, eyebrows and tags; Figtree (Regular 400, SemiBold 600 for table headers) for body copy, card copy and labels - instead of a fallback typeface.\n\n---\n\n`
|
|
490
491
|
: "";
|
|
491
492
|
const userImagesNote = uploadedImages.length > 0
|
|
492
493
|
? `## User-provided attachments\nThese files from the user are mounted in your sandbox. Images MUST be inserted into the deck (not just described) at the placement given; other files (PDF, spreadsheet, document) are source material to read and use for the content as described:\n${uploadedImages
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unboundai/tb-ppt-mgagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|