equisense-research-mcp 0.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.
Files changed (3) hide show
  1. package/README.md +101 -0
  2. package/index.js +216 -0
  3. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # equisense-research MCP server
2
+
3
+ Thin Node.js wrapper that exposes the EquiSense AI equity-research endpoint as a single MCP tool:
4
+
5
+ | Tool | Endpoint | What it does |
6
+ |---|---|---|
7
+ | `ask_research` | `POST /api/v1/research/ask` | Natural-language Q&A over Indian-listed companies. Returns answer, detected company, intent, follow-up suggestions. |
8
+
9
+ Same brain the WhatsApp chat uses, just over MCP instead of WhatsApp.
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
+ ```
21
+
22
+ ## Environment variables
23
+
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. |
29
+
30
+ ### Minting the token
31
+
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):
33
+
34
+ ```bash
35
+ curl -X POST "http://localhost:8080/api/v1/admin/auth/mint-mcp-token?phoneNumber=9876543210"
36
+ ```
37
+
38
+ Response:
39
+
40
+ ```json
41
+ {
42
+ "token": "<payload>.<sig>",
43
+ "expiresAt": 1777777777,
44
+ "userId": "user-abc",
45
+ "phone": "9876543210",
46
+ "ttlSeconds": 7776000
47
+ }
48
+ ```
49
+
50
+ **Security notes:**
51
+
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.
55
+
56
+ ## Register with Claude Code
57
+
58
+ ```bash
59
+ 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
63
+ ```
64
+
65
+ Verify:
66
+
67
+ ```bash
68
+ claude mcp list
69
+ ```
70
+
71
+ Should show `equisense-research - ✓ Connected`.
72
+
73
+ ## First-run smoke test
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).
81
+
82
+ ## Tests
83
+
84
+ ```bash
85
+ npm test
86
+ ```
87
+
88
+ Runs Vitest:
89
+
90
+ - `test/unit.test.js` — TOOLS shape, fetch body shape, HTTP error mapping (401/402/403/429/5xx)
91
+ - `test/integration.test.js` — fail-fast on missing env, stdio handshake + `tools/list` RPC
92
+
93
+ ## Troubleshooting
94
+
95
+ | Symptom | Likely cause | Fix |
96
+ |---|---|---|
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 |
99
+ | `AI_EQUITY_RESEARCH quota exhausted (HTTP 402)` | Daily quota hit | Wait until tomorrow OR upgrade the user's plan |
100
+ | `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 |
package/index.js ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ // EquiSense Research MCP server.
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.
8
+ //
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.
11
+ //
12
+ // Transport: stdio. Register in Claude Code via:
13
+ // claude mcp add equisense-research --scope local \
14
+ // --env EQUISENSE_BASE_URL=http://localhost:8080 \
15
+ // --env EQUISENSE_MCP_TOKEN='<minted>' \
16
+ // -- node /abs/path/to/index.js
17
+
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import {
21
+ CallToolRequestSchema,
22
+ ListToolsRequestSchema,
23
+ } from "@modelcontextprotocol/sdk/types.js";
24
+
25
+ const BASE_URL = process.env.EQUISENSE_BASE_URL || "http://localhost:8080";
26
+ const REQUEST_TIMEOUT_MS = parseInt(
27
+ process.env.EQUISENSE_TIMEOUT_MS || "90000",
28
+ 10,
29
+ );
30
+ const MCP_TOKEN = process.env.EQUISENSE_MCP_TOKEN;
31
+
32
+ if (!MCP_TOKEN || MCP_TOKEN.trim() === "") {
33
+ 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>.",
36
+ );
37
+ process.exit(1);
38
+ }
39
+
40
+ export const TOOLS = [
41
+ {
42
+ name: "ask_research",
43
+ description:
44
+ "Ask EquiSense's AI equity-research engine a natural-language question " +
45
+ "about an Indian-listed company (NSE/BSE). Returns the answer, the " +
46
+ "detected company, the inferred query intent, and follow-up question " +
47
+ "suggestions. Pass a stable session_id to preserve multi-turn context " +
48
+ "across calls.",
49
+ inputSchema: {
50
+ type: "object",
51
+ required: ["query"],
52
+ properties: {
53
+ query: {
54
+ type: "string",
55
+ description:
56
+ "Natural-language question. Examples: 'What's the bull case for OLAELEC?', " +
57
+ "'Compare margins of TCS vs Infosys.', 'Why did HDFC Bank stock fall last week?'",
58
+ },
59
+ isin: {
60
+ type: "string",
61
+ description:
62
+ "Optional 12-char ISIN to pin the answer to a specific security. " +
63
+ "If omitted, the backend infers the company from the query text.",
64
+ },
65
+ session_id: {
66
+ type: "string",
67
+ description:
68
+ "Stable identifier for the chat session. Pass the same value across " +
69
+ "follow-up calls so the backend can apply session memory.",
70
+ },
71
+ },
72
+ },
73
+ },
74
+ ];
75
+
76
+ async function callRest(path, options = {}) {
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
79
+ try {
80
+ const response = await fetch(`${BASE_URL}${path}`, {
81
+ ...options,
82
+ signal: controller.signal,
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ Accept: "application/json",
86
+ Cookie: `ES_AUTH=${MCP_TOKEN}`,
87
+ ...(options.headers || {}),
88
+ },
89
+ });
90
+ const text = await response.text();
91
+ if (!response.ok) {
92
+ throw new Error(formatHttpError(path, response.status, text));
93
+ }
94
+ return text ? JSON.parse(text) : null;
95
+ } finally {
96
+ clearTimeout(timer);
97
+ }
98
+ }
99
+
100
+ function formatHttpError(path, status, bodyText) {
101
+ const snippet = (bodyText || "").slice(0, 500);
102
+ if (status === 401) {
103
+ return (
104
+ "Auth failed (HTTP 401) calling " +
105
+ 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."
108
+ );
109
+ }
110
+ if (status === 402) {
111
+ return (
112
+ "AI_EQUITY_RESEARCH quota exhausted (HTTP 402) calling " +
113
+ path +
114
+ ". Retry tomorrow or upgrade the plan. Body: " +
115
+ snippet
116
+ );
117
+ }
118
+ if (status === 429) {
119
+ return (
120
+ "Rate limited (HTTP 429) calling " +
121
+ path +
122
+ ". Retry after a short delay. Body: " +
123
+ snippet
124
+ );
125
+ }
126
+ if (status === 403) {
127
+ return (
128
+ "Forbidden (HTTP 403) calling " +
129
+ path +
130
+ ". The user this token represents may not have AI_EQUITY_RESEARCH access. " +
131
+ "Body: " +
132
+ snippet
133
+ );
134
+ }
135
+ return `EquiSense REST ${path} returned HTTP ${status}: ${snippet}`;
136
+ }
137
+
138
+ export async function askResearch(args) {
139
+ const body = {
140
+ query: args.query,
141
+ isin: args.isin ?? null,
142
+ sessionId: args.session_id ?? null,
143
+ };
144
+ const data = await callRest("/api/v1/research/ask", {
145
+ method: "POST",
146
+ body: JSON.stringify(body),
147
+ });
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: JSON.stringify(data, null, 2),
153
+ },
154
+ ],
155
+ };
156
+ }
157
+
158
+ const TOOL_HANDLERS = {
159
+ ask_research: askResearch,
160
+ };
161
+
162
+ function createServer() {
163
+ const server = new Server(
164
+ {
165
+ name: "equisense-research",
166
+ version: "0.1.0",
167
+ },
168
+ {
169
+ capabilities: {
170
+ tools: {},
171
+ },
172
+ },
173
+ );
174
+
175
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
176
+ tools: TOOLS,
177
+ }));
178
+
179
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
180
+ const { name, arguments: args } = request.params;
181
+ const handler = TOOL_HANDLERS[name];
182
+ if (!handler) {
183
+ throw new Error(`Unknown tool: ${name}`);
184
+ }
185
+ try {
186
+ return await handler(args);
187
+ } catch (err) {
188
+ return {
189
+ isError: true,
190
+ content: [
191
+ {
192
+ type: "text",
193
+ text: `Tool ${name} failed: ${err.message}`,
194
+ },
195
+ ],
196
+ };
197
+ }
198
+ });
199
+
200
+ return server;
201
+ }
202
+
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");
208
+
209
+ if (isEntrypoint) {
210
+ const server = createServer();
211
+ const transport = new StdioServerTransport();
212
+ await server.connect(transport);
213
+ console.error("equisense-research MCP server ready (stdio)");
214
+ }
215
+
216
+ export { createServer };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
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.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "equisense-research-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "start": "node index.js",
16
+ "test": "vitest run",
17
+ "prepublishOnly": "npm test"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "equisense",
23
+ "equity-research",
24
+ "indian-stocks",
25
+ "claude",
26
+ "ai"
27
+ ],
28
+ "author": "EquiSense",
29
+ "license": "MIT",
30
+ "homepage": "https://equisense.ai",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/shivanshu-dixit/equity-sense.git",
34
+ "directory": "mcp-server/equisense-research"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/shivanshu-dixit/equity-sense/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.0.4"
44
+ },
45
+ "devDependencies": {
46
+ "vitest": "^2.1.4"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
+ }
51
+ }