@candledottv/mcp 0.10.15 → 0.10.17

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 +10 -3
  2. package/dist/index.js +137 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -55,6 +55,12 @@ Both take decimal amounts, never raw base units, and resolve the scale themselve
55
55
  there the caller genuinely picks the new token's quote pair: `quoteAsset` if given, otherwise
56
56
  SOL on Solana and ETH on Hood.
57
57
 
58
+ Both launch tools also take `mode: "exp-custom"` with `curve: { virtualQuote, threshold }`: a
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
62
+ [Custom curves](https://docs.candle.tv/developers/headless-launch#custom-curves-choose-v-and-g-switched-off).
63
+
58
64
  For a full walkthrough of `candle_launch_and_seed` and `candle_trade`, including the keyless read
59
65
  tools, getting a key, funding the embedded wallet, and idempotent retries, see
60
66
  [Candle MCP server](https://docs.candle.tv/developers/mcp-server).
@@ -120,7 +126,7 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
120
126
  | `candle_execution_status` | One call before trading: wallets to spend from, tier, and whether trading is possible | Composes the wallet and tier reads | `CANDLE_AGENT_API_KEY` |
121
127
  | `candle_get_operation` | Look up a trade or launch by the id its write used, and whether it landed | `GET /api/v1/trade/agent/jobs/:clientId` or `/api/v1/launch/headless/jobs/:clientId` | `CANDLE_AGENT_API_KEY` |
122
128
  | `candle_get_fees` | The creator fees the account's launched tokens can pay: raw amounts from chain reads, and the reason a token cannot be claimed | `GET /api/v1/agent/fees` | `CANDLE_AGENT_API_KEY` (`account:read`) |
123
- | `candle_claim_fees` | Collect one token's creator fees to the wallet fixed at launch (no destination argument); `dryRun` sends nothing. Answers `LAUNCH_DISABLED` until the server enables agent claims | `POST /api/v1/agent/fees/:mint/claim` | `CANDLE_AGENT_API_KEY` (`launch:write`) |
129
+ | `candle_claim_fees` | Collect one token's creator fees to the wallet fixed at launch (no destination argument); `dryRun` sends nothing | `POST /api/v1/agent/fees/:mint/claim` | `CANDLE_AGENT_API_KEY` (`launch:write`) |
124
130
  | `candle_get_staking` | Read CNDL/sCNDL, withdrawals, rewards, pool wait and wallet eligibility notes | `GET /api/v1/agent/staking` |
125
131
  | `candle_stake` | Stake raw CNDL; stable clientStakeId, dryRun first | `POST /api/v1/agent/staking/stake/build` |
126
132
  | `candle_unstake` | Start a withdrawal of raw sCNDL; no instant unstake | `POST /api/v1/agent/staking/unstake/build` |
@@ -307,8 +313,9 @@ the section and its coverage fields. Agent boards are outside this slice.
307
313
 
308
314
  Solana staking is available through the agent rail while the deployment enables it (off by
309
315
  default). There is no instant unstake. Read the pool wait and wallet eligibility notes first:
310
- pending withdrawal CNDL counts toward nothing for Pro; outside the primary wallet sCNDL earns
311
- Solana rewards but no Hood rewards, and Pro follows the served tier mode.
316
+ pending withdrawal CNDL counts toward nothing for Pro; sCNDL in any eligible account wallet
317
+ counts toward Pro and earns the account's Hood reward, except a wallet another account also
318
+ claims, wallets past the 200-wallet cap, and an account with no EVM wallet.
312
319
 
313
320
  Tools: `candle_get_staking`, `candle_stake`, `candle_unstake`, `candle_finish_unstake`,
314
321
  `candle_claim_staking_reward`. Build tools accept stable `clientStakeId`, raw `amountRaw`
package/dist/index.js CHANGED
@@ -57,6 +57,45 @@ function assertTransportSecurity(apiUrl, env) {
57
57
  return;
58
58
  throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + (isPrivateHost(parsed.hostname) ? "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint." : "URL. CANDLE_ALLOW_INSECURE_HTTP does not apply here: it covers private networks only, " + "and this is a public address."));
59
59
  }
60
+ var REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
61
+ var MAX_REDIRECTS = 5;
62
+
63
+ class CrossOriginRedirectError extends Error {
64
+ from;
65
+ to;
66
+ constructor(from, to) {
67
+ super(`The API at ${from} redirected to ${to}, another origin. Not followed, so the API key was not sent there.`);
68
+ this.from = from;
69
+ this.to = to;
70
+ this.name = "CrossOriginRedirectError";
71
+ }
72
+ }
73
+ function keyedRedirectGuard(doFetch) {
74
+ return async (url, init) => {
75
+ if (!init?.headers || !new Headers(init.headers).has("x-api-key"))
76
+ return doFetch(url, init);
77
+ let current = url;
78
+ let next = { ...init, redirect: "manual" };
79
+ for (let hops = 0;; hops++) {
80
+ const response = await doFetch(current, next);
81
+ const headers = response.headers;
82
+ const location = REDIRECT_STATUSES.has(response.status) ? headers?.get?.("location") : null;
83
+ if (!location)
84
+ return response;
85
+ const from = new URL(current);
86
+ const target = new URL(location, from);
87
+ if (target.origin !== from.origin)
88
+ throw new CrossOriginRedirectError(from.origin, target.origin);
89
+ if (hops >= MAX_REDIRECTS)
90
+ throw new Error(`More than ${MAX_REDIRECTS} redirects from ${url}`);
91
+ const method = (next.method ?? "GET").toUpperCase();
92
+ if (response.status === 303 && method !== "GET" && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST") {
93
+ next = { ...next, method: "GET", body: undefined };
94
+ }
95
+ current = target.href;
96
+ }
97
+ };
98
+ }
60
99
  function resolveConfig(env = process.env) {
61
100
  const apiUrl = env.CANDLE_API_URL?.trim() || DEFAULT_API_URL;
62
101
  assertTransportSecurity(apiUrl, env);
@@ -112,7 +151,7 @@ import { lstat, open, realpath } from "node:fs/promises";
112
151
  import { dirname, isAbsolute, relative, sep } from "node:path";
113
152
 
114
153
  // src/version.ts
115
- var SERVER_VERSION = "0.10.15";
154
+ var SERVER_VERSION = "0.10.17";
116
155
 
117
156
  // src/update-notice.ts
118
157
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -796,6 +835,10 @@ var HYPERLIQUID_EXCHANGE_URLS = {
796
835
  mainnet: "https://api.hyperliquid.xyz/exchange",
797
836
  testnet: "https://api.hyperliquid-testnet.xyz/exchange"
798
837
  };
838
+ var HYPERLIQUID_INFO_URLS = {
839
+ mainnet: "https://api.hyperliquid.xyz/info",
840
+ testnet: "https://api.hyperliquid-testnet.xyz/info"
841
+ };
799
842
  var CANDLE_HYPERLIQUID_BUILDER_ADDRESS = null;
800
843
  var HYPERLIQUID_MAX_BUILDER_FEE_TENTHS_BPS = 100;
801
844
  var HYPERLIQUID_MAX_BUILDER_FEE_RATE = "0.1%";
@@ -1101,6 +1144,67 @@ async function hyperliquidCloseOrder(fetcher, network, address, params) {
1101
1144
  const size = asked < held ? String(params.size) : String(szi).replace(/^-/, "");
1102
1145
  return { asset, isBuy: signed < 0n, size };
1103
1146
  }
1147
+ var HYPERLIQUID_DEFAULT_SLIPPAGE_BPS = 100;
1148
+ var HYPERLIQUID_MID_DRIFT_BPS = 100;
1149
+ async function hyperliquidOrderMarket(fetcher, network, address, method, params) {
1150
+ const read = async (body) => {
1151
+ const res = await fetcher(HYPERLIQUID_INFO_URLS[network], {
1152
+ method: "POST",
1153
+ headers: { "content-type": "application/json" },
1154
+ body: JSON.stringify(body)
1155
+ });
1156
+ if (!res.ok)
1157
+ throw new Error("Cannot read the market from Hyperliquid");
1158
+ return JSON.parse(await res.text());
1159
+ };
1160
+ let coin = String(params.coin ?? "").toUpperCase();
1161
+ let order;
1162
+ if (method === "modify") {
1163
+ const status = await read({ type: "orderStatus", user: address, oid: params.cloid });
1164
+ const resting = status?.order?.order ?? null;
1165
+ if (!resting || typeof resting.coin !== "string" || decimal(resting.limitPx) === null)
1166
+ throw new Error("No resting order with this cloid on Hyperliquid");
1167
+ coin = resting.coin.toUpperCase();
1168
+ order = { isBuy: resting.side === "B", limitPx: String(resting.limitPx) };
1169
+ }
1170
+ const [meta, contexts] = await read({ type: "metaAndAssetCtxs" });
1171
+ const asset = meta.universe.findIndex((m) => m.name.toUpperCase() === coin);
1172
+ if (asset < 0 || asset >= 1e4)
1173
+ throw new Error("No main-universe market for this coin on Hyperliquid");
1174
+ const context = contexts[asset];
1175
+ const midPx = context?.midPx ?? context?.markPx;
1176
+ if ((decimal(midPx) ?? 0n) <= 0n)
1177
+ throw new Error("No Hyperliquid price for this coin");
1178
+ return { asset, midPx: String(midPx), ...order ? { order } : {} };
1179
+ }
1180
+ function checkOrderMarket(action, intent, market) {
1181
+ const list = action.type === "modify" ? [action.order] : action.orders;
1182
+ if (!Array.isArray(list) || list.length === 0)
1183
+ return "missing requested order";
1184
+ if (list.some((o) => !o || o.a !== market.asset))
1185
+ return "order asset is not the requested coin's on Hyperliquid";
1186
+ const first = list[0];
1187
+ const p = intent.params;
1188
+ if (intent.method === "open" && p.price == null) {
1189
+ const requested = p.slippageBps;
1190
+ const slippage = typeof requested === "number" && Number.isInteger(requested) && requested > 0 ? requested : HYPERLIQUID_DEFAULT_SLIPPAGE_BPS;
1191
+ const bound = BigInt(slippage + HYPERLIQUID_MID_DRIFT_BPS);
1192
+ const px = decimal(first.p) ?? 0n;
1193
+ const mid = decimal(market.midPx) ?? 0n;
1194
+ const within = first.b === true ? px * 10000n <= mid * (10000n + bound) : px * 10000n >= mid * (10000n - bound);
1195
+ if (mid <= 0n || !within)
1196
+ return "market order price is outside its slippage of Hyperliquid's mid";
1197
+ }
1198
+ if (intent.method === "modify") {
1199
+ if (!market.order)
1200
+ return "no resting order on Hyperliquid to check the modify against";
1201
+ if (first.b !== market.order.isBuy)
1202
+ return "modify changes the resting order's side";
1203
+ if (p.price == null && decimal(first.p) !== decimal(market.order.limitPx))
1204
+ return "modify changes the resting order's price";
1205
+ }
1206
+ return null;
1207
+ }
1104
1208
  function verifyPerpsBuild(build, opts) {
1105
1209
  try {
1106
1210
  const network = opts.network ?? "mainnet";
@@ -1128,6 +1232,13 @@ function verifyPerpsBuild(build, opts) {
1128
1232
  if (!order || order.a !== opts.closeOrder.asset || order.b !== opts.closeOrder.isBuy || decimal(order.s) !== decimal(opts.closeOrder.size) || order.r !== true)
1129
1233
  return { ok: false, reason: "close side, asset or size does not match the requested position" };
1130
1234
  }
1235
+ if (opts.market) {
1236
+ if (opts.intent?.method !== "open" && opts.intent?.method !== "modify")
1237
+ return { ok: false, reason: "a market check needs an open or modify intent" };
1238
+ const marketRefusal = checkOrderMarket(action, opts.intent, opts.market);
1239
+ if (marketRefusal)
1240
+ return { ok: false, reason: marketRefusal };
1241
+ }
1131
1242
  if (typed.primary_type === "Agent") {
1132
1243
  const type = action.type;
1133
1244
  if (typeof type !== "string" || !HYPERLIQUID_ALLOWED_ACTION_TYPES.includes(type)) {
@@ -1345,6 +1456,18 @@ async function executePerps(tool, args, cfg, env, fetch2) {
1345
1456
  if (!closeCheck.ok)
1346
1457
  return fail("PERPS_BUILD_REFUSED", `Refused to sign this build: ${closeCheck.reason}. Nothing was signed.`);
1347
1458
  }
1459
+ if (method === "open") {
1460
+ const market = await hyperliquidOrderMarket(fetch2, network, row.address, method, fields);
1461
+ const marketCheck = verifyPerpsBuild(build, {
1462
+ builder,
1463
+ network,
1464
+ address: row.address,
1465
+ intent: { method, params: fields },
1466
+ market
1467
+ });
1468
+ if (!marketCheck.ok)
1469
+ return fail("PERPS_BUILD_REFUSED", `Refused to sign this build: ${marketCheck.reason}. Nothing was signed.`);
1470
+ }
1348
1471
  if (!willSubmit || signerPem === null)
1349
1472
  return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
1350
1473
  if (!appId || !row.privyWalletId)
@@ -1591,7 +1714,7 @@ var PLAN_CAPABILITY_LABELS = {
1591
1714
  createLinkedWallets: "Create linked wallets",
1592
1715
  importLinkedWallets: "Import linked wallets",
1593
1716
  limitOrders: "Limit orders",
1594
- quant: "Quant on Telegram"
1717
+ quant: "Quant deep analyses (Max)"
1595
1718
  };
1596
1719
  var PLAN_LABELS = { free: "Free", believer: "Believer", pro: "Pro", max: "Max" };
1597
1720
  function planLabel(plan) {
@@ -1653,8 +1776,9 @@ function planTableMarkdown(table) {
1653
1776
 
1654
1777
  // src/tools.ts
1655
1778
  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.";
1779
+ 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.";
1656
1780
  var localProfileTools = new WeakMap;
1657
- var STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. Outside the account's main wallet, sCNDL earns no Hood staking rewards; Pro eligibility follows the deployment's tier mode (main-wallet mode excludes it). CNDL in a pending withdrawal counts toward nothing for Pro.";
1781
+ 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.";
1658
1782
  var stakingWriteShape = {
1659
1783
  payer: z2.enum(["main", "linked"]).optional().describe("main is the embedded wallet; linked names a linked or TEE wallet"),
1660
1784
  linkedWalletId: z2.string().min(1).optional(),
@@ -2072,7 +2196,11 @@ var launchTokenShape = {
2072
2196
  bannerUrl: z2.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"),
2073
2197
  chain: z2.string().optional().describe('"solana" or "hood"; defaults to solana'),
2074
2198
  quoteAsset: z2.string().optional().describe("Quote asset symbol; defaults per chain"),
2075
- mode: z2.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"),
2199
+ mode: z2.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"),
2200
+ curve: z2.object({
2201
+ virtualQuote: z2.string().describe('V: the virtual quote the curve starts with, in whole quote units, e.g. "12" SOL'),
2202
+ threshold: z2.string().describe('G: the quote the curve raises before it graduates, in whole quote units, e.g. "40" SOL')
2203
+ }).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"),
2076
2204
  stakerAllocationBps: z2.number().optional().describe("Staker allocation in basis points"),
2077
2205
  dexVersion: z2.string().nullable().optional().describe('"v3" or "v4"; required for hood launches'),
2078
2206
  buyAmount: z2.union([z2.string(), z2.number()]).optional().describe("Initial buy, in quote base units (string or number)"),
@@ -2229,7 +2357,7 @@ function registerToolSubset(server, options) {
2229
2357
  const allowed = new Set(options.tools);
2230
2358
  const getConfig = options.getConfig;
2231
2359
  const env = options.env ?? {};
2232
- const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
2360
+ const doFetch = keyedRedirectGuard(options.fetch ?? ((url, init) => fetch(url, init)));
2233
2361
  const register = (name, definition, handler) => {
2234
2362
  if (!allowed.has(name))
2235
2363
  return;
@@ -2243,7 +2371,7 @@ function registerToolSubset(server, options) {
2243
2371
  const imageDir = configuredImageDir(env);
2244
2372
  register("candle_launch_token", {
2245
2373
  title: "Launch a token on Candle",
2246
- description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything." + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
2374
+ description: "Launch a new token via the Candle headless launch API, from the account's embedded wallet. Works on every plan, Free included. Set dryRun: true to validate without spending anything. " + CUSTOM_CURVE_RULE + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
2247
2375
  inputSchema: imageDir ? localLaunchTokenShape : launchTokenShape
2248
2376
  }, async (args) => {
2249
2377
  const image = await resolveLaunchImage(args, imageDir, getConfig(), doFetch);
@@ -2390,7 +2518,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
2390
2518
  }, async (args) => callAndRelay("candle_get_fees", args, getConfig(), doFetch));
2391
2519
  register("candle_claim_fees", {
2392
2520
  title: "Claim a token's creator fees",
2393
- description: "Collect one token's creator fees and pay them to the wallet fixed at launch. There is no destination " + "argument and the tool cannot send funds anywhere else. Candle signs with the embedded wallet, which " + "pays the network fee. Needs launch:write, and the server keeps it off until it is enabled (LAUNCH_DISABLED). " + "Call it with dryRun: true first; a real claim moves real funds into the recipient wallet, which may be " + "the wallet this agent trades from. Not idempotent by id: if the result is lost, read candle_get_fees " + "before claiming again.",
2521
+ description: "Collect one token's creator fees and pay them to the wallet fixed at launch. There is no destination " + "argument and the tool cannot send funds anywhere else. Candle signs with the embedded wallet, which " + "pays the network fee. Needs launch:write. " + "Call it with dryRun: true first; a real claim moves real funds into the recipient wallet, which may be " + "the wallet this agent trades from. Not idempotent by id: if the result is lost, read candle_get_fees " + "before claiming again.",
2394
2522
  inputSchema: claimFeesShape
2395
2523
  }, async (args) => callAndRelay("candle_claim_fees", args, getConfig(), doFetch));
2396
2524
  register("candle_get_wallets", {
@@ -2494,7 +2622,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
2494
2622
  });
2495
2623
  register("candle_launch_and_seed", {
2496
2624
  title: "Launch a token and seed it",
2497
- 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." + (imageDir ? IMAGE_PATH_DISCLOSURE : ""),
2625
+ 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 : ""),
2498
2626
  inputSchema: imageDir ? localLaunchAndSeedShape : launchAndSeedShape
2499
2627
  }, async (args) => {
2500
2628
  const result = await executeLaunchAndSeed(args, getConfig(), doFetch, imageDir);
@@ -2556,7 +2684,7 @@ async function registerEligibleProfileTool(server, env = process.env, doFetch =
2556
2684
  let eligible = false;
2557
2685
  if (cfg.apiKey && wanted.length > 0) {
2558
2686
  try {
2559
- const res = await doFetch(`${cfg.apiUrl}/api/v1/agent/keys/self/public-profile`, {
2687
+ const res = await keyedRedirectGuard(doFetch)(`${cfg.apiUrl}/api/v1/agent/keys/self/public-profile`, {
2560
2688
  method: "GET",
2561
2689
  headers: { "x-api-key": cfg.apiKey },
2562
2690
  signal: AbortSignal.timeout(15000)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.10.15",
3
+ "version": "0.10.17",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",