omnipost-mcp 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +71 -2
- package/package.json +51 -4
- package/src/bridge.js +190 -0
- package/src/index.js +37 -0
- package/tools-snapshot.json +1436 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OmniGoAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# omnipost-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for [OmniPost](https://omnigoai.com/en/products/omnipost/). It lets Claude Code, Claude Desktop, Cursor, Cline and other MCP clients publish articles, image posts and videos to 40+ platforms: Zhihu, CSDN, WeChat Official Accounts, Xiaohongshu, Bilibili, Weibo, Douyin, Dev.to, Mastodon, Bluesky, WordPress and more.
|
|
4
|
+
|
|
5
|
+
This package does no publishing itself. It forwards your AI client's requests to the OmniPost desktop app running on the same machine, where your platform accounts are logged in. Nothing goes through a cloud server.
|
|
6
|
+
|
|
7
|
+
[中文说明](#中文说明)
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
1. Install the OmniPost desktop app (Windows / macOS) from https://omnigoai.com/en/download/omnipost/ and keep it open.
|
|
12
|
+
2. Log in to the platforms you want to publish to inside the app.
|
|
13
|
+
3. Node.js 18.17 or newer.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Claude Code:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
claude mcp add omnipost -- npx -y omnipost-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Claude Desktop, Cursor, Cline and other clients (MCP config file):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"omnipost": { "command": "npx", "args": ["-y", "omnipost-mcp"] }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then ask your agent something like: "Publish this Markdown to Zhihu and CSDN, and put it in my WeChat Official Account drafts."
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
- Finds the running app through `~/.omnipost/server.json` (falls back to port 8787) and connects to its built-in MCP endpoint `http://127.0.0.1:<port>/sse`.
|
|
38
|
+
- Tool list and tool calls are forwarded unchanged, so you always get the tools of the app version you have installed.
|
|
39
|
+
- If the app is not running, the tools are still listed from a bundled snapshot, and each call returns a short "install / open OmniPost" message instead of a raw connection error.
|
|
40
|
+
- Long calls (for example a large video upload) are allowed to run for up to 6 hours without being cut off.
|
|
41
|
+
- Plan limits are enforced by the app, not by this package.
|
|
42
|
+
|
|
43
|
+
Environment variables:
|
|
44
|
+
|
|
45
|
+
- `OMNIPOST_MCP_URL`: use a specific endpoint, e.g. `http://127.0.0.1:8787/sse`.
|
|
46
|
+
- `OMNIPOST_DATA_DIR`: where to look for `server.json` (default `~/.omnipost`).
|
|
47
|
+
|
|
48
|
+
You can also connect to the app directly without this package if your client supports SSE: `claude mcp add --transport sse omnipost http://127.0.0.1:8787/sse`.
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install
|
|
54
|
+
npm test # bridge tests against a fake app
|
|
55
|
+
npm run snapshot # refresh tools-snapshot.json from the app running on this machine
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 中文说明
|
|
59
|
+
|
|
60
|
+
`omnipost-mcp` 让 Claude Code、Claude Desktop、Cursor 等 AI 工具通过标准 MCP 协议调用 OmniPost,把写好的内容发到知乎、CSDN、公众号、小红书、B 站等 40 多个平台。
|
|
61
|
+
|
|
62
|
+
它本身不发布,只把请求转给你电脑上正在运行的 OmniPost 桌面应用,账号登录都在应用里,不经过任何云端。
|
|
63
|
+
|
|
64
|
+
1. 安装并打开 OmniPost:https://omnigoai.com/zh/download/omnipost/ ,在应用里登录要发的平台。
|
|
65
|
+
2. Claude Code 里运行:`claude mcp add omnipost -- npx -y omnipost-mcp`。其它客户端按上面的配置文件写法添加。
|
|
66
|
+
3. 对 AI 说「把这篇发到知乎和 CSDN,公众号进草稿箱」。
|
|
67
|
+
|
|
68
|
+
应用没开时,AI 工具仍能看到工具列表,调用时会提示先安装并打开 OmniPost。
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnipost-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "MCP server for OmniPost: lets Claude Code, Claude Desktop, Cursor and other MCP clients publish articles, image posts and videos to 40+ Chinese and overseas platforms through the OmniPost desktop app running on your machine.",
|
|
5
|
+
"mcpName": "io.github.OmniGoAI/omnipost",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"omnipost-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"tools-snapshot.json",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node src/index.js",
|
|
19
|
+
"test": "node --test \"test/*.test.js\"",
|
|
20
|
+
"snapshot": "node scripts/snapshot.mjs"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.17"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"undici": "^6.21.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"omnipost",
|
|
33
|
+
"claude",
|
|
34
|
+
"publishing",
|
|
35
|
+
"xiaohongshu",
|
|
36
|
+
"wechat",
|
|
37
|
+
"zhihu",
|
|
38
|
+
"csdn",
|
|
39
|
+
"bilibili",
|
|
40
|
+
"multi-platform"
|
|
41
|
+
],
|
|
42
|
+
"homepage": "https://github.com/OmniGoAI/omnipost-skill/tree/main/mcp",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/OmniGoAI/omnipost-skill.git",
|
|
46
|
+
"directory": "mcp"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/OmniGoAI/omnipost-skill/issues"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"author": "OmniGoAI"
|
|
53
|
+
}
|
package/src/bridge.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// OmniPost MCP bridge.
|
|
2
|
+
//
|
|
3
|
+
// A standard MCP server that does no work itself: it forwards tool listing and tool calls to the
|
|
4
|
+
// OmniPost desktop app running on the same machine (its built-in MCP endpoint, http://127.0.0.1:<port>/sse).
|
|
5
|
+
// When the app is not running, tools are still listed from a bundled snapshot so clients and
|
|
6
|
+
// directories can introspect the server, and every call returns a clear "install / open OmniPost" hint.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
12
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import { Agent, fetch as undiciFetch } from 'undici';
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_PORT = 8787;
|
|
17
|
+
export const DOWNLOAD_URL = 'https://omnigoai.com/en/download/omnipost/';
|
|
18
|
+
|
|
19
|
+
// Publishing a large video can keep a tool call open for a long time. Keep this in line with the
|
|
20
|
+
// OmniPost CLI (6 hours). A client-side timeout here would make the agent think the publish failed
|
|
21
|
+
// and retry it, uploading the same content twice.
|
|
22
|
+
export const CALL_TIMEOUT_MS = 6 * 3600 * 1000;
|
|
23
|
+
|
|
24
|
+
// Number of tool calls currently in progress. The process waits for these before exiting, so a
|
|
25
|
+
// client that closes stdin right after sending a request still gets its answer.
|
|
26
|
+
let inFlightCalls = 0;
|
|
27
|
+
export const inFlight = () => inFlightCalls;
|
|
28
|
+
|
|
29
|
+
// Node's built-in fetch drops a response body that stays idle for 5 minutes. The app's SSE stream
|
|
30
|
+
// is silent while a long publish is running, so use a dispatcher without body/header timeouts.
|
|
31
|
+
const longLivedAgent = new Agent({ bodyTimeout: 0, headersTimeout: 0, keepAliveTimeout: 60_000 });
|
|
32
|
+
const longLivedFetch = (url, init = {}) => undiciFetch(url, { ...init, dispatcher: longLivedAgent });
|
|
33
|
+
|
|
34
|
+
/** Where the running app's MCP endpoint is. Order: OMNIPOST_MCP_URL > ~/.omnipost/server.json > default port. */
|
|
35
|
+
export function resolveEndpoint({ env = process.env, home = os.homedir() } = {}) {
|
|
36
|
+
if (env.OMNIPOST_MCP_URL) return env.OMNIPOST_MCP_URL;
|
|
37
|
+
const dataDir = env.OMNIPOST_DATA_DIR || path.join(home, '.omnipost');
|
|
38
|
+
let port = DEFAULT_PORT;
|
|
39
|
+
try {
|
|
40
|
+
const info = JSON.parse(fs.readFileSync(path.join(dataDir, 'server.json'), 'utf8'));
|
|
41
|
+
if (Number.isInteger(info?.port) && info.port > 0) port = info.port;
|
|
42
|
+
} catch {
|
|
43
|
+
// no server.json yet: fall back to the default port
|
|
44
|
+
}
|
|
45
|
+
return `http://127.0.0.1:${port}/sse`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadSnapshot(file = new URL('../tools-snapshot.json', import.meta.url)) {
|
|
49
|
+
try {
|
|
50
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
51
|
+
return Array.isArray(data?.tools) ? data.tools : [];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function notRunningResult(endpoint, cause) {
|
|
58
|
+
const text = [
|
|
59
|
+
'OmniPost desktop app is not reachable, so this tool could not run.',
|
|
60
|
+
`Tried: ${endpoint}${cause ? ` (${cause})` : ''}`,
|
|
61
|
+
'',
|
|
62
|
+
'To fix: install OmniPost and keep it open, then log in to the platforms you want to publish to inside the app.',
|
|
63
|
+
`Download: ${DOWNLOAD_URL}`,
|
|
64
|
+
'',
|
|
65
|
+
'未检测到正在运行的 OmniPost 桌面应用,工具无法执行。请先安装并打开 OmniPost,在应用里登录要发布的平台账号后重试。',
|
|
66
|
+
].join('\n');
|
|
67
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Connection to the desktop app, created lazily and recreated after a failure, so the bridge picks
|
|
72
|
+
* the app up whenever the user opens or restarts it.
|
|
73
|
+
*/
|
|
74
|
+
export class AppConnection {
|
|
75
|
+
constructor({ resolve = () => resolveEndpoint(), connectTimeoutMs = 5000, transportFactory } = {}) {
|
|
76
|
+
this.resolve = resolve;
|
|
77
|
+
this.connectTimeoutMs = connectTimeoutMs;
|
|
78
|
+
this.transportFactory = transportFactory || ((url) => new SSEClientTransport(new URL(url), {
|
|
79
|
+
fetch: longLivedFetch,
|
|
80
|
+
eventSourceInit: { fetch: longLivedFetch },
|
|
81
|
+
}));
|
|
82
|
+
this.client = null;
|
|
83
|
+
this.endpoint = null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async get() {
|
|
87
|
+
if (this.client) return this.client;
|
|
88
|
+
const endpoint = this.resolve();
|
|
89
|
+
const client = new Client({ name: 'omnipost-mcp-bridge', version: '0.1.1' });
|
|
90
|
+
client.onclose = () => { if (this.client === client) this.client = null; };
|
|
91
|
+
const connecting = client.connect(this.transportFactory(endpoint));
|
|
92
|
+
let timer;
|
|
93
|
+
const timeout = new Promise((_, reject) => {
|
|
94
|
+
timer = setTimeout(() => reject(new Error(`connect timed out after ${this.connectTimeoutMs} ms`)), this.connectTimeoutMs);
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
await Promise.race([connecting, timeout]);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
try { await client.close(); } catch { /* ignore */ }
|
|
100
|
+
throw Object.assign(err instanceof Error ? err : new Error(String(err)), { endpoint });
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
this.client = client;
|
|
105
|
+
this.endpoint = endpoint;
|
|
106
|
+
return client;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async reset() {
|
|
110
|
+
const c = this.client;
|
|
111
|
+
this.client = null;
|
|
112
|
+
if (c) { try { await c.close(); } catch { /* ignore */ } }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function shortCause(err) {
|
|
117
|
+
return String(err?.cause?.code || err?.code || err?.message || err).slice(0, 160);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createBridgeServer({ connection = new AppConnection(), snapshot = loadSnapshot(), version = '0.1.1' } = {}) {
|
|
121
|
+
const server = new Server(
|
|
122
|
+
{ name: 'omnipost', version },
|
|
123
|
+
{
|
|
124
|
+
capabilities: { tools: {} },
|
|
125
|
+
instructions:
|
|
126
|
+
'OmniPost publishes content you have already written to 40+ platforms (Zhihu, CSDN, WeChat Official Accounts, ' +
|
|
127
|
+
'Xiaohongshu, Bilibili, Dev.to, Mastodon, Bluesky, WordPress...). It needs the OmniPost desktop app running on ' +
|
|
128
|
+
'this machine with the target accounts logged in. Call list_alerts before publishing; most Chinese platforms ' +
|
|
129
|
+
'create drafts by default.',
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
134
|
+
try {
|
|
135
|
+
const client = await connection.get();
|
|
136
|
+
return await client.listTools();
|
|
137
|
+
} catch {
|
|
138
|
+
await connection.reset();
|
|
139
|
+
return { tools: snapshot };
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
144
|
+
inFlightCalls += 1;
|
|
145
|
+
try {
|
|
146
|
+
return await handleCall(request);
|
|
147
|
+
} finally {
|
|
148
|
+
inFlightCalls -= 1;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
async function handleCall(request) {
|
|
153
|
+
const { name, arguments: args } = request.params;
|
|
154
|
+
let client;
|
|
155
|
+
try {
|
|
156
|
+
client = await connection.get();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
await connection.reset();
|
|
159
|
+
return notRunningResult(err?.endpoint || connection.resolve(), shortCause(err));
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return await client.callTool({ name, arguments: args || {} }, undefined, {
|
|
163
|
+
timeout: CALL_TIMEOUT_MS,
|
|
164
|
+
maxTotalTimeout: CALL_TIMEOUT_MS,
|
|
165
|
+
});
|
|
166
|
+
} catch (err) {
|
|
167
|
+
// The link dropped mid-call (app closed or restarted). Do not retry automatically: the call may
|
|
168
|
+
// already have published something, and a blind retry could publish it twice.
|
|
169
|
+
await connection.reset();
|
|
170
|
+
return {
|
|
171
|
+
content: [{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: `Lost the connection to the OmniPost app during "${name}" (${shortCause(err)}). ` +
|
|
174
|
+
'It may or may not have completed. Check list_posts before retrying so nothing is published twice.',
|
|
175
|
+
}],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// When the AI client goes away, drop the link to the app too, so nothing is left open.
|
|
182
|
+
server.onclose = () => { if (inFlightCalls === 0) connection.reset(); };
|
|
183
|
+
|
|
184
|
+
return server;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Close pooled sockets so the process can exit promptly (used on shutdown and in tests). */
|
|
188
|
+
export async function shutdown() {
|
|
189
|
+
try { await longLivedAgent.close(); } catch { /* ignore */ }
|
|
190
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// omnipost-mcp: stdio MCP server that forwards to the OmniPost desktop app on this machine.
|
|
3
|
+
// Claude Code: claude mcp add omnipost -- npx -y omnipost-mcp
|
|
4
|
+
// Other clients: { "command": "npx", "args": ["-y", "omnipost-mcp"] }
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { createBridgeServer, resolveEndpoint, shutdown, inFlight } from './bridge.js';
|
|
7
|
+
|
|
8
|
+
let exiting = false;
|
|
9
|
+
async function exit(code = 0, { immediate = false } = {}) {
|
|
10
|
+
if (exiting) return;
|
|
11
|
+
exiting = true;
|
|
12
|
+
// Let calls that are already running finish and send their answers first. The short pause lets a
|
|
13
|
+
// request that arrived just before stdin closed get registered as in flight.
|
|
14
|
+
if (!immediate) await new Promise((r) => setTimeout(r, 300));
|
|
15
|
+
while (!immediate && inFlight() > 0) await new Promise((r) => setTimeout(r, 200));
|
|
16
|
+
await shutdown();
|
|
17
|
+
process.exit(code);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const server = createBridgeServer();
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
const prev = server.onclose;
|
|
24
|
+
server.onclose = () => { prev?.(); exit(0); };
|
|
25
|
+
await server.connect(transport);
|
|
26
|
+
// The client closing our stdin means it is done with us.
|
|
27
|
+
process.stdin.on('end', () => exit(0));
|
|
28
|
+
process.stdin.on('close', () => exit(0));
|
|
29
|
+
process.on('SIGINT', () => exit(0, { immediate: true }));
|
|
30
|
+
process.on('SIGTERM', () => exit(0, { immediate: true }));
|
|
31
|
+
process.stderr.write(`[omnipost-mcp] ready, forwarding to ${resolveEndpoint()}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main().catch((err) => {
|
|
35
|
+
process.stderr.write(`[omnipost-mcp] failed to start: ${err?.stack || err}\n`);
|
|
36
|
+
exit(1, { immediate: true });
|
|
37
|
+
});
|