@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 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
- 12 tools, mapped to every documented `/v2` operation.
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. Accepts an inline query or a draft id. |
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, or place an order on a connected exchange. Order actions need `ELFA_HMAC_SECRET` and a connected exchange.
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` and `x-elfa-hmac-secret` request headers, falling back to the environment. Set `ELFA_MCP_ALLOWED_ORIGINS` to a comma separated allowlist when exposing it publicly.
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` or `auto_exchanges`.
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(what) {
22
- if (what === "apiKey") {
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, and ELFA_HMAC_SECRET if this action requires signing.`;
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("apiKey"));
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: "2.0.0",
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 Team",
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 z6 } from "zod";
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: z6.enum([
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: z6.string().optional().describe("Required for get, sessions and session."),
653
- sessionId: z6.string().optional().describe("Required for method=session."),
654
- executionId: z6.string().optional().describe("Required for method=execution."),
655
- status: z6.string().optional().describe("Filter by status."),
656
- type: z6.string().optional().describe("Filter executions by type."),
657
- search: z6.string().optional().describe("Free text filter for method=list."),
658
- limit: z6.number().int().min(1).max(50).default(10).describe("Page size for list style methods."),
659
- offset: z6.number().int().min(0).default(0).describe("Pagination 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: z6.string(),
663
- data: z6.unknown(),
664
- pollAfterSeconds: z6.number().nullable()
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 z7 } from "zod";
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. Queries whose action places an order also need request signing.",
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: z7.enum(["create", "cancel", "delete"]).describe(
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: z7.string().max(120).optional().describe("Short title shown in the notification when this fires."),
756
- description: z7.string().max(500).optional().describe("Why this was set up, shown alongside the title."),
757
- queryId: z7.string().optional().describe("Required for cancel and delete.")
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: z7.string(),
761
- data: z7.unknown()
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 z8 } from "zod";
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: z8.string().optional().describe("Validate a stored draft instead of an inline query."),
810
- title: z8.string().max(120).optional().describe("Title to validate with the query."),
811
- description: z8.string().max(500).optional().describe("Description to validate with the query.")
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: z8.boolean(),
815
- errors: z8.array(z8.unknown()),
816
- warnings: z8.array(z8.unknown()),
817
- estimatedCredits: z8.number().nullable(),
818
- estimatedCost: z8.record(z8.unknown()).nullable()
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 z9 } from "zod";
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: z9.enum([
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: z9.string().optional().describe("The question, required for analysisType=chat."),
871
- sessionId: z9.string().optional().describe("Continue an earlier conversation."),
872
- speed: z9.enum(["fast", "expert", "adaptive"]).default("fast").describe(
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: z9.string().optional().describe("Token symbol, for token analysis."),
876
- chain: z9.string().optional().describe("Chain, paired with contractAddress."),
877
- contractAddress: z9.string().optional().describe("Contract address, paired with chain."),
878
- username: z9.string().optional().describe("X username, for analysisType=accountAnalysis.")
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: z9.string(),
882
- sessionId: z9.string().nullable(),
883
- creditsConsumed: z9.number().nullable()
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 z10 } from "zod";
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: z10.enum(["top", "search", "news"]).describe(
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: z10.string().optional().describe('Ticker for mode=top, for example "BTC" or "$SOL".'),
944
- keywords: z10.string().optional().describe("Up to 5 comma separated keywords for mode=search."),
945
- accountName: z10.string().optional().describe("X username for mode=search, without the @."),
946
- coinIds: z10.string().optional().describe("Comma separated CoinGecko coin ids to filter mode=news."),
947
- searchType: z10.enum(["and", "or"]).optional().describe("How multiple keywords combine in mode=search."),
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: z10.number().int().optional().describe(
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: z10.number().int().min(1).max(30).default(10).describe("Result count for mode=search."),
956
- cursor: z10.string().optional().describe("Pagination cursor from a previous mode=search response."),
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: z10.string(),
962
- notice: z10.string(),
963
- total: z10.number().nullable(),
964
- next: z10.string().nullable(),
965
- mentions: z10.array(z10.record(z10.unknown()))
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 z11 } from "zod";
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: z11.enum(["market", "keywords"]).default("market").describe("market is a broad sweep. keywords needs the keywords argument."),
1080
- keywords: z11.string().optional().describe("Up to 5 comma separated keywords for scope=keywords."),
1081
- searchType: z11.enum(["and", "or"]).optional().describe("How multiple keywords combine for scope=keywords."),
1082
- timeFrame: z11.enum(["day", "week"]).optional().describe("Lookback for scope=market."),
1083
- maxNarratives: z11.number().int().min(1).max(20).optional().describe("Cap the narratives returned for scope=market."),
1084
- maxTweetsPerNarrative: z11.number().int().min(1).max(20).optional().describe("Cap the sources per narrative for scope=market."),
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: z11.string(),
1091
- notice: z11.string(),
1092
- items: z11.array(z11.record(z11.unknown())),
1093
- matched: z11.number().nullable(),
1094
- note: z11.string().nullable()
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 z12 } from "zod";
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: z12.boolean(),
1204
- key: z12.record(z12.unknown()).nullable(),
1205
- problem: z12.string().nullable()
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 z13 } from "zod";
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: z13.enum(["tokens", "contracts_twitter", "contracts_telegram"]).default("tokens").describe("Which trending list to return."),
1238
- timeWindow: z13.string().optional().describe(
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: z13.number().int().min(1).default(5).describe("Drop entries below this mention count.")
1143
+ minMentions: z12.number().int().min(1).default(5).describe("Drop entries below this mention count.")
1246
1144
  },
1247
1145
  outputSchema: {
1248
- scope: z13.string(),
1249
- page: z13.number().nullable(),
1250
- pageSize: z13.number().nullable(),
1251
- total: z13.number().nullable(),
1252
- items: z13.array(z13.record(z13.unknown()))
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. Actions that place orders, and connecting an exchange, need ELFA_HMAC_SECRET set.
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
- ...config.allowedOrigins.length > 0 ? { allowedOrigins: config.allowedOrigins, enableDnsRebindingProtection: true } : {}
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": "2.0.0",
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. Accepts an inline query or a draft id.",
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": "2.0.0",
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 Team",
6
+ "author": "Elfa AI",
7
7
  "type": "module",
8
8
  "mcpName": "ai.elfa/mcp",
9
9
  "main": "dist/index.js",