kadenzo-mcp 1.0.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.
Files changed (3) hide show
  1. package/README.md +48 -0
  2. package/index.js +150 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Kadenzo MCP server
2
+
3
+ Connect any MCP-capable AI agent — **Claude Desktop, Cursor, ChatGPT, Codex** — to
4
+ [Kadenzo](https://kadenzo.app) and let it schedule, manage, and analyze social posts
5
+ across **11 networks** (Instagram, TikTok, X, LinkedIn, YouTube, Facebook, Pinterest,
6
+ Threads, Bluesky, Mastodon, Telegram). It wraps the Kadenzo Studio public API as
7
+ [Model Context Protocol](https://modelcontextprotocol.io) tools, so your agent doesn't
8
+ just *draft* content — it can **ship** it.
9
+
10
+ ## Tools
11
+
12
+ | Tool | What it does |
13
+ |------|--------------|
14
+ | `list_accounts` | List connected accounts (ids + platforms) |
15
+ | `schedule_post` | Schedule a post for a future time (+ `validate_only`, `options`, `thread`) |
16
+ | `list_posts` | List your posts, newest first (filter by status, paginate) |
17
+ | `get_post` | One post: status, per-channel outcome, options/thread |
18
+ | `update_post` | Edit a not-yet-published post |
19
+ | `cancel_post` | Cancel a scheduled post before it publishes |
20
+ | `upload_media` | Upload a local image/video file, get a URL for `media_urls` |
21
+ | `get_account_analytics` | Recent posts + engagement metrics for an account |
22
+ | `get_post_analytics` | Per-channel metrics for a post you scheduled |
23
+
24
+ ## Setup
25
+
26
+ 1. Generate an API key in Kadenzo: **Settings → API keys** (`studio.kadenzo.app/dashboard/settings?section=api`). The API is available on paid plans.
27
+ 2. Add the server to your MCP client. **Claude Desktop** (`claude_desktop_config.json`) / **Cursor** (`~/.cursor/mcp.json`):
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "kadenzo": {
33
+ "command": "npx",
34
+ "args": ["-y", "kadenzo-mcp"],
35
+ "env": { "KADENZO_API_KEY": "kdz_live_xxxxxxxxxxxxxxxxxxxxxxxx" }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ 3. Restart the client. Ask your agent things like *"list my connected accounts,"* *"schedule this to X and Bluesky tomorrow at 9am,"* or *"how did my last Instagram posts perform?"*
42
+
43
+ ## Notes
44
+
45
+ - Posts are **schedule-only** — provide a future `scheduled_for`; they publish automatically.
46
+ - `KADENZO_API_BASE` overrides the API base URL (default `https://studio.kadenzo.app/api/v1`).
47
+ - Requires Node 18+ (uses the built-in `fetch`/`FormData`).
48
+ - Learn more / full API reference: <https://studio.kadenzo.app/developers> · <https://kadenzo.app>
package/index.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Kadenzo MCP server — exposes the Kadenzo Studio public API (studio.kadenzo.app/api/v1)
4
+ * as Model Context Protocol tools, so any MCP-capable agent (Claude Desktop, Cursor,
5
+ * ChatGPT, …) can schedule, manage, and analyze social posts across 11 networks.
6
+ *
7
+ * Auth: set KADENZO_API_KEY (generate at studio.kadenzo.app/dashboard/settings?section=api).
8
+ * Transport: stdio (the standard for locally-run MCP servers).
9
+ */
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
12
+ import { z } from 'zod'
13
+ import { readFile } from 'node:fs/promises'
14
+ import { basename } from 'node:path'
15
+
16
+ const API_KEY = process.env.KADENZO_API_KEY
17
+ const BASE = (process.env.KADENZO_API_BASE || 'https://studio.kadenzo.app/api/v1').replace(/\/$/, '')
18
+
19
+ if (!API_KEY) {
20
+ console.error('KADENZO_API_KEY is not set. Generate a key at https://studio.kadenzo.app/dashboard/settings?section=api and set it in your MCP client config.')
21
+ process.exit(1)
22
+ }
23
+
24
+ async function api(method, path, { body, query } = {}) {
25
+ const url = new URL(BASE + path)
26
+ if (query) for (const [k, v] of Object.entries(query)) if (v != null) url.searchParams.set(k, String(v))
27
+ const res = await fetch(url, {
28
+ method,
29
+ headers: { Authorization: `Bearer ${API_KEY}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
30
+ body: body ? JSON.stringify(body) : undefined,
31
+ })
32
+ const text = await res.text()
33
+ let data
34
+ try { data = JSON.parse(text) } catch { data = text }
35
+ if (!res.ok) {
36
+ const msg = data && typeof data === 'object' ? data.error || JSON.stringify(data) : String(data)
37
+ throw new Error(`${res.status} ${msg}`)
38
+ }
39
+ return data
40
+ }
41
+
42
+ const ok = (data) => ({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] })
43
+ const fail = (e) => ({ content: [{ type: 'text', text: `Error: ${e?.message || e}` }], isError: true })
44
+
45
+ const server = new McpServer({ name: 'kadenzo', version: '1.0.0' })
46
+
47
+ server.tool(
48
+ 'list_accounts',
49
+ 'List the social accounts connected to this Kadenzo workspace — returns each account id, platform, and username. Use the ids as account_ids when scheduling.',
50
+ {},
51
+ async () => { try { return ok(await api('GET', '/accounts')) } catch (e) { return fail(e) } },
52
+ )
53
+
54
+ server.tool(
55
+ 'schedule_post',
56
+ 'Schedule a social post to one or more connected accounts for a FUTURE time (it publishes automatically). Set validate_only=true to check accounts/limits/timing without scheduling.',
57
+ {
58
+ content: z.string().optional().describe('Post text. Optional only if media_urls is provided.'),
59
+ account_ids: z.array(z.string()).describe('Account ids from list_accounts (at least one).'),
60
+ scheduled_for: z.string().describe('ISO 8601 future datetime, e.g. "2026-07-02T09:00:00Z".'),
61
+ media_urls: z.array(z.string()).optional().describe('Public image/video URLs, or URLs returned by upload_media.'),
62
+ platform_content: z.record(z.string()).optional().describe('Per-platform text overrides, e.g. {"twitter":"shorter text"}.'),
63
+ options: z.record(z.any()).optional().describe('Per-platform format options, e.g. {"instagram":{"as_reel":true,"first_comment":"#tags"},"tiktok":{"privacy_level":"PUBLIC_TO_EVERYONE"}}.'),
64
+ thread: z.record(z.array(z.string())).optional().describe('Multi-post threads, e.g. {"x":["1/…","2/…"],"bluesky":["…"]}.'),
65
+ validate_only: z.boolean().optional().describe('If true, validate only (dry run) and do not schedule.'),
66
+ },
67
+ async ({ validate_only, ...rest }) => {
68
+ try {
69
+ const body = { ...rest }
70
+ if (validate_only) body.dry_run = true
71
+ return ok(await api('POST', '/posts', { body }))
72
+ } catch (e) { return fail(e) }
73
+ },
74
+ )
75
+
76
+ server.tool(
77
+ 'list_posts',
78
+ 'List your posts, newest first. Optionally filter by status (comma-separated, e.g. "pending,posted") and paginate with limit (1-100) and offset.',
79
+ {
80
+ status: z.string().optional().describe('Comma-separated statuses, or omit for all.'),
81
+ limit: z.number().optional(),
82
+ offset: z.number().optional(),
83
+ },
84
+ async (a) => { try { return ok(await api('GET', '/posts', { query: a })) } catch (e) { return fail(e) } },
85
+ )
86
+
87
+ server.tool(
88
+ 'get_post',
89
+ 'Get one post: roll-up status, per-channel outcome, and any options/thread that were set on it.',
90
+ { id: z.string().describe('The post id.') },
91
+ async ({ id }) => { try { return ok(await api('GET', `/posts/${id}`)) } catch (e) { return fail(e) } },
92
+ )
93
+
94
+ server.tool(
95
+ 'update_post',
96
+ 'Edit a post that has not published yet — change content, accounts, time, media, options, or thread.',
97
+ {
98
+ id: z.string(),
99
+ content: z.string().optional(),
100
+ account_ids: z.array(z.string()).optional(),
101
+ scheduled_for: z.string().optional(),
102
+ media_urls: z.array(z.string()).optional(),
103
+ platform_content: z.record(z.string()).optional(),
104
+ options: z.record(z.any()).optional(),
105
+ thread: z.record(z.array(z.string())).optional(),
106
+ },
107
+ async ({ id, ...body }) => { try { return ok(await api('PATCH', `/posts/${id}`, { body })) } catch (e) { return fail(e) } },
108
+ )
109
+
110
+ server.tool(
111
+ 'cancel_post',
112
+ 'Cancel a scheduled post before it publishes. Already-published posts cannot be cancelled.',
113
+ { id: z.string() },
114
+ async ({ id }) => { try { return ok(await api('DELETE', `/posts/${id}`)) } catch (e) { return fail(e) } },
115
+ )
116
+
117
+ server.tool(
118
+ 'upload_media',
119
+ 'Upload a local image or video file and get a hosted URL to use in media_urls when scheduling.',
120
+ { path: z.string().describe('Absolute path to a local image/video file.') },
121
+ async ({ path }) => {
122
+ try {
123
+ const buf = await readFile(path)
124
+ const fd = new FormData()
125
+ fd.append('file', new Blob([buf]), basename(path))
126
+ const res = await fetch(`${BASE}/media`, { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}` }, body: fd })
127
+ const data = await res.json().catch(() => ({}))
128
+ if (!res.ok) throw new Error(`${res.status} ${data.error || JSON.stringify(data)}`)
129
+ return ok(data)
130
+ } catch (e) { return fail(e) }
131
+ },
132
+ )
133
+
134
+ server.tool(
135
+ 'get_account_analytics',
136
+ 'Recent posts and their engagement metrics for one connected account (likes, comments, views, etc.). The reliable analytics surface.',
137
+ { account_id: z.string(), limit: z.number().optional().describe('1-100, default 25.') },
138
+ async ({ account_id, limit }) => { try { return ok(await api('GET', `/accounts/${account_id}/analytics`, { query: { limit } })) } catch (e) { return fail(e) } },
139
+ )
140
+
141
+ server.tool(
142
+ 'get_post_analytics',
143
+ 'Per-channel engagement metrics for a specific post you scheduled (best-effort; use get_account_analytics for the full picture).',
144
+ { id: z.string() },
145
+ async ({ id }) => { try { return ok(await api('GET', `/posts/${id}/analytics`)) } catch (e) { return fail(e) } },
146
+ )
147
+
148
+ const transport = new StdioServerTransport()
149
+ await server.connect(transport)
150
+ console.error('Kadenzo MCP server running (stdio). 9 tools available.')
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "kadenzo-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Kadenzo — schedule, manage, and analyze social posts across 11 networks from any AI agent (Claude, Cursor, ChatGPT).",
5
+ "type": "module",
6
+ "bin": {
7
+ "kadenzo-mcp": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "social-media",
17
+ "scheduler",
18
+ "kadenzo",
19
+ "ai-agents"
20
+ ],
21
+ "homepage": "https://kadenzo.app",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.10.0",
28
+ "zod": "^3.23.8"
29
+ }
30
+ }