@vivipilot/cli 0.1.0 → 0.1.4
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/dist/chunk-FGUKEVKT.js +1 -0
- package/dist/chunk-FN7FHZ3D.js +1 -0
- package/dist/chunk-JJDAKVXA.js +8 -0
- package/dist/chunk-L3C7EOPY.js +2 -0
- package/dist/chunk-ZIATVRTF.js +3 -0
- package/dist/chunk-ZZ72PPC3.js +1 -0
- package/dist/cli.d.ts +5 -4
- package/dist/cli.js +57 -495
- package/dist/config-DAG58pP-.d.ts +16 -0
- package/dist/index.d.ts +121 -7
- package/dist/index.js +1 -7
- package/dist/manifest-TDCIHZ46.js +1 -0
- package/dist/mcp-DTGU64NI.js +1 -0
- package/dist/render-CVXYAUBY.js +1 -0
- package/package.json +6 -2
- package/dist/api.d.ts +0 -86
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -77
- package/dist/api.js.map +0 -1
- package/dist/args.d.ts +0 -11
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js +0 -53
- package/dist/args.js.map +0 -1
- package/dist/browser.d.ts +0 -31
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js +0 -162
- package/dist/browser.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts +0 -15
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -58
- package/dist/config.js.map +0 -1
- package/dist/errors.d.ts +0 -6
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -12
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/manifest.d.ts +0 -40
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -90
- package/dist/manifest.js.map +0 -1
- package/dist/mcp.d.ts +0 -13
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js +0 -392
- package/dist/mcp.js.map +0 -1
- package/dist/render.d.ts +0 -21
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -369
- package/dist/render.js.map +0 -1
- package/src/api.ts +0 -163
- package/src/args.test.ts +0 -21
- package/src/args.ts +0 -64
- package/src/browser.test.ts +0 -103
- package/src/browser.ts +0 -174
- package/src/cli.ts +0 -656
- package/src/config.test.ts +0 -30
- package/src/config.ts +0 -71
- package/src/errors.ts +0 -14
- package/src/index.ts +0 -25
- package/src/manifest.test.ts +0 -105
- package/src/manifest.ts +0 -126
- package/src/mcp.test.ts +0 -48
- package/src/mcp.ts +0 -438
- package/src/render.ts +0 -424
- package/tsconfig.json +0 -26
package/src/mcp.ts
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import { VivipilotApiClient, type EstimateRequest } from "./api.js";
|
|
6
|
-
import { type CliConfig, type Env, resolveApiUrl } from "./config.js";
|
|
7
|
-
|
|
8
|
-
type StdioLike = {
|
|
9
|
-
stdin: NodeJS.ReadableStream;
|
|
10
|
-
stdout: NodeJS.WritableStream;
|
|
11
|
-
stderr: NodeJS.WritableStream;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type JsonRpcId = string | number | null;
|
|
15
|
-
type JsonRpcRequest = {
|
|
16
|
-
jsonrpc: "2.0";
|
|
17
|
-
id?: JsonRpcId;
|
|
18
|
-
method?: string;
|
|
19
|
-
params?: unknown;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type ToolResult = {
|
|
23
|
-
content: Array<{ type: "text"; text: string }>;
|
|
24
|
-
structuredContent?: Record<string, unknown>;
|
|
25
|
-
isError?: boolean;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type McpServerOptions = {
|
|
29
|
-
config: CliConfig;
|
|
30
|
-
env?: Env;
|
|
31
|
-
} & StdioLike;
|
|
32
|
-
|
|
33
|
-
const PROTOCOL_VERSION = "2025-06-18";
|
|
34
|
-
|
|
35
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function optionalNumber(args: Record<string, unknown>, key: string): number | undefined {
|
|
40
|
-
const value = args[key];
|
|
41
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function optionalString(args: Record<string, unknown>, key: string): string | undefined {
|
|
45
|
-
const value = args[key];
|
|
46
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function estimateRequestFromToolArgs(args: Record<string, unknown>): EstimateRequest {
|
|
50
|
-
const prompt = optionalString(args, "prompt");
|
|
51
|
-
if (!prompt) throw new Error("prompt is required");
|
|
52
|
-
const engine = optionalString(args, "engine");
|
|
53
|
-
const enginePreference = engine === "pixi" || engine === "three" || engine === "auto" ? engine : undefined;
|
|
54
|
-
const width = optionalNumber(args, "width");
|
|
55
|
-
const height = optionalNumber(args, "height");
|
|
56
|
-
const fps = optionalNumber(args, "fps");
|
|
57
|
-
const durationSeconds = optionalNumber(args, "durationSeconds") ?? optionalNumber(args, "duration");
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
prompt,
|
|
61
|
-
...(width || height || fps ? { canvas: { width, height, fps } } : {}),
|
|
62
|
-
...(durationSeconds ? { durationSeconds } : {}),
|
|
63
|
-
...(enginePreference ? { enginePreference } : {}),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function jsonToolResult(value: Record<string, unknown>, isError = false): ToolResult {
|
|
68
|
-
return {
|
|
69
|
-
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
70
|
-
structuredContent: value,
|
|
71
|
-
isError,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function toolError(error: unknown): ToolResult {
|
|
76
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
-
return jsonToolResult({ ok: false, error: message }, true);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function writeJsonFile(path: string, value: unknown): Promise<void> {
|
|
81
|
-
await mkdir(dirname(path), { recursive: true });
|
|
82
|
-
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function verifyLocalManifest(manifestPath: string, config: CliConfig, env: Env) {
|
|
86
|
-
const { verifyManifestFile } = await import("./manifest.js");
|
|
87
|
-
return verifyManifestFile(manifestPath, config, env);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function tools() {
|
|
91
|
-
return [
|
|
92
|
-
{
|
|
93
|
-
name: "vivipilot_get_balance",
|
|
94
|
-
title: "Get Vivipilot paid credit balance",
|
|
95
|
-
description: "Returns Vivipilot account balance and paid top-up balance. CLI/MCP generation is paid-only and never grants trial credits.",
|
|
96
|
-
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: "vivipilot_estimate_motion_layout",
|
|
100
|
-
title: "Estimate Vivipilot motion layout credits",
|
|
101
|
-
description: "Estimates paid credits for a cloud layout generation. This does not burn credits.",
|
|
102
|
-
inputSchema: {
|
|
103
|
-
type: "object",
|
|
104
|
-
properties: {
|
|
105
|
-
prompt: { type: "string" },
|
|
106
|
-
width: { type: "number" },
|
|
107
|
-
height: { type: "number" },
|
|
108
|
-
fps: { type: "number" },
|
|
109
|
-
durationSeconds: { type: "number" },
|
|
110
|
-
engine: { type: "string", enum: ["pixi", "three", "auto"] },
|
|
111
|
-
},
|
|
112
|
-
required: ["prompt"],
|
|
113
|
-
additionalProperties: false,
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "vivipilot_generate_motion_layout",
|
|
118
|
-
title: "Generate signed Vivipilot render manifest",
|
|
119
|
-
description: "Paid-only cloud generation. Burns paid credits only after cloud orchestration is wired; local rendering uses the client's hardware.",
|
|
120
|
-
inputSchema: {
|
|
121
|
-
type: "object",
|
|
122
|
-
properties: {
|
|
123
|
-
prompt: { type: "string" },
|
|
124
|
-
out: { type: "string" },
|
|
125
|
-
idempotencyKey: { type: "string" },
|
|
126
|
-
width: { type: "number" },
|
|
127
|
-
height: { type: "number" },
|
|
128
|
-
fps: { type: "number" },
|
|
129
|
-
durationSeconds: { type: "number" },
|
|
130
|
-
engine: { type: "string", enum: ["pixi", "three", "auto"] },
|
|
131
|
-
},
|
|
132
|
-
required: ["prompt"],
|
|
133
|
-
additionalProperties: false,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
name: "vivipilot_verify_manifest",
|
|
138
|
-
title: "Verify Vivipilot render manifest",
|
|
139
|
-
description: "Verifies a signed Vivipilot manifest locally using public keys. This does not burn credits.",
|
|
140
|
-
inputSchema: {
|
|
141
|
-
type: "object",
|
|
142
|
-
properties: { manifestPath: { type: "string" } },
|
|
143
|
-
required: ["manifestPath"],
|
|
144
|
-
additionalProperties: false,
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
name: "vivipilot_render_video",
|
|
149
|
-
title: "Render Vivipilot video locally",
|
|
150
|
-
description: "Renders a signed manifest on local hardware after signature verification. No cloud rendering is used.",
|
|
151
|
-
inputSchema: {
|
|
152
|
-
type: "object",
|
|
153
|
-
properties: {
|
|
154
|
-
manifestPath: { type: "string" },
|
|
155
|
-
out: { type: "string" },
|
|
156
|
-
verifyOnly: { type: "boolean" },
|
|
157
|
-
},
|
|
158
|
-
required: ["manifestPath"],
|
|
159
|
-
additionalProperties: false,
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: "vivipilot_get_generation_status",
|
|
164
|
-
title: "Get Vivipilot generation status",
|
|
165
|
-
description: "Checks cloud status for a Vivipilot layout generation. This does not burn credits.",
|
|
166
|
-
inputSchema: {
|
|
167
|
-
type: "object",
|
|
168
|
-
properties: { generationId: { type: "string" } },
|
|
169
|
-
required: ["generationId"],
|
|
170
|
-
additionalProperties: false,
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function callTool(name: string, args: unknown, client: VivipilotApiClient, config: CliConfig, env: Env): Promise<ToolResult> {
|
|
177
|
-
const toolArgs = isRecord(args) ? args : {};
|
|
178
|
-
try {
|
|
179
|
-
switch (name) {
|
|
180
|
-
case "vivipilot_get_balance": {
|
|
181
|
-
const balance = await client.balance() as Record<string, unknown>;
|
|
182
|
-
const paidBalance = typeof balance.paidBalance === "number" ? balance.paidBalance : 0;
|
|
183
|
-
const nextActions = paidBalance > 0
|
|
184
|
-
? [
|
|
185
|
-
{ tool: "vivipilot_estimate_motion_layout", reason: "Estimate credits for a generation before spending." },
|
|
186
|
-
{ tool: "vivipilot_generate_motion_layout", reason: "Generate a signed manifest (paid credits will be burned)." },
|
|
187
|
-
]
|
|
188
|
-
: [
|
|
189
|
-
{ action: "topup", reason: `Buy credits at ${resolveApiUrl(config, env)}/edit?billing=topup before generating. CLI/MCP generation is paid-only.` },
|
|
190
|
-
];
|
|
191
|
-
return jsonToolResult({ ok: true, ...balance, nextActions });
|
|
192
|
-
}
|
|
193
|
-
case "vivipilot_estimate_motion_layout": {
|
|
194
|
-
const estimate = await client.estimate(estimateRequestFromToolArgs(toolArgs)) as Record<string, unknown>;
|
|
195
|
-
const estimatedCredits = typeof estimate.estimatedCredits === "number" ? estimate.estimatedCredits : 0;
|
|
196
|
-
return jsonToolResult({
|
|
197
|
-
ok: true,
|
|
198
|
-
...estimate,
|
|
199
|
-
nextActions: [
|
|
200
|
-
{ tool: "vivipilot_generate_motion_layout", reason: `Generate now for ~${estimatedCredits} credits (paid-only).` },
|
|
201
|
-
{ tool: "vivipilot_get_balance", reason: "Check balance before generating." },
|
|
202
|
-
],
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
case "vivipilot_generate_motion_layout": {
|
|
206
|
-
const out = optionalString(toolArgs, "out") ?? "scene.vivi.json";
|
|
207
|
-
const idempotencyKey = optionalString(toolArgs, "idempotencyKey") ?? `mcp_${randomUUID()}`;
|
|
208
|
-
const request = estimateRequestFromToolArgs(toolArgs);
|
|
209
|
-
|
|
210
|
-
// Use async start + poll for progress visibility
|
|
211
|
-
const startResult = await client.startGenerate(
|
|
212
|
-
{ ...request, outputFormat: "manifest" },
|
|
213
|
-
idempotencyKey,
|
|
214
|
-
);
|
|
215
|
-
const generationId = startResult.generationId;
|
|
216
|
-
|
|
217
|
-
// If already completed (idempotent replay), fetch manifest
|
|
218
|
-
if (startResult.idempotentReplay && startResult.status === "completed") {
|
|
219
|
-
const status = await client.generationProgress(generationId);
|
|
220
|
-
if (status.manifestData) {
|
|
221
|
-
await writeJsonFile(out, status.manifestData);
|
|
222
|
-
return jsonToolResult({
|
|
223
|
-
ok: true,
|
|
224
|
-
generationId,
|
|
225
|
-
manifestPath: out,
|
|
226
|
-
creditsCharged: status.creditsCharged ?? null,
|
|
227
|
-
status: "completed",
|
|
228
|
-
nextActions: [
|
|
229
|
-
{ tool: "vivipilot_verify_manifest", args: { manifestPath: out }, reason: "Verify the signed manifest locally before rendering." },
|
|
230
|
-
{ tool: "vivipilot_render_video", args: { manifestPath: out }, reason: "Render the manifest to video on local hardware." },
|
|
231
|
-
],
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Poll until terminal
|
|
237
|
-
const maxWaitMs = 300_000;
|
|
238
|
-
const pollInterval = 2000;
|
|
239
|
-
const startTime = Date.now();
|
|
240
|
-
let lastProgress = "";
|
|
241
|
-
|
|
242
|
-
while (Date.now() - startTime < maxWaitMs) {
|
|
243
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
244
|
-
let status;
|
|
245
|
-
try {
|
|
246
|
-
status = await client.generationProgress(generationId);
|
|
247
|
-
} catch {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (status.progressMessage && status.progressMessage !== lastProgress) {
|
|
252
|
-
lastProgress = status.progressMessage;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (status.status === "completed" && status.manifestData) {
|
|
256
|
-
await writeJsonFile(out, status.manifestData);
|
|
257
|
-
return jsonToolResult({
|
|
258
|
-
ok: true,
|
|
259
|
-
generationId,
|
|
260
|
-
manifestPath: out,
|
|
261
|
-
creditsCharged: status.creditsCharged ?? null,
|
|
262
|
-
engine: status.engine,
|
|
263
|
-
status: "completed",
|
|
264
|
-
progressLog: lastProgress,
|
|
265
|
-
nextActions: [
|
|
266
|
-
{ tool: "vivipilot_verify_manifest", args: { manifestPath: out }, reason: "Verify the signed manifest locally before rendering." },
|
|
267
|
-
{ tool: "vivipilot_render_video", args: { manifestPath: out }, reason: "Render the manifest to video on local hardware." },
|
|
268
|
-
],
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (status.status === "failed") {
|
|
273
|
-
return jsonToolResult({
|
|
274
|
-
ok: false,
|
|
275
|
-
generationId,
|
|
276
|
-
error: status.error ?? "Generation failed.",
|
|
277
|
-
progressLog: lastProgress,
|
|
278
|
-
nextActions: [
|
|
279
|
-
{ tool: "vivipilot_generate_motion_layout", reason: "Retry with a new prompt or idempotency key." },
|
|
280
|
-
{ tool: "vivipilot_get_balance", reason: "Check balance before retrying." },
|
|
281
|
-
],
|
|
282
|
-
}, true);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return jsonToolResult({
|
|
287
|
-
ok: false,
|
|
288
|
-
generationId,
|
|
289
|
-
error: "Generation timed out after 5 minutes.",
|
|
290
|
-
nextActions: [
|
|
291
|
-
{ tool: "vivipilot_get_generation_status", args: { generationId }, reason: "Check if generation completed after timeout." },
|
|
292
|
-
],
|
|
293
|
-
}, true);
|
|
294
|
-
}
|
|
295
|
-
case "vivipilot_verify_manifest": {
|
|
296
|
-
const manifestPath = optionalString(toolArgs, "manifestPath");
|
|
297
|
-
if (!manifestPath) throw new Error("manifestPath is required");
|
|
298
|
-
const result = await verifyLocalManifest(manifestPath, config, env);
|
|
299
|
-
if (!result.ok) throw new Error(result.message);
|
|
300
|
-
return jsonToolResult({
|
|
301
|
-
ok: true,
|
|
302
|
-
manifestId: result.manifest.manifestId,
|
|
303
|
-
generationId: result.manifest.generationId,
|
|
304
|
-
canonicalPayloadHash: result.canonicalPayloadHash,
|
|
305
|
-
nextActions: [
|
|
306
|
-
{ tool: "vivipilot_render_video", args: { manifestPath }, reason: "Render the verified manifest to video." },
|
|
307
|
-
],
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
case "vivipilot_render_video": {
|
|
311
|
-
const manifestPath = optionalString(toolArgs, "manifestPath");
|
|
312
|
-
if (!manifestPath) throw new Error("manifestPath is required");
|
|
313
|
-
const out = optionalString(toolArgs, "out") ?? "video.mp4";
|
|
314
|
-
const format = optionalString(toolArgs, "format") as "mp4" | "webm" | "gif" | "mov" | undefined;
|
|
315
|
-
const scale = optionalNumber(toolArgs, "scale");
|
|
316
|
-
const fps = optionalNumber(toolArgs, "fps");
|
|
317
|
-
const transparent = toolArgs.transparent === true;
|
|
318
|
-
const verifyOnly = toolArgs.verifyOnly === true;
|
|
319
|
-
|
|
320
|
-
const { renderManifest } = await import("./render.js");
|
|
321
|
-
const result = await renderManifest(
|
|
322
|
-
{
|
|
323
|
-
manifestPath,
|
|
324
|
-
outPath: out,
|
|
325
|
-
...(format ? { format } : {}),
|
|
326
|
-
...(scale ? { scale } : {}),
|
|
327
|
-
...(fps ? { fps } : {}),
|
|
328
|
-
...(transparent ? { transparent } : {}),
|
|
329
|
-
verifyOnly,
|
|
330
|
-
},
|
|
331
|
-
config,
|
|
332
|
-
env,
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
return jsonToolResult({
|
|
336
|
-
ok: true,
|
|
337
|
-
manifestId: result.manifestId,
|
|
338
|
-
out: result.outPath,
|
|
339
|
-
size: result.size,
|
|
340
|
-
nextActions: [
|
|
341
|
-
{ action: "file_ready", reason: `Video written to ${result.outPath} (${(result.size / 1024 / 1024).toFixed(2)} MB).` },
|
|
342
|
-
],
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
case "vivipilot_get_generation_status": {
|
|
346
|
-
const generationId = optionalString(toolArgs, "generationId");
|
|
347
|
-
if (!generationId) throw new Error("generationId is required");
|
|
348
|
-
const status = await client.generationProgress(generationId) as Record<string, unknown>;
|
|
349
|
-
const nextActions: Array<{ tool: string; args?: Record<string, unknown>; reason: string }> = [];
|
|
350
|
-
if (status.status === "completed" && status.manifest) {
|
|
351
|
-
nextActions.push({ tool: "vivipilot_verify_manifest", reason: "Verify the completed manifest." });
|
|
352
|
-
nextActions.push({ tool: "vivipilot_render_video", reason: "Render the completed manifest to video." });
|
|
353
|
-
} else if (status.status === "failed") {
|
|
354
|
-
nextActions.push({ tool: "vivipilot_generate_motion_layout", reason: "Retry with a new idempotency key." });
|
|
355
|
-
nextActions.push({ tool: "vivipilot_get_balance", reason: "Check balance before retrying." });
|
|
356
|
-
} else {
|
|
357
|
-
nextActions.push({ tool: "vivipilot_get_generation_status", args: { generationId }, reason: "Poll again — generation still in progress." });
|
|
358
|
-
}
|
|
359
|
-
return jsonToolResult({ ok: true, ...status, nextActions });
|
|
360
|
-
}
|
|
361
|
-
default:
|
|
362
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
363
|
-
}
|
|
364
|
-
} catch (error) {
|
|
365
|
-
return toolError(error);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function send(stdout: NodeJS.WritableStream, message: Record<string, unknown>): void {
|
|
370
|
-
stdout.write(`${JSON.stringify(message)}\n`);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function sendResult(stdout: NodeJS.WritableStream, id: JsonRpcId, result: unknown): void {
|
|
374
|
-
send(stdout, { jsonrpc: "2.0", id, result });
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function sendError(stdout: NodeJS.WritableStream, id: JsonRpcId | undefined, code: number, message: string): void {
|
|
378
|
-
send(stdout, { jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export async function startMcpServer(options: McpServerOptions): Promise<void> {
|
|
382
|
-
const env = options.env ?? process.env;
|
|
383
|
-
const client = new VivipilotApiClient({ config: options.config, env });
|
|
384
|
-
const lines = createInterface({ input: options.stdin });
|
|
385
|
-
|
|
386
|
-
for await (const line of lines) {
|
|
387
|
-
const trimmed = line.trim();
|
|
388
|
-
if (!trimmed) continue;
|
|
389
|
-
|
|
390
|
-
let message: JsonRpcRequest;
|
|
391
|
-
try {
|
|
392
|
-
message = JSON.parse(trimmed) as JsonRpcRequest;
|
|
393
|
-
} catch {
|
|
394
|
-
sendError(options.stdout, null, -32700, "Parse error");
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const id = message.id;
|
|
399
|
-
try {
|
|
400
|
-
switch (message.method) {
|
|
401
|
-
case "initialize":
|
|
402
|
-
sendResult(options.stdout, id ?? null, {
|
|
403
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
404
|
-
capabilities: { tools: { listChanged: false } },
|
|
405
|
-
serverInfo: {
|
|
406
|
-
name: "vivipilot",
|
|
407
|
-
title: "Vivipilot MCP",
|
|
408
|
-
version: "0.1.0",
|
|
409
|
-
},
|
|
410
|
-
instructions: "Vivipilot generation tools are paid-only and require VIVIPILOT_API_KEY or `vivipilot login` configuration. Local render tools use the client's hardware.",
|
|
411
|
-
});
|
|
412
|
-
break;
|
|
413
|
-
case "notifications/initialized":
|
|
414
|
-
break;
|
|
415
|
-
case "ping":
|
|
416
|
-
sendResult(options.stdout, id ?? null, {});
|
|
417
|
-
break;
|
|
418
|
-
case "tools/list":
|
|
419
|
-
sendResult(options.stdout, id ?? null, { tools: tools() });
|
|
420
|
-
break;
|
|
421
|
-
case "tools/call": {
|
|
422
|
-
if (!isRecord(message.params) || typeof message.params.name !== "string") {
|
|
423
|
-
sendError(options.stdout, id, -32602, "tools/call requires params.name");
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
const result = await callTool(message.params.name, message.params.arguments, client, options.config, env);
|
|
427
|
-
sendResult(options.stdout, id ?? null, result);
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
default:
|
|
431
|
-
if (id !== undefined) sendError(options.stdout, id, -32601, `Method not found: ${message.method ?? ""}`);
|
|
432
|
-
}
|
|
433
|
-
} catch (error) {
|
|
434
|
-
const messageText = error instanceof Error ? error.message : String(error);
|
|
435
|
-
if (id !== undefined) sendError(options.stdout, id, -32603, messageText);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|