bharatstock-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/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to `bharatstock-mcp` are documented here (newest first).
4
+
5
+ ## 0.1.0 (2026-09-22)
6
+
7
+ - Initial release.
8
+ - Local **stdio** server (`bharatstock-mcp`) and remote **Streamable HTTP**
9
+ server (`bharatstock-mcp-http`) wrapping the official `bharatstock` JS SDK.
10
+ - Remote server is multi-tenant (per-request key) and accepts the key on
11
+ `Authorization: Bearer` or `X-API-Key`.
12
+ - MCP is a Developer/Pro feature, gated server-side via `/v1/mcp/authorize`
13
+ (checked at stdio startup and on every remote request).
14
+ - Tools: `get_stock_quote`, `get_company_profile`, `get_stock_quotes_batch`,
15
+ `get_stock_prices`, `get_financials`, `get_stock_fundamentals`,
16
+ `get_technical_indicators`, `get_shareholding`, `get_corporate_actions`,
17
+ `get_stock_bulk_deals`, `get_stock_block_deals`, `get_stock_insider_trades`,
18
+ `compare_sector_peers`, `search_stocks`, `get_stock_rank`,
19
+ `get_price_shockers`, `run_screener`, `list_indices`, `get_index_prices`,
20
+ `get_fii_dii_activity`, `get_bulk_deals`, `get_block_deals`,
21
+ `get_insider_trades`, `search_mutual_fund_schemes`,
22
+ `get_mutual_fund_scheme`, `get_mutual_fund_nav`, `get_mutual_fund_returns`,
23
+ `get_mutual_fund_holdings`, `get_data_status`.
24
+ - Reads `BHARATSTOCK_API_KEY` and optional `BHARATSTOCK_BASE_URL` from env.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BharatStock
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # bharatstock-mcp
2
+
3
+ Official **Model Context Protocol (MCP)** server for the [BharatStock API](https://bharatstockapi.com) โ€” reliable Indian stock market data (NSE/BSE) exposed as tools your AI assistant can call directly.
4
+
5
+ Point any MCP host (Claude Desktop, Cursor, Kiro, VS Code, etc.) at this server and your model can ask for live quotes, financials, mutual-fund NAVs, screener results and more โ€” no code required.
6
+
7
+ ๐Ÿ“‹ **[Jump to Changelog](#changelog)**
8
+
9
+ ## What you need
10
+
11
+ - Node.js 20+
12
+ - A BharatStock API key (`bsk_live_...`) on the **Developer or Pro plan** โ€” MCP access is a paid feature. The server verifies your plan on startup and refuses to run for Free/Starter keys. Your plan's rate limit and history depth apply exactly as they do over REST. Sign up / upgrade at [bharatstockapi.com](https://bharatstockapi.com/#pricing).
13
+
14
+ ## Setup
15
+
16
+ Add the server to your MCP host's config. Most hosts use a `mcpServers` block:
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "bharatstock": {
22
+ "command": "npx",
23
+ "args": ["-y", "bharatstock-mcp"],
24
+ "env": { "BHARATSTOCK_API_KEY": "bsk_live_your_key_here" }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ `npx -y bharatstock-mcp` downloads and runs the server on demand โ€” no global install needed. Restart your host and the BharatStock tools appear.
31
+
32
+ ### Environment variables
33
+
34
+ | Variable | Required | Description |
35
+ | --- | --- | --- |
36
+ | `BHARATSTOCK_API_KEY` | yes | Your `bsk_live_...` key. |
37
+ | `BHARATSTOCK_BASE_URL` | no | Override the API host (self-hosted/staging). Defaults to the public API. |
38
+
39
+ ## Tools
40
+
41
+ Each tool wraps a consumer-facing BharatStock endpoint. Your model picks the right one from its description.
42
+
43
+ **Stocks**
44
+ - `get_stock_quote` โ€” latest price + market cap + headline metrics for a ticker
45
+ - `get_company_profile` โ€” company identity/classification for a ticker
46
+ - `get_stock_quotes_batch` โ€” latest snapshot for up to 50 tickers at once
47
+ - `get_stock_prices` โ€” historical daily OHLCV (split/bonus-adjusted)
48
+ - `get_financials` โ€” quarterly/annual P&L, balance sheet, cash flow
49
+ - `get_stock_fundamentals` โ€” current ratios (P/E, P/B, ROE, ROCE, EPS, yieldโ€ฆ)
50
+ - `get_technical_indicators` โ€” daily indicators over a date range
51
+ - `get_shareholding` โ€” quarterly promoter/FII/DII/public holding
52
+ - `get_corporate_actions` โ€” dividends, bonuses, splits, rights, buybacks
53
+ - `get_stock_bulk_deals` / `get_stock_block_deals` โ€” deals for one stock
54
+ - `get_stock_insider_trades` โ€” SEBI PIT disclosures for one stock
55
+ - `compare_sector_peers` โ€” relative valuation across a sector
56
+
57
+ **Discovery & rankings**
58
+ - `search_stocks` โ€” resolve a name to a ticker
59
+ - `get_stock_rank` โ€” top gainers / losers / most-active
60
+ - `get_price_shockers` โ€” stocks moving beyond a % threshold
61
+ - `run_screener` โ€” fundamental screen (`pe_ratio.lt.15`, `roe.gt.18`, โ€ฆ)
62
+
63
+ **Indices & market**
64
+ - `list_indices` โ€” indices with latest values
65
+ - `get_index_prices` โ€” historical index values
66
+ - `get_fii_dii_activity` โ€” institutional net buy/sell by day
67
+ - `get_bulk_deals` / `get_block_deals` / `get_insider_trades` โ€” market-wide
68
+
69
+ **Mutual funds**
70
+ - `search_mutual_fund_schemes` โ€” find a scheme_code across all AMCs
71
+ - `get_mutual_fund_scheme` โ€” scheme identity + latest NAV
72
+ - `get_mutual_fund_nav` โ€” daily NAV history
73
+ - `get_mutual_fund_returns` โ€” trailing returns (1mโ€ฆ5y, CAGR)
74
+ - `get_mutual_fund_holdings` โ€” which funds hold a given stock
75
+
76
+ **Platform**
77
+ - `get_data_status` โ€” public data-integrity/freshness report
78
+
79
+ ## Remote (hosted HTTP) mode
80
+
81
+ If your AI host connects to a **URL** instead of spawning a local process, use our hosted remote MCP endpoint โ€” no install:
82
+
83
+ ```
84
+ https://bharatstockapi.com/v1/mcp
85
+ ```
86
+
87
+ Config for a URL-based host:
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "bharatstock": {
93
+ "url": "https://bharatstockapi.com/v1/mcp",
94
+ "headers": { "Authorization": "Bearer bsk_live_..." }
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ Pass your key on **either** header (Bearer preferred): `Authorization: Bearer bsk_live_...` or `X-API-Key: bsk_live_...`. Same Developer/Pro gate and rate/limit rules as everywhere else. No OAuth โ€” the API key is the credential.
101
+
102
+ ### Self-hosting the remote server (optional)
103
+
104
+ This package also ships a standalone remote server (`bharatstock-mcp-http`) if you'd rather run your own:
105
+
106
+ ```bash
107
+ BHARATSTOCK_BASE_URL=https://bharatstockapi.com PORT=8080 npx -y bharatstock-mcp-http
108
+ # POST /mcp (the MCP endpoint) | GET /healthz (liveness)
109
+ ```
110
+
111
+ It's stateless (a fresh server per request), multi-tenant (each request carries its own key), and validates/plan-gates every request before any tool runs.
112
+
113
+ ## How it works
114
+
115
+ This server is a thin layer over the official [`bharatstock`](https://www.npmjs.com/package/bharatstock) JS SDK. Auth, retries, base URL and typed decoding all come from the SDK, so the tool set tracks the API surface. Tool results are returned as JSON text.
116
+
117
+ ## Notes
118
+
119
+ - **MCP is a Developer/Pro feature.** On startup the server calls the API to check your plan; a Free/Starter key gets a clear "upgrade" message and the server exits without connecting.
120
+ - Free-tier keys are limited to a recent history window on deep-history tools (prices, financials, NAV, etc.); paid keys get the full archive โ€” identical to the REST behaviour.
121
+ - Errors from the API (invalid key, rate limit, not found) are returned as a tool error message the model can read, not a crash.
122
+
123
+ ## License
124
+
125
+ MIT
126
+
127
+ ## Changelog
128
+
129
+ - **0.1.0** (2026-09-22): initial release. Local stdio server (`bharatstock-mcp`) + remote Streamable HTTP server (`bharatstock-mcp-http`, multi-tenant, Bearer/X-API-Key auth). MCP is a Developer/Pro feature, gated via `/v1/mcp/authorize`. Tools for stocks (quote, profile, prices, financials, fundamentals, technicals, shareholding, corporate actions, deals, insider trades, peer compare), discovery (search, movers, price shockers, screener), indices, market-wide (FII/DII, deals, insider), and mutual funds (schemes, scheme, NAV, returns, holdings), plus data status.
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/auth.ts
4
+ var DEFAULT_UPGRADE_URL = "https://bharatstockapi.com/#pricing";
5
+ async function checkAuthorization(baseUrl, apiKey, fetchImpl = fetch) {
6
+ let res;
7
+ try {
8
+ res = await fetchImpl(`${baseUrl}/v1/mcp/authorize`, {
9
+ method: "GET",
10
+ headers: { "X-API-Key": apiKey, Accept: "application/json" }
11
+ });
12
+ } catch (err) {
13
+ return { ok: true };
14
+ }
15
+ if (res.ok) {
16
+ let plan;
17
+ try {
18
+ const body = await res.json();
19
+ plan = body?.plan;
20
+ } catch {
21
+ }
22
+ return { ok: true, plan };
23
+ }
24
+ if (res.status === 401) {
25
+ return { ok: false, status: 401, message: "Invalid or missing API key." };
26
+ }
27
+ if (res.status === 403) {
28
+ let message = "MCP access requires the Developer or Pro plan.";
29
+ let upgradeUrl = DEFAULT_UPGRADE_URL;
30
+ try {
31
+ const body = await res.json();
32
+ if (body?.detail?.message) message = body.detail.message;
33
+ if (body?.detail?.upgrade_url) upgradeUrl = body.detail.upgrade_url;
34
+ } catch {
35
+ }
36
+ return { ok: false, status: 403, message, upgradeUrl };
37
+ }
38
+ return { ok: true };
39
+ }
40
+
41
+ // src/server.ts
42
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
43
+ import { BharatStock } from "bharatstock";
44
+ import { z } from "zod";
45
+ var SERVER_NAME = "bharatstock";
46
+ var SERVER_VERSION = "0.1.0";
47
+ async function run(fn) {
48
+ try {
49
+ const data = await fn();
50
+ return {
51
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
52
+ };
53
+ } catch (err) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ return {
56
+ isError: true,
57
+ content: [{ type: "text", text: `BharatStock API error: ${message}` }]
58
+ };
59
+ }
60
+ }
61
+ function buildServer(options = {}) {
62
+ const client = new BharatStock(options);
63
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
64
+ const ticker = z.string().describe("NSE/BSE ticker symbol (e.g. RELIANCE, TCS) or ISIN.");
65
+ const exchange = z.enum(["NSE", "BSE"]).optional().describe("Optional exchange to disambiguate a cross-listed stock.");
66
+ const fromDate = z.string().optional().describe("Start date, YYYY-MM-DD (inclusive).");
67
+ const toDate = z.string().optional().describe("End date, YYYY-MM-DD (inclusive).");
68
+ server.registerTool(
69
+ "get_stock_quote",
70
+ {
71
+ title: "Get stock quote",
72
+ description: "Latest snapshot for a stock: company name, latest price, day change, 52-week high/low, market cap, and headline valuation metrics (P/E, P/B, EPS). Use this for a single 'what is X trading at' question.",
73
+ inputSchema: { ticker, exchange }
74
+ },
75
+ async ({ ticker: ticker2, exchange: exchange2 }) => run(() => client.stocks.get(ticker2, { exchange: exchange2 }))
76
+ );
77
+ server.registerTool(
78
+ "get_company_profile",
79
+ {
80
+ title: "Get company profile",
81
+ description: "Company identity and classification for a stock: full company name, sector, exchange listing(s), and identifiers. This is the same detail record as get_stock_quote (which also carries price/metrics); use this when you only need who the company is, not the live price.",
82
+ inputSchema: { ticker, exchange }
83
+ },
84
+ async ({ ticker: ticker2, exchange: exchange2 }) => run(() => client.stocks.get(ticker2, { exchange: exchange2 }))
85
+ );
86
+ server.registerTool(
87
+ "get_stock_quotes_batch",
88
+ {
89
+ title: "Get quotes for multiple stocks",
90
+ description: "Latest price snapshot for up to 50 tickers in one call. Prefer this over many get_stock_quote calls when comparing several stocks.",
91
+ inputSchema: {
92
+ symbols: z.array(z.string()).min(1).max(50).describe("List of ticker symbols (max 50).")
93
+ }
94
+ },
95
+ async ({ symbols }) => run(() => client.stocks.quotes(symbols))
96
+ );
97
+ server.registerTool(
98
+ "get_stock_prices",
99
+ {
100
+ title: "Get historical daily prices",
101
+ description: "Historical end-of-day OHLCV for a stock, including bonus/split adjusted close. Paginated. Free-tier keys get a recent window; paid keys get the full multi-year archive.",
102
+ inputSchema: {
103
+ ticker,
104
+ fromDate,
105
+ toDate,
106
+ exchange,
107
+ page: z.number().int().positive().optional(),
108
+ pageSize: z.number().int().positive().max(500).optional()
109
+ }
110
+ },
111
+ async ({ ticker: ticker2, fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize }) => run(() => client.stocks.prices(ticker2, { fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize }))
112
+ );
113
+ server.registerTool(
114
+ "get_financials",
115
+ {
116
+ title: "Get financial statements",
117
+ description: "Quarterly or annual financial statements (P&L, balance sheet, cash flow) for a stock, as filed with the exchange. Set periodType to 'annual' for yearly figures or 'quarterly' for quarterly results.",
118
+ inputSchema: {
119
+ ticker,
120
+ periodType: z.enum(["quarterly", "annual"]).optional().describe("Reporting cadence. Defaults to quarterly."),
121
+ exchange,
122
+ page: z.number().int().positive().optional(),
123
+ pageSize: z.number().int().positive().max(50).optional()
124
+ }
125
+ },
126
+ async ({ ticker: ticker2, periodType, exchange: exchange2, page, pageSize }) => run(() => client.stocks.financials(ticker2, { periodType, exchange: exchange2, page, pageSize }))
127
+ );
128
+ server.registerTool(
129
+ "get_stock_fundamentals",
130
+ {
131
+ title: "Get stock fundamentals / ratios",
132
+ description: "Current computed valuation and profitability ratios for a stock: P/E, P/B, ROE, ROCE, EPS, dividend yield, margins, and more. Use this for 'is X expensive / how profitable is X' questions.",
133
+ inputSchema: { ticker, exchange }
134
+ },
135
+ async ({ ticker: ticker2, exchange: exchange2 }) => run(() => client.stocks.ratios(ticker2, { exchange: exchange2 }))
136
+ );
137
+ server.registerTool(
138
+ "get_technical_indicators",
139
+ {
140
+ title: "Get technical indicators",
141
+ description: "Daily technical indicators for a stock (moving averages, RSI, etc.) over a date range. Free-tier keys get a recent window.",
142
+ inputSchema: {
143
+ ticker,
144
+ fromDate,
145
+ toDate,
146
+ exchange,
147
+ page: z.number().int().positive().optional(),
148
+ pageSize: z.number().int().positive().max(500).optional()
149
+ }
150
+ },
151
+ async ({ ticker: ticker2, fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize }) => run(
152
+ () => client.stocks.technicalIndicators(ticker2, { fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize })
153
+ )
154
+ );
155
+ server.registerTool(
156
+ "get_shareholding",
157
+ {
158
+ title: "Get shareholding pattern",
159
+ description: "Quarterly shareholding pattern for a stock: promoter / FII / DII / public holding percentages over time.",
160
+ inputSchema: {
161
+ ticker,
162
+ exchange,
163
+ page: z.number().int().positive().optional(),
164
+ pageSize: z.number().int().positive().max(50).optional()
165
+ }
166
+ },
167
+ async ({ ticker: ticker2, exchange: exchange2, page, pageSize }) => run(() => client.stocks.shareholding(ticker2, { exchange: exchange2, page, pageSize }))
168
+ );
169
+ server.registerTool(
170
+ "get_corporate_actions",
171
+ {
172
+ title: "Get corporate actions",
173
+ description: "Corporate actions for a stock: dividends, bonuses, stock splits, rights issues, and buybacks, with ex-dates.",
174
+ inputSchema: {
175
+ ticker,
176
+ actionType: z.string().optional().describe("Filter by type, e.g. dividend, bonus, split."),
177
+ exchange,
178
+ page: z.number().int().positive().optional(),
179
+ pageSize: z.number().int().positive().max(50).optional()
180
+ }
181
+ },
182
+ async ({ ticker: ticker2, actionType, exchange: exchange2, page, pageSize }) => run(() => client.stocks.corporateActions(ticker2, { actionType, exchange: exchange2, page, pageSize }))
183
+ );
184
+ server.registerTool(
185
+ "get_stock_bulk_deals",
186
+ {
187
+ title: "Get bulk deals for a stock",
188
+ description: "Bulk deals reported for a specific stock.",
189
+ inputSchema: {
190
+ ticker,
191
+ page: z.number().int().positive().optional(),
192
+ pageSize: z.number().int().positive().max(100).optional()
193
+ }
194
+ },
195
+ async ({ ticker: ticker2, page, pageSize }) => run(() => client.stocks.bulkDeals(ticker2, { page, pageSize }))
196
+ );
197
+ server.registerTool(
198
+ "get_stock_block_deals",
199
+ {
200
+ title: "Get block deals for a stock",
201
+ description: "Block deals reported for a specific stock.",
202
+ inputSchema: {
203
+ ticker,
204
+ page: z.number().int().positive().optional(),
205
+ pageSize: z.number().int().positive().max(100).optional()
206
+ }
207
+ },
208
+ async ({ ticker: ticker2, page, pageSize }) => run(() => client.stocks.blockDeals(ticker2, { page, pageSize }))
209
+ );
210
+ server.registerTool(
211
+ "get_stock_insider_trades",
212
+ {
213
+ title: "Get insider trades for a stock",
214
+ description: "SEBI PIT insider-trading disclosures for a specific stock.",
215
+ inputSchema: {
216
+ ticker,
217
+ page: z.number().int().positive().optional(),
218
+ pageSize: z.number().int().positive().max(100).optional()
219
+ }
220
+ },
221
+ async ({ ticker: ticker2, page, pageSize }) => run(() => client.stocks.insiderTrades(ticker2, { page, pageSize }))
222
+ );
223
+ server.registerTool(
224
+ "compare_sector_peers",
225
+ {
226
+ title: "Compare peers in a sector",
227
+ description: "Compare valuation/profitability ratios across the peers in a sector, for a quick relative-value view.",
228
+ inputSchema: {
229
+ sector: z.string().describe("Sector name, e.g. 'Information Technology'."),
230
+ sort: z.string().optional().describe("Sort key, e.g. market_cap, pe_ratio."),
231
+ limit: z.number().int().positive().max(100).optional()
232
+ }
233
+ },
234
+ async ({ sector, sort, limit }) => run(() => client.stocks.compare(sector, { sort, limit }))
235
+ );
236
+ server.registerTool(
237
+ "search_stocks",
238
+ {
239
+ title: "Search stocks",
240
+ description: "Fuzzy-search listed stocks by ticker symbol or company name. Use this to resolve a company name to a ticker before calling other tools.",
241
+ inputSchema: {
242
+ q: z.string().describe("Search text (symbol fragment or company name)."),
243
+ limit: z.number().int().positive().max(50).optional()
244
+ }
245
+ },
246
+ async ({ q, limit }) => run(() => client.search(q, { limit }))
247
+ );
248
+ server.registerTool(
249
+ "get_stock_rank",
250
+ {
251
+ title: "Get top market movers",
252
+ description: "Ranked market movers: top gainers, top losers, or most-active stocks for the latest trading session. Use category to pick which.",
253
+ inputSchema: {
254
+ category: z.enum(["gainers", "losers", "active"]).optional().describe("Which ranking. Defaults to gainers."),
255
+ limit: z.number().int().positive().max(100).optional()
256
+ }
257
+ },
258
+ async ({ category, limit }) => run(() => client.movers({ category, limit }))
259
+ );
260
+ server.registerTool(
261
+ "get_price_shockers",
262
+ {
263
+ title: "Get price shockers",
264
+ description: "Stocks that moved at least a threshold percentage vs the previous close, in either direction.",
265
+ inputSchema: {
266
+ minChangePct: z.number().positive().optional().describe("Minimum abs % move. Default 5."),
267
+ direction: z.enum(["up", "down", "both"]).optional(),
268
+ limit: z.number().int().positive().max(100).optional()
269
+ }
270
+ },
271
+ async ({ minChangePct, direction, limit }) => run(() => client.priceShockers({ minChangePct, direction, limit }))
272
+ );
273
+ server.registerTool(
274
+ "run_screener",
275
+ {
276
+ title: "Run stock screener",
277
+ description: "Screen stocks by fundamental filters. `filters` is a list of 'metric.operator.value' strings, e.g. ['pe_ratio.lt.15', 'roe.gt.18']. marketCap filter values are in Crores. Optionally scope by sector.",
278
+ inputSchema: {
279
+ filters: z.array(z.string()).optional().describe("Filter expressions like 'pe_ratio.lt.15'."),
280
+ sector: z.string().optional(),
281
+ exchange,
282
+ sortBy: z.string().optional().describe("Sort key, default market_cap."),
283
+ sortOrder: z.enum(["asc", "desc"]).optional(),
284
+ page: z.number().int().positive().optional(),
285
+ pageSize: z.number().int().positive().max(100).optional()
286
+ }
287
+ },
288
+ async ({ filters, sector, exchange: exchange2, sortBy, sortOrder, page, pageSize }) => run(
289
+ () => client.screener.run({ filters, sector, exchange: exchange2, sortBy, sortOrder, page, pageSize })
290
+ )
291
+ );
292
+ server.registerTool(
293
+ "list_indices",
294
+ {
295
+ title: "List indices",
296
+ description: "List market indices (NIFTY 50, SENSEX, sectoral indices, etc.) with their latest values.",
297
+ inputSchema: {
298
+ page: z.number().int().positive().optional(),
299
+ pageSize: z.number().int().positive().max(200).optional()
300
+ }
301
+ },
302
+ async ({ page, pageSize }) => run(() => client.indices.list({ page, pageSize }))
303
+ );
304
+ server.registerTool(
305
+ "get_index_prices",
306
+ {
307
+ title: "Get historical index prices",
308
+ description: "Historical daily values for an index over a date range. Free-tier keys get a recent window; paid keys get the full archive.",
309
+ inputSchema: {
310
+ name: z.string().describe("Index name, e.g. 'NIFTY 50'."),
311
+ fromDate,
312
+ toDate,
313
+ page: z.number().int().positive().optional(),
314
+ pageSize: z.number().int().positive().max(500).optional()
315
+ }
316
+ },
317
+ async ({ name, fromDate: fromDate2, toDate: toDate2, page, pageSize }) => run(() => client.indices.prices(name, { fromDate: fromDate2, toDate: toDate2, page, pageSize }))
318
+ );
319
+ server.registerTool(
320
+ "get_fii_dii_activity",
321
+ {
322
+ title: "Get FII/DII activity",
323
+ description: "Foreign (FII) and domestic (DII) institutional investor net buy/sell activity (Rs Crores) by trading day. Pass latest=true for only the most recent day.",
324
+ inputSchema: {
325
+ fromDate,
326
+ toDate,
327
+ limit: z.number().int().positive().max(500).optional(),
328
+ latest: z.boolean().optional().describe("Return only the most recent day.")
329
+ }
330
+ },
331
+ async ({ fromDate: fromDate2, toDate: toDate2, limit, latest }) => run(() => client.market.fiiDii({ fromDate: fromDate2, toDate: toDate2, limit, latest }))
332
+ );
333
+ server.registerTool(
334
+ "get_bulk_deals",
335
+ {
336
+ title: "Get market-wide bulk deals",
337
+ description: "Bulk deals across the whole market for the latest sessions.",
338
+ inputSchema: {
339
+ buySell: z.enum(["BUY", "SELL"]).optional(),
340
+ page: z.number().int().positive().optional(),
341
+ pageSize: z.number().int().positive().max(100).optional()
342
+ }
343
+ },
344
+ async ({ buySell, page, pageSize }) => run(() => client.deals.bulk({ buySell, page, pageSize }))
345
+ );
346
+ server.registerTool(
347
+ "get_block_deals",
348
+ {
349
+ title: "Get market-wide block deals",
350
+ description: "Block deals across the whole market for the latest sessions.",
351
+ inputSchema: {
352
+ buySell: z.enum(["BUY", "SELL"]).optional(),
353
+ page: z.number().int().positive().optional(),
354
+ pageSize: z.number().int().positive().max(100).optional()
355
+ }
356
+ },
357
+ async ({ buySell, page, pageSize }) => run(() => client.deals.block({ buySell, page, pageSize }))
358
+ );
359
+ server.registerTool(
360
+ "get_insider_trades",
361
+ {
362
+ title: "Get market-wide insider trades",
363
+ description: "SEBI PIT insider-trading disclosures across the whole market.",
364
+ inputSchema: {
365
+ page: z.number().int().positive().optional(),
366
+ pageSize: z.number().int().positive().max(100).optional()
367
+ }
368
+ },
369
+ async ({ page, pageSize }) => run(() => client.deals.insiderTrades({ page, pageSize }))
370
+ );
371
+ server.registerTool(
372
+ "search_mutual_fund_schemes",
373
+ {
374
+ title: "Search mutual-fund schemes",
375
+ description: "List/search mutual-fund schemes across all AMCs (industry-wide AMFI dataset). Filter by name (q), AMC, category, plan (Direct/Regular), option (Growth/IDCW), or exact ISIN. Use this to find a scheme_code.",
376
+ inputSchema: {
377
+ q: z.string().optional(),
378
+ amc: z.string().optional(),
379
+ category: z.string().optional(),
380
+ plan: z.string().optional(),
381
+ option: z.string().optional(),
382
+ isin: z.string().optional(),
383
+ limit: z.number().int().positive().max(200).optional(),
384
+ offset: z.number().int().nonnegative().optional()
385
+ }
386
+ },
387
+ async (args) => run(() => client.mutualFunds.listSchemes(args))
388
+ );
389
+ server.registerTool(
390
+ "get_mutual_fund_scheme",
391
+ {
392
+ title: "Get mutual-fund scheme detail",
393
+ description: "One scheme's identity plus its latest NAV (with as-of date).",
394
+ inputSchema: {
395
+ schemeCode: z.string().describe("AMFI scheme code.")
396
+ }
397
+ },
398
+ async ({ schemeCode }) => run(() => client.mutualFunds.getScheme(schemeCode))
399
+ );
400
+ server.registerTool(
401
+ "get_mutual_fund_nav",
402
+ {
403
+ title: "Get mutual-fund NAV history",
404
+ description: "Daily NAV history for a scheme, most recent first. Free-tier keys are limited to a recent window; paid keys get the full archive.",
405
+ inputSchema: {
406
+ schemeCode: z.string().describe("AMFI scheme code."),
407
+ fromDate,
408
+ toDate,
409
+ limit: z.number().int().positive().max(2e3).optional()
410
+ }
411
+ },
412
+ async ({ schemeCode, fromDate: fromDate2, toDate: toDate2, limit }) => run(() => client.mutualFunds.nav(schemeCode, { fromDate: fromDate2, toDate: toDate2, limit }))
413
+ );
414
+ server.registerTool(
415
+ "get_mutual_fund_returns",
416
+ {
417
+ title: "Get mutual-fund trailing returns",
418
+ description: "Trailing returns (1m/3m/6m/1y/3y/5y + since-inception) computed from the scheme's NAV series (CAGR for >=1y horizons).",
419
+ inputSchema: {
420
+ schemeCode: z.string().describe("AMFI scheme code.")
421
+ }
422
+ },
423
+ async ({ schemeCode }) => run(() => client.mutualFunds.returns(schemeCode))
424
+ );
425
+ server.registerTool(
426
+ "get_mutual_fund_holdings",
427
+ {
428
+ title: "Get mutual-fund holdings for a stock",
429
+ description: "Which mutual-fund schemes hold a given stock (portfolio positions aggregated per stock), optionally for a specific portfolio month. Answers 'which funds own X'.",
430
+ inputSchema: {
431
+ ticker,
432
+ month: z.string().optional().describe("Portfolio month YYYY-MM-DD (month-end). Latest if omitted."),
433
+ page: z.number().int().positive().optional(),
434
+ pageSize: z.number().int().positive().max(200).optional()
435
+ }
436
+ },
437
+ async ({ ticker: ticker2, month, page, pageSize }) => run(() => client.stocks.mfHoldings(ticker2, { month, page, pageSize }))
438
+ );
439
+ server.registerTool(
440
+ "get_data_status",
441
+ {
442
+ title: "Get data-integrity status",
443
+ description: "Public data-trust report: freshness and correctness status of the BharatStock pipelines. No sensitive detail.",
444
+ inputSchema: {}
445
+ },
446
+ async () => run(() => client.status())
447
+ );
448
+ return server;
449
+ }
450
+
451
+ export {
452
+ checkAuthorization,
453
+ SERVER_NAME,
454
+ SERVER_VERSION,
455
+ buildServer
456
+ };
package/dist/http.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import * as http from 'http';
2
+ import { IncomingMessage, ServerResponse } from 'node:http';
3
+
4
+ /** Extract the API key from Authorization: Bearer or X-API-Key. */
5
+ declare function extractApiKey(headers: IncomingMessage["headers"]): string | undefined;
6
+ declare function createHttpServer(): http.Server<typeof IncomingMessage, typeof ServerResponse>;
7
+
8
+ export { createHttpServer, extractApiKey };
package/dist/http.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildServer,
4
+ checkAuthorization
5
+ } from "./chunk-RPAWW7CN.js";
6
+
7
+ // src/http.ts
8
+ import { randomUUID } from "crypto";
9
+ import { createServer } from "http";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
+ import { DEFAULT_BASE_URL } from "bharatstock";
12
+ var PORT = Number(process.env.PORT || 8080);
13
+ var BASE_URL = (process.env.BHARATSTOCK_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
14
+ function extractApiKey(headers) {
15
+ const auth = headers["authorization"];
16
+ if (typeof auth === "string") {
17
+ const m = auth.match(/^Bearer\s+(.+)$/i);
18
+ if (m) return m[1].trim();
19
+ }
20
+ const xApiKey = headers["x-api-key"];
21
+ if (typeof xApiKey === "string" && xApiKey.trim()) return xApiKey.trim();
22
+ if (Array.isArray(xApiKey) && xApiKey[0]) return xApiKey[0].trim();
23
+ return void 0;
24
+ }
25
+ function sendJson(res, status, body) {
26
+ const payload = JSON.stringify(body);
27
+ res.writeHead(status, { "Content-Type": "application/json" });
28
+ res.end(payload);
29
+ }
30
+ function jsonRpcError(res, status, message) {
31
+ sendJson(res, status, {
32
+ jsonrpc: "2.0",
33
+ error: { code: status === 401 || status === 403 ? -32001 : -32e3, message },
34
+ id: null
35
+ });
36
+ }
37
+ async function readBody(req) {
38
+ const chunks = [];
39
+ for await (const chunk of req) chunks.push(chunk);
40
+ if (chunks.length === 0) return void 0;
41
+ const raw = Buffer.concat(chunks).toString("utf8");
42
+ try {
43
+ return JSON.parse(raw);
44
+ } catch {
45
+ return void 0;
46
+ }
47
+ }
48
+ async function handleMcp(req, res) {
49
+ const apiKey = extractApiKey(req.headers);
50
+ if (!apiKey) {
51
+ jsonRpcError(res, 401, "Missing API key. Pass Authorization: Bearer <key> or X-API-Key.");
52
+ return;
53
+ }
54
+ const gate = await checkAuthorization(BASE_URL, apiKey);
55
+ if (!gate.ok) {
56
+ const suffix = gate.upgradeUrl ? ` Upgrade at ${gate.upgradeUrl}` : "";
57
+ jsonRpcError(res, gate.status, `${gate.message}${suffix}`);
58
+ return;
59
+ }
60
+ const body = await readBody(req);
61
+ const server = buildServer({ apiKey, baseUrl: BASE_URL });
62
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
63
+ res.on("close", () => {
64
+ void transport.close();
65
+ void server.close();
66
+ });
67
+ try {
68
+ await server.connect(transport);
69
+ await transport.handleRequest(req, res, body);
70
+ } catch (err) {
71
+ if (!res.headersSent) {
72
+ jsonRpcError(res, 500, `Internal server error: ${String(err)}`);
73
+ }
74
+ }
75
+ }
76
+ function createHttpServer() {
77
+ return createServer((req, res) => {
78
+ const url = req.url || "/";
79
+ const path = url.split("?")[0];
80
+ if (req.method === "GET" && path === "/healthz") {
81
+ sendJson(res, 200, { status: "ok", service: "bharatstock-mcp-http" });
82
+ return;
83
+ }
84
+ if (path === "/mcp") {
85
+ if (req.method === "POST") {
86
+ void handleMcp(req, res);
87
+ return;
88
+ }
89
+ res.writeHead(405, { "Content-Type": "application/json", Allow: "POST" }).end(
90
+ JSON.stringify({
91
+ jsonrpc: "2.0",
92
+ error: { code: -32e3, message: "Method not allowed. This endpoint is POST-only (stateless)." },
93
+ id: null
94
+ })
95
+ );
96
+ return;
97
+ }
98
+ jsonRpcError(res, 404, "Not found. The MCP endpoint is POST /mcp.");
99
+ });
100
+ }
101
+ function main() {
102
+ const server = createHttpServer();
103
+ server.listen(PORT, () => {
104
+ console.log(`bharatstock-mcp-http listening on :${PORT} (POST /mcp), API ${BASE_URL} [${randomUUID().slice(0, 8)}]`);
105
+ });
106
+ }
107
+ if (process.env.NODE_ENV !== "test") {
108
+ main();
109
+ }
110
+ export {
111
+ createHttpServer,
112
+ extractApiKey
113
+ };
@@ -0,0 +1,49 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { BharatStockOptions } from 'bharatstock';
3
+
4
+ /**
5
+ * Builds the BharatStock MCP server: a thin layer that exposes the
6
+ * BharatStock REST API's consumer-facing (X-API-Key gated) endpoints as
7
+ * Model Context Protocol tools.
8
+ *
9
+ * Every tool is a thin wrapper over the official `bharatstock` JS SDK, so
10
+ * this server inherits the SDK's auth, base URL, retry/backoff and typed
11
+ * decoding, and stays in sync with the API surface. When the SDK adds a
12
+ * response field, it flows through automatically (results are serialized as
13
+ * JSON); a genuinely NEW endpoint is the only case that needs a new tool.
14
+ */
15
+
16
+ declare const SERVER_NAME = "bharatstock";
17
+ declare const SERVER_VERSION = "0.1.0";
18
+ /**
19
+ * Construct the MCP server and register all tools against a BharatStock
20
+ * client. Exposed separately from the stdio wiring so tests can build a
21
+ * server with an injected `fetchImpl` and drive it over an in-memory
22
+ * transport (no network, no real key).
23
+ */
24
+ declare function buildServer(options?: BharatStockOptions): McpServer;
25
+
26
+ /**
27
+ * Shared plan-gate check for both the stdio and HTTP transports.
28
+ *
29
+ * MCP access is a Developer/Pro feature. The authoritative gate is
30
+ * server-side: `GET /v1/mcp/authorize` on the BharatStock API returns 200
31
+ * for an eligible key, 401 for an invalid key, and 403 for a valid key on an
32
+ * ineligible plan. Every MCP tool call is also a real X-API-Key request, so
33
+ * this check is about failing fast with a clear message, not the last line
34
+ * of defence.
35
+ */
36
+ type AuthResult = {
37
+ ok: true;
38
+ plan?: string;
39
+ } | {
40
+ ok: false;
41
+ status: number;
42
+ message: string;
43
+ upgradeUrl?: string;
44
+ };
45
+ /** Call GET /v1/mcp/authorize with the given key. `baseUrl` must not have a
46
+ * trailing slash. `fetchImpl` is injectable for tests. */
47
+ declare function checkAuthorization(baseUrl: string, apiKey: string, fetchImpl?: typeof fetch): Promise<AuthResult>;
48
+
49
+ export { SERVER_NAME, SERVER_VERSION, buildServer, checkAuthorization };
package/dist/index.js ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ SERVER_NAME,
4
+ SERVER_VERSION,
5
+ buildServer,
6
+ checkAuthorization
7
+ } from "./chunk-RPAWW7CN.js";
8
+
9
+ // src/index.ts
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { DEFAULT_BASE_URL } from "bharatstock";
12
+ async function main() {
13
+ const apiKey = process.env.BHARATSTOCK_API_KEY;
14
+ if (!apiKey) {
15
+ process.stderr.write(
16
+ "bharatstock-mcp: BHARATSTOCK_API_KEY is not set. Set it in your MCP host config's `env` block. Get a key at https://bharatstockapi.com\n"
17
+ );
18
+ process.exit(1);
19
+ }
20
+ const baseUrl = (process.env.BHARATSTOCK_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, "");
21
+ const gate = await checkAuthorization(baseUrl, apiKey);
22
+ if (!gate.ok) {
23
+ const suffix = gate.upgradeUrl ? ` Upgrade at ${gate.upgradeUrl}` : "";
24
+ process.stderr.write(`bharatstock-mcp: ${gate.message}${suffix}
25
+ `);
26
+ process.exit(1);
27
+ }
28
+ const server = buildServer({ apiKey, baseUrl });
29
+ const transport = new StdioServerTransport();
30
+ await server.connect(transport);
31
+ }
32
+ main().catch((err) => {
33
+ process.stderr.write(`bharatstock-mcp failed to start: ${String(err)}
34
+ `);
35
+ process.exit(1);
36
+ });
37
+ export {
38
+ SERVER_NAME,
39
+ SERVER_VERSION,
40
+ buildServer,
41
+ checkAuthorization
42
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "bharatstock-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Official Model Context Protocol (MCP) server for the BharatStock API - reliable Indian stock market data (NSE/BSE) as AI-agent tools.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "bharatstock-mcp": "./dist/index.js",
9
+ "bharatstock-mcp-http": "./dist/http.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "CHANGELOG.md"
15
+ ],
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "nse",
20
+ "bse",
21
+ "india",
22
+ "stocks",
23
+ "market-data",
24
+ "finance",
25
+ "ai-tools"
26
+ ],
27
+ "homepage": "https://bharatstockapi.com",
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "test": "vitest run",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.12.0",
38
+ "bharatstock": "^0.1.8",
39
+ "zod": "^3.23.8"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.0.0",
43
+ "tsup": "^8.0.2",
44
+ "typescript": "^5.5.4",
45
+ "vitest": "^5.0.0"
46
+ }
47
+ }