@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.
- package/README.md +7 -3
- package/dist/index.js +139 -10
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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({
|
|
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('
|
|
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:
|
|
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: "
|
|
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
|
}
|