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.
- package/README.md +3 -1
- package/extension/assets/{index.ts-BeiBzFaR.js → index.ts-Ch2Bzedq.js} +10 -10
- package/extension/manifest.json +1 -1
- package/extension/service-worker-loader.js +1 -1
- package/extension//345/256/211/350/243/205/344/270/216/346/233/264/346/226/260.txt +1 -1
- package/index.cjs +22 -5
- package/package.json +1 -1
- package/source/package.json +1 -1
- package/source/src/__tests__/bridge.test.ts +26 -3
- package/source/src/bridge.ts +20 -3
- package/source/src/index.ts +1 -1
- package/source/src/server.ts +1 -1
package/extension/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import './assets/index.ts-
|
|
1
|
+
import './assets/index.ts-Ch2Bzedq.js';
|
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.
|
|
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
|
-
|
|
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
|
|
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\
|
|
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
package/source/package.json
CHANGED
|
@@ -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()
|
package/source/src/bridge.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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) {
|
package/source/src/index.ts
CHANGED
|
@@ -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)
|
package/source/src/server.ts
CHANGED
|
@@ -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 时转告用户。
|
|
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。',
|