cryptoquant-mcp 0.2.0 → 1.0.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 (55) hide show
  1. package/README.md +84 -118
  2. package/dist/api-key.js +76 -0
  3. package/dist/api-key.js.map +1 -0
  4. package/dist/config.js +24 -18
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.js +33 -40
  7. package/dist/index.js.map +1 -1
  8. package/dist/proxy.js +133 -0
  9. package/dist/proxy.js.map +1 -0
  10. package/dist/utils.js +6 -64
  11. package/dist/utils.js.map +1 -1
  12. package/package.json +16 -11
  13. package/dist/auth/storage.d.ts +0 -11
  14. package/dist/auth/storage.d.ts.map +0 -1
  15. package/dist/auth/storage.js +0 -53
  16. package/dist/auth/storage.js.map +0 -1
  17. package/dist/cache/storage.d.ts +0 -47
  18. package/dist/cache/storage.d.ts.map +0 -1
  19. package/dist/cache/storage.js +0 -140
  20. package/dist/cache/storage.js.map +0 -1
  21. package/dist/cache/summary.d.ts +0 -16
  22. package/dist/cache/summary.d.ts.map +0 -1
  23. package/dist/cache/summary.js +0 -85
  24. package/dist/cache/summary.js.map +0 -1
  25. package/dist/cache/types.d.ts +0 -76
  26. package/dist/cache/types.d.ts.map +0 -1
  27. package/dist/cache/types.js +0 -6
  28. package/dist/cache/types.js.map +0 -1
  29. package/dist/config.d.ts +0 -18
  30. package/dist/config.d.ts.map +0 -1
  31. package/dist/data/metrics.toon +0 -293
  32. package/dist/discovery.d.ts +0 -70
  33. package/dist/discovery.d.ts.map +0 -1
  34. package/dist/discovery.js +0 -171
  35. package/dist/discovery.js.map +0 -1
  36. package/dist/index.d.ts +0 -18
  37. package/dist/index.d.ts.map +0 -1
  38. package/dist/permissions.d.ts +0 -36
  39. package/dist/permissions.d.ts.map +0 -1
  40. package/dist/permissions.js +0 -159
  41. package/dist/permissions.js.map +0 -1
  42. package/dist/plan-limits.d.ts +0 -71
  43. package/dist/plan-limits.d.ts.map +0 -1
  44. package/dist/plan-limits.js +0 -416
  45. package/dist/plan-limits.js.map +0 -1
  46. package/dist/tools/auth.d.ts +0 -3
  47. package/dist/tools/auth.d.ts.map +0 -1
  48. package/dist/tools/auth.js +0 -196
  49. package/dist/tools/auth.js.map +0 -1
  50. package/dist/tools/core.d.ts +0 -3
  51. package/dist/tools/core.d.ts.map +0 -1
  52. package/dist/tools/core.js +0 -546
  53. package/dist/tools/core.js.map +0 -1
  54. package/dist/utils.d.ts +0 -45
  55. package/dist/utils.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,107 +1,74 @@
1
- # CryptoQuant MCP Server
1
+ # CryptoQuant MCP
2
2
 
3
- <p align="center">
4
- <strong>On-Chain Analytics for Claude and AI Coding Agents</strong>
5
- </p>
3
+ [![npm version](https://img.shields.io/npm/v/cryptoquant-mcp.svg)](https://www.npmjs.com/package/cryptoquant-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
5
 
7
- <p align="center">
8
- <a href="#installation">Installation</a> •
9
- <a href="#mcp-tools">MCP Tools</a> •
10
- <a href="#authentication">Authentication</a>
11
- </p>
6
+ CryptoQuant on-chain, market, and derivatives data, plus CryptoQuant Research and QuickTake insights, for AI agents through the [Model Context Protocol](https://modelcontextprotocol.io).
12
7
 
13
- ---
8
+ This repository contains:
14
9
 
15
- ## What is this?
10
+ - **Remote server setup** for `https://mcp.cryptoquant.com/mcp` (recommended; you sign in with OAuth)
11
+ - **An Agent Plugins / Cursor plugin** (`plugin.json`, `mcp.json`, `skills/`) that connects to the remote server and teaches the agent which metric answers which question
12
+ - **A local stdio proxy** (`npx cryptoquant-mcp`) for clients that cannot connect to remote MCP servers. It forwards every message to the remote server unchanged.
16
13
 
17
- CryptoQuant MCP Server brings on-chain analytics directly into your AI assistant:
14
+ <!-- TODO: demo GIF (to be recorded by the CryptoQuant team) -->
18
15
 
19
- - **Natural language queries**: Ask in any language - "비트코인 가격 전망?" or "Is BTC overvalued?"
20
- - **Real-time metrics**: MVRV, SOPR, Exchange Flows, Funding Rates
21
- - **Market insights**: AI-powered interpretation of on-chain data
22
- - **Whale tracking**: Monitor large holder movements
16
+ ## Remote server (recommended)
23
17
 
24
- ---
18
+ Server URL: `https://mcp.cryptoquant.com/mcp` (Streamable HTTP, OAuth)
25
19
 
26
- ## Installation
20
+ No API key is needed in your config. The first time you connect, your client opens a CryptoQuant sign-in page. More setup details: [CryptoQuant user guide](https://userguide.cryptoquant.com/api/mcp-server-beta/remote-mcp-recommended).
27
21
 
28
- ### Quick Start (Claude Desktop, Cursor, etc.)
22
+ ### Claude (claude.ai and Claude Desktop)
29
23
 
30
- **Step 1**: Add to your MCP config file:
24
+ Open **Settings → Connectors → Add custom connector** and paste the server URL.
31
25
 
32
- | App | Config File |
33
- |-----|-------------|
34
- | **Claude Desktop (Mac)** | `~/Library/Application Support/Claude/claude_desktop_config.json` |
35
- | **Claude Desktop (Windows)** | `%APPDATA%\Claude\claude_desktop_config.json` |
36
- | **Claude Code** | `~/.claude/mcp.json` or project `.mcp.json` |
37
- | **Cursor** | Project `.cursor/mcp.json` |
26
+ ### ChatGPT
27
+
28
+ Add a custom connector with the server URL (developer mode). Availability depends on your ChatGPT plan.
29
+
30
+ ### Cursor
31
+
32
+ Install the **CryptoQuant** plugin from the Cursor marketplace (once listed). It adds the remote server and the `cryptoquant-onchain` skill.
33
+
34
+ Or add the server yourself in `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
38
35
 
39
36
  ```json
40
37
  {
41
38
  "mcpServers": {
42
39
  "cryptoquant": {
43
- "command": "npx",
44
- "args": ["-y", "cryptoquant-mcp"]
40
+ "url": "https://mcp.cryptoquant.com/mcp"
45
41
  }
46
42
  }
47
43
  }
48
44
  ```
49
45
 
50
- **Step 2**: Restart your app
46
+ ### VS Code
51
47
 
52
- **Step 3**: Call `initialize()` to verify connection. Done!
48
+ `.vscode/mcp.json`:
53
49
 
54
- ### Local Development
50
+ ```json
51
+ {
52
+ "servers": {
53
+ "cryptoquant": {
54
+ "type": "http",
55
+ "url": "https://mcp.cryptoquant.com/mcp"
56
+ }
57
+ }
58
+ }
59
+ ```
55
60
 
56
- For contributors:
61
+ ### Claude Code
57
62
 
58
63
  ```bash
59
- git clone https://github.com/CryptoQuantOfficial/cryptoquant-mcp.git
60
- cd cryptoquant-mcp
61
- npm install && npm run build
64
+ claude mcp add --transport http cryptoquant https://mcp.cryptoquant.com/mcp
62
65
  ```
63
66
 
64
- ---
65
-
66
- ## MCP Tools
67
-
68
- The MCP server provides these tools for API access:
69
-
70
- | Tool | Description |
71
- |------|-------------|
72
- | `initialize` | Start session with API key, returns plan info |
73
- | `discover_endpoints` | Browse 245+ available endpoints |
74
- | `get_endpoint_info` | Get endpoint parameter details |
75
- | `query_data` | Query raw API data |
76
- | `describe_metric` | Get metric descriptions and thresholds |
77
- | `list_assets` | List supported assets |
78
- | `reset_session` | Clear session (logout) |
79
-
80
- ### Supported Assets
81
-
82
- BTC, ETH, ALT, Stablecoin, ERC20, TRX, XRP
83
-
84
- ---
67
+ Then run `/mcp` in Claude Code to sign in.
85
68
 
86
- ## Natural Language Queries
69
+ ## Local install (npx proxy)
87
70
 
88
- Ask questions in any language - Claude will route to the right metrics:
89
-
90
- | Query | Intent | Metric |
91
- |-------|--------|--------|
92
- | "비트코인 가격 전망이 어때?" | VALUATION | MVRV |
93
- | "Is BTC overvalued?" | VALUATION | MVRV |
94
- | "고래들 움직임 보여줘" | WHALE_ACTIVITY | whale-ratio |
95
- | "What's the funding rate?" | LEVERAGE | funding-rates |
96
- | "익절/손절 상황?" | PROFIT_BEHAVIOR | SOPR |
97
-
98
- ---
99
-
100
- ## Authentication
101
-
102
- ### Option A. Environment Variable (Recommended)
103
-
104
- Add your API key to the MCP config:
71
+ For clients that only support local (stdio) servers. Requires Node.js 18+ and a [CryptoQuant API key](https://cryptoquant.com/account/api).
105
72
 
106
73
  ```json
107
74
  {
@@ -117,67 +84,66 @@ Add your API key to the MCP config:
117
84
  }
118
85
  ```
119
86
 
120
- ### Option B. Direct Parameter
87
+ | App | Config file |
88
+ | --- | --- |
89
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
90
+ | Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
91
+ | Cursor | `.cursor/mcp.json` or `~/.cursor/mcp.json` |
92
+ | Claude Code | project `.mcp.json` |
121
93
 
122
- Call `initialize()` with your API key:
94
+ | Variable | Required | Description |
95
+ | --- | --- | --- |
96
+ | `CRYPTOQUANT_API_KEY` | Yes | Your API key, sent to the remote server as a bearer token. If it is not set, the key saved by 0.x in `~/.cryptoquant/credentials` is used. |
97
+ | `CRYPTOQUANT_MCP_URL` | No | Overrides the server URL (default `https://mcp.cryptoquant.com/mcp`). Must be `https`; plain `http` is accepted only for loopback addresses (`localhost`, `127.0.0.1`, `[::1]`). |
123
98
 
124
- ```
125
- initialize(api_key="your-api-key")
126
- ```
127
-
128
- The key will be saved to `~/.cryptoquant/credentials` for future sessions.
129
-
130
- **Get your API key**: [https://cryptoquant.com/settings/api](https://cryptoquant.com/settings/api)
99
+ Without a key, the proxy prints setup instructions to stderr and exits.
131
100
 
132
- ### Managing Credentials
101
+ ## Tools
133
102
 
134
- ```bash
135
- # Switch accounts
136
- reset_session(clear_stored=true)
137
- initialize(api_key="new-api-key")
138
- ```
103
+ The tools come from the remote server. This list is for reference and may change.
139
104
 
140
- ---
105
+ | Tool | Description |
106
+ | --- | --- |
107
+ | `discover_endpoints` | Find data endpoints by asset and category |
108
+ | `get_endpoint_info` | Parameters of an endpoint (window, exchange, token, ...) |
109
+ | `query_data` | Query on-chain, market, and derivatives data |
110
+ | `describe_metric` | Definition, thresholds, and interpretation of a metric |
111
+ | `list_assets` | Supported assets |
112
+ | `recent_research` | Latest CryptoQuant Research articles |
113
+ | `recent_quicktake` | Latest QuickTake articles from CryptoQuant Verified Authors |
114
+ | `query_research` | Search Research articles by topic |
115
+ | `query_quicktake` | Search QuickTake articles by topic |
141
116
 
142
- ## Requirements
117
+ ## Assets, dates, and rate limits
143
118
 
144
- | Component | Requirement |
145
- |-----------|-------------|
146
- | Node.js | v18+ |
147
- | API Access | [CryptoQuant API key](https://cryptoquant.com/settings/api) |
119
+ - **Assets:** BTC, ETH, XRP, TRX, stablecoins, ERC-20 tokens, and other altcoins. `list_assets` returns the current list.
120
+ - **Dates:** `yyyyMMdd` for `window=day` (e.g. `20260115`), `yyyyMMddTHHmmss` for other windows (e.g. `20260115T000000`).
121
+ - **Rate limits and history:** depend on your CryptoQuant plan. The server tells the agent your plan's limits when it connects. See [plans](https://cryptoquant.com/pricing).
148
122
 
149
- ---
123
+ ## Migrating from 0.x
150
124
 
151
- ## License
125
+ 1.0 turns the npm package into a proxy to the remote server, so the local and remote servers now offer the same tools.
152
126
 
153
- MIT License - see [LICENSE](LICENSE)
127
+ - The `initialize` and `reset_session` tools are removed. Set `CRYPTOQUANT_API_KEY` in your MCP config instead. A key you saved in 0.x with `initialize(api_key=...)` keeps working; it is read from `~/.cryptoquant/credentials`.
128
+ - QuickTake and Research tools are now available locally.
129
+ - The package no longer exports a JavaScript library API.
130
+ - `CRYPTOQUANT_API_URL` (the 0.x API base URL override) is no longer used; use `CRYPTOQUANT_MCP_URL` to point at another MCP server.
131
+ - `npx -y cryptoquant-mcp` always fetches the latest version, so existing configs switch to 1.0 on the next start. Prompts or workflows that call `initialize` need to be updated.
154
132
 
155
- ---
133
+ See [CHANGELOG.md](CHANGELOG.md).
156
134
 
157
135
  ## Development
158
136
 
159
- ### Setup
160
-
161
- After cloning the repository, install dependencies to set up git hooks:
162
-
163
137
  ```bash
138
+ git clone https://github.com/CryptoQuantOfficial/cryptoquant-mcp.git
139
+ cd cryptoquant-mcp
164
140
  npm install
141
+ npm test
142
+ npm run build
165
143
  ```
166
144
 
167
- This automatically configures [husky](https://typicode.github.io/husky/) for pre-commit hooks.
168
-
169
- ### Pre-commit Hooks
170
-
171
- When committing changes to `src/`, ESLint runs automatically:
145
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
172
146
 
173
- - Lint check runs only when files in `src/` are staged
174
- - Commit is blocked if lint errors are found
175
- - Fix errors before committing: `npm run lint`
176
-
177
- ---
147
+ ## License
178
148
 
179
- <p align="center">
180
- <a href="https://cryptoquant.com">CryptoQuant</a> •
181
- <a href="https://docs.cryptoquant.com">Docs</a> •
182
- <a href="https://github.com/CryptoQuantOfficial/cryptoquant-mcp/issues">Issues</a>
183
- </p>
149
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { DEFAULT_MCP_URL } from "./config.js";
5
+ export const API_KEY_URL = "https://cryptoquant.com/account/api";
6
+ export function credentialsPath(home = homedir()) {
7
+ return join(home, ".cryptoquant", "credentials");
8
+ }
9
+ function clean(value) {
10
+ if (typeof value !== "string")
11
+ return null;
12
+ const trimmed = value.trim();
13
+ return trimmed.length > 0 ? trimmed : null;
14
+ }
15
+ /** Reads the key that 0.x saved via initialize(api_key=...). Never writes. */
16
+ function readStoredKey(path) {
17
+ try {
18
+ const data = JSON.parse(readFileSync(path, "utf-8"));
19
+ if (data === null || typeof data !== "object" || Array.isArray(data))
20
+ return null;
21
+ return clean(data.api_key);
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function resolveApiKey({ env = process.env, home = homedir(), } = {}) {
28
+ const fromEnv = clean(env.CRYPTOQUANT_API_KEY);
29
+ if (fromEnv)
30
+ return { key: fromEnv, source: "env" };
31
+ const fromFile = readStoredKey(credentialsPath(home));
32
+ if (fromFile)
33
+ return { key: fromFile, source: "credentials" };
34
+ return null;
35
+ }
36
+ /** Printable ASCII only: anything else cannot be sent in an Authorization header. */
37
+ export function isValidApiKey(key) {
38
+ return /^[\x21-\x7E]+$/.test(key);
39
+ }
40
+ export function keySourceLabel(source) {
41
+ return source === "env" ? "CRYPTOQUANT_API_KEY" : "~/.cryptoquant/credentials";
42
+ }
43
+ /** Never includes the key or any part of it. */
44
+ export function invalidKeyMessage(source) {
45
+ return [
46
+ `[cryptoquant-mcp] The API key from ${keySourceLabel(source)} contains invalid characters`,
47
+ "(only printable ASCII without spaces is allowed). Check for stray quotes, line breaks, or spaces.",
48
+ "",
49
+ `Get an API key: ${API_KEY_URL}`,
50
+ "",
51
+ ].join("\n");
52
+ }
53
+ export function missingKeyMessage() {
54
+ return [
55
+ "[cryptoquant-mcp] No CryptoQuant API key found.",
56
+ "",
57
+ "Set CRYPTOQUANT_API_KEY in your MCP client config:",
58
+ "",
59
+ " {",
60
+ ' "mcpServers": {',
61
+ ' "cryptoquant": {',
62
+ ' "command": "npx",',
63
+ ' "args": ["-y", "cryptoquant-mcp"],',
64
+ ' "env": { "CRYPTOQUANT_API_KEY": "<your-api-key>" }',
65
+ " }",
66
+ " }",
67
+ " }",
68
+ "",
69
+ `Get an API key: ${API_KEY_URL}`,
70
+ "",
71
+ "If your client supports remote MCP servers, connect to the remote server instead",
72
+ `and sign in with OAuth (no API key in your config): ${DEFAULT_MCP_URL}`,
73
+ "",
74
+ ].join("\n");
75
+ }
76
+ //# sourceMappingURL=api-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key.js","sourceRoot":"","sources":["../src/api-key.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -1,23 +1,29 @@
1
- /**
2
- * Centralized configuration for CryptoQuant MCP Server
3
- *
4
- * API URL Priority:
5
- * 1. CRYPTOQUANT_API_URL environment variable (optional override)
6
- * 2. Default production URL
7
- */
8
- const DEFAULT_API_URL = "https://api.cryptoquant.com/v1";
9
- /**
10
- * Get the configured API URL.
11
- * Returns the base URL with /v1 suffix (e.g., "https://api.cryptoquant.com/v1")
12
- */
13
- export function getApiUrl() {
14
- return process.env.CRYPTOQUANT_API_URL || DEFAULT_API_URL;
1
+ export const DEFAULT_MCP_URL = "https://mcp.cryptoquant.com/mcp";
2
+ export class ConfigError extends Error {
3
+ name = "ConfigError";
4
+ }
5
+ function isLoopback(hostname) {
6
+ return hostname === "localhost" || hostname === "[::1]" || /^127(\.\d{1,3}){3}$/.test(hostname);
15
7
  }
16
8
  /**
17
- * Get the API base URL without version suffix.
18
- * Returns the base URL (e.g., "https://api.cryptoquant.com")
9
+ * CRYPTOQUANT_MCP_URL overrides the server (stage testing). The API key travels
10
+ * in a header, so plain http is allowed only when it cannot leave the machine.
19
11
  */
20
- export function getApiBaseUrl() {
21
- return getApiUrl().replace(/\/v1$/, "");
12
+ export function resolveServerUrl(env = process.env) {
13
+ const raw = env.CRYPTOQUANT_MCP_URL?.trim();
14
+ if (!raw)
15
+ return new URL(DEFAULT_MCP_URL);
16
+ let url;
17
+ try {
18
+ url = new URL(raw);
19
+ }
20
+ catch {
21
+ throw new ConfigError("CRYPTOQUANT_MCP_URL is not a valid URL.");
22
+ }
23
+ if (url.protocol === "https:")
24
+ return url;
25
+ if (url.protocol === "http:" && isLoopback(url.hostname))
26
+ return url;
27
+ throw new ConfigError("CRYPTOQUANT_MCP_URL must use https (plain http is allowed only for localhost, 127.0.0.0/8, and [::1]).");
22
28
  }
23
29
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,eAAe,GAAG,gCAAgC,CAAC;AAEzD;;;GAGG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,eAAe,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,SAAS,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,iCAAiC,CAAC;AAEjE,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,GAAG,aAAa,CAAC;CAC/B;AAED,SAAS,UAAU,CAAC,QAAgB;IAClC,OAAO,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,OAAO,IAAI,qBAAqB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAClG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACnE,MAAM,GAAG,GAAG,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;IAE1C,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CAAC,yCAAyC,CAAC,CAAC;IACnE,CAAC;IAED,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC;IAC1C,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,GAAG,CAAC;IACrE,MAAM,IAAI,WAAW,CACnB,wGAAwG,CACzG,CAAC;AACJ,CAAC"}
package/dist/index.js CHANGED
@@ -1,47 +1,40 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { registerAuthTools } from "./tools/auth.js";
5
- import { registerCoreTools } from "./tools/core.js";
2
+ import { invalidKeyMessage, isValidApiKey, keySourceLabel, missingKeyMessage, resolveApiKey } from "./api-key.js";
3
+ import { ConfigError, resolveServerUrl } from "./config.js";
4
+ import { startProxy } from "./proxy.js";
6
5
  import { logger } from "./utils.js";
7
- export { fetchDiscoveryEndpoints, getEndpointCatalog, isDiscoveryLoaded, searchEndpoints, getEndpointByPath, getParameterOptions, getDiscoverySummary, getAssetCategoryMap, resetDiscovery, } from "./discovery.js";
8
- export { initializePermissions, getPermissionState, setGuestMode, resetPermissions, clearDiscoveryCache, getDiscoveryCachePath, getCachedDiscovery, } from "./permissions.js";
9
- export { fetchPlanLimits, getPlanLimitsState, getPlanLimit, getEndpointResultLimit, getEndpointWindowLimits, hasEndpointAccess, parseDurationToDate, getEarliestAllowedDate, validateDateRange, getRequiredPlan, getApiRateLimit, getStaticEndpoints, resetPlanLimits, loadPlanLimitsFromCache, getAccessibleEndpointsSummary, detectPlanFromLimits, } from "./plan-limits.js";
10
- export { CACHE_VERSION, CACHE_TTL_DAYS } from "./cache/types.js";
11
- export { getCacheFilePath, isCacheValid, readCache, writeCache, invalidateCache, clearAllCaches, getCacheAgeDays, getCacheStatus, getCachePath, } from "./cache/storage.js";
12
- export { generateSummary, extractRawResponse } from "./cache/summary.js";
13
- export { getStoredApiKey, saveApiKey, updateValidatedAt, clearCredentials, getCredentialsPath, } from "./auth/storage.js";
14
- // =============================================================================
15
- // Utility Exports
16
- // =============================================================================
17
- export { jsonResponse, errorResponse, getPlanNote, capitalizeFirst, logger, } from "./utils.js";
18
- // =============================================================================
19
- // Config Exports
20
- // =============================================================================
21
- export { getApiUrl, getApiBaseUrl } from "./config.js";
22
- // =============================================================================
23
- // Tool Registration Exports
24
- // =============================================================================
25
- export { registerAuthTools } from "./tools/auth.js";
26
- export { registerCoreTools } from "./tools/core.js";
27
- // =============================================================================
28
- // MCP Server Startup
29
- // =============================================================================
30
6
  async function main() {
31
- const server = new McpServer({
32
- name: "cryptoquant",
33
- version: "1.0.0",
34
- });
35
- // Register tool categories
36
- registerAuthTools(server);
37
- registerCoreTools(server);
38
- // Start server with stdio transport
39
- const transport = new StdioServerTransport();
40
- await server.connect(transport);
41
- logger.info("CryptoQuant MCP Server running on stdio");
7
+ let url;
8
+ try {
9
+ url = resolveServerUrl();
10
+ }
11
+ catch (error) {
12
+ if (error instanceof ConfigError) {
13
+ process.stderr.write(`[cryptoquant-mcp] ${error.message}\n`);
14
+ return 1;
15
+ }
16
+ throw error;
17
+ }
18
+ const resolved = resolveApiKey();
19
+ if (!resolved) {
20
+ process.stderr.write(missingKeyMessage());
21
+ return 1;
22
+ }
23
+ if (!isValidApiKey(resolved.key)) {
24
+ process.stderr.write(invalidKeyMessage(resolved.source));
25
+ return 1;
26
+ }
27
+ logger.info(`CryptoQuant MCP proxy → ${url.origin}${url.pathname} (API key from ${keySourceLabel(resolved.source)})`);
28
+ const proxy = await startProxy({ url, apiKey: resolved.key });
29
+ await proxy.closed;
30
+ return 0;
42
31
  }
43
- main().catch((error) => {
44
- logger.error("Failed to start server:", error);
32
+ main().then((code) => {
33
+ // Let stdout/stderr drain instead of exiting mid-write; force exit if a handle lingers.
34
+ process.exitCode = code;
35
+ setTimeout(() => process.exit(code), 1_000).unref();
36
+ }, (error) => {
37
+ logger.error("Fatal:", error instanceof Error ? error.message : String(error));
45
38
  process.exit(1);
46
39
  });
47
40
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAcpC,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,GACf,MAAM,gBAAgB,CAAC;AAOxB,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAkB1B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,uBAAuB,EACvB,6BAA6B,EAC7B,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAa1B,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,UAAU,EACV,eAAe,EACf,cAAc,EACd,eAAe,EACf,cAAc,EACd,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAOzE,OAAO,EACL,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAE3B,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAChF,OAAO,EACL,YAAY,EACZ,aAAa,EACb,WAAW,EACX,eAAe,EACf,MAAM,GACP,MAAM,YAAY,CAAC;AAEpB,gFAAgF;AAChF,iBAAiB;AACjB,gFAAgF;AAChF,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEvD,gFAAgF;AAChF,4BAA4B;AAC5B,gFAAgF;AAChF,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEpD,gFAAgF;AAChF,qBAAqB;AACrB,gFAAgF;AAChF,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,2BAA2B;IAC3B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE1B,oCAAoC;IACpC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEhC,MAAM,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;AACzD,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,MAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;IAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClH,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpC,KAAK,UAAU,IAAI;IACjB,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;YAC7D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,aAAa,EAAE,CAAC;IACjC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,2BAA2B,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,kBAAkB,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAEtH,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9D,MAAM,KAAK,CAAC,MAAM,CAAC;IACnB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,IAAI,EAAE,CAAC,IAAI,CACT,CAAC,IAAI,EAAE,EAAE;IACP,wFAAwF;IACxF,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IACxB,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC;AACtD,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;IACjB,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CACF,CAAC"}
package/dist/proxy.js ADDED
@@ -0,0 +1,133 @@
1
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { API_KEY_URL } from "./api-key.js";
4
+ import { logger } from "./utils.js";
5
+ export const API_KEY_REJECTED = -32001;
6
+ export const UPSTREAM_ERROR = -32603;
7
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
8
+ function isRequest(message) {
9
+ return "method" in message && "id" in message && message.id !== undefined;
10
+ }
11
+ function isResponse(message) {
12
+ return "id" in message && !("method" in message) && ("result" in message || "error" in message);
13
+ }
14
+ function label(message) {
15
+ return "method" in message ? message.method : "response";
16
+ }
17
+ function summarize(error, redact) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ const cause = error instanceof Error && error.cause instanceof Error ? ` (${error.cause.message})` : "";
20
+ const text = redact(`${message}${cause}`);
21
+ return text.length > 300 ? `${text.slice(0, 297)}...` : text;
22
+ }
23
+ function toRpcError(error, redact) {
24
+ if (error instanceof StreamableHTTPError && error.code === 401) {
25
+ return {
26
+ code: API_KEY_REJECTED,
27
+ message: `CryptoQuant rejected the API key. Check CRYPTOQUANT_API_KEY, or create a key at ${API_KEY_URL}`,
28
+ };
29
+ }
30
+ return { code: UPSTREAM_ERROR, message: `CryptoQuant MCP server request failed: ${summarize(error, redact)}` };
31
+ }
32
+ /**
33
+ * Relays JSON-RPC between a stdio MCP client and the remote Streamable HTTP server.
34
+ * Messages are forwarded as is; the proxy only answers on the server's behalf when
35
+ * a request cannot reach it (401, network, 5xx, timeout).
36
+ */
37
+ export async function startProxy(options) {
38
+ const stdin = options.stdin ?? process.stdin;
39
+ const stdout = options.stdout ?? process.stdout;
40
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
41
+ const local = new StdioServerTransport(stdin, stdout);
42
+ const remote = new StreamableHTTPClientTransport(options.url, {
43
+ requestInit: { headers: { Authorization: `Bearer ${options.apiKey}` } },
44
+ });
45
+ // The one place that scrubs the key from anything that can reach stdout or stderr.
46
+ const redact = (text) => (options.apiKey ? text.split(options.apiKey).join("***") : text);
47
+ const pending = new Map();
48
+ let closing = false;
49
+ let markClosed;
50
+ const closed = new Promise((resolve) => {
51
+ markClosed = resolve;
52
+ });
53
+ const reply = (message) => {
54
+ if (closing)
55
+ return;
56
+ local.send(message).catch((error) => logger.error("Failed to write to stdout:", summarize(error, redact)));
57
+ };
58
+ const settle = (id) => {
59
+ const entry = pending.get(id);
60
+ if (!entry)
61
+ return undefined;
62
+ clearTimeout(entry.timer);
63
+ pending.delete(id);
64
+ return entry;
65
+ };
66
+ const fail = (id, error) => {
67
+ if (!settle(id))
68
+ return; // already answered or timed out
69
+ reply({ jsonrpc: "2.0", id, error });
70
+ };
71
+ remote.onmessage = (message) => {
72
+ if (isResponse(message)) {
73
+ const entry = settle(message.id);
74
+ if (!entry) {
75
+ logger.warn(`Dropping a late or unknown response (id ${String(message.id)})`);
76
+ return;
77
+ }
78
+ if (entry.method === "initialize" && "result" in message) {
79
+ const version = message.result.protocolVersion;
80
+ if (typeof version === "string")
81
+ remote.setProtocolVersion(version);
82
+ }
83
+ }
84
+ reply(message);
85
+ };
86
+ // The SDK reports a failed send() here as well as by rejecting it, so this is the single
87
+ // warn for request failures; the send() catch below only builds the JSON-RPC error.
88
+ remote.onerror = (error) => {
89
+ if (closing || error.name === "AbortError")
90
+ return;
91
+ logger.warn("Remote transport error:", summarize(error, redact));
92
+ };
93
+ local.onmessage = (message) => {
94
+ if (isRequest(message)) {
95
+ const id = message.id;
96
+ settle(id); // a client reusing a pending id must not leave the old timer running
97
+ const timer = setTimeout(() => fail(id, { code: UPSTREAM_ERROR, message: `CryptoQuant MCP server did not respond within ${timeoutMs / 1000}s` }), timeoutMs);
98
+ pending.set(id, { method: message.method, timer });
99
+ }
100
+ if ("method" in message && message.method === "notifications/cancelled" && "params" in message) {
101
+ const requestId = message.params?.requestId;
102
+ if (typeof requestId === "string" || typeof requestId === "number")
103
+ settle(requestId);
104
+ }
105
+ remote.send(message).catch((error) => {
106
+ if (isRequest(message)) {
107
+ fail(message.id, toRpcError(error, redact));
108
+ }
109
+ else {
110
+ logger.warn(`Failed to forward ${label(message)}:`, summarize(error, redact));
111
+ }
112
+ });
113
+ };
114
+ local.onerror = (error) => logger.warn("Invalid message from client:", summarize(error, redact));
115
+ const shutdown = async () => {
116
+ if (closing)
117
+ return closed;
118
+ closing = true;
119
+ for (const entry of pending.values())
120
+ clearTimeout(entry.timer);
121
+ pending.clear();
122
+ await remote.close().catch(() => undefined);
123
+ await local.close().catch(() => undefined);
124
+ markClosed();
125
+ return closed;
126
+ };
127
+ stdin.once("end", () => void shutdown());
128
+ stdin.once("close", () => void shutdown());
129
+ await remote.start();
130
+ await local.start();
131
+ return { closed, close: shutdown };
132
+ }
133
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}