@candledottv/mcp 0.6.4 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/index.js +139 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @candledottv/mcp
2
2
 
3
3
  An MCP (Model Context Protocol) server for the Candle agent rail. It exposes Candle's REST API as
4
- fifteen tools over stdio, so an MCP-capable agent can launch tokens (optionally seeded with a dev
4
+ nineteen tools over stdio, so an MCP-capable agent can launch tokens (optionally seeded with a dev
5
5
  buy in the same call), trade, convert between base assets (including across chains), read market
6
6
  and feed data, report on-chain activity, and check an agent profile without hand-rolling HTTP
7
7
  calls.
@@ -31,7 +31,7 @@ Environment below -- or skip env editing entirely: install the Candle CLI
31
31
  environment.
32
32
 
33
33
  `CANDLE_MCP_TOOLS` (optional) is a comma-separated allowlist of tool names; only those register.
34
- Unset means all fifteen. An unknown name fails startup with the valid names in the message, rather
34
+ Unset means all nineteen. An unknown name fails startup with the valid names in the message, rather
35
35
  than silently registering the wrong surface. `candle mcp --read-only` / `--tools` set this for
36
36
  you.
37
37
 
@@ -95,6 +95,10 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
95
95
  | `candle_sweep` | Sweep a wallet to one destination | One `POST /api/v1/agent/transfer` per asset, `amountRaw: "max"` | `CANDLE_AGENT_API_KEY` (`transfer:write`) |
96
96
  | `candle_resolve_token` | Turn a bare mint or contract address into Candle's market for it | `GET /api/v1/markets/:chain/:mint` | none |
97
97
  | `candle_get_wallets` | The account's embedded wallets, one per chain, with delegation state | `GET /api/v1/agent/wallets/embedded` | `CANDLE_AGENT_API_KEY` |
98
+ | `candle_get_profile_wallets` | Which wallets an agent profile may spend from, and whether it is scoped | `GET /api/v1/agent/keys/:prefix/wallets` | `CANDLE_AGENT_API_KEY` |
99
+ | `candle_set_profile_wallets` | Replace the wallets an agent profile may spend from | `PUT /api/v1/agent/keys/:prefix/wallets` | `CANDLE_AGENT_API_KEY` |
100
+ | `candle_get_profile_pnl` | An agent profile's realized P&L, fees, and open positions at cost basis | `GET /api/v1/agent/keys/:prefix/pnl` | `CANDLE_AGENT_API_KEY` |
101
+ | `candle_get_profile_trades` | An agent profile's orders, fills, fees and transaction hashes | `GET /api/v1/agent/keys/:prefix/trades` | `CANDLE_AGENT_API_KEY` |
98
102
  | `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` |
99
103
  | `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` |
100
104
 
@@ -113,7 +117,7 @@ explicitly. Assets with nothing spendable report `empty`; a failed asset never s
113
117
  ## Errors
114
118
 
115
119
  This package never reinterprets an error body, and that body is not one uniform shape across all
116
- fifteen tools:
120
+ nineteen tools:
117
121
 
118
122
  - `candle_launch_token`, `candle_get_market`, and `candle_get_feed` hit endpoints that use the
119
123
  structured envelope `{ success: false, error: { code, message, ... } }`. Branch on `error.code`.
package/dist/index.js CHANGED
@@ -106,7 +106,7 @@ function percentOfBalance(balanceRaw, percent) {
106
106
  import { randomUUID } from "node:crypto";
107
107
 
108
108
  // src/version.ts
109
- var SERVER_VERSION = "0.6.4";
109
+ var SERVER_VERSION = "0.8.0";
110
110
 
111
111
  // src/update-notice.ts
112
112
  var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
@@ -499,6 +499,10 @@ var TOOL_NAMES = [
499
499
  "candle_transfer",
500
500
  "candle_sweep",
501
501
  "candle_get_wallets",
502
+ "candle_get_profile_wallets",
503
+ "candle_set_profile_wallets",
504
+ "candle_get_profile_pnl",
505
+ "candle_get_profile_trades",
502
506
  "candle_resolve_token",
503
507
  "candle_execution_status",
504
508
  "candle_get_operation"
@@ -572,8 +576,15 @@ function buildRequest(name, args, cfg) {
572
576
  };
573
577
  }
574
578
  case "candle_get_feed": {
575
- const { bucket, chain } = args;
576
- const query = new URLSearchParams({ bucket, ...chain ? { chain } : {} });
579
+ const { bucket, chain, where, sort, fields, limit } = args;
580
+ const query = new URLSearchParams({
581
+ bucket,
582
+ ...chain ? { chain } : {},
583
+ ...where ? { where } : {},
584
+ ...sort ? { sort } : {},
585
+ ...fields ? { fields } : {},
586
+ ...limit ? { limit } : {}
587
+ });
577
588
  return {
578
589
  url: `${base2}/api/v1/markets/feed?${query.toString()}`,
579
590
  init: { method: "GET", headers: jsonHeaders() }
@@ -613,6 +624,39 @@ function buildRequest(name, args, cfg) {
613
624
  init: { method: "GET", headers: jsonHeaders(apiKey) }
614
625
  };
615
626
  }
627
+ case "candle_get_profile_wallets": {
628
+ const apiKey = requireApiKey2(cfg);
629
+ const { keyPrefix } = args;
630
+ return {
631
+ url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`,
632
+ init: { method: "GET", headers: jsonHeaders(apiKey) }
633
+ };
634
+ }
635
+ case "candle_set_profile_wallets": {
636
+ const apiKey = requireApiKey2(cfg);
637
+ const { keyPrefix, walletIds } = args;
638
+ return {
639
+ url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/wallets`,
640
+ init: { method: "PUT", headers: jsonHeaders(apiKey), body: JSON.stringify({ walletIds }) }
641
+ };
642
+ }
643
+ case "candle_get_profile_pnl": {
644
+ const apiKey = requireApiKey2(cfg);
645
+ const { keyPrefix } = args;
646
+ return {
647
+ url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/pnl`,
648
+ init: { method: "GET", headers: jsonHeaders(apiKey) }
649
+ };
650
+ }
651
+ case "candle_get_profile_trades": {
652
+ const apiKey = requireApiKey2(cfg);
653
+ const { keyPrefix, limit } = args;
654
+ const query = limit === undefined ? "" : `?limit=${encodeURIComponent(String(limit))}`;
655
+ return {
656
+ url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/trades${query}`,
657
+ init: { method: "GET", headers: jsonHeaders(apiKey) }
658
+ };
659
+ }
616
660
  case "candle_transfer": {
617
661
  const apiKey = requireApiKey2(cfg);
618
662
  return {
@@ -659,7 +703,11 @@ var tokenForensicsShape = {
659
703
  };
660
704
  var getFeedShape = {
661
705
  bucket: z.enum(["new", "graduated", "onfire", "bluechip"]),
662
- chain: z.string().optional().describe("Optional chain filter")
706
+ chain: z.string().optional().describe("Optional chain filter"),
707
+ where: z.string().optional().describe('JSON filter, e.g. {"marketCap":{"lt":150000},"liquidityUsd":{"gte":25000},"mintAuthorityDisabled":{"eq":true}}. ' + "Comparators: eq, ne, lt, lte, gt, gte, present. An ABSENT field satisfies none of them except " + "present:false, so a filter for mintAuthorityDisabled eq true returns only tokens that actually say " + "true, never ones where the flag is simply missing. Use present:false to find the tokens with no data."),
708
+ sort: z.string().optional().describe('Sort as "field" or "field:asc" / "field:desc". A bare field means desc.'),
709
+ fields: z.string().optional().describe("Comma-separated fields to return, e.g. symbol,marketCap,liquidityUsd. chain, address and symbol always " + "ride along. Cuts a 135KB response to a couple of KB."),
710
+ limit: z.string().optional().describe("Max rows to return, 1-200.")
663
711
  };
664
712
  var reportActivityShape = {
665
713
  chain: z.string().describe('"solana" or "hood"'),
@@ -675,6 +723,20 @@ var getOperationShape = {
675
723
  var resolveTokenShape = {
676
724
  mint: z.string().describe("Token mint (Solana, base58) or contract address (Hood, 0x-prefixed)")
677
725
  };
726
+ var profileWalletsShape = {
727
+ keyPrefix: z.string().describe("The profile's API key prefix, as listed by candle keys list or in the dashboard")
728
+ };
729
+ var profilePnlShape = {
730
+ keyPrefix: z.string().describe("The profile's API key prefix")
731
+ };
732
+ var profileTradesShape = {
733
+ keyPrefix: z.string().describe("The profile's API key prefix"),
734
+ limit: z.number().optional().describe("How many of the most recent trades to return. Default 200, max 1000.")
735
+ };
736
+ var setProfileWalletsShape = {
737
+ keyPrefix: z.string().describe("The profile's API key prefix"),
738
+ walletIds: z.array(z.string()).describe("The linked-wallet ids the profile may spend from -- ids, not addresses. This REPLACES the " + "whole set: any wallet omitted loses access. An empty array assigns none.")
739
+ };
678
740
  var swapShape = {
679
741
  from: z.enum(["SOL", "USDC", "CNDL", "ETH", "USDG"]).describe("Base asset to spend"),
680
742
  to: z.enum(["SOL", "USDC", "CNDL", "ETH", "USDG"]).describe("Base asset to receive; must differ from `from`"),
@@ -688,7 +750,7 @@ var tradeShape = {
688
750
  side: z.enum(["buy", "sell"]),
689
751
  amount: z.string().optional().describe("Decimal amount. Buys: how much of THIS TOKEN'S OWN quote asset to spend (SOL for a " + 'SOL-launched token, USDC for a USDC-quoted one, and so on: e.g. "0.5"). Sells: how many ' + "TOKENS to sell. Pass exactly one of amount or percent."),
690
752
  percent: z.number().optional().describe("Sells only: sell this percent (integer 1-100) of the wallet's holding, on either chain."),
691
- quoteAsset: z.string().optional().describe('Quote asset ("sol", "usdc", "cndl") for an arbitrary Solana mint Candle never launched ' + "(Pro/Max only); defaults to sol. Ignored for a Candle-launched token, whose quote comes " + "from the token itself, so it never changes how a buy amount is interpreted there."),
753
+ quoteAsset: z.string().optional().describe('What the wallet spends on a buy or receives on a sell: "sol", "usdc" or "cndl" on Solana, ' + '"eth" or "usdg" on Hood. Safe to pass through from candle_quote. On Solana it applies only ' + "to an arbitrary mint Candle never launched (Pro/Max) and is ignored for a Candle token, " + "whose quote comes from the token itself. On Hood it is the settlement asset of a DEX " + "trade; a USDG buy adds an approval transaction an ETH buy does not. It is not the route: " + "the cheapest path to the asset is chosen separately. Defaults to sol / ETH settlement."),
692
754
  maxSlippageBps: z.number().optional().describe("Max slippage in basis points; API default applies when omitted"),
693
755
  clientTradeId: z.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result. Retrying with the " + "SAME id is safe (idempotent replay); a new id is a SECOND trade.")
694
756
  };
@@ -726,17 +788,25 @@ function registerTools(server, env = process.env) {
726
788
  }, async (args) => callAndRelay("candle_launch_token", args, cfg));
727
789
  register("candle_get_market", {
728
790
  title: "Get market state",
729
- description: "Read the current market state for a token: lifecycle, pool address, whether buys are open.",
791
+ description: "Read the current market state for a token: lifecycle, pool address, whether buys are " + `open. Reads only; moves nothing. No key needed.
792
+
793
+ ` + "COVERAGE: this answers for tokens that have a CANDLE market. candle_get_feed indexes " + "the wider market too (pump.fun, pons.family), so a mint that feed just returned can " + "still come back MARKET_NOT_FOUND here. That is a coverage boundary, not a fault and " + "not a reason to retry or re-authenticate -- say Candle has no market for it and move on.",
730
794
  inputSchema: getMarketShape
731
795
  }, async (args) => callAndRelay("candle_get_market", args, cfg));
732
796
  register("candle_token_forensics", {
733
797
  title: "Token forensics",
734
- description: "Gate a buy before making it: deployer history, who bought in the deploy window (the creator's own wallets are marked disclosed; strangers in the same slot are the bundle signal), holder concentration, and a risk tier (LOW/MODERATE/HIGH/CRITICAL) with per-factor reasons. Every measurement carries a coverage note -- 'unavailable' is not 'clean'. No key needed.",
798
+ description: `Gate a buy before making it: deployer history, who bought in the deploy window (the creator's own wallets are marked disclosed; strangers in the same slot are the bundle signal), holder concentration, and a risk tier (LOW/MODERATE/HIGH/CRITICAL) with per-factor reasons. Every measurement carries a coverage note -- 'unavailable' is not 'clean'. No key needed.
799
+
800
+ MARKET_NOT_FOUND means Candle has no market for that token and this could not run. That is also not 'clean': report that you could not check it, rather than reporting the token as safe. That refusal now carries error.coverage -- covered:false, a reason ('external_launchpad' when the token launched somewhere else, 'unknown_mint' when nobody has indexed it), the launchpad when known, and every check that consequently did not run. Read it instead of guessing. Most of the feed answers this way.`,
735
801
  inputSchema: tokenForensicsShape
736
802
  }, async (args) => callAndRelay("candle_token_forensics", args, cfg));
737
803
  register("candle_get_feed", {
738
804
  title: "Get a token feed",
739
- description: "Read one of the trade page's public feeds: new, graduated, onfire, or bluechip.",
805
+ description: "Read one of the trade page's public feeds: new, graduated, onfire, or bluechip. Reads " + `only; moves nothing. No key needed. Start here when nobody has named a token.
806
+
807
+ ` + "This indexes the WIDER market, not just Candle's own launches, so rows carry a " + "`launchpad` (pump.fun, pons.family, ...). A row appearing here does NOT mean Candle " + "has a market for it: candle_get_market and candle_token_forensics can legitimately " + `answer MARKET_NOT_FOUND for a mint this returned.
808
+
809
+ ` + "Filter, sort and pick fields SERVER-SIDE rather than reading the whole feed: an " + "unfiltered response is around 135KB and will not fit in a tool result. See `where`, " + "`sort` and `fields`.\n\n" + "One rule to know before screening on safety: a missing field is NOT a false one. " + "mintAuthorityDisabled and freezeAuthorityDisabled are absent on a real share of rows, " + "and absent means nobody checked, not that the authority is disabled. `where` never lets " + 'an absent field satisfy a comparison, so {"mintAuthorityDisabled":{"eq":true}} returns ' + "only tokens that actually say so.",
740
810
  inputSchema: getFeedShape
741
811
  }, async (args) => callAndRelay("candle_get_feed", args, cfg));
742
812
  register("candle_report_activity", {
@@ -767,6 +837,26 @@ function registerTools(server, env = process.env) {
767
837
  description: "The account's EMBEDDED wallets, one per chain, with their delegation state. These are " + "the wallets candle_trade, candle_swap and candle_transfer spend from, so this is how an " + "agent finds its own funding addresses. Reads only; moves nothing. Not the same as the " + "account's LINKED wallets, which are the owner's own wallets and are not spent from here. " + "Balances are not included: read a specific one with the market and balance endpoints.",
768
838
  inputSchema: {}
769
839
  }, async () => callAndRelay("candle_get_wallets", {}, cfg));
840
+ register("candle_get_profile_wallets", {
841
+ title: "Read which wallets an agent profile can spend from",
842
+ description: "An agent profile (API key) either spends from EVERY wallet on its account or only from " + "the ones assigned to it. Read walletScope before drawing any conclusion from the list: " + "an empty list means 'every wallet' under scope 'all' and 'none at all' under 'selected'. " + "Reads only; moves nothing.",
843
+ inputSchema: profileWalletsShape
844
+ }, async (args) => callAndRelay("candle_get_profile_wallets", args, cfg));
845
+ register("candle_set_profile_wallets", {
846
+ title: "Set which wallets an agent profile can spend from",
847
+ description: "REPLACES the profile's whole wallet set with the ids given, so a wallet left out of the " + "list loses access; pass an empty list to leave the profile with no wallets. NARROWING " + "ONLY: an API key can remove wallets from its OWN profile, but naming one it does not " + "already hold is a grant and needs a human in the dashboard, as does editing any other " + "profile. Takes effect only while the profile's scope is 'selected'. Wallet ids are the " + "linked-wallet ids, not addresses.",
848
+ inputSchema: setProfileWalletsShape
849
+ }, async (args) => callAndRelay("candle_set_profile_wallets", args, cfg));
850
+ register("candle_get_profile_pnl", {
851
+ title: "Read an agent profile's realized P&L",
852
+ description: "Realized profit for this profile's own fills, the Candle fees charged against it, and the " + "positions it still holds with their COST BASIS -- not their current value, which is not marked " + "here. Deposits, withdrawals and transfers are excluded: funding a wallet is not profit. Check " + "`unvalued` and `truncated` before quoting the number; they mean the total is partial. Reads only.",
853
+ inputSchema: profilePnlShape
854
+ }, async (args) => callAndRelay("candle_get_profile_pnl", args, cfg));
855
+ register("candle_get_profile_trades", {
856
+ title: "Read an agent profile's trade history",
857
+ description: "Orders, actual fills, fees, timestamps and transaction hashes for this profile. Includes FAILED " + "trades, with an errorCode saying why each did not go through, so this answers 'what happened to " + "my order' as well as 'what did I trade'. Reads only; moves nothing.",
858
+ inputSchema: profileTradesShape
859
+ }, async (args) => callAndRelay("candle_get_profile_trades", args, cfg));
770
860
  register("candle_resolve_token", {
771
861
  title: "Resolve a contract address to a token",
772
862
  description: "Turn a bare contract address or mint into Candle's market for it: chain, symbol, " + "decimals, quote asset, and whether Candle can trade it. Start here when a human gives " + "you an address and nothing else. The chain is read off the address's own shape and is " + "not guessed, so it does not need to be supplied. Reads only; moves nothing. A 404 means " + "Candle has no market for that address, which is an answer, not a failure to retry.",
@@ -812,7 +902,18 @@ function registerTools(server, env = process.env) {
812
902
  });
813
903
  register("candle_trade", {
814
904
  title: "Buy or sell a token",
815
- description: "Execute a buy or sell through the Candle trade rail. MOVES REAL FUNDS: the payer is the " + "account's embedded (main) wallet, executed server-side via delegation. Amounts are " + "decimal (never raw base units). Retry a timeout with the SAME clientTradeId from the " + "result; a new id is a second trade.",
905
+ description: "Buy or sell a token. MOVES REAL FUNDS: the payer is the account's embedded (main) " + `wallet, executed server-side via delegation.
906
+
907
+ ` + `Before the first trade of a run, once:
908
+ ` + "1. candle_execution_status -- confirms the wallets, the tier and this key's spend " + "limits. Call it at the start, or after an auth error; do not infer readiness from a " + `failed trade.
909
+ ` + "2. candle_resolve_token -- if a human handed you a bare address. It returns the chain, " + `so you never have to guess it.
910
+ ` + "3. candle_token_forensics -- before you quote or buy anything. It returns a risk tier " + "and per-factor reasons. MARKET_NOT_FOUND there means Candle has no market for the " + `token, NOT that the token is clean.
911
+
912
+ ` + "Arguments: `mint` and `side` are required. Amounts are DECIMAL, never raw base units " + '(amount: "0.5", not lamports). Omitting the amount on a sell sells the whole ' + `position.
913
+
914
+ ` + `After the call:
915
+ ` + "- A timeout is not a failure. Retry with the SAME clientTradeId from the result -- it " + `coalesces the duplicate. A NEW id is a SECOND trade, and that is how you double-spend.
916
+ ` + "- If you no longer hold the result, do not re-send to find out what happened. Ask " + "candle_get_operation with the clientTradeId; a 404 there means the trade never reached " + "the rail and nothing moved.",
816
917
  inputSchema: tradeShape
817
918
  }, async (args) => {
818
919
  const result = await executeTrade(args, cfg, fetch);
@@ -829,8 +930,36 @@ function registerTools(server, env = process.env) {
829
930
  }
830
931
 
831
932
  // src/server.ts
933
+ var INSTRUCTIONS = `Candle is a trading and token-launch rail for agents. You hold a scoped API key, never a private key; signing and funding stay with the key owner's wallet.
934
+
935
+ START HERE — five tools need NO credential. Call these first to confirm the server is wired before asking anyone for anything:
936
+ candle_get_market price, market cap, volume, curve state for one token
937
+ candle_get_feed the roster: hot streak, new pairs, graduated, blue chip
938
+ candle_resolve_token a ticker or partial name -> mint address + chain
939
+ candle_token_forensics call this before quoting or buying, whenever the token has a Candle market. Returns deployer history, who bought in the deploy window (strangers in the same slot are the bundle signal), holder concentration, and a risk tier LOW/MODERATE/HIGH/CRITICAL with per-factor reasons
940
+ candle_get_agent_profile your own tier, caps and verified activity
941
+
942
+ COVERAGE — read this before you treat an error as a broken server.
943
+ candle_get_feed indexes the wider market (pump.fun, pons.family and other external launchpads).
944
+ candle_get_market and candle_token_forensics answer for tokens that have a CANDLE market. So a
945
+ mint that candle_get_feed just returned can still come back MARKET_NOT_FOUND from those two, and
946
+ that is a coverage boundary, not a fault and not a reason to retry, re-auth, or tell the human the
947
+ integration is down. Report it as "Candle has no market for this token, so I could not run
948
+ forensics on it" and let the human decide.
949
+
950
+ Never let a MARKET_NOT_FOUND stand in for a clean bill of health. The same rule governs the
951
+ coverage note on every forensics measurement: "unavailable" is NOT "clean" — say so rather than
952
+ reporting a token as safe.
953
+
954
+ WRITING (trade, launch, transfer, sweep, swap) needs a key. If a call returns an auth error, the fix is on the human's side: they run \`candle auth login\`, which authorizes a device in the browser and stores the credentials. Do not ask them to paste a key into a config file, and do not retry the call until they confirm.
955
+
956
+ Two chains: solana and hood. Most tools take an explicit chain — resolve it with candle_resolve_token rather than guessing.
957
+
958
+ Writes are idempotent by client token and may be asynchronous: poll candle_execution_status or candle_get_operation rather than re-issuing a call. Re-issuing is how you double-spend.
959
+
960
+ Full reference, error catalogue and end-to-end recipes: https://docs.candle.tv — and AGENTS.md in github.com/candledottv/agentic.`;
832
961
  function createCandleMcpServer(env = process.env) {
833
- const server = new McpServer({ name: "candle-mcp", version: SERVER_VERSION });
962
+ const server = new McpServer({ name: "candle-mcp", version: SERVER_VERSION }, { instructions: INSTRUCTIONS });
834
963
  registerTools(server, env);
835
964
  return server;
836
965
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.6.4",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server for the Candle agent rail: launch tokens, read markets, report activity",
5
5
  "type": "module",
6
6
  "license": "MIT",