@lightcone-ai/daemon 0.14.0 → 0.14.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.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { pathToFileURL } from 'url';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+
7
+ import { detailSections, planVideo } from './core.js';
8
+
9
+ function isExecutedDirectly(metaUrl) {
10
+ const entry = process.argv[1];
11
+ if (!entry) return false;
12
+ try {
13
+ return pathToFileURL(entry).href === metaUrl;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function toTextContent(payload) {
20
+ return {
21
+ content: [{
22
+ type: 'text',
23
+ text: JSON.stringify(payload, null, 2),
24
+ }],
25
+ };
26
+ }
27
+
28
+ function toErrorContent(error) {
29
+ return {
30
+ isError: true,
31
+ content: [{
32
+ type: 'text',
33
+ text: `Error: ${error.message}`,
34
+ }],
35
+ };
36
+ }
37
+
38
+ export function createVideoNarrationPlannerServer({
39
+ env = process.env,
40
+ fetchFn = globalThis.fetch,
41
+ } = {}) {
42
+ const server = new McpServer({ name: 'official-video-narration-planner', version: '0.1.0' });
43
+
44
+ server.tool(
45
+ 'plan_video',
46
+ 'Stage 2: plan narrative arc + phase plan for URL narration video. Enforces highlights<=3 and phases<=5.',
47
+ {
48
+ understanding: z.record(z.any()).describe('Stage 1 output page_understanding object.'),
49
+ persona: z.string().optional().describe('Target audience/persona description.'),
50
+ target_platform: z.string().describe('Target platform: 小红书/xiaohongshu, 抖音/douyin, 视频号/wechat_video.'),
51
+ total_duration_s: z.number().int().min(20).max(90).optional().describe('Optional explicit target duration in seconds.'),
52
+ },
53
+ async (args) => {
54
+ try {
55
+ const payload = planVideo(args ?? {});
56
+ return toTextContent(payload);
57
+ } catch (error) {
58
+ return toErrorContent(error);
59
+ }
60
+ }
61
+ );
62
+
63
+ server.tool(
64
+ 'detail_sections',
65
+ 'Stage 3: expand each phase into sentence+voice settings, call TTS voiceover, and fill duration/dwell.',
66
+ {
67
+ strategy: z.record(z.any()).describe('Output of plan_video (video strategy with phase_plan).'),
68
+ workspace_id: z.string().optional().describe('Workspace id for TTS generation. Defaults to WORKSPACE_ID env if provided.'),
69
+ credential_id: z.string().optional().describe('Optional explicit tts_provider credential id.'),
70
+ format: z.enum(['mp3', 'wav', 'flac']).optional().describe('Desired audio format for generated voiceover.'),
71
+ strict_tts: z.boolean().optional().describe('When true, fail if any TTS call fails; when false, fallback to estimated durations.'),
72
+ },
73
+ async (args) => {
74
+ try {
75
+ const payload = await detailSections(args ?? {}, { env, fetchFn });
76
+ return toTextContent(payload);
77
+ } catch (error) {
78
+ return toErrorContent(error);
79
+ }
80
+ }
81
+ );
82
+
83
+ return { server };
84
+ }
85
+
86
+ export async function startVideoNarrationPlannerServer(options = {}) {
87
+ const { server } = createVideoNarrationPlannerServer(options);
88
+ const transport = new StdioServerTransport();
89
+ await server.connect(transport);
90
+ console.error('[video-narration-planner] MCP server started');
91
+ }
92
+
93
+ if (isExecutedDirectly(import.meta.url)) {
94
+ startVideoNarrationPlannerServer().catch((error) => {
95
+ console.error(`[video-narration-planner] failed to start: ${error.message}`);
96
+ process.exitCode = 1;
97
+ });
98
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "video-narration-planner",
3
+ "name": "Official Video Narration Planner MCP",
4
+ "version": "0.1.0",
5
+ "runtime": "node",
6
+ "entrypoint": "index.js",
7
+ "tool_declarations": [
8
+ { "name": "plan_video", "classification": "cacheable" },
9
+ { "name": "detail_sections", "classification": "mandatory" }
10
+ ],
11
+ "smoke_test": {
12
+ "tool": "plan_video",
13
+ "arguments": {
14
+ "understanding": {
15
+ "url": "https://example.com/job-detail",
16
+ "core_message": "岗位职责与薪资透明,适合应届生快速判断是否投递",
17
+ "visual_hotspots": [
18
+ { "id": "hero", "y_range": [120, 320], "weight": 10, "text_excerpt": "岗位标题与薪资" },
19
+ { "id": "requirements", "y_range": [780, 1080], "weight": 8, "text_excerpt": "学历与经验要求" },
20
+ { "id": "apply", "y_range": [1420, 1660], "weight": 7, "text_excerpt": "投递入口" }
21
+ ],
22
+ "skip_zones": [
23
+ { "y_range": [2300, 3200], "reason": "广告与推荐位" }
24
+ ]
25
+ },
26
+ "persona": "校招求职学生",
27
+ "target_platform": "douyin"
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,449 @@
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 { z } from 'zod';
5
+ import { pathToFileURL } from 'url';
6
+
7
+ const SERVER_NAME = 'sophon-data';
8
+ const SERVER_VERSION = '0.1.0';
9
+ const DEFAULT_TIMEOUT_MS = 10000;
10
+ const MAX_HTTP_ERROR_TEXT = 400;
11
+
12
+ function isExecutedDirectly(metaUrl) {
13
+ const entry = process.argv[1];
14
+ if (!entry) return false;
15
+ try {
16
+ return pathToFileURL(entry).href === metaUrl;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function clampInt(value, min, max, fallback) {
23
+ const numberValue = Number(value);
24
+ if (!Number.isFinite(numberValue)) return fallback;
25
+ const truncated = Math.trunc(numberValue);
26
+ if (truncated < min) return min;
27
+ if (truncated > max) return max;
28
+ return truncated;
29
+ }
30
+
31
+ function normalizeOptionalString(value) {
32
+ if (value == null) return undefined;
33
+ const normalized = String(value).trim();
34
+ return normalized ? normalized : undefined;
35
+ }
36
+
37
+ function parseMaybeJson(value) {
38
+ if (!value || typeof value !== 'string') return null;
39
+ try {
40
+ return JSON.parse(value);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function toJsonTextContent(payload) {
47
+ return {
48
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
49
+ };
50
+ }
51
+
52
+ class SophonMcpError extends Error {
53
+ constructor({ code, message, retriable, httpStatus }) {
54
+ super(message);
55
+ this.name = 'SophonMcpError';
56
+ this.code = code;
57
+ this.retriable = Boolean(retriable);
58
+ this.httpStatus = Number.isFinite(httpStatus) ? Number(httpStatus) : 0;
59
+ }
60
+ }
61
+
62
+ function toNormalizedErrorPayload(error) {
63
+ if (error instanceof SophonMcpError) {
64
+ return {
65
+ code: error.code,
66
+ message: error.message,
67
+ retriable: error.retriable,
68
+ http_status: error.httpStatus,
69
+ };
70
+ }
71
+
72
+ if (error?.name === 'AbortError') {
73
+ return {
74
+ code: 'request_timeout',
75
+ message: 'request timed out when calling sophon-cub',
76
+ retriable: true,
77
+ http_status: 504,
78
+ };
79
+ }
80
+
81
+ return {
82
+ code: 'upstream_unavailable',
83
+ message: String(error?.message ?? 'failed to call sophon-cub'),
84
+ retriable: true,
85
+ http_status: 0,
86
+ };
87
+ }
88
+
89
+ function toMcpErrorResult(error) {
90
+ return {
91
+ isError: true,
92
+ content: [{ type: 'text', text: JSON.stringify(toNormalizedErrorPayload(error), null, 2) }],
93
+ };
94
+ }
95
+
96
+ function defaultErrorByStatus(status) {
97
+ if (status === 400 || status === 422) {
98
+ return { code: 'invalid_request', retriable: false };
99
+ }
100
+ if (status === 401) {
101
+ return { code: 'unauthorized', retriable: false };
102
+ }
103
+ if (status === 404) {
104
+ return { code: 'not_found', retriable: false };
105
+ }
106
+ if (status === 503) {
107
+ return { code: 'service_unavailable', retriable: true };
108
+ }
109
+ if (status >= 500) {
110
+ return { code: 'internal_error', retriable: true };
111
+ }
112
+ return { code: 'http_error', retriable: false };
113
+ }
114
+
115
+ function buildHttpError({ status, responseBody, responseText }) {
116
+ const bodyCode = normalizeOptionalString(responseBody?.error?.code);
117
+ const bodyMessage = normalizeOptionalString(responseBody?.error?.message);
118
+ const textMessage = normalizeOptionalString(responseText)
119
+ ? String(responseText).replace(/\s+/g, ' ').slice(0, MAX_HTTP_ERROR_TEXT)
120
+ : '';
121
+
122
+ const fallback = defaultErrorByStatus(status);
123
+
124
+ return new SophonMcpError({
125
+ code: bodyCode ?? fallback.code,
126
+ message: bodyMessage ?? textMessage ?? `sophon-cub returned HTTP ${status}`,
127
+ retriable: fallback.retriable,
128
+ httpStatus: status,
129
+ });
130
+ }
131
+
132
+ function normalizeBaseUrl(rawBaseUrl) {
133
+ const normalized = normalizeOptionalString(rawBaseUrl);
134
+ if (!normalized) {
135
+ throw new SophonMcpError({
136
+ code: 'invalid_config',
137
+ message: 'SOPHON_API_BASE_URL is required',
138
+ retriable: false,
139
+ httpStatus: 0,
140
+ });
141
+ }
142
+ const trimmed = normalized.replace(/\/+$/, '');
143
+ let validated;
144
+ try {
145
+ validated = new URL(trimmed);
146
+ } catch {
147
+ throw new SophonMcpError({
148
+ code: 'invalid_config',
149
+ message: 'SOPHON_API_BASE_URL must be a valid URL',
150
+ retriable: false,
151
+ httpStatus: 0,
152
+ });
153
+ }
154
+
155
+ if (!validated.protocol || !validated.hostname) {
156
+ throw new SophonMcpError({
157
+ code: 'invalid_config',
158
+ message: 'SOPHON_API_BASE_URL must include protocol and host',
159
+ retriable: false,
160
+ httpStatus: 0,
161
+ });
162
+ }
163
+
164
+ return trimmed;
165
+ }
166
+
167
+ function buildQueryParams(query = {}) {
168
+ const params = new URLSearchParams();
169
+ for (const [key, value] of Object.entries(query)) {
170
+ if (value == null) continue;
171
+ if (typeof value === 'string') {
172
+ const normalized = normalizeOptionalString(value);
173
+ if (!normalized) continue;
174
+ params.set(key, normalized);
175
+ continue;
176
+ }
177
+ if (typeof value === 'boolean') {
178
+ params.set(key, value ? 'true' : 'false');
179
+ continue;
180
+ }
181
+ if (typeof value === 'number') {
182
+ if (!Number.isFinite(value)) continue;
183
+ params.set(key, String(value));
184
+ continue;
185
+ }
186
+ params.set(key, String(value));
187
+ }
188
+ return params;
189
+ }
190
+
191
+ export function createSophonApiClient({
192
+ env = process.env,
193
+ fetchFn = globalThis.fetch,
194
+ timeoutMs = DEFAULT_TIMEOUT_MS,
195
+ } = {}) {
196
+ if (typeof fetchFn !== 'function') {
197
+ throw new SophonMcpError({
198
+ code: 'invalid_config',
199
+ message: 'fetch API is unavailable in current runtime',
200
+ retriable: false,
201
+ httpStatus: 0,
202
+ });
203
+ }
204
+
205
+ const baseUrl = normalizeBaseUrl(env.SOPHON_API_BASE_URL);
206
+ const token = normalizeOptionalString(env.SOPHON_API_TOKEN);
207
+ if (!token) {
208
+ throw new SophonMcpError({
209
+ code: 'invalid_config',
210
+ message: 'SOPHON_API_TOKEN is required',
211
+ retriable: false,
212
+ httpStatus: 0,
213
+ });
214
+ }
215
+
216
+ async function requestJson({ path, query = {}, method = 'GET' }) {
217
+ const url = new URL(path, `${baseUrl}/`);
218
+ const params = buildQueryParams(query);
219
+ for (const [key, value] of params.entries()) {
220
+ url.searchParams.set(key, value);
221
+ }
222
+
223
+ const controller = new AbortController();
224
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
225
+ try {
226
+ const response = await fetchFn(url, {
227
+ method,
228
+ headers: {
229
+ Accept: 'application/json',
230
+ Authorization: `Bearer ${token}`,
231
+ },
232
+ signal: controller.signal,
233
+ });
234
+
235
+ const responseText = await response.text();
236
+ const responseBody = parseMaybeJson(responseText);
237
+
238
+ if (!response.ok) {
239
+ throw buildHttpError({
240
+ status: response.status,
241
+ responseBody,
242
+ responseText,
243
+ });
244
+ }
245
+
246
+ if (!responseText.trim()) {
247
+ return {};
248
+ }
249
+
250
+ if (responseBody === null) {
251
+ throw new SophonMcpError({
252
+ code: 'invalid_response',
253
+ message: `sophon-cub returned non-JSON payload for ${url.pathname}`,
254
+ retriable: false,
255
+ httpStatus: response.status,
256
+ });
257
+ }
258
+
259
+ return responseBody;
260
+ } finally {
261
+ clearTimeout(timer);
262
+ }
263
+ }
264
+
265
+ return { requestJson };
266
+ }
267
+
268
+ function normalizeCursor(value) {
269
+ return normalizeOptionalString(value) ?? null;
270
+ }
271
+
272
+ export function createSophonToolHandlers({ apiClient, logger = console.error } = {}) {
273
+ if (!apiClient || typeof apiClient.requestJson !== 'function') {
274
+ throw new SophonMcpError({
275
+ code: 'invalid_config',
276
+ message: 'apiClient.requestJson is required',
277
+ retriable: false,
278
+ httpStatus: 0,
279
+ });
280
+ }
281
+
282
+ return {
283
+ async sophon_list_sources({ kind, enabled, limit, cursor }) {
284
+ try {
285
+ const payload = await apiClient.requestJson({
286
+ path: '/sources',
287
+ query: {
288
+ kind,
289
+ enabled,
290
+ limit,
291
+ cursor,
292
+ },
293
+ });
294
+ return toJsonTextContent({
295
+ items: Array.isArray(payload?.items) ? payload.items : [],
296
+ next_cursor: normalizeCursor(payload?.next_cursor),
297
+ });
298
+ } catch (error) {
299
+ logger(`[sophon-data] sophon_list_sources failed: ${error.message}`);
300
+ return toMcpErrorResult(error);
301
+ }
302
+ },
303
+
304
+ async sophon_list_timeseries({ source_id, series_key_prefix, limit }) {
305
+ try {
306
+ const payload = await apiClient.requestJson({
307
+ path: '/timeseries',
308
+ query: {
309
+ source_id,
310
+ series_key_prefix,
311
+ limit,
312
+ },
313
+ });
314
+ return toJsonTextContent({
315
+ items: Array.isArray(payload?.items) ? payload.items : [],
316
+ });
317
+ } catch (error) {
318
+ logger(`[sophon-data] sophon_list_timeseries failed: ${error.message}`);
319
+ return toMcpErrorResult(error);
320
+ }
321
+ },
322
+
323
+ async sophon_query_timeseries({ series_id, since, until, limit, cursor }) {
324
+ try {
325
+ const payload = await apiClient.requestJson({
326
+ path: `/timeseries/${encodeURIComponent(String(series_id))}/points`,
327
+ query: {
328
+ since,
329
+ until,
330
+ limit,
331
+ cursor,
332
+ },
333
+ });
334
+ return toJsonTextContent({
335
+ points: Array.isArray(payload?.points) ? payload.points : [],
336
+ next_cursor: normalizeCursor(payload?.next_cursor),
337
+ });
338
+ } catch (error) {
339
+ logger(`[sophon-data] sophon_query_timeseries failed: ${error.message}`);
340
+ return toMcpErrorResult(error);
341
+ }
342
+ },
343
+
344
+ async sophon_query_documents({ q, source_id, source_kind, since, until, language, limit, cursor }) {
345
+ try {
346
+ const payload = await apiClient.requestJson({
347
+ path: '/documents',
348
+ query: {
349
+ q,
350
+ source_id,
351
+ source_kind,
352
+ since,
353
+ until,
354
+ language,
355
+ limit,
356
+ cursor,
357
+ },
358
+ });
359
+ return toJsonTextContent({
360
+ items: Array.isArray(payload?.items) ? payload.items : [],
361
+ next_cursor: normalizeCursor(payload?.next_cursor),
362
+ });
363
+ } catch (error) {
364
+ logger(`[sophon-data] sophon_query_documents failed: ${error.message}`);
365
+ return toMcpErrorResult(error);
366
+ }
367
+ },
368
+ };
369
+ }
370
+
371
+ export function createSophonDataMcpServer({
372
+ env = process.env,
373
+ fetchFn = globalThis.fetch,
374
+ logger = console.error,
375
+ } = {}) {
376
+ const timeoutMs = clampInt(env.SOPHON_API_TIMEOUT_MS, 1000, 60000, DEFAULT_TIMEOUT_MS);
377
+ const apiClient = createSophonApiClient({ env, fetchFn, timeoutMs });
378
+ const handlers = createSophonToolHandlers({ apiClient, logger });
379
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
380
+
381
+ server.tool(
382
+ 'sophon_list_sources',
383
+ 'List source catalog entries from sophon-cub.',
384
+ {
385
+ kind: z.string().optional().describe('Filter by source kind, e.g. rss/news/timeseries.'),
386
+ enabled: z.boolean().optional().describe('Filter by source enabled flag.'),
387
+ limit: z.number().int().min(1).max(100).optional().describe('Maximum returned items.'),
388
+ cursor: z.string().optional().describe('Opaque pagination cursor from previous response.'),
389
+ },
390
+ handlers.sophon_list_sources
391
+ );
392
+
393
+ server.tool(
394
+ 'sophon_list_timeseries',
395
+ 'List timeseries metadata entries from sophon-cub.',
396
+ {
397
+ source_id: z.string().optional().describe('Filter by source id.'),
398
+ series_key_prefix: z.string().optional().describe('Filter by series key prefix.'),
399
+ limit: z.number().int().min(1).max(1000).optional().describe('Maximum returned items.'),
400
+ },
401
+ handlers.sophon_list_timeseries
402
+ );
403
+
404
+ server.tool(
405
+ 'sophon_query_timeseries',
406
+ 'Query timeseries points from sophon-cub by series and time window.',
407
+ {
408
+ series_id: z.number().int().positive().describe('Timeseries id.'),
409
+ since: z.string().optional().describe('ISO8601 lower bound (inclusive).'),
410
+ until: z.string().optional().describe('ISO8601 upper bound (inclusive).'),
411
+ limit: z.number().int().min(1).max(10000).optional().describe('Maximum returned points.'),
412
+ cursor: z.string().optional().describe('Opaque pagination cursor from previous response.'),
413
+ },
414
+ handlers.sophon_query_timeseries
415
+ );
416
+
417
+ server.tool(
418
+ 'sophon_query_documents',
419
+ 'Query document summaries from sophon-cub with filters.',
420
+ {
421
+ q: z.string().optional().describe('Full-text keyword query.'),
422
+ source_id: z.string().optional().describe('Filter by source id.'),
423
+ source_kind: z.string().optional().describe('Filter by source kind.'),
424
+ since: z.string().optional().describe('ISO8601 lower bound for publish/collect time.'),
425
+ until: z.string().optional().describe('ISO8601 upper bound for publish/collect time.'),
426
+ language: z.string().optional().describe('Language filter, e.g. zh/en.'),
427
+ limit: z.number().int().min(1).max(100).optional().describe('Maximum returned items.'),
428
+ cursor: z.string().optional().describe('Opaque pagination cursor from previous response.'),
429
+ },
430
+ handlers.sophon_query_documents
431
+ );
432
+
433
+ return { server, apiClient };
434
+ }
435
+
436
+ export async function startSophonDataMcpServer(options = {}) {
437
+ const { server } = createSophonDataMcpServer(options);
438
+ const transport = new StdioServerTransport();
439
+ await server.connect(transport);
440
+ console.error('[sophon-data] MCP server started');
441
+ }
442
+
443
+ if (isExecutedDirectly(import.meta.url)) {
444
+ startSophonDataMcpServer().catch((error) => {
445
+ const payload = toNormalizedErrorPayload(error);
446
+ console.error(`[sophon-data] failed to start: ${payload.code}: ${payload.message}`);
447
+ process.exitCode = 1;
448
+ });
449
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "sophon-data",
3
+ "name": "Sophon Data MCP Server",
4
+ "version": "0.1.0",
5
+ "runtime": "node",
6
+ "entrypoint": "index.js",
7
+ "tool_declarations": [
8
+ { "name": "sophon_list_sources", "classification": "cacheable" },
9
+ { "name": "sophon_list_timeseries", "classification": "cacheable" },
10
+ { "name": "sophon_query_timeseries", "classification": "cacheable" },
11
+ { "name": "sophon_query_documents", "classification": "cacheable" }
12
+ ],
13
+ "smoke_test": {
14
+ "tool": "sophon_list_sources",
15
+ "arguments": {
16
+ "limit": 1
17
+ }
18
+ }
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {