writedock-mcp 0.4.13 → 0.4.15

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.
@@ -4,7 +4,7 @@
4
4
  "name": "写作台 WriteDock",
5
5
  "short_name": "写作台",
6
6
  "description": "连接自己的 AI agent,通过 MCP 发布小红书长文、图文和视频,微博图文、视频及长文章,知乎、百家号、B站专栏文章与抖音图文视频。",
7
- "version": "0.4.13",
7
+ "version": "0.4.15",
8
8
  "minimum_chrome_version": "116",
9
9
  "icons": {
10
10
  "16": "assets/icon-16.png",
@@ -1 +1 @@
1
- import './assets/index.ts-BeiBzFaR.js';
1
+ import './assets/index.ts-Ch2Bzedq.js';
@@ -1,4 +1,4 @@
1
- WriteDock 安装与更新 v0.4.13
1
+ WriteDock 安装与更新 v0.4.15
2
2
 
3
3
  首次使用
4
4
  1. 把这句话发给你的 agent:帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。
package/index.cjs CHANGED
@@ -29740,7 +29740,7 @@ var MediaStore = class {
29740
29740
  // package.json
29741
29741
  var package_default = {
29742
29742
  name: "@writedock/mcp-server",
29743
- version: "0.4.13",
29743
+ version: "0.4.15",
29744
29744
  private: true,
29745
29745
  type: "module",
29746
29746
  description: "WriteDock local MCP server for the browser extension",
@@ -30029,7 +30029,8 @@ var LocalBridge = class {
30029
30029
  }
30030
30030
  connectionFields() {
30031
30031
  const connected = this.client?.readyState === import_websocket.default.OPEN, notice = this.notice();
30032
- return { connected, extensionId: this.extensionId, ...this.extensionVersion ? { extension_version: this.extensionVersion } : {}, ...notice ? { update_notice: notice } : {}, protocol: "writedock/1", ...mcpReleaseInfo() };
30032
+ const { extension_bundle_version: _bundled, ...release } = mcpReleaseInfo();
30033
+ return { connected, extensionId: this.extensionId, ...this.extensionVersion ? { extension_version: this.extensionVersion } : {}, ...notice ? { update_notice: notice } : {}, protocol: "writedock/1", ...release };
30033
30034
  }
30034
30035
  /** How long nothing has needed this service. An open extension connection counts as needed. */
30035
30036
  idleFor() {
@@ -30235,12 +30236,28 @@ var LocalBridge = class {
30235
30236
  if (!(error2 instanceof TypeError)) throw error2;
30236
30237
  this.starting = void 0;
30237
30238
  await this.start();
30238
- if (this.primary) return { connected: false, protocol: "writedock/1", ...mcpReleaseInfo() };
30239
+ if (this.primary) return this.connectionFields();
30239
30240
  return this.httpCall("/status");
30240
30241
  }
30241
30242
  }
30243
+ /**
30244
+ * The service may have only just started, for example on the first publication after a reboot, and the extension
30245
+ * then needs a moment to connect. Nothing has been sent yet, so waiting here cannot submit anything twice.
30246
+ */
30247
+ async waitForExtension() {
30248
+ const end = Date.now() + (this.options.extensionWaitMs ?? 45e3);
30249
+ while (Date.now() < end) {
30250
+ try {
30251
+ if (this.primary ? this.client?.readyState === import_websocket.default.OPEN : (await this.httpCall("/status")).connected === true) return;
30252
+ } catch {
30253
+ return;
30254
+ }
30255
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
30256
+ }
30257
+ }
30242
30258
  async request(method, params = {}) {
30243
30259
  await this.start();
30260
+ await this.waitForExtension();
30244
30261
  if (this.primary) return this.requestConnected(method, params);
30245
30262
  try {
30246
30263
  return (await this.httpCall("/request", { method, params })).result;
@@ -50394,7 +50411,7 @@ function createMcpServer(bridge) {
50394
50411
  })).catch((error2) => ({ content: [{ type: "text", text: error2 instanceof Error ? error2.message : String(error2) }], isError: true }));
50395
50412
  const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
50396
50413
  server.registerTool("get_connection_status", {
50397
- description: "\u68C0\u67E5\u672C\u673A MCP \u4E0E\u6269\u5C55\u7684\u8FDE\u63A5\u3001\u8FD0\u884C\u7248\u672C\u548C npm \u66F4\u65B0\u63D0\u793A\u3002update_available=true \u65F6\u544A\u77E5\u7528\u6237\u91CD\u65B0\u542F\u52A8 WriteDock MCP\uFF0C\u4FDD\u7559\u914D\u7F6E\uFF1B\u53D1\u5E03\u4EFB\u52A1\u8FDB\u884C\u4E2D\u4E0D\u8981\u81EA\u52A8\u91CD\u542F\u3002\u6709 update_notice \u65F6\u8F6C\u544A\u7528\u6237\u3002extension_bundle_version \u53EA\u662F npm \u5305\u5185\u7F6E\u7684\u6269\u5C55\u7248\u672C\uFF0Cextension_version \u624D\u662F\u7528\u6237\u6B63\u5728\u7528\u7684\u3002\u65E7\u7248\u670D\u52A1\u53EF\u80FD\u4E0D\u8FD4\u56DE\u7248\u672C\u5B57\u6BB5\uFF1Blatest_version \u7F3A\u5931\u6216 update_checking=true \u65F6\uFF0C\u4E0D\u5F97\u58F0\u79F0\u5DF2\u662F\u6700\u65B0\u7248\u3002",
50414
+ description: "\u68C0\u67E5\u672C\u673A MCP \u4E0E\u6269\u5C55\u7684\u8FDE\u63A5\u3001\u8FD0\u884C\u7248\u672C\u548C npm \u66F4\u65B0\u63D0\u793A\u3002update_available=true \u65F6\u544A\u77E5\u7528\u6237\u91CD\u65B0\u542F\u52A8 WriteDock MCP\uFF0C\u4FDD\u7559\u914D\u7F6E\uFF1B\u53D1\u5E03\u4EFB\u52A1\u8FDB\u884C\u4E2D\u4E0D\u8981\u81EA\u52A8\u91CD\u542F\u3002\u6709 update_notice \u65F6\u8F6C\u544A\u7528\u6237\u3002extension_version \u662F\u7528\u6237\u6B63\u5728\u7528\u7684\u6269\u5C55\u7248\u672C\uFF1B\u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u8868\u793A\u7248\u672C\u672A\u77E5\uFF0C\u4E0D\u5F97\u58F0\u79F0\u6269\u5C55\u4E0E MCP \u7248\u672C\u4E00\u81F4\u3002\u914D\u7F6E\u91CC\u7684 writedock-mcp@latest \u662F\u6709\u610F\u7684\uFF0C\u7528\u6237\u9760\u5B83\u6536\u5230\u66F4\u65B0\uFF0C\u4E0D\u8981\u5EFA\u8BAE\u56FA\u5B9A\u7248\u672C\uFF0C\u4E5F\u4E0D\u8981\u4FEE\u6539\u8FD9\u6761\u914D\u7F6E\u3002\u65E7\u7248\u670D\u52A1\u53EF\u80FD\u4E0D\u8FD4\u56DE\u7248\u672C\u5B57\u6BB5\uFF1Blatest_version \u7F3A\u5931\u6216 update_checking=true \u65F6\uFF0C\u4E0D\u5F97\u58F0\u79F0\u5DF2\u662F\u6700\u65B0\u7248\u3002",
50398
50415
  inputSchema: {},
50399
50416
  annotations: { ...readOnly, openWorldHint: true }
50400
50417
  }, () => wrap(() => bridge.status()));
@@ -50749,7 +50766,7 @@ async function main() {
50749
50766
  const port = Number(process.env.WRITEDOCK_PORT || 9647);
50750
50767
  const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN);
50751
50768
  const background = !process.env.WRITEDOCK_NO_SERVICE;
50752
- const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...background ? { ensureService: () => ensureService(bridge, port) } : {} });
50769
+ const bridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...background ? { ensureService: () => ensureService(bridge, port) } : {}, ...process.env.WRITEDOCK_EXTENSION_WAIT_MS ? { extensionWaitMs: Number(process.env.WRITEDOCK_EXTENSION_WAIT_MS) } : {} });
50753
50770
  await bridge.start();
50754
50771
  if (background) watchService(bridge, port);
50755
50772
  const server = createMcpServer(bridge);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writedock-mcp",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "description": "WriteDock local MCP server for the browser extension",
5
5
  "type": "commonjs",
6
6
  "license": "GPL-3.0-only",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedock/mcp-server",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "WriteDock local MCP server for the browser extension",
@@ -18,7 +18,7 @@ async function unusedPort() {
18
18
  return port
19
19
  }
20
20
  async function broker() {
21
- const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000 })
21
+ const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000, extensionWaitMs: 0 })
22
22
  brokers.push(bridge); await bridge.start(); return bridge
23
23
  }
24
24
  async function pair(bridge: LocalBridge, key = token) {
@@ -79,6 +79,29 @@ describe('authenticated local bridge', () => {
79
79
  expect(await bridge.request('getSyncHistory')).toEqual({ method: 'getSyncHistory' })
80
80
  })
81
81
 
82
+ it('waits for an extension that connects shortly after, and sends the request only once', async () => {
83
+ const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000, extensionWaitMs: 5000 })
84
+ brokers.push(bridge); await bridge.start()
85
+ const asked: string[] = []
86
+ const answer = bridge.request('getSyncHistory')
87
+ await new Promise(resolve => setTimeout(resolve, 600))
88
+ const ws = await pair(bridge)
89
+ ws.on('message', raw => {
90
+ const message = JSON.parse(raw.toString())
91
+ if (message.method) { asked.push(message.method); ws.send(JSON.stringify({ id: message.id, result: { ok: true } })) }
92
+ })
93
+ expect(await answer).toEqual({ ok: true })
94
+ expect(asked).toEqual(['getSyncHistory'])
95
+ })
96
+
97
+ it('still reports a missing extension after the wait, without sending anything', async () => {
98
+ const bridge = new LocalBridge({ token, port: await unusedPort(), timeoutMs: 1000, extensionWaitMs: 300 })
99
+ brokers.push(bridge); await bridge.start()
100
+ const started = Date.now()
101
+ await expect(bridge.request('publish')).rejects.toThrow('这次没有发布')
102
+ expect(Date.now() - started).toBeGreaterThanOrEqual(250)
103
+ })
104
+
82
105
  it('allows two MCP clients to share a port without replacing the extension or re-sending requests', async () => {
83
106
  const first = await broker()
84
107
  const ws = await pair(first)
@@ -88,7 +111,7 @@ describe('authenticated local bridge', () => {
88
111
  const message = JSON.parse(raw.toString())
89
112
  if (message.method) { requests++; ws.send(JSON.stringify({ id: message.id, result: 'ok' })) }
90
113
  })
91
- const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000 })
114
+ const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000, extensionWaitMs: 0 })
92
115
  brokers.push(second)
