@candledottv/mcp 0.10.19 → 0.10.21

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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +195 -49
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,8 +57,10 @@ Both take decimal amounts, never raw base units, and resolve the scale themselve
57
57
 
58
58
  Both launch tools also take `mode: "exp-custom"` with `curve: { virtualQuote, threshold }`: a
59
59
  Solana curve with your own V and G, in whole SOL or CNDL as decimal strings, inside limits the
60
- API enforces and each tool's description states. Dry-run first: the dry run returns the curve's
61
- terms. It works only where the deployment has switched it on. See
60
+ API enforces and each tool's description states. Any `exp-` mode takes `creatorFeeBps` (0, 100 or
61
+ 200). Dry-run first: the dry run returns the launch preview (`curveTerms`: the curve's terms, the
62
+ share your own `buyAmount` takes, the fees and the migration terms) and `devBuyMax`, the dev-buy
63
+ ceiling that applies. `exp-custom` works only where the deployment has switched it on. See
62
64
  [Custom curves](https://docs.candle.tv/developers/headless-launch#custom-curves-choose-v-and-g-switched-off).
63
65
 
64
66
  For a full walkthrough of `candle_launch_and_seed` and `candle_trade`, including the keyless read
package/dist/index.js CHANGED
@@ -151,7 +151,7 @@ import { lstat, open, realpath } from "node:fs/promises";
151
151
  import { dirname, isAbsolute, relative, sep } from "node:path";
152
152
 
153
153
  // src/version.ts
154
- var SERVER_VERSION = "0.10.19";
154
+ var SERVER_VERSION = "0.10.21";
155
155
 
156
156
  // src/update-notice.ts
157
157
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -257,13 +257,13 @@ async function missingInside(dirReal, path, fs) {
257
257
  }
258
258
  }
259
259
  }
260
- async function readImageInDir(imageDir, imagePath, fs = nodeFs) {
260
+ async function readImageInDir(imageDir, imagePath, fs = nodeFs, field = "imagePath") {
261
261
  if (!isAbsolute(imagePath))
262
- throw new ImageRefusal("MCP_VALIDATION", `imagePath must be an absolute path inside CANDLE_IMAGE_DIR; this server's working directory is not something a caller can know. ${NOTHING_UPLOADED}`);
262
+ throw new ImageRefusal("MCP_VALIDATION", `${field} must be an absolute path inside CANDLE_IMAGE_DIR; this server's working directory is not something a caller can know. ${NOTHING_UPLOADED}`);
263
263
  const dirReal = await realImageDir(imageDir, fs);
264
264
  const outside = () => new ImageRefusal("IMAGE_OUTSIDE_DIR", OUTSIDE_IMAGE_DIR);
265
- const unreadable = (why) => new ImageRefusal("IMAGE_UNREADABLE", `Could not read the image at imagePath: ${why}. ${NOTHING_UPLOADED}`);
266
- const tooLarge = () => new ImageRefusal("IMAGE_TOO_LARGE", `The file at imagePath is over 10MB, the largest image Candle hosts. ${NOTHING_UPLOADED}`);
265
+ const unreadable = (why) => new ImageRefusal("IMAGE_UNREADABLE", `Could not read the image at ${field}: ${why}. ${NOTHING_UPLOADED}`);
266
+ const tooLarge = () => new ImageRefusal("IMAGE_TOO_LARGE", `The file at ${field} is over 10MB, the largest image Candle hosts. ${NOTHING_UPLOADED}`);
267
267
  let fileReal;
268
268
  try {
269
269
  fileReal = await fs.realpath(imagePath);
@@ -321,47 +321,22 @@ async function readImageInDir(imageDir, imagePath, fs = nodeFs) {
321
321
  }
322
322
  const contentType = sniffImageType(bytes);
323
323
  if (!contentType)
324
- throw new ImageRefusal("IMAGE_TYPE_UNSUPPORTED", `The file at imagePath is not a PNG, JPEG, GIF or WebP image. The type is read from the file's first bytes, not its name. ${NOTHING_UPLOADED}`);
324
+ throw new ImageRefusal("IMAGE_TYPE_UNSUPPORTED", `The file at ${field} is not a PNG, JPEG, GIF or WebP image. The type is read from the file's first bytes, not its name. ${NOTHING_UPLOADED}`);
325
325
  return { bytes, contentType };
326
326
  }
327
- async function resolveLaunchImage(args, imageDir, cfg, doFetch, fs = nodeFs) {
328
- const { imagePath, ...rest } = args;
329
- const hasPath = typeof imagePath === "string" && imagePath.length > 0;
330
- const hasUrl = typeof rest.imageUrl === "string" && rest.imageUrl.length > 0;
331
- const invalid = (message) => ({ refused: { code: "MCP_VALIDATION", message } });
332
- if (imageDir === undefined) {
333
- if (imagePath !== undefined)
334
- return invalid(`imagePath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass imageUrl, an https URL. ${NOTHING_UPLOADED}`);
335
- return { args: rest };
336
- }
337
- if (hasPath && hasUrl)
338
- return invalid(`Pass exactly one of imageUrl or imagePath, not both. ${NOTHING_UPLOADED}`);
339
- if (!hasPath && !hasUrl)
340
- return invalid("Pass imageUrl (an https URL) or imagePath (an absolute path to an image file inside CANDLE_IMAGE_DIR).");
341
- if (!hasPath)
342
- return { args: rest };
343
- if (!cfg.apiKey)
344
- return invalid(`CANDLE_AGENT_API_KEY is required to upload an image; the key needs the launch:write scope. ${NOTHING_UPLOADED}`);
345
- let image;
346
- try {
347
- image = await readImageInDir(imageDir, imagePath, fs);
348
- } catch (error) {
349
- if (error instanceof ImageRefusal)
350
- return { refused: { code: error.code, message: error.message } };
351
- throw error;
352
- }
327
+ async function uploadLocalImage(image, kind, cfg, apiKey, doFetch) {
353
328
  let res;
354
329
  try {
355
- res = await doFetch(`${cfg.apiUrl.replace(/\/$/, "")}/api/v1/uploads/agent-image`, {
330
+ res = await doFetch(`${cfg.apiUrl.replace(/\/$/, "")}/api/v1/uploads/agent-image${kind === "banner" ? "?kind=banner" : ""}`, {
356
331
  method: "POST",
357
- headers: { "Content-Type": image.contentType, "x-api-key": cfg.apiKey },
332
+ headers: { "Content-Type": image.contentType, "x-api-key": apiKey },
358
333
  body: image.bytes
359
334
  });
360
335
  } catch (error) {
361
336
  return {
362
337
  refused: {
363
338
  code: "MCP_TRANSPORT",
364
- message: `The image upload failed in transit (${error instanceof Error ? error.message : String(error)}). ` + "No launch was attempted. The upload may or may not have reached Candle; if it did, the file is public."
339
+ message: `The ${kind} upload failed in transit (${error instanceof Error ? error.message : String(error)}). ` + "No launch was attempted. The upload may or may not have reached Candle; if it did, the file is public."
365
340
  }
366
341
  };
367
342
  }
@@ -379,15 +354,92 @@ async function resolveLaunchImage(args, imageDir, cfg, doFetch, fs = nodeFs) {
379
354
  return {
380
355
  refused: {
381
356
  code: "MCP_TRANSPORT",
382
- message: "Candle did not return an https URL for the uploaded image. No launch was attempted."
357
+ message: `Candle did not return an https URL for the uploaded ${kind}. No launch was attempted.`
383
358
  }
384
359
  };
385
- return { args: { ...rest, imageUrl: uploaded }, uploaded };
360
+ return { url: uploaded };
361
+ }
362
+ async function resolveLaunchImage(args, imageDir, cfg, doFetch, fs = nodeFs) {
363
+ const { imagePath, bannerPath, ...rest } = args;
364
+ const hasPath = typeof imagePath === "string" && imagePath.length > 0;
365
+ const hasUrl = typeof rest.imageUrl === "string" && rest.imageUrl.length > 0;
366
+ const hasBannerPath = typeof bannerPath === "string" && bannerPath.length > 0;
367
+ const hasBannerUrl = typeof rest.bannerUrl === "string" && rest.bannerUrl.length > 0;
368
+ const invalid = (message) => ({ refused: { code: "MCP_VALIDATION", message } });
369
+ if (imageDir === undefined) {
370
+ if (imagePath !== undefined)
371
+ return invalid(`imagePath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass imageUrl, an https URL. ${NOTHING_UPLOADED}`);
372
+ if (bannerPath !== undefined)
373
+ return invalid(`bannerPath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass bannerUrl, an https URL. ${NOTHING_UPLOADED}`);
374
+ return { args: rest };
375
+ }
376
+ if (hasPath && hasUrl)
377
+ return invalid(`Pass exactly one of imageUrl or imagePath, not both. ${NOTHING_UPLOADED}`);
378
+ if (hasBannerPath && hasBannerUrl)
379
+ return invalid(`Pass at most one of bannerUrl or bannerPath, not both. ${NOTHING_UPLOADED}`);
380
+ if (!hasPath && !hasUrl)
381
+ return invalid("Pass imageUrl (an https URL) or imagePath (an absolute path to an image file inside CANDLE_IMAGE_DIR).");
382
+ if (!hasPath && !hasBannerPath)
383
+ return { args: rest };
384
+ if (!cfg.apiKey)
385
+ return invalid(`CANDLE_AGENT_API_KEY is required to upload an image; the key needs the launch:write scope. ${NOTHING_UPLOADED}`);
386
+ const read = async (path, field) => {
387
+ try {
388
+ return await readImageInDir(imageDir, path, fs, field);
389
+ } catch (error) {
390
+ if (error instanceof ImageRefusal)
391
+ return { refused: { code: error.code, message: error.message } };
392
+ throw error;
393
+ }
394
+ };
395
+ let image;
396
+ let banner;
397
+ if (hasPath) {
398
+ const result = await read(imagePath, "imagePath");
399
+ if ("refused" in result)
400
+ return result;
401
+ image = result;
402
+ }
403
+ if (hasBannerPath) {
404
+ const result = await read(bannerPath, "bannerPath");
405
+ if ("refused" in result)
406
+ return result;
407
+ banner = result;
408
+ }
409
+ const launchArgs = { ...rest };
410
+ let uploaded;
411
+ let uploadedBanner;
412
+ if (image) {
413
+ const result = await uploadLocalImage(image, "image", cfg, cfg.apiKey, doFetch);
414
+ if (!("url" in result))
415
+ return result;
416
+ uploaded = result.url;
417
+ launchArgs.imageUrl = uploaded;
418
+ }
419
+ if (banner) {
420
+ const result = await uploadLocalImage(banner, "banner", cfg, cfg.apiKey, doFetch);
421
+ if (!("url" in result)) {
422
+ if (uploaded === undefined)
423
+ return result;
424
+ const why = "refused" in result ? result.refused.message : result.uploadFailed;
425
+ return invalid(`The banner upload failed: ${why} The image was already uploaded to ${uploaded}; pass that as imageUrl on the retry. No launch was attempted.`);
426
+ }
427
+ uploadedBanner = result.url;
428
+ launchArgs.bannerUrl = uploadedBanner;
429
+ }
430
+ return { args: launchArgs, ...uploaded ? { uploaded } : {}, ...uploadedBanner ? { uploadedBanner } : {} };
386
431
  }
387
432
  function imageRefusalBody(refused, extra) {
388
433
  return JSON.stringify({ ...extra, success: false, error: { code: refused.code, message: refused.message } });
389
434
  }
390
435
  var UPLOADED_IMAGE_NOTE = "imagePath was uploaded to imageUrl, which is public and permanent. To retry, or to launch after a dry run, pass this imageUrl instead of imagePath, so the file is not uploaded again.";
436
+ var UPLOADED_BANNER_NOTE = "bannerPath was uploaded to bannerUrl, which is public and permanent. To retry, or to launch after a dry run, pass this bannerUrl instead of bannerPath, so the file is not uploaded again.";
437
+ function uploadedFields(image) {
438
+ return {
439
+ ...image.uploaded !== undefined ? { imageUrl: image.uploaded, imageNote: UPLOADED_IMAGE_NOTE } : {},
440
+ ...image.uploadedBanner !== undefined ? { bannerUrl: image.uploadedBanner, bannerNote: UPLOADED_BANNER_NOTE } : {}
441
+ };
442
+ }
391
443
 
392
444
  // src/orchestrate.ts
393
445
  import { randomUUID } from "node:crypto";
@@ -665,7 +717,7 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
665
717
  }
666
718
  return { text: JSON.stringify({ clientLaunchId, api }), isError: true };
667
719
  }
668
- const uploaded = image.uploaded === undefined ? {} : { imageUrl: image.uploaded, imageNote: UPLOADED_IMAGE_NOTE };
720
+ const uploaded = uploadedFields(image);
669
721
  const path = dryRun ? "/api/v1/launch/headless/dry-run" : "/api/v1/launch/headless";
670
722
  const posted = await postJson(`${base(cfg)}${path}`, apiKey, {
671
723
  ...image.args,
@@ -674,7 +726,7 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
674
726
  }, doFetch);
675
727
  if ("thrown" in posted) {
676
728
  const failed = transportError("clientLaunchId", clientLaunchId, posted.thrown);
677
- return image.uploaded === undefined ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
729
+ return Object.keys(uploaded).length === 0 ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
678
730
  }
679
731
  if (!posted.ok) {
680
732
  return { text: JSON.stringify({ clientLaunchId, ...uploaded, api: posted.body }), isError: true };
@@ -2422,18 +2474,55 @@ function perpsCandleWindow(opts, now) {
2422
2474
  return usage(`interval must be one of ${PERPS_CANDLE_INTERVALS.join(" ")}.`);
2423
2475
  const count = bounded(opts.count, 100, 1, 5000, "count");
2424
2476
  const raw = opts.end;
2425
- const endTime = raw === undefined ? now : typeof raw === "number" || /^\d+$/.test(raw) ? Number(raw) : /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(raw) ? Date.parse(raw) : NaN;
2477
+ const endTime = raw === undefined ? now : epoch(raw);
2426
2478
  const startTime = endTime - count * at(INTERVAL_MS, index);
2427
2479
  if (!Number.isSafeInteger(endTime) || endTime < 0 || endTime > now || startTime < 0)
2428
2480
  return usage("end must be an ISO 8601 time or epoch milliseconds, no later than now.");
2429
2481
  return { interval, count, startTime, endTime };
2430
2482
  }
2483
+ function epoch(raw) {
2484
+ return typeof raw === "number" || /^\d+$/.test(raw) ? Number(raw) : /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(raw) ? Date.parse(raw) : NaN;
2485
+ }
2486
+ var PERPS_FUNDING_HISTORY_MAX_MS = 30 * 86400000;
2487
+ var PERPS_FUNDING_HISTORY_MAX_ROWS = 500;
2488
+ function perpsFundingWindow(opts, now) {
2489
+ const endTime = opts.end === undefined ? now : epoch(opts.end);
2490
+ if (!Number.isSafeInteger(endTime) || endTime < 0 || endTime > now)
2491
+ return usage("end must be an ISO 8601 time or epoch milliseconds, no later than now.");
2492
+ const startTime = opts.start === undefined ? endTime - 86400000 : epoch(opts.start);
2493
+ if (!Number.isSafeInteger(startTime) || startTime < 0 || startTime > endTime)
2494
+ return usage("start must be an ISO 8601 time or epoch milliseconds, no later than end.");
2495
+ if (endTime - startTime > PERPS_FUNDING_HISTORY_MAX_MS)
2496
+ return usage("start must be at most 30 days before end.");
2497
+ return { startTime, endTime };
2498
+ }
2431
2499
  function bounded(value, fallback, min, max, name) {
2432
2500
  const n = value ?? fallback;
2433
2501
  if (!Number.isInteger(n) || n < min || n > max)
2434
2502
  return usage(`${name} must be an integer from ${min} to ${max}.`);
2435
2503
  return n;
2436
2504
  }
2505
+ function parsePerpsFundingHistory(value, coin, window) {
2506
+ if (!Array.isArray(value))
2507
+ return unavailable();
2508
+ const funding = [];
2509
+ for (const row of value) {
2510
+ if (!object(row) || row.coin !== coin || !integer(row.time) || !decimal2(row.fundingRate) || !decimal2(row.premium))
2511
+ return unavailable();
2512
+ if (row.time >= window.startTime && row.time <= window.endTime)
2513
+ funding.push({
2514
+ time: row.time,
2515
+ fundingRate: row.fundingRate,
2516
+ fundingAprPct: fixed(scaled(row.fundingRate) * 876000n, 2),
2517
+ premium: row.premium
2518
+ });
2519
+ }
2520
+ funding.sort((a, b) => a.time - b.time);
2521
+ return {
2522
+ funding: funding.slice(0, PERPS_FUNDING_HISTORY_MAX_ROWS),
2523
+ truncated: funding.length > PERPS_FUNDING_HISTORY_MAX_ROWS
2524
+ };
2525
+ }
2437
2526
 
2438
2527
  class PerpsMarketReader {
2439
2528
  opts;
@@ -2711,6 +2800,44 @@ class PerpsMarketReader {
2711
2800
  candles: candles.slice(-window.count)
2712
2801
  };
2713
2802
  }
2803
+ async fundingHistory(coin, opts = {}) {
2804
+ this.key();
2805
+ const window = perpsFundingWindow(opts, this.now());
2806
+ const [config, core] = await Promise.all([this.config(), this.core()]);
2807
+ const canonical2 = at(core.universe, resolve(core, coin)).name;
2808
+ const value = await info(this.opts.fetcher, this.opts.network, {
2809
+ type: "fundingHistory",
2810
+ coin: canonical2,
2811
+ startTime: window.startTime,
2812
+ endTime: window.endTime
2813
+ });
2814
+ return {
2815
+ ...this.envelope(config),
2816
+ coin: canonical2,
2817
+ startTime: window.startTime,
2818
+ endTime: window.endTime,
2819
+ ...parsePerpsFundingHistory(value, canonical2, window)
2820
+ };
2821
+ }
2822
+ async predictedFunding() {
2823
+ this.key();
2824
+ const [config, core, extras] = await Promise.all([this.config(), this.core(), this.extras()]);
2825
+ if (extras.predicted === null)
2826
+ return unavailable();
2827
+ const predicted = [];
2828
+ for (const meta of core.universe) {
2829
+ const row = extras.predicted.get(meta.name);
2830
+ if (row)
2831
+ predicted.push({
2832
+ coin: meta.name,
2833
+ fundingRate: row.fundingRate,
2834
+ fundingAprPct: fixed(scaled(row.fundingRate) * 876000n, 2),
2835
+ nextFundingTime: row.nextFundingTime
2836
+ });
2837
+ }
2838
+ predicted.sort((a, b) => a.coin < b.coin ? -1 : a.coin > b.coin ? 1 : 0);
2839
+ return { ...this.envelope(config, extras.warnings, this.extrasCache?.at ?? this.now()), predicted };
2840
+ }
2714
2841
  }
2715
2842
 
2716
2843
  // src/perps-history.ts
@@ -4280,7 +4407,8 @@ async function executePerpsDeposit(args, cfg, env, fetch2) {
4280
4407
  asset,
4281
4408
  amountRaw,
4282
4409
  ...typeof args.maxSlippageBps === "number" ? { maxSlippageBps: args.maxSlippageBps } : {},
4283
- ...hood ? {} : { perpsWalletId: account.id }
4410
+ ...hood ? {} : { perpsWalletId: account.id },
4411
+ ...willSubmit ? {} : { preview: true }
4284
4412
  });
4285
4413
  if (build.job !== undefined)
4286
4414
  return { text: JSON.stringify(build) };
@@ -4289,6 +4417,8 @@ async function executePerpsDeposit(args, cfg, env, fetch2) {
4289
4417
  return fail("PERPS_BUILD_REFUSED", `Refused to sign this deposit: ${problem}. Nothing was signed.`);
4290
4418
  if (!willSubmit || signerPem === null)
4291
4419
  return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
4420
+ if (build.preview === true || typeof build.depositId !== "string")
4421
+ return fail("PERPS_BUILD_REFUSED", "Refused to sign this deposit: Candle answered a preview. Nothing was signed.");
4292
4422
  if (!appId || !source.privyWalletId)
4293
4423
  return fail("SIGNER_UNAVAILABLE", "The server did not return its relay identifiers.");
4294
4424
  const pem = signerPem;
@@ -4515,7 +4645,7 @@ function planTableMarkdown(table) {
4515
4645
 
4516
4646
  // src/tools.ts
4517
4647
  var VAMP_CANDIDATES_DESCRIPTION = "Lists Solana tokens that are close to graduating or graduated in the last 6 hours whose ticker other tokens " + "are copying, most-copied in the last 24 hours first. Each entry carries a launch draft (name, ticker, image URL) " + "for `candle_launch_token` or `candle_launch_and_seed`; draft notes identify fields you must supply or adjust. " + "Upload your own image through the agent image upload when the image is unavailable. Use `dryRun: true` first. " + "Counts exclude the candidate itself and cover both Solana and Hood launches at the venues returned in " + "`coverage`, not Candle's own. `coverage.since` is the returned Solana coverage date; Hood history may differ.";
4518
- var CUSTOM_CURVE_RULE = 'mode "exp-custom" with curve { virtualQuote, threshold } launches a Solana curve with your own V (the virtual ' + "quote it starts with) and G (the quote it raises before graduating), in whole SOL or CNDL as decimal strings, " + "accepted when V is 1 to 100 SOL and G 1 to 250 SOL (66,000 to 6,600,000 and 66,000 to 16,500,000 CNDL), when " + "every price up to 20 x G stays under the ceiling (V + 20G)^2 < V x X at this stakerAllocationBps, when neither " + "is finer than the quote's decimals, and when the dev buy is at most 10% of G; a curve past a limit is " + "CURVE_OUT_OF_RANGE, and at the ceiling it carries maxThresholdRaw, the largest G that fits. " + "Dry-run it first (dryRun: true): the dry run spends nothing and returns curveTerms, the market cap at start and " + "at graduation and the supply sold by graduation. Only where the API has EXP_CUSTOM_CURVES on; " + "elsewhere exp-custom is refused as an unknown mode.";
4648
+ var CUSTOM_CURVE_RULE = 'mode "exp-custom" with curve { virtualQuote, threshold } launches a Solana curve with your own V (the virtual ' + "quote it starts with) and G (the quote it raises before graduating), in whole SOL or CNDL as decimal strings, " + "accepted when V is 1 to 100 SOL and G 1 to 250 SOL (66,000 to 6,600,000 and 66,000 to 16,500,000 CNDL), when " + "every price up to 20 x G stays under the ceiling (V + 20G)^2 < V x X at this stakerAllocationBps, when neither " + "is finer than the quote's decimals, and when the dev buy is at most 0.5 SOL (2,000,000 CNDL), or 10% of G when " + "that is lower (every exp- mode; an operator may set the 0.5 SOL lower); a curve past a limit is " + "CURVE_OUT_OF_RANGE, and at the ceiling it carries maxThresholdRaw, the largest G that fits. " + "Dry-run it first (dryRun: true): the dry run spends nothing and returns curveTerms, the market cap at start and " + "at graduation, the supply sold by graduation, the share your own buyAmount takes, the fees and the migration " + "terms, and devBuyMax, the dev-buy ceiling that applies. Only where the API has EXP_CUSTOM_CURVES on; " + "elsewhere exp-custom is refused as an unknown mode.";
4519
4649
  var localProfileTools = new WeakMap;
4520
4650
  var STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.";
4521
4651
  var stakingWriteShape = {
@@ -4945,15 +5075,26 @@ var launchTokenShape = {
4945
5075
  name: z4.string().describe("Token name"),
4946
5076
  symbol: z4.string().describe("Token symbol"),
4947
5077
  imageUrl: z4.string().describe("https URL to the token image. Must be roughly SQUARE (aspect ratio at most 1.5:1): it " + "renders as a small circle/square avatar everywhere. Share cards, OG images and banners " + "are rejected with IMAGE_WRONG_SHAPE -- pass those as bannerUrl instead"),
4948
- bannerUrl: z4.string().optional().describe("Optional https URL to WIDE artwork for the token page's banner strip (wider than 1.5:1, " + "e.g. 1200x630). This is where a share card or OG image belongs. A square image here is " + "rejected with BANNER_WRONG_SHAPE. Omit it and the strip falls back to imageUrl"),
5078
+ bannerUrl: z4.string().optional().describe("Optional https URL to WIDE artwork for the token page's banner strip (wider than 1.5:1, " + "e.g. 1200x630). This is where a share card or OG image belongs. A square image here is " + "rejected with BANNER_WRONG_SHAPE: send the wide original, not a cropped copy. Omit it and the strip " + "falls back to imageUrl"),
4949
5079
  chain: z4.string().optional().describe('"solana" or "hood"; defaults to solana'),
4950
5080
  quoteAsset: z4.string().optional().describe("Quote asset symbol; defaults per chain"),
4951
- mode: z4.string().optional().describe('"open" or "exclusive"; defaults to open. "test-open" / "test-exclusive" launch the ' + "low-threshold test curves (~1/80 economics) where the API has ENABLE_TEST_CURVES on. " + '"exp-custom" launches a Solana curve with your own V and G, given in curve, where the API has it ' + "switched on"),
5081
+ mode: z4.string().optional().describe('"open" or "exclusive"; defaults to open. "test-open" / "test-exclusive" launch the ' + "low-threshold test curves (~1/80 economics) where the API has ENABLE_TEST_CURVES on. " + '"exp-a", "exp-b" and "exp-c" are the experimental fast Solana curves (sol or cndl). ' + '"exp-custom" launches a Solana curve with your own V and G, given in curve, where the API has it ' + "switched on"),
4952
5082
  curve: z4.object({
4953
5083
  virtualQuote: z4.string().describe('V: the virtual quote the curve starts with, in whole quote units, e.g. "12" SOL'),
4954
5084
  threshold: z4.string().describe('G: the quote the curve raises before it graduates, in whole quote units, e.g. "40" SOL')
4955
5085
  }).strict().optional().describe('mode "exp-custom" only, and required there. Decimal strings in whole units of the quote asset (sol or ' + "cndl), never raw units. Dry-run first: the API checks the limits and returns the curve's terms"),
5086
+ creatorFeeBps: z4.union([z4.literal(0), z4.literal(100), z4.literal(200)]).optional().describe("An exp- mode only: an extra creator fee of 100 or 200 basis points on top of the curve's 1% and the " + "pool's 0.5%, paid to the creator in full. 0 is the same as leaving it out"),
4956
5087
  stakerAllocationBps: z4.number().optional().describe("Staker allocation in basis points"),
5088
+ poolFee: z4.union([
5089
+ z4.object({ tier: z4.union([z4.literal(25), z4.literal(50), z4.literal(100)]) }).strict(),
5090
+ z4.object({
5091
+ startBps: z4.number().int().max(2500),
5092
+ endBps: z4.union([z4.literal(25), z4.literal(50), z4.literal(100)]),
5093
+ minutes: z4.number().positive().max(30)
5094
+ }).strict()
5095
+ ]).optional().describe("Solana only: the graduated pool's fee. { tier: 25 | 50 | 100 } is a flat fee in bps (default 50), or " + "{ startBps, endBps, minutes } is an anti-snipe fall from startBps (at most 2500, at least endBps) to " + "endBps (25, 50 or 100) over minutes (at most 30). Cannot be combined with creatorFeeBps unless the tier is 50. " + "Omit for the standard 50 bps pool"),
5096
+ poolOpen: z4.enum(["legacy", "curve_price"]).optional().describe(`Solana preset tiers only. "curve_price" opens the pool at the curve's last price (the whole reserve is ` + "seeded and the remaining base burned); stakerAllocationBps plus the team allocation must fit the tier's limit, " + "which the dry run returns as graduation.maxStakerCarveBps. Omit for legacy"),
5097
+ expPoolShape: z4.enum(["floor", "full_range"]).optional().describe('Solana exp- curves only. "full_range" opens the pool across the whole price range. Omit for floor'),
4957
5098
  dexVersion: z4.string().nullable().optional().describe('"v3" or "v4"; required for hood launches'),
4958
5099
  buyAmount: z4.union([z4.string(), z4.number()]).optional().describe("Initial buy, in quote base units (string or number)"),
4959
5100
  description: z4.string().optional(),
@@ -5075,11 +5216,13 @@ var seedShape = {
5075
5216
  var launchAndSeedShape = { ...seedableLaunchShape, ...seedShape };
5076
5217
  var localImageShape = {
5077
5218
  imageUrl: launchTokenShape.imageUrl.optional().describe(`${launchTokenShape.imageUrl.description}. Pass this or imagePath, never both`),
5078
- imagePath: z4.string().optional().describe("An absolute image path inside CANDLE_IMAGE_DIR. The file is uploaded and becomes public and permanent, " + "including on a dry run. PNG, JPEG, GIF or WebP, at most 10MB, read from the file's first bytes. " + "Uploading spends no funds and does not count against the launch cap; only the upload limit applies. " + "Pass this or imageUrl, never both. The result carries the imageUrl used: pass that on a retry, or " + "after a dry run, instead of uploading the file again. bannerUrl takes no file")
5219
+ imagePath: z4.string().optional().describe("An absolute image path inside CANDLE_IMAGE_DIR. The file is uploaded and becomes public and permanent, " + "including on a dry run. PNG, JPEG, GIF or WebP, at most 10MB, read from the file's first bytes. " + "Uploading spends no funds and does not count against the launch cap; only the upload limit applies. " + "Pass this or imageUrl, never both. The result carries the imageUrl used: pass that on a retry, or " + "after a dry run, instead of uploading the file again. This is the square token image: Candle crops " + "it to a square, so a wide banner belongs in bannerPath, not here"),
5220
+ bannerUrl: launchTokenShape.bannerUrl.describe(`${launchTokenShape.bannerUrl.description}. Pass this or bannerPath, never both`),
5221
+ bannerPath: z4.string().optional().describe("An absolute path, inside CANDLE_IMAGE_DIR, to WIDE banner artwork for the token page strip (wider than " + "1.5:1, e.g. 2171x724 or 1200x630). The file is uploaded uncropped and becomes public and permanent, " + "including on a dry run; PNG, JPEG, GIF or WebP, at most 10MB. A square image is refused with " + "BANNER_WRONG_SHAPE: send the wide original, not a cropped copy. Pass this or bannerUrl, never both. " + "The result carries the bannerUrl used: pass that on a retry instead of uploading the file again")
5079
5222
  };
5080
5223
  var localLaunchTokenShape = { ...launchTokenShape, ...localImageShape };
5081
5224
  var localLaunchAndSeedShape = { ...seedableLaunchShape, ...localImageShape, ...seedShape };
5082
- var IMAGE_PATH_DISCLOSURE = " With imagePath, the image file is uploaded first and becomes public and permanent, on a dry run too; " + "nothing about a dry run keeps that file private.";
5225
+ var IMAGE_PATH_DISCLOSURE = " With imagePath or bannerPath, the file is uploaded first and becomes public and permanent, on a dry run too; " + "nothing about a dry run keeps that file private. imagePath is the square token image (Candle crops it); " + "a wide banner goes in bannerPath, which is not cropped.";
5083
5226
  var transferShape = {
5084
5227
  chain: z4.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
5085
5228
  asset: z4.enum(["SOL", "USDC", "CNDL", "ETH", "USDG"]).optional().describe("A base asset key. Pass exactly one of asset or mint."),
@@ -5134,9 +5277,12 @@ function registerToolSubset(server, options) {
5134
5277
  if ("uploadFailed" in image)
5135
5278
  return { content: [{ type: "text", text: image.uploadFailed }], isError: true };
5136
5279
  const relayed = await callAndRelay("candle_launch_token", image.args, getConfig(), doFetch);
5137
- if (image.uploaded === undefined)
5280
+ if (image.uploaded === undefined && image.uploadedBanner === undefined)
5138
5281
  return relayed;
5139
- const uploaded = JSON.stringify({ imageUrl: image.uploaded, note: UPLOADED_IMAGE_NOTE });
5282
+ const uploaded = JSON.stringify({
5283
+ ...image.uploaded !== undefined ? { imageUrl: image.uploaded, note: UPLOADED_IMAGE_NOTE } : {},
5284
+ ...image.uploadedBanner !== undefined ? { bannerUrl: image.uploadedBanner, bannerNote: UPLOADED_BANNER_NOTE } : {}
5285
+ });
5140
5286
  return { ...relayed, content: [...relayed.content, { type: "text", text: uploaded }] };
5141
5287
  });
5142
5288
  register("candle_get_market", {
@@ -5503,7 +5649,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
5503
5649
  }, perpsTool("candle_perps_reduce_only"));
5504
5650
  register("candle_perps_deposit", {
5505
5651
  title: "Deposit to Hyperliquid",
5506
- description: "MOVES REAL FUNDS unless submit is false. Deposit onto the key's Hyperliquid account through Relay: SOL or " + "USDC from its Solana TEE wallet, or ETH or USDG from its Hood TEE wallet, credited as Hyperliquid perps " + "USDC at the key's EVM TEE wallet's own address (Candle resolves it; no recipient is taken). Refused below a " + "floor that covers Hyperliquid's 1 USDC first-deposit charge. No Candle fee. Needs swap:write and a raw cap " + "on the asset, and CANDLE_KEY_SIGNER_PEM_FILE to sign. There is no withdrawal from Hyperliquid yet.",
5652
+ description: "MOVES REAL FUNDS unless submit is false. Deposit onto the key's Hyperliquid account through Relay: SOL or " + "USDC from its Solana TEE wallet, or ETH or USDG from its Hood TEE wallet, credited as Hyperliquid perps " + "USDC at the key's EVM TEE wallet's own address (Candle resolves it; no recipient is taken). Refused below a " + "floor that covers Hyperliquid's 1 USDC first-deposit charge. No Candle fee. Needs swap:write (any cap on " + "the asset still applies), and CANDLE_KEY_SIGNER_PEM_FILE to sign. There is no withdrawal from Hyperliquid yet.",
5507
5653
  inputSchema: perpsDepositShape
5508
5654
  }, async (args) => {
5509
5655
  const result = await executePerpsDeposit(args, getConfig(), env, doFetch);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.10.19",
3
+ "version": "0.10.21",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",