@dritan/mcp 0.9.0 → 0.10.1

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/.env.example CHANGED
@@ -1,4 +1,5 @@
1
1
  DRITAN_API_KEY=
2
+ DRITAN_MCP_AUTH_FILE=
2
3
  DRITAN_BASE_URL=https://us-east.dritan.dev
3
4
  DRITAN_CONTROL_BASE_URL=https://api.dritan.dev
4
5
  DRITAN_WS_BASE_URL=wss://us-east.dritan.dev
package/README.md CHANGED
@@ -41,6 +41,7 @@ npm run build && npm start
41
41
  - `system_check_prereqs`
42
42
  - `auth_status`
43
43
  - `auth_set_api_key`
44
+ - `auth_clear_api_key`
44
45
  - `wallet_create_local`
45
46
  - `wallet_get_address`
46
47
  - `wallet_get_balance`
@@ -82,12 +83,13 @@ npm run build && npm start
82
83
  - Wallets default to the current working directory (`process.cwd()`).
83
84
  - Private keys never leave local files; only public address/signature are returned.
84
85
  - `swap_sign_and_broadcast` signs locally, then broadcasts via Dritan.
85
- - `auth_set_api_key` activates a key for the running MCP process without restart.
86
+ - `auth_set_api_key` activates a key for the running MCP process without restart and persists it to a local auth store.
86
87
  - `auth_set_api_key` and successful `x402_create_api_key` responses include a capability summary so agents can immediately guide users to next actions.
87
- - Agent onboarding without `DRITAN_API_KEY` should present two options:
88
- - Option 1: paid x402 flow (`wallet_create_local` in current directory -> share wallet + backup file path -> user chooses SOL amount and funds agent wallet -> if no key exists use `x402_create_api_key_quote` -> `wallet_transfer_sol` -> `x402_create_api_key`).
89
- - Option 2: user gets a free key at `https://dritan.dev`.
90
- - `x402_create_api_key` auto-activates returned keys for the current MCP session.
88
+ - Agent onboarding without an active API key should use x402-first flow (`wallet_create_local` in current directory -> share wallet + backup file path -> user chooses SOL amount and funds agent wallet -> if no key exists use `x402_create_api_key_quote` -> `wallet_transfer_sol` -> `x402_create_api_key`).
89
+ - Free key at `https://dritan.dev` is fallback only if the user declines funding or x402 cannot proceed.
90
+ - `x402_create_api_key` auto-activates returned keys and persists them locally for restart recovery.
91
+ - Default auth store path is `.dritan-mcp/auth.json` under current working directory; override with `DRITAN_MCP_AUTH_FILE`.
92
+ - Use `auth_clear_api_key` to remove in-memory + persisted key state (and optionally clear process env key).
91
93
  - `token_get_ohlcv_chart` returns a shareable chart URL plus a ready-to-send markdown image snippet.
92
94
  - `token_get_ohlcv_chart` supports `chartType: "line-volume" | "candlestick"` (default is `candlestick`).
93
95
  - `ths_get_top_wallets` returns a paginated leaderboard of THS-ranked wallets (`page`, `limit`) for smart-wallet discovery workflows.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, readFileSync, existsSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, existsSync, writeFileSync, rmSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -9,6 +9,7 @@ import { DritanClient, MeteoraThsClient, } from "dritan-sdk";
9
9
  import { Connection, Keypair, PublicKey, SystemProgram, Transaction, VersionedTransaction, clusterApiUrl, } from "@solana/web3.js";
10
10
  import { z } from "zod";
11
11
  const DEFAULT_WALLET_DIR = process.cwd();
12
+ const DEFAULT_API_KEY_STORE_PATH = resolve(process.cwd(), ".dritan-mcp", "auth.json");
12
13
  const LAMPORTS_PER_SOL = 1_000_000_000;
13
14
  function normalizeApiKey(value) {
14
15
  const trimmed = value?.trim();
@@ -21,8 +22,57 @@ function apiKeyPreview(apiKey) {
21
22
  return `${apiKey.slice(0, 4)}...`;
22
23
  return `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`;
23
24
  }
24
- let runtimeApiKey = normalizeApiKey(process.env.DRITAN_API_KEY);
25
- let runtimeApiKeySource = runtimeApiKey ? "env" : "none";
25
+ function getApiKeyStorePath() {
26
+ const configured = process.env.DRITAN_MCP_AUTH_FILE?.trim();
27
+ return configured ? resolve(configured) : DEFAULT_API_KEY_STORE_PATH;
28
+ }
29
+ const API_KEY_STORE_PATH = getApiKeyStorePath();
30
+ function loadPersistedApiKey() {
31
+ if (!existsSync(API_KEY_STORE_PATH))
32
+ return null;
33
+ try {
34
+ const raw = readFileSync(API_KEY_STORE_PATH, "utf8");
35
+ const parsed = JSON.parse(raw);
36
+ return normalizeApiKey(parsed.apiKey);
37
+ }
38
+ catch (error) {
39
+ console.warn(`[dritan-mcp] Failed to read persisted API key from ${API_KEY_STORE_PATH}: ${error instanceof Error ? error.message : String(error)}`);
40
+ return null;
41
+ }
42
+ }
43
+ function persistApiKey(apiKey, source) {
44
+ const payload = {
45
+ apiKey,
46
+ source,
47
+ updatedAt: new Date().toISOString(),
48
+ };
49
+ try {
50
+ mkdirSync(dirname(API_KEY_STORE_PATH), { recursive: true, mode: 0o700 });
51
+ writeFileSync(API_KEY_STORE_PATH, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
52
+ }
53
+ catch (error) {
54
+ console.warn(`[dritan-mcp] Failed to persist API key to ${API_KEY_STORE_PATH}: ${error instanceof Error ? error.message : String(error)}`);
55
+ }
56
+ }
57
+ function clearPersistedApiKey() {
58
+ if (!existsSync(API_KEY_STORE_PATH))
59
+ return false;
60
+ try {
61
+ rmSync(API_KEY_STORE_PATH, { force: true });
62
+ return true;
63
+ }
64
+ catch (error) {
65
+ console.warn(`[dritan-mcp] Failed to remove persisted API key file ${API_KEY_STORE_PATH}: ${error instanceof Error ? error.message : String(error)}`);
66
+ return false;
67
+ }
68
+ }
69
+ const persistedStartupKey = loadPersistedApiKey();
70
+ const envStartupKey = normalizeApiKey(process.env.DRITAN_API_KEY);
71
+ let runtimeApiKey = persistedStartupKey ?? envStartupKey;
72
+ let runtimeApiKeySource = persistedStartupKey ? "persisted" : envStartupKey ? "env" : "none";
73
+ if (runtimeApiKey) {
74
+ process.env.DRITAN_API_KEY = runtimeApiKey;
75
+ }
26
76
  function setRuntimeApiKey(apiKey, source) {
27
77
  const normalized = normalizeApiKey(apiKey);
28
78
  if (!normalized) {
@@ -30,12 +80,20 @@ function setRuntimeApiKey(apiKey, source) {
30
80
  }
31
81
  runtimeApiKey = normalized;
32
82
  runtimeApiKeySource = source;
83
+ persistApiKey(normalized, source);
33
84
  process.env.DRITAN_API_KEY = normalized;
34
85
  return normalized;
35
86
  }
36
87
  function getActiveApiKey() {
37
88
  if (runtimeApiKey)
38
89
  return runtimeApiKey;
90
+ const fromPersisted = loadPersistedApiKey();
91
+ if (fromPersisted) {
92
+ runtimeApiKey = fromPersisted;
93
+ runtimeApiKeySource = "persisted";
94
+ process.env.DRITAN_API_KEY = fromPersisted;
95
+ return fromPersisted;
96
+ }
39
97
  const fromEnv = normalizeApiKey(process.env.DRITAN_API_KEY);
40
98
  if (fromEnv) {
41
99
  runtimeApiKey = fromEnv;
@@ -73,7 +131,7 @@ const server = new Server({
73
131
  " b) Ask user how much SOL they want to allocate and provide both wallet address and backup file path.",
74
132
  " c) If no active key exists, create quote -> pay from agent wallet -> claim key.",
75
133
  "2) Fallback only: if user explicitly declines funding or x402 cannot proceed, then offer free key flow at https://dritan.dev.",
76
- "After key is obtained, set it with auth_set_api_key (no restart needed), or restart with DRITAN_API_KEY configured.",
134
+ `After key is obtained, set it with auth_set_api_key (no restart needed); MCP persists it at ${API_KEY_STORE_PATH} for restart recovery.`,
77
135
  "Suggested setup command:",
78
136
  " claude mcp add dritan-mcp -e DRITAN_API_KEY=<your-key> -- npx @dritan/mcp@latest",
79
137
  ].join("\n"),
@@ -84,10 +142,11 @@ function getControlBaseUrl() {
84
142
  function missingApiKeyError() {
85
143
  return new Error([
86
144
  "Missing DRITAN_API_KEY in environment.",
145
+ `No active key found in persisted auth store (${API_KEY_STORE_PATH}).`,
87
146
  "Onboarding is x402-first by default: use wallet and x402 tools before asking user for an API key.",
88
147
  "Paid flow order: create wallet in current directory -> tell user funding amount + backup file path -> if no key exists then create/claim x402 key.",
89
148
  "Fallback only if user declines funding or x402 is not possible: user can create a free key at https://dritan.dev and set DRITAN_API_KEY.",
90
- "You can activate a key immediately with auth_set_api_key without restarting MCP.",
149
+ "You can activate a key immediately with auth_set_api_key without restarting MCP; key is persisted locally for restart recovery.",
91
150
  ].join(" "));
92
151
  }
93
152
  const postAuthCapabilities = [
@@ -270,7 +329,7 @@ function formatChartLabel(ts) {
270
329
  return String(ts);
271
330
  return date.toISOString().replace("T", " ").slice(0, 16);
272
331
  }
273
- function buildLineVolumeOhlcvChartUrl(mint, timeframe, bars, width, height) {
332
+ function buildLineVolumeOhlcvChartConfig(mint, timeframe, bars) {
274
333
  const labels = bars.map((bar) => formatChartLabel(bar.time));
275
334
  const closeSeries = bars.map((bar) => Number(bar.close.toFixed(12)));
276
335
  const volumeSeries = bars.map((bar) => Number(bar.volume.toFixed(12)));
@@ -314,12 +373,9 @@ function buildLineVolumeOhlcvChartUrl(mint, timeframe, bars, width, height) {
314
373
  },
315
374
  },
316
375
  };
317
- const encoded = encodeURIComponent(JSON.stringify(config));
318
- // QuickChart defaults to Chart.js v2. Our config uses v3+/v4 scale syntax (`options.scales.{id}`),
319
- // so pinning `v=4` prevents runtime render errors like "Cannot read properties of undefined (reading 'options')".
320
- return `https://quickchart.io/chart?w=${width}&h=${height}&f=png&v=4&c=${encoded}`;
376
+ return config;
321
377
  }
322
- function buildCandlestickOhlcvChartUrl(mint, timeframe, bars, width, height) {
378
+ function buildCandlestickOhlcvChartConfig(mint, timeframe, bars) {
323
379
  const labels = bars.map((bar) => formatChartLabel(bar.time));
324
380
  const candles = bars.map((bar, index) => ({
325
381
  x: labels[index],
@@ -382,15 +438,47 @@ function buildCandlestickOhlcvChartUrl(mint, timeframe, bars, width, height) {
382
438
  },
383
439
  },
384
440
  };
441
+ return config;
442
+ }
443
+ function buildQuickChartDirectUrl(config, width, height) {
385
444
  const encoded = encodeURIComponent(JSON.stringify(config));
386
- // Pin Chart.js v4 for stable scale behavior and financial chart rendering.
445
+ // QuickChart defaults to Chart.js v2. Our config uses v3+/v4 scale syntax (`options.scales.{id}`),
446
+ // so pinning `v=4` prevents runtime render errors like "Cannot read properties of undefined (reading 'options')".
387
447
  return `https://quickchart.io/chart?w=${width}&h=${height}&f=png&v=4&c=${encoded}`;
388
448
  }
389
- function buildOhlcvChartUrl(chartType, mint, timeframe, bars, width, height) {
449
+ async function buildQuickChartShortUrl(config, width, height) {
450
+ const controller = new AbortController();
451
+ const timeout = setTimeout(() => controller.abort(), 5_000);
452
+ try {
453
+ const response = await fetch("https://quickchart.io/chart/create", {
454
+ method: "POST",
455
+ headers: { "content-type": "application/json" },
456
+ body: JSON.stringify({
457
+ chart: config,
458
+ width,
459
+ height,
460
+ format: "png",
461
+ version: "4",
462
+ }),
463
+ signal: controller.signal,
464
+ });
465
+ if (!response.ok)
466
+ return null;
467
+ const payload = (await response.json());
468
+ return typeof payload.url === "string" && payload.url.length > 0 ? payload.url : null;
469
+ }
470
+ catch {
471
+ return null;
472
+ }
473
+ finally {
474
+ clearTimeout(timeout);
475
+ }
476
+ }
477
+ function buildOhlcvChartConfig(chartType, mint, timeframe, bars) {
390
478
  if (chartType === "candlestick") {
391
- return buildCandlestickOhlcvChartUrl(mint, timeframe, bars, width, height);
479
+ return buildCandlestickOhlcvChartConfig(mint, timeframe, bars);
392
480
  }
393
- return buildLineVolumeOhlcvChartUrl(mint, timeframe, bars, width, height);
481
+ return buildLineVolumeOhlcvChartConfig(mint, timeframe, bars);
394
482
  }
395
483
  function getPlatformInstallHint(binary) {
396
484
  switch (process.platform) {
@@ -504,6 +592,9 @@ const walletCreateSchema = z.object({
504
592
  const authSetApiKeySchema = z.object({
505
593
  apiKey: z.string().min(8),
506
594
  });
595
+ const authClearApiKeySchema = z.object({
596
+ clearEnv: z.boolean().optional(),
597
+ });
507
598
  const walletPathSchema = z.object({
508
599
  walletPath: z.string().min(1),
509
600
  });
@@ -637,7 +728,7 @@ const tools = [
637
728
  },
638
729
  {
639
730
  name: "auth_set_api_key",
640
- description: "Set the active Dritan API key for this running MCP process without restart (runtime session scope).",
731
+ description: "Set the active Dritan API key for this running MCP process and persist it locally for restart recovery.",
641
732
  inputSchema: {
642
733
  type: "object",
643
734
  required: ["apiKey"],
@@ -646,6 +737,16 @@ const tools = [
646
737
  },
647
738
  },
648
739
  },
740
+ {
741
+ name: "auth_clear_api_key",
742
+ description: "Clear the active in-memory API key and delete the persisted auth file. Optionally clear DRITAN_API_KEY in this process env.",
743
+ inputSchema: {
744
+ type: "object",
745
+ properties: {
746
+ clearEnv: { type: "boolean", description: "Also remove DRITAN_API_KEY from this process environment." },
747
+ },
748
+ },
749
+ },
649
750
  {
650
751
  name: "wallet_create_local",
651
752
  description: "Create a local Solana wallet using solana-keygen and return path + public address.",
@@ -1104,9 +1205,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1104
1205
  const solanaCli = checkSolanaCli();
1105
1206
  const activeApiKey = getActiveApiKey();
1106
1207
  const apiKeySet = !!activeApiKey;
1208
+ const persistedApiKeyPresent = existsSync(API_KEY_STORE_PATH);
1107
1209
  return ok({
1108
1210
  ready: solanaCli.ok && apiKeySet,
1109
1211
  readyForX402Onboarding: solanaCli.ok,
1212
+ apiKeyStorePath: API_KEY_STORE_PATH,
1213
+ persistedApiKeyPresent,
1110
1214
  checks: [
1111
1215
  solanaCli,
1112
1216
  {
@@ -1116,7 +1220,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1116
1220
  preview: apiKeyPreview(activeApiKey),
1117
1221
  hint: apiKeySet
1118
1222
  ? "API key is configured."
1119
- : "Missing DRITAN_API_KEY. Start x402 flow first (wallet_create_local -> fund wallet -> quote/claim). Use free key only as fallback.",
1223
+ : "Missing active API key. Start x402 flow first (wallet_create_local -> fund wallet -> quote/claim). Use free key only as fallback.",
1120
1224
  },
1121
1225
  ],
1122
1226
  nextAction: !apiKeySet
@@ -1133,6 +1237,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1133
1237
  source: runtimeApiKeySource,
1134
1238
  preview: apiKeyPreview(activeApiKey),
1135
1239
  controlBaseUrl: getControlBaseUrl(),
1240
+ apiKeyStorePath: API_KEY_STORE_PATH,
1241
+ persistedApiKeyPresent: existsSync(API_KEY_STORE_PATH),
1136
1242
  onboardingOptions: [
1137
1243
  "Default (x402-first): create wallet in current directory -> share wallet + backup file path -> user funds wallet -> if no key exists, quote/transfer/claim key.",
1138
1244
  "Fallback only: if user declines funding or x402 cannot proceed, user can create key at https://dritan.dev and set it with auth_set_api_key.",
@@ -1145,12 +1251,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1145
1251
  const activated = setRuntimeApiKey(input.apiKey, "runtime");
1146
1252
  return ok({
1147
1253
  ok: true,
1148
- message: "API key activated for this MCP session without restart.",
1254
+ message: "API key activated and persisted for this MCP session without restart.",
1149
1255
  source: runtimeApiKeySource,
1150
1256
  preview: apiKeyPreview(activated),
1257
+ apiKeyStorePath: API_KEY_STORE_PATH,
1151
1258
  ...buildPostAuthGuidance(),
1152
1259
  });
1153
1260
  }
1261
+ case "auth_clear_api_key": {
1262
+ const input = authClearApiKeySchema.parse(args);
1263
+ runtimeApiKey = null;
1264
+ runtimeApiKeySource = "none";
1265
+ const persistedApiKeyRemoved = clearPersistedApiKey();
1266
+ const envCleared = !!input.clearEnv;
1267
+ if (envCleared) {
1268
+ delete process.env.DRITAN_API_KEY;
1269
+ }
1270
+ return ok({
1271
+ ok: true,
1272
+ source: runtimeApiKeySource,
1273
+ persistedApiKeyRemoved,
1274
+ envCleared,
1275
+ apiKeyStorePath: API_KEY_STORE_PATH,
1276
+ message: "Active key cleared from memory and persisted store. If clearEnv=false and env still has DRITAN_API_KEY, restarting may load that env key.",
1277
+ });
1278
+ }
1154
1279
  case "dritan_health": {
1155
1280
  return ok(await checkDritanHealth());
1156
1281
  }
@@ -1268,7 +1393,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1268
1393
  activated: true,
1269
1394
  source: runtimeApiKeySource,
1270
1395
  preview: apiKeyPreview(activated),
1271
- message: "x402-created API key is active for this MCP session (no restart needed).",
1396
+ apiKeyStorePath: API_KEY_STORE_PATH,
1397
+ message: "x402-created API key is active for this MCP session and persisted locally for restart recovery (no restart needed).",
1272
1398
  };
1273
1399
  Object.assign(payload, buildPostAuthGuidance());
1274
1400
  }
@@ -1343,12 +1469,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1343
1469
  if (trimmedBars.length === 0) {
1344
1470
  throw new Error(`No OHLCV data available for ${input.mint} (${input.timeframe})`);
1345
1471
  }
1346
- const chartUrl = buildOhlcvChartUrl(input.chartType, input.mint, input.timeframe, trimmedBars, input.width, input.height);
1472
+ const chartConfig = buildOhlcvChartConfig(input.chartType, input.mint, input.timeframe, trimmedBars);
1473
+ const shortChartUrl = await buildQuickChartShortUrl(chartConfig, input.width, input.height);
1474
+ const chartUrl = shortChartUrl ?? buildQuickChartDirectUrl(chartConfig, input.width, input.height);
1347
1475
  return ok({
1348
1476
  mint: input.mint,
1349
1477
  timeframe: input.timeframe,
1350
1478
  chartType: input.chartType,
1351
1479
  points: trimmedBars.length,
1480
+ chartUrlType: shortChartUrl ? "short" : "direct",
1352
1481
  chartUrl,
1353
1482
  markdown: `![${input.mint} ${input.timeframe} chart](${chartUrl})`,
1354
1483
  lastBar: trimmedBars[trimmedBars.length - 1],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dritan/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "MCP server for Dritan SDK market data and local Solana wallet swap execution",