@publishq/mcp 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -11
- package/dist/index.js +283 -226
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,11 +14,33 @@
|
|
|
14
14
|
|
|
15
15
|
This is the official [MCP](https://modelcontextprotocol.io) server for [PublishQ](https://publishq.com). Point Claude Desktop, Cursor, or any MCP-compatible agent at it and they can list your connected accounts, create and schedule posts, manage workspaces, and upload media, all through the [PublishQ API](https://publishq.com/docs).
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
There are two ways to use it:
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
- **Hosted:** connect to `https://publishq.com/mcp`. You sign in to PublishQ in your browser and approve the app. Nothing to install and no key to copy.
|
|
20
|
+
- **Local:** run this package over stdio with `npx`.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
## Hosted server
|
|
23
|
+
|
|
24
|
+
Add `https://publishq.com/mcp` as a remote MCP server in your client. On first use it opens PublishQ in your browser, you sign in and choose what the app may do, and the client stores the login and refreshes it on its own.
|
|
25
|
+
|
|
26
|
+
- **Claude (claude.ai, Desktop, mobile):** Settings, Connectors, Add custom connector, then paste the URL.
|
|
27
|
+
- **ChatGPT:** turn on developer mode under Settings, then create an app with the URL and OAuth as the authentication.
|
|
28
|
+
- **Claude Code:** `claude mcp add --transport http publishq https://publishq.com/mcp`
|
|
29
|
+
- **Cursor, VS Code, Kiro and other clients:** add a server of type `http` with the URL.
|
|
30
|
+
|
|
31
|
+
Clients that cannot sign in through a browser can send an API key instead, as an `Authorization: Bearer pq_live_...` header.
|
|
32
|
+
|
|
33
|
+
You can see and disconnect every connected app under **API Keys** in the PublishQ dashboard.
|
|
34
|
+
|
|
35
|
+
## Local server
|
|
36
|
+
|
|
37
|
+
Sign in once with the CLI, and the local server uses that login:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @publishq/cli@latest login
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or create an API key under **API Keys** in the PublishQ dashboard (it starts with `pq_live_`) and pass it as `PUBLISHQ_API_KEY`.
|
|
22
44
|
|
|
23
45
|
### Claude Desktop
|
|
24
46
|
|
|
@@ -29,10 +51,7 @@ Add this to your `claude_desktop_config.json`:
|
|
|
29
51
|
"mcpServers": {
|
|
30
52
|
"publishq": {
|
|
31
53
|
"command": "npx",
|
|
32
|
-
"args": ["-y", "@publishq/mcp@latest"]
|
|
33
|
-
"env": {
|
|
34
|
-
"PUBLISHQ_API_KEY": "pq_live_your_key_here"
|
|
35
|
-
}
|
|
54
|
+
"args": ["-y", "@publishq/mcp@latest"]
|
|
36
55
|
}
|
|
37
56
|
}
|
|
38
57
|
}
|
|
@@ -42,13 +61,13 @@ Restart Claude Desktop and you'll see the PublishQ tools available. Now you can
|
|
|
42
61
|
|
|
43
62
|
### Other clients (Cursor, Windsurf, etc.)
|
|
44
63
|
|
|
45
|
-
Any MCP client that launches a stdio server works the same way: run the command `npx -y @publishq/mcp@latest` with `PUBLISHQ_API_KEY` set in the environment.
|
|
64
|
+
Any MCP client that launches a stdio server works the same way: run the command `npx -y @publishq/mcp@latest`, after `publishq login` or with `PUBLISHQ_API_KEY` set in the environment.
|
|
46
65
|
|
|
47
66
|
## Configuration
|
|
48
67
|
|
|
49
68
|
| Variable | Required | Description |
|
|
50
69
|
| ------------------ | -------- | ------------------------------------------------------------------ |
|
|
51
|
-
| `PUBLISHQ_API_KEY` |
|
|
70
|
+
| `PUBLISHQ_API_KEY` | No | Your PublishQ API key (`pq_live_...`). Not needed after `publishq login`. |
|
|
52
71
|
| `PUBLISHQ_API_URL` | No | Override the API base URL (defaults to `https://publishq.com/api/v1`). |
|
|
53
72
|
|
|
54
73
|
## Tools
|
|
@@ -68,14 +87,15 @@ Any MCP client that launches a stdio server works the same way: run the command
|
|
|
68
87
|
| `publishq_create_workspace` | Create a workspace |
|
|
69
88
|
| `publishq_list_media` | List uploaded/generated media |
|
|
70
89
|
| `publishq_get_media` | Get one media file by ID |
|
|
71
|
-
| `publishq_upload_media` | Upload a local
|
|
90
|
+
| `publishq_upload_media` | Upload a file and get a media ID: a local path (local server) or a link or chat attachment (hosted server) |
|
|
91
|
+
| `publishq_create_media_upload` | Get a signed URL to upload a file from your own disk, up to 4 GB |
|
|
72
92
|
| `publishq_delete_media` | Delete a media file no draft or scheduled post uses |
|
|
73
93
|
|
|
74
94
|
The tools are thin wrappers over [`@publishq/sdk`](https://www.npmjs.com/package/@publishq/sdk), so their behavior and scopes match the [PublishQ API](https://publishq.com/docs). `publishq_create_post` accepts an `accounts` array whose entries contain `accountId` plus optional `postOverrides` and `platformSpecificSettings`. `publishq_update_post` accepts content, scheduling, complete media replacement or removal, and thumbnail timestamp changes. `publishq_list_media` can filter by media IDs, types, and sources. For TikTok, the agent should call `publishq_get_tiktok_creator_info` first and then provide the required privacy and interaction settings in that account object. A tool call that hits an API error returns the API error so the agent can react.
|
|
75
95
|
|
|
76
96
|
## Scopes
|
|
77
97
|
|
|
78
|
-
Tools require the same
|
|
98
|
+
Tools require the same scopes as the underlying endpoints: `posts:read`/`posts:write`, `accounts:read`, `workspaces:read`/`workspaces:write`, and `media:read`/`media:write`. Grant a key or an app only the scopes your agent needs.
|
|
79
99
|
|
|
80
100
|
## Related
|
|
81
101
|
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { openAsBlob, statSync } from "fs";
|
|
5
5
|
import { basename } from "path";
|
|
6
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import PublishQ from "@publishq/sdk";
|
|
8
|
+
import { resolveCredential } from "@publishq/sdk/credentials";
|
|
9
|
+
|
|
10
|
+
// src/tools.ts
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
12
|
import {
|
|
9
13
|
CallToolRequestSchema,
|
|
10
14
|
ErrorCode,
|
|
11
15
|
ListToolsRequestSchema,
|
|
12
16
|
McpError
|
|
13
17
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
zCreateMediaUploadBody,
|
|
15
20
|
zCreatePostBody,
|
|
16
21
|
zCreateWorkspaceBody,
|
|
17
22
|
zDeleteMediaPath,
|
|
@@ -28,18 +33,6 @@ import PublishQ, {
|
|
|
28
33
|
zUpdatePostPath
|
|
29
34
|
} from "@publishq/sdk";
|
|
30
35
|
import { toJSONSchema } from "zod/v4-mini";
|
|
31
|
-
var OPEN_AS_BLOB_MAX_BYTES = 2 ** 32;
|
|
32
|
-
var API_KEY = process.env.PUBLISHQ_API_KEY;
|
|
33
|
-
if (!API_KEY) {
|
|
34
|
-
console.error(
|
|
35
|
-
"ERROR: PUBLISHQ_API_KEY environment variable is required. Create a key at https://publishq.com/app/api-keys."
|
|
36
|
-
);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
var publishq = new PublishQ({
|
|
40
|
-
apiKey: API_KEY,
|
|
41
|
-
...process.env.PUBLISHQ_API_URL ? { baseUrl: process.env.PUBLISHQ_API_URL } : {}
|
|
42
|
-
});
|
|
43
36
|
function describeError(error) {
|
|
44
37
|
const cause = error.cause;
|
|
45
38
|
if (cause instanceof Error) {
|
|
@@ -74,8 +67,38 @@ async function run(call) {
|
|
|
74
67
|
return errorResult(error);
|
|
75
68
|
}
|
|
76
69
|
}
|
|
70
|
+
var isPlainObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
71
|
+
var isAnnotationOnly = (node) => isPlainObject(node) && Object.keys(node).every((key) => key === "description" || key === "title");
|
|
72
|
+
var isEmpty = (value) => isPlainObject(value) && Object.keys(value).length === 0;
|
|
73
|
+
function booleanSchema(value) {
|
|
74
|
+
if (isEmpty(value)) return true;
|
|
75
|
+
if (isPlainObject(value) && Object.keys(value).length === 1 && isEmpty(value.not)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
function portableSchema(node) {
|
|
81
|
+
if (Array.isArray(node)) return node.map(portableSchema);
|
|
82
|
+
if (!isPlainObject(node)) return node;
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const [key, value] of Object.entries(node)) {
|
|
85
|
+
result[key] = key === "additionalProperties" ? booleanSchema(value) ?? portableSchema(value) : portableSchema(value);
|
|
86
|
+
}
|
|
87
|
+
const allOf = result.allOf;
|
|
88
|
+
if (Array.isArray(allOf)) {
|
|
89
|
+
const annotations = allOf.filter(isAnnotationOnly);
|
|
90
|
+
const schemas = allOf.filter((member) => !isAnnotationOnly(member));
|
|
91
|
+
if (annotations.length > 0 && schemas.length === 1 && isPlainObject(schemas[0])) {
|
|
92
|
+
delete result.allOf;
|
|
93
|
+
return { ...Object.assign({}, ...annotations), ...schemas[0], ...result };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
77
98
|
function advertisedSchema(schema) {
|
|
78
|
-
const jsonSchema =
|
|
99
|
+
const jsonSchema = portableSchema(
|
|
100
|
+
toJSONSchema(schema, { io: "input", target: "draft-7" })
|
|
101
|
+
);
|
|
79
102
|
if (jsonSchema.type !== "object") {
|
|
80
103
|
throw new Error("MCP tool input schema must describe an object.");
|
|
81
104
|
}
|
|
@@ -96,224 +119,258 @@ function mergedAdvertisedSchema(...schemas) {
|
|
|
96
119
|
additionalProperties: false
|
|
97
120
|
};
|
|
98
121
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
122
|
+
function publishqTools(publishq2) {
|
|
123
|
+
const tools = [];
|
|
124
|
+
const register = (definition, handler) => {
|
|
125
|
+
tools.push({ definition, handler });
|
|
126
|
+
};
|
|
127
|
+
register(
|
|
128
|
+
{
|
|
129
|
+
name: "publishq_list_posts",
|
|
130
|
+
title: "List posts",
|
|
131
|
+
description: "List the posts on the authenticated PublishQ account. Paginated. Each post includes its connected accounts and status (DRAFT, SCHEDULED, PUBLISHING, PUBLISHED, FAILED).",
|
|
132
|
+
inputSchema: advertisedSchema(zListPostsQuery),
|
|
133
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
134
|
+
},
|
|
135
|
+
(args) => run(publishq2.posts.list(args))
|
|
136
|
+
);
|
|
137
|
+
register(
|
|
138
|
+
{
|
|
139
|
+
name: "publishq_get_post",
|
|
140
|
+
title: "Get a post",
|
|
141
|
+
description: "Get one post by its PublishQ post ID.",
|
|
142
|
+
inputSchema: advertisedSchema(zGetPostPath),
|
|
143
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
144
|
+
},
|
|
145
|
+
(args) => run(publishq2.posts.get(args.id))
|
|
146
|
+
);
|
|
147
|
+
register(
|
|
148
|
+
{
|
|
149
|
+
name: "publishq_create_post",
|
|
150
|
+
title: "Create a post",
|
|
151
|
+
description: "Create a post for one or more connected accounts. Get account IDs from publishq_list_accounts. Each account accepts postOverrides and platformSpecificSettings. Before posting to TikTok, call publishq_get_tiktok_creator_info and include all required TikTok settings. Omit both scheduledAt and publishNow to save as a draft; set publishNow to publish immediately, or scheduledAt (ISO 8601) to schedule for later.",
|
|
152
|
+
inputSchema: advertisedSchema(zCreatePostBody),
|
|
153
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
154
|
+
},
|
|
155
|
+
(args) => run(publishq2.posts.create(args))
|
|
156
|
+
);
|
|
157
|
+
register(
|
|
158
|
+
{
|
|
159
|
+
name: "publishq_update_post",
|
|
160
|
+
title: "Update a post",
|
|
161
|
+
description: "Update a draft or scheduled post. Supports content, scheduledAt, complete media replacement/removal through mediaIds, thumbnailTimestamp, and accounts (each with postOverrides and platformSpecificSettings, for example a YouTube thumbnailMediaId). Only the fields you send change; send a field as null to remove it. Published and publishing posts cannot be updated.",
|
|
162
|
+
inputSchema: mergedAdvertisedSchema(zUpdatePostPath, zUpdatePostBody),
|
|
163
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true }
|
|
164
|
+
},
|
|
165
|
+
(args) => {
|
|
166
|
+
const { id, ...body } = args;
|
|
167
|
+
return run(
|
|
168
|
+
publishq2.posts.update(id, body)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
register(
|
|
173
|
+
{
|
|
174
|
+
name: "publishq_delete_post",
|
|
175
|
+
title: "Delete a post",
|
|
176
|
+
description: "Delete a scheduled or draft post by its ID. This cannot be undone.",
|
|
177
|
+
inputSchema: advertisedSchema(zDeletePostPath),
|
|
178
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
179
|
+
},
|
|
180
|
+
(args) => run(publishq2.posts.delete(args.id))
|
|
181
|
+
);
|
|
182
|
+
register(
|
|
183
|
+
{
|
|
184
|
+
name: "publishq_remove_post_account",
|
|
185
|
+
title: "Remove an account from a post",
|
|
186
|
+
description: "Remove one account from an existing post so it will no longer be published there. Other accounts in the same post are unaffected.",
|
|
187
|
+
inputSchema: advertisedSchema(zRemovePostAccountPath),
|
|
188
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
189
|
+
},
|
|
190
|
+
(args) => run(
|
|
191
|
+
publishq2.posts.removeAccount(
|
|
192
|
+
args.id,
|
|
193
|
+
args.accountId
|
|
194
|
+
)
|
|
170
195
|
)
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
196
|
+
);
|
|
197
|
+
register(
|
|
198
|
+
{
|
|
199
|
+
name: "publishq_list_accounts",
|
|
200
|
+
title: "List connected accounts",
|
|
201
|
+
description: "List the social accounts connected to the authenticated PublishQ account. Optionally filter by workspace and platform. Use the returned account IDs when creating posts.",
|
|
202
|
+
inputSchema: advertisedSchema(zListAccountsQuery),
|
|
203
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
204
|
+
},
|
|
205
|
+
(args) => run(publishq2.accounts.list(args))
|
|
206
|
+
);
|
|
207
|
+
register(
|
|
208
|
+
{
|
|
209
|
+
name: "publishq_get_tiktok_creator_info",
|
|
210
|
+
title: "Get TikTok creator info",
|
|
211
|
+
description: "Get a TikTok account's current privacy options, interaction settings, and max video duration. Call this before creating a TikTok post. The privacyLevel you set must be one of the returned privacyLevelOptions.",
|
|
212
|
+
inputSchema: advertisedSchema(zGetTikTokCreatorInfoPath),
|
|
213
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
214
|
+
},
|
|
215
|
+
(args) => run(publishq2.accounts.getTikTokCreatorInfo(args.id))
|
|
216
|
+
);
|
|
217
|
+
register(
|
|
218
|
+
{
|
|
219
|
+
name: "publishq_list_workspaces",
|
|
220
|
+
title: "List workspaces",
|
|
221
|
+
description: "List your workspaces, including their names, colors, and account counts.",
|
|
222
|
+
inputSchema: { type: "object", properties: {} },
|
|
223
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
224
|
+
},
|
|
225
|
+
() => run(publishq2.workspaces.list())
|
|
226
|
+
);
|
|
227
|
+
register(
|
|
228
|
+
{
|
|
229
|
+
name: "publishq_get_workspace",
|
|
230
|
+
title: "Get a workspace",
|
|
231
|
+
description: "Get one workspace by its ID.",
|
|
232
|
+
inputSchema: advertisedSchema(zGetWorkspacePath),
|
|
233
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
234
|
+
},
|
|
235
|
+
(args) => run(publishq2.workspaces.get(args.id))
|
|
236
|
+
);
|
|
237
|
+
register(
|
|
238
|
+
{
|
|
239
|
+
name: "publishq_create_workspace",
|
|
240
|
+
title: "Create a workspace",
|
|
241
|
+
description: "Create a workspace to organize your connected accounts.",
|
|
242
|
+
inputSchema: advertisedSchema(zCreateWorkspaceBody),
|
|
243
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
244
|
+
},
|
|
245
|
+
(args) => run(publishq2.workspaces.create(args))
|
|
246
|
+
);
|
|
247
|
+
register(
|
|
248
|
+
{
|
|
249
|
+
name: "publishq_list_media",
|
|
250
|
+
title: "List media",
|
|
251
|
+
description: "List your uploaded and generated media files. Optionally filter by media ID, type, and source.",
|
|
252
|
+
inputSchema: advertisedSchema(zListMediaQuery),
|
|
253
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
254
|
+
},
|
|
255
|
+
(args) => run(publishq2.media.list(args))
|
|
256
|
+
);
|
|
257
|
+
register(
|
|
258
|
+
{
|
|
259
|
+
name: "publishq_get_media",
|
|
260
|
+
title: "Get a media file",
|
|
261
|
+
description: "Get one media file by its ID, including its URL and usage.",
|
|
262
|
+
inputSchema: advertisedSchema(zGetMediaPath),
|
|
263
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
264
|
+
},
|
|
265
|
+
(args) => run(publishq2.media.get(args.id))
|
|
266
|
+
);
|
|
267
|
+
register(
|
|
268
|
+
{
|
|
269
|
+
name: "publishq_create_media_upload",
|
|
270
|
+
title: "Start a media upload",
|
|
271
|
+
description: 'Get a signed upload URL for a file of up to 4 GB on the machine you run commands on, and the media ID it becomes. Send the file with a PUT to uploadUrl, with the same Content-Type and no Authorization header, for example `curl -X PUT -H "Content-Type: image/jpeg" --upload-file ./photo.jpg "<uploadUrl>"`. Then attach the media ID to a post through mediaIds. The URL expires after an hour.',
|
|
272
|
+
inputSchema: advertisedSchema(zCreateMediaUploadBody),
|
|
273
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
274
|
+
},
|
|
275
|
+
(args) => run(publishq2.media.createUpload(args))
|
|
276
|
+
);
|
|
277
|
+
register(
|
|
278
|
+
{
|
|
279
|
+
name: "publishq_delete_media",
|
|
280
|
+
title: "Delete a media file",
|
|
281
|
+
description: "Delete a media file by its ID, from the media library and from storage. Refused while a draft or scheduled post uses it: publish or delete that post first. This cannot be undone.",
|
|
282
|
+
inputSchema: advertisedSchema(zDeleteMediaPath),
|
|
283
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
284
|
+
},
|
|
285
|
+
(args) => run(publishq2.media.delete(args.id))
|
|
286
|
+
);
|
|
287
|
+
return tools;
|
|
288
|
+
}
|
|
289
|
+
function createPublishQServer(tools) {
|
|
290
|
+
const byName = new Map(tools.map((tool) => [tool.definition.name, tool]));
|
|
291
|
+
const server2 = new McpServer(
|
|
292
|
+
{ name: "publishq", version: "0.1.0" },
|
|
293
|
+
{ capabilities: { tools: {} } }
|
|
294
|
+
);
|
|
295
|
+
server2.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
296
|
+
tools: tools.map(({ definition }) => definition)
|
|
297
|
+
}));
|
|
298
|
+
server2.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
299
|
+
const registered = byName.get(request.params.name);
|
|
300
|
+
if (!registered) {
|
|
301
|
+
throw new McpError(
|
|
302
|
+
ErrorCode.InvalidParams,
|
|
303
|
+
`Unknown tool: ${request.params.name}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
return await registered.handler(request.params.arguments ?? {});
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return errorResult(error);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return server2;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/index.ts
|
|
316
|
+
var OPEN_AS_BLOB_MAX_BYTES = 2 ** 32;
|
|
317
|
+
var credential = resolveCredential();
|
|
318
|
+
if (!credential) {
|
|
319
|
+
console.error(
|
|
320
|
+
"ERROR: not signed in. Run `npx @publishq/cli login`, or set the PUBLISHQ_API_KEY environment variable to a key from https://publishq.com/app/api-keys."
|
|
321
|
+
);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
var publishq = new PublishQ({
|
|
325
|
+
apiKey: credential.apiKey,
|
|
326
|
+
...credential.baseUrl ? { baseUrl: credential.baseUrl } : {}
|
|
327
|
+
});
|
|
328
|
+
var server = createPublishQServer([
|
|
329
|
+
...publishqTools(publishq),
|
|
254
330
|
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
331
|
+
definition: {
|
|
332
|
+
name: "publishq_upload_media",
|
|
333
|
+
title: "Upload a media file",
|
|
334
|
+
description: "Upload a local media file of up to 4 GB to PublishQ and get back a media ID you can attach to posts via publishq_create_post (mediaIds). The file is read from the local filesystem where this MCP server runs.",
|
|
335
|
+
inputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
properties: {
|
|
338
|
+
filePath: {
|
|
339
|
+
type: "string",
|
|
340
|
+
description: "Absolute path to the local file to upload"
|
|
341
|
+
},
|
|
342
|
+
filename: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "Override the uploaded file name"
|
|
345
|
+
}
|
|
264
346
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
description: "Override the uploaded file name"
|
|
268
|
-
}
|
|
347
|
+
required: ["filePath"],
|
|
348
|
+
additionalProperties: false
|
|
269
349
|
},
|
|
270
|
-
|
|
271
|
-
additionalProperties: false
|
|
350
|
+
annotations: { readOnlyHint: false, openWorldHint: true }
|
|
272
351
|
},
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
352
|
+
handler: async (args) => {
|
|
353
|
+
const filePath = args.filePath;
|
|
354
|
+
const filename = args.filename;
|
|
355
|
+
if (statSync(filePath).size >= OPEN_AS_BLOB_MAX_BYTES) {
|
|
356
|
+
return {
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: "text",
|
|
360
|
+
text: `${filePath} is too big to upload: files must be smaller than 4 GB.`
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
isError: true
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const file = new File(
|
|
367
|
+
[await openAsBlob(filePath)],
|
|
368
|
+
filename ?? basename(filePath)
|
|
369
|
+
);
|
|
370
|
+
return run(publishq.media.upload({ file }));
|
|
288
371
|
}
|
|
289
|
-
const file = new File(
|
|
290
|
-
[await openAsBlob(filePath)],
|
|
291
|
-
filename ?? basename(filePath)
|
|
292
|
-
);
|
|
293
|
-
return run(publishq.media.upload({ file }));
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
var server = new McpServer(
|
|
297
|
-
{ name: "publishq", version: "0.1.0" },
|
|
298
|
-
{ capabilities: { tools: {} } }
|
|
299
|
-
);
|
|
300
|
-
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
301
|
-
tools: [...tools.values()].map(({ definition }) => definition)
|
|
302
|
-
}));
|
|
303
|
-
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
304
|
-
const registered = tools.get(request.params.name);
|
|
305
|
-
if (!registered) {
|
|
306
|
-
throw new McpError(
|
|
307
|
-
ErrorCode.InvalidParams,
|
|
308
|
-
`Unknown tool: ${request.params.name}`
|
|
309
|
-
);
|
|
310
372
|
}
|
|
311
|
-
|
|
312
|
-
return await registered.handler(request.params.arguments ?? {});
|
|
313
|
-
} catch (error) {
|
|
314
|
-
return errorResult(error);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
373
|
+
]);
|
|
317
374
|
async function main() {
|
|
318
375
|
const transport = new StdioServerTransport();
|
|
319
376
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@publishq/mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for PublishQ. Let AI agents schedule and publish across your social platforms.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
33
33
|
"zod": "4.4.3",
|
|
34
|
-
"@publishq/sdk": "^0.0.
|
|
34
|
+
"@publishq/sdk": "^0.0.7"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^20",
|