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.
- package/README.md +101 -0
- package/index.js +216 -0
- 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
|
+
}
|