@candledottv/mcp 0.10.21 → 0.10.22

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 +1 -1
  2. package/dist/index.js +124 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -116,7 +116,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
116
116
  | `candle_list_agents` | The agent directory: every published agent with its strategy, declared models, all-time P&L and trade and launch counts, sorted `newest`, `active` or `pnl`, filtered by `strategy`, searched by `q` | `GET /api/v1/agent/directory` | none |
117
117
  | `candle_get_plans` | Plans: prices, fees, limits and what each can do, as JSON and a Markdown table | `GET /api/v1/agent/plans` | none |
118
118
  | `candle_trade` | Buy or sell a token | Reads the market for its decimals (or wallet balance, for a percent sell) then `POST /api/v1/trade/agent/build` | `CANDLE_AGENT_API_KEY` (`swap:write`) |
119
- | `candle_launch_and_seed` | Launch a token and seed it | `POST /api/v1/launch/headless` (or `/dry-run`), then a follow-up `GET /api/v1/markets/:chain/:mint`; with `imagePath`, `POST /api/v1/uploads/agent-image` first | `CANDLE_AGENT_API_KEY` |
119
+ | `candle_launch_and_seed` | Launch a token and seed it | `POST /api/v1/launch/headless` (or `/dry-run`); a Solana seed is an atomic launch instead, `POST /api/v1/launch/atomic/build` then `/submit` (or `/atomic/dry-run`), one Jito bundle from the embedded wallet (Pro or Max, `swap:write`); then a follow-up `GET /api/v1/markets/:chain/:mint`; with `imagePath`, `POST /api/v1/uploads/agent-image` first | `CANDLE_AGENT_API_KEY` |
120
120
  | `candle_swap` | Swap between base assets | `POST /api/v1/agent/swap` | `CANDLE_AGENT_API_KEY` (`swap:write`) |
121
121
  | `candle_transfer` | Transfer an asset | `POST /api/v1/agent/transfer` | `CANDLE_AGENT_API_KEY`, Withdraw (`transfer:write`) |
122
122
  | `candle_sweep` | Sweep a wallet to one destination | One `POST /api/v1/agent/transfer` per asset, `amountRaw: "max"` | `CANDLE_AGENT_API_KEY`, Withdraw (`transfer:write`) |
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.21";
154
+ var SERVER_VERSION = "0.10.22";
155
155
 
156
156
  // src/update-notice.ts
157
157
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -691,7 +691,7 @@ async function executeTrade(args, cfg, doFetch) {
691
691
  const wrapped = JSON.stringify({ clientTradeId, resolved, api: posted.body });
692
692
  return posted.ok ? { text: wrapped } : { text: wrapped, isError: true };
693
693
  }
694
- async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
694
+ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs, opts = {}) {
695
695
  const apiKey = requireApiKey(cfg);
696
696
  const clientLaunchId = args.clientLaunchId ?? randomUUID();
697
697
  const { devBuy, dryRun, buyAmount: _rawBuyAmount, ...imageFields } = args;
@@ -705,6 +705,12 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
705
705
  return converted.err;
706
706
  buyAmount = converted.raw;
707
707
  }
708
+ const atomic = buyAmount !== undefined && (args.chain ?? "solana") === "solana" && !opts.headlessSeed;
709
+ if (atomic) {
710
+ const refused = seedCeilingRefusal(args.quoteAsset ?? "sol", args.mode, buyAmount, clientLaunchId);
711
+ if (refused)
712
+ return refused;
713
+ }
708
714
  const image = await resolveLaunchImage(imageFields, imageDir, cfg, doFetch, imageFs);
709
715
  if ("refused" in image)
710
716
  return { text: imageRefusalBody(image.refused, { clientLaunchId }), isError: true };
@@ -718,16 +724,20 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
718
724
  return { text: JSON.stringify({ clientLaunchId, api }), isError: true };
719
725
  }
720
726
  const uploaded = uploadedFields(image);
727
+ const transportFailure = (thrown) => {
728
+ const failed = transportError("clientLaunchId", clientLaunchId, thrown);
729
+ return Object.keys(uploaded).length === 0 ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
730
+ };
731
+ if (atomic)
732
+ return launchAndSeedAtomic({ ...image.args, clientLaunchId }, buyAmount, { clientLaunchId, uploaded, dryRun: dryRun === true, transportFailure }, cfg, doFetch);
721
733
  const path = dryRun ? "/api/v1/launch/headless/dry-run" : "/api/v1/launch/headless";
722
734
  const posted = await postJson(`${base(cfg)}${path}`, apiKey, {
723
735
  ...image.args,
724
736
  clientLaunchId,
725
737
  ...buyAmount !== undefined ? { buyAmount } : {}
726
738
  }, doFetch);
727
- if ("thrown" in posted) {
728
- const failed = transportError("clientLaunchId", clientLaunchId, posted.thrown);
729
- return Object.keys(uploaded).length === 0 ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
730
- }
739
+ if ("thrown" in posted)
740
+ return transportFailure(posted.thrown);
731
741
  if (!posted.ok) {
732
742
  return { text: JSON.stringify({ clientLaunchId, ...uploaded, api: posted.body }), isError: true };
733
743
  }
@@ -735,25 +745,115 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
735
745
  return { text: JSON.stringify({ clientLaunchId, dryRun: true, ...uploaded, api: posted.body }) };
736
746
  }
737
747
  const launch = posted.body;
738
- let market = null;
739
- let note;
748
+ const { market, note } = await readLaunchedMarket(launch.chain ?? "solana", launch.mint, cfg, doFetch);
749
+ return {
750
+ text: JSON.stringify({ clientLaunchId, ...uploaded, launch, market, ...note ? { note } : {} })
751
+ };
752
+ }
753
+ async function readLaunchedMarket(chain, mint, cfg, doFetch) {
754
+ const note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
740
755
  try {
741
- if (launch.mint === undefined)
756
+ if (mint === undefined)
742
757
  throw new Error("the launch response carried no mint");
743
- const marketRes = await doFetch(`${base(cfg)}/api/v1/markets/${encodeURIComponent(launch.chain ?? "solana")}/${encodeURIComponent(launch.mint)}`, {
758
+ const marketRes = await doFetch(`${base(cfg)}/api/v1/markets/${encodeURIComponent(chain)}/${encodeURIComponent(mint)}`, {
744
759
  method: "GET",
745
760
  headers: headers()
746
761
  });
747
- if (marketRes.ok) {
748
- market = JSON.parse(await marketRes.text()).market ?? null;
749
- } else {
750
- note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
751
- }
762
+ if (!marketRes.ok)
763
+ return { market: null, note };
764
+ return { market: JSON.parse(await marketRes.text()).market ?? null };
752
765
  } catch {
753
- note = "launch confirmed; the follow-up market read failed, read it via candle_get_market";
766
+ return { market: null, note };
767
+ }
768
+ }
769
+ var SOLANA_SEED_CEILINGS = {
770
+ sol: { raw: 500000000n, unit: "lamports", expRaw: 500000000n },
771
+ usdc: { raw: 500000000n, unit: "USDC raw units" },
772
+ cndl: { raw: 2000000000000n, unit: "CNDL raw units", expRaw: 33000000000n }
773
+ };
774
+ function seedCeilingRefusal(quoteAsset, mode, buyAmount, clientLaunchId) {
775
+ const quote = quoteAsset.toLowerCase();
776
+ const ceiling = SOLANA_SEED_CEILINGS[quote];
777
+ if (!ceiling)
778
+ return;
779
+ const expCap = ceiling.expRaw;
780
+ const preset = mode === "exp-a" || mode === "exp-b" || mode === "exp-c";
781
+ const effective = preset && expCap !== undefined && expCap < ceiling.raw ? expCap : ceiling.raw;
782
+ if (BigInt(buyAmount) <= effective)
783
+ return;
784
+ if (preset && expCap !== undefined) {
785
+ return {
786
+ text: JSON.stringify({
787
+ clientLaunchId,
788
+ success: false,
789
+ error: {
790
+ code: "DEV_BUY_TOO_HIGH",
791
+ message: `buyAmount exceeds this launch's dev-buy ceiling of ${effective} ${ceiling.unit}: at most ${ceiling.raw} ${ceiling.unit}, or 10% of the threshold when that is lower`,
792
+ field: "devBuy",
793
+ retryable: false
794
+ }
795
+ }),
796
+ isError: true
797
+ };
754
798
  }
755
799
  return {
756
- text: JSON.stringify({ clientLaunchId, ...uploaded, launch, market, ...note ? { note } : {} })
800
+ text: JSON.stringify({
801
+ clientLaunchId,
802
+ success: false,
803
+ error: {
804
+ code: "DEV_BUY_TOO_HIGH",
805
+ message: `buyAmount exceeds the per-launch dev-buy ceiling of ${effective} ${ceiling.unit} for ${quote} on solana`,
806
+ field: "devBuy",
807
+ retryable: false
808
+ }
809
+ }),
810
+ isError: true
811
+ };
812
+ }
813
+ var MAIN_PAYER = { type: "main" };
814
+ async function launchAndSeedAtomic(launchBody, buyAmount, ctx, cfg, doFetch) {
815
+ const apiKey = requireApiKey(cfg);
816
+ const { clientLaunchId, uploaded } = ctx;
817
+ const failed = (api, extra = {}) => ({
818
+ text: JSON.stringify({ clientLaunchId, ...uploaded, ...extra, api }),
819
+ isError: true
820
+ });
821
+ const body = { ...launchBody, payer: MAIN_PAYER, firstBuys: [{ payer: MAIN_PAYER, amountRaw: buyAmount }] };
822
+ const built = await postJson(`${base(cfg)}/api/v1/launch/atomic/${ctx.dryRun ? "dry-run" : "build"}`, apiKey, body, doFetch);
823
+ if ("thrown" in built)
824
+ return ctx.transportFailure(built.thrown);
825
+ if (!built.ok)
826
+ return failed(built.body);
827
+ if (ctx.dryRun)
828
+ return { text: JSON.stringify({ clientLaunchId, dryRun: true, ...uploaded, api: built.body }) };
829
+ const bundle = built.body;
830
+ if (typeof bundle.bundleId !== "string" || !Array.isArray(bundle.legs) || bundle.legs.length !== 2 || bundle.legs.some((leg) => leg?.signer !== "server"))
831
+ return failed(built.body, {
832
+ success: false,
833
+ error: {
834
+ code: "MCP_INVALID_RESPONSE",
835
+ message: "Candle did not build this launch and its seed as two legs it signs from the embedded wallet; nothing was sent. Retry with the same clientLaunchId to build it again."
836
+ }
837
+ });
838
+ const bundleId = bundle.bundleId;
839
+ const submitted = await postJson(`${base(cfg)}/api/v1/launch/atomic/submit`, apiKey, { bundleId, signedTxsBase64: [] }, doFetch);
840
+ if ("thrown" in submitted)
841
+ return ctx.transportFailure(submitted.thrown);
842
+ const outcome = submitted.body;
843
+ if (!submitted.ok || outcome.status !== "landed") {
844
+ const next = outcome.status === "failed" ? "Neither the launch nor the seed is on chain. Retry with the same clientLaunchId: Candle starts a clean attempt under it." : outcome.status === "timeout" ? "The bundle is unresolved and may still land. Call candle_get_operation with this clientLaunchId and kind launch; never relaunch under a new id, which would be a second token." : undefined;
845
+ return failed(submitted.body, { bundleId, ...next ? { next } : {} });
846
+ }
847
+ const { market, note } = await readLaunchedMarket("solana", outcome.mint, cfg, doFetch);
848
+ return {
849
+ text: JSON.stringify({
850
+ clientLaunchId,
851
+ bundleId,
852
+ ...uploaded,
853
+ launch: submitted.body,
854
+ market,
855
+ ...note ? { note } : {}
856
+ })
757
857
  };
758
858
  }
759
859
  var SWEEP_BASE_ASSETS = {
@@ -5093,7 +5193,7 @@ var launchTokenShape = {
5093
5193
  minutes: z4.number().positive().max(30)
5094
5194
  }).strict()
5095
5195
  ]).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"),
5196
+ 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. Omitted defaults to curve_price when those " + "allocations fit, otherwise falls back to legacy without refusal. Set legacy explicitly to opt out. " + "Explicit curve_price is refused when the allocations exceed the limit"),
5097
5197
  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'),
5098
5198
  dexVersion: z4.string().nullable().optional().describe('"v3" or "v4"; required for hood launches'),
5099
5199
  buyAmount: z4.union([z4.string(), z4.number()]).optional().describe("Initial buy, in quote base units (string or number)"),
@@ -5211,7 +5311,7 @@ var tradeShape = {
5211
5311
  var { buyAmount: _rawBuyAmount, ...seedableLaunchShape } = launchTokenShape;
5212
5312
  var seedShape = {
5213
5313
  clientLaunchId: z4.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result."),
5214
- devBuy: z4.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), bundled into the launch transaction itself on solana and sent as a follow-up " + "transaction on hood. Paid from the account's embedded wallet. Follows quoteAsset, which " + "defaults to sol on solana and eth on hood. Capped by the platform dev-buy ceiling; for " + "a larger seed, launch then follow with candle_trade.")
5314
+ devBuy: z4.string().optional().describe('Seed buy in DECIMAL units of the quote asset this launch selects (e.g. "0.25" SOL, or ' + "ETH on hood), paid from the account's embedded wallet; the tool's description says how " + "each chain sends it. Follows quoteAsset, which defaults to sol on solana and eth on hood. " + "Capped by the platform dev-buy ceiling; for a larger seed, launch then follow with candle_trade.")
5215
5315
  };
5216
5316
  var launchAndSeedShape = { ...seedableLaunchShape, ...seedShape };
5217
5317
  var localImageShape = {
@@ -5527,17 +5627,19 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
5527
5627
  });
5528
5628
  register("candle_launch_and_seed", {
5529
5629
  title: "Launch a token and seed it",
5530
- description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed (in the " + "launch transaction on solana, a best-effort follow-up on hood). Works on every plan, Free included (this is not the atomic launch). Then " + "return the fresh market state and token links in one result. MOVES REAL FUNDS unless " + "dryRun. Seeds above the platform dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH); " + "launch, then top up with candle_trade. " + CUSTOM_CURVE_RULE + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
5630
+ description: "Launch a new token from the account's embedded wallet, with an optional dev-buy seed, then " + "return the fresh market state and token links in one result. On solana a seed makes this " + (options.headlessSeed === true ? "the seed is a separate first-buy transaction in the same Jito bundle as the launch, so " + "both land or neither does. That works on every plan, Free included, and needs no " + "swap:write. On hood the seed is " : "the atomic launch: the launch and the seed are two transactions Candle signs and sends as " + "one Jito bundle, so both land or neither does. That needs Pro or Max (TIER_REQUIRED " + "otherwise) and swap:write on the key, and the result carries bundleId. On hood the seed is ") + "a best-effort follow-up. A launch with no seed works on every plan, Free included. MOVES " + "REAL FUNDS unless dryRun; dry-run first, which spends nothing. Seeds above the platform " + "dev-buy ceiling are rejected (DEV_BUY_TOO_HIGH): at most 0.5 SOL / 500 USDC / 2,000,000 " + "CNDL, or on an exp- curve 10% of the threshold when that is lower; the server holds the " + "creator's own seed to the effective ceiling, so launch with the largest devBuy it allows " + "and top up with candle_trade. " + CUSTOM_CURVE_RULE + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
5531
5631
  inputSchema: imageDir ? localLaunchAndSeedShape : launchAndSeedShape
5532
5632
  }, async (args) => {
5533
- const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir);
5633
+ const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir, undefined, {
5634
+ headlessSeed: options.headlessSeed === true
5635
+ });
5534
5636
  return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
5535
5637
  });
5536
5638
  const marketRead = createPerpsMarketExecutor(getConfig, env, doFetch);
5537
5639
  for (const name of PERPS_MARKET_TOOL_NAMES) {
5538
5640
  register(name, {
5539
5641
  title: name.replace("candle_perps_", "Hyperliquid "),
5540
- description: "Read live market data directly from Hyperliquid's public API on this machine. Needs a Candle agent key of any scope, no signer. candle.tradeable is Candle's build gate at read time, not a promise; OI caps may still refuse an order. The hosted connector does not carry these tools.",
5642
+ description: "Read live market data directly from Hyperliquid's public API on this machine. Needs a Candle agent key of any scope, no signer. candle.tradeable is Candle's build gate at read time, not a promise; OI caps may still refuse an order. The hosted connector offers the same four reads at its Read level, read by Candle's server instead of this machine.",
5541
5643
  inputSchema: perpsMarketShapes[name],
5542
5644
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5543
5645
  }, async (args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.10.21",
3
+ "version": "0.10.22",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",