@candledottv/mcp 0.4.0 → 0.5.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 +10 -3
  2. package/dist/index.js +66 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -25,8 +25,10 @@ server with no `env` block at all and those three work immediately:
25
25
  ```
26
26
 
27
27
  Add `CANDLE_AGENT_API_KEY` only once you're ready to launch, trade, or report activity, see
28
- Environment below -- or skip env editing entirely: after `npx @candledottv/cli auth login`,
29
- `candle mcp` launches this server with the stored key and API URL in its environment.
28
+ Environment below -- or skip env editing entirely: install the Candle CLI
29
+ (`curl -fsSL https://candle.tv/install.sh | bash` or `brew install candledottv/tap/candle`), run
30
+ `candle auth login`, and `candle mcp` launches this server with the stored key and API URL in its
31
+ environment.
30
32
 
31
33
  `CANDLE_MCP_TOOLS` (optional) is a comma-separated allowlist of tool names; only those register.
32
34
  Unset means all eight. An unknown name fails startup with the valid names in the message, rather
@@ -59,7 +61,12 @@ tools, getting a key, funding the embedded wallet, and idempotent retries, see
59
61
  ## Environment
60
62
 
61
63
  - `CANDLE_API_URL` -- base URL of the Candle API. Defaults to `https://api.alpha.candle.tv` (the alpha deployment; production does not serve the agent API yet). Set it to
62
- `http://localhost:3001` when developing against a local API.
64
+ `http://localhost:3001` when developing against a local API. A cleartext `http://` URL pointing
65
+ at a NON-loopback host is refused at startup, since every write tool sends `x-api-key`; loopback
66
+ (`localhost`, `127.0.0.0/8`, `::1`) needs no opt-in.
67
+ - `CANDLE_ALLOW_INSECURE_HTTP` -- set to any non-empty value to allow an `http://` API URL to a
68
+ non-loopback host. For a trusted local endpoint that is not loopback, such as a devcontainer
69
+ reaching its host; not for anything that leaves the machine.
63
70
  - `CANDLE_AGENT_API_KEY` -- an agent API key (`cndl_live_...` / `cndl_test_...`), issued from a
64
71
  Candle account's agent settings page. Only required by `candle_launch_token`,
65
72
  `candle_report_activity`, `candle_trade`, `candle_launch_and_seed`, and `candle_swap`; the three
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/index.ts
3
+ // src/server.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
@@ -9,9 +9,36 @@ import { z } from "zod";
9
9
 
10
10
  // src/client.ts
11
11
  var DEFAULT_API_URL = "https://api.alpha.candle.tv";
12
- function resolveConfig() {
13
- const apiUrl = process.env.CANDLE_API_URL?.trim() || DEFAULT_API_URL;
14
- const apiKey = process.env.CANDLE_AGENT_API_KEY?.trim() || process.env.CANDLE_API_KEY?.trim();
12
+ function isLoopbackHost(hostname) {
13
+ const host = hostname.toLowerCase();
14
+ if (host === "localhost" || host.endsWith(".localhost"))
15
+ return true;
16
+ if (host === "::1" || host === "[::1]")
17
+ return true;
18
+ return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
19
+ }
20
+ function assertTransportSecurity(apiUrl, env) {
21
+ let parsed;
22
+ try {
23
+ parsed = new URL(apiUrl);
24
+ } catch {
25
+ throw new Error(`CANDLE_API_URL is not a valid URL: ${JSON.stringify(apiUrl)}`);
26
+ }
27
+ if (parsed.protocol === "https:")
28
+ return;
29
+ if (parsed.protocol !== "http:") {
30
+ throw new Error(`CANDLE_API_URL must be http or https, got ${parsed.protocol.replace(":", "")}`);
31
+ }
32
+ if (isLoopbackHost(parsed.hostname))
33
+ return;
34
+ if (env.CANDLE_ALLOW_INSECURE_HTTP?.trim())
35
+ return;
36
+ throw new Error(`Refusing to send credentials in the clear to ${parsed.origin}. Set CANDLE_API_URL to an https:// ` + "URL, or set CANDLE_ALLOW_INSECURE_HTTP=1 if this really is a trusted local endpoint.");
37
+ }
38
+ function resolveConfig(env = process.env) {
39
+ const apiUrl = env.CANDLE_API_URL?.trim() || DEFAULT_API_URL;
40
+ assertTransportSecurity(apiUrl, env);
41
+ const apiKey = env.CANDLE_AGENT_API_KEY?.trim() || env.CANDLE_API_KEY?.trim();
15
42
  return apiKey ? { apiUrl, apiKey } : { apiUrl };
16
43
  }
17
44
 
@@ -135,7 +162,7 @@ async function postJson(url, apiKey, payload, doFetch) {
135
162
  }
136
163
  }
137
164
  async function readMarket(mint, cfg, doFetch, extra) {
138
- const res = await doFetch(`${base(cfg)}/api/v1/markets/${chainForMint(mint)}/${mint}`, {
165
+ const res = await doFetch(`${base(cfg)}/api/v1/markets/${chainForMint(mint)}/${encodeURIComponent(mint)}`, {
139
166
  method: "GET",
140
167
  headers: headers()
141
168
  });
@@ -210,7 +237,7 @@ async function executeTrade(args, cfg, doFetch) {
210
237
  if (!address) {
211
238
  return errText(`percent sells need an embedded ${chain === "hood" ? "EVM" : "Solana"} wallet, and this account has none`, { clientTradeId });
212
239
  }
213
- const balRes = await doFetch(`${base(cfg)}/api/v1/tokens/${args.mint}/balance/${address}`, {
240
+ const balRes = await doFetch(`${base(cfg)}/api/v1/tokens/${encodeURIComponent(args.mint)}/balance/${encodeURIComponent(address)}`, {
214
241
  method: "GET",
215
242
  headers: headers()
216
243
  });
@@ -274,7 +301,9 @@ async function executeLaunchAndSeed(args, cfg, doFetch) {
274
301
  let market = null;
275
302
  let note;
276
303
  try {
277
- const marketRes = await doFetch(`${base(cfg)}/api/v1/markets/${launch.chain ?? "solana"}/${launch.mint}`, {
304
+ if (launch.mint === undefined)
305
+ throw new Error("the launch response carried no mint");
306
+ const marketRes = await doFetch(`${base(cfg)}/api/v1/markets/${encodeURIComponent(launch.chain ?? "solana")}/${encodeURIComponent(launch.mint)}`, {
278
307
  method: "GET",
279
308
  headers: headers()
280
309
  });
@@ -408,12 +437,15 @@ function buildRequest(name, args, cfg) {
408
437
  }
409
438
  case "candle_get_market": {
410
439
  const { chain, mint } = args;
411
- return { url: `${base2}/api/v1/markets/${chain}/${mint}`, init: { method: "GET", headers: jsonHeaders() } };
440
+ return {
441
+ url: `${base2}/api/v1/markets/${encodeURIComponent(chain)}/${encodeURIComponent(mint)}`,
442
+ init: { method: "GET", headers: jsonHeaders() }
443
+ };
412
444
  }
413
445
  case "candle_token_forensics": {
414
446
  const { chain, mint } = args;
415
447
  return {
416
- url: `${base2}/api/v1/markets/${chain}/${mint}/forensics`,
448
+ url: `${base2}/api/v1/markets/${encodeURIComponent(chain)}/${encodeURIComponent(mint)}/forensics`,
417
449
  init: { method: "GET", headers: jsonHeaders() }
418
450
  };
419
451
  }
@@ -434,7 +466,10 @@ function buildRequest(name, args, cfg) {
434
466
  }
435
467
  case "candle_get_agent_profile": {
436
468
  const { idOrWallet } = args;
437
- return { url: `${base2}/api/v1/users/${idOrWallet}/agent`, init: { method: "GET", headers: jsonHeaders() } };
469
+ return {
470
+ url: `${base2}/api/v1/users/${encodeURIComponent(idOrWallet)}/agent`,
471
+ init: { method: "GET", headers: jsonHeaders() }
472
+ };
438
473
  }
439
474
  case "candle_swap": {
440
475
  const apiKey = requireApiKey2(cfg);
@@ -533,7 +568,7 @@ var sweepShape = {
533
568
  mints: z.array(z.string()).optional().describe("Extra token mints/contracts to sweep besides the chain's base assets (own-wallet destinations only).")
534
569
  };
535
570
  function registerTools(server, env = process.env) {
536
- const cfg = resolveConfig();
571
+ const cfg = resolveConfig(env);
537
572
  const allowed = resolveToolAllowlist(env);
538
573
  const register = (name, ...rest) => {
539
574
  if (!allowed.has(name))
@@ -607,10 +642,25 @@ function registerTools(server, env = process.env) {
607
642
  }
608
643
 
609
644
  // src/version.ts
610
- var SERVER_VERSION = "0.4.0";
645
+ var SERVER_VERSION = "0.5.0";
646
+
647
+ // src/server.ts
648
+ function createCandleMcpServer(env = process.env) {
649
+ const server = new McpServer({ name: "candle-mcp", version: SERVER_VERSION });
650
+ registerTools(server, env);
651
+ return server;
652
+ }
653
+ async function runStdioServer(env = process.env, transport = new StdioServerTransport) {
654
+ const server = createCandleMcpServer(env);
655
+ await server.connect(transport);
656
+ await new Promise((resolve) => {
657
+ const sdkOnClose = transport.onclose;
658
+ transport.onclose = () => {
659
+ sdkOnClose?.();
660
+ resolve();
661
+ };
662
+ });
663
+ }
611
664
 
612
665
  // src/index.ts
613
- var server = new McpServer({ name: "candle-mcp", version: SERVER_VERSION });
614
- registerTools(server);
615
- var transport = new StdioServerTransport;
616
- await server.connect(transport);
666
+ await runStdioServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",