equisense-research-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +82 -43
  2. package/index.js +56 -18
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # equisense-research MCP server
2
2
 
3
- Thin Node.js wrapper that exposes the EquiSense AI equity-research endpoint as a single MCP tool:
3
+ Thin Node.js MCP wrapper that exposes the EquiSense AI equity-research engine as a single tool:
4
4
 
5
5
  | Tool | Endpoint | What it does |
6
6
  |---|---|---|
@@ -8,35 +8,25 @@ Thin Node.js wrapper that exposes the EquiSense AI equity-research endpoint as a
8
8
 
9
9
  Same brain the WhatsApp chat uses, just over MCP instead of WhatsApp.
10
10
 
11
- ## Why Node?
12
-
13
- The Java app (Spring Boot 3.1) is too old for the Spring AI MCP server starter (needs 3.4+). All research logic, LLM routing, and feature metering stay server-side in Java; this wrapper just speaks MCP.
14
-
15
- ## Setup
16
-
17
- ```bash
18
- cd mcp-server/equisense-research
19
- npm install
20
- ```
11
+ Published on npm: [`equisense-research-mcp`](https://www.npmjs.com/package/equisense-research-mcp)
21
12
 
22
- ## Environment variables
13
+ ---
23
14
 
24
- | Var | Default | Purpose |
25
- |---|---|---|
26
- | `EQUISENSE_BASE_URL` | `http://localhost:8080` | Spring Boot app root |
27
- | `EQUISENSE_TIMEOUT_MS` | `90000` | Per-request timeout (research queries can take 30–90s) |
28
- | `EQUISENSE_MCP_TOKEN` | **required** | HMAC bearer token. Treat like a password. |
15
+ ## Quick start
29
16
 
30
- ### Minting the token
17
+ ### 1. Mint a token
31
18
 
32
- The token is the same shape as the user's `ES_AUTH` session cookie it grants full session access for 90 days. Mint via the admin endpoint (admin-gated by the upstream nginx filter chain in prod; reachable on localhost in dev):
19
+ The MCP authenticates via a 90-day HMAC bearer token, byte-identical to a logged-in `ES_AUTH` session cookie. Mint it via the admin endpoint (admin-IP-restricted in prod via nginx; reachable on localhost in dev):
33
20
 
34
21
  ```bash
22
+ # Prod (must be on an admin-allowlisted IP)
23
+ curl -X POST "https://equisense.ai/api/v1/admin/auth/mint-mcp-token?phoneNumber=9876543210"
24
+
25
+ # Dev
35
26
  curl -X POST "http://localhost:8080/api/v1/admin/auth/mint-mcp-token?phoneNumber=9876543210"
36
27
  ```
37
28
 
38
29
  Response:
39
-
40
30
  ```json
41
31
  {
42
32
  "token": "<payload>.<sig>",
@@ -47,22 +37,30 @@ Response:
47
37
  }
48
38
  ```
49
39
 
50
- **Security notes:**
40
+ Copy the `token`. Treat it like a password.
51
41
 
52
- - The minted token IS a full session bearer credential. Anyone with it can act as that user for 90 days.
53
- - There is no per-token denylist in v1 — the only way to revoke is to wait 90 days or rotate the global `auth.token.secret` (which logs everyone out). Don't paste this token anywhere you wouldn't paste your account password.
54
- - Never commit the token. Never log it. Never paste it into chat.
42
+ ### 2. Register the MCP with Claude Code
55
43
 
56
- ## Register with Claude Code
44
+ **Recommended (npx, no install):**
57
45
 
58
46
  ```bash
59
47
  claude mcp add equisense-research --scope local \
60
- --env EQUISENSE_BASE_URL=http://localhost:8080 \
61
- --env EQUISENSE_MCP_TOKEN='<token from mint endpoint>' \
62
- -- node /absolute/path/to/equity-sense/mcp-server/equisense-research/index.js
48
+ --env EQUISENSE_BASE_URL=https://equisense.ai \
49
+ --env EQUISENSE_MCP_TOKEN='<token from step 1>' \
50
+ -- npx -y equisense-research-mcp
63
51
  ```
64
52
 
65
- Verify:
53
+ **Alternative (global install):**
54
+
55
+ ```bash
56
+ npm install -g equisense-research-mcp
57
+ claude mcp add equisense-research --scope local \
58
+ --env EQUISENSE_BASE_URL=https://equisense.ai \
59
+ --env EQUISENSE_MCP_TOKEN='<token>' \
60
+ -- equisense-research-mcp
61
+ ```
62
+
63
+ ### 3. Verify
66
64
 
67
65
  ```bash
68
66
  claude mcp list
@@ -70,32 +68,73 @@ claude mcp list
70
68
 
71
69
  Should show `equisense-research - ✓ Connected`.
72
70
 
73
- ## First-run smoke test
71
+ ### 4. Try it
72
+
73
+ In any Claude Code session:
74
74
 
75
- 1. Make sure the Java app is running: `mvn spring-boot:run -Dspring-boot.run.profiles=dev`
76
- 2. Mint a token for your test user (see above).
77
- 3. Register the MCP with the minted token in the env.
78
- 4. In Claude Code, ask: *"Use ask_research: bull case for OLAELEC."*
79
- 5. Confirm a structured response with `answer`, `companyName`, `followUpQuestions` comes back.
80
- 6. Confirm one `AI_EQUITY_RESEARCH` event lands in the user's metering ledger (the call is metered, same as the UI path).
75
+ > Use ask_research: bull case for OLAELEC
81
76
 
82
- ## Tests
77
+ You should get back `{ answer, companyName, isin, detectedIntent, followUpQuestions, responseTimeMs }`.
78
+
79
+ ---
80
+
81
+ ## Environment variables
82
+
83
+ | Var | Default | Purpose |
84
+ |---|---|---|
85
+ | `EQUISENSE_BASE_URL` | `http://localhost:8080` | Backend root URL |
86
+ | `EQUISENSE_TIMEOUT_MS` | `90000` | Per-request timeout (research queries can take 30–90s) |
87
+ | `EQUISENSE_MCP_TOKEN` | **required** | HMAC bearer token. Treat like a password. |
88
+
89
+ ## Security notes
90
+
91
+ - The minted token grants **full session access** for 90 days, scope-equivalent to a logged-in browser session for that user. Not scoped to research-only.
92
+ - There is no per-token revocation in v1. The only way to invalidate a token early is to rotate the global `auth.token.secret` (which logs out every user).
93
+ - Never commit the token. Never log it. Never paste it into chat.
94
+ - Each `ask_research` call counts against the represented user's `AI_EQUITY_RESEARCH` daily quota — same metering as the web UI.
95
+
96
+ ---
97
+
98
+ ## Contributing / running from source
83
99
 
84
100
  ```bash
101
+ git clone git@github.com:shivanshu-dixit/equity-sense.git
102
+ cd equity-sense/mcp-server/equisense-research
103
+ npm install
85
104
  npm test
86
105
  ```
87
106
 
88
- Runs Vitest:
89
-
90
- - `test/unit.test.js` — TOOLS shape, fetch body shape, HTTP error mapping (401/402/403/429/5xx)
107
+ Tests:
108
+ - `test/unit.test.js` — TOOLS shape, fetch body, HTTP error mapping (401/402/403/429/5xx)
91
109
  - `test/integration.test.js` — fail-fast on missing env, stdio handshake + `tools/list` RPC
92
110
 
111
+ Local registration against from-source code (instead of npm):
112
+
113
+ ```bash
114
+ claude mcp add equisense-research --scope local \
115
+ --env EQUISENSE_BASE_URL=http://localhost:8080 \
116
+ --env EQUISENSE_MCP_TOKEN='<token>' \
117
+ -- node /absolute/path/to/equity-sense/mcp-server/equisense-research/index.js
118
+ ```
119
+
120
+ ## Why Node?
121
+
122
+ The Java app (Spring Boot 3.1) is too old for the Spring AI MCP server starter (needs 3.4+). All research logic, LLM routing, and feature metering stay server-side in Java; this wrapper just speaks MCP.
123
+
124
+ ---
125
+
93
126
  ## Troubleshooting
94
127
 
95
128
  | Symptom | Likely cause | Fix |
96
129
  |---|---|---|
97
- | `FATAL: EQUISENSE_MCP_TOKEN env var is required` on startup | env var unset or empty | Pass `--env EQUISENSE_MCP_TOKEN=...` to `claude mcp add` |
98
- | `Auth failed (HTTP 401) ... EQUISENSE_MCP_TOKEN is expired or invalid` | Token > 90 days old OR `auth.token.secret` rotated | Re-mint and update the env |
130
+ | `FATAL: EQUISENSE_MCP_TOKEN env var is required` on startup | Env var unset or empty | Pass `--env EQUISENSE_MCP_TOKEN=...` to `claude mcp add` |
131
+ | `Auth failed (HTTP 401) ... re-mint via /api/v1/admin/auth/mint-mcp-token` | Token > 90 days old OR `auth.token.secret` rotated | Re-mint and update the env |
99
132
  | `AI_EQUITY_RESEARCH quota exhausted (HTTP 402)` | Daily quota hit | Wait until tomorrow OR upgrade the user's plan |
133
+ | `Rate limited (HTTP 429)` | Transient | Retry in a few seconds |
100
134
  | `Forbidden (HTTP 403)` | User doesn't have `AI_EQUITY_RESEARCH` feature | Check the user's license/plan |
101
- | Hangs > 90s with no response | Backend research query timed out | Bump `EQUISENSE_TIMEOUT_MS`; check Spring Boot logs |
135
+ | Hangs > 90s with no response | Backend research query timed out | Bump `EQUISENSE_TIMEOUT_MS`; check backend logs |
136
+ | `403` minting in prod | Your IP isn't admin-allowlisted | SSH to prod box and mint from there, or have an admin mint for you |
137
+
138
+ ## License
139
+
140
+ MIT
package/index.js CHANGED
@@ -1,18 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  // EquiSense Research MCP server.
3
3
  //
4
- // Single tool: ask_research — proxies POST /api/v1/research/ask. The Spring Boot
5
- // app authenticates the caller via a Cookie: ES_AUTH=<token> header. The token
6
- // is minted via POST /api/v1/admin/auth/mint-mcp-token (admin-only) and passed
7
- // in here through the EQUISENSE_MCP_TOKEN env var.
4
+ // Single tool: ask_research — proxies POST /api/v1/research/ask. The user mints a
5
+ // scoped, revocable token in the web app (Settings Claude) and passes it here
6
+ // via the EQUISENSE_MCP_TOKEN env var.
8
7
  //
9
- // Treat the token like a password it grants full session access to that user
10
- // for SESSION_TTL_SECONDS (90 days). Never log it. Never commit it.
8
+ // Auth: we send the token BOTH as `Authorization: Bearer <token>` and as a
9
+ // `Cookie: ES_AUTH=<token>` header. New self-serve tokens are RESEARCH-scoped and
10
+ // authenticate via the Bearer path (McpAuthFilter); legacy admin-minted full-session
11
+ // tokens authenticate via the cookie path (PersistentAuthFilter). Sending both means
12
+ // either kind of token works with no client-side branching — the backend ignores the
13
+ // header that doesn't apply.
14
+ //
15
+ // Treat the token like a password. A scoped token grants read-only research access;
16
+ // revoke it any time in Settings → Claude. Never log it. Never commit it.
11
17
  //
12
18
  // Transport: stdio. Register in Claude Code via:
13
19
  // claude mcp add equisense-research --scope local \
14
- // --env EQUISENSE_BASE_URL=http://localhost:8080 \
15
- // --env EQUISENSE_MCP_TOKEN='<minted>' \
20
+ // --env EQUISENSE_BASE_URL=https://equisense.ai \
21
+ // --env EQUISENSE_MCP_TOKEN='<token from Settings → Claude>' \
16
22
  // -- node /abs/path/to/index.js
17
23
 
18
24
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -31,12 +37,18 @@ const MCP_TOKEN = process.env.EQUISENSE_MCP_TOKEN;
31
37
 
32
38
  if (!MCP_TOKEN || MCP_TOKEN.trim() === "") {
33
39
  console.error(
34
- "FATAL: EQUISENSE_MCP_TOKEN env var is required. Mint one via " +
35
- "POST /api/v1/admin/auth/mint-mcp-token?phoneNumber=<userPhone>.",
40
+ "FATAL: EQUISENSE_MCP_TOKEN env var is required. Generate one in the " +
41
+ "EquiSense web app under Settings → Claude.",
36
42
  );
37
43
  process.exit(1);
38
44
  }
39
45
 
46
+ // Client identity from the MCP initialize handshake (e.g. "claude-ai",
47
+ // "Claude Code"). Captured once, forwarded as X-MCP-Client so the backend can
48
+ // auto-name the connection in Settings → Claude. Null until the handshake lands
49
+ // and when askResearch is exercised directly in unit tests.
50
+ let clientName = null;
51
+
40
52
  export const TOOLS = [
41
53
  {
42
54
  name: "ask_research",
@@ -83,7 +95,10 @@ async function callRest(path, options = {}) {
83
95
  headers: {
84
96
  "Content-Type": "application/json",
85
97
  Accept: "application/json",
98
+ // Bearer for new scoped tokens; Cookie for legacy session tokens. See header note.
99
+ Authorization: `Bearer ${MCP_TOKEN}`,
86
100
  Cookie: `ES_AUTH=${MCP_TOKEN}`,
101
+ ...(clientName ? { "X-MCP-Client": clientName } : {}),
87
102
  ...(options.headers || {}),
88
103
  },
89
104
  });
@@ -103,8 +118,8 @@ function formatHttpError(path, status, bodyText) {
103
118
  return (
104
119
  "Auth failed (HTTP 401) calling " +
105
120
  path +
106
- ". EQUISENSE_MCP_TOKEN is expired or invalid. Re-mint via " +
107
- "POST /api/v1/admin/auth/mint-mcp-token and update the env var."
121
+ ". EQUISENSE_MCP_TOKEN is expired, revoked, or invalid. Generate a fresh one " +
122
+ "in the EquiSense web app under Settings → Claude and update the env var."
108
123
  );
109
124
  }
110
125
  if (status === 402) {
@@ -163,7 +178,7 @@ function createServer() {
163
178
  const server = new Server(
164
179
  {
165
180
  name: "equisense-research",
166
- version: "0.1.0",
181
+ version: "0.2.0",
167
182
  },
168
183
  {
169
184
  capabilities: {
@@ -172,6 +187,16 @@ function createServer() {
172
187
  },
173
188
  );
174
189
 
190
+ // Capture the client identity once the initialize handshake completes, so
191
+ // callRest can forward it as X-MCP-Client for auto-naming the connection.
192
+ server.oninitialized = () => {
193
+ try {
194
+ clientName = server.getClientVersion()?.name || null;
195
+ } catch {
196
+ clientName = null;
197
+ }
198
+ };
199
+
175
200
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
176
201
  tools: TOOLS,
177
202
  }));
@@ -200,11 +225,24 @@ function createServer() {
200
225
  return server;
201
226
  }
202
227
 
203
- // Only start the server when this file is the entrypoint. Lets the test suite
204
- // import TOOLS/askResearch without spawning a stdio server.
205
- const isEntrypoint =
206
- import.meta.url === `file://${process.argv[1]}` ||
207
- process.argv[1]?.endsWith("index.js");
228
+ // Only start the server when this file is the entrypoint. Resolve symlinks on
229
+ // both sides so the check survives npx, global bin installs, and pnpm shims.
230
+ // Without realpath, a `bin` entry symlinked to index.js shows up in argv[1] as
231
+ // `<cache>/.bin/equisense-research-mcp` — not ending in index.js — and the
232
+ // server silently never starts.
233
+ import { realpathSync } from "node:fs";
234
+ import { fileURLToPath } from "node:url";
235
+
236
+ const isEntrypoint = (() => {
237
+ try {
238
+ if (!process.argv[1]) return false;
239
+ const thisFile = realpathSync(fileURLToPath(import.meta.url));
240
+ const argvFile = realpathSync(process.argv[1]);
241
+ return thisFile === argvFile;
242
+ } catch {
243
+ return false;
244
+ }
245
+ })();
208
246
 
209
247
  if (isEntrypoint) {
210
248
  const server = createServer();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "equisense-research-mcp",
3
- "version": "0.1.0",
4
- "description": "MCP server wrapper for the EquiSense AI equity-research API. Exposes a single ask_research tool that proxies POST /api/v1/research/ask, authenticated via a minted ES_AUTH bearer token.",
3
+ "version": "0.2.0",
4
+ "description": "MCP server wrapper for the EquiSense AI equity-research API. Exposes a single ask_research tool that proxies POST /api/v1/research/ask, authenticated via a scoped, revocable token minted in Settings → Claude.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {