planpost-mcp 0.2.1 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to `planpost-mcp`. Versions follow [semver](https://semver.org).
4
+
5
+ ## 0.4.0 — 2026-11-09
6
+
7
+ ### Added
8
+ - **OAuth sign-in for the hosted server.** With `--http`, the server is now an
9
+ OAuth 2.1 resource server per the MCP authorization spec (revision
10
+ 2026-07-28): protected-resource metadata at
11
+ `/.well-known/oauth-protected-resource`, a `401` +
12
+ `WWW-Authenticate: Bearer resource_metadata="…"` challenge for
13
+ unauthenticated requests, access-token validation by introspection with an
14
+ audience check, and no token passthrough. Clients that support remote MCP
15
+ authorization (Claude, ChatGPT, Cursor, Gemini CLI, VS Code) can connect to
16
+ `https://mcp.planpo.st/mcp` with no key. Enabled by
17
+ `PLANPOST_MCP_PUBLIC_URL` + `PLANPOST_MCP_SERVICE_SECRET`; without them the
18
+ server behaves exactly as before.
19
+ - **Tool annotations** on all 49 tools: `title`, `readOnlyHint`,
20
+ `destructiveHint`, `idempotentHint`, `openWorldHint`.
21
+ - **Agent bundles** in the repository: `skills/planpost/SKILL.md`
22
+ (`npx skills add`), `gemini-extension.json` + `GEMINI.md` (Gemini CLI
23
+ extension), `.claude-plugin/` (Claude Code plugin and marketplace) and
24
+ `.cursor-plugin/` + `mcp.json` (Cursor plugin).
25
+ - Tests for hosted OAuth mode and for manifest consistency; `npm test` now
26
+ builds first.
27
+
28
+ ### Changed
29
+ - `server.json`: the hosted remote's `Authorization` header is optional
30
+ (omit it to sign in with OAuth).
31
+ - API-key authentication (`Authorization: Bearer pk_…` over HTTP,
32
+ `PLANPOST_API_KEY` over stdio) is unchanged.
33
+
34
+ ## 0.3.0 — 2026-11-08
35
+
36
+ - Listed on the official MCP Registry as `io.github.lukBB/planpost`
37
+ (`server.json`, `mcpName`).
38
+ - Hosted-first README: install snippets for Claude Code, Claude Desktop,
39
+ Cursor, Codex CLI and Gemini CLI.
40
+ - `retry_failed` and `get_delivery_report` tools; publish failures carry an
41
+ error class and a suggested fix.
42
+ - `npm test`: registry schema validation and a stdio boot test.
43
+
44
+ ## 0.2.x
45
+
46
+ - Streamable HTTP mode (`--http`) with per-request API keys, powering
47
+ `https://mcp.planpo.st/mcp`.
48
+ - Account groups, approvals, smart scheduling, carousels, video rendering,
49
+ content sources, brand voice, revenue overview.
package/README.md CHANGED
@@ -1,9 +1,42 @@
1
1
  # planpost-mcp
2
2
 
3
- MCP (Model Context Protocol) server for [PlanPost](https://planpo.st) — lets Claude and other AI agents schedule social media posts, create videos and carousels, manage content sources, and read analytics and revenue data by talking to your PlanPost account.
3
+ MCP (Model Context Protocol) server for [PlanPost](https://planpo.st), the agentic social scheduler with revenue attribution. It lets Claude, ChatGPT, Cursor, Gemini CLI and other AI agents schedule social media posts, create videos and carousels, manage content sources, and read analytics and revenue data by talking to your PlanPost account.
4
4
 
5
5
  ## Quick start
6
6
 
7
+ ### The short way: sign in
8
+
9
+ Add `https://mcp.planpo.st/mcp` to your agent as a remote MCP server (a
10
+ "connector" in Claude and ChatGPT) and leave the authentication fields empty.
11
+ The first time the agent uses it, a browser opens on planpo.st: log in, review
12
+ what the agent may do, and choose whether its posts wait for your approval.
13
+ No key to create or paste.
14
+
15
+ ```bash
16
+ claude mcp add --transport http planpost https://mcp.planpo.st/mcp
17
+ ```
18
+
19
+ ```json
20
+ { "mcpServers": { "planpost": { "url": "https://mcp.planpo.st/mcp" } } }
21
+ ```
22
+
23
+ Connected agents are listed under **Settings → API Keys → Connected apps**,
24
+ where you can remove one at any time. They use the same permissions and the
25
+ same monthly call allowance as API keys.
26
+
27
+ The repository also ships ready-made bundles: a Claude Code plugin
28
+ (`.claude-plugin/`), a Cursor plugin (`.cursor-plugin/`), a Gemini CLI
29
+ extension (`gemini-extension.json`) and an agent skill:
30
+
31
+ ```bash
32
+ npx skills add lukBB/planpost-mcp --skill planpost
33
+ gemini extensions install https://github.com/lukBB/planpost-mcp
34
+ claude plugin marketplace add lukBB/planpost-mcp && claude plugin install planpost@planpost-plugins
35
+ ```
36
+
37
+ The rest of this section is the API-key route: use it for clients without
38
+ OAuth support, for CI, or when you want a key limited to a few permissions.
39
+
7
40
  ### 1. Get an API key
8
41
 
9
42
  In the PlanPost dashboard go to **Settings → API Keys → Create Key** and select the permissions you need (see the table below). The key (`pk_live_…`) is shown **once** — copy it.
@@ -12,13 +45,36 @@ In the PlanPost dashboard go to **Settings → API Keys → Create Key** and sel
12
45
 
13
46
  ### 2. Add the server to your agent
14
47
 
15
- **Claude Code:**
48
+ Two ways to connect. **Hosted** is the default: point your client at
49
+ `https://mcp.planpo.st/mcp`, nothing to install, your key travels as a header.
50
+ **Local** runs the same server on your machine with `npx`, so the key never
51
+ leaves it. Both expose the same 49 tools.
52
+
53
+ Replace `pk_live_xxx` with your key throughout.
54
+
55
+ **Claude Code**
56
+
57
+ ```bash
58
+ claude mcp add --transport http planpost https://mcp.planpo.st/mcp \
59
+ --header "Authorization: Bearer pk_live_xxx"
60
+ ```
61
+
62
+ Local fallback:
16
63
 
17
64
  ```bash
18
65
  claude mcp add planpost -e PLANPOST_API_KEY=pk_live_xxx -- npx -y planpost-mcp
19
66
  ```
20
67
 
21
- **Claude Desktop** (`claude_desktop_config.json`):
68
+ Run `claude mcp list` afterwards to confirm it is connected.
69
+
70
+ **Claude Desktop**
71
+
72
+ Hosted: Settings → Connectors → Add custom connector. Name `PlanPost`, URL
73
+ `https://mcp.planpo.st/mcp`, then add the header `Authorization: Bearer pk_live_xxx`.
74
+
75
+ Local fallback, in `claude_desktop_config.json` (macOS:
76
+ `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`), then
77
+ restart the app:
22
78
 
23
79
  ```json
24
80
  {
@@ -32,11 +88,71 @@ claude mcp add planpost -e PLANPOST_API_KEY=pk_live_xxx -- npx -y planpost-mcp
32
88
  }
33
89
  ```
34
90
 
35
- **Any other MCP client:** run `npx -y planpost-mcp` over stdio with the `PLANPOST_API_KEY` environment variable set.
91
+ **Cursor** (`~/.cursor/mcp.json`, or `.cursor/mcp.json` per project)
92
+
93
+ ```json
94
+ {
95
+ "mcpServers": {
96
+ "planpost": {
97
+ "url": "https://mcp.planpo.st/mcp",
98
+ "headers": { "Authorization": "Bearer pk_live_xxx" }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Local fallback: replace the entry with
105
+ `{ "command": "npx", "args": ["-y", "planpost-mcp"], "env": { "PLANPOST_API_KEY": "pk_live_xxx" } }`.
106
+ A project-level `.cursor/mcp.json` is committed with your repo, so prefer the
107
+ hosted form and keep the key out of the file.
108
+
109
+ **OpenAI Codex CLI** (`~/.codex/config.toml`)
110
+
111
+ ```toml
112
+ [mcp_servers.planpost]
113
+ url = "https://mcp.planpo.st/mcp"
114
+ bearer_token_env_var = "PLANPOST_API_KEY"
115
+ ```
116
+
117
+ Export `PLANPOST_API_KEY` in your shell; Codex reads the token from the
118
+ environment, not the file. `codex mcp add` does the same thing interactively.
119
+
120
+ Local fallback:
36
121
 
37
- ### Remote (HTTP) mode
122
+ ```toml
123
+ [mcp_servers.planpost]
124
+ command = "npx"
125
+ args = ["-y", "planpost-mcp"]
126
+
127
+ [mcp_servers.planpost.env]
128
+ PLANPOST_API_KEY = "pk_live_xxx"
129
+ ```
38
130
 
39
- The server can also run as a shared, multi-tenant HTTP endpoint — no local install for users, each request authenticated by its own key:
131
+ **Gemini CLI** (`~/.gemini/settings.json`, or `.gemini/settings.json` per project)
132
+
133
+ ```json
134
+ {
135
+ "mcpServers": {
136
+ "planpost": {
137
+ "httpUrl": "https://mcp.planpo.st/mcp",
138
+ "headers": { "Authorization": "Bearer pk_live_xxx" },
139
+ "timeout": 10000
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ Gemini CLI uses `httpUrl` for streamable HTTP, not `url`; the wrong key is a
146
+ silent no-op. Local fallback: the same `command`/`args`/`env` entry as Cursor.
147
+
148
+ **Any other MCP client:** connect to `https://mcp.planpo.st/mcp` (streamable
149
+ HTTP, `Authorization: Bearer pk_live_xxx` on every request), or run
150
+ `npx -y planpost-mcp` over stdio with `PLANPOST_API_KEY` set. Per-client
151
+ walkthroughs with screenshots: [planpo.st/docs/agents](https://planpo.st/docs/agents).
152
+
153
+ ### Self-hosting the HTTP endpoint
154
+
155
+ `mcp.planpo.st` is this same package running with `--http`. To run your own:
40
156
 
41
157
  ```bash
42
158
  npx -y planpost-mcp --http --port 3020 # exposes POST /mcp (+ /health)
@@ -48,16 +164,27 @@ Behind a reverse proxy, bind to loopback so the port isn't reachable directly:
48
164
  HOST=127.0.0.1 npx -y planpost-mcp --http --port 3020
49
165
  ```
50
166
 
51
- Connect from any HTTP-capable MCP client, e.g.:
167
+ The transport is stateless (streamable HTTP); credentials are read from every
168
+ request and scoped to it, so one hosted instance serves many users safely. Put
169
+ it behind HTTPS in production. `--host` / `HOST` default to `0.0.0.0`, so
170
+ containers and LAN use work without extra flags.
52
171
 
53
- ```bash
54
- claude mcp add --transport http planpost https://mcp.yourhost.com/mcp \
55
- --header "Authorization: Bearer pk_live_xxx"
56
- ```
172
+ A self-hosted endpoint accepts API keys (`Authorization: Bearer pk_...`) out of
173
+ the box. OAuth sign-in is switched on by three variables, and needs a PlanPost
174
+ API that runs the authorization server (`/oauth/*`):
57
175
 
58
- The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...` header is required on every request and scoped per request, so one hosted instance serves many users safely. Put it behind HTTPS (reverse proxy) in production.
176
+ | Variable | Description |
177
+ |---|---|
178
+ | `PLANPOST_MCP_PUBLIC_URL` | Public URL of this endpoint, e.g. `https://mcp.example.com/mcp`. Tokens are issued for exactly this URL. |
179
+ | `PLANPOST_MCP_SERVICE_SECRET` | 32+ random characters shared with the API (`MCP_SERVICE_SECRET` there). |
180
+ | `PLANPOST_OAUTH_ISSUER` | Authorization server origin. Defaults to `PLANPOST_API_URL`. |
181
+ | `OPENAI_APPS_CHALLENGE` | Optional. Token served at `/.well-known/openai-apps-challenge` for ChatGPT directory domain verification. |
59
182
 
60
- `--host` / `HOST` default to `0.0.0.0`, so containers and LAN use work without extra flags.
183
+ With those set the server follows the MCP authorization spec: it serves
184
+ `/.well-known/oauth-protected-resource`, answers unauthenticated requests with
185
+ `401` and a `WWW-Authenticate: Bearer resource_metadata="…"` challenge,
186
+ validates access tokens by introspection, checks they were issued for this
187
+ endpoint, and never forwards them upstream.
61
188
 
62
189
  ### 3. Talk to it
63
190
 
@@ -73,11 +200,13 @@ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
73
200
 
74
201
  | Variable | Required | Description |
75
202
  |---|---|---|
76
- | `PLANPOST_API_KEY` | ✅ | API key from Settings → API Keys (`pk_live_…`) |
203
+ | `PLANPOST_API_KEY` | stdio only | API key from Settings → API Keys (`pk_live_…`). Not needed with `--http`, where each request brings its own credentials. |
77
204
  | `PLANPOST_API_URL` | — | API base URL (default `https://api.planpo.st`) |
78
205
 
79
206
  ## Tools
80
207
 
208
+ 49 tools, grouped below. The permission column is the scope a tool needs, whether it comes from an API key or from OAuth sign-in; a credential without the scope gets a clear error rather than a silent failure. Every tool carries MCP annotations (a title plus read-only, destructive and open-world hints) so clients know which calls to confirm.
209
+
81
210
  ### Posts & publishing
82
211
 
83
212
  | Tool | Required permission | Description |
@@ -88,6 +217,13 @@ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
88
217
  | `update_post` | `posts:write` | Update a draft/scheduled post |
89
218
  | `delete_post` | `posts:write` | Delete a post |
90
219
  | `publish_post` | `posts:write` | Publish immediately |
220
+ | `retry_failed` | `posts:write` | Re-queue the platforms that failed for a transient reason; never re-posts one that went out |
221
+ | `get_delivery_report` | `posts:read` | The morning check: recent failed or partly failed posts with each platform's error and fix, unhealthy connections, approvals running late |
222
+ | `preview_post` | `posts:read` | Show exactly what each platform will receive (text after overrides, attachments, warnings) |
223
+ | `bulk_create_posts` | `posts:write` | Create a whole campaign of posts from one manifest |
224
+ | `list_campaigns` | `posts:read` | Campaign tags in use, with post counts by status and platform |
225
+ | `delete_posts` | `posts:write` | Bulk-delete posts matching a filter (a filter is required) |
226
+ | `list_media` | `media:read` | Media already uploaded, so an asset can be reused instead of re-uploaded |
91
227
  | `upload_media` | `media:write` | Get a presigned URL to upload an image/video |
92
228
  | `repurpose_content` | `repurpose:write` | Save repurposed content as draft posts |
93
229
 
@@ -98,8 +234,31 @@ The transport is stateless (streamable HTTP); the `Authorization: Bearer pk_...`
98
234
  | `list_connections` | `connections:read` | Connected social accounts |
99
235
  | `list_account_groups` | `groups:read` | Named account groups (e.g. "Polish Pages") |
100
236
  | `get_account_group` | `groups:read` | Resolve a group by id/name/default → connection IDs |
237
+ | `create_account_group` | `groups:write` | Create a named group bundling connections |
238
+ | `update_account_group` | `groups:write` | Rename, recolour, set default, or replace a group's connections |
239
+ | `delete_account_group` | `groups:write` | Delete a group (the accounts stay connected) |
240
+ | `add_connections_to_group` | `groups:write` | Add connections to a group |
241
+ | `remove_connections_from_group` | `groups:write` | Remove connections from a group |
242
+ | `get_connection_link` | `connections:read` | Link the user opens in a browser to connect a new social account (OAuth needs a human) |
243
+
244
+ Connecting a new social account requires OAuth in a browser: `get_connection_link` hands the user the link, and the agent can target the account once it is connected.
245
+
246
+ ### Approvals
247
+
248
+ | Tool | Required permission | Description |
249
+ |---|---|---|
250
+ | `submit_for_approval` | `approvals:write` | Send a post to a human reviewer (required when the API key is approval-gated) |
251
+ | `list_pending_approvals` | `approvals:read` | Posts waiting for review, with outcomes |
252
+ | `get_approval_status` | `approvals:read` | Pending / approved / rejected / changes requested for one submission |
253
+
254
+ ### Scheduling
101
255
 
102
- Connecting a new social account requires OAuth in the dashboard — agents can target any account that's already connected.
256
+ | Tool | Required permission | Description |
257
+ |---|---|---|
258
+ | `get_best_times` | `schedule:read` | Best posting times per platform from the account's engagement history |
259
+ | `suggest_schedule` | `schedule:read` | Spread N posts across a date range at optimal times; returns concrete timestamps |
260
+ | `get_posting_schedule` | `schedule:read` | Weekly queue slots, global plus per-group overrides |
261
+ | `set_posting_schedule` | `schedule:write` | Replace the weekly queue slots for the global schedule or one group (Pro) |
103
262
 
104
263
  ### Video & carousels
105
264
 
@@ -113,6 +272,7 @@ Connecting a new social account requires OAuth in the dashboard — agents can t
113
272
  | `render_carousel` | `carousels:write` | Render slides into branded PNGs in the media library |
114
273
  | `get_carousel_render` | `carousels:read` | Poll a render; completed renders return post-ready media assets |
115
274
  | `list_brand_kits` | `brand-kits:read` | Saved brand kits (palette, fonts, logo, handle) for `render_carousel` |
275
+ | `generate_image` | `images:write` | Generate an image with AI for a post (1 AI credit); pass the URL to `create_post` |
116
276
 
117
277
  ### Content sources
118
278
 
@@ -133,6 +293,13 @@ Connecting a new social account requires OAuth in the dashboard — agents can t
133
293
 
134
294
  Revenue data is read-only over the API; connecting Stripe/RevenueCat/Shopify happens in the dashboard (Settings → Revenue).
135
295
 
296
+ ### Brand voice (Pro)
297
+
298
+ | Tool | Required permission | Description |
299
+ |---|---|---|
300
+ | `get_brand_voices` | `brand-voice:read` | Tone-of-voice profiles analysed from the user's content |
301
+ | `get_brand_voice_prompt` | `brand-voice:read` | Ready-to-use system prompt for a voice; apply it when drafting posts |
302
+
136
303
  ## A typical agent workflow
137
304
 
138
305
  1. `get_account_group` (`default: true`) → connection IDs
@@ -152,10 +319,55 @@ Or, for a branded carousel post end-to-end:
152
319
  ```bash
153
320
  npm install
154
321
  npm run build # tsc → dist/
322
+ npm test # builds, validates server.json and the plugin manifests, boots stdio, exercises hosted OAuth mode
155
323
  PLANPOST_API_KEY=pk_test_xxx node dist/index.js
156
324
  ```
157
325
 
158
- The server speaks MCP over stdio. It calls the PlanPost public API (`/api/public/v1`) with your key; nothing else is stored or sent anywhere.
326
+ The server speaks MCP over stdio, or streamable HTTP with `--http`. It calls the PlanPost public API (`/api/public/v1`) with your credentials; nothing else is stored or sent anywhere. Changes are listed in [CHANGELOG.md](./CHANGELOG.md).
327
+
328
+ ## Maintainers
329
+
330
+ Releasing a version means publishing twice: the code to npm, then the metadata
331
+ to the [official MCP Registry](https://registry.modelcontextprotocol.io), which
332
+ only stores `server.json` and checks that the npm package it points at exists
333
+ and carries the matching `mcpName`. Glama and other directories import from the
334
+ registry, so the second step is what makes a release discoverable.
335
+
336
+ 1. Bump `version` in `package.json` **and** in `server.json` (top level and
337
+ `packages[0].version`), `gemini-extension.json`, `.claude-plugin/plugin.json`
338
+ and `.cursor-plugin/plugin.json`, and add a `CHANGELOG.md` entry. `npm test`
339
+ fails if any of them disagree.
340
+ 2. Publish to npm first; the registry rejects a version npm does not have yet:
341
+
342
+ ```bash
343
+ npm publish --access public
344
+ ```
345
+
346
+ 3. Install the publisher CLI once (`brew install mcp-publisher`, or the tarball
347
+ from the registry's GitHub releases), then log in. The `io.github.lukBB/*`
348
+ namespace is proven by GitHub device-flow login as that account:
349
+
350
+ ```bash
351
+ mcp-publisher login github
352
+ ```
353
+
354
+ 4. Validate and publish from this directory:
355
+
356
+ ```bash
357
+ mcp-publisher validate
358
+ mcp-publisher publish
359
+ ```
360
+
361
+ 5. Confirm the listing:
362
+
363
+ ```bash
364
+ curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.lukBB/planpost"
365
+ ```
366
+
367
+ An expired token shows as "Invalid or expired Registry JWT token": run the login
368
+ again. `mcp-publisher status` deprecates or deletes a published version.
369
+ A tagged GitHub release is what the Gemini CLI extension gallery and the
370
+ plugin marketplaces pick up, so tag `vX.Y.Z` after publishing.
159
371
 
160
372
  ## License
161
373
 
@@ -0,0 +1,3 @@
1
+ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare const ANNOTATED_TOOLS: string[];
3
+ export declare function annotationsFor(name: string): ToolAnnotations;
@@ -0,0 +1,66 @@
1
+ const TOOLS = {
2
+ list_posts: ["List posts", "read"],
3
+ get_post: ["Get a post", "read"],
4
+ // Can publish immediately, and a published post cannot be recalled.
5
+ create_post: ["Create, schedule or publish a post", "destroy", "world"],
6
+ preview_post: ["Preview a post per platform", "read"],
7
+ bulk_create_posts: ["Create several posts", "destroy", "world"],
8
+ list_campaigns: ["List campaigns", "read"],
9
+ delete_posts: ["Delete several posts", "destroy"],
10
+ list_media: ["List media library", "read"],
11
+ update_post: ["Edit a post", "destroy", "world"],
12
+ delete_post: ["Delete a post", "destroy"],
13
+ retry_failed: ["Retry failed platforms", "destroy", "world"],
14
+ get_delivery_report: ["Get delivery report", "read"],
15
+ publish_post: ["Publish a post now", "destroy", "world"],
16
+ list_connections: ["List connected social accounts", "read"],
17
+ list_account_groups: ["List account groups", "read"],
18
+ get_account_group: ["Get an account group", "read"],
19
+ create_account_group: ["Create an account group", "write"],
20
+ update_account_group: ["Edit an account group", "destroy"],
21
+ delete_account_group: ["Delete an account group", "destroy"],
22
+ add_connections_to_group: ["Add accounts to a group", "write"],
23
+ remove_connections_from_group: ["Remove accounts from a group", "destroy"],
24
+ get_connection_link: ["Get a link to connect an account", "read"],
25
+ submit_for_approval: ["Submit a post for approval", "write"],
26
+ list_pending_approvals: ["List pending approvals", "read"],
27
+ get_approval_status: ["Get approval status", "read"],
28
+ get_analytics: ["Get post analytics", "read"],
29
+ upload_media: ["Get a media upload URL", "write"],
30
+ repurpose_content: ["Repurpose content with AI", "write"],
31
+ list_video_templates: ["List video templates", "read"],
32
+ create_video: ["Render a video", "write"],
33
+ get_video_status: ["Get video render status", "read"],
34
+ generate_carousel: ["Draft carousel slides", "write"],
35
+ list_carousel_styles: ["List carousel styles", "read"],
36
+ render_carousel: ["Render a carousel", "write"],
37
+ get_carousel_render: ["Get carousel render status", "read"],
38
+ list_brand_kits: ["List brand kits", "read"],
39
+ list_content_sources: ["List content sources", "read"],
40
+ add_content_source: ["Add a content source", "write", "world"],
41
+ delete_content_source: ["Delete a content source", "destroy"],
42
+ fetch_source_content: ["Sync a content source", "write", "world"],
43
+ list_imported_content: ["List imported content", "read"],
44
+ get_revenue_overview: ["Get revenue overview", "read"],
45
+ generate_image: ["Generate an image", "write"],
46
+ get_brand_voices: ["List brand voices", "read"],
47
+ get_brand_voice_prompt: ["Get a brand voice prompt", "read"],
48
+ get_best_times: ["Get best times to post", "read"],
49
+ suggest_schedule: ["Suggest a schedule", "read"],
50
+ get_posting_schedule: ["Get posting schedule", "read"],
51
+ set_posting_schedule: ["Replace posting schedule", "destroy"],
52
+ };
53
+ export const ANNOTATED_TOOLS = Object.keys(TOOLS);
54
+ export function annotationsFor(name) {
55
+ const entry = TOOLS[name];
56
+ if (!entry)
57
+ throw new Error(`Tool "${name}" has no entry in annotations.ts`);
58
+ const [title, kind, world] = entry;
59
+ return {
60
+ title,
61
+ readOnlyHint: kind === "read",
62
+ destructiveHint: kind === "destroy",
63
+ idempotentHint: kind === "read",
64
+ openWorldHint: world === "world",
65
+ };
66
+ }