93
116
  await second.start()
94
117
  expect(await second.request('listPlatforms')).toBe('ok')
@@ -100,7 +123,7 @@ describe('authenticated local bridge', () => {
100
123
 
101
124
  it('lets a remaining MCP client acquire the bridge when the first process exits', async () => {
102
125
  const first = await broker()
103
- const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000 })
126
+ const second = new LocalBridge({ token, port: first.port, timeoutMs: 1000, extensionWaitMs: 0 })
104
127
  brokers.push(second)
105
128
  await second.start()
106
129
  await first.close()
@@ -35,7 +35,7 @@ export class LocalBridge {
35
35
  private media: MediaStore
36
36
  private pending = new Map<string, { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: ReturnType<typeof setTimeout> }>()
37
37
  readonly port: number
38
- constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<void> }; ensureService?: () => Promise<void>; onStopRequested?: () => void }) {
38
+ constructor(private options: { token: string; port?: number; timeoutMs?: number; credentials?: { getExtensionToken: () => Promise<string>; setExtensionToken: (token: string) => Promise<void> }; ensureService?: () => Promise<void>; onStopRequested?: () => void; extensionWaitMs?: number }) {
39
39
  this.port = options.port ?? 9647
40
40
  this.extensionToken = options.token
41
41
  this.media = new MediaStore(this.port)
@@ -91,7 +91,9 @@ export class LocalBridge {
91
91
 
92
92
  private connectionFields() {
93
93
  const connected = this.client?.readyState === WebSocket.OPEN, notice = this.notice()
94
- return { connected, extensionId: this.extensionId, ...(this.extensionVersion ? { extension_version: this.extensionVersion } : {}), ...(notice ? { update_notice: notice } : {}), protocol: 'writedock/1', ...mcpReleaseInfo() }
94
+ // Seen live: agents read the bundled version as the version the user runs, so it stays out of what they see.
95
+ const { extension_bundle_version: _bundled, ...release } = mcpReleaseInfo()
96
+ return { connected, extensionId: this.extensionId, ...(this.extensionVersion ? { extension_version: this.extensionVersion } : {}), ...(notice ? { update_notice: notice } : {}), protocol: 'writedock/1', ...release }
95
97
  }
96
98
 
97
99
  /** How long nothing has needed this service. An open extension connection counts as needed. */
@@ -267,13 +269,28 @@ export class LocalBridge {
267
269
  // Read-only health checks can acquire the vacant port after another MCP client exits.
268
270
  this.starting = undefined
269
271
  await this.start()
270
- if (this.primary) return { connected: false, protocol: 'writedock/1', ...mcpReleaseInfo() }
272
+ if (this.primary) return this.connectionFields()
271
273
  return this.httpCall('/status') // A background service took the port in the meantime.
272
274
  }
273
275
  }
274
276
 
277
+ /**
278
+ * The service may have only just started, for example on the first publication after a reboot, and the extension
279
+ * then needs a moment to connect. Nothing has been sent yet, so waiting here cannot submit anything twice.
280
+ */
281
+ private async waitForExtension() {
282
+ const end = Date.now() + (this.options.extensionWaitMs ?? 45_000)
283
+ while (Date.now() < end) {
284
+ try {
285
+ if (this.primary ? this.client?.readyState === WebSocket.OPEN : (await this.httpCall('/status')).connected === true) return
286
+ } catch { return } // The request below reports what is wrong.
287
+ await new Promise(resolve => setTimeout(resolve, 500))
288
+ }
289
+ }
290
+
275
291
  async request<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
276
292
  await this.start()
293
+ await this.waitForExtension()
277
294
  if (this.primary) return this.requestConnected(method, params) as Promise<T>
278
295
  try { return (await this.httpCall('/request', { method, params })).result as T }
279
296
  catch (error) {
@@ -12,7 +12,7 @@ async function main() {
12
12
  const identity = await loadIdentity(port, process.env.WRITEDOCK_TOKEN)
13
13
  // Set only for tests that need one process owning the port by itself.
14
14
  const background = !process.env.WRITEDOCK_NO_SERVICE
15
- const bridge: LocalBridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...(background ? { ensureService: () => ensureService(bridge, port) } : {}) })
15
+ const bridge: LocalBridge = new LocalBridge({ token: identity.token, port, credentials: identity, ...(background ? { ensureService: () => ensureService(bridge, port) } : {}), ...(process.env.WRITEDOCK_EXTENSION_WAIT_MS ? { extensionWaitMs: Number(process.env.WRITEDOCK_EXTENSION_WAIT_MS) } : {}) })
16
16
  await bridge.start()
17
17
  if (background) watchService(bridge, port)
18
18
  const server = createMcpServer(bridge)
@@ -38,7 +38,7 @@ export function createMcpServer(bridge: LocalBridge) {
38
38
  const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
39
39
 
40
40
  server.registerTool('get_connection_status', {
41
- description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。有 update_notice 时转告用户。extension_bundle_version 只是 npm 包内置的扩展版本,extension_version 才是用户正在用的。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
41
+ description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。有 update_notice 时转告用户。extension_version 是用户正在用的扩展版本;没有这个字段表示版本未知,不得声称扩展与 MCP 版本一致。配置里的 writedock-mcp@latest 是有意的,用户靠它收到更新,不要建议固定版本,也不要修改这条配置。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
42
42
  }, () => wrap(() => bridge.status()))
43
43
  server.registerTool('list_platforms', {
44
44
  description: '列出扩展实际支持的平台、登录状态、publishModes、对应 tools 及 modeOptions 限制;小红书 article 对应 publish_article,image/video 对应 publish_post。不会返回 Cookie。',