From 764881045b2eb4abe8c6b6838c26c7cb0bdb29c5 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 15 Oct 2026 17:19:04 -0300 Subject: [PATCH 1/4] Build the OpenAI plugin ZIP with a script into build/ node scripts/build.ts writes build/trypost-plugin.zip from HEAD, and the list of packed paths lives only in the script, so the README, the tests and CI no longer repeat the git archive command. build/ is git-ignored. The README now says the demo video URL goes in plugin.json, which is the only place the OpenAI dashboard reads it from. --- .github/workflows/tests.yml | 16 +++++----- .gitignore | 1 + AGENTS.md | 4 +-- README.md | 6 ++-- scripts/build.ts | 23 +++++++++++++++ tests/build.test.ts | 59 +++++++++++++++++++++++++++++++++++++ tests/repository.test.ts | 21 +------------ tsconfig.json | 9 ++++-- 8 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 scripts/build.ts create mode 100644 tests/build.test.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 73a305a..5c4b252 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,17 +65,17 @@ jobs: steps: - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - name: Build the plugin ZIP - run: git archive --format=zip --prefix=trypost/ -o trypost-plugin.zip HEAD .codex-plugin .mcp.json skills assets + run: node scripts/build.ts - - name: Check the ZIP layout - run: | - unzip -l trypost-plugin.zip - for file in trypost/.codex-plugin/plugin.json trypost/.mcp.json trypost/skills/trypost/SKILL.md trypost/assets/icon-512.png; do - unzip -l trypost-plugin.zip "$file" > /dev/null || { echo "::error::$file is missing from the ZIP"; exit 1; } - done + - name: List the ZIP + run: unzip -l build/trypost-plugin.zip - uses: actions/upload-artifact@v7 with: name: trypost-plugin - path: trypost-plugin.zip + path: build/trypost-plugin.zip diff --git a/.gitignore b/.gitignore index f4aeeab..0fc617f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dist/ coverage/ *.tmp *.temp +build/ diff --git a/AGENTS.md b/AGENTS.md index abd73c0..0eb5f28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ This repository packages TryPost as a plugin for Claude Code, Cursor, Grok, Chat | Manifest and skill tests | `node --test` | | TypeScript type check | `tsc -p .` | | Claude plugin validation | `claude plugin validate --strict` on the plugin, its marketplace and its skills, with a pinned Claude Code version | -| OpenAI plugin package | Builds the upload ZIP, checks its layout and attaches it to the run | +| OpenAI plugin package | Builds the upload ZIP with `node scripts/build.ts` and attaches it to the run | - Pin every tool and action: exact versions for npm packages, a major tag for GitHub actions. Pick versions released at least two weeks earlier. - `.github/workflows/sync-skill.yml` copies the root `SKILL.md` into `skills/trypost/SKILL.md` on each release. Still keep both files identical in every commit; a test compares them. @@ -63,7 +63,7 @@ This repository packages TryPost as a plugin for Claude Code, Cursor, Grok, Chat - Review needs exactly five positive and three negative test cases. Positive cases name tools the skill documents. - Never mention prices, plans, trials, discounts or upgrades in the listing or the skill. - Never put reviewer credentials, the demo video, client secrets or the domain verification token in the package; they go in the OpenAI dashboard. The verification token is served by the TryPost app at `https://app.trypost.it/.well-known/openai-apps-challenge`. -- Build the upload with the command in the README; a test checks that it packs everything the manifest references. +- Build the upload with `node scripts/build.ts`: it writes `build/trypost-plugin.zip` (git-ignored) from `HEAD`. The list of packed paths lives only in `scripts/build.ts` (`pluginPaths`); `tests/build.test.ts` checks it covers everything the manifest references. Never commit the ZIP. ## Skill (`SKILL.md`) diff --git a/README.md b/README.md index ddcfd90..66de72b 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ This repo ships an OpenAI plugin manifest at `.codex-plugin/plugin.json`, with t Build the ZIP to upload at [platform.openai.com/plugins](https://platform.openai.com/plugins): ```bash -git archive --format=zip --prefix=trypost/ -o trypost-plugin.zip HEAD .codex-plugin .mcp.json skills assets +node scripts/build.ts ``` -Reviewer credentials, the demo video URL and the domain verification token are entered in the dashboard, never in the package. +It writes `build/trypost-plugin.zip` (git-ignored) from the last commit, so commit your changes before building. The demo video URL goes in `.codex-plugin/plugin.json` (`extensions.com.openai.review.demo_recording_url`); reviewer credentials and the domain verification token are entered in the dashboard, never in the package. ### Cursor plugin @@ -138,6 +138,7 @@ skills/trypost/ Agent skill (SKILL.md) mcp.json Shared MCP server definition assets/ Logo and icons SKILL.md Same skill, repo root (npx skills add / OpenClaw) +scripts/build.ts Builds the OpenAI plugin ZIP into build/ tests/ Manifest, MCP, skill and packaging tests (TypeScript, node --test) tsconfig.json TypeScript settings for the tests AGENTS.md Rules for contributors and coding agents @@ -151,6 +152,7 @@ Everything is TypeScript, run directly by Node 24+ (no build step, no `package.j ```bash node --test # run the tests +node scripts/build.ts # build the OpenAI plugin ZIP into build/ npm install --no-save --no-package-lock typescript@7.0.2 @types/node@24.19.0 npx tsc -p . # type-check diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..e2e842c --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,23 @@ +import { execFileSync } from 'node:child_process'; +import { mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +export const root = join(import.meta.dirname, '..'); + +export const pluginPaths = ['.codex-plugin', '.mcp.json', 'skills', 'assets']; + +export const outputFile = 'build/trypost-plugin.zip'; + +export const buildPluginZip = (): string => { + mkdirSync(join(root, 'build'), { recursive: true }); + + execFileSync('git', ['archive', '--format=zip', '--prefix=trypost/', '-o', outputFile, 'HEAD', ...pluginPaths], { + cwd: root, + }); + + return join(root, outputFile); +}; + +if (import.meta.main) { + console.log(buildPluginZip()); +} diff --git a/tests/build.test.ts b/tests/build.test.ts new file mode 100644 index 0000000..6017ab4 --- /dev/null +++ b/tests/build.test.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { describe, test } from 'node:test'; + +import { buildPluginZip, outputFile, pluginPaths, root } from '../scripts/build.ts'; +import { exists, read, readJson } from './helpers.ts'; +import type { PluginManifest } from './types.ts'; + +const zipEntries = (file: string): string[] => + execFileSync('unzip', ['-Z1', file], { encoding: 'utf8' }).split('\n').filter(Boolean); + +describe('OpenAI plugin build', () => { + test('packs only paths that exist', () => { + assert.ok(pluginPaths.every(exists), `a packed path does not exist: ${pluginPaths.join(', ')}`); + }); + + test('packs everything the OpenAI manifest references', () => { + const manifest = readJson('.codex-plugin/plugin.json'); + const needed = ['.codex-plugin', '.mcp.json', 'skills', manifest.interface?.logo, manifest.interface?.composerIcon]; + + for (const entry of needed) { + const file = entry?.replace(/^\.\//, '') ?? ''; + + assert.ok( + pluginPaths.some((path) => file === path || file.startsWith(`${path}/`)), + `${file} is not packed`, + ); + } + }); + + test('writes a ZIP into build/ with the plugin under one trypost/ folder', () => { + const file = buildPluginZip(); + const entries = zipEntries(file); + + assert.equal(file, `${root}/${outputFile}`); + + for (const entry of ['.codex-plugin/plugin.json', '.mcp.json', 'skills/trypost/SKILL.md', 'assets/icon-512.png']) { + assert.ok(entries.includes(`trypost/${entry}`), `trypost/${entry} is missing from the ZIP`); + } + + assert.deepEqual( + entries.filter((entry) => !entry.startsWith('trypost/')), + [], + ); + assert.deepEqual( + entries.filter((entry) => /^trypost\/(tests|scripts|\.github|README\.md|AGENTS\.md)/.test(entry)), + [], + ); + }); + + test('keeps the build output out of git', () => { + assert.doesNotThrow(() => execFileSync('git', ['check-ignore', '--quiet', outputFile], { cwd: root })); + }); + + test('the README builds the ZIP with the script', () => { + assert.ok(read('README.md').includes('node scripts/build.ts')); + assert.ok(!read('README.md').includes('git archive')); + }); +}); diff --git a/tests/repository.test.ts b/tests/repository.test.ts index b14027c..bd9ccdb 100644 --- a/tests/repository.test.ts +++ b/tests/repository.test.ts @@ -2,8 +2,7 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { describe, test } from 'node:test'; -import { exists, networkCount, read, readJson, root, sizeOf } from './helpers.ts'; -import type { PluginManifest } from './types.ts'; +import { exists, networkCount, read, root, sizeOf } from './helpers.ts'; const git = (...args: string[]): string[] => execFileSync('git', args, { cwd: root, encoding: 'utf8' }).split('\n').filter(Boolean); @@ -21,24 +20,6 @@ describe('README', () => { test('never counts networks', () => { assert.doesNotMatch(readme, networkCount); }); - - test('the ZIP command packs everything the OpenAI manifest needs', () => { - const [, paths = ''] = readme.match(/git archive .+ HEAD (.+)/) ?? []; - const packed = paths.split(' '); - const manifest = readJson('.codex-plugin/plugin.json'); - const needed = ['.codex-plugin', '.mcp.json', 'skills', manifest.interface?.logo, manifest.interface?.composerIcon]; - - assert.ok(packed.every(exists), `the README packs a path that does not exist: ${packed.join(', ')}`); - - for (const entry of needed) { - const file = entry?.replace(/^\.\//, '') ?? ''; - - assert.ok( - packed.some((path) => file === path || file.startsWith(`${path}/`)), - `${file} is not packed`, - ); - } - }); }); describe('repository', () => { diff --git a/tsconfig.json b/tsconfig.json index 6cfe5ed..e1804f8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,9 @@ "target": "esnext", "module": "esnext", "moduleResolution": "bundler", - "types": ["node"], + "types": [ + "node" + ], "strict": true, "noEmit": true, "allowImportingTsExtensions": true, @@ -12,5 +14,8 @@ "noUncheckedIndexedAccess": true, "skipLibCheck": true }, - "include": ["tests/**/*.ts"] + "include": [ + "scripts/**/*.ts", + "tests/**/*.ts" + ] } From 3f6d3d1058260ea59e2a31410fe0d5b9cdb31711 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 15 Oct 2026 20:11:18 -0300 Subject: [PATCH 2/4] Version the plugins 2.0.1 ChatGPT's sign-in prompt moved to the TryPost server (trypost#431); a new package version asks the OpenAI directory to scan the server again. --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .cursor-plugin/marketplace.json | 2 +- .cursor-plugin/plugin.json | 2 +- .grok-plugin/plugin.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8ed82a5..c933803 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "TryPost plugin for planning, scheduling and publishing social media posts", - "version": "2.0.0" + "version": "2.0.1" }, "plugins": [ { diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 63f9284..8025658 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "trypost", - "version": "2.0.0", + "version": "2.0.1", "description": "Plan, schedule and publish social media posts, manage your queue and approvals, and read post analytics in TryPost.", "author": { "name": "Paulo Castellano", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 643d89c..a5d0e3d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "trypost", - "version": "2.0.0", + "version": "2.0.1", "description": "Plan, schedule and publish social media posts, manage your queue and approvals, and read post analytics in TryPost.", "author": { "name": "TryPost", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 9f7993e..e1ac00c 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "trypost", "source": "./", - "version": "2.0.0", + "version": "2.0.1", "description": "Plan, schedule and publish social media posts, manage your queue and approvals, and read post analytics in TryPost.", "author": { "name": "Paulo Castellano", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index fd13c0a..b933d7d 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "trypost", - "version": "2.0.0", + "version": "2.0.1", "description": "Plan, schedule and publish social media posts, manage your queue and approvals, and read post analytics in TryPost.", "author": { "name": "Paulo Castellano", diff --git a/.grok-plugin/plugin.json b/.grok-plugin/plugin.json index f456d62..07ecd91 100644 --- a/.grok-plugin/plugin.json +++ b/.grok-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "trypost", - "version": "2.0.0", + "version": "2.0.1", "description": "Plan, schedule and publish social media posts, manage your queue and approvals, and read post analytics in TryPost.", "author": { "name": "Paulo Castellano", From 11bc6edc01c3d000e62b1ccd95d2c79b958a8b43 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 15 Oct 2026 20:11:54 -0300 Subject: [PATCH 3/4] Name the OpenAI build after its platform build/trypost-chatgpt.zip, so a Claude package can sit next to it later. The CI artifact follows the same name. --- .github/workflows/tests.yml | 6 +++--- AGENTS.md | 2 +- README.md | 2 +- scripts/build.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5c4b252..7a7de72 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -73,9 +73,9 @@ jobs: run: node scripts/build.ts - name: List the ZIP - run: unzip -l build/trypost-plugin.zip + run: unzip -l build/trypost-chatgpt.zip - uses: actions/upload-artifact@v7 with: - name: trypost-plugin - path: build/trypost-plugin.zip + name: trypost-chatgpt + path: build/trypost-chatgpt.zip diff --git a/AGENTS.md b/AGENTS.md index 0eb5f28..5a3f040 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ This repository packages TryPost as a plugin for Claude Code, Cursor, Grok, Chat - Review needs exactly five positive and three negative test cases. Positive cases name tools the skill documents. - Never mention prices, plans, trials, discounts or upgrades in the listing or the skill. - Never put reviewer credentials, the demo video, client secrets or the domain verification token in the package; they go in the OpenAI dashboard. The verification token is served by the TryPost app at `https://app.trypost.it/.well-known/openai-apps-challenge`. -- Build the upload with `node scripts/build.ts`: it writes `build/trypost-plugin.zip` (git-ignored) from `HEAD`. The list of packed paths lives only in `scripts/build.ts` (`pluginPaths`); `tests/build.test.ts` checks it covers everything the manifest references. Never commit the ZIP. +- Build the upload with `node scripts/build.ts`: it writes `build/trypost-chatgpt.zip` (git-ignored) from `HEAD`. The list of packed paths lives only in `scripts/build.ts` (`pluginPaths`); `tests/build.test.ts` checks it covers everything the manifest references. Never commit the ZIP. ## Skill (`SKILL.md`) diff --git a/README.md b/README.md index 66de72b..ada4800 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Build the ZIP to upload at [platform.openai.com/plugins](https://platform.openai node scripts/build.ts ``` -It writes `build/trypost-plugin.zip` (git-ignored) from the last commit, so commit your changes before building. The demo video URL goes in `.codex-plugin/plugin.json` (`extensions.com.openai.review.demo_recording_url`); reviewer credentials and the domain verification token are entered in the dashboard, never in the package. +It writes `build/trypost-chatgpt.zip` (git-ignored) from the last commit, so commit your changes before building. The demo video URL goes in `.codex-plugin/plugin.json` (`extensions.com.openai.review.demo_recording_url`); reviewer credentials and the domain verification token are entered in the dashboard, never in the package. ### Cursor plugin diff --git a/scripts/build.ts b/scripts/build.ts index e2e842c..41519d9 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -6,7 +6,7 @@ export const root = join(import.meta.dirname, '..'); export const pluginPaths = ['.codex-plugin', '.mcp.json', 'skills', 'assets']; -export const outputFile = 'build/trypost-plugin.zip'; +export const outputFile = 'build/trypost-chatgpt.zip'; export const buildPluginZip = (): string => { mkdirSync(join(root, 'build'), { recursive: true }); From 39459b55845fd2cefd20abb1957537325f7f3aa4 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Thu, 15 Oct 2026 20:34:58 -0300 Subject: [PATCH 4/4] Name the MCP tools by their server and guide sign-in Anthropic's skill guide asks for fully qualified MCP tool names (ServerName:tool_name): a Codex session missed the TryPost tools because the skill named list-social-accounts-tool while the client listed mcp__trypost__list_social_accounts_tool. Every tool is now written trypost:, and a Connecting section says how clients may show the names, that the tools appear once the user connects TryPost, where to sign in or sign up, and that the web app is not a substitute. The new setup skill is the OpenAI onboarding skill: it checks the connection, lists the channels and guides sign-in after install. --- .codex-plugin/plugin.json | 1 + SKILL.md | 156 ++++++++++++++++++++------------------ skills/setup/SKILL.md | 11 +++ skills/trypost/SKILL.md | 156 ++++++++++++++++++++------------------ tests/build.test.ts | 2 +- tests/helpers.ts | 3 + tests/skill.test.ts | 49 +++++++++++- tests/types.ts | 1 + 8 files changed, 227 insertions(+), 152 deletions(-) create mode 100644 skills/setup/SKILL.md diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index a5d0e3d..efae7bf 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -41,6 +41,7 @@ }, "extensions": { "com.openai": { + "onboardingSkill": "./skills/setup/SKILL.md", "review": { "test_cases": { "positive": [ diff --git a/SKILL.md b/SKILL.md index 59f8b70..05cb59b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,44 +12,50 @@ TryPost schedules and publishes social posts from one workspace. This plugin reg Docs: https://docs.trypost.it/ai/tools-reference App: https://app.trypost.it +## Connecting + +- Every tool here comes from the `trypost` MCP server and is written `trypost:`. Clients may show the same tool in another form, for example `trypost.list-social-accounts-tool` or `mcp__trypost__list_social_accounts_tool`. If no TryPost tool is listed, search the client's tools for `trypost` before concluding they are missing: some clients load MCP tools on demand. +- The tools are listed only once the user has connected TryPost. If they are missing, or a call asks the user to sign in, the user needs to connect TryPost from the client's plugin or connector settings: that opens TryPost's sign-in (https://app.trypost.it/login), where they pick the workspace to use. No account yet: https://app.trypost.it/register. +- The TryPost web app in a browser is not a substitute for these tools. + ## Authentication - OAuth 2.1 with Dynamic Client Registration and PKCE, scope `mcp:use`. On the first call the client opens a browser: the user signs in, picks the **workspace** this connection may use, and authorizes. Every tool acts on that one workspace; to work in another, connect again and pick it. - API keys (Personal Access Tokens) are for the REST API only. Sent to the MCP endpoint they are refused with `403` `MCP OAuth authorization required.` - Other HTTP errors: `401` (token missing, expired or revoked, or the bound workspace was deleted: reconnect), `403` `Workspace access denied.` (the user no longer belongs to the workspace), `402` `Active subscription required.` (TryPost Cloud account without app access; self-hosted skips this check). -- Times in arguments are ISO 8601; without an offset they are read as UTC. Post times in responses are UTC (`Y-m-d H:i:s`). No tool returns the user's own time zone; each channel's zone is `timezone` in `list-social-accounts-tool`. +- Times in arguments are ISO 8601; without an offset they are read as UTC. Post times in responses are UTC (`Y-m-d H:i:s`). No tool returns the user's own time zone; each channel's zone is `timezone` in `trypost:list-social-accounts-tool`. ## Permissions -`get-workspace-tool` returns `me.is_admin`, `me.requires_approval` and `me.publishes_directly`. +`trypost:get-workspace-tool` returns `me.is_admin`, `me.requires_approval` and `me.publishes_directly`. | Who | Can call | |---|---| | Any member | Posts (create, update, publish, preview, delete, media, notes, metrics), ideas and stages, analytics, labels, signatures, accounts and posting-schedule reads, free slots, Pinterest boards, Discord channels, TikTok creator info, Instagram audio, content types, workspace | -| Members who publish directly (owner, admins, members without approval) | Also `approve-post-tool`, `reject-post-tool`, `set-post-recurrence-tool`, `reorder-queue-tool`, `move-post-to-slot-tool`, every repurpose tool | -| Admins (and the account owner) | Also `update-posting-schedule-tool`, `generate-posting-schedule-tool`, `copy-posting-schedule-tool`, every webhook tool, every API key tool | +| Members who publish directly (owner, admins, members without approval) | Also `trypost:approve-post-tool`, `trypost:reject-post-tool`, `trypost:set-post-recurrence-tool`, `trypost:reorder-queue-tool`, `trypost:move-post-to-slot-tool`, every repurpose tool | +| Admins (and the account owner) | Also `trypost:update-posting-schedule-tool`, `trypost:generate-posting-schedule-tool`, `trypost:copy-posting-schedule-tool`, every webhook tool, every API key tool | A refused call returns a tool error (`This action is unauthorized.`). Ids from another workspace answer as not found. ## The post model - A post has exactly **one channel**: `social_account_id`, `content_type` and `meta` sit at the top level of the post. The channel of an existing post cannot change. -- `create-post-tool` creates one post on one channel. `create-posts-tool` creates **one independent post per destination**; the posts share a `post_group_id` and are then edited, published, approved and deleted one by one. -- `status` (lifecycle): `draft`, `pending_approval`, `scheduled`, `publishing`, `published`, `failed`. The channel side of a post has its own publication status and `error_message` / `platform_url` (see `get-post-tool`). -- Create and update take `status`: `draft` (default on create), `scheduled` (with `scheduled_at`, `queue` or `queue_slot`), or `publishing` (publish now). `publish-post-tool` does the same for an existing draft or scheduled post. +- `trypost:create-post-tool` creates one post on one channel. `trypost:create-posts-tool` creates **one independent post per destination**; the posts share a `post_group_id` and are then edited, published, approved and deleted one by one. +- `status` (lifecycle): `draft`, `pending_approval`, `scheduled`, `publishing`, `published`, `failed`. The channel side of a post has its own publication status and `error_message` / `platform_url` (see `trypost:get-post-tool`). +- Create and update take `status`: `draft` (default on create), `scheduled` (with `scheduled_at`, `queue` or `queue_slot`), or `publishing` (publish now). `trypost:publish-post-tool` does the same for an existing draft or scheduled post. - Posts that are `publishing`, `published` or `failed` cannot be changed or deleted. Deleting a post never removes it from the network. ## Workflow ``` -1. get-workspace-tool # does the caller need approval? -2. list-social-accounts-tool # channel id, status, timezone, max_content_length, has_posting_schedule -3. list-content-types-tool # content_type, media rules, required_meta -4. resolve required meta # get-tiktok-creator-info-tool / list-pinterest-boards-tool / list-discord-channels-tool -5. create-post-tool or create-posts-tool # status draft | scheduled | publishing -6. preview-post-tool # exact text the network gets, with length stats -7. publish-post-tool # for drafts: now, scheduled_at, or queue -8. get-post-metrics-tool # after it is published +1. trypost:get-workspace-tool # does the caller need approval? +2. trypost:list-social-accounts-tool # channel id, status, timezone, max_content_length, has_posting_schedule +3. trypost:list-content-types-tool # content_type, media rules, required_meta +4. resolve required meta # trypost:get-tiktok-creator-info-tool / trypost:list-pinterest-boards-tool / trypost:list-discord-channels-tool +5. trypost:create-post-tool or trypost:create-posts-tool # status draft | scheduled | publishing +6. trypost:preview-post-tool # exact text the network gets, with length stats +7. trypost:publish-post-tool # for drafts: now, scheduled_at, or queue +8. trypost:get-post-metrics-tool # after it is published ``` Accounts with `status` `token_expired` or `disconnected` must be reconnected in the TryPost app; MCP cannot connect, reconnect or disconnect channels. @@ -74,7 +80,7 @@ Accounts with `status` `token_expired` or `disconnected` must be reconnected in | `discord` | Discord | `discord_message` | 2000 | | `google_business` | Google Business Profile | `google_business_post` | 1500 | -The account's real limit is `max_content_length` in `list-social-accounts-tool`. `list-content-types-tool` is the live catalog; summary of the media rules: +The account's real limit is `max_content_length` in `trypost:list-social-accounts-tool`. `trypost:list-content-types-tool` is the live catalog; summary of the media rules: | Content type | Media | |---|---| @@ -99,7 +105,7 @@ When `content_type` is omitted it is chosen like the web composer: Pinterest pic ## Per-network `meta` -Unknown keys are dropped. Enum values are exact strings. `update-post-tool` merges `meta` with the stored settings (`null` clears a key; `thread_replies` replaces the whole list). +Unknown keys are dropped. Enum values are exact strings. `trypost:update-post-tool` merges `meta` with the stored settings (`null` clears a key; `thread_replies` replaces the whole list). | Network | Keys | Required to schedule or publish | |---|---|---| @@ -117,7 +123,7 @@ Unknown keys are dropped. Enum values are exact strings. `update-post-tool` merg Every scheduled or published post also needs text or media. -**Instagram music**: `meta.audio` works only on `instagram_reel` posts from an `instagram-facebook` account. Find a track with `search-instagram-audio-tool` and pass `{id}` (optional `title`, `artist`, `cover_url`, `preview_url`, `audio_volume` / `video_volume` 0–100). On an `instagram` account it is refused. +**Instagram music**: `meta.audio` works only on `instagram_reel` posts from an `instagram-facebook` account. Find a track with `trypost:search-instagram-audio-tool` and pass `{id}` (optional `title`, `artist`, `cover_url`, `preview_url`, `audio_volume` / `video_volume` 0–100). On an `instagram` account it is refused. **Per-media settings** (`media[].meta`): `alt_text` (max 2000), `user_tags` (Instagram feed images: up to 20 `{username, x, y}`, x/y from 0 to 1), `cover_offset_ms` (video cover frame where `supports_video_cover`). @@ -131,20 +137,20 @@ Every scheduled or published post also needs text or media. | Tool | Notes | |---|---| -| `list-posts-tool` | Newest scheduled date first. Filters: `status` (`draft` \| `scheduled` \| `pending_approval` \| `published` \| `failed`), `search`, `channels[]`, `labels[]`, `untagged`, `page` | -| `get-post-tool` | Text, media, labels, status, `schedule_mode`, `scheduled_at`, `published_at`, recurrence, approval fields, `post_group_id`, and the channel's `content_type`, `meta`, status, `error_message`, `platform_url` | -| `create-post-tool` | `social_account_id` (required), `content`, `media[]`, `content_type`, `meta`, `status`, `scheduled_at`, `queue` (`next` \| `top`), `queue_slot`, `label_ids[]` | -| `create-posts-tool` | `status` (required), `destinations[]` (required: `{social_account_id, content_type?, content?, media?, meta?}`), shared `content`, `media[]`, `scheduled_at`, `queue`, `label_ids[]`. No `queue_slot`. Returns `{posts}` | -| `update-post-tool` | `post_id` plus any of `content`, `media[]` (replaces the list), `content_type`, `meta` (merged), `status`, `scheduled_at`, `queue`, `label_ids[]` (replaces). `status: draft` unschedules | -| `publish-post-tool` | `post_id`, optional `scheduled_at` or `queue`; omit both to publish now. Validated like a scheduled post; on failure nothing is published and the error names the field | -| `preview-post-tool` | The exact text the network gets (X links shown defused when that is enabled) with length stats. Nothing is truncated | -| `delete-post-tool` | Permanent, TryPost only | -| `get-post-metrics-tool` | Saved analytics of a published post; `unsupported` for unpublished posts or networks without analytics | -| `approve-post-tool` | `post_id`, optional `scheduled_at` or `publish_now`. Required when the requested time has passed or the author asked to publish now | -| `reject-post-tool` | Back to draft for its author; no reason stored | -| `set-post-recurrence-tool` | Scheduled post repeats every `interval` (1–365) `frequency` (`day` \| `week` \| `month` \| `year`), `times` (1–100) more times; last occurrence on or before 2037-12-31 | -| `clear-post-recurrence-tool` | Stops repeating; the post keeps its time | -| `list-post-notes-tool` / `create-post-note-tool` / `update-post-note-tool` / `delete-post-note-tool` | Internal team comments, never published. `body` max 2000. Only the author edits or deletes a note | +| `trypost:list-posts-tool` | Newest scheduled date first. Filters: `status` (`draft` \| `scheduled` \| `pending_approval` \| `published` \| `failed`), `search`, `channels[]`, `labels[]`, `untagged`, `page` | +| `trypost:get-post-tool` | Text, media, labels, status, `schedule_mode`, `scheduled_at`, `published_at`, recurrence, approval fields, `post_group_id`, and the channel's `content_type`, `meta`, status, `error_message`, `platform_url` | +| `trypost:create-post-tool` | `social_account_id` (required), `content`, `media[]`, `content_type`, `meta`, `status`, `scheduled_at`, `queue` (`next` \| `top`), `queue_slot`, `label_ids[]` | +| `trypost:create-posts-tool` | `status` (required), `destinations[]` (required: `{social_account_id, content_type?, content?, media?, meta?}`), shared `content`, `media[]`, `scheduled_at`, `queue`, `label_ids[]`. No `queue_slot`. Returns `{posts}` | +| `trypost:update-post-tool` | `post_id` plus any of `content`, `media[]` (replaces the list), `content_type`, `meta` (merged), `status`, `scheduled_at`, `queue`, `label_ids[]` (replaces). `status: draft` unschedules | +| `trypost:publish-post-tool` | `post_id`, optional `scheduled_at` or `queue`; omit both to publish now. Validated like a scheduled post; on failure nothing is published and the error names the field | +| `trypost:preview-post-tool` | The exact text the network gets (X links shown defused when that is enabled) with length stats. Nothing is truncated | +| `trypost:delete-post-tool` | Permanent, TryPost only | +| `trypost:get-post-metrics-tool` | Saved analytics of a published post; `unsupported` for unpublished posts or networks without analytics | +| `trypost:approve-post-tool` | `post_id`, optional `scheduled_at` or `publish_now`. Required when the requested time has passed or the author asked to publish now | +| `trypost:reject-post-tool` | Back to draft for its author; no reason stored | +| `trypost:set-post-recurrence-tool` | Scheduled post repeats every `interval` (1–365) `frequency` (`day` \| `week` \| `month` \| `year`), `times` (1–100) more times; last occurrence on or before 2037-12-31 | +| `trypost:clear-post-recurrence-tool` | Stops repeating; the post keeps its time | +| `trypost:list-post-notes-tool` / `trypost:create-post-note-tool` / `trypost:update-post-note-tool` / `trypost:delete-post-note-tool` | Internal team comments, never published. `body` max 2000. Only the author edits or deletes a note | ### Media @@ -152,11 +158,11 @@ Media goes in `media[]` on create/update, each item with exactly one of `url` (p | Tool | Notes | |---|---| -| `attach-media-from-url-tool` | `post_id`, `urls[]` (1–10 `{url, alt?, meta?}`). Appends. Returns `attached_count`, `failed_urls` and `failures[]` (`unreachable`, `type_not_allowed`, `too_large`, `host_not_allowed`) | -| `request-media-upload-tool` | One-shot signed URL for a local file: `upload_token`, `upload_url`, `field_name` (`media`), `expires_at` (15 minutes by default), `max_bytes`, `max_bytes_by_type` | -| `attach-media-from-upload-tool` | `post_id`, `upload_token`, optional `alt`, `meta`. Appends | +| `trypost:attach-media-from-url-tool` | `post_id`, `urls[]` (1–10 `{url, alt?, meta?}`). Appends. Returns `attached_count`, `failed_urls` and `failures[]` (`unreachable`, `type_not_allowed`, `too_large`, `host_not_allowed`) | +| `trypost:request-media-upload-tool` | One-shot signed URL for a local file: `upload_token`, `upload_url`, `field_name` (`media`), `expires_at` (15 minutes by default), `max_bytes`, `max_bytes_by_type` | +| `trypost:attach-media-from-upload-tool` | `post_id`, `upload_token`, optional `alt`, `meta`. Appends | -Local file: `request-media-upload-tool`, then `curl -F media=@./clip.mp4 ""`, then pass the `upload_token` in `media[]` or call `attach-media-from-upload-tool`. An upload is kept 24 hours and is spent by the first post that takes it; reuse the file on another post by its media `id`. +Local file: `trypost:request-media-upload-tool`, then `curl -F media=@./clip.mp4 ""`, then pass the `upload_token` in `media[]` or call `trypost:attach-media-from-upload-tool`. An upload is kept 24 hours and is spent by the first post that takes it; reuse the file on another post by its media `id`. Accepted files: JPEG, PNG, GIF, WebP (HEIC/HEIF when the server can convert), MP4, MOV, PDF. Server ceilings by default: 10 MB and 67 megapixels per image, 1 GB per video, 100 MB per PDF; each content type's own limits apply at schedule/publish time. @@ -164,23 +170,23 @@ Accepted files: JPEG, PNG, GIF, WebP (HEIC/HEIF when the server can convert), MP | Tool | Notes | |---|---| -| `list-social-accounts-tool` | `id`, `platform`, `display_name`, `username`, `status`, `timezone`, `posting_goal`, `has_posting_schedule`, `max_content_length`, `long_posts`, `verified_badge` | -| `get-posting-schedule-tool` | `account_id`. Time zone, weekly goal, seven days of local times (day 0 = Sunday) | -| `update-posting-schedule-tool` | `account_id`, `timezone` (IANA), `posting_goal` (1–28 or null), `posting_schedule` (7 `{day, enabled, times[]}`, max 4 `HH:mm` per day, or null to clear) | -| `generate-posting-schedule-tool` | `account_id`, `mode` (`goal` \| `recommended`), `goal` (1–28). Builds from the network's recommended windows; replaces the schedule | -| `copy-posting-schedule-tool` | `account_id`, `from` (a channel with a schedule) | -| `list-free-slots-tool` | Next free slots as UTC instants: at most 30, within 90 days | -| `move-post-to-slot-tool` | `account_id`, `post_id`, `slot_at`. A scheduled post becomes a queue post in that free slot | -| `reorder-queue-tool` | `account_id`, `post_ids[]` (exactly the first queued posts, new order). Posts swap among the slots they hold | -| `list-pinterest-boards-tool` / `create-pinterest-board-tool` | Board `id` → `meta.board_id`. Create takes `name` (max 50) | -| `list-discord-channels-tool` | Text/announcement channels the bot can post to; `id` → `meta.channel_id` | -| `get-tiktok-creator-info-tool` | `privacy_level_options`, `comment_disabled` / `duet_disabled` / `stitch_disabled` (keep the matching `allow_*` false), `max_video_post_duration_sec` (not checked at schedule time), `can_post` | -| `search-instagram-audio-tool` | `account_id` (`instagram-facebook`), optional `q` (omit for trending), `type` (`music` \| `original_sound`) | -| `get-instagram-audio-tool` | `account_id`, `audio_id`. Fails when the track is no longer available | +| `trypost:list-social-accounts-tool` | `id`, `platform`, `display_name`, `username`, `status`, `timezone`, `posting_goal`, `has_posting_schedule`, `max_content_length`, `long_posts`, `verified_badge` | +| `trypost:get-posting-schedule-tool` | `account_id`. Time zone, weekly goal, seven days of local times (day 0 = Sunday) | +| `trypost:update-posting-schedule-tool` | `account_id`, `timezone` (IANA), `posting_goal` (1–28 or null), `posting_schedule` (7 `{day, enabled, times[]}`, max 4 `HH:mm` per day, or null to clear) | +| `trypost:generate-posting-schedule-tool` | `account_id`, `mode` (`goal` \| `recommended`), `goal` (1–28). Builds from the network's recommended windows; replaces the schedule | +| `trypost:copy-posting-schedule-tool` | `account_id`, `from` (a channel with a schedule) | +| `trypost:list-free-slots-tool` | Next free slots as UTC instants: at most 30, within 90 days | +| `trypost:move-post-to-slot-tool` | `account_id`, `post_id`, `slot_at`. A scheduled post becomes a queue post in that free slot | +| `trypost:reorder-queue-tool` | `account_id`, `post_ids[]` (exactly the first queued posts, new order). Posts swap among the slots they hold | +| `trypost:list-pinterest-boards-tool` / `trypost:create-pinterest-board-tool` | Board `id` → `meta.board_id`. Create takes `name` (max 50) | +| `trypost:list-discord-channels-tool` | Text/announcement channels the bot can post to; `id` → `meta.channel_id` | +| `trypost:get-tiktok-creator-info-tool` | `privacy_level_options`, `comment_disabled` / `duet_disabled` / `stitch_disabled` (keep the matching `allow_*` false), `max_video_post_duration_sec` (not checked at schedule time), `can_post` | +| `trypost:search-instagram-audio-tool` | `account_id` (`instagram-facebook`), optional `q` (omit for trending), `type` (`music` \| `original_sound`) | +| `trypost:get-instagram-audio-tool` | `account_id`, `audio_id`. Fails when the track is no longer available | ### Ideas -`list-ideas-tool` (filters `stages[]`, `unassigned`, `labels[]`, `untagged`), `get-idea-tool`, `create-idea-tool` (`title`, `body`, `idea_stage_id`, `media_ids[]`, `label_ids[]`; needs a title, body or media), `update-idea-tool` (`media_ids` / `label_ids` replace), `duplicate-idea-tool`, `delete-ideas-tool` (up to 500), `move-ideas-tool` (`idea_id`, `idea_stage_id` or none, `idea_ids[]` = the full target column in order). Stages: `list-idea-stages-tool`, `create-idea-stage-tool` (`name` max 60), `update-idea-stage-tool`, `delete-idea-stage-tool` (ideas move to unassigned), `reorder-idea-stages-tool` (every stage id). Ideas cannot be converted into posts by MCP; create a post with the idea's text. +`trypost:list-ideas-tool` (filters `stages[]`, `unassigned`, `labels[]`, `untagged`), `trypost:get-idea-tool`, `trypost:create-idea-tool` (`title`, `body`, `idea_stage_id`, `media_ids[]`, `label_ids[]`; needs a title, body or media), `trypost:update-idea-tool` (`media_ids` / `label_ids` replace), `trypost:duplicate-idea-tool`, `trypost:delete-ideas-tool` (up to 500), `trypost:move-ideas-tool` (`idea_id`, `idea_stage_id` or none, `idea_ids[]` = the full target column in order). Stages: `trypost:list-idea-stages-tool`, `trypost:create-idea-stage-tool` (`name` max 60), `trypost:update-idea-stage-tool`, `trypost:delete-idea-stage-tool` (ideas move to unassigned), `trypost:reorder-idea-stages-tool` (every stage id). Ideas cannot be converted into posts by MCP; create a post with the idea's text. ### Analytics @@ -188,10 +194,10 @@ Ranges: `range` `7d` \| `30d` (default) \| `mtd` \| `custom` with `start` / `end | Tool | Notes | |---|---| -| `get-analytics-report-tool` | Workspace summary, follower and post charts, top posts, per-account performance. Optional `channels[]` | -| `get-analytics-publication-tool` | `publication_id` from the report's top posts, including posts imported from the network | -| `get-channel-insights-tool` | `account_id`; summary, metrics and series vs the previous range, plus 12-month follower `growth`. Filters `labels[]`, `untagged`, `types[]` | -| `list-channel-publications-tool` | `account_id`; best posts ranked by `sort`, `period` `current` \| `previous`, top 50, paginated | +| `trypost:get-analytics-report-tool` | Workspace summary, follower and post charts, top posts, per-account performance. Optional `channels[]` | +| `trypost:get-analytics-publication-tool` | `publication_id` from the report's top posts, including posts imported from the network | +| `trypost:get-channel-insights-tool` | `account_id`; summary, metrics and series vs the previous range, plus 12-month follower `growth`. Filters `labels[]`, `untagged`, `types[]` | +| `trypost:list-channel-publications-tool` | `account_id`; best posts ranked by `sort`, `period` `current` \| `previous`, top 50, paginated | Channel insights and channel publications are not available for LinkedIn, LinkedIn Page, Telegram, Discord and Google Business. @@ -201,54 +207,54 @@ Watches one Instagram or Facebook account for one format (`reel`, `video`, `stor | Tool | Notes | |---|---| -| `list-repurpose-source-formats-tool` | Watchable formats | -| `list-repurposes-tool` / `get-repurpose-tool` | Rows with destinations, status and last polling error | -| `create-repurpose-tool` | `source_social_account_id`, `source_format` (default `reel`), `publish_mode` (`publish` default \| `draft`), `destinations[]` (`{social_account_id, content_type, meta?}`, a type that accepts video). Starts as draft. One repurpose per source and format; the source cannot be a destination; Google Business cannot be a destination | -| `update-repurpose-tool` | Changing source or format resets the watermark | -| `activate-repurpose-tool` | Needs a usable source, one destination and the destinations' required meta. Only videos published from then on are copied | -| `pause-repurpose-tool` / `resume-repurpose-tool` | Pause keeps the watermark; resuming a pause made by TryPost starts from now | -| `disable-repurpose-tool` | Clears the watermark | -| `list-repurpose-items-tool` | Every source video seen and why one was skipped | -| `delete-repurpose-tool` | Stops polling; posts already created stay | +| `trypost:list-repurpose-source-formats-tool` | Watchable formats | +| `trypost:list-repurposes-tool` / `trypost:get-repurpose-tool` | Rows with destinations, status and last polling error | +| `trypost:create-repurpose-tool` | `source_social_account_id`, `source_format` (default `reel`), `publish_mode` (`publish` default \| `draft`), `destinations[]` (`{social_account_id, content_type, meta?}`, a type that accepts video). Starts as draft. One repurpose per source and format; the source cannot be a destination; Google Business cannot be a destination | +| `trypost:update-repurpose-tool` | Changing source or format resets the watermark | +| `trypost:activate-repurpose-tool` | Needs a usable source, one destination and the destinations' required meta. Only videos published from then on are copied | +| `trypost:pause-repurpose-tool` / `trypost:resume-repurpose-tool` | Pause keeps the watermark; resuming a pause made by TryPost starts from now | +| `trypost:disable-repurpose-tool` | Clears the watermark | +| `trypost:list-repurpose-items-tool` | Every source video seen and why one was skipped | +| `trypost:delete-repurpose-tool` | Stops polling; posts already created stay | ### Labels, signatures, workspace, platform | Tool | Notes | |---|---| -| `list-labels-tool`, `create-label-tool`, `update-label-tool`, `delete-label-tool` | `name`, `color` (`#RRGGBB`). Deleting detaches the label from posts | -| `list-signatures-tool`, `create-signature-tool`, `update-signature-tool`, `delete-signature-tool` | `name`, `content`. A signature is never added automatically: append its content to the post text | -| `get-workspace-tool` | Current workspace and the caller's `me` flags | -| `list-content-types-tool` | Per platform: `allowed_media_types`, `default_content_type`, `max_content_length`, `max_hashtags`, `alt_text_max_length`, `required_meta`. Per content type: media counts, accepted types, GIF/MOV, byte caps, duration, aspect ratios, `captionless`, `document_must_be_alone`, `supports_thread_replies` | +| `trypost:list-labels-tool`, `trypost:create-label-tool`, `trypost:update-label-tool`, `trypost:delete-label-tool` | `name`, `color` (`#RRGGBB`). Deleting detaches the label from posts | +| `trypost:list-signatures-tool`, `trypost:create-signature-tool`, `trypost:update-signature-tool`, `trypost:delete-signature-tool` | `name`, `content`. A signature is never added automatically: append its content to the post text | +| `trypost:get-workspace-tool` | Current workspace and the caller's `me` flags | +| `trypost:list-content-types-tool` | Per platform: `allowed_media_types`, `default_content_type`, `max_content_length`, `max_hashtags`, `alt_text_max_length`, `required_meta`. Per content type: media counts, accepted types, GIF/MOV, byte caps, duration, aspect ratios, `captionless`, `document_must_be_alone`, `supports_thread_replies` | ### Webhooks (admins) Events: `post.created`, `post.scheduled`, `post.unscheduled`, `post.published`, `post.failed`, `post.deleted`. Each delivery is a JSON POST signed in `X-Webhook-Signature` (HMAC-SHA256 hex of the body). After 5 failed deliveries in a row the webhook is `paused`. -`list-webhooks-tool` (no secrets), `get-webhook-tool` (includes `signing_secret`), `create-webhook-tool` (`endpoint` public URL, `events[]`; created enabled; no test sent), `update-webhook-tool` (`endpoint`, `events`, `status` `enabled` \| `disabled`; re-enabling resets failures), `delete-webhook-tool` (with its logs), `send-webhook-test-tool` (signed `webhook.test` ping, no log), `rotate-webhook-secret-tool` (old secret stops at once), `list-webhook-logs-tool`, `replay-webhook-log-tool` (`webhook_id`, `log_id`; new delivery even when paused or disabled). +`trypost:list-webhooks-tool` (no secrets), `trypost:get-webhook-tool` (includes `signing_secret`), `trypost:create-webhook-tool` (`endpoint` public URL, `events[]`; created enabled; no test sent), `trypost:update-webhook-tool` (`endpoint`, `events`, `status` `enabled` \| `disabled`; re-enabling resets failures), `trypost:delete-webhook-tool` (with its logs), `trypost:send-webhook-test-tool` (signed `webhook.test` ping, no log), `trypost:rotate-webhook-secret-tool` (old secret stops at once), `trypost:list-webhook-logs-tool`, `trypost:replay-webhook-log-tool` (`webhook_id`, `log_id`; new delivery even when paused or disabled). ### API keys (admins) -Manage Personal Access Tokens for the REST API (never usable on MCP). `list-api-keys-tool` (metadata only; OAuth connections excluded), `create-api-key-tool` (`name`, optional `expires_at`; returns `plain_token` once), `delete-api-key-tool` (`api_key_id`; integrations using it stop working). +Manage Personal Access Tokens for the REST API (never usable on MCP). `trypost:list-api-keys-tool` (metadata only; OAuth connections excluded), `trypost:create-api-key-tool` (`name`, optional `expires_at`; returns `plain_token` once), `trypost:delete-api-key-tool` (`api_key_id`; integrations using it stop working). ## Queue and slots - The queue needs posting times on the channel (`has_posting_schedule`). `queue: next` takes the first free slot; `queue: top` takes the first slot and shifts the queued posts behind it to the next gap. Do not combine `queue` with `scheduled_at`. -- `queue_slot` (create-post-tool) or `move-post-to-slot-tool` places a post in one exact free slot from `list-free-slots-tool`. A slot held by any scheduled post or by a queue request pending approval is refused. +- `queue_slot` (trypost:create-post-tool) or `trypost:move-post-to-slot-tool` places a post in one exact free slot from `trypost:list-free-slots-tool`. A slot held by any scheduled post or by a queue request pending approval is refused. - A queued post keeps its slot. Deleting, drafting or re-timing it leaves the slot free; nothing is pulled forward. Schedule changes re-place only posts whose slot disappeared. ## Approvals -When `me.requires_approval` is true, scheduling, queueing or publishing stores the post as `pending_approval` instead (also when such a member edits a scheduled post or attaches media to it). A member who publishes directly then calls `approve-post-tool` or `reject-post-tool`. A pending queue request reserves its slot. Pending posts are visible to approvers and to the member who asked. +When `me.requires_approval` is true, scheduling, queueing or publishing stores the post as `pending_approval` instead (also when such a member edits a scheduled post or attaches media to it). A member who publishes directly then calls `trypost:approve-post-tool` or `trypost:reject-post-tool`. A pending queue request reserves its slot. Pending posts are visible to approvers and to the member who asked. ## Gotchas -1. One post = one channel. For several channels use `create-posts-tool`; each result is a separate post sharing `post_group_id`. -2. `create-post-tool` defaults to `draft`. Send `status: scheduled` / `publishing`, or call `publish-post-tool`. +1. One post = one channel. For several channels use `trypost:create-posts-tool`; each result is a separate post sharing `post_group_id`. +2. `trypost:create-post-tool` defaults to `draft`. Send `status: scheduled` / `publishing`, or call `trypost:publish-post-tool`. 3. TikTok `privacy_level`, Pinterest `board_id`, Discord `channel_id`, YouTube title, and Google Business `event` (EVENT/OFFER) are required before scheduling or publishing. 4. The text limit is per account: an X account without `long_posts` is 280. Instagram allows at most 5 hashtags. Stories send no text. -5. `update-post-tool` `media[]` and `label_ids[]` replace the lists; `meta` merges. +5. `trypost:update-post-tool` `media[]` and `label_ids[]` replace the lists; `meta` merges. 6. Instagram music needs an `instagram-facebook` account and an `instagram_reel`. 7. YouTube is Shorts only (`youtube_short`, one video up to 3 minutes). 8. Facebook is a Page, not a personal profile. Google Business Profile posts take at most one image. 9. MCP cannot connect channels, generate content with AI, convert ideas into posts, or manage templates, members or billing; use the web app. -10. `publish-post-tool`, `approve-post-tool`, `rotate-webhook-secret-tool` and the delete tools (except `delete-repurpose-tool`) are marked destructive; read tools are marked read-only. +10. `trypost:publish-post-tool`, `trypost:approve-post-tool`, `trypost:rotate-webhook-secret-tool` and the delete tools (except `trypost:delete-repurpose-tool`) are marked destructive; read tools are marked read-only. diff --git a/skills/setup/SKILL.md b/skills/setup/SKILL.md new file mode 100644 index 0000000..64bd3cb --- /dev/null +++ b/skills/setup/SKILL.md @@ -0,0 +1,11 @@ +--- +name: trypost-setup +description: Checks that TryPost is connected after the plugin is installed and helps the user sign in, pick a workspace and see their connected channels. Use right after installing TryPost, or when the TryPost tools are not available. +--- + +# TryPost setup + +1. Call `trypost:get-workspace-tool`. When it answers, tell the user which workspace is connected and whether their posts need approval (`me.requires_approval`). +2. Call `trypost:list-social-accounts-tool` and list the connected channels. A channel with `status` `token_expired` or `disconnected` must be reconnected in the TryPost app (Accounts). With no channel, the user connects one in the TryPost app at https://app.trypost.it: channels cannot be connected through the plugin. +3. If the TryPost tools are not listed, or a call asks the user to sign in, they have not connected TryPost yet. Ask them to connect TryPost from the plugin settings: it opens TryPost's sign-in (https://app.trypost.it/login), where they pick the workspace this connection may use. No account yet: https://app.trypost.it/register. Some clients show the tools as `trypost.` or `mcp__trypost__`; search for `trypost` before concluding they are missing. +4. To use another workspace, disconnect TryPost in the plugin settings and connect again, picking that workspace. diff --git a/skills/trypost/SKILL.md b/skills/trypost/SKILL.md index 59f8b70..05cb59b 100644 --- a/skills/trypost/SKILL.md +++ b/skills/trypost/SKILL.md @@ -12,44 +12,50 @@ TryPost schedules and publishes social posts from one workspace. This plugin reg Docs: https://docs.trypost.it/ai/tools-reference App: https://app.trypost.it +## Connecting + +- Every tool here comes from the `trypost` MCP server and is written `trypost:`. Clients may show the same tool in another form, for example `trypost.list-social-accounts-tool` or `mcp__trypost__list_social_accounts_tool`. If no TryPost tool is listed, search the client's tools for `trypost` before concluding they are missing: some clients load MCP tools on demand. +- The tools are listed only once the user has connected TryPost. If they are missing, or a call asks the user to sign in, the user needs to connect TryPost from the client's plugin or connector settings: that opens TryPost's sign-in (https://app.trypost.it/login), where they pick the workspace to use. No account yet: https://app.trypost.it/register. +- The TryPost web app in a browser is not a substitute for these tools. + ## Authentication - OAuth 2.1 with Dynamic Client Registration and PKCE, scope `mcp:use`. On the first call the client opens a browser: the user signs in, picks the **workspace** this connection may use, and authorizes. Every tool acts on that one workspace; to work in another, connect again and pick it. - API keys (Personal Access Tokens) are for the REST API only. Sent to the MCP endpoint they are refused with `403` `MCP OAuth authorization required.` - Other HTTP errors: `401` (token missing, expired or revoked, or the bound workspace was deleted: reconnect), `403` `Workspace access denied.` (the user no longer belongs to the workspace), `402` `Active subscription required.` (TryPost Cloud account without app access; self-hosted skips this check). -- Times in arguments are ISO 8601; without an offset they are read as UTC. Post times in responses are UTC (`Y-m-d H:i:s`). No tool returns the user's own time zone; each channel's zone is `timezone` in `list-social-accounts-tool`. +- Times in arguments are ISO 8601; without an offset they are read as UTC. Post times in responses are UTC (`Y-m-d H:i:s`). No tool returns the user's own time zone; each channel's zone is `timezone` in `trypost:list-social-accounts-tool`. ## Permissions -`get-workspace-tool` returns `me.is_admin`, `me.requires_approval` and `me.publishes_directly`. +`trypost:get-workspace-tool` returns `me.is_admin`, `me.requires_approval` and `me.publishes_directly`. | Who | Can call | |---|---| | Any member | Posts (create, update, publish, preview, delete, media, notes, metrics), ideas and stages, analytics, labels, signatures, accounts and posting-schedule reads, free slots, Pinterest boards, Discord channels, TikTok creator info, Instagram audio, content types, workspace | -| Members who publish directly (owner, admins, members without approval) | Also `approve-post-tool`, `reject-post-tool`, `set-post-recurrence-tool`, `reorder-queue-tool`, `move-post-to-slot-tool`, every repurpose tool | -| Admins (and the account owner) | Also `update-posting-schedule-tool`, `generate-posting-schedule-tool`, `copy-posting-schedule-tool`, every webhook tool, every API key tool | +| Members who publish directly (owner, admins, members without approval) | Also `trypost:approve-post-tool`, `trypost:reject-post-tool`, `trypost:set-post-recurrence-tool`, `trypost:reorder-queue-tool`, `trypost:move-post-to-slot-tool`, every repurpose tool | +| Admins (and the account owner) | Also `trypost:update-posting-schedule-tool`, `trypost:generate-posting-schedule-tool`, `trypost:copy-posting-schedule-tool`, every webhook tool, every API key tool | A refused call returns a tool error (`This action is unauthorized.`). Ids from another workspace answer as not found. ## The post model - A post has exactly **one channel**: `social_account_id`, `content_type` and `meta` sit at the top level of the post. The channel of an existing post cannot change. -- `create-post-tool` creates one post on one channel. `create-posts-tool` creates **one independent post per destination**; the posts share a `post_group_id` and are then edited, published, approved and deleted one by one. -- `status` (lifecycle): `draft`, `pending_approval`, `scheduled`, `publishing`, `published`, `failed`. The channel side of a post has its own publication status and `error_message` / `platform_url` (see `get-post-tool`). -- Create and update take `status`: `draft` (default on create), `scheduled` (with `scheduled_at`, `queue` or `queue_slot`), or `publishing` (publish now). `publish-post-tool` does the same for an existing draft or scheduled post. +- `trypost:create-post-tool` creates one post on one channel. `trypost:create-posts-tool` creates **one independent post per destination**; the posts share a `post_group_id` and are then edited, published, approved and deleted one by one. +- `status` (lifecycle): `draft`, `pending_approval`, `scheduled`, `publishing`, `published`, `failed`. The channel side of a post has its own publication status and `error_message` / `platform_url` (see `trypost:get-post-tool`). +- Create and update take `status`: `draft` (default on create), `scheduled` (with `scheduled_at`, `queue` or `queue_slot`), or `publishing` (publish now). `trypost:publish-post-tool` does the same for an existing draft or scheduled post. - Posts that are `publishing`, `published` or `failed` cannot be changed or deleted. Deleting a post never removes it from the network. ## Workflow ``` -1. get-workspace-tool # does the caller need approval? -2. list-social-accounts-tool # channel id, status, timezone, max_content_length, has_posting_schedule -3. list-content-types-tool # content_type, media rules, required_meta -4. resolve required meta # get-tiktok-creator-info-tool / list-pinterest-boards-tool / list-discord-channels-tool -5. create-post-tool or create-posts-tool # status draft | scheduled | publishing -6. preview-post-tool # exact text the network gets, with length stats -7. publish-post-tool # for drafts: now, scheduled_at, or queue -8. get-post-metrics-tool # after it is published +1. trypost:get-workspace-tool # does the caller need approval? +2. trypost:list-social-accounts-tool # channel id, status, timezone, max_content_length, has_posting_schedule +3. trypost:list-content-types-tool # content_type, media rules, required_meta +4. resolve required meta # trypost:get-tiktok-creator-info-tool / trypost:list-pinterest-boards-tool / trypost:list-discord-channels-tool +5. trypost:create-post-tool or trypost:create-posts-tool # status draft | scheduled | publishing +6. trypost:preview-post-tool # exact text the network gets, with length stats +7. trypost:publish-post-tool # for drafts: now, scheduled_at, or queue +8. trypost:get-post-metrics-tool # after it is published ``` Accounts with `status` `token_expired` or `disconnected` must be reconnected in the TryPost app; MCP cannot connect, reconnect or disconnect channels. @@ -74,7 +80,7 @@ Accounts with `status` `token_expired` or `disconnected` must be reconnected in | `discord` | Discord | `discord_message` | 2000 | | `google_business` | Google Business Profile | `google_business_post` | 1500 | -The account's real limit is `max_content_length` in `list-social-accounts-tool`. `list-content-types-tool` is the live catalog; summary of the media rules: +The account's real limit is `max_content_length` in `trypost:list-social-accounts-tool`. `trypost:list-content-types-tool` is the live catalog; summary of the media rules: | Content type | Media | |---|---| @@ -99,7 +105,7 @@ When `content_type` is omitted it is chosen like the web composer: Pinterest pic ## Per-network `meta` -Unknown keys are dropped. Enum values are exact strings. `update-post-tool` merges `meta` with the stored settings (`null` clears a key; `thread_replies` replaces the whole list). +Unknown keys are dropped. Enum values are exact strings. `trypost:update-post-tool` merges `meta` with the stored settings (`null` clears a key; `thread_replies` replaces the whole list). | Network | Keys | Required to schedule or publish | |---|---|---| @@ -117,7 +123,7 @@ Unknown keys are dropped. Enum values are exact strings. `update-post-tool` merg Every scheduled or published post also needs text or media. -**Instagram music**: `meta.audio` works only on `instagram_reel` posts from an `instagram-facebook` account. Find a track with `search-instagram-audio-tool` and pass `{id}` (optional `title`, `artist`, `cover_url`, `preview_url`, `audio_volume` / `video_volume` 0–100). On an `instagram` account it is refused. +**Instagram music**: `meta.audio` works only on `instagram_reel` posts from an `instagram-facebook` account. Find a track with `trypost:search-instagram-audio-tool` and pass `{id}` (optional `title`, `artist`, `cover_url`, `preview_url`, `audio_volume` / `video_volume` 0–100). On an `instagram` account it is refused. **Per-media settings** (`media[].meta`): `alt_text` (max 2000), `user_tags` (Instagram feed images: up to 20 `{username, x, y}`, x/y from 0 to 1), `cover_offset_ms` (video cover frame where `supports_video_cover`). @@ -131,20 +137,20 @@ Every scheduled or published post also needs text or media. | Tool | Notes | |---|---| -| `list-posts-tool` | Newest scheduled date first. Filters: `status` (`draft` \| `scheduled` \| `pending_approval` \| `published` \| `failed`), `search`, `channels[]`, `labels[]`, `untagged`, `page` | -| `get-post-tool` | Text, media, labels, status, `schedule_mode`, `scheduled_at`, `published_at`, recurrence, approval fields, `post_group_id`, and the channel's `content_type`, `meta`, status, `error_message`, `platform_url` | -| `create-post-tool` | `social_account_id` (required), `content`, `media[]`, `content_type`, `meta`, `status`, `scheduled_at`, `queue` (`next` \| `top`), `queue_slot`, `label_ids[]` | -| `create-posts-tool` | `status` (required), `destinations[]` (required: `{social_account_id, content_type?, content?, media?, meta?}`), shared `content`, `media[]`, `scheduled_at`, `queue`, `label_ids[]`. No `queue_slot`. Returns `{posts}` | -| `update-post-tool` | `post_id` plus any of `content`, `media[]` (replaces the list), `content_type`, `meta` (merged), `status`, `scheduled_at`, `queue`, `label_ids[]` (replaces). `status: draft` unschedules | -| `publish-post-tool` | `post_id`, optional `scheduled_at` or `queue`; omit both to publish now. Validated like a scheduled post; on failure nothing is published and the error names the field | -| `preview-post-tool` | The exact text the network gets (X links shown defused when that is enabled) with length stats. Nothing is truncated | -| `delete-post-tool` | Permanent, TryPost only | -| `get-post-metrics-tool` | Saved analytics of a published post; `unsupported` for unpublished posts or networks without analytics | -| `approve-post-tool` | `post_id`, optional `scheduled_at` or `publish_now`. Required when the requested time has passed or the author asked to publish now | -| `reject-post-tool` | Back to draft for its author; no reason stored | -| `set-post-recurrence-tool` | Scheduled post repeats every `interval` (1–365) `frequency` (`day` \| `week` \| `month` \| `year`), `times` (1–100) more times; last occurrence on or before 2037-12-31 | -| `clear-post-recurrence-tool` | Stops repeating; the post keeps its time | -| `list-post-notes-tool` / `create-post-note-tool` / `update-post-note-tool` / `delete-post-note-tool` | Internal team comments, never published. `body` max 2000. Only the author edits or deletes a note | +| `trypost:list-posts-tool` | Newest scheduled date first. Filters: `status` (`draft` \| `scheduled` \| `pending_approval` \| `published` \| `failed`), `search`, `channels[]`, `labels[]`, `untagged`, `page` | +| `trypost:get-post-tool` | Text, media, labels, status, `schedule_mode`, `scheduled_at`, `published_at`, recurrence, approval fields, `post_group_id`, and the channel's `content_type`, `meta`, status, `error_message`, `platform_url` | +| `trypost:create-post-tool` | `social_account_id` (required), `content`, `media[]`, `content_type`, `meta`, `status`, `scheduled_at`, `queue` (`next` \| `top`), `queue_slot`, `label_ids[]` | +| `trypost:create-posts-tool` | `status` (required), `destinations[]` (required: `{social_account_id, content_type?, content?, media?, meta?}`), shared `content`, `media[]`, `scheduled_at`, `queue`, `label_ids[]`. No `queue_slot`. Returns `{posts}` | +| `trypost:update-post-tool` | `post_id` plus any of `content`, `media[]` (replaces the list), `content_type`, `meta` (merged), `status`, `scheduled_at`, `queue`, `label_ids[]` (replaces). `status: draft` unschedules | +| `trypost:publish-post-tool` | `post_id`, optional `scheduled_at` or `queue`; omit both to publish now. Validated like a scheduled post; on failure nothing is published and the error names the field | +| `trypost:preview-post-tool` | The exact text the network gets (X links shown defused when that is enabled) with length stats. Nothing is truncated | +| `trypost:delete-post-tool` | Permanent, TryPost only | +| `trypost:get-post-metrics-tool` | Saved analytics of a published post; `unsupported` for unpublished posts or networks without analytics | +| `trypost:approve-post-tool` | `post_id`, optional `scheduled_at` or `publish_now`. Required when the requested time has passed or the author asked to publish now | +| `trypost:reject-post-tool` | Back to draft for its author; no reason stored | +| `trypost:set-post-recurrence-tool` | Scheduled post repeats every `interval` (1–365) `frequency` (`day` \| `week` \| `month` \| `year`), `times` (1–100) more times; last occurrence on or before 2037-12-31 | +| `trypost:clear-post-recurrence-tool` | Stops repeating; the post keeps its time | +| `trypost:list-post-notes-tool` / `trypost:create-post-note-tool` / `trypost:update-post-note-tool` / `trypost:delete-post-note-tool` | Internal team comments, never published. `body` max 2000. Only the author edits or deletes a note | ### Media @@ -152,11 +158,11 @@ Media goes in `media[]` on create/update, each item with exactly one of `url` (p | Tool | Notes | |---|---| -| `attach-media-from-url-tool` | `post_id`, `urls[]` (1–10 `{url, alt?, meta?}`). Appends. Returns `attached_count`, `failed_urls` and `failures[]` (`unreachable`, `type_not_allowed`, `too_large`, `host_not_allowed`) | -| `request-media-upload-tool` | One-shot signed URL for a local file: `upload_token`, `upload_url`, `field_name` (`media`), `expires_at` (15 minutes by default), `max_bytes`, `max_bytes_by_type` | -| `attach-media-from-upload-tool` | `post_id`, `upload_token`, optional `alt`, `meta`. Appends | +| `trypost:attach-media-from-url-tool` | `post_id`, `urls[]` (1–10 `{url, alt?, meta?}`). Appends. Returns `attached_count`, `failed_urls` and `failures[]` (`unreachable`, `type_not_allowed`, `too_large`, `host_not_allowed`) | +| `trypost:request-media-upload-tool` | One-shot signed URL for a local file: `upload_token`, `upload_url`, `field_name` (`media`), `expires_at` (15 minutes by default), `max_bytes`, `max_bytes_by_type` | +| `trypost:attach-media-from-upload-tool` | `post_id`, `upload_token`, optional `alt`, `meta`. Appends | -Local file: `request-media-upload-tool`, then `curl -F media=@./clip.mp4 ""`, then pass the `upload_token` in `media[]` or call `attach-media-from-upload-tool`. An upload is kept 24 hours and is spent by the first post that takes it; reuse the file on another post by its media `id`. +Local file: `trypost:request-media-upload-tool`, then `curl -F media=@./clip.mp4 ""`, then pass the `upload_token` in `media[]` or call `trypost:attach-media-from-upload-tool`. An upload is kept 24 hours and is spent by the first post that takes it; reuse the file on another post by its media `id`. Accepted files: JPEG, PNG, GIF, WebP (HEIC/HEIF when the server can convert), MP4, MOV, PDF. Server ceilings by default: 10 MB and 67 megapixels per image, 1 GB per video, 100 MB per PDF; each content type's own limits apply at schedule/publish time. @@ -164,23 +170,23 @@ Accepted files: JPEG, PNG, GIF, WebP (HEIC/HEIF when the server can convert), MP | Tool | Notes | |---|---| -| `list-social-accounts-tool` | `id`, `platform`, `display_name`, `username`, `status`, `timezone`, `posting_goal`, `has_posting_schedule`, `max_content_length`, `long_posts`, `verified_badge` | -| `get-posting-schedule-tool` | `account_id`. Time zone, weekly goal, seven days of local times (day 0 = Sunday) | -| `update-posting-schedule-tool` | `account_id`, `timezone` (IANA), `posting_goal` (1–28 or null), `posting_schedule` (7 `{day, enabled, times[]}`, max 4 `HH:mm` per day, or null to clear) | -| `generate-posting-schedule-tool` | `account_id`, `mode` (`goal` \| `recommended`), `goal` (1–28). Builds from the network's recommended windows; replaces the schedule | -| `copy-posting-schedule-tool` | `account_id`, `from` (a channel with a schedule) | -| `list-free-slots-tool` | Next free slots as UTC instants: at most 30, within 90 days | -| `move-post-to-slot-tool` | `account_id`, `post_id`, `slot_at`. A scheduled post becomes a queue post in that free slot | -| `reorder-queue-tool` | `account_id`, `post_ids[]` (exactly the first queued posts, new order). Posts swap among the slots they hold | -| `list-pinterest-boards-tool` / `create-pinterest-board-tool` | Board `id` → `meta.board_id`. Create takes `name` (max 50) | -| `list-discord-channels-tool` | Text/announcement channels the bot can post to; `id` → `meta.channel_id` | -| `get-tiktok-creator-info-tool` | `privacy_level_options`, `comment_disabled` / `duet_disabled` / `stitch_disabled` (keep the matching `allow_*` false), `max_video_post_duration_sec` (not checked at schedule time), `can_post` | -| `search-instagram-audio-tool` | `account_id` (`instagram-facebook`), optional `q` (omit for trending), `type` (`music` \| `original_sound`) | -| `get-instagram-audio-tool` | `account_id`, `audio_id`. Fails when the track is no longer available | +| `trypost:list-social-accounts-tool` | `id`, `platform`, `display_name`, `username`, `status`, `timezone`, `posting_goal`, `has_posting_schedule`, `max_content_length`, `long_posts`, `verified_badge` | +| `trypost:get-posting-schedule-tool` | `account_id`. Time zone, weekly goal, seven days of local times (day 0 = Sunday) | +| `trypost:update-posting-schedule-tool` | `account_id`, `timezone` (IANA), `posting_goal` (1–28 or null), `posting_schedule` (7 `{day, enabled, times[]}`, max 4 `HH:mm` per day, or null to clear) | +| `trypost:generate-posting-schedule-tool` | `account_id`, `mode` (`goal` \| `recommended`), `goal` (1–28). Builds from the network's recommended windows; replaces the schedule | +| `trypost:copy-posting-schedule-tool` | `account_id`, `from` (a channel with a schedule) | +| `trypost:list-free-slots-tool` | Next free slots as UTC instants: at most 30, within 90 days | +| `trypost:move-post-to-slot-tool` | `account_id`, `post_id`, `slot_at`. A scheduled post becomes a queue post in that free slot | +| `trypost:reorder-queue-tool` | `account_id`, `post_ids[]` (exactly the first queued posts, new order). Posts swap among the slots they hold | +| `trypost:list-pinterest-boards-tool` / `trypost:create-pinterest-board-tool` | Board `id` → `meta.board_id`. Create takes `name` (max 50) | +| `trypost:list-discord-channels-tool` | Text/announcement channels the bot can post to; `id` → `meta.channel_id` | +| `trypost:get-tiktok-creator-info-tool` | `privacy_level_options`, `comment_disabled` / `duet_disabled` / `stitch_disabled` (keep the matching `allow_*` false), `max_video_post_duration_sec` (not checked at schedule time), `can_post` | +| `trypost:search-instagram-audio-tool` | `account_id` (`instagram-facebook`), optional `q` (omit for trending), `type` (`music` \| `original_sound`) | +| `trypost:get-instagram-audio-tool` | `account_id`, `audio_id`. Fails when the track is no longer available | ### Ideas -`list-ideas-tool` (filters `stages[]`, `unassigned`, `labels[]`, `untagged`), `get-idea-tool`, `create-idea-tool` (`title`, `body`, `idea_stage_id`, `media_ids[]`, `label_ids[]`; needs a title, body or media), `update-idea-tool` (`media_ids` / `label_ids` replace), `duplicate-idea-tool`, `delete-ideas-tool` (up to 500), `move-ideas-tool` (`idea_id`, `idea_stage_id` or none, `idea_ids[]` = the full target column in order). Stages: `list-idea-stages-tool`, `create-idea-stage-tool` (`name` max 60), `update-idea-stage-tool`, `delete-idea-stage-tool` (ideas move to unassigned), `reorder-idea-stages-tool` (every stage id). Ideas cannot be converted into posts by MCP; create a post with the idea's text. +`trypost:list-ideas-tool` (filters `stages[]`, `unassigned`, `labels[]`, `untagged`), `trypost:get-idea-tool`, `trypost:create-idea-tool` (`title`, `body`, `idea_stage_id`, `media_ids[]`, `label_ids[]`; needs a title, body or media), `trypost:update-idea-tool` (`media_ids` / `label_ids` replace), `trypost:duplicate-idea-tool`, `trypost:delete-ideas-tool` (up to 500), `trypost:move-ideas-tool` (`idea_id`, `idea_stage_id` or none, `idea_ids[]` = the full target column in order). Stages: `trypost:list-idea-stages-tool`, `trypost:create-idea-stage-tool` (`name` max 60), `trypost:update-idea-stage-tool`, `trypost:delete-idea-stage-tool` (ideas move to unassigned), `trypost:reorder-idea-stages-tool` (every stage id). Ideas cannot be converted into posts by MCP; create a post with the idea's text. ### Analytics @@ -188,10 +194,10 @@ Ranges: `range` `7d` \| `30d` (default) \| `mtd` \| `custom` with `start` / `end | Tool | Notes | |---|---| -| `get-analytics-report-tool` | Workspace summary, follower and post charts, top posts, per-account performance. Optional `channels[]` | -| `get-analytics-publication-tool` | `publication_id` from the report's top posts, including posts imported from the network | -| `get-channel-insights-tool` | `account_id`; summary, metrics and series vs the previous range, plus 12-month follower `growth`. Filters `labels[]`, `untagged`, `types[]` | -| `list-channel-publications-tool` | `account_id`; best posts ranked by `sort`, `period` `current` \| `previous`, top 50, paginated | +| `trypost:get-analytics-report-tool` | Workspace summary, follower and post charts, top posts, per-account performance. Optional `channels[]` | +| `trypost:get-analytics-publication-tool` | `publication_id` from the report's top posts, including posts imported from the network | +| `trypost:get-channel-insights-tool` | `account_id`; summary, metrics and series vs the previous range, plus 12-month follower `growth`. Filters `labels[]`, `untagged`, `types[]` | +| `trypost:list-channel-publications-tool` | `account_id`; best posts ranked by `sort`, `period` `current` \| `previous`, top 50, paginated | Channel insights and channel publications are not available for LinkedIn, LinkedIn Page, Telegram, Discord and Google Business. @@ -201,54 +207,54 @@ Watches one Instagram or Facebook account for one format (`reel`, `video`, `stor | Tool | Notes | |---|---| -| `list-repurpose-source-formats-tool` | Watchable formats | -| `list-repurposes-tool` / `get-repurpose-tool` | Rows with destinations, status and last polling error | -| `create-repurpose-tool` | `source_social_account_id`, `source_format` (default `reel`), `publish_mode` (`publish` default \| `draft`), `destinations[]` (`{social_account_id, content_type, meta?}`, a type that accepts video). Starts as draft. One repurpose per source and format; the source cannot be a destination; Google Business cannot be a destination | -| `update-repurpose-tool` | Changing source or format resets the watermark | -| `activate-repurpose-tool` | Needs a usable source, one destination and the destinations' required meta. Only videos published from then on are copied | -| `pause-repurpose-tool` / `resume-repurpose-tool` | Pause keeps the watermark; resuming a pause made by TryPost starts from now | -| `disable-repurpose-tool` | Clears the watermark | -| `list-repurpose-items-tool` | Every source video seen and why one was skipped | -| `delete-repurpose-tool` | Stops polling; posts already created stay | +| `trypost:list-repurpose-source-formats-tool` | Watchable formats | +| `trypost:list-repurposes-tool` / `trypost:get-repurpose-tool` | Rows with destinations, status and last polling error | +| `trypost:create-repurpose-tool` | `source_social_account_id`, `source_format` (default `reel`), `publish_mode` (`publish` default \| `draft`), `destinations[]` (`{social_account_id, content_type, meta?}`, a type that accepts video). Starts as draft. One repurpose per source and format; the source cannot be a destination; Google Business cannot be a destination | +| `trypost:update-repurpose-tool` | Changing source or format resets the watermark | +| `trypost:activate-repurpose-tool` | Needs a usable source, one destination and the destinations' required meta. Only videos published from then on are copied | +| `trypost:pause-repurpose-tool` / `trypost:resume-repurpose-tool` | Pause keeps the watermark; resuming a pause made by TryPost starts from now | +| `trypost:disable-repurpose-tool` | Clears the watermark | +| `trypost:list-repurpose-items-tool` | Every source video seen and why one was skipped | +| `trypost:delete-repurpose-tool` | Stops polling; posts already created stay | ### Labels, signatures, workspace, platform | Tool | Notes | |---|---| -| `list-labels-tool`, `create-label-tool`, `update-label-tool`, `delete-label-tool` | `name`, `color` (`#RRGGBB`). Deleting detaches the label from posts | -| `list-signatures-tool`, `create-signature-tool`, `update-signature-tool`, `delete-signature-tool` | `name`, `content`. A signature is never added automatically: append its content to the post text | -| `get-workspace-tool` | Current workspace and the caller's `me` flags | -| `list-content-types-tool` | Per platform: `allowed_media_types`, `default_content_type`, `max_content_length`, `max_hashtags`, `alt_text_max_length`, `required_meta`. Per content type: media counts, accepted types, GIF/MOV, byte caps, duration, aspect ratios, `captionless`, `document_must_be_alone`, `supports_thread_replies` | +| `trypost:list-labels-tool`, `trypost:create-label-tool`, `trypost:update-label-tool`, `trypost:delete-label-tool` | `name`, `color` (`#RRGGBB`). Deleting detaches the label from posts | +| `trypost:list-signatures-tool`, `trypost:create-signature-tool`, `trypost:update-signature-tool`, `trypost:delete-signature-tool` | `name`, `content`. A signature is never added automatically: append its content to the post text | +| `trypost:get-workspace-tool` | Current workspace and the caller's `me` flags | +| `trypost:list-content-types-tool` | Per platform: `allowed_media_types`, `default_content_type`, `max_content_length`, `max_hashtags`, `alt_text_max_length`, `required_meta`. Per content type: media counts, accepted types, GIF/MOV, byte caps, duration, aspect ratios, `captionless`, `document_must_be_alone`, `supports_thread_replies` | ### Webhooks (admins) Events: `post.created`, `post.scheduled`, `post.unscheduled`, `post.published`, `post.failed`, `post.deleted`. Each delivery is a JSON POST signed in `X-Webhook-Signature` (HMAC-SHA256 hex of the body). After 5 failed deliveries in a row the webhook is `paused`. -`list-webhooks-tool` (no secrets), `get-webhook-tool` (includes `signing_secret`), `create-webhook-tool` (`endpoint` public URL, `events[]`; created enabled; no test sent), `update-webhook-tool` (`endpoint`, `events`, `status` `enabled` \| `disabled`; re-enabling resets failures), `delete-webhook-tool` (with its logs), `send-webhook-test-tool` (signed `webhook.test` ping, no log), `rotate-webhook-secret-tool` (old secret stops at once), `list-webhook-logs-tool`, `replay-webhook-log-tool` (`webhook_id`, `log_id`; new delivery even when paused or disabled). +`trypost:list-webhooks-tool` (no secrets), `trypost:get-webhook-tool` (includes `signing_secret`), `trypost:create-webhook-tool` (`endpoint` public URL, `events[]`; created enabled; no test sent), `trypost:update-webhook-tool` (`endpoint`, `events`, `status` `enabled` \| `disabled`; re-enabling resets failures), `trypost:delete-webhook-tool` (with its logs), `trypost:send-webhook-test-tool` (signed `webhook.test` ping, no log), `trypost:rotate-webhook-secret-tool` (old secret stops at once), `trypost:list-webhook-logs-tool`, `trypost:replay-webhook-log-tool` (`webhook_id`, `log_id`; new delivery even when paused or disabled). ### API keys (admins) -Manage Personal Access Tokens for the REST API (never usable on MCP). `list-api-keys-tool` (metadata only; OAuth connections excluded), `create-api-key-tool` (`name`, optional `expires_at`; returns `plain_token` once), `delete-api-key-tool` (`api_key_id`; integrations using it stop working). +Manage Personal Access Tokens for the REST API (never usable on MCP). `trypost:list-api-keys-tool` (metadata only; OAuth connections excluded), `trypost:create-api-key-tool` (`name`, optional `expires_at`; returns `plain_token` once), `trypost:delete-api-key-tool` (`api_key_id`; integrations using it stop working). ## Queue and slots - The queue needs posting times on the channel (`has_posting_schedule`). `queue: next` takes the first free slot; `queue: top` takes the first slot and shifts the queued posts behind it to the next gap. Do not combine `queue` with `scheduled_at`. -- `queue_slot` (create-post-tool) or `move-post-to-slot-tool` places a post in one exact free slot from `list-free-slots-tool`. A slot held by any scheduled post or by a queue request pending approval is refused. +- `queue_slot` (trypost:create-post-tool) or `trypost:move-post-to-slot-tool` places a post in one exact free slot from `trypost:list-free-slots-tool`. A slot held by any scheduled post or by a queue request pending approval is refused. - A queued post keeps its slot. Deleting, drafting or re-timing it leaves the slot free; nothing is pulled forward. Schedule changes re-place only posts whose slot disappeared. ## Approvals -When `me.requires_approval` is true, scheduling, queueing or publishing stores the post as `pending_approval` instead (also when such a member edits a scheduled post or attaches media to it). A member who publishes directly then calls `approve-post-tool` or `reject-post-tool`. A pending queue request reserves its slot. Pending posts are visible to approvers and to the member who asked. +When `me.requires_approval` is true, scheduling, queueing or publishing stores the post as `pending_approval` instead (also when such a member edits a scheduled post or attaches media to it). A member who publishes directly then calls `trypost:approve-post-tool` or `trypost:reject-post-tool`. A pending queue request reserves its slot. Pending posts are visible to approvers and to the member who asked. ## Gotchas -1. One post = one channel. For several channels use `create-posts-tool`; each result is a separate post sharing `post_group_id`. -2. `create-post-tool` defaults to `draft`. Send `status: scheduled` / `publishing`, or call `publish-post-tool`. +1. One post = one channel. For several channels use `trypost:create-posts-tool`; each result is a separate post sharing `post_group_id`. +2. `trypost:create-post-tool` defaults to `draft`. Send `status: scheduled` / `publishing`, or call `trypost:publish-post-tool`. 3. TikTok `privacy_level`, Pinterest `board_id`, Discord `channel_id`, YouTube title, and Google Business `event` (EVENT/OFFER) are required before scheduling or publishing. 4. The text limit is per account: an X account without `long_posts` is 280. Instagram allows at most 5 hashtags. Stories send no text. -5. `update-post-tool` `media[]` and `label_ids[]` replace the lists; `meta` merges. +5. `trypost:update-post-tool` `media[]` and `label_ids[]` replace the lists; `meta` merges. 6. Instagram music needs an `instagram-facebook` account and an `instagram_reel`. 7. YouTube is Shorts only (`youtube_short`, one video up to 3 minutes). 8. Facebook is a Page, not a personal profile. Google Business Profile posts take at most one image. 9. MCP cannot connect channels, generate content with AI, convert ideas into posts, or manage templates, members or billing; use the web app. -10. `publish-post-tool`, `approve-post-tool`, `rotate-webhook-secret-tool` and the delete tools (except `delete-repurpose-tool`) are marked destructive; read tools are marked read-only. +10. `trypost:publish-post-tool`, `trypost:approve-post-tool`, `trypost:rotate-webhook-secret-tool` and the delete tools (except `trypost:delete-repurpose-tool`) are marked destructive; read tools are marked read-only. diff --git a/tests/build.test.ts b/tests/build.test.ts index 6017ab4..f9ee21f 100644 --- a/tests/build.test.ts +++ b/tests/build.test.ts @@ -34,7 +34,7 @@ describe('OpenAI plugin build', () => { assert.equal(file, `${root}/${outputFile}`); - for (const entry of ['.codex-plugin/plugin.json', '.mcp.json', 'skills/trypost/SKILL.md', 'assets/icon-512.png']) { + for (const entry of ['.codex-plugin/plugin.json', '.mcp.json', 'skills/trypost/SKILL.md', 'skills/setup/SKILL.md', 'assets/icon-512.png']) { assert.ok(entries.includes(`trypost/${entry}`), `trypost/${entry} is missing from the ZIP`); } diff --git a/tests/helpers.ts b/tests/helpers.ts index 94ce204..971d960 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -55,3 +55,6 @@ export const contrastWithWhite = (hex: string): number => { }; export const toolNames = (text: string): Set => new Set(text.match(/\b[a-z-]+-tool\b/g)); + +export const unqualifiedToolNames = (text: string): string[] => + text.match(/(? { } }); }); + +describe('skill tool names and sign-in', () => { + const skill = read('skills/trypost/SKILL.md'); + + test('names every tool with its trypost server', () => { + assert.deepEqual(unqualifiedToolNames(skill), []); + }); + + test('explains where the tools come from and how the user signs in', () => { + assert.ok(skill.includes('## Connecting')); + assert.ok(skill.includes('mcp__trypost__list_social_accounts_tool')); + assert.ok(skill.includes('https://app.trypost.it/login')); + assert.ok(skill.includes('https://app.trypost.it/register')); + }); +}); + +describe('setup skill', () => { + const setup = read('skills/setup/SKILL.md'); + const { name, description = '' } = frontMatter('skills/setup/SKILL.md'); + const onboardingSkill = readJson('.codex-plugin/plugin.json').extensions?.['com.openai']?.onboardingSkill ?? ''; + + test('is the onboarding skill the OpenAI manifest declares', () => { + assert.equal(onboardingSkill, './skills/setup/SKILL.md'); + assert.ok(exists(onboardingSkill)); + }); + + test('front matter is valid', () => { + assert.equal(name, 'trypost-setup'); + assert.ok(description.length > 0 && description.length <= 1024); + }); + + test('only uses tools the main skill documents, by their qualified names', () => { + const documented = toolNames(read('skills/trypost/SKILL.md')); + + assert.deepEqual(unqualifiedToolNames(setup), []); + + for (const tool of toolNames(setup)) { + assert.ok(documented.has(tool), `${tool} is not in the main skill`); + } + }); + + test('tells the user how to sign in and where to create an account', () => { + assert.ok(setup.includes('https://app.trypost.it/login')); + assert.ok(setup.includes('https://app.trypost.it/register')); + }); +}); diff --git a/tests/types.ts b/tests/types.ts index 7fce7ec..facaa56 100644 --- a/tests/types.ts +++ b/tests/types.ts @@ -47,6 +47,7 @@ export type ReviewTestCase = { }; export type OpenAiSettings = { + onboardingSkill?: string; hooks?: unknown; apps?: unknown; review?: {