ihateposting-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/tools.d.ts +21 -0
- package/dist/tools.js +62 -0
- package/package.json +35 -0
- package/src/index.ts +28 -0
- package/src/tools.ts +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ihateposting-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [iHatePosting](https://ihateposting.com) — draft, schedule, and inspect
|
|
4
|
+
social posts from Claude, Cursor, ChatGPT, or any MCP client.
|
|
5
|
+
|
|
6
|
+
Write once, publish everywhere: iHatePosting cross-posts to Bluesky, X, LinkedIn,
|
|
7
|
+
Facebook, Instagram, Threads, Mastodon, Telegram, Discord, Tumblr, Pinterest,
|
|
8
|
+
YouTube and more.
|
|
9
|
+
|
|
10
|
+
## Get an API key
|
|
11
|
+
|
|
12
|
+
iHatePosting → **Settings → API & MCP** → create a key (`pk_live_…`).
|
|
13
|
+
Treat it like a password: it can post on your behalf.
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | What it does |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `create_post` | Create a post — publish now, schedule, or save as a draft |
|
|
20
|
+
| `list_posts` | The 50 most recent posts with per-platform status + live URLs |
|
|
21
|
+
| `list_accounts` | Your connected social accounts and their health |
|
|
22
|
+
|
|
23
|
+
## Use it
|
|
24
|
+
|
|
25
|
+
### Claude Desktop / Claude Code / Cursor (local, stdio)
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"ihateposting": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "ihateposting-mcp"],
|
|
33
|
+
"env": { "IHATEPOSTING_API_KEY": "pk_live_…" }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ChatGPT and other remote clients (hosted, HTTP)
|
|
40
|
+
|
|
41
|
+
ChatGPT can't run a local process, so point it at the hosted endpoint instead:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
https://ihateposting.com/api/mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Authenticate with a header: `Authorization: Bearer pk_live_…`
|
|
48
|
+
|
|
49
|
+
## Environment
|
|
50
|
+
|
|
51
|
+
| Variable | Default | Notes |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `IHATEPOSTING_API_KEY` | — | Required (stdio). Your `pk_live_…` key. |
|
|
54
|
+
| `IHATEPOSTING_API_URL` | `https://ihateposting.com` | Override for self-hosted/staging. |
|
|
55
|
+
|
|
56
|
+
## Notes
|
|
57
|
+
|
|
58
|
+
- Instagram, Pinterest and YouTube require media, which these tools can't attach —
|
|
59
|
+
save a draft and finish it in the composer.
|
|
60
|
+
- Every call is scoped to the key's owner; the server holds no state of its own.
|
|
61
|
+
|
|
62
|
+
MIT © iHatePosting
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { makeApiCaller, registerTools, MCP_NAME, MCP_VERSION } from "./tools.js";
|
|
5
|
+
/**
|
|
6
|
+
* iHatePosting MCP server — STDIO transport (plan §6.1 item 74, Pro).
|
|
7
|
+
* For clients that spawn a local process: Claude Desktop, Claude Code, Cursor.
|
|
8
|
+
* ChatGPT cannot use this (it only accepts a remote URL) — point ChatGPT at
|
|
9
|
+
* the hosted HTTP endpoint instead: https://ihateposting.com/api/mcp
|
|
10
|
+
*
|
|
11
|
+
* Config (env):
|
|
12
|
+
* IHATEPOSTING_API_KEY pk_live_… from iHatePosting → Settings → API & MCP (required)
|
|
13
|
+
* IHATEPOSTING_API_URL defaults to https://ihateposting.com
|
|
14
|
+
*
|
|
15
|
+
* Claude Desktop / Claude Code config:
|
|
16
|
+
* { "mcpServers": { "ihateposting": {
|
|
17
|
+
* "command": "npx", "args": ["-y", "ihateposting-mcp"],
|
|
18
|
+
* "env": { "IHATEPOSTING_API_KEY": "pk_live_…" } } } }
|
|
19
|
+
*/
|
|
20
|
+
const API_URL = process.env.IHATEPOSTING_API_URL ?? "https://ihateposting.com";
|
|
21
|
+
const API_KEY = process.env.IHATEPOSTING_API_KEY;
|
|
22
|
+
const server = new McpServer({ name: MCP_NAME, version: MCP_VERSION });
|
|
23
|
+
registerTools(server, makeApiCaller(API_URL, API_KEY));
|
|
24
|
+
await server.connect(new StdioServerTransport());
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* The iHatePosting MCP tool set, shared by BOTH transports:
|
|
4
|
+
* - stdio (packages/mcp/src/index.ts) → npx ihateposting-mcp, for
|
|
5
|
+
* Claude Desktop / Claude Code / Cursor, which spawn a local process.
|
|
6
|
+
* - HTTP (apps/web/src/app/api/mcp/route.ts) → https://ihateposting.com/api/mcp,
|
|
7
|
+
* for ChatGPT and any other client that can only reach a remote URL.
|
|
8
|
+
*
|
|
9
|
+
* Both transports call the same public REST API with the caller's API key, so
|
|
10
|
+
* the tools stay identical and there is exactly one place to add a new one.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MCP_NAME = "ihateposting";
|
|
13
|
+
export declare const MCP_VERSION = "0.2.0";
|
|
14
|
+
/** Platforms the text-only create_post tool can target. */
|
|
15
|
+
export declare const MCP_PLATFORMS: readonly ["bluesky", "x", "linkedin", "facebook", "threads", "mastodon", "telegram", "discord", "tumblr"];
|
|
16
|
+
/** Calls the iHatePosting REST API with the caller's key. Never throws — every
|
|
17
|
+
* failure is returned as JSON text so the model can explain it to the user. */
|
|
18
|
+
export type ApiCaller = (path: string, init?: RequestInit) => Promise<string>;
|
|
19
|
+
export declare function makeApiCaller(apiUrl: string, apiKey: string | undefined): ApiCaller;
|
|
20
|
+
/** Registers every iHatePosting tool on a server instance. */
|
|
21
|
+
export declare function registerTools(server: McpServer, api: ApiCaller): void;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The iHatePosting MCP tool set, shared by BOTH transports:
|
|
4
|
+
* - stdio (packages/mcp/src/index.ts) → npx ihateposting-mcp, for
|
|
5
|
+
* Claude Desktop / Claude Code / Cursor, which spawn a local process.
|
|
6
|
+
* - HTTP (apps/web/src/app/api/mcp/route.ts) → https://ihateposting.com/api/mcp,
|
|
7
|
+
* for ChatGPT and any other client that can only reach a remote URL.
|
|
8
|
+
*
|
|
9
|
+
* Both transports call the same public REST API with the caller's API key, so
|
|
10
|
+
* the tools stay identical and there is exactly one place to add a new one.
|
|
11
|
+
*/
|
|
12
|
+
export const MCP_NAME = "ihateposting";
|
|
13
|
+
export const MCP_VERSION = "0.2.0";
|
|
14
|
+
/** Platforms the text-only create_post tool can target. */
|
|
15
|
+
export const MCP_PLATFORMS = [
|
|
16
|
+
"bluesky",
|
|
17
|
+
"x",
|
|
18
|
+
"linkedin",
|
|
19
|
+
"facebook",
|
|
20
|
+
"threads",
|
|
21
|
+
"mastodon",
|
|
22
|
+
"telegram",
|
|
23
|
+
"discord",
|
|
24
|
+
"tumblr",
|
|
25
|
+
];
|
|
26
|
+
export function makeApiCaller(apiUrl, apiKey) {
|
|
27
|
+
const base = apiUrl.replace(/\/$/, "");
|
|
28
|
+
return async (path, init) => {
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
return JSON.stringify({
|
|
31
|
+
error: "No API key — set IHATEPOSTING_API_KEY (stdio) or send Authorization: Bearer <key> (HTTP). Create one in iHatePosting → Settings → API & MCP.",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${base}${path}`, {
|
|
36
|
+
...init,
|
|
37
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", ...init?.headers },
|
|
38
|
+
});
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
return JSON.stringify({ error: `iHatePosting API ${res.status}`, detail: text.slice(0, 500) });
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return JSON.stringify({ error: `Couldn't reach iHatePosting: ${err instanceof Error ? err.message : String(err)}` });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Registers every iHatePosting tool on a server instance. */
|
|
50
|
+
export function registerTools(server, api) {
|
|
51
|
+
server.tool("create_post", "Create a social post on iHatePosting. action 'now' publishes immediately to every connected account of the named platforms; 'schedule' needs scheduledDate (YYYY-MM-DD) + scheduledTime (e.g. '9:00 AM', in the account owner's timezone); 'draft' just saves it. Note: Instagram/Pinterest/YouTube need media, which this tool can't attach — save a draft for those and finish in the composer.", {
|
|
52
|
+
text: z.string().min(1).describe("The post text"),
|
|
53
|
+
platforms: z.array(z.enum(MCP_PLATFORMS)).min(1).describe("Platforms to post to"),
|
|
54
|
+
action: z.enum(["now", "schedule", "draft"]).default("draft"),
|
|
55
|
+
scheduledDate: z.string().optional().describe("YYYY-MM-DD (schedule only)"),
|
|
56
|
+
scheduledTime: z.string().optional().describe("e.g. '9:00 AM' (schedule only)"),
|
|
57
|
+
}, async (input) => ({
|
|
58
|
+
content: [{ type: "text", text: await api("/api/v1/posts", { method: "POST", body: JSON.stringify(input) }) }],
|
|
59
|
+
}));
|
|
60
|
+
server.tool("list_posts", "List the 50 most recent iHatePosting posts with per-platform status (draft/scheduled/published/failed) and live URLs.", {}, async () => ({ content: [{ type: "text", text: await api("/api/v1/posts") }] }));
|
|
61
|
+
server.tool("list_accounts", "List the connected social accounts (platform, handle, health) available for posting.", {}, async () => ({ content: [{ type: "text", text: await api("/api/v1/accounts") }] }));
|
|
62
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ihateposting-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for iHatePosting — draft, schedule, and inspect social posts from Claude, Cursor, or any MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://ihateposting.com",
|
|
8
|
+
"keywords": ["mcp", "modelcontextprotocol", "social-media", "scheduler", "ihateposting"],
|
|
9
|
+
"bin": {
|
|
10
|
+
"ihateposting-mcp": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./tools": "./src/tools.ts"
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "src", "README.md"],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "tsx src/index.ts",
|
|
22
|
+
"build": "tsc -p tsconfig.build.json",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"zod": "^4.4.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.1.0",
|
|
32
|
+
"tsx": "^4.0.0",
|
|
33
|
+
"typescript": "^6.0.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { makeApiCaller, registerTools, MCP_NAME, MCP_VERSION } from "./tools.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* iHatePosting MCP server — STDIO transport (plan §6.1 item 74, Pro).
|
|
8
|
+
* For clients that spawn a local process: Claude Desktop, Claude Code, Cursor.
|
|
9
|
+
* ChatGPT cannot use this (it only accepts a remote URL) — point ChatGPT at
|
|
10
|
+
* the hosted HTTP endpoint instead: https://ihateposting.com/api/mcp
|
|
11
|
+
*
|
|
12
|
+
* Config (env):
|
|
13
|
+
* IHATEPOSTING_API_KEY pk_live_… from iHatePosting → Settings → API & MCP (required)
|
|
14
|
+
* IHATEPOSTING_API_URL defaults to https://ihateposting.com
|
|
15
|
+
*
|
|
16
|
+
* Claude Desktop / Claude Code config:
|
|
17
|
+
* { "mcpServers": { "ihateposting": {
|
|
18
|
+
* "command": "npx", "args": ["-y", "ihateposting-mcp"],
|
|
19
|
+
* "env": { "IHATEPOSTING_API_KEY": "pk_live_…" } } } }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const API_URL = process.env.IHATEPOSTING_API_URL ?? "https://ihateposting.com";
|
|
23
|
+
const API_KEY = process.env.IHATEPOSTING_API_KEY;
|
|
24
|
+
|
|
25
|
+
const server = new McpServer({ name: MCP_NAME, version: MCP_VERSION });
|
|
26
|
+
registerTools(server, makeApiCaller(API_URL, API_KEY));
|
|
27
|
+
|
|
28
|
+
await server.connect(new StdioServerTransport());
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The iHatePosting MCP tool set, shared by BOTH transports:
|
|
6
|
+
* - stdio (packages/mcp/src/index.ts) → npx ihateposting-mcp, for
|
|
7
|
+
* Claude Desktop / Claude Code / Cursor, which spawn a local process.
|
|
8
|
+
* - HTTP (apps/web/src/app/api/mcp/route.ts) → https://ihateposting.com/api/mcp,
|
|
9
|
+
* for ChatGPT and any other client that can only reach a remote URL.
|
|
10
|
+
*
|
|
11
|
+
* Both transports call the same public REST API with the caller's API key, so
|
|
12
|
+
* the tools stay identical and there is exactly one place to add a new one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const MCP_NAME = "ihateposting";
|
|
16
|
+
export const MCP_VERSION = "0.2.0";
|
|
17
|
+
|
|
18
|
+
/** Platforms the text-only create_post tool can target. */
|
|
19
|
+
export const MCP_PLATFORMS = [
|
|
20
|
+
"bluesky",
|
|
21
|
+
"x",
|
|
22
|
+
"linkedin",
|
|
23
|
+
"facebook",
|
|
24
|
+
"threads",
|
|
25
|
+
"mastodon",
|
|
26
|
+
"telegram",
|
|
27
|
+
"discord",
|
|
28
|
+
"tumblr",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/** Calls the iHatePosting REST API with the caller's key. Never throws — every
|
|
32
|
+
* failure is returned as JSON text so the model can explain it to the user. */
|
|
33
|
+
export type ApiCaller = (path: string, init?: RequestInit) => Promise<string>;
|
|
34
|
+
|
|
35
|
+
export function makeApiCaller(apiUrl: string, apiKey: string | undefined): ApiCaller {
|
|
36
|
+
const base = apiUrl.replace(/\/$/, "");
|
|
37
|
+
return async (path, init) => {
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
error: "No API key — set IHATEPOSTING_API_KEY (stdio) or send Authorization: Bearer <key> (HTTP). Create one in iHatePosting → Settings → API & MCP.",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(`${base}${path}`, {
|
|
45
|
+
...init,
|
|
46
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", ...init?.headers },
|
|
47
|
+
});
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
if (!res.ok) return JSON.stringify({ error: `iHatePosting API ${res.status}`, detail: text.slice(0, 500) });
|
|
50
|
+
return text;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return JSON.stringify({ error: `Couldn't reach iHatePosting: ${err instanceof Error ? err.message : String(err)}` });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registers every iHatePosting tool on a server instance. */
|
|
58
|
+
export function registerTools(server: McpServer, api: ApiCaller): void {
|
|
59
|
+
server.tool(
|
|
60
|
+
"create_post",
|
|
61
|
+
"Create a social post on iHatePosting. action 'now' publishes immediately to every connected account of the named platforms; 'schedule' needs scheduledDate (YYYY-MM-DD) + scheduledTime (e.g. '9:00 AM', in the account owner's timezone); 'draft' just saves it. Note: Instagram/Pinterest/YouTube need media, which this tool can't attach — save a draft for those and finish in the composer.",
|
|
62
|
+
{
|
|
63
|
+
text: z.string().min(1).describe("The post text"),
|
|
64
|
+
platforms: z.array(z.enum(MCP_PLATFORMS)).min(1).describe("Platforms to post to"),
|
|
65
|
+
action: z.enum(["now", "schedule", "draft"]).default("draft"),
|
|
66
|
+
scheduledDate: z.string().optional().describe("YYYY-MM-DD (schedule only)"),
|
|
67
|
+
scheduledTime: z.string().optional().describe("e.g. '9:00 AM' (schedule only)"),
|
|
68
|
+
},
|
|
69
|
+
async (input) => ({
|
|
70
|
+
content: [{ type: "text" as const, text: await api("/api/v1/posts", { method: "POST", body: JSON.stringify(input) }) }],
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
server.tool(
|
|
75
|
+
"list_posts",
|
|
76
|
+
"List the 50 most recent iHatePosting posts with per-platform status (draft/scheduled/published/failed) and live URLs.",
|
|
77
|
+
{},
|
|
78
|
+
async () => ({ content: [{ type: "text" as const, text: await api("/api/v1/posts") }] }),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
server.tool(
|
|
82
|
+
"list_accounts",
|
|
83
|
+
"List the connected social accounts (platform, handle, health) available for posting.",
|
|
84
|
+
{},
|
|
85
|
+
async () => ({ content: [{ type: "text" as const, text: await api("/api/v1/accounts") }] }),
|
|
86
|
+
);
|
|
87
|
+
}
|