bharatstock-mcp 0.1.2 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to `bharatstock-mcp` are documented here (newest first).
4
4
 
5
+ ## 0.2.0 (2026-11-06)
6
+ - Added an internal **operator mode** (not for customers). When the
7
+ `BHARATSTOCK_ADMIN_SECRET` env var is set alongside the API key, the server
8
+ additionally registers four support-ticket tools — `get_support_tickets`,
9
+ `get_support_ticket`, `set_ticket_status`, `log_ticket_event` — which call
10
+ the hosted MCP endpoint with that secret to list, inspect, and update
11
+ support tickets. A normal customer config (no admin secret) never registers
12
+ them, and the hosted server independently re-checks the secret, so the
13
+ consumer surface is unchanged.
14
+
15
+ ## 0.1.5 (2026-11-03)
16
+ - Broadened the listing metadata for discoverability: a keyword-rich
17
+ `description` and expanded `keywords` (NSE, BSE, NIFTY, SENSEX, mutual funds,
18
+ NAVs, shareholding, FII/DII, corporate actions, bulk/block deals, insider
19
+ trades, fundamentals screener, Indian equities). No tool/schema change.
20
+
21
+ ## 0.1.4 (2026-11-03)
22
+ - Published to the official MCP Registry as
23
+ `com.bharatstockapi/bharatstock-mcp`. No tool/schema change.
24
+
25
+ ## 0.1.3 (2026-11-02)
26
+
27
+ - Bumped the `bharatstock` JS SDK dependency to ^0.1.12. The financials tool
28
+ output now carries the as-filed segment reconciliation fields:
29
+ `segmentResultsTotal` (the filing's own segment total — the anchor the
30
+ per-segment results roll up toward PBIT) and `segmentNotes` / `resultsNotes`
31
+ (the narrative note blocks; the segment note carries the unallocated /
32
+ elimination / exceptional reconciliation in prose). Pass-through via the SDK;
33
+ no tool/schema change.
34
+
5
35
  ## 0.1.2 (2026-10-05)
6
36
 
7
37
  - Bumped the `bharatstock` JS SDK dependency to ^0.1.11, which made paginated
package/README.md CHANGED
@@ -128,6 +128,10 @@ MIT
128
128
 
129
129
  ## Changelog
130
130
 
131
+ - **0.2.0** (2026-11-06): added an internal operator mode (not for customers). Setting `BHARATSTOCK_ADMIN_SECRET` alongside the API key registers four support-ticket management tools that call the hosted MCP endpoint with that secret. A normal customer config (no admin secret) is unaffected — the consumer tool surface is unchanged.
132
+ - **0.1.5** (2026-11-03): broadened the registry/npm listing metadata — a keyword-rich `description` and expanded `keywords` (NSE, BSE, NIFTY, SENSEX, mutual funds, shareholding, FII/DII, corporate actions, screener, Indian equities) so the server is easier to find. No tool/schema change.
133
+ - **0.1.4** (2026-11-03): published to the official MCP Registry as `com.bharatstockapi/bharatstock-mcp`. No tool/schema change.
134
+ - **0.1.3** (2026-11-02): picks up `bharatstock` JS SDK 0.1.12, so the financials tool output now carries the as-filed segment reconciliation fields — `segmentResultsTotal` (the filing's own segment total) and `segmentNotes` / `resultsNotes` (the narrative note blocks; the segment note carries the unallocated/elimination/exceptional reconciliation in prose). Pass-through via the SDK; no tool/schema change.
131
135
  - **0.1.2** (2026-10-05): picks up `bharatstock` JS SDK 0.1.11, whose paginated responses now pass through every API field (new or modified) with no SDK change — so every field the API returns (e.g. a scheme portfolio's `coveragePct`, and anything added later) now flows through to the MCP tool output. No tool/schema change.
132
136
  - **0.1.1** (2026-09-20): added two mutual-fund portfolio tools — `list_mutual_fund_portfolios` (discover the schemes we hold monthly portfolio data for, by scheme name) and `get_mutual_fund_portfolio_holdings` (a scheme's equity portfolio for a month, with month-over-month quantity change). These are the inverse of `get_mutual_fund_holdings` and are keyed by the holdings scheme-name string, not the AMFI scheme_code.
133
137
  - **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.
@@ -41,9 +41,115 @@ async function checkAuthorization(baseUrl, apiKey, fetchImpl = fetch) {
41
41
  // src/server.ts
42
42
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
43
43
  import { BharatStock } from "bharatstock";
44
+ import { z as z2 } from "zod";
45
+
46
+ // src/operator.ts
44
47
  import { z } from "zod";
48
+ async function callOperatorTool(opts, name, args) {
49
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
50
+ const url = `${opts.baseUrl.replace(/\/$/, "")}/v1/mcp`;
51
+ const rpc = {
52
+ jsonrpc: "2.0",
53
+ id: Date.now(),
54
+ method: "tools/call",
55
+ params: { name, arguments: args }
56
+ };
57
+ try {
58
+ const resp = await fetchImpl(url, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ "X-Admin-Secret": opts.adminSecret
63
+ },
64
+ body: JSON.stringify(rpc)
65
+ });
66
+ const bodyText = await resp.text();
67
+ let body;
68
+ try {
69
+ body = JSON.parse(bodyText);
70
+ } catch {
71
+ body = void 0;
72
+ }
73
+ if (!resp.ok) {
74
+ const msg = body?.error?.message || bodyText.slice(0, 500) || `HTTP ${resp.status}`;
75
+ return { isError: true, content: [{ type: "text", text: `BharatStock operator error: ${msg}` }] };
76
+ }
77
+ const rpcErr = body?.error;
78
+ if (rpcErr) {
79
+ return { isError: true, content: [{ type: "text", text: `BharatStock operator error: ${rpcErr.message}` }] };
80
+ }
81
+ const result = body?.result;
82
+ if (result && Array.isArray(result.content)) {
83
+ return result;
84
+ }
85
+ return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }] };
86
+ } catch (err) {
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ return { isError: true, content: [{ type: "text", text: `BharatStock operator transport error: ${message}` }] };
89
+ }
90
+ }
91
+ function registerOperatorTools(server, opts) {
92
+ server.registerTool(
93
+ "get_support_tickets",
94
+ {
95
+ title: "List support tickets (operator)",
96
+ description: "OPERATOR-ONLY. List support tickets (customer feedback/email reports) with status (open/investigating/waiting/resolved/closed, or 'all') and category filters. Default shows the live queue (excludes resolved/closed).",
97
+ inputSchema: {
98
+ status: z.string().optional(),
99
+ category: z.string().optional(),
100
+ limit: z.number().int().positive().optional(),
101
+ offset: z.number().int().nonnegative().optional()
102
+ }
103
+ },
104
+ async (args) => callOperatorTool(opts, "get_support_tickets", args)
105
+ );
106
+ server.registerTool(
107
+ "get_support_ticket",
108
+ {
109
+ title: "Get one support ticket in full (operator)",
110
+ description: "OPERATOR-ONLY. One support ticket in full: the original message, its activity trail (status changes, notes, fixes applied), and any drafted reply. Use this to pick up a ticket and see everything already tried.",
111
+ inputSchema: { id: z.number().int().positive() }
112
+ },
113
+ async (args) => callOperatorTool(opts, "get_support_ticket", args)
114
+ );
115
+ server.registerTool(
116
+ "set_ticket_status",
117
+ {
118
+ title: "Set a support ticket's status (operator, write)",
119
+ description: "OPERATOR-ONLY WRITE. Transition a ticket's status (open/investigating/waiting/resolved/closed) with an optional note. Moving to resolved/closed with a note records the resolution summary.",
120
+ inputSchema: {
121
+ id: z.number().int().positive(),
122
+ status: z.enum(["open", "investigating", "waiting", "resolved", "closed"]),
123
+ note: z.string().optional()
124
+ }
125
+ },
126
+ async (args) => callOperatorTool(opts, "set_ticket_status", args)
127
+ );
128
+ server.registerTool(
129
+ "log_ticket_event",
130
+ {
131
+ title: "Append an event to a ticket's trail (operator, write)",
132
+ description: "OPERATOR-ONLY WRITE. Append an entry to a ticket's activity trail, e.g. event_type='fix_applied', detail='ran fix_eps_sign, 4 rows, DQ green'. event_type one of status_change/note/reply_sent/fix_applied/reopened/category_change.",
133
+ inputSchema: {
134
+ id: z.number().int().positive(),
135
+ event_type: z.enum([
136
+ "status_change",
137
+ "note",
138
+ "reply_sent",
139
+ "fix_applied",
140
+ "reopened",
141
+ "category_change"
142
+ ]),
143
+ detail: z.string().min(1)
144
+ }
145
+ },
146
+ async (args) => callOperatorTool(opts, "log_ticket_event", args)
147
+ );
148
+ }
149
+
150
+ // src/server.ts
45
151
  var SERVER_NAME = "bharatstock";
46
- var SERVER_VERSION = "0.1.2";
152
+ var SERVER_VERSION = "0.2.0";
47
153
  async function run(fn) {
48
154
  try {
49
155
  const data = await fn();
@@ -59,12 +165,13 @@ async function run(fn) {
59
165
  }
60
166
  }
61
167
  function buildServer(options = {}) {
62
- const client = new BharatStock(options);
168
+ const { adminSecret, operatorFetchImpl, ...sdkOptions } = options;
169
+ const client = new BharatStock(sdkOptions);
63
170
  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).");
171
+ const ticker = z2.string().describe("NSE/BSE ticker symbol (e.g. RELIANCE, TCS) or ISIN.");
172
+ const exchange = z2.enum(["NSE", "BSE"]).optional().describe("Optional exchange to disambiguate a cross-listed stock.");
173
+ const fromDate = z2.string().optional().describe("Start date, YYYY-MM-DD (inclusive).");
174
+ const toDate = z2.string().optional().describe("End date, YYYY-MM-DD (inclusive).");
68
175
  server.registerTool(
69
176
  "get_stock_quote",
70
177
  {
@@ -89,7 +196,7 @@ function buildServer(options = {}) {
89
196
  title: "Get quotes for multiple stocks",
90
197
  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
198
  inputSchema: {
92
- symbols: z.array(z.string()).min(1).max(50).describe("List of ticker symbols (max 50).")
199
+ symbols: z2.array(z2.string()).min(1).max(50).describe("List of ticker symbols (max 50).")
93
200
  }
94
201
  },
95
202
  async ({ symbols }) => run(() => client.stocks.quotes(symbols))
@@ -104,8 +211,8 @@ function buildServer(options = {}) {
104
211
  fromDate,
105
212
  toDate,
106
213
  exchange,
107
- page: z.number().int().positive().optional(),
108
- pageSize: z.number().int().positive().max(500).optional()
214
+ page: z2.number().int().positive().optional(),
215
+ pageSize: z2.number().int().positive().max(500).optional()
109
216
  }
110
217
  },
111
218
  async ({ ticker: ticker2, fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize }) => run(() => client.stocks.prices(ticker2, { fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize }))
@@ -117,10 +224,10 @@ function buildServer(options = {}) {
117
224
  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
225
  inputSchema: {
119
226
  ticker,
120
- periodType: z.enum(["quarterly", "annual"]).optional().describe("Reporting cadence. Defaults to quarterly."),
227
+ periodType: z2.enum(["quarterly", "annual"]).optional().describe("Reporting cadence. Defaults to quarterly."),
121
228
  exchange,
122
- page: z.number().int().positive().optional(),
123
- pageSize: z.number().int().positive().max(50).optional()
229
+ page: z2.number().int().positive().optional(),
230
+ pageSize: z2.number().int().positive().max(50).optional()
124
231
  }
125
232
  },
126
233
  async ({ ticker: ticker2, periodType, exchange: exchange2, page, pageSize }) => run(() => client.stocks.financials(ticker2, { periodType, exchange: exchange2, page, pageSize }))
@@ -144,8 +251,8 @@ function buildServer(options = {}) {
144
251
  fromDate,
145
252
  toDate,
146
253
  exchange,
147
- page: z.number().int().positive().optional(),
148
- pageSize: z.number().int().positive().max(500).optional()
254
+ page: z2.number().int().positive().optional(),
255
+ pageSize: z2.number().int().positive().max(500).optional()
149
256
  }
150
257
  },
151
258
  async ({ ticker: ticker2, fromDate: fromDate2, toDate: toDate2, exchange: exchange2, page, pageSize }) => run(
@@ -160,8 +267,8 @@ function buildServer(options = {}) {
160
267
  inputSchema: {
161
268
  ticker,
162
269
  exchange,
163
- page: z.number().int().positive().optional(),
164
- pageSize: z.number().int().positive().max(50).optional()
270
+ page: z2.number().int().positive().optional(),
271
+ pageSize: z2.number().int().positive().max(50).optional()
165
272
  }
166
273
  },
167
274
  async ({ ticker: ticker2, exchange: exchange2, page, pageSize }) => run(() => client.stocks.shareholding(ticker2, { exchange: exchange2, page, pageSize }))
@@ -173,10 +280,10 @@ function buildServer(options = {}) {
173
280
  description: "Corporate actions for a stock: dividends, bonuses, stock splits, rights issues, and buybacks, with ex-dates.",
174
281
  inputSchema: {
175
282
  ticker,
176
- actionType: z.string().optional().describe("Filter by type, e.g. dividend, bonus, split."),
283
+ actionType: z2.string().optional().describe("Filter by type, e.g. dividend, bonus, split."),
177
284
  exchange,
178
- page: z.number().int().positive().optional(),
179
- pageSize: z.number().int().positive().max(50).optional()
285
+ page: z2.number().int().positive().optional(),
286
+ pageSize: z2.number().int().positive().max(50).optional()
180
287
  }
181
288
  },
182
289
  async ({ ticker: ticker2, actionType, exchange: exchange2, page, pageSize }) => run(() => client.stocks.corporateActions(ticker2, { actionType, exchange: exchange2, page, pageSize }))
@@ -188,8 +295,8 @@ function buildServer(options = {}) {
188
295
  description: "Bulk deals reported for a specific stock.",
189
296
  inputSchema: {
190
297
  ticker,
191
- page: z.number().int().positive().optional(),
192
- pageSize: z.number().int().positive().max(100).optional()
298
+ page: z2.number().int().positive().optional(),
299
+ pageSize: z2.number().int().positive().max(100).optional()
193
300
  }
194
301
  },
195
302
  async ({ ticker: ticker2, page, pageSize }) => run(() => client.stocks.bulkDeals(ticker2, { page, pageSize }))
@@ -201,8 +308,8 @@ function buildServer(options = {}) {
201
308
  description: "Block deals reported for a specific stock.",
202
309
  inputSchema: {
203
310
  ticker,
204
- page: z.number().int().positive().optional(),
205
- pageSize: z.number().int().positive().max(100).optional()
311
+ page: z2.number().int().positive().optional(),
312
+ pageSize: z2.number().int().positive().max(100).optional()
206
313
  }
207
314
  },
208
315
  async ({ ticker: ticker2, page, pageSize }) => run(() => client.stocks.blockDeals(ticker2, { page, pageSize }))
@@ -214,8 +321,8 @@ function buildServer(options = {}) {
214
321
  description: "SEBI PIT insider-trading disclosures for a specific stock.",
215
322
  inputSchema: {
216
323
  ticker,
217
- page: z.number().int().positive().optional(),
218
- pageSize: z.number().int().positive().max(100).optional()
324
+ page: z2.number().int().positive().optional(),
325
+ pageSize: z2.number().int().positive().max(100).optional()
219
326
  }
220
327
  },
221
328
  async ({ ticker: ticker2, page, pageSize }) => run(() => client.stocks.insiderTrades(ticker2, { page, pageSize }))
@@ -226,9 +333,9 @@ function buildServer(options = {}) {
226
333
  title: "Compare peers in a sector",
227
334
  description: "Compare valuation/profitability ratios across the peers in a sector, for a quick relative-value view.",
228
335
  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()
336
+ sector: z2.string().describe("Sector name, e.g. 'Information Technology'."),
337
+ sort: z2.string().optional().describe("Sort key, e.g. market_cap, pe_ratio."),
338
+ limit: z2.number().int().positive().max(100).optional()
232
339
  }
233
340
  },
234
341
  async ({ sector, sort, limit }) => run(() => client.stocks.compare(sector, { sort, limit }))
@@ -239,8 +346,8 @@ function buildServer(options = {}) {
239
346
  title: "Search stocks",
240
347
  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
348
  inputSchema: {
242
- q: z.string().describe("Search text (symbol fragment or company name)."),
243
- limit: z.number().int().positive().max(50).optional()
349
+ q: z2.string().describe("Search text (symbol fragment or company name)."),
350
+ limit: z2.number().int().positive().max(50).optional()
244
351
  }
245
352
  },
246
353
  async ({ q, limit }) => run(() => client.search(q, { limit }))
@@ -251,8 +358,8 @@ function buildServer(options = {}) {
251
358
  title: "Get top market movers",
252
359
  description: "Ranked market movers: top gainers, top losers, or most-active stocks for the latest trading session. Use category to pick which.",
253
360
  inputSchema: {
254
- category: z.enum(["gainers", "losers", "active"]).optional().describe("Which ranking. Defaults to gainers."),
255
- limit: z.number().int().positive().max(100).optional()
361
+ category: z2.enum(["gainers", "losers", "active"]).optional().describe("Which ranking. Defaults to gainers."),
362
+ limit: z2.number().int().positive().max(100).optional()
256
363
  }
257
364
  },
258
365
  async ({ category, limit }) => run(() => client.movers({ category, limit }))
@@ -263,9 +370,9 @@ function buildServer(options = {}) {
263
370
  title: "Get price shockers",
264
371
  description: "Stocks that moved at least a threshold percentage vs the previous close, in either direction.",
265
372
  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()
373
+ minChangePct: z2.number().positive().optional().describe("Minimum abs % move. Default 5."),
374
+ direction: z2.enum(["up", "down", "both"]).optional(),
375
+ limit: z2.number().int().positive().max(100).optional()
269
376
  }
270
377
  },
271
378
  async ({ minChangePct, direction, limit }) => run(() => client.priceShockers({ minChangePct, direction, limit }))
@@ -276,13 +383,13 @@ function buildServer(options = {}) {
276
383
  title: "Run stock screener",
277
384
  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
385
  inputSchema: {
279
- filters: z.array(z.string()).optional().describe("Filter expressions like 'pe_ratio.lt.15'."),
280
- sector: z.string().optional(),
386
+ filters: z2.array(z2.string()).optional().describe("Filter expressions like 'pe_ratio.lt.15'."),
387
+ sector: z2.string().optional(),
281
388
  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()
389
+ sortBy: z2.string().optional().describe("Sort key, default market_cap."),
390
+ sortOrder: z2.enum(["asc", "desc"]).optional(),
391
+ page: z2.number().int().positive().optional(),
392
+ pageSize: z2.number().int().positive().max(100).optional()
286
393
  }
287
394
  },
288
395
  async ({ filters, sector, exchange: exchange2, sortBy, sortOrder, page, pageSize }) => run(
@@ -295,8 +402,8 @@ function buildServer(options = {}) {
295
402
  title: "List indices",
296
403
  description: "List market indices (NIFTY 50, SENSEX, sectoral indices, etc.) with their latest values.",
297
404
  inputSchema: {
298
- page: z.number().int().positive().optional(),
299
- pageSize: z.number().int().positive().max(200).optional()
405
+ page: z2.number().int().positive().optional(),
406
+ pageSize: z2.number().int().positive().max(200).optional()
300
407
  }
301
408
  },
302
409
  async ({ page, pageSize }) => run(() => client.indices.list({ page, pageSize }))
@@ -307,11 +414,11 @@ function buildServer(options = {}) {
307
414
  title: "Get historical index prices",
308
415
  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
416
  inputSchema: {
310
- name: z.string().describe("Index name, e.g. 'NIFTY 50'."),
417
+ name: z2.string().describe("Index name, e.g. 'NIFTY 50'."),
311
418
  fromDate,
312
419
  toDate,
313
- page: z.number().int().positive().optional(),
314
- pageSize: z.number().int().positive().max(500).optional()
420
+ page: z2.number().int().positive().optional(),
421
+ pageSize: z2.number().int().positive().max(500).optional()
315
422
  }
316
423
  },
317
424
  async ({ name, fromDate: fromDate2, toDate: toDate2, page, pageSize }) => run(() => client.indices.prices(name, { fromDate: fromDate2, toDate: toDate2, page, pageSize }))
@@ -324,8 +431,8 @@ function buildServer(options = {}) {
324
431
  inputSchema: {
325
432
  fromDate,
326
433
  toDate,
327
- limit: z.number().int().positive().max(500).optional(),
328
- latest: z.boolean().optional().describe("Return only the most recent day.")
434
+ limit: z2.number().int().positive().max(500).optional(),
435
+ latest: z2.boolean().optional().describe("Return only the most recent day.")
329
436
  }
330
437
  },
331
438
  async ({ fromDate: fromDate2, toDate: toDate2, limit, latest }) => run(() => client.market.fiiDii({ fromDate: fromDate2, toDate: toDate2, limit, latest }))
@@ -336,9 +443,9 @@ function buildServer(options = {}) {
336
443
  title: "Get market-wide bulk deals",
337
444
  description: "Bulk deals across the whole market for the latest sessions.",
338
445
  inputSchema: {
339
- buySell: z.enum(["BUY", "SELL"]).optional(),
340
- page: z.number().int().positive().optional(),
341
- pageSize: z.number().int().positive().max(100).optional()
446
+ buySell: z2.enum(["BUY", "SELL"]).optional(),
447
+ page: z2.number().int().positive().optional(),
448
+ pageSize: z2.number().int().positive().max(100).optional()
342
449
  }
343
450
  },
344
451
  async ({ buySell, page, pageSize }) => run(() => client.deals.bulk({ buySell, page, pageSize }))
@@ -349,9 +456,9 @@ function buildServer(options = {}) {
349
456
  title: "Get market-wide block deals",
350
457
  description: "Block deals across the whole market for the latest sessions.",
351
458
  inputSchema: {
352
- buySell: z.enum(["BUY", "SELL"]).optional(),
353
- page: z.number().int().positive().optional(),
354
- pageSize: z.number().int().positive().max(100).optional()
459
+ buySell: z2.enum(["BUY", "SELL"]).optional(),
460
+ page: z2.number().int().positive().optional(),
461
+ pageSize: z2.number().int().positive().max(100).optional()
355
462
  }
356
463
  },
357
464
  async ({ buySell, page, pageSize }) => run(() => client.deals.block({ buySell, page, pageSize }))
@@ -362,8 +469,8 @@ function buildServer(options = {}) {
362
469
  title: "Get market-wide insider trades",
363
470
  description: "SEBI PIT insider-trading disclosures across the whole market.",
364
471
  inputSchema: {
365
- page: z.number().int().positive().optional(),
366
- pageSize: z.number().int().positive().max(100).optional()
472
+ page: z2.number().int().positive().optional(),
473
+ pageSize: z2.number().int().positive().max(100).optional()
367
474
  }
368
475
  },
369
476
  async ({ page, pageSize }) => run(() => client.deals.insiderTrades({ page, pageSize }))
@@ -374,14 +481,14 @@ function buildServer(options = {}) {
374
481
  title: "Search mutual-fund schemes",
375
482
  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
483
  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()
484
+ q: z2.string().optional(),
485
+ amc: z2.string().optional(),
486
+ category: z2.string().optional(),
487
+ plan: z2.string().optional(),
488
+ option: z2.string().optional(),
489
+ isin: z2.string().optional(),
490
+ limit: z2.number().int().positive().max(200).optional(),
491
+ offset: z2.number().int().nonnegative().optional()
385
492
  }
386
493
  },
387
494
  async (args) => run(() => client.mutualFunds.listSchemes(args))
@@ -392,7 +499,7 @@ function buildServer(options = {}) {
392
499
  title: "Get mutual-fund scheme detail",
393
500
  description: "One scheme's identity plus its latest NAV (with as-of date).",
394
501
  inputSchema: {
395
- schemeCode: z.string().describe("AMFI scheme code.")
502
+ schemeCode: z2.string().describe("AMFI scheme code.")
396
503
  }
397
504
  },
398
505
  async ({ schemeCode }) => run(() => client.mutualFunds.getScheme(schemeCode))
@@ -403,10 +510,10 @@ function buildServer(options = {}) {
403
510
  title: "Get mutual-fund NAV history",
404
511
  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
512
  inputSchema: {
406
- schemeCode: z.string().describe("AMFI scheme code."),
513
+ schemeCode: z2.string().describe("AMFI scheme code."),
407
514
  fromDate,
408
515
  toDate,
409
- limit: z.number().int().positive().max(2e3).optional()
516
+ limit: z2.number().int().positive().max(2e3).optional()
410
517
  }
411
518
  },
412
519
  async ({ schemeCode, fromDate: fromDate2, toDate: toDate2, limit }) => run(() => client.mutualFunds.nav(schemeCode, { fromDate: fromDate2, toDate: toDate2, limit }))
@@ -417,7 +524,7 @@ function buildServer(options = {}) {
417
524
  title: "Get mutual-fund trailing returns",
418
525
  description: "Trailing returns (1m/3m/6m/1y/3y/5y + since-inception) computed from the scheme's NAV series (CAGR for >=1y horizons).",
419
526
  inputSchema: {
420
- schemeCode: z.string().describe("AMFI scheme code.")
527
+ schemeCode: z2.string().describe("AMFI scheme code.")
421
528
  }
422
529
  },
423
530
  async ({ schemeCode }) => run(() => client.mutualFunds.returns(schemeCode))
@@ -429,9 +536,9 @@ function buildServer(options = {}) {
429
536
  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
537
  inputSchema: {
431
538
  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()
539
+ month: z2.string().optional().describe("Portfolio month YYYY-MM-DD (month-end). Latest if omitted."),
540
+ page: z2.number().int().positive().optional(),
541
+ pageSize: z2.number().int().positive().max(200).optional()
435
542
  }
436
543
  },
437
544
  async ({ ticker: ticker2, month, page, pageSize }) => run(() => client.stocks.mfHoldings(ticker2, { month, page, pageSize }))
@@ -442,10 +549,10 @@ function buildServer(options = {}) {
442
549
  title: "List / search mutual-fund portfolios",
443
550
  description: "List/search the mutual-fund schemes we hold monthly PORTFOLIO data for (scheme discovery, to populate a picker). This is the INVERSE of get_mutual_fund_holdings: that answers 'which funds own stock X', this answers 'which schemes do we have portfolios for'. Each row is keyed by the holdings scheme-NAME string (NOT the AMFI scheme_code used by the scheme/NAV tools) and has no Direct/Regular distinction, since AMFI publishes monthly portfolios at the scheme level. Use the returned scheme_name with get_mutual_fund_portfolio_holdings.",
444
551
  inputSchema: {
445
- q: z.string().optional().describe("Substring match on scheme name."),
446
- fundHouse: z.string().optional().describe("Filter by fund house (substring)."),
447
- limit: z.number().int().positive().max(200).optional(),
448
- offset: z.number().int().nonnegative().optional()
552
+ q: z2.string().optional().describe("Substring match on scheme name."),
553
+ fundHouse: z2.string().optional().describe("Filter by fund house (substring)."),
554
+ limit: z2.number().int().positive().max(200).optional(),
555
+ offset: z2.number().int().nonnegative().optional()
449
556
  }
450
557
  },
451
558
  async ({ q, fundHouse, limit, offset }) => run(() => client.mutualFunds.listPortfolios({ q, fundHouse, limit, offset }))
@@ -456,10 +563,10 @@ function buildServer(options = {}) {
456
563
  title: "Get a mutual-fund scheme's portfolio holdings",
457
564
  description: "One scheme's equity PORTFOLIO for a month (the stocks it holds) with month-over-month quantity change. The INVERSE of get_mutual_fund_holdings (which funds own a stock). schemeName must match the stored holdings name exactly, as returned by list_mutual_fund_portfolios \u2014 it is the scheme-NAME string, NOT the AMFI scheme_code. month is 'YYYY-MM' (latest if omitted). An unknown scheme name returns an empty portfolio, not an error.",
458
565
  inputSchema: {
459
- schemeName: z.string().describe("Holdings scheme name, exactly as returned by list_mutual_fund_portfolios."),
460
- month: z.string().optional().describe("Portfolio month YYYY-MM. Latest if omitted."),
461
- page: z.number().int().positive().optional(),
462
- pageSize: z.number().int().positive().max(100).optional()
566
+ schemeName: z2.string().describe("Holdings scheme name, exactly as returned by list_mutual_fund_portfolios."),
567
+ month: z2.string().optional().describe("Portfolio month YYYY-MM. Latest if omitted."),
568
+ page: z2.number().int().positive().optional(),
569
+ pageSize: z2.number().int().positive().max(100).optional()
463
570
  }
464
571
  },
465
572
  async ({ schemeName, month, page, pageSize }) => run(() => client.mutualFunds.getPortfolioHoldings(schemeName, { month, page, pageSize }))
@@ -473,6 +580,10 @@ function buildServer(options = {}) {
473
580
  },
474
581
  async () => run(() => client.status())
475
582
  );
583
+ if (adminSecret) {
584
+ const baseUrl = sdkOptions.baseUrl ?? "https://bharatstockapi.com";
585
+ registerOperatorTools(server, { baseUrl, adminSecret, fetchImpl: operatorFetchImpl });
586
+ }
476
587
  return server;
477
588
  }
478
589
 
package/dist/http.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  buildServer,
4
4
  checkAuthorization
5
- } from "./chunk-LW5UFTIW.js";
5
+ } from "./chunk-NXHQTUBJ.js";
6
6
 
7
7
  // src/http.ts
8
8
  import { randomUUID } from "crypto";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,34 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { BharatStockOptions } from 'bharatstock';
3
3
 
4
+ /**
5
+ * OPERATOR mode for the BharatStock MCP server.
6
+ *
7
+ * These tools are for the operator ONLY — they manage support tickets and are
8
+ * NOT part of the consumer surface. They are registered only when a
9
+ * `BHARATSTOCK_ADMIN_SECRET` is configured (see index.ts), and they do NOT go
10
+ * through the consumer `bharatstock` SDK (which is X-API-Key scoped and knows
11
+ * nothing about tickets). Instead each tool makes a JSON-RPC `tools/call` to
12
+ * the hosted MCP endpoint `POST {baseUrl}/v1/mcp` with an `X-Admin-Secret`
13
+ * header — the server exposes the operator tool catalogue only to a request
14
+ * that carries a valid admin secret (see app/api/v1/mcp.py).
15
+ *
16
+ * So the wall is end-to-end: a normal (API-key) MCP session never sees these
17
+ * tools (they aren't registered), and even if a tool name were guessed, the
18
+ * hosted server rejects an operator tool without the admin secret.
19
+ */
20
+
21
+ type FetchLike = (input: string, init?: {
22
+ method?: string;
23
+ headers?: Record<string, string>;
24
+ body?: string;
25
+ }) => Promise<{
26
+ ok: boolean;
27
+ status: number;
28
+ text: () => Promise<string>;
29
+ json: () => Promise<unknown>;
30
+ }>;
31
+
4
32
  /**
5
33
  * Builds the BharatStock MCP server: a thin layer that exposes the
6
34
  * BharatStock REST API's consumer-facing (X-API-Key gated) endpoints as
@@ -14,14 +42,25 @@ import { BharatStockOptions } from 'bharatstock';
14
42
  */
15
43
 
16
44
  declare const SERVER_NAME = "bharatstock";
17
- declare const SERVER_VERSION = "0.1.2";
45
+ declare const SERVER_VERSION = "0.2.0";
46
+ /** Extra, non-consumer options for buildServer. When `adminSecret` is set the
47
+ * OPERATOR ticket tools are additionally registered (see operator.ts); they
48
+ * are absent otherwise, so a normal consumer session never sees them. */
49
+ interface BuildServerExtras {
50
+ /** Operator admin secret. When present, register the support-ticket tools. */
51
+ adminSecret?: string;
52
+ /** Injectable fetch for the operator tools' JSON-RPC calls (tests). Named
53
+ * distinctly from the SDK's own `fetchImpl` so the two don't collide when
54
+ * the options objects are intersected. */
55
+ operatorFetchImpl?: FetchLike;
56
+ }
18
57
  /**
19
58
  * Construct the MCP server and register all tools against a BharatStock
20
59
  * client. Exposed separately from the stdio wiring so tests can build a
21
60
  * server with an injected `fetchImpl` and drive it over an in-memory
22
61
  * transport (no network, no real key).
23
62
  */
24
- declare function buildServer(options?: BharatStockOptions): McpServer;
63
+ declare function buildServer(options?: BharatStockOptions & BuildServerExtras): McpServer;
25
64
 
26
65
  /**
27
66
  * Shared plan-gate check for both the stdio and HTTP transports.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  SERVER_VERSION,
5
5
  buildServer,
6
6
  checkAuthorization
7
- } from "./chunk-LW5UFTIW.js";
7
+ } from "./chunk-NXHQTUBJ.js";
8
8
 
9
9
  // src/index.ts
10
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -25,7 +25,11 @@ async function main() {
25
25
  `);
26
26
  process.exit(1);
27
27
  }
28
- const server = buildServer({ apiKey, baseUrl });
28
+ const adminSecret = process.env.BHARATSTOCK_ADMIN_SECRET || void 0;
29
+ if (adminSecret) {
30
+ process.stderr.write("bharatstock-mcp: operator mode ON (support-ticket tools registered).\n");
31
+ }
32
+ const server = buildServer({ apiKey, baseUrl, adminSecret });
29
33
  const transport = new StdioServerTransport();
30
34
  await server.connect(transport);
31
35
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "bharatstock-mcp",
3
- "version": "0.1.2",
4
- "description": "Official Model Context Protocol (MCP) server for the BharatStock API - reliable Indian stock market data (NSE/BSE) as AI-agent tools.",
3
+ "version": "0.2.0",
4
+ "mcpName": "com.bharatstockapi/bharatstock-mcp",
5
+ "description": "Official Model Context Protocol (MCP) server for the BharatStock API - Indian stock market data (NSE/BSE) as AI-agent tools: real-time quotes, EOD prices, financial statements, ratios, shareholding patterns (FII/DII/promoter), corporate actions, NIFTY/SENSEX indices, mutual fund holdings and NAVs, FII/DII activity, bulk/block deals, insider trades, and a fundamentals stock screener for Indian equities.",
5
6
  "license": "MIT",
6
7
  "type": "module",
7
8
  "bin": {
@@ -16,13 +17,32 @@
16
17
  "keywords": [
17
18
  "mcp",
18
19
  "model-context-protocol",
20
+ "mcp-server",
19
21
  "nse",
20
22
  "bse",
23
+ "nifty",
24
+ "sensex",
21
25
  "india",
26
+ "indian-stocks",
27
+ "indian-stock-market",
22
28
  "stocks",
29
+ "stock-market",
30
+ "stock-api",
31
+ "stock-screener",
32
+ "equities",
33
+ "mutual-funds",
34
+ "nav",
35
+ "shareholding",
36
+ "fii-dii",
37
+ "corporate-actions",
38
+ "financials",
39
+ "fundamentals",
23
40
  "market-data",
24
41
  "finance",
25
- "ai-tools"
42
+ "trading",
43
+ "investing",
44
+ "ai-tools",
45
+ "llm"
26
46
  ],
27
47
  "homepage": "https://bharatstockapi.com",
28
48
  "engines": {
@@ -35,7 +55,7 @@
35
55
  },
36
56
  "dependencies": {
37
57
  "@modelcontextprotocol/sdk": "^1.12.0",
38
- "bharatstock": "^0.1.11",
58
+ "bharatstock": "^0.1.12",
39
59
  "zod": "^3.23.8"
40
60
  },
41
61
  "devDependencies": {