@lightcone-ai/daemon 0.11.0 → 0.13.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.
@@ -3,12 +3,15 @@
3
3
  * Uses deterministic CDP operations — no AI vision required.
4
4
  */
5
5
  import { formatTextWithTags } from '../text.js';
6
+ import { PublisherAdapter } from './publisher-adapter.js';
6
7
 
7
8
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
8
9
  function randomInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
9
10
 
10
11
  const PACE_SCALE = Math.max(0.2, Number(process.env.PUBLISHER_PACE_SCALE ?? '1') || 1);
11
12
  const DRY_RUN = process.env.XHS_PUBLISH_DRY_RUN !== '0';
13
+ const TITLE_MIN_LENGTH = 2;
14
+ const TITLE_MAX_LENGTH = 20;
12
15
  async function humanPause(minMs, maxMs, label = '') {
13
16
  const ms = Math.round(randomInt(minMs, maxMs) * PACE_SCALE);
14
17
  if (label) console.error(`[XhsAdapter] pause ${label}: ${ms}ms`);
@@ -17,22 +20,74 @@ async function humanPause(minMs, maxMs, label = '') {
17
20
 
18
21
  const REQUIREMENTS = {
19
22
  image_text: {
23
+ platform: 'xhs',
24
+ content_type: 'image_text',
20
25
  max_text_length: 1000,
21
26
  max_images: 9,
22
27
  image_formats: ['jpg', 'jpeg', 'png', 'webp'],
23
- required_fields: ['text', 'images'],
24
- notes: '当前自动化支持上传图文,必须提供至少 1 张本地图片;标题建议 2-20 字;图片建议 3:4 竖版;话题标签写在正文末尾如 #话题名',
28
+ required_fields: ['title', 'text', 'images'],
29
+ optional_fields: ['tags'],
30
+ notes: [
31
+ '标题需 2-20 字',
32
+ '必须提供至少 1 张本地图片;图片建议 3:4 竖版',
33
+ '话题标签以 tags 数组传入,系统会自动拼接到正文',
34
+ '广告/合作内容必须带平台广告标识;命中策略会在 precheck 阻断',
35
+ ],
25
36
  },
26
37
  short_video: {
38
+ platform: 'xhs',
39
+ content_type: 'short_video',
27
40
  max_text_length: 1000,
28
- max_images: 1,
29
- video_max_duration: 600,
41
+ video_max_duration_sec: 600,
30
42
  video_formats: ['mp4', 'mov'],
31
- required_fields: ['text', 'video'],
32
- notes: '封面图建议 3:4;视频时长不超过 10 分钟',
43
+ required_fields: ['title', 'text', 'video'],
44
+ optional_fields: ['tags', 'cover'],
45
+ notes: [
46
+ '标题需 2-20 字',
47
+ '视频时长不超过 10 分钟',
48
+ '封面图建议 3:4;当前封面上传依赖页面能力',
49
+ '广告/合作内容必须带平台广告标识;命中策略会在 precheck 阻断',
50
+ ],
33
51
  },
34
52
  };
35
53
 
54
+ const XHS_CAPABILITIES = Object.freeze({
55
+ platform: 'xhs',
56
+ display_name: '小红书',
57
+ spec_version: '0.1.0',
58
+ supportedDrivers: ['cdp'],
59
+ supportedContentTypes: ['image_text', 'short_video'],
60
+ max_image: 9,
61
+ max_duration: 600,
62
+ short_video_max_duration: 600,
63
+ image_formats: ['jpg', 'jpeg', 'png', 'webp'],
64
+ video_formats: ['mp4', 'mov'],
65
+ ai_label_required: true,
66
+ ad_disclosure_required: true,
67
+ sensitive_word_db_ref: {
68
+ skill: 'platform-policy-db',
69
+ platform: 'xhs',
70
+ version: 'xhs-2026.04',
71
+ },
72
+ cover_required_for_short_video: false,
73
+ cover_required_for_long_video: false,
74
+ dry_run_supported: true,
75
+ schedule_supported: false,
76
+ default_publish_rate: {
77
+ per_account_per_day_max: 4,
78
+ notes: '建议保持保守发布频率,避免触发平台风控',
79
+ },
80
+ platform_extras: {
81
+ platform: 'xhs',
82
+ topics_max: 10,
83
+ hashtag_max: 10,
84
+ poi_supported: true,
85
+ brand_partnership_required_for_ads: true,
86
+ cover_aspect_ratios: ['3:4', '1:1'],
87
+ },
88
+ evidence: 'verified',
89
+ });
90
+
36
91
  const PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish';
37
92
  const IMAGE_PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?target=image';
38
93
  const VIDEO_PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?source=video';
@@ -55,9 +110,13 @@ const ERROR_SELECTORS = [
55
110
  '[role="alert"]',
56
111
  ];
57
112
 
58
- export class XhsAdapter {
113
+ export class XhsAdapter extends PublisherAdapter {
59
114
  constructor(cdp) {
60
- this._cdp = cdp;
115
+ super(cdp);
116
+ }
117
+
118
+ getCapabilities() {
119
+ return XHS_CAPABILITIES;
61
120
  }
62
121
 
63
122
  getRequirements(contentType) {
@@ -74,10 +133,23 @@ export class XhsAdapter {
74
133
  const url = state.url;
75
134
  const loggedIn = state.isCreatorLoggedIn && !state.hasLoginHint;
76
135
  console.error(`[XhsAdapter] checkLoginStatus: loggedIn=${loggedIn} url=${url}`);
77
- return { loggedIn, url, profileDir, userId: null, nickname: null };
136
+ return {
137
+ loggedIn,
138
+ url,
139
+ profileDir,
140
+ userId: null,
141
+ nickname: null,
142
+ authMode: 'cdp',
143
+ lastVerifiedAt: new Date().toISOString(),
144
+ };
78
145
  }
79
146
 
80
147
  async publishImageText({ title, text, tags = [], images = [] }) {
148
+ const normalizedTitle = this._normalizeAndValidateTitle(title);
149
+ if (images.length > XHS_CAPABILITIES.max_image) {
150
+ throw new Error(`INPUT_MEDIA_REJECTED: 小红书图文最多 ${XHS_CAPABILITIES.max_image} 张图`);
151
+ }
152
+
81
153
  await this._openPublishComposer('image');
82
154
  await this._assertReadyForPublish();
83
155
  await humanPause(2500, 5500, 'composer-ready');
@@ -93,10 +165,8 @@ export class XhsAdapter {
93
165
  }
94
166
 
95
167
  // Fill title
96
- if (title) {
97
- await this._fillField(TITLE_SELECTOR, title);
98
- await humanPause(1200, 2800, 'after-title');
99
- }
168
+ await this._fillField(TITLE_SELECTOR, normalizedTitle);
169
+ await humanPause(1200, 2800, 'after-title');
100
170
 
101
171
  // Fill content text and only append tags that are not already present.
102
172
  const fullText = formatTextWithTags(text, tags);
@@ -119,6 +189,8 @@ export class XhsAdapter {
119
189
  }
120
190
 
121
191
  async publishShortVideo({ title, text, tags = [], video, cover }) {
192
+ const normalizedTitle = this._normalizeAndValidateTitle(title);
193
+
122
194
  await this._openPublishComposer('video');
123
195
  await this._assertReadyForPublish();
124
196
  await humanPause(2500, 5500, 'composer-ready');
@@ -138,10 +210,8 @@ export class XhsAdapter {
138
210
  await humanPause(1800, 4200, 'after-cover-upload');
139
211
  }
140
212
 
141
- if (title) {
142
- await this._fillField(TITLE_SELECTOR, title);
143
- await humanPause(1200, 2800, 'after-title');
144
- }
213
+ await this._fillField(TITLE_SELECTOR, normalizedTitle);
214
+ await humanPause(1200, 2800, 'after-title');
145
215
 
146
216
  const fullText = formatTextWithTags(text, tags);
147
217
  await this._fillField(CONTENT_SELECTOR, fullText);
@@ -162,6 +232,22 @@ export class XhsAdapter {
162
232
  return { success: true, post_url: result.postUrl || result.url };
163
233
  }
164
234
 
235
+ async withdrawPost() {
236
+ throw new Error('PUBLISH_UNSUPPORTED_FEATURE: 小红书当前自动化未提供撤稿能力');
237
+ }
238
+
239
+ _normalizeAndValidateTitle(title) {
240
+ const normalized = String(title ?? '').trim();
241
+ if (!normalized) {
242
+ throw new Error(`INPUT_REQUIRED_FIELD_MISSING: 小红书标题不能为空(${TITLE_MIN_LENGTH}-${TITLE_MAX_LENGTH} 字)`);
243
+ }
244
+ const length = [...normalized].length;
245
+ if (length < TITLE_MIN_LENGTH || length > TITLE_MAX_LENGTH) {
246
+ throw new Error(`INPUT_TEXT_TOO_LONG: 小红书标题需 ${TITLE_MIN_LENGTH}-${TITLE_MAX_LENGTH} 字,当前 ${length} 字`);
247
+ }
248
+ return normalized;
249
+ }
250
+
165
251
  // ── CDP helpers ─────────────────────────────────────────────────────────────
166
252
 
167
253
  async _openPublishComposer(kind) {
@@ -18,6 +18,8 @@ import { getSession, closeSession } from './chrome-pool.js';
18
18
  import { XhsAdapter } from './adapters/xhs.js';
19
19
  import { DouyinAdapter } from './adapters/douyin.js';
20
20
  import { KuaishouAdapter } from './adapters/kuaishou.js';
21
+ import { callOfficialTool } from './official-tool-client.js';
22
+ import { runPublishPrecheck } from './precheck.js';
21
23
  import { withProfileLock } from '../../src/profile-lock.js';
22
24
 
23
25
  const SERVER_URL = process.env.SERVER_URL ?? '';
@@ -41,24 +43,40 @@ const PLATFORM_LABELS = {
41
43
  kuaishou: '快手',
42
44
  };
43
45
 
46
+ const ADAPTER_REGISTRY = Object.freeze({
47
+ xhs: XhsAdapter,
48
+ douyin: DouyinAdapter,
49
+ kuaishou: KuaishouAdapter,
50
+ });
51
+
44
52
  function getProfileDir(platform) {
45
53
  const key = PLATFORM_ENV_KEYS[platform];
46
54
  if (!key) return null;
47
55
  return process.env[key] ?? null;
48
56
  }
49
57
 
58
+ function getAdapterClass(platform) {
59
+ const AdapterClass = ADAPTER_REGISTRY[platform];
60
+ if (!AdapterClass) throw new Error(`Unknown platform: ${platform}`);
61
+ return AdapterClass;
62
+ }
63
+
64
+ function createAdapter(platform, cdp) {
65
+ const AdapterClass = getAdapterClass(platform);
66
+ return new AdapterClass(cdp);
67
+ }
68
+
69
+ function createStaticAdapter(platform) {
70
+ return createAdapter(platform, null);
71
+ }
72
+
50
73
  async function getAdapter(platform) {
51
74
  const profileDir = getProfileDir(platform);
52
75
  if (!profileDir) {
53
76
  throw new Error(`No profile dir for platform="${platform}". Has the user logged in and authorized this agent?`);
54
77
  }
55
78
  const cdp = await getSession(platform, profileDir);
56
- switch (platform) {
57
- case 'xhs': return new XhsAdapter(cdp);
58
- case 'douyin': return new DouyinAdapter(cdp);
59
- case 'kuaishou': return new KuaishouAdapter(cdp);
60
- default: throw new Error(`Unknown platform: ${platform}`);
61
- }
79
+ return createAdapter(platform, cdp);
62
80
  }
63
81
 
64
82
  async function withPublisherProfile(platform, fn) {
@@ -111,7 +129,7 @@ async function completeApproval(actionId, ok, result, error) {
111
129
  }
112
130
  }
113
131
 
114
- const MEDIA_LIMITS = {
132
+ const DEFAULT_MEDIA_LIMITS = {
115
133
  xhs: { maxImages: 9, imageExts: ['.jpg', '.jpeg', '.png', '.webp'], videoExts: ['.mp4', '.mov'] },
116
134
  douyin: { maxImages: 35, imageExts: ['.jpg', '.jpeg', '.png', '.webp'], videoExts: ['.mp4', '.mov', '.webm'] },
117
135
  kuaishou: { maxImages: 12, imageExts: ['.jpg', '.jpeg', '.png'], videoExts: ['.mp4', '.mov'] },
@@ -218,8 +236,37 @@ function validateLocalFile(filePath, { kind, required = false, allowedExts = []
218
236
  return realFile;
219
237
  }
220
238
 
239
+ function normalizeFormatExts(formats = [], fallback = []) {
240
+ if (!Array.isArray(formats) || formats.length === 0) return fallback;
241
+ return formats
242
+ .map(item => String(item ?? '').trim().toLowerCase())
243
+ .filter(Boolean)
244
+ .map(item => (item.startsWith('.') ? item : `.${item}`));
245
+ }
246
+
247
+ function resolveMediaLimits(platform, contentType) {
248
+ const fallback = DEFAULT_MEDIA_LIMITS[platform] ?? DEFAULT_MEDIA_LIMITS.xhs;
249
+ const adapter = createStaticAdapter(platform);
250
+ const requirements = adapter.getRequirements(contentType) ?? {};
251
+ const capabilities = typeof adapter.getCapabilities === 'function'
252
+ ? (adapter.getCapabilities() ?? {})
253
+ : {};
254
+
255
+ return {
256
+ maxImages: Number(requirements.max_images ?? capabilities.max_image ?? fallback.maxImages),
257
+ imageExts: normalizeFormatExts(
258
+ requirements.image_formats ?? capabilities.image_formats,
259
+ fallback.imageExts
260
+ ),
261
+ videoExts: normalizeFormatExts(
262
+ requirements.video_formats ?? capabilities.video_formats,
263
+ fallback.videoExts
264
+ ),
265
+ };
266
+ }
267
+
221
268
  function validateMedia({ platform, contentType, images = [], video, cover }) {
222
- const limits = MEDIA_LIMITS[platform] ?? MEDIA_LIMITS.xhs;
269
+ const limits = resolveMediaLimits(platform, contentType);
223
270
  if (contentType === 'image_text') {
224
271
  if (images.length === 0) {
225
272
  throw new Error(`image_text requires at least 1 image on ${platform}. Provide absolute image paths inside the agent workspace artifacts directory.`);
@@ -267,28 +314,77 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
267
314
  const label = PLATFORM_LABELS[platform] ?? platform;
268
315
  try {
269
316
  const approvalData = await validateApproval(approval_action_id, platform);
317
+ const approvalPayload = approvalData?.payload && typeof approvalData.payload === 'object' && !Array.isArray(approvalData.payload)
318
+ ? approvalData.payload
319
+ : {};
320
+ const precheck = await runPublishPrecheck({
321
+ platform,
322
+ title,
323
+ text,
324
+ tags: tags ?? [],
325
+ payload: approvalPayload,
326
+ callTool: callOfficialTool,
327
+ });
328
+ if (!precheck.ok) {
329
+ const blockerSummary = precheck.blockers.map(item => `${item.code}: ${item.message}`).join(' | ');
330
+ throw new Error(`PUBLISH_PRECHECK_BLOCKED:${blockerSummary || 'precheck failed'}`);
331
+ }
332
+
270
333
  const localMedia = await materializeMedia({ images: images ?? [], video, cover }, approvalData);
334
+ const staticAdapter = createStaticAdapter(platform);
335
+ const req = staticAdapter.getRequirements(content_type);
336
+ if (!req) throw new Error(`INPUT_UNSUPPORTED_CONTENT_TYPE: ${platform} does not support ${content_type}`);
271
337
  const media = validateMedia({ platform, contentType: content_type, ...localMedia });
272
- const result = await withPublisherProfile(platform, async () => {
338
+ const { publishResult, healthCheck } = await withPublisherProfile(platform, async () => {
273
339
  const adapter = await getAdapter(platform);
340
+ let publishResult = null;
274
341
  if (content_type === 'image_text') {
275
- return adapter.publishImageText({ title, text, tags: tags ?? [], images: media.images });
342
+ publishResult = await adapter.publishImageText({ title, text, tags: tags ?? [], images: media.images });
343
+ } else if (content_type === 'short_video') {
344
+ publishResult = await adapter.publishShortVideo({ title, text, tags: tags ?? [], video: media.video, cover: media.cover });
345
+ } else {
346
+ throw new Error(`Unsupported content_type: ${content_type}`);
276
347
  }
277
- if (content_type === 'short_video') {
278
- return adapter.publishShortVideo({ title, text, tags: tags ?? [], video: media.video, cover: media.cover });
348
+
349
+ let healthCheck = null;
350
+ try {
351
+ healthCheck = await adapter.checkLoginStatus();
352
+ } catch (error) {
353
+ healthCheck = {
354
+ loggedIn: null,
355
+ error: error.message,
356
+ };
279
357
  }
280
- throw new Error(`Unsupported content_type: ${content_type}`);
358
+ return { publishResult, healthCheck };
281
359
  });
282
360
 
283
- await completeApproval(approval_action_id, true, result, null);
284
- if (result?.dry_run) {
361
+ const completionResult = {
362
+ precheck: {
363
+ blockers: precheck.blockers,
364
+ warnings: precheck.warnings,
365
+ policy_version: precheck?.policyScan?.policy_version ?? null,
366
+ advisory: precheck?.advisory?.advisory ?? null,
367
+ },
368
+ publish_result: publishResult,
369
+ post_publish_health: healthCheck,
370
+ };
371
+ await completeApproval(approval_action_id, true, completionResult, null);
372
+
373
+ const advisoryMessage = precheck?.advisory?.advisory?.level === 'suggest_delay'
374
+ ? `\n发布建议:${precheck.advisory.advisory.message}`
375
+ : '';
376
+ const healthMessage = healthCheck?.loggedIn === false
377
+ ? '\n发布后巡检:账号登录状态异常,请尽快复核凭据。'
378
+ : '';
379
+
380
+ if (publishResult?.dry_run) {
285
381
  return {
286
- content: [{ type: 'text', text: `✓ ${label}发布流程已完成到发布前一步,已跳过最终“发布”点击。` }],
382
+ content: [{ type: 'text', text: `✓ ${label}发布流程已完成到发布前一步,已跳过最终“发布”点击。${advisoryMessage}${healthMessage}` }],
287
383
  };
288
384
  }
289
- const postUrl = result.post_url ? `\n发布链接: ${result.post_url}` : '';
385
+ const postUrl = publishResult?.post_url ? `\n发布链接: ${publishResult.post_url}` : '';
290
386
  return {
291
- content: [{ type: 'text', text: `✓ 已成功发布到${label}。${postUrl}` }],
387
+ content: [{ type: 'text', text: `✓ 已成功发布到${label}。${postUrl}${advisoryMessage}${healthMessage}` }],
292
388
  };
293
389
  } catch (err) {
294
390
  if (err.message?.startsWith('LOGIN_EXPIRED')) {
@@ -299,6 +395,14 @@ images/video 字段填写本地绝对路径(在 agent workspace 目录下)
299
395
  isError: true,
300
396
  };
301
397
  }
398
+ if (err.message?.startsWith('PUBLISH_PRECHECK_BLOCKED:')) {
399
+ const message = err.message.replace('PUBLISH_PRECHECK_BLOCKED:', '').trim();
400
+ if (approval_action_id) await completeApproval(approval_action_id, false, null, err.message);
401
+ return {
402
+ content: [{ type: 'text', text: `发布前预检未通过:${message}` }],
403
+ isError: true,
404
+ };
405
+ }
302
406
  if (approval_action_id) await completeApproval(approval_action_id, false, null, err.message);
303
407
  return {
304
408
  content: [{ type: 'text', text: `发布失败: ${err.message}` }],
@@ -319,13 +423,7 @@ server.tool(
319
423
  },
320
424
  async ({ platform, content_type }) => {
321
425
  try {
322
- let adapter;
323
- switch (platform) {
324
- case 'xhs': adapter = new XhsAdapter(null); break;
325
- case 'douyin': adapter = new DouyinAdapter(null); break;
326
- case 'kuaishou': adapter = new KuaishouAdapter(null); break;
327
- default: throw new Error(`Unknown platform: ${platform}`);
328
- }
426
+ const adapter = createStaticAdapter(platform);
329
427
  const req = adapter.getRequirements(content_type);
330
428
  if (!req) return { content: [{ type: 'text', text: `该平台不支持 ${content_type} 类型` }] };
331
429
  return { content: [{ type: 'text', text: JSON.stringify(req, null, 2) }] };
@@ -0,0 +1,171 @@
1
+ import { spawn } from 'child_process';
2
+ import { resolveMcpServerEntrypoint } from '../../../src/mcp/registry.js';
3
+
4
+ function extractErrorText(result) {
5
+ const chunks = Array.isArray(result?.content) ? result.content : [];
6
+ const texts = chunks
7
+ .map(chunk => (typeof chunk?.text === 'string' ? chunk.text.trim() : ''))
8
+ .filter(Boolean);
9
+ if (texts.length > 0) return texts.join('\n');
10
+ return 'unknown_mcp_error';
11
+ }
12
+
13
+ function parseJsonContent(result) {
14
+ const chunks = Array.isArray(result?.content) ? result.content : [];
15
+ const text = chunks
16
+ .map(chunk => (typeof chunk?.text === 'string' ? chunk.text : ''))
17
+ .join('\n')
18
+ .trim();
19
+ if (!text) return {};
20
+ try {
21
+ return JSON.parse(text);
22
+ } catch {
23
+ return { raw_text: text };
24
+ }
25
+ }
26
+
27
+ class StdioJsonRpcClient {
28
+ constructor(command, args = [], env = {}) {
29
+ this.command = command;
30
+ this.args = args;
31
+ this.env = env;
32
+ this.proc = null;
33
+ this.stdoutBuffer = '';
34
+ this.nextId = 1;
35
+ this.pending = new Map();
36
+ }
37
+
38
+ async start() {
39
+ if (this.proc) return;
40
+ this.proc = spawn(this.command, this.args, {
41
+ cwd: process.cwd(),
42
+ env: {
43
+ ...process.env,
44
+ ...this.env,
45
+ },
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ });
48
+
49
+ this.proc.stdout.setEncoding('utf8');
50
+ this.proc.stdout.on('data', chunk => this._handleStdout(chunk));
51
+ this.proc.on('exit', (code) => {
52
+ const error = new Error(`process_exited:${code}`);
53
+ for (const pending of this.pending.values()) {
54
+ clearTimeout(pending.timer);
55
+ pending.reject(error);
56
+ }
57
+ this.pending.clear();
58
+ });
59
+ }
60
+
61
+ _handleStdout(chunk) {
62
+ this.stdoutBuffer += chunk;
63
+ const lines = this.stdoutBuffer.split('\n');
64
+ this.stdoutBuffer = lines.pop() ?? '';
65
+
66
+ for (const line of lines) {
67
+ const text = line.trim();
68
+ if (!text) continue;
69
+
70
+ let payload;
71
+ try {
72
+ payload = JSON.parse(text);
73
+ } catch {
74
+ continue;
75
+ }
76
+ if (payload?.id == null) continue;
77
+
78
+ const pending = this.pending.get(payload.id);
79
+ if (!pending) continue;
80
+
81
+ clearTimeout(pending.timer);
82
+ this.pending.delete(payload.id);
83
+
84
+ if (payload.error) {
85
+ pending.reject(new Error(payload.error.message ?? 'jsonrpc_error'));
86
+ } else {
87
+ pending.resolve(payload.result);
88
+ }
89
+ }
90
+ }
91
+
92
+ request(method, params = {}, timeoutMs = 10000) {
93
+ if (!this.proc) {
94
+ return Promise.reject(new Error('client_not_started'));
95
+ }
96
+ const id = this.nextId++;
97
+ const payload = {
98
+ jsonrpc: '2.0',
99
+ id,
100
+ method,
101
+ params,
102
+ };
103
+
104
+ return new Promise((resolve, reject) => {
105
+ const timer = setTimeout(() => {
106
+ this.pending.delete(id);
107
+ reject(new Error(`timeout:${method}`));
108
+ }, timeoutMs);
109
+
110
+ this.pending.set(id, { resolve, reject, timer });
111
+ this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
112
+ });
113
+ }
114
+
115
+ notify(method, params = {}) {
116
+ if (!this.proc) return;
117
+ const payload = {
118
+ jsonrpc: '2.0',
119
+ method,
120
+ params,
121
+ };
122
+ this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
123
+ }
124
+
125
+ async close() {
126
+ if (!this.proc) return;
127
+ if (!this.proc.killed) {
128
+ this.proc.kill('SIGTERM');
129
+ await new Promise(resolve => setTimeout(resolve, 100));
130
+ if (!this.proc.killed) this.proc.kill('SIGKILL');
131
+ }
132
+ this.proc = null;
133
+ }
134
+ }
135
+
136
+ export async function callOfficialTool({
137
+ serverId,
138
+ toolName,
139
+ argumentsPayload = {},
140
+ timeoutMs = 10000,
141
+ }) {
142
+ const entrypointPath = resolveMcpServerEntrypoint(serverId, { strict: true });
143
+ if (!entrypointPath) {
144
+ throw new Error(`official_server_not_found:${serverId}`);
145
+ }
146
+
147
+ const client = new StdioJsonRpcClient(process.execPath, [entrypointPath]);
148
+ try {
149
+ await client.start();
150
+ await client.request('initialize', {
151
+ protocolVersion: '2024-11-05',
152
+ capabilities: {},
153
+ clientInfo: {
154
+ name: 'publisher-official-tool-client',
155
+ version: '0.1.0',
156
+ },
157
+ }, timeoutMs);
158
+ client.notify('notifications/initialized', {});
159
+
160
+ const result = await client.request('tools/call', {
161
+ name: toolName,
162
+ arguments: argumentsPayload ?? {},
163
+ }, timeoutMs);
164
+ if (result?.isError === true) {
165
+ throw new Error(extractErrorText(result));
166
+ }
167
+ return parseJsonContent(result);
168
+ } finally {
169
+ await client.close();
170
+ }
171
+ }