@elfa-ai/mcp 2.0.0 → 4.1.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/CHANGELOG.md +55 -0
- package/README.md +26 -22
- package/dist/index.js +129 -223
- package/manifest.json +11 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **DNS rebinding protection is now armed by default on the HTTP transport.** `enableDnsRebindingProtection` was only passed when `ELFA_MCP_ALLOWED_ORIGINS` was set, and that variable is unset in the documented local run (`ELFA_MCP_TRANSPORT=http npx -y @elfa-ai/mcp`), so the control shipped present but disarmed. The transport accepted any `Host` and any `Origin` while holding `ELFA_API_KEY`, which let a page the user visits re-point its own hostname at `127.0.0.1` and reach `tools/list` and `tools/call` at that process's own privilege — including the `auto_*` tools that spend credits. Binding to `127.0.0.1` was never a mitigation, because the victim's browser is already on loopback.
|
|
8
|
+
|
|
9
|
+
Setting the origin allowlist did not close it either. `allowedOrigins` was passed but `allowedHosts` never was, and the SDK guards each check with its own `length > 0` test, so `Host` validation never ran. An origin allowlist cannot carry this alone: a rebound request is same-origin and sends no `Origin` header, which leaves `Host` as the only header still naming the attacker's domain.
|
|
10
|
+
|
|
11
|
+
Refs GHSA-w48q-cv73-mx4w / CVE-2025-66414. Reported by mcpdone.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`ELFA_MCP_ALLOWED_HOSTS`**, a comma separated `Host` allowlist. It defaults to the loopback names the server binds — `ELFA_MCP_HOST:PORT`, `localhost:PORT` and `127.0.0.1:PORT` — so the documented local run is closed with no configuration.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Action required for hosted deployments.** The host allowlist fails closed, so a deployment answering on any `Host` other than the loopback names the server binds now returns `403` until it sets `ELFA_MCP_ALLOWED_HOSTS`. That covers a public domain, a reverse proxy, and a container that maps the port to a different one than the server binds, since the default is derived from `ELFA_MCP_PORT`.
|
|
20
|
+
|
|
21
|
+
A minor rather than a major, because nothing about the interaction with this server changes: the eleven tools, their arguments, the stdio transport and every client-facing contract are untouched. A self-hosted HTTP deployment needs one environment variable.
|
|
22
|
+
|
|
23
|
+
**Migration:** set `ELFA_MCP_ALLOWED_HOSTS` to the host values the deployment serves, for example `ELFA_MCP_ALLOWED_HOSTS=mcp.example.com`. The stdio transport is unaffected and needs no change.
|
|
24
|
+
|
|
25
|
+
## 4.0.0
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
- **`ELFA_HMAC_SECRET` and request signing are gone.** Elfa no longer documents HMAC signing for `/v2/auto/*` — the API key alone authenticates every route, including mutations. Removed here: the `ELFA_HMAC_SECRET` environment variable, the `x-elfa-hmac-secret` request header on the HTTP transport, the `elfa_hmac_secret` field in the Claude Desktop bundle, and the registry entry in `server.json`.
|
|
30
|
+
|
|
31
|
+
**Migration:** unset `ELFA_HMAC_SECRET` and stop sending `x-elfa-hmac-secret`. Neither is read any more. Nothing replaces them.
|
|
32
|
+
|
|
33
|
+
- **The client-side signing gate is gone.** `auto_query_write` and `auto_draft` no longer inspect a query's action shape and pre-reject it before sending. Every action type Elfa documents for API-key Auto — `notify`, `telegram_bot`, `webhook`, `llm` — is accepted with the API key alone, so the gate could only produce false rejections.
|
|
34
|
+
|
|
35
|
+
- **`hmacEnabled` is no longer reported by `api_status`.** The field was removed from the key-status response, so surfacing it would report something the API no longer sends.
|
|
36
|
+
|
|
37
|
+
- **`pacifica` is no longer a valid `auto_validate` exchange.** The documented enum for `method=symbol` is now `hyperliquid`, `gmx`, `binance`.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Spec refreshed to `2.6.3`** from `2.5.0`. A credit is now $0.0145, so the x402 reference prices move to $0.0145 (1 credit), $0.0725 (5 credits) and $0.261 (18 credits). Accounts already on PAYG keep $0.009 per credit until 28 September 2026, 16:00 UTC.
|
|
42
|
+
- **`getMarketEvents-v2` is recorded as deliberately unexposed.** It is limited to select Enterprise customers and the published operation takes no parameters, so there is no usable tool surface. Eleven tools, unchanged.
|
|
43
|
+
- Install troubleshooting no longer claims a `403` on an Auto call means Auto is not enabled in the portal. There is no separate Auto enablement step; a new API key can call `/v2/auto/*` immediately.
|
|
44
|
+
- `SECURITY.md` now states that the API key is read from the environment on stdio and can arrive per request as `x-elfa-api-key` over HTTP, and refers to the tools by their current names.
|
|
45
|
+
|
|
46
|
+
## 3.0.0
|
|
47
|
+
|
|
48
|
+
### Removed
|
|
49
|
+
|
|
50
|
+
- **The `auto_exchanges` tool is gone.** Exchange connections are no longer part of the documented Auto surface, so listing, connecting and disconnecting venues is no longer exposed here. The three operations are recorded in `manifest.json` under `unexposed`, so the drift check still accounts for every documented `/v2` operation. The endpoints remain reachable over the API for anyone who needs them.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- **`validate_symbol` moved to `auto_validate`.** It is now `auto_validate` with `method=symbol`, taking `exchange` and `symbol`, and returns `valid: true/false` with the unsupported case reported as a validation error. The default `method=query` behaviour is unchanged.
|
|
55
|
+
- **Eleven tools**, down from twelve.
|
|
56
|
+
- Signing guidance everywhere — server instructions, `auto_query_write`, README, install docs, the desktop bundle and the registry entry — now describes `ELFA_HMAC_SECRET` as signing Auto mutations that are not plain notifications, rather than exchange linking and order placement.
|
|
57
|
+
|
|
3
58
|
## 2.0.0
|
|
4
59
|
|
|
5
60
|
### Changed
|
package/README.md
CHANGED
|
@@ -48,7 +48,6 @@ Ask *"what's trending in crypto right now?"* to confirm it works.
|
|
|
48
48
|
| Variable | Required | Purpose |
|
|
49
49
|
| --- | --- | --- |
|
|
50
50
|
| `ELFA_API_KEY` | yes | Authenticates every request |
|
|
51
|
-
| `ELFA_HMAC_SECRET` | no | Signs Auto requests. Needed only to connect an exchange, or to run a query whose action places an order |
|
|
52
51
|
| `ELFA_TIMEOUT` | no | Request timeout in ms, default `120000` |
|
|
53
52
|
| `ELFA_RETRIES` | no | Retries on failure, default `0` |
|
|
54
53
|
| `ELFA_MCP_MAX_RESPONSE_CHARS` | no | Response size ceiling, default `60000` |
|
|
@@ -58,31 +57,29 @@ The timeout is high and retries are off on purpose. The interpretation endpoints
|
|
|
58
57
|
|
|
59
58
|
Some MCP clients apply their own timeout, often around 60 seconds. `narratives` and `market_chat` can exceed that; the request still completes and is still charged, even if the client gives up first.
|
|
60
59
|
|
|
61
|
-
Without `ELFA_HMAC_SECRET` everything still works except exchange linking and order-placing queries — those return a message telling you what to set.
|
|
62
|
-
|
|
63
60
|
## Tools
|
|
64
61
|
|
|
65
62
|
<!-- tools:start -->
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
11 tools, mapped to every documented `/v2` operation.
|
|
68
65
|
|
|
69
66
|
| Tool | Mode | Cost | What it does |
|
|
70
67
|
| --- | --- | --- | --- |
|
|
71
68
|
| `api_status` | read | Free | Check API key tier, credit usage and remaining requests. Also confirms the API is reachable. |
|
|
72
|
-
| `mentions` | read | 1 per call | Social mentions from X and Telegram. mode=top ranks a ticker's mentions by engagement, mode=search filters by keyword or account, mode=news returns the token news feed. |
|
|
69
|
+
| `mentions` | read | 1 per call | Social mentions from X and Telegram. mode=top ranks a ticker's mentions by engagement, mode=search filters by keyword or account, mode=news returns the token news feed, which is X posts from accounts tagged as news sources rather than articles from news outlets. |
|
|
73
70
|
| `trending` | read | 1 per call | What is gaining social attention. scope=tokens for tickers, scope=contracts_twitter or scope=contracts_telegram for contract addresses. |
|
|
74
71
|
| `narratives` | read | 5 per call | Written narrative analysis with source links. scope=market extracts market-wide narratives, scope=keywords summarises events for specific keywords. |
|
|
75
|
-
| `account_stats` | read | 1 per call | Smart follower and engagement stats for an X account. |
|
|
72
|
+
| `account_stats` | read | 1 per call | Smart follower and engagement stats for an X account. Legacy: it still works, but will be removed on 28 October 2026. |
|
|
76
73
|
| `market_chat` | read | Varies by speed | Ask for written market analysis. Supports conversational chat, macro overview, quick summary, token intro, token analysis and account analysis. |
|
|
77
74
|
| `auto_build` | read | 1 plus LLM usage | Turn a plain-language monitoring request into an EQL query. Returns a draft to validate and activate, it does not activate anything itself. |
|
|
78
|
-
| `auto_validate` | read | Free | Check EQL syntax and get a cost estimate before activating
|
|
75
|
+
| `auto_validate` | read | Free | Check EQL syntax and get a cost estimate before activating, or check that a symbol has market data on a venue. |
|
|
79
76
|
| `auto_query` | read | Free | Read side of Auto: list queries, poll one query, and read its executions and LLM sessions. |
|
|
80
77
|
| `auto_query_write` | write | 5 plus LLM usage to create, free to cancel or delete | Activate, cancel or delete an Auto query. Activated queries run unattended and fire their action when conditions are met. |
|
|
81
78
|
| `auto_draft` | write | Free, except convert which costs the same as creating a query | Manage inactive Auto drafts. Drafts do not evaluate until converted into an active query. |
|
|
82
|
-
| `auto_exchanges` | write | Free | List, connect and disconnect the exchange accounts Auto uses for order actions, and check whether a symbol is tradeable. |
|
|
83
79
|
|
|
84
80
|
Not exposed as tools:
|
|
85
81
|
|
|
82
|
+
- `getMarketEvents-v2` — Available only to select Enterprise customers, and the published operation takes no parameters. Contact sales@elfa.ai for access.
|
|
86
83
|
- `chat-stream-v2` — A tool call returns one result, so streaming adds nothing. market_chat covers the same analysis.
|
|
87
84
|
- `auto-stream-queries-v2` — Long lived streams have no tool equivalent. Poll with auto_query.
|
|
88
85
|
- `auto-stream-query-v2` — Long lived streams have no tool equivalent. Poll with auto_query.
|
|
@@ -114,18 +111,7 @@ The flow is three steps:
|
|
|
114
111
|
2. `auto_validate` — check the syntax and get the credit cost
|
|
115
112
|
3. `auto_query_write` — activate it
|
|
116
113
|
|
|
117
|
-
Actions can notify you, call a webhook, message a Telegram bot, run an LLM analysis
|
|
118
|
-
|
|
119
|
-
### Connecting an exchange
|
|
120
|
-
|
|
121
|
-
| Venue | Connect from here | Credentials |
|
|
122
|
-
| --- | --- | --- |
|
|
123
|
-
| `binance` | yes | `apiKey`, `secret` |
|
|
124
|
-
| `pacifica` | yes | `privateKey`, `walletAddress` |
|
|
125
|
-
| `hyperliquid` | no, wallet is set up in the Elfa app | none |
|
|
126
|
-
| `gmx` | no, wallet is set up in the Elfa app | none |
|
|
127
|
-
|
|
128
|
-
Credentials are verified on connect, so a wrong value fails there rather than when an order fires. They are ordinary tool arguments, so they pass through the model and land in the client's transcript.
|
|
114
|
+
Actions can notify you, call a webhook, message a Telegram bot, or run an LLM analysis.
|
|
129
115
|
|
|
130
116
|
There is no push channel over MCP. Poll `auto_query` with `method=get`, and wait for the returned `pollAfterSeconds` between calls.
|
|
131
117
|
|
|
@@ -137,14 +123,32 @@ The same server runs over Streamable HTTP for hosted deployments:
|
|
|
137
123
|
ELFA_MCP_TRANSPORT=http ELFA_MCP_PORT=3000 npx -y @elfa-ai/mcp
|
|
138
124
|
```
|
|
139
125
|
|
|
140
|
-
It is stateless — no sessions, one server instance per request, safe behind a load balancer. Credentials come from `x-elfa-api-key`
|
|
126
|
+
It is stateless — no sessions, one server instance per request, safe behind a load balancer. Credentials come from the `x-elfa-api-key` request header, falling back to the environment.
|
|
127
|
+
|
|
128
|
+
DNS rebinding protection is on by default. The server accepts only the loopback names it binds — `localhost:PORT` and `127.0.0.1:PORT` — which covers the local run above and nothing else. Any deployment that answers on a different `Host` must list the values it serves:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
ELFA_MCP_ALLOWED_HOSTS=mcp.example.com
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
That includes a public domain, a reverse proxy, and a container that maps the port to a different one than the server binds. A `Host` the list does not cover is rejected with 403.
|
|
135
|
+
|
|
136
|
+
| Variable | Required | Purpose |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `ELFA_MCP_TRANSPORT` | no | `http` to serve over Streamable HTTP, default `stdio` |
|
|
139
|
+
| `ELFA_MCP_HOST` | no | Bind address, default `127.0.0.1` |
|
|
140
|
+
| `ELFA_MCP_PORT` | no | Bind port, default `3000` |
|
|
141
|
+
| `ELFA_MCP_ALLOWED_HOSTS` | no | Comma separated `Host` allowlist, defaults to the loopback names bound |
|
|
142
|
+
| `ELFA_MCP_ALLOWED_ORIGINS` | no | Comma separated `Origin` allowlist |
|
|
143
|
+
|
|
144
|
+
Set `ELFA_MCP_ALLOWED_ORIGINS` as well when browsers call the server directly. It complements the host allowlist rather than replacing it: a rebound request is same origin, so it carries no `Origin` header for that list to check, and the `Host` header is the only one still naming the attacker's domain.
|
|
141
145
|
|
|
142
146
|
## Safety
|
|
143
147
|
|
|
144
148
|
`api_status` is the fastest way to tell an auth problem from a credit problem.
|
|
145
149
|
|
|
146
150
|
|
|
147
|
-
Mentions, news and narratives return third-party social text that anyone can write. The server marks it as untrusted in every response, and the server instructions tell the model to treat it as data. Keep that in mind before letting an agent chain from that content into `auto_query_write
|
|
151
|
+
Mentions, news and narratives return third-party social text that anyone can write. The server marks it as untrusted in every response, and the server instructions tell the model to treat it as data. Keep that in mind before letting an agent chain from that content into `auto_query_write`.
|
|
148
152
|
|
|
149
153
|
## Development
|
|
150
154
|
|
package/dist/index.js
CHANGED
|
@@ -18,11 +18,8 @@ function messageOf(error) {
|
|
|
18
18
|
if (typeof error === "string") return error;
|
|
19
19
|
return "Unknown error";
|
|
20
20
|
}
|
|
21
|
-
function missingCredential(
|
|
22
|
-
|
|
23
|
-
return `No Elfa API key configured. Set ELFA_API_KEY in the MCP server environment, or send it as an x-elfa-api-key header when using the HTTP transport. Get a key at ${DOCS}.`;
|
|
24
|
-
}
|
|
25
|
-
return `This action requires request signing. Set ELFA_HMAC_SECRET in the MCP server environment, then retry. Notification-only Auto queries work without it. See ${DOCS}/auto/api-key-auth.`;
|
|
21
|
+
function missingCredential() {
|
|
22
|
+
return `No Elfa API key configured. Set ELFA_API_KEY in the MCP server environment, or send it as an x-elfa-api-key header when using the HTTP transport. Get a key at ${DOCS}.`;
|
|
26
23
|
}
|
|
27
24
|
function describeError(error) {
|
|
28
25
|
const status = statusOf(error);
|
|
@@ -31,7 +28,7 @@ function describeError(error) {
|
|
|
31
28
|
case 400:
|
|
32
29
|
return `Request rejected as invalid: ${detail}. Check the argument values and retry.`;
|
|
33
30
|
case 401:
|
|
34
|
-
return `Authentication failed: ${detail}. Verify ELFA_API_KEY
|
|
31
|
+
return `Authentication failed: ${detail}. Verify ELFA_API_KEY.`;
|
|
35
32
|
case 402:
|
|
36
33
|
return `Out of credits: ${detail}. Top up or upgrade the plan, then retry.`;
|
|
37
34
|
case 403:
|
|
@@ -60,21 +57,18 @@ var CredentialError = class extends Error {
|
|
|
60
57
|
};
|
|
61
58
|
function buildDeps(config, overrides = {}) {
|
|
62
59
|
const apiKey = overrides.apiKey ?? config.apiKey;
|
|
63
|
-
const hmacSecret = overrides.hmacSecret ?? config.hmacSecret;
|
|
64
60
|
if (!apiKey) {
|
|
65
|
-
throw new CredentialError(missingCredential(
|
|
61
|
+
throw new CredentialError(missingCredential());
|
|
66
62
|
}
|
|
67
63
|
const sdk = new ElfaSDK({
|
|
68
64
|
elfaApiKey: apiKey,
|
|
69
65
|
timeout: config.timeout,
|
|
70
66
|
retries: config.retries,
|
|
71
|
-
...hmacSecret ? { hmacSecret } : {},
|
|
72
67
|
...config.baseUrl ? { baseUrl: config.baseUrl } : {},
|
|
73
68
|
...config.extraHeaders ? { headers: config.extraHeaders } : {}
|
|
74
69
|
});
|
|
75
70
|
return {
|
|
76
71
|
sdk,
|
|
77
|
-
hasHmac: Boolean(hmacSecret),
|
|
78
72
|
maxResponseChars: config.maxResponseChars
|
|
79
73
|
};
|
|
80
74
|
}
|
|
@@ -125,8 +119,8 @@ function loadConfig(env = process.env) {
|
|
|
125
119
|
port: num(env.ELFA_MCP_PORT, DEFAULT_PORT),
|
|
126
120
|
host: env.ELFA_MCP_HOST || DEFAULT_HOST,
|
|
127
121
|
allowedOrigins: list(env.ELFA_MCP_ALLOWED_ORIGINS),
|
|
122
|
+
allowedHosts: list(env.ELFA_MCP_ALLOWED_HOSTS),
|
|
128
123
|
apiKey: env.ELFA_API_KEY || void 0,
|
|
129
|
-
hmacSecret: env.ELFA_HMAC_SECRET || void 0,
|
|
130
124
|
baseUrl: env.ELFA_BASE_URL || void 0,
|
|
131
125
|
extraHeaders: headers(env.ELFA_EXTRA_HEADERS),
|
|
132
126
|
timeout: num(env.ELFA_TIMEOUT, DEFAULT_TIMEOUT),
|
|
@@ -148,10 +142,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
148
142
|
// package.json
|
|
149
143
|
var package_default = {
|
|
150
144
|
name: "@elfa-ai/mcp",
|
|
151
|
-
version: "
|
|
145
|
+
version: "4.1.0",
|
|
152
146
|
description: "Model Context Protocol server for the Elfa API - crypto social intelligence and the Auto condition engine",
|
|
153
147
|
license: "MIT",
|
|
154
|
-
author: "Elfa
|
|
148
|
+
author: "Elfa AI",
|
|
155
149
|
type: "module",
|
|
156
150
|
mcpName: "ai.elfa/mcp",
|
|
157
151
|
main: "dist/index.js",
|
|
@@ -337,29 +331,6 @@ function pickDefined(input) {
|
|
|
337
331
|
}
|
|
338
332
|
return out;
|
|
339
333
|
}
|
|
340
|
-
var NOTIFICATION_ACTIONS = /* @__PURE__ */ new Set(["notify", "telegram_bot", "webhook"]);
|
|
341
|
-
function isRecord(value) {
|
|
342
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
343
|
-
}
|
|
344
|
-
function isNotificationAction(action) {
|
|
345
|
-
if (!isRecord(action)) return false;
|
|
346
|
-
if (typeof action.type !== "string") return false;
|
|
347
|
-
if (NOTIFICATION_ACTIONS.has(action.type)) return true;
|
|
348
|
-
if (action.type === "llm") {
|
|
349
|
-
if (!isRecord(action.params)) return false;
|
|
350
|
-
if (!isRecord(action.params.callback)) return false;
|
|
351
|
-
const callbackAction = action.params.callback.action;
|
|
352
|
-
if (!isRecord(callbackAction)) return false;
|
|
353
|
-
return typeof callbackAction.type === "string" && NOTIFICATION_ACTIONS.has(callbackAction.type);
|
|
354
|
-
}
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
function requiresSignature(query) {
|
|
358
|
-
if (!isRecord(query)) return true;
|
|
359
|
-
const actions = query.actions;
|
|
360
|
-
if (!Array.isArray(actions) || actions.length === 0) return true;
|
|
361
|
-
return !actions.every(isNotificationAction);
|
|
362
|
-
}
|
|
363
334
|
|
|
364
335
|
// src/tools/account.ts
|
|
365
336
|
function registerAccountStats(server, deps) {
|
|
@@ -367,7 +338,7 @@ function registerAccountStats(server, deps) {
|
|
|
367
338
|
"account_stats",
|
|
368
339
|
{
|
|
369
340
|
title: "Account stats",
|
|
370
|
-
description: "Smart follower and engagement stats for an X account. 1 credit per call. Use it to judge whether an account's reach is real before weighting what it posts.",
|
|
341
|
+
description: "Smart follower and engagement stats for an X account. 1 credit per call. Use it to judge whether an account's reach is real before weighting what it posts. Returns raw metrics, not a reputation score, so the signal is in the ratios. Legacy: it still works today and keeps working until 28 October 2026, when it will be removed, and it is not being extended before then. Build new work on mentions with mode=search, which returns the posting account alongside each post's engagement metrics.",
|
|
371
342
|
inputSchema: {
|
|
372
343
|
username: z2.string().describe("X username, with or without the @.")
|
|
373
344
|
},
|
|
@@ -486,9 +457,6 @@ function registerAutoDraft(server, deps) {
|
|
|
486
457
|
if (!args.query) {
|
|
487
458
|
return fail("method=upsert needs a query. Build one with auto_build.");
|
|
488
459
|
}
|
|
489
|
-
if (requiresSignature(args.query) && !deps.hasHmac) {
|
|
490
|
-
return fail(missingCredential("hmacSecret"));
|
|
491
|
-
}
|
|
492
460
|
}
|
|
493
461
|
const needsId = args.method === "get" || args.method === "delete" || args.method === "convert";
|
|
494
462
|
if (needsId && !args.draftId) {
|
|
@@ -541,98 +509,8 @@ function registerAutoDraft(server, deps) {
|
|
|
541
509
|
);
|
|
542
510
|
}
|
|
543
511
|
|
|
544
|
-
// src/tools/autoExchanges.ts
|
|
545
|
-
import { z as z5 } from "zod";
|
|
546
|
-
var EXCHANGES = ["hyperliquid", "gmx", "binance", "pacifica"];
|
|
547
|
-
function registerAutoExchanges(server, deps) {
|
|
548
|
-
server.registerTool(
|
|
549
|
-
"auto_exchanges",
|
|
550
|
-
{
|
|
551
|
-
title: "Auto exchange connections",
|
|
552
|
-
description: "The exchange accounts Auto uses when a query's action places an order. Free. method=validate_symbol checks a symbol is tradeable before you build a query around it. Connecting and disconnecting change what Auto can trade with, so confirm with the user first, and both need request signing. binance and pacifica connect from here with credentials. hyperliquid and gmx use a wallet set up in the Elfa app, so use method=list to confirm those rather than trying to connect them.",
|
|
553
|
-
inputSchema: {
|
|
554
|
-
method: z5.enum(["list", "connect", "disconnect", "validate_symbol"]).describe("Which action to perform."),
|
|
555
|
-
exchange: z5.enum(EXCHANGES).optional().describe("Required for connect, disconnect and validate_symbol."),
|
|
556
|
-
symbol: z5.string().optional().describe("Required for method=validate_symbol."),
|
|
557
|
-
credentialType: z5.string().optional().describe(
|
|
558
|
-
"Credential type for method=connect, as documented per venue. Read it off an existing connection with method=list if you are unsure."
|
|
559
|
-
),
|
|
560
|
-
metadata: z5.record(z5.unknown()).optional().describe("Non-secret connection metadata for method=connect."),
|
|
561
|
-
credentials: z5.record(z5.unknown()).optional().describe(
|
|
562
|
-
"Venue credentials for method=connect. binance needs apiKey and secret, pacifica needs privateKey and walletAddress. They are verified on connect, so a wrong value fails here rather than at trade time. They also pass through the conversation and are written to the client transcript, so tell the user that before asking for them."
|
|
563
|
-
)
|
|
564
|
-
},
|
|
565
|
-
outputSchema: {
|
|
566
|
-
method: z5.string(),
|
|
567
|
-
data: z5.unknown()
|
|
568
|
-
},
|
|
569
|
-
annotations: {
|
|
570
|
-
readOnlyHint: false,
|
|
571
|
-
destructiveHint: true,
|
|
572
|
-
idempotentHint: false,
|
|
573
|
-
openWorldHint: false
|
|
574
|
-
}
|
|
575
|
-
},
|
|
576
|
-
async (args) => {
|
|
577
|
-
const needsExchange = args.method !== "list";
|
|
578
|
-
if (needsExchange && !args.exchange) {
|
|
579
|
-
return fail(
|
|
580
|
-
`method=${args.method} needs exchange, one of ${EXCHANGES.join(", ")}.`
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
if (args.method === "validate_symbol" && !args.symbol) {
|
|
584
|
-
return fail('method=validate_symbol needs symbol, for example symbol: "BTC".');
|
|
585
|
-
}
|
|
586
|
-
if ((args.method === "connect" || args.method === "disconnect") && !deps.hasHmac) {
|
|
587
|
-
return fail(missingCredential("hmacSecret"));
|
|
588
|
-
}
|
|
589
|
-
if (args.method === "connect" && !args.credentialType) {
|
|
590
|
-
return fail(
|
|
591
|
-
"method=connect needs credentialType. See https://docs.elfa.ai/auto/trading-execution for the value each venue expects."
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
return run(deps, async () => {
|
|
595
|
-
switch (args.method) {
|
|
596
|
-
case "connect":
|
|
597
|
-
return {
|
|
598
|
-
method: args.method,
|
|
599
|
-
data: await deps.sdk.auto.connectExchange(
|
|
600
|
-
pickDefined({
|
|
601
|
-
exchange: args.exchange,
|
|
602
|
-
credentialType: args.credentialType,
|
|
603
|
-
metadata: args.metadata,
|
|
604
|
-
credentials: args.credentials
|
|
605
|
-
})
|
|
606
|
-
)
|
|
607
|
-
};
|
|
608
|
-
case "disconnect":
|
|
609
|
-
return {
|
|
610
|
-
method: args.method,
|
|
611
|
-
data: await deps.sdk.auto.disconnectExchange(
|
|
612
|
-
args.exchange
|
|
613
|
-
)
|
|
614
|
-
};
|
|
615
|
-
case "validate_symbol":
|
|
616
|
-
return {
|
|
617
|
-
method: args.method,
|
|
618
|
-
data: await deps.sdk.auto.validateSymbol(
|
|
619
|
-
args.exchange,
|
|
620
|
-
args.symbol
|
|
621
|
-
)
|
|
622
|
-
};
|
|
623
|
-
default:
|
|
624
|
-
return {
|
|
625
|
-
method: "list",
|
|
626
|
-
data: await deps.sdk.auto.listExchanges()
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
512
|
// src/tools/autoQuery.ts
|
|
635
|
-
import { z as
|
|
513
|
+
import { z as z5 } from "zod";
|
|
636
514
|
var ACTIVE = /* @__PURE__ */ new Set(["active", "pending", "running", "armed"]);
|
|
637
515
|
function registerAutoQuery(server, deps) {
|
|
638
516
|
server.registerTool(
|
|
@@ -641,7 +519,7 @@ function registerAutoQuery(server, deps) {
|
|
|
641
519
|
title: "Read Auto queries",
|
|
642
520
|
description: "Read side of Auto. Free. method=list browses queries, method=get polls one query and returns its latest evaluation, method=executions and method=execution show what fired, method=sessions and method=session return the LLM analysis attached to a query. There is no push channel here, poll with method=get and respect pollAfterSeconds.",
|
|
643
521
|
inputSchema: {
|
|
644
|
-
method:
|
|
522
|
+
method: z5.enum([
|
|
645
523
|
"list",
|
|
646
524
|
"get",
|
|
647
525
|
"executions",
|
|
@@ -649,19 +527,19 @@ function registerAutoQuery(server, deps) {
|
|
|
649
527
|
"sessions",
|
|
650
528
|
"session"
|
|
651
529
|
]).describe("Which read to perform."),
|
|
652
|
-
queryId:
|
|
653
|
-
sessionId:
|
|
654
|
-
executionId:
|
|
655
|
-
status:
|
|
656
|
-
type:
|
|
657
|
-
search:
|
|
658
|
-
limit:
|
|
659
|
-
offset:
|
|
530
|
+
queryId: z5.string().optional().describe("Required for get, sessions and session."),
|
|
531
|
+
sessionId: z5.string().optional().describe("Required for method=session."),
|
|
532
|
+
executionId: z5.string().optional().describe("Required for method=execution."),
|
|
533
|
+
status: z5.string().optional().describe("Filter by status."),
|
|
534
|
+
type: z5.string().optional().describe("Filter executions by type."),
|
|
535
|
+
search: z5.string().optional().describe("Free text filter for method=list."),
|
|
536
|
+
limit: z5.number().int().min(1).max(50).default(10).describe("Page size for list style methods."),
|
|
537
|
+
offset: z5.number().int().min(0).default(0).describe("Pagination offset.")
|
|
660
538
|
},
|
|
661
539
|
outputSchema: {
|
|
662
|
-
method:
|
|
663
|
-
data:
|
|
664
|
-
pollAfterSeconds:
|
|
540
|
+
method: z5.string(),
|
|
541
|
+
data: z5.unknown(),
|
|
542
|
+
pollAfterSeconds: z5.number().nullable()
|
|
665
543
|
},
|
|
666
544
|
annotations: {
|
|
667
545
|
readOnlyHint: true,
|
|
@@ -740,25 +618,25 @@ function registerAutoQuery(server, deps) {
|
|
|
740
618
|
}
|
|
741
619
|
|
|
742
620
|
// src/tools/autoQueryWrite.ts
|
|
743
|
-
import { z as
|
|
621
|
+
import { z as z6 } from "zod";
|
|
744
622
|
function registerAutoQueryWrite(server, deps) {
|
|
745
623
|
server.registerTool(
|
|
746
624
|
"auto_query_write",
|
|
747
625
|
{
|
|
748
626
|
title: "Write Auto queries",
|
|
749
|
-
description: "Activate, cancel or delete an Auto query. Creating costs 5 credits plus LLM usage, cancel and delete are free. An activated query runs unattended and fires its action without asking again, so validate it with auto_validate and confirm the cost and the action with the user before calling this.
|
|
627
|
+
description: "Activate, cancel or delete an Auto query. Creating costs 5 credits plus LLM usage, cancel and delete are free. An activated query runs unattended and fires its action without asking again, so validate it with auto_validate and confirm the cost and the action with the user before calling this.",
|
|
750
628
|
inputSchema: {
|
|
751
|
-
method:
|
|
629
|
+
method: z6.enum(["create", "cancel", "delete"]).describe(
|
|
752
630
|
"create activates a new query. cancel stops an active one. delete removes one that has already finished."
|
|
753
631
|
),
|
|
754
632
|
query: eqlQueryArg.optional(),
|
|
755
|
-
title:
|
|
756
|
-
description:
|
|
757
|
-
queryId:
|
|
633
|
+
title: z6.string().max(120).optional().describe("Short title shown in the notification when this fires."),
|
|
634
|
+
description: z6.string().max(500).optional().describe("Why this was set up, shown alongside the title."),
|
|
635
|
+
queryId: z6.string().optional().describe("Required for cancel and delete.")
|
|
758
636
|
},
|
|
759
637
|
outputSchema: {
|
|
760
|
-
method:
|
|
761
|
-
data:
|
|
638
|
+
method: z6.string(),
|
|
639
|
+
data: z6.unknown()
|
|
762
640
|
},
|
|
763
641
|
annotations: {
|
|
764
642
|
readOnlyHint: false,
|
|
@@ -772,9 +650,6 @@ function registerAutoQueryWrite(server, deps) {
|
|
|
772
650
|
if (!args.query) {
|
|
773
651
|
return fail("method=create needs a query. Build one with auto_build.");
|
|
774
652
|
}
|
|
775
|
-
if (requiresSignature(args.query) && !deps.hasHmac) {
|
|
776
|
-
return fail(missingCredential("hmacSecret"));
|
|
777
|
-
}
|
|
778
653
|
} else if (!args.queryId) {
|
|
779
654
|
return fail(`method=${args.method} needs queryId. Find one with auto_query.`);
|
|
780
655
|
}
|
|
@@ -797,25 +672,29 @@ function registerAutoQueryWrite(server, deps) {
|
|
|
797
672
|
}
|
|
798
673
|
|
|
799
674
|
// src/tools/autoValidate.ts
|
|
800
|
-
import { z as
|
|
675
|
+
import { z as z7 } from "zod";
|
|
676
|
+
var EXCHANGES = ["hyperliquid", "gmx", "binance"];
|
|
801
677
|
function registerAutoValidate(server, deps) {
|
|
802
678
|
server.registerTool(
|
|
803
679
|
"auto_validate",
|
|
804
680
|
{
|
|
805
681
|
title: "Validate Auto query",
|
|
806
|
-
description: "Check EQL syntax and get a cost estimate before anything is activated. Free. Always run this before auto_query_write, and show the estimated cost to the user. Pass either an inline query or a draftId.",
|
|
682
|
+
description: "Check EQL syntax and get a cost estimate before anything is activated. Free. Always run this before auto_query_write, and show the estimated cost to the user. Pass either an inline query or a draftId. method=symbol instead checks that a symbol has market data on a venue, before you build price or ta conditions around it.",
|
|
807
683
|
inputSchema: {
|
|
684
|
+
method: z7.enum(["query", "symbol"]).default("query").describe("query validates EQL, symbol checks symbol support."),
|
|
808
685
|
query: eqlQueryArg.optional(),
|
|
809
|
-
draftId:
|
|
810
|
-
title:
|
|
811
|
-
description:
|
|
686
|
+
draftId: z7.string().optional().describe("Validate a stored draft instead of an inline query."),
|
|
687
|
+
title: z7.string().max(120).optional().describe("Title to validate with the query."),
|
|
688
|
+
description: z7.string().max(500).optional().describe("Description to validate with the query."),
|
|
689
|
+
exchange: z7.enum(EXCHANGES).optional().describe("Required for method=symbol."),
|
|
690
|
+
symbol: z7.string().optional().describe('Required for method=symbol, for example "BTC".')
|
|
812
691
|
},
|
|
813
692
|
outputSchema: {
|
|
814
|
-
valid:
|
|
815
|
-
errors:
|
|
816
|
-
warnings:
|
|
817
|
-
estimatedCredits:
|
|
818
|
-
estimatedCost:
|
|
693
|
+
valid: z7.boolean(),
|
|
694
|
+
errors: z7.array(z7.unknown()),
|
|
695
|
+
warnings: z7.array(z7.unknown()),
|
|
696
|
+
estimatedCredits: z7.number().nullable(),
|
|
697
|
+
estimatedCost: z7.record(z7.unknown()).nullable()
|
|
819
698
|
},
|
|
820
699
|
annotations: {
|
|
821
700
|
readOnlyHint: true,
|
|
@@ -825,6 +704,26 @@ function registerAutoValidate(server, deps) {
|
|
|
825
704
|
}
|
|
826
705
|
},
|
|
827
706
|
async (args) => {
|
|
707
|
+
if (args.method === "symbol") {
|
|
708
|
+
if (!args.exchange || !args.symbol) {
|
|
709
|
+
return fail(
|
|
710
|
+
`method=symbol needs exchange, one of ${EXCHANGES.join(", ")}, and symbol.`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
const exchange = args.exchange;
|
|
714
|
+
const symbol = args.symbol;
|
|
715
|
+
return run(deps, async () => {
|
|
716
|
+
const response = await deps.sdk.auto.validateSymbol(exchange, symbol);
|
|
717
|
+
const supported = response.supported === "true";
|
|
718
|
+
return {
|
|
719
|
+
valid: supported,
|
|
720
|
+
errors: supported ? [] : [`${symbol} has no market data on ${exchange}.`],
|
|
721
|
+
warnings: [],
|
|
722
|
+
estimatedCredits: null,
|
|
723
|
+
estimatedCost: null
|
|
724
|
+
};
|
|
725
|
+
});
|
|
726
|
+
}
|
|
828
727
|
if (!args.query && !args.draftId) {
|
|
829
728
|
return fail("Pass either query or draftId. Retry with one of them set.");
|
|
830
729
|
}
|
|
@@ -849,7 +748,7 @@ function registerAutoValidate(server, deps) {
|
|
|
849
748
|
}
|
|
850
749
|
|
|
851
750
|
// src/tools/chat.ts
|
|
852
|
-
import { z as
|
|
751
|
+
import { z as z8 } from "zod";
|
|
853
752
|
function registerChat(server, deps) {
|
|
854
753
|
server.registerTool(
|
|
855
754
|
"market_chat",
|
|
@@ -857,7 +756,7 @@ function registerChat(server, deps) {
|
|
|
857
756
|
title: "Market chat",
|
|
858
757
|
description: "Ask Elfa for written market analysis grounded in its social data. Costs credits and varies by speed, so fast is the cheaper option. Pass sessionId from a previous reply to continue the same conversation.",
|
|
859
758
|
inputSchema: {
|
|
860
|
-
analysisType:
|
|
759
|
+
analysisType: z8.enum([
|
|
861
760
|
"chat",
|
|
862
761
|
"macro",
|
|
863
762
|
"summary",
|
|
@@ -867,20 +766,20 @@ function registerChat(server, deps) {
|
|
|
867
766
|
]).default("chat").describe(
|
|
868
767
|
"chat needs message. tokenIntro and tokenAnalysis need symbol, or chain plus contractAddress. accountAnalysis needs username. macro and summary need nothing else."
|
|
869
768
|
),
|
|
870
|
-
message:
|
|
871
|
-
sessionId:
|
|
872
|
-
speed:
|
|
769
|
+
message: z8.string().optional().describe("The question, required for analysisType=chat."),
|
|
770
|
+
sessionId: z8.string().optional().describe("Continue an earlier conversation."),
|
|
771
|
+
speed: z8.enum(["fast", "expert", "adaptive"]).default("fast").describe(
|
|
873
772
|
"Defaults to fast, which is cheaper and shallower. Ask for expert when the answer needs deeper reasoning."
|
|
874
773
|
),
|
|
875
|
-
symbol:
|
|
876
|
-
chain:
|
|
877
|
-
contractAddress:
|
|
878
|
-
username:
|
|
774
|
+
symbol: z8.string().optional().describe("Token symbol, for token analysis."),
|
|
775
|
+
chain: z8.string().optional().describe("Chain, paired with contractAddress."),
|
|
776
|
+
contractAddress: z8.string().optional().describe("Contract address, paired with chain."),
|
|
777
|
+
username: z8.string().optional().describe("X username, for analysisType=accountAnalysis.")
|
|
879
778
|
},
|
|
880
779
|
outputSchema: {
|
|
881
|
-
message:
|
|
882
|
-
sessionId:
|
|
883
|
-
creditsConsumed:
|
|
780
|
+
message: z8.string(),
|
|
781
|
+
sessionId: z8.string().nullable(),
|
|
782
|
+
creditsConsumed: z8.number().nullable()
|
|
884
783
|
},
|
|
885
784
|
annotations: {
|
|
886
785
|
readOnlyHint: true,
|
|
@@ -929,40 +828,40 @@ function registerChat(server, deps) {
|
|
|
929
828
|
}
|
|
930
829
|
|
|
931
830
|
// src/tools/mentions.ts
|
|
932
|
-
import { z as
|
|
831
|
+
import { z as z9 } from "zod";
|
|
933
832
|
function registerMentions(server, deps) {
|
|
934
833
|
server.registerTool(
|
|
935
834
|
"mentions",
|
|
936
835
|
{
|
|
937
836
|
title: "Mentions",
|
|
938
|
-
description: "Social mentions from X and Telegram. 1 credit per call. Returns engagement metrics and a link per post, not the post text. mode=top ranks one ticker's mentions by engagement. mode=search filters by keywords or by account. mode=news returns the token news feed.",
|
|
837
|
+
description: "Social mentions from X and Telegram. 1 credit per call. Returns engagement metrics and a link per post, not the post text. mode=top ranks one ticker's mentions by engagement. mode=search filters by keywords or by account. mode=news returns the token news feed, which is X posts from accounts tagged as news sources rather than articles from news outlets.",
|
|
939
838
|
inputSchema: {
|
|
940
|
-
mode:
|
|
839
|
+
mode: z9.enum(["top", "search", "news"]).describe(
|
|
941
840
|
"top needs ticker. search needs keywords or accountName. news is a feed and needs neither."
|
|
942
841
|
),
|
|
943
|
-
ticker:
|
|
944
|
-
keywords:
|
|
945
|
-
accountName:
|
|
946
|
-
coinIds:
|
|
947
|
-
searchType:
|
|
842
|
+
ticker: z9.string().optional().describe('Ticker for mode=top, for example "BTC" or "$SOL".'),
|
|
843
|
+
keywords: z9.string().optional().describe("Up to 5 comma separated keywords for mode=search."),
|
|
844
|
+
accountName: z9.string().optional().describe("X username for mode=search, without the @."),
|
|
845
|
+
coinIds: z9.string().optional().describe("Comma separated CoinGecko coin ids to filter mode=news."),
|
|
846
|
+
searchType: z9.enum(["and", "or"]).optional().describe("How multiple keywords combine in mode=search."),
|
|
948
847
|
timeWindow: timeWindowArg,
|
|
949
|
-
from:
|
|
848
|
+
from: z9.number().int().optional().describe(
|
|
950
849
|
"Start of an absolute range, unix seconds. Use with to. For mode=search the range must span at least 1 day and at most 30."
|
|
951
850
|
),
|
|
952
851
|
to: toArg,
|
|
953
852
|
page: pageArg,
|
|
954
853
|
pageSize: pageSizeArg,
|
|
955
|
-
limit:
|
|
956
|
-
cursor:
|
|
854
|
+
limit: z9.number().int().min(1).max(30).default(10).describe("Result count for mode=search."),
|
|
855
|
+
cursor: z9.string().optional().describe("Pagination cursor from a previous mode=search response."),
|
|
957
856
|
reposts: repostsArg,
|
|
958
857
|
verbosity: verbosityArg
|
|
959
858
|
},
|
|
960
859
|
outputSchema: {
|
|
961
|
-
mode:
|
|
962
|
-
notice:
|
|
963
|
-
total:
|
|
964
|
-
next:
|
|
965
|
-
mentions:
|
|
860
|
+
mode: z9.string(),
|
|
861
|
+
notice: z9.string(),
|
|
862
|
+
total: z9.number().nullable(),
|
|
863
|
+
next: z9.string().nullable(),
|
|
864
|
+
mentions: z9.array(z9.record(z9.unknown()))
|
|
966
865
|
},
|
|
967
866
|
annotations: {
|
|
968
867
|
readOnlyHint: true,
|
|
@@ -1068,7 +967,7 @@ function registerMentions(server, deps) {
|
|
|
1068
967
|
}
|
|
1069
968
|
|
|
1070
969
|
// src/tools/narratives.ts
|
|
1071
|
-
import { z as
|
|
970
|
+
import { z as z10 } from "zod";
|
|
1072
971
|
function registerNarratives(server, deps) {
|
|
1073
972
|
server.registerTool(
|
|
1074
973
|
"narratives",
|
|
@@ -1076,22 +975,22 @@ function registerNarratives(server, deps) {
|
|
|
1076
975
|
title: "Narratives",
|
|
1077
976
|
description: "Written narrative analysis with source links, for when counts are not enough. 5 credits per call, so prefer trending or mentions when metrics will do. scope=market extracts the narratives moving the market. scope=keywords summarises events for keywords you supply, and can take over a minute to return.",
|
|
1078
977
|
inputSchema: {
|
|
1079
|
-
scope:
|
|
1080
|
-
keywords:
|
|
1081
|
-
searchType:
|
|
1082
|
-
timeFrame:
|
|
1083
|
-
maxNarratives:
|
|
1084
|
-
maxTweetsPerNarrative:
|
|
978
|
+
scope: z10.enum(["market", "keywords"]).default("market").describe("market is a broad sweep. keywords needs the keywords argument."),
|
|
979
|
+
keywords: z10.string().optional().describe("Up to 5 comma separated keywords for scope=keywords."),
|
|
980
|
+
searchType: z10.enum(["and", "or"]).optional().describe("How multiple keywords combine for scope=keywords."),
|
|
981
|
+
timeFrame: z10.enum(["day", "week"]).optional().describe("Lookback for scope=market."),
|
|
982
|
+
maxNarratives: z10.number().int().min(1).max(20).optional().describe("Cap the narratives returned for scope=market."),
|
|
983
|
+
maxTweetsPerNarrative: z10.number().int().min(1).max(20).optional().describe("Cap the sources per narrative for scope=market."),
|
|
1085
984
|
timeWindow: timeWindowArg,
|
|
1086
985
|
from: fromArg,
|
|
1087
986
|
to: toArg
|
|
1088
987
|
},
|
|
1089
988
|
outputSchema: {
|
|
1090
|
-
scope:
|
|
1091
|
-
notice:
|
|
1092
|
-
items:
|
|
1093
|
-
matched:
|
|
1094
|
-
note:
|
|
989
|
+
scope: z10.string(),
|
|
990
|
+
notice: z10.string(),
|
|
991
|
+
items: z10.array(z10.record(z10.unknown())),
|
|
992
|
+
matched: z10.number().nullable(),
|
|
993
|
+
note: z10.string().nullable()
|
|
1095
994
|
},
|
|
1096
995
|
annotations: {
|
|
1097
996
|
readOnlyHint: true,
|
|
@@ -1164,7 +1063,7 @@ function registerNarratives(server, deps) {
|
|
|
1164
1063
|
}
|
|
1165
1064
|
|
|
1166
1065
|
// src/tools/status.ts
|
|
1167
|
-
import { z as
|
|
1066
|
+
import { z as z11 } from "zod";
|
|
1168
1067
|
var REPORTED = [
|
|
1169
1068
|
"tier",
|
|
1170
1069
|
"status",
|
|
@@ -1182,7 +1081,6 @@ var REPORTED = [
|
|
|
1182
1081
|
"depositCredits",
|
|
1183
1082
|
"spendCapCredits",
|
|
1184
1083
|
"athenaEnabled",
|
|
1185
|
-
"hmacEnabled",
|
|
1186
1084
|
"scopes"
|
|
1187
1085
|
];
|
|
1188
1086
|
function summarise(data) {
|
|
@@ -1200,9 +1098,9 @@ function registerStatus(server, deps) {
|
|
|
1200
1098
|
description: "Check the Elfa API key tier, credit usage and remaining requests, and confirm the API is reachable. Free. Call this first when a request fails with an auth or credit error.",
|
|
1201
1099
|
inputSchema: {},
|
|
1202
1100
|
outputSchema: {
|
|
1203
|
-
reachable:
|
|
1204
|
-
key:
|
|
1205
|
-
problem:
|
|
1101
|
+
reachable: z11.boolean(),
|
|
1102
|
+
key: z11.record(z11.unknown()).nullable(),
|
|
1103
|
+
problem: z11.string().nullable()
|
|
1206
1104
|
},
|
|
1207
1105
|
annotations: {
|
|
1208
1106
|
readOnlyHint: true,
|
|
@@ -1226,7 +1124,7 @@ function registerStatus(server, deps) {
|
|
|
1226
1124
|
}
|
|
1227
1125
|
|
|
1228
1126
|
// src/tools/trending.ts
|
|
1229
|
-
import { z as
|
|
1127
|
+
import { z as z12 } from "zod";
|
|
1230
1128
|
function registerTrending(server, deps) {
|
|
1231
1129
|
server.registerTool(
|
|
1232
1130
|
"trending",
|
|
@@ -1234,22 +1132,22 @@ function registerTrending(server, deps) {
|
|
|
1234
1132
|
title: "Trending",
|
|
1235
1133
|
description: "What is gaining social attention right now. 1 credit per call. scope=tokens returns tickers ranked by mention volume with the change against the previous window. scope=contracts_twitter and scope=contracts_telegram return trending contract addresses.",
|
|
1236
1134
|
inputSchema: {
|
|
1237
|
-
scope:
|
|
1238
|
-
timeWindow:
|
|
1135
|
+
scope: z12.enum(["tokens", "contracts_twitter", "contracts_telegram"]).default("tokens").describe("Which trending list to return."),
|
|
1136
|
+
timeWindow: z12.string().optional().describe(
|
|
1239
1137
|
'Relative window such as "24h" or "7d". Defaults to 24h unless both from and to are given.'
|
|
1240
1138
|
),
|
|
1241
1139
|
from: fromArg,
|
|
1242
1140
|
to: toArg,
|
|
1243
1141
|
page: pageArg,
|
|
1244
1142
|
pageSize: pageSizeArg,
|
|
1245
|
-
minMentions:
|
|
1143
|
+
minMentions: z12.number().int().min(1).default(5).describe("Drop entries below this mention count.")
|
|
1246
1144
|
},
|
|
1247
1145
|
outputSchema: {
|
|
1248
|
-
scope:
|
|
1249
|
-
page:
|
|
1250
|
-
pageSize:
|
|
1251
|
-
total:
|
|
1252
|
-
items:
|
|
1146
|
+
scope: z12.string(),
|
|
1147
|
+
page: z12.number().nullable(),
|
|
1148
|
+
pageSize: z12.number().nullable(),
|
|
1149
|
+
total: z12.number().nullable(),
|
|
1150
|
+
items: z12.array(z12.record(z12.unknown()))
|
|
1253
1151
|
},
|
|
1254
1152
|
annotations: {
|
|
1255
1153
|
readOnlyHint: true,
|
|
@@ -1308,7 +1206,7 @@ var INSTRUCTIONS = `Elfa gives you crypto social intelligence plus Auto, a condi
|
|
|
1308
1206
|
|
|
1309
1207
|
Start cheap. trending, mentions and account_stats cost 1 credit. narratives costs 5 and market_chat costs more, so reach for them only when metrics are not enough. If a call fails on auth or credits, check api_status.
|
|
1310
1208
|
|
|
1311
|
-
Auto is a three step flow: auto_build drafts EQL from plain language, auto_validate checks it and returns the cost, auto_query_write activates it. Never activate without showing the user the estimated cost and the action that will fire.
|
|
1209
|
+
Auto is a three step flow: auto_build drafts EQL from plain language, auto_validate checks it and returns the cost, auto_query_write activates it. Never activate without showing the user the estimated cost and the action that will fire.
|
|
1312
1210
|
|
|
1313
1211
|
Auto has no push channel here. Poll auto_query with method=get and wait for pollAfterSeconds between calls.
|
|
1314
1212
|
|
|
@@ -1329,7 +1227,6 @@ function createServer(deps) {
|
|
|
1329
1227
|
registerAutoQuery(server, deps);
|
|
1330
1228
|
registerAutoQueryWrite(server, deps);
|
|
1331
1229
|
registerAutoDraft(server, deps);
|
|
1332
|
-
registerAutoExchanges(server, deps);
|
|
1333
1230
|
return server;
|
|
1334
1231
|
}
|
|
1335
1232
|
|
|
@@ -1346,6 +1243,14 @@ function jsonRpcError(res, status, message) {
|
|
|
1346
1243
|
id: null
|
|
1347
1244
|
});
|
|
1348
1245
|
}
|
|
1246
|
+
function defaultAllowedHosts(config) {
|
|
1247
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
1248
|
+
for (const name of [config.host, "localhost", "127.0.0.1", "[::1]"]) {
|
|
1249
|
+
hosts.add(`${name}:${config.port}`);
|
|
1250
|
+
if (config.port === 80) hosts.add(name);
|
|
1251
|
+
}
|
|
1252
|
+
return [...hosts];
|
|
1253
|
+
}
|
|
1349
1254
|
function createHttpApp(config) {
|
|
1350
1255
|
const app = express();
|
|
1351
1256
|
app.disable("x-powered-by");
|
|
@@ -1372,12 +1277,13 @@ function createHttpApp(config) {
|
|
|
1372
1277
|
let transport;
|
|
1373
1278
|
try {
|
|
1374
1279
|
const deps = buildDeps(config, {
|
|
1375
|
-
apiKey: header(req, "x-elfa-api-key")
|
|
1376
|
-
hmacSecret: header(req, "x-elfa-hmac-secret")
|
|
1280
|
+
apiKey: header(req, "x-elfa-api-key")
|
|
1377
1281
|
});
|
|
1378
1282
|
server = createServer(deps);
|
|
1379
1283
|
transport = new StreamableHTTPServerTransport({
|
|
1380
|
-
|
|
1284
|
+
enableDnsRebindingProtection: true,
|
|
1285
|
+
allowedHosts: config.allowedHosts.length > 0 ? config.allowedHosts : defaultAllowedHosts(config),
|
|
1286
|
+
...config.allowedOrigins.length > 0 ? { allowedOrigins: config.allowedOrigins } : {}
|
|
1381
1287
|
});
|
|
1382
1288
|
res.on("close", () => {
|
|
1383
1289
|
void transport?.close();
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"server": {
|
|
3
3
|
"name": "elfa",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"docs": "https://docs.elfa.ai",
|
|
6
6
|
"spec": "https://docs.elfa.ai/swagger/swagger.json",
|
|
7
7
|
"specScope": "/v2"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{
|
|
27
27
|
"name": "mentions",
|
|
28
28
|
"title": "Mentions",
|
|
29
|
-
"summary": "Social mentions from X and Telegram. mode=top ranks a ticker's mentions by engagement, mode=search filters by keyword or account, mode=news returns the token news feed.",
|
|
29
|
+
"summary": "Social mentions from X and Telegram. mode=top ranks a ticker's mentions by engagement, mode=search filters by keyword or account, mode=news returns the token news feed, which is X posts from accounts tagged as news sources rather than articles from news outlets.",
|
|
30
30
|
"credits": "1 per call",
|
|
31
31
|
"annotations": {
|
|
32
32
|
"readOnlyHint": true,
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
{
|
|
77
77
|
"name": "account_stats",
|
|
78
78
|
"title": "Account stats",
|
|
79
|
-
"summary": "Smart follower and engagement stats for an X account.",
|
|
79
|
+
"summary": "Smart follower and engagement stats for an X account. Legacy: it still works, but will be removed on 28 October 2026.",
|
|
80
80
|
"credits": "1 per call",
|
|
81
81
|
"annotations": {
|
|
82
82
|
"readOnlyHint": true,
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
{
|
|
122
122
|
"name": "auto_validate",
|
|
123
123
|
"title": "Validate Auto query",
|
|
124
|
-
"summary": "Check EQL syntax and get a cost estimate before activating
|
|
124
|
+
"summary": "Check EQL syntax and get a cost estimate before activating, or check that a symbol has market data on a venue.",
|
|
125
125
|
"credits": "Free",
|
|
126
126
|
"annotations": {
|
|
127
127
|
"readOnlyHint": true,
|
|
@@ -131,7 +131,8 @@
|
|
|
131
131
|
},
|
|
132
132
|
"operations": [
|
|
133
133
|
"auto-validate-query-v2",
|
|
134
|
-
"auto-validate-query-draft-v2"
|
|
134
|
+
"auto-validate-query-draft-v2",
|
|
135
|
+
"auto-validate-symbol-v2"
|
|
135
136
|
]
|
|
136
137
|
},
|
|
137
138
|
{
|
|
@@ -189,27 +190,14 @@
|
|
|
189
190
|
"auto-delete-query-draft-v2",
|
|
190
191
|
"auto-convert-query-draft-v2"
|
|
191
192
|
]
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
"name": "auto_exchanges",
|
|
195
|
-
"title": "Auto exchange connections",
|
|
196
|
-
"summary": "List, connect and disconnect the exchange accounts Auto uses for order actions, and check whether a symbol is tradeable.",
|
|
197
|
-
"credits": "Free",
|
|
198
|
-
"annotations": {
|
|
199
|
-
"readOnlyHint": false,
|
|
200
|
-
"destructiveHint": true,
|
|
201
|
-
"idempotentHint": false,
|
|
202
|
-
"openWorldHint": false
|
|
203
|
-
},
|
|
204
|
-
"operations": [
|
|
205
|
-
"auto-list-exchanges-v2",
|
|
206
|
-
"auto-connect-exchange-v2",
|
|
207
|
-
"auto-disconnect-exchange-v2",
|
|
208
|
-
"auto-validate-symbol-v2"
|
|
209
|
-
]
|
|
210
193
|
}
|
|
211
194
|
],
|
|
212
195
|
"unexposed": [
|
|
196
|
+
{
|
|
197
|
+
"operationId": "getMarketEvents-v2",
|
|
198
|
+
"reason": "restricted",
|
|
199
|
+
"note": "Available only to select Enterprise customers, and the published operation takes no parameters. Contact sales@elfa.ai for access."
|
|
200
|
+
},
|
|
213
201
|
{
|
|
214
202
|
"operationId": "chat-stream-v2",
|
|
215
203
|
"reason": "sse",
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elfa-ai/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Model Context Protocol server for the Elfa API - crypto social intelligence and the Auto condition engine",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "Elfa
|
|
6
|
+
"author": "Elfa AI",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"mcpName": "ai.elfa/mcp",
|
|
9
9
|
"main": "dist/index.js",
|