@elfa-ai/mcp 4.3.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.5.0
4
+
5
+ ### Added
6
+
7
+ - **`market_chat` returns its sources.** The reply now includes `sources`: the links the research behind the answer read (X posts, news, DEX and explorer pages, prediction markets), in the order it read them, each with a `url`, a `kind` and an optional `title` such as "Searched X". Links only, never the page or post text. A source was read, not necessarily cited in `message`, and the list is empty when the answer used no linkable source. The tool asks for them with `includeSources: true` on `POST /v2/chat` (API 2.8.1); it costs nothing extra. `kind` is a plain string, so treat a value you don't recognise as `web`.
8
+
9
+ ## 4.4.0
10
+
11
+ ### Added
12
+
13
+ - **Upstream requests carry an `elfa-mcp` User-Agent.** Every upstream call now carries `User-Agent: elfa-mcp/<version> (<transport>)`, where the transport is `stdio` or `http`, instead of the SDK's generic `@elfa-ai/sdk/<version>`. When the server knows which app is connected, it adds that too: on HTTP, the client's own `User-Agent`; on stdio, the `clientInfo` name and version from the MCP handshake, e.g. `elfa-mcp/4.4.0 (stdio; client=claude-ai/0.1.0)`. The client part is cut to printable ASCII and 100 characters, with `(`, `)`, `;` and `\` turned into spaces so the comment stays parseable. A `User-Agent` set in `ELFA_EXTRA_HEADERS`, in any casing, still wins.
14
+ - **The HTTP server logs each handshake.** One JSON line per successful `initialize` goes to stderr, `{"event":"initialize","client":{"name":…,"version":…}}`, so you can see which apps connect even when they never call a tool. Nothing else from the request is logged.
15
+
3
16
  ## 4.3.0
4
17
 
5
18
  ### Changed
package/README.md CHANGED
@@ -53,6 +53,8 @@ Ask *"what's trending in crypto right now?"* to confirm it works.
53
53
  | `ELFA_MCP_MAX_RESPONSE_CHARS` | no | Response size ceiling, default `60000` |
54
54
  | `ELFA_EXTRA_HEADERS` | no | JSON object of extra headers to send upstream, for proxies and non-production environments |
55
55
 
56
+ Upstream requests identify themselves as `User-Agent: elfa-mcp/<version> (<transport>; client=<app>)`. On stdio, `<app>` is the MCP client's name and version from the handshake; over HTTP, it is the connecting client's own `User-Agent` header. Set `User-Agent` in `ELFA_EXTRA_HEADERS` to send your own instead.
57
+
56
58
  The timeout is high and retries are off on purpose. The interpretation endpoints are LLM-backed and can take over a minute, and they cost credits per attempt, so a silent retry would bill you again for a call you never saw. Raise `ELFA_RETRIES` only if you are calling the cheap measurement endpoints.
57
59
 
58
60
  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.
package/dist/index.js CHANGED
@@ -52,6 +52,103 @@ function describeError(error) {
52
52
  return detail;
53
53
  }
54
54
 
55
+ // package.json
56
+ var package_default = {
57
+ name: "@elfa-ai/mcp",
58
+ version: "4.5.0",
59
+ description: "Model Context Protocol server for the Elfa API - crypto social intelligence and the Auto condition engine",
60
+ license: "MIT",
61
+ author: "Elfa AI",
62
+ type: "module",
63
+ mcpName: "ai.elfa/mcp",
64
+ main: "dist/index.js",
65
+ types: "dist/index.d.ts",
66
+ bin: {
67
+ "elfa-mcp": "dist/index.js"
68
+ },
69
+ files: [
70
+ "dist/**/*",
71
+ "manifest.json",
72
+ "README.md",
73
+ "CHANGELOG.md",
74
+ "LICENSE"
75
+ ],
76
+ engines: {
77
+ node: ">=20.0.0"
78
+ },
79
+ publishConfig: {
80
+ access: "public",
81
+ registry: "https://registry.npmjs.org"
82
+ },
83
+ scripts: {
84
+ build: "tsup",
85
+ dev: "tsx watch src/index.ts",
86
+ start: "node dist/index.js",
87
+ typecheck: "tsc --noEmit",
88
+ test: "vitest run",
89
+ "test:watch": "vitest",
90
+ "check:drift": "tsx scripts/check-drift.ts",
91
+ "update:schema": "curl -fsSL -o swagger.json https://docs.elfa.ai/swagger/swagger.json",
92
+ "docs:tools": "tsx scripts/gen-tool-table.ts",
93
+ "docs:check": "tsx scripts/gen-tool-table.ts --check",
94
+ verify: "npm run typecheck && npm run test && npm run check:drift && npm run docs:check",
95
+ prepublishOnly: "npm run build",
96
+ "build:bundle": "tsx scripts/build-bundle.ts"
97
+ },
98
+ keywords: [
99
+ "elfa",
100
+ "mcp",
101
+ "model-context-protocol",
102
+ "crypto",
103
+ "social-intelligence",
104
+ "trading",
105
+ "agent"
106
+ ],
107
+ repository: {
108
+ type: "git",
109
+ url: "git+https://github.com/elfa-ai/mcp.git"
110
+ },
111
+ bugs: {
112
+ url: "https://github.com/elfa-ai/mcp/issues"
113
+ },
114
+ homepage: "https://docs.elfa.ai",
115
+ dependencies: {
116
+ "@elfa-ai/sdk": "^3.0.1",
117
+ "@modelcontextprotocol/sdk": "^1.20.0",
118
+ express: "^5.1.0",
119
+ zod: "^3.25.0"
120
+ },
121
+ devDependencies: {
122
+ "@anthropic-ai/mcpb": "^2.1.2",
123
+ "@types/express": "^5.0.0",
124
+ "@types/node": "^22.10.0",
125
+ tsup: "^8.5.1",
126
+ tsx: "^4.19.0",
127
+ typescript: "^5.9.0",
128
+ vitest: "^3.0.0"
129
+ }
130
+ };
131
+
132
+ // src/userAgent.ts
133
+ var PRODUCT = "elfa-mcp";
134
+ var CLIENT_MAX_LENGTH = 100;
135
+ function sanitizeClient(value) {
136
+ if (!value) return void 0;
137
+ const clean = value.replace(/[^\x20-\x7e]|[();\\]/g, " ").replace(/\s+/g, " ").trim().slice(0, CLIENT_MAX_LENGTH).trim();
138
+ return clean || void 0;
139
+ }
140
+ function userAgent(transport, client) {
141
+ const name = sanitizeClient(client);
142
+ const detail = name ? `${transport}; client=${name}` : transport;
143
+ return `${PRODUCT}/${package_default.version} (${detail})`;
144
+ }
145
+ function withUserAgent(headers2, value) {
146
+ const operatorSetsOne = Object.keys(headers2 ?? {}).some(
147
+ (name) => name.toLowerCase() === "user-agent"
148
+ );
149
+ return operatorSetsOne ? { ...headers2 } : { "User-Agent": value, ...headers2 };
150
+ }
151
+
55
152
  // src/client.ts
56
153
  var CredentialError = class extends Error {
57
154
  };
@@ -65,13 +162,25 @@ function buildDeps(config, overrides = {}) {
65
162
  timeout: config.timeout,
66
163
  retries: config.retries,
67
164
  ...config.baseUrl ? { baseUrl: config.baseUrl } : {},
68
- ...config.extraHeaders ? { headers: config.extraHeaders } : {}
165
+ headers: withUserAgent(
166
+ config.extraHeaders,
167
+ userAgent(config.transport, overrides.client)
168
+ )
69
169
  });
70
170
  return {
71
171
  sdk,
72
172
  maxResponseChars: config.maxResponseChars
73
173
  };
74
174
  }
175
+ function identifyClientOnInitialize(server, deps, config) {
176
+ server.server.oninitialized = () => {
177
+ const info = server.server.getClientVersion();
178
+ if (!info) return;
179
+ deps.sdk = buildDeps(config, {
180
+ client: `${info.name}/${info.version}`
181
+ }).sdk;
182
+ };
183
+ }
75
184
 
76
185
  // src/config.ts
77
186
  var DEFAULT_PORT = 3e3;
@@ -170,83 +279,6 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
170
279
  // src/server.ts
171
280
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
172
281
 
173
- // package.json
174
- var package_default = {
175
- name: "@elfa-ai/mcp",
176
- version: "4.3.0",
177
- description: "Model Context Protocol server for the Elfa API - crypto social intelligence and the Auto condition engine",
178
- license: "MIT",
179
- author: "Elfa AI",
180
- type: "module",
181
- mcpName: "ai.elfa/mcp",
182
- main: "dist/index.js",
183
- types: "dist/index.d.ts",
184
- bin: {
185
- "elfa-mcp": "dist/index.js"
186
- },
187
- files: [
188
- "dist/**/*",
189
- "manifest.json",
190
- "README.md",
191
- "CHANGELOG.md",
192
- "LICENSE"
193
- ],
194
- engines: {
195
- node: ">=20.0.0"
196
- },
197
- publishConfig: {
198
- access: "public",
199
- registry: "https://registry.npmjs.org"
200
- },
201
- scripts: {
202
- build: "tsup",
203
- dev: "tsx watch src/index.ts",
204
- start: "node dist/index.js",
205
- typecheck: "tsc --noEmit",
206
- test: "vitest run",
207
- "test:watch": "vitest",
208
- "check:drift": "tsx scripts/check-drift.ts",
209
- "update:schema": "curl -fsSL -o swagger.json https://docs.elfa.ai/swagger/swagger.json",
210
- "docs:tools": "tsx scripts/gen-tool-table.ts",
211
- "docs:check": "tsx scripts/gen-tool-table.ts --check",
212
- verify: "npm run typecheck && npm run test && npm run check:drift && npm run docs:check",
213
- prepublishOnly: "npm run build",
214
- "build:bundle": "tsx scripts/build-bundle.ts"
215
- },
216
- keywords: [
217
- "elfa",
218
- "mcp",
219
- "model-context-protocol",
220
- "crypto",
221
- "social-intelligence",
222
- "trading",
223
- "agent"
224
- ],
225
- repository: {
226
- type: "git",
227
- url: "git+https://github.com/elfa-ai/mcp.git"
228
- },
229
- bugs: {
230
- url: "https://github.com/elfa-ai/mcp/issues"
231
- },
232
- homepage: "https://docs.elfa.ai",
233
- dependencies: {
234
- "@elfa-ai/sdk": "^3.0.1",
235
- "@modelcontextprotocol/sdk": "^1.20.0",
236
- express: "^5.1.0",
237
- zod: "^3.25.0"
238
- },
239
- devDependencies: {
240
- "@anthropic-ai/mcpb": "^2.1.2",
241
- "@types/express": "^5.0.0",
242
- "@types/node": "^22.10.0",
243
- tsup: "^8.5.1",
244
- tsx: "^4.19.0",
245
- typescript: "^5.9.0",
246
- vitest: "^3.0.0"
247
- }
248
- };
249
-
250
282
  // src/entitlements.ts
251
283
  import { createHash } from "crypto";
252
284
 
@@ -853,12 +885,23 @@ function registerAutoValidate(server, deps) {
853
885
 
854
886
  // src/tools/chat.ts
855
887
  import { z as z8 } from "zod";
888
+ function readSources(value) {
889
+ if (!Array.isArray(value)) return [];
890
+ const sources = [];
891
+ for (const item of value) {
892
+ if (typeof item !== "object" || item === null) continue;
893
+ const { url, kind, title } = item;
894
+ if (typeof url !== "string" || typeof kind !== "string") continue;
895
+ sources.push(typeof title === "string" ? { url, kind, title } : { url, kind });
896
+ }
897
+ return sources;
898
+ }
856
899
  function registerChat(server, deps) {
857
900
  return server.registerTool(
858
901
  "market_chat",
859
902
  {
860
903
  title: "Market chat",
861
- 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.",
904
+ 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. The reply includes sources: links to the posts and pages the research read (links only, not their text), so you can cite or check them.",
862
905
  inputSchema: {
863
906
  analysisType: z8.enum([
864
907
  "chat",
@@ -883,7 +926,16 @@ function registerChat(server, deps) {
883
926
  outputSchema: {
884
927
  message: z8.string(),
885
928
  sessionId: z8.string().nullable(),
886
- creditsConsumed: z8.number().nullable()
929
+ creditsConsumed: z8.number().nullable(),
930
+ sources: z8.array(
931
+ z8.object({
932
+ url: z8.string(),
933
+ kind: z8.string(),
934
+ title: z8.string().optional()
935
+ })
936
+ ).describe(
937
+ "Links the research read, in order, without duplicates. kind is x_post, x_account, telegram, dex, explorer, prediction_market or web; treat any other value as web. A source was read, not necessarily cited."
938
+ )
887
939
  },
888
940
  annotations: {
889
941
  readOnlyHint: true,
@@ -918,13 +970,16 @@ function registerChat(server, deps) {
918
970
  speed: args.speed,
919
971
  message: args.message,
920
972
  sessionId: args.sessionId,
921
- assetMetadata: Object.keys(assetMetadata).length > 0 ? assetMetadata : void 0
973
+ assetMetadata: Object.keys(assetMetadata).length > 0 ? assetMetadata : void 0,
974
+ includeSources: true
922
975
  })
923
976
  );
977
+ const data = response.data;
924
978
  return {
925
- message: response.data.message,
926
- sessionId: response.data.sessionId ?? null,
927
- creditsConsumed: response.data.creditsConsumed ?? null
979
+ message: data.message,
980
+ sessionId: data.sessionId ?? null,
981
+ creditsConsumed: data.creditsConsumed ?? null,
982
+ sources: readSources(data.sources)
928
983
  };
929
984
  });
930
985
  }
@@ -1446,6 +1501,9 @@ function header(req, name) {
1446
1501
  const value = req.headers[name];
1447
1502
  return Array.isArray(value) ? value[0] : value;
1448
1503
  }
1504
+ function clientField(value) {
1505
+ return sanitizeClient(typeof value === "string" ? value : void 0) ?? "unknown";
1506
+ }
1449
1507
  function jsonRpcError(res, status, message) {
1450
1508
  res.status(status).json({
1451
1509
  jsonrpc: "2.0",
@@ -1466,6 +1524,8 @@ function createHttpApp(config, options = {}) {
1466
1524
  const oauth = config.oauth;
1467
1525
  const verifier = options.verifier ?? (oauth ? new TokenVerifier(oauth) : void 0);
1468
1526
  const loadEntitlements = options.entitlements ?? fetchEntitlements;
1527
+ const log = options.log ?? ((line) => process.stderr.write(`${line}
1528
+ `));
1469
1529
  const entitlementCache = new EntitlementCache();
1470
1530
  app.disable("x-powered-by");
1471
1531
  app.use(express.json({ limit: "4mb" }));
@@ -1533,7 +1593,10 @@ function createHttpApp(config, options = {}) {
1533
1593
  try {
1534
1594
  const apiKey = await resolveApiKey(req, res);
1535
1595
  if (apiKey === void 0) return;
1536
- const deps = buildDeps(config, { apiKey: apiKey ?? void 0 });
1596
+ const deps = buildDeps(config, {
1597
+ apiKey: apiKey ?? void 0,
1598
+ client: header(req, "user-agent")
1599
+ });
1537
1600
  const entitlements = apiKey ? await entitlementCache.get(apiKey, () => loadEntitlements(deps)) : void 0;
1538
1601
  server = createServer(deps, entitlements);
1539
1602
  transport = new StreamableHTTPServerTransport({
@@ -1541,6 +1604,13 @@ function createHttpApp(config, options = {}) {
1541
1604
  allowedHosts: config.allowedHosts.length > 0 ? config.allowedHosts : defaultAllowedHosts(config),
1542
1605
  ...config.allowedOrigins.length > 0 ? { allowedOrigins: config.allowedOrigins } : {}
1543
1606
  });
1607
+ res.on("finish", () => {
1608
+ if (res.statusCode >= 400) return;
1609
+ const info = server?.server.getClientVersion();
1610
+ if (!info) return;
1611
+ const client = { name: clientField(info.name), version: clientField(info.version) };
1612
+ log(JSON.stringify({ event: "initialize", client }));
1613
+ });
1544
1614
  res.on("close", () => {
1545
1615
  void transport?.close();
1546
1616
  void server?.close();
@@ -1581,6 +1651,7 @@ async function main() {
1581
1651
  }
1582
1652
  const deps = buildDeps(config);
1583
1653
  const server = createServer(deps, await fetchEntitlements(deps));
1654
+ identifyClientOnInitialize(server, deps, config);
1584
1655
  await server.connect(new StdioServerTransport());
1585
1656
  }
1586
1657
  main().catch((error) => {
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "server": {
3
3
  "name": "elfa",
4
- "version": "4.3.0",
4
+ "version": "4.5.0",
5
5
  "docs": "https://docs.elfa.ai",
6
6
  "spec": "https://docs.elfa.ai/swagger/swagger.json",
7
7
  "specScope": "/v2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elfa-ai/mcp",
3
- "version": "4.3.0",
3
+ "version": "4.5.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
6
  "author": "Elfa AI",