@warlok-net/mcp-server 0.1.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.
package/dist/cli.js ADDED
@@ -0,0 +1,719 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
28
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
30
+ var import_sdk = require("@warlok-net/sdk");
31
+
32
+ // src/screenshot.ts
33
+ var import_promises = __toESM(require("fs/promises"));
34
+ var import_node_path = __toESM(require("path"));
35
+ async function loadChromium() {
36
+ try {
37
+ const playwright = await import("playwright");
38
+ return playwright.chromium;
39
+ } catch {
40
+ throw new Error(
41
+ "capture_ui_screenshot requires Playwright. Install it with: npm i playwright && npx playwright install chromium"
42
+ );
43
+ }
44
+ }
45
+ var DEFAULT_VIEWPORT_WIDTH = 1440;
46
+ var DEFAULT_VIEWPORT_HEIGHT = 900;
47
+ var DEFAULT_TIMEOUT_MS = 3e4;
48
+ var DEFAULT_WAIT_MS = 250;
49
+ var DEFAULT_FORMAT = "png";
50
+ async function captureUiScreenshot(input) {
51
+ const startedAt = Date.now();
52
+ const url = parseHttpUrl(input.url);
53
+ const width = clampInteger(input.viewportWidth, 320, 7680, DEFAULT_VIEWPORT_WIDTH);
54
+ const height = clampInteger(
55
+ input.viewportHeight,
56
+ 240,
57
+ 4320,
58
+ DEFAULT_VIEWPORT_HEIGHT
59
+ );
60
+ const timeoutMs = clampInteger(input.timeoutMs, 5e3, 12e4, DEFAULT_TIMEOUT_MS);
61
+ const waitForMs = clampInteger(input.waitForMs, 0, 3e4, DEFAULT_WAIT_MS);
62
+ const fullPage = input.fullPage ?? true;
63
+ const colorScheme = input.colorScheme ?? "dark";
64
+ const format = input.format ?? inferFormatFromOutputPath(input.outputPath);
65
+ const outputPath = resolveOutputPath(url, input.outputPath, format);
66
+ await import_promises.default.mkdir(import_node_path.default.dirname(outputPath), { recursive: true });
67
+ const chromium = await loadChromium();
68
+ const browser = await chromium.launch({ headless: true });
69
+ try {
70
+ const context = await browser.newContext({
71
+ viewport: { width, height },
72
+ colorScheme,
73
+ deviceScaleFactor: 1
74
+ });
75
+ const page = await context.newPage();
76
+ page.setDefaultTimeout(timeoutMs);
77
+ await page.goto(url.toString(), { waitUntil: "networkidle", timeout: timeoutMs });
78
+ if (input.waitForSelector) {
79
+ await page.waitForSelector(input.waitForSelector, {
80
+ state: "visible",
81
+ timeout: timeoutMs
82
+ });
83
+ }
84
+ if (waitForMs > 0) {
85
+ await page.waitForTimeout(waitForMs);
86
+ }
87
+ if (input.selector) {
88
+ const locator = page.locator(input.selector).first();
89
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
90
+ await locator.screenshot({
91
+ path: outputPath,
92
+ type: format
93
+ });
94
+ } else {
95
+ await page.screenshot({
96
+ path: outputPath,
97
+ fullPage,
98
+ type: format
99
+ });
100
+ }
101
+ await context.close();
102
+ } finally {
103
+ await browser.close();
104
+ }
105
+ const stat = await import_promises.default.stat(outputPath);
106
+ return {
107
+ outputPath,
108
+ bytes: stat.size,
109
+ width,
110
+ height,
111
+ url: url.toString(),
112
+ selector: input.selector ?? null,
113
+ fullPage,
114
+ colorScheme,
115
+ format,
116
+ durationMs: Date.now() - startedAt
117
+ };
118
+ }
119
+ function parseHttpUrl(value) {
120
+ let parsed;
121
+ try {
122
+ parsed = new URL(value);
123
+ } catch {
124
+ throw new Error("Invalid URL. Expected a valid absolute URL.");
125
+ }
126
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
127
+ throw new Error("Invalid URL protocol. Only http/https URLs are supported.");
128
+ }
129
+ return parsed;
130
+ }
131
+ function resolveOutputPath(url, requestedPath, format) {
132
+ if (requestedPath) {
133
+ const absolute = import_node_path.default.isAbsolute(requestedPath) ? requestedPath : import_node_path.default.resolve(outputRoot(), requestedPath);
134
+ return ensureExt(absolute, format);
135
+ }
136
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
137
+ const host = sanitizeSegment(url.host);
138
+ const pathname = sanitizeSegment(url.pathname.replace(/\//g, "_") || "root");
139
+ const filename = `${now}-${host}-${pathname}.${format}`;
140
+ return import_node_path.default.join(outputRoot(), filename);
141
+ }
142
+ function outputRoot() {
143
+ const configured = process.env.WARLOK_SCREENSHOT_DIR;
144
+ if (configured && configured.trim()) {
145
+ return import_node_path.default.resolve(configured.trim());
146
+ }
147
+ return import_node_path.default.resolve(process.cwd(), "tmp", "mcp-screenshots");
148
+ }
149
+ function ensureExt(value, format) {
150
+ const ext = import_node_path.default.extname(value).toLowerCase();
151
+ if (ext === ".png" || ext === ".jpg" || ext === ".jpeg" || ext === ".webp") {
152
+ return value;
153
+ }
154
+ return `${value}.${format}`;
155
+ }
156
+ function sanitizeSegment(value) {
157
+ return value.toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "capture";
158
+ }
159
+ function inferFormatFromOutputPath(pathname) {
160
+ if (!pathname) return DEFAULT_FORMAT;
161
+ const ext = import_node_path.default.extname(pathname).toLowerCase();
162
+ if (ext === ".jpg" || ext === ".jpeg") return "jpeg";
163
+ return "png";
164
+ }
165
+ function clampInteger(value, min, max, fallback) {
166
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
167
+ const rounded = Math.round(value);
168
+ if (rounded < min) return min;
169
+ if (rounded > max) return max;
170
+ return rounded;
171
+ }
172
+
173
+ // src/index.ts
174
+ var TOOLS = [
175
+ {
176
+ name: "generate_3d_model",
177
+ description: "Generate a 3D model from text description or image. Returns an asset_id to track status.",
178
+ inputSchema: {
179
+ type: "object",
180
+ properties: {
181
+ name: {
182
+ type: "string",
183
+ description: "Name for the generation (max 100 characters)"
184
+ },
185
+ type: {
186
+ type: "string",
187
+ enum: [
188
+ "character",
189
+ "creature",
190
+ "environment",
191
+ "prop",
192
+ "weapon",
193
+ "vehicle",
194
+ "material",
195
+ "noun",
196
+ "person",
197
+ "place",
198
+ "thing"
199
+ ],
200
+ description: "Entity or legacy asset type"
201
+ },
202
+ prompt: {
203
+ type: "string",
204
+ description: "Text prompt describing what to generate"
205
+ },
206
+ image_url: {
207
+ type: "string",
208
+ description: "Optional URL to a reference image"
209
+ },
210
+ auto_rig: {
211
+ type: "boolean",
212
+ description: "Whether to rig for Mixamo when generating a person",
213
+ default: true
214
+ }
215
+ },
216
+ required: ["type"]
217
+ }
218
+ },
219
+ {
220
+ name: "get_model_status",
221
+ description: "Check generation status by asset_id.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ asset_id: {
226
+ type: "string",
227
+ description: "The generation ID"
228
+ }
229
+ },
230
+ required: ["asset_id"]
231
+ }
232
+ },
233
+ {
234
+ name: "list_models",
235
+ description: "List generations for the current account.",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ limit: {
240
+ type: "number",
241
+ description: "Maximum rows to return (default: 20)",
242
+ default: 20
243
+ },
244
+ type: {
245
+ type: "string",
246
+ enum: [
247
+ "character",
248
+ "creature",
249
+ "environment",
250
+ "prop",
251
+ "weapon",
252
+ "vehicle",
253
+ "material",
254
+ "noun",
255
+ "person",
256
+ "place",
257
+ "thing"
258
+ ],
259
+ description: "Filter by entity/legacy type"
260
+ },
261
+ status: {
262
+ type: "string",
263
+ enum: [
264
+ "pending",
265
+ "preprocessing",
266
+ "generating",
267
+ "rigging",
268
+ "complete",
269
+ "failed"
270
+ ],
271
+ description: "Filter by generation status"
272
+ }
273
+ }
274
+ }
275
+ },
276
+ {
277
+ name: "get_model_download_url",
278
+ description: "Get the best available download URL for a completed generation.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ asset_id: {
283
+ type: "string",
284
+ description: "The generation ID"
285
+ },
286
+ format: {
287
+ type: "string",
288
+ enum: ["model", "rigged"],
289
+ description: "Preferred format (default: best available)"
290
+ }
291
+ },
292
+ required: ["asset_id"]
293
+ }
294
+ },
295
+ {
296
+ name: "delete_model",
297
+ description: "Delete a generation.",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ asset_id: {
302
+ type: "string",
303
+ description: "The generation ID to delete"
304
+ }
305
+ },
306
+ required: ["asset_id"]
307
+ }
308
+ },
309
+ {
310
+ name: "capture_ui_screenshot",
311
+ description: "Capture a deterministic Playwright screenshot for UI/design iteration.",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {
315
+ url: {
316
+ type: "string",
317
+ description: "HTTP/HTTPS URL to capture"
318
+ },
319
+ output_path: {
320
+ type: "string",
321
+ description: "Optional output file path. Relative paths are resolved in tmp/mcp-screenshots."
322
+ },
323
+ selector: {
324
+ type: "string",
325
+ description: "Optional CSS selector to capture only a specific element."
326
+ },
327
+ wait_for_selector: {
328
+ type: "string",
329
+ description: "Optional selector to wait for before capture (useful for async UI)."
330
+ },
331
+ wait_for_ms: {
332
+ type: "number",
333
+ description: "Extra delay before capture in milliseconds (default: 250).",
334
+ default: 250
335
+ },
336
+ timeout_ms: {
337
+ type: "number",
338
+ description: "Navigation and selector timeout in milliseconds (default: 30000).",
339
+ default: 3e4
340
+ },
341
+ full_page: {
342
+ type: "boolean",
343
+ description: "Capture full page when selector is not set (default: true).",
344
+ default: true
345
+ },
346
+ viewport_width: {
347
+ type: "number",
348
+ description: "Viewport width in px (default: 1440).",
349
+ default: 1440
350
+ },
351
+ viewport_height: {
352
+ type: "number",
353
+ description: "Viewport height in px (default: 900).",
354
+ default: 900
355
+ },
356
+ color_scheme: {
357
+ type: "string",
358
+ enum: ["light", "dark", "no-preference"],
359
+ description: "Preferred color scheme (default: dark).",
360
+ default: "dark"
361
+ },
362
+ format: {
363
+ type: "string",
364
+ enum: ["png", "jpeg"],
365
+ description: "Screenshot format (default inferred from output_path, else png)."
366
+ }
367
+ },
368
+ required: ["url"]
369
+ }
370
+ }
371
+ ];
372
+ function createServer(apiKey2, baseUrl2) {
373
+ const client = new import_sdk.WarlokClient({ apiKey: apiKey2, baseUrl: baseUrl2 });
374
+ const server = new import_server.Server(
375
+ {
376
+ name: "warlok-mcp",
377
+ version: "0.1.0"
378
+ },
379
+ {
380
+ capabilities: {
381
+ tools: {}
382
+ }
383
+ }
384
+ );
385
+ server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
386
+ tools: TOOLS
387
+ }));
388
+ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
389
+ const { name, arguments: args } = request.params;
390
+ try {
391
+ switch (name) {
392
+ case "generate_3d_model": {
393
+ const generation = await client.generate({
394
+ name: stringOrUndefined(args, "name"),
395
+ type: requiredType(args),
396
+ prompt: stringOrUndefined(args, "prompt"),
397
+ imageUrl: stringOrUndefined(args, "image_url"),
398
+ options: {
399
+ autoRig: booleanOrUndefined(args, "auto_rig")
400
+ }
401
+ });
402
+ return {
403
+ content: [
404
+ {
405
+ type: "text",
406
+ text: JSON.stringify(
407
+ {
408
+ success: true,
409
+ asset_id: generation.id,
410
+ name: generation.name,
411
+ entity_type: generation.entityType,
412
+ status: generation.status,
413
+ message: `Generation started. Use get_model_status with asset_id "${generation.id}".`
414
+ },
415
+ null,
416
+ 2
417
+ )
418
+ }
419
+ ]
420
+ };
421
+ }
422
+ case "get_model_status": {
423
+ const generation = await client.getGeneration(requiredAssetId(args));
424
+ return {
425
+ content: [
426
+ {
427
+ type: "text",
428
+ text: JSON.stringify(formatGenerationStatus(generation), null, 2)
429
+ }
430
+ ]
431
+ };
432
+ }
433
+ case "list_models": {
434
+ const generations = await client.listGenerations({
435
+ limit: numberOrUndefined(args, "limit") || 20,
436
+ type: typeOrUndefined(args),
437
+ status: statusOrUndefined(args)
438
+ });
439
+ return {
440
+ content: [
441
+ {
442
+ type: "text",
443
+ text: JSON.stringify(
444
+ {
445
+ total: generations.length,
446
+ models: generations.map((generation) => ({
447
+ id: generation.id,
448
+ name: generation.name,
449
+ entity_type: generation.entityType,
450
+ status: generation.status,
451
+ created_at: generation.createdAt
452
+ }))
453
+ },
454
+ null,
455
+ 2
456
+ )
457
+ }
458
+ ]
459
+ };
460
+ }
461
+ case "get_model_download_url": {
462
+ const generation = await client.getGeneration(requiredAssetId(args));
463
+ if (generation.status !== "complete") {
464
+ return {
465
+ content: [
466
+ {
467
+ type: "text",
468
+ text: JSON.stringify({
469
+ success: false,
470
+ error: `Generation is not complete. Current status: ${generation.status}`
471
+ })
472
+ }
473
+ ]
474
+ };
475
+ }
476
+ const format = downloadFormatOrUndefined(args);
477
+ const url = format === "model" ? generation.outputs.modelUrl : format === "rigged" ? generation.outputs.riggedModelUrl : generation.outputs.riggedModelUrl || generation.outputs.modelUrl;
478
+ if (!url) {
479
+ return {
480
+ content: [
481
+ {
482
+ type: "text",
483
+ text: JSON.stringify({
484
+ success: false,
485
+ error: "No download URL available for this generation"
486
+ })
487
+ }
488
+ ]
489
+ };
490
+ }
491
+ return {
492
+ content: [
493
+ {
494
+ type: "text",
495
+ text: JSON.stringify(
496
+ {
497
+ success: true,
498
+ asset_id: generation.id,
499
+ name: generation.name,
500
+ download_url: url,
501
+ format: format || "best_available",
502
+ prompt: generation.input.prompt || null,
503
+ preview_png_url: generation.outputs.previewPngUrl || null,
504
+ nanobanana_png_url: generation.outputs.promptImageUrl || generation.provenance?.preprocessPromptImageUrl || null
505
+ },
506
+ null,
507
+ 2
508
+ )
509
+ }
510
+ ]
511
+ };
512
+ }
513
+ case "delete_model": {
514
+ const assetId = requiredAssetId(args);
515
+ await client.deleteGeneration(assetId);
516
+ return {
517
+ content: [
518
+ {
519
+ type: "text",
520
+ text: JSON.stringify({
521
+ success: true,
522
+ message: `Generation ${assetId} deleted successfully`
523
+ })
524
+ }
525
+ ]
526
+ };
527
+ }
528
+ case "capture_ui_screenshot": {
529
+ const screenshot = await captureUiScreenshot({
530
+ url: requiredUrl(args),
531
+ outputPath: stringOrUndefined(args, "output_path"),
532
+ selector: stringOrUndefined(args, "selector"),
533
+ waitForSelector: stringOrUndefined(args, "wait_for_selector"),
534
+ waitForMs: numberOrUndefined(args, "wait_for_ms"),
535
+ timeoutMs: numberOrUndefined(args, "timeout_ms"),
536
+ fullPage: booleanOrUndefined(args, "full_page"),
537
+ viewportWidth: numberOrUndefined(args, "viewport_width"),
538
+ viewportHeight: numberOrUndefined(args, "viewport_height"),
539
+ colorScheme: colorSchemeOrUndefined(args),
540
+ format: screenshotFormatOrUndefined(args)
541
+ });
542
+ return {
543
+ content: [
544
+ {
545
+ type: "text",
546
+ text: JSON.stringify(
547
+ {
548
+ success: true,
549
+ ...screenshot
550
+ },
551
+ null,
552
+ 2
553
+ )
554
+ }
555
+ ]
556
+ };
557
+ }
558
+ default:
559
+ throw new import_types.McpError(import_types.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
560
+ }
561
+ } catch (error) {
562
+ if (error instanceof import_types.McpError) {
563
+ throw error;
564
+ }
565
+ const message = error instanceof Error ? error.message : "Unknown error";
566
+ return {
567
+ content: [
568
+ {
569
+ type: "text",
570
+ text: JSON.stringify({ success: false, error: message })
571
+ }
572
+ ],
573
+ isError: true
574
+ };
575
+ }
576
+ });
577
+ return server;
578
+ }
579
+ function requiredAssetId(args) {
580
+ const value = stringOrUndefined(args, "asset_id");
581
+ if (!value) {
582
+ throw new import_types.McpError(import_types.ErrorCode.InvalidParams, "asset_id is required");
583
+ }
584
+ return value;
585
+ }
586
+ function requiredType(args) {
587
+ const value = stringOrUndefined(args, "type");
588
+ if (!value) {
589
+ throw new import_types.McpError(import_types.ErrorCode.InvalidParams, "type is required");
590
+ }
591
+ return value;
592
+ }
593
+ function requiredUrl(args) {
594
+ const value = stringOrUndefined(args, "url");
595
+ if (!value) {
596
+ throw new import_types.McpError(import_types.ErrorCode.InvalidParams, "url is required");
597
+ }
598
+ return value;
599
+ }
600
+ function typeOrUndefined(args) {
601
+ const value = stringOrUndefined(args, "type");
602
+ return value;
603
+ }
604
+ function statusOrUndefined(args) {
605
+ const value = stringOrUndefined(args, "status");
606
+ return value;
607
+ }
608
+ function downloadFormatOrUndefined(args) {
609
+ const value = stringOrUndefined(args, "format");
610
+ if (value === "model" || value === "rigged") {
611
+ return value;
612
+ }
613
+ return void 0;
614
+ }
615
+ function stringOrUndefined(args, key) {
616
+ const value = args?.[key];
617
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
618
+ }
619
+ function booleanOrUndefined(args, key) {
620
+ const value = args?.[key];
621
+ return typeof value === "boolean" ? value : void 0;
622
+ }
623
+ function numberOrUndefined(args, key) {
624
+ const value = args?.[key];
625
+ return typeof value === "number" ? value : void 0;
626
+ }
627
+ function colorSchemeOrUndefined(args) {
628
+ const value = stringOrUndefined(args, "color_scheme");
629
+ if (value === "light" || value === "dark" || value === "no-preference") {
630
+ return value;
631
+ }
632
+ return void 0;
633
+ }
634
+ function screenshotFormatOrUndefined(args) {
635
+ const value = stringOrUndefined(args, "format");
636
+ if (value === "png" || value === "jpeg") {
637
+ return value;
638
+ }
639
+ return void 0;
640
+ }
641
+ function formatGenerationStatus(generation) {
642
+ const base = {
643
+ success: true,
644
+ asset_id: generation.id,
645
+ name: generation.name,
646
+ entity_type: generation.entityType,
647
+ status: generation.status,
648
+ progress: generation.progress,
649
+ prompt: generation.input.prompt || null,
650
+ preview_png_url: generation.outputs.previewPngUrl || null,
651
+ nanobanana_png_url: generation.outputs.promptImageUrl || generation.provenance?.preprocessPromptImageUrl || null,
652
+ created_at: generation.createdAt,
653
+ updated_at: generation.updatedAt
654
+ };
655
+ if (generation.status === "complete") {
656
+ return {
657
+ ...base,
658
+ model_url: generation.outputs.modelUrl,
659
+ rigged_model_url: generation.outputs.riggedModelUrl,
660
+ mixamo_ready: generation.outputs.mixamoReady
661
+ };
662
+ }
663
+ if (generation.status === "failed") {
664
+ return {
665
+ ...base,
666
+ error: generation.error
667
+ };
668
+ }
669
+ return {
670
+ ...base,
671
+ message: getStatusMessage(generation.status, generation.progress)
672
+ };
673
+ }
674
+ function getStatusMessage(status, progress) {
675
+ const messages = {
676
+ pending: "Queued for processing",
677
+ preprocessing: "Preparing specialized PNG",
678
+ generating: "Generating 3D mesh",
679
+ rigging: "Rigging for Mixamo"
680
+ };
681
+ return `${messages[status] || "Processing"} (${progress}% complete)`;
682
+ }
683
+ async function runServer(apiKey2, baseUrl2) {
684
+ const server = createServer(apiKey2, baseUrl2);
685
+ const transport = new import_stdio.StdioServerTransport();
686
+ await server.connect(transport);
687
+ process.on("SIGINT", async () => {
688
+ await server.close();
689
+ process.exit(0);
690
+ });
691
+ }
692
+
693
+ // src/cli.ts
694
+ var apiKey = process.env.WARLOK_API_KEY;
695
+ var baseUrl = process.env.WARLOK_API_URL;
696
+ if (!apiKey) {
697
+ console.error("Error: WARLOK_API_KEY environment variable is required");
698
+ console.error("");
699
+ console.error("Usage:");
700
+ console.error(" WARLOK_API_KEY=wk_... warlok-mcp");
701
+ console.error("");
702
+ console.error("Or set it in your Claude Desktop config:");
703
+ console.error(" {");
704
+ console.error(' "mcpServers": {');
705
+ console.error(' "warlok": {');
706
+ console.error(' "command": "npx",');
707
+ console.error(' "args": ["@warlok-net/mcp-server"],');
708
+ console.error(' "env": {');
709
+ console.error(' "WARLOK_API_KEY": "wk_..."');
710
+ console.error(" }");
711
+ console.error(" }");
712
+ console.error(" }");
713
+ console.error(" }");
714
+ process.exit(1);
715
+ }
716
+ runServer(apiKey, baseUrl).catch((error) => {
717
+ console.error("Server error:", error);
718
+ process.exit(1);
719
+ });