@pungoyal/kite-cli 0.4.0 → 0.5.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 CHANGED
@@ -28,6 +28,52 @@ $ kite holdings
28
28
  Day's change +₹287.40
29
29
  ```
30
30
 
31
+ ## Why you can trust it
32
+
33
+ It places real orders with real money, under an unofficial banner — so the safety
34
+ is built into the architecture, and every claim below is verifiable rather than
35
+ aspirational:
36
+
37
+ - **Try it risk-free first.** Zerodha runs a public sandbox with fake money and no
38
+ subscription. `kite login --env sandbox` gives you a full session to rehearse
39
+ every command against before you ever point it at your own account.
40
+ - **It never silently moves money.** Every order previews the *resolved* order —
41
+ the actual contract, lot size and computed value, not an echo of your flags —
42
+ and waits for confirmation. There is deliberately no config key that turns that
43
+ off ([Safety](#safety)).
44
+ - **It never blindly retries a write.** Kite has no idempotency key, so a
45
+ timed-out order is genuinely ambiguous. Rather than retry, the CLI tags every
46
+ order and reconciles against the orderbook to tell you what actually happened
47
+ ([Safety](#safety)).
48
+ - **Your secrets stay put.** The API secret lives in your OS keyring (or an
49
+ encrypted file), is never accepted as a command-line argument, and is scrubbed
50
+ from every log, error and stack trace — with [tests](https://github.com/pungoyal/kite-cli/blob/main/test/redact.test.ts)
51
+ that prove it ([Security](#security)).
52
+ - **Verifiable builds.** Published only from CI via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/)
53
+ (OIDC, no long-lived token). Check the provenance yourself with `npm audit signatures`.
54
+ - **A small, auditable surface.** ~10 direct dependencies, most of them
55
+ zero-dependency, enforced by a dependency budget in CI.
56
+
57
+ ## How it compares
58
+
59
+ Zerodha maintains excellent official SDKs — [`pykiteconnect`](https://github.com/zerodha/pykiteconnect)
60
+ and [`kiteconnectjs`](https://github.com/zerodha/kiteconnectjs). If you're building an
61
+ application, reach for those: they give you the full API, programmatically, and they're
62
+ the foundation the whole ecosystem is built on. `kite-cli` is complementary — the same
63
+ API as a ready-to-use tool, for when you'd rather not write code:
64
+
65
+ - **Zero code for everyday use.** `kite holdings`, `kite watch --holdings`,
66
+ `kite orders place …` run straight from the shell — and from any language that can
67
+ shell out.
68
+ - **An opinionated safety layer.** A kill switch, per-order value cap, resolved-order
69
+ confirmation, and unique-tag reconciliation for ambiguous writes come built in —
70
+ decisions the official SDKs deliberately leave open so each application can make its
71
+ own.
72
+ - **Composable output.** Every command speaks `--json` on stdout, so it drops
73
+ straight into `jq`, cron jobs, and pipelines.
74
+ - **A library too, when you need one.** The same client is [exported](#library-use),
75
+ so you can start in the shell and drop into code without switching tools.
76
+
31
77
  ## Install
32
78
 
33
79
  ```bash
@@ -38,7 +84,21 @@ Requires **Node 22.12 or newer**.
38
84
 
39
85
  ## Getting started
40
86
 
41
- You need a [Kite Connect](https://developers.kite.trade) app to get an API key and secret. Set your app's redirect URL to:
87
+ **Kick the tyres first no account, no subscription, no risk.** Zerodha runs a
88
+ public sandbox with fake money, and every command behaves exactly as it does
89
+ against a real account:
90
+
91
+ ```bash
92
+ kite login --env sandbox
93
+ kite --env sandbox holdings
94
+ kite --env sandbox orders place NSE:INFY --side BUY --quantity 1 --dry-run
95
+ ```
96
+
97
+ Learn the commands and see the confirmations there before any real money is
98
+ involved.
99
+
100
+ **Then connect your own account.** You need a [Kite Connect](https://developers.kite.trade)
101
+ app to get an API key and secret. Set your app's redirect URL to:
42
102
 
43
103
  ```
44
104
  http://127.0.0.1:51101/callback
@@ -52,13 +112,6 @@ kite login
52
112
 
53
113
  This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. The login URL is also printed to the terminal — press `c` while it's waiting to copy it to your clipboard (handy if the browser didn't open, or you want to log in on another device). Your API secret goes into your OS keyring; the daily access token is stored alongside it.
54
114
 
55
- **Want to try it without a subscription or real money?** Zerodha runs a public sandbox:
56
-
57
- ```bash
58
- kite login --env sandbox
59
- kite --env sandbox holdings
60
- ```
61
-
62
115
  **Running more than one Zerodha account?** See [Multiple accounts](#multiple-accounts).
63
116
 
64
117
  ## Usage
@@ -147,6 +200,7 @@ kite orders list --open # working orders only
147
200
  kite orders get 250720000123456 # full state history and fills
148
201
  kite orders modify <id> --price 1520
149
202
  kite orders cancel <id>
203
+ kite orders reconcile <tag> # did an order that seemed to fail actually reach Kite?
150
204
  kite trades # today's fills
151
205
 
152
206
  kite gtt list
@@ -245,6 +299,31 @@ kite whoami --json || kite login # exit code 3 means "no session"
245
299
 
246
300
  `NO_COLOR` is honoured, and colour is disabled automatically when stdout is not a TTY.
247
301
 
302
+ ### Agents (MCP)
303
+
304
+ `kite mcp` exposes Kite's **read-only** endpoints to an LLM agent over the
305
+ [Model Context Protocol](https://modelcontextprotocol.io), so Claude — or any MCP
306
+ client — can answer "how's my portfolio doing?" against live data. It can read
307
+ your profile, holdings, positions, funds, orders, trades, quotes and instruments;
308
+ it **cannot** place, modify or cancel anything. Trading stays at a
309
+ human-confirmed terminal, by design.
310
+
311
+ Point an MCP client at it — try the sandbox first:
312
+
313
+ ```json
314
+ {
315
+ "mcpServers": {
316
+ "kite": {
317
+ "command": "kite",
318
+ "args": ["mcp", "--env", "sandbox"]
319
+ }
320
+ }
321
+ }
322
+ ```
323
+
324
+ Drop the `--env sandbox` args to run against your real account (still read-only).
325
+ The server needs a live session, so `kite login` first.
326
+
248
327
  ## Multiple accounts
249
328
 
250
329
  If you run more than one Zerodha account — your own, a family member's, an HUF — each
@@ -329,6 +408,17 @@ $ kite orders place NSE:INFY -s BUY -q 10 --type LIMIT --price 1500 --yes
329
408
  · It was not placed twice. Do not re-run this command.
330
409
  ```
331
410
 
411
+ That reconciliation happens automatically, but only while the placing process is alive. If you lose it — a killed shell, a crashed script, a slept laptop — run it again on demand:
412
+
413
+ ```console
414
+ $ kite orders reconcile kcmrt88o648c1bce
415
+ ✓ Found an order for tag kcmrt88o648c1bce:
416
+ … COMPLETE …
417
+ · If placing this order looked like it failed, it went through — do not place it again.
418
+ ```
419
+
420
+ With no tag, `kite orders reconcile` lists the orders this CLI placed today, and `--json` carries a `placed` boolean for scripts.
421
+
332
422
  Automatic retries are restricted to `GET`/`HEAD` at the transport layer. `POST`, `PUT` and `DELETE` are never retried — in this API those are place, modify and cancel.
333
423
 
334
424
  ## Security
@@ -394,6 +484,8 @@ searchable site at **[pungoyal.github.io/kite-cli](https://pungoyal.github.io/ki
394
484
 
395
485
  - [Safety model](https://pungoyal.github.io/kite-cli/safety) — the full layered safety model (kill
396
486
  switch, value cap, confirmation escalation, order-tag reconciliation).
487
+ - [MCP server](https://pungoyal.github.io/kite-cli/mcp) — the read-only Model Context Protocol
488
+ server for LLM agents: its tools, setup, and why it exposes no writes.
397
489
  - [Configuration](https://pungoyal.github.io/kite-cli/configuration) — every config key and
398
490
  environment variable, with precedence.
399
491
  - [Troubleshooting](https://pungoyal.github.io/kite-cli/troubleshooting) — symptom-first fixes
@@ -0,0 +1,2 @@
1
+ import type { CommandFactory } from './types.js';
2
+ export declare const mcpCommands: CommandFactory;
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ import { McpServer } from '../core/mcp.js';
4
+ /**
5
+ * `kite mcp` — a read-only Model Context Protocol server over stdio.
6
+ *
7
+ * Launched by an MCP client (Claude and others) as a subprocess, it lets an
8
+ * agent inspect a Kite account — holdings, positions, funds, live quotes, the
9
+ * orderbook, instrument search — but never place, modify or cancel an order.
10
+ * The rationale for read-only lives in src/core/mcp.ts.
11
+ *
12
+ * The stdio contract is strict: the JSON-RPC protocol owns stdout, so every
13
+ * human-facing message here goes to stderr (io.info/note already do).
14
+ */
15
+ // Single source of truth for the version, like run.ts. package.json sits two
16
+ // levels up from both src/commands/ (dev) and dist/commands/ (published).
17
+ const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
18
+ /** Instruments accepted by quote-style tools, e.g. ["NSE:INFY", "NSE:TCS"]. */
19
+ const instrumentsArg = z.object({
20
+ instruments: z.array(z.string().min(1)).min(1).describe('Instruments as EXCHANGE:TRADINGSYMBOL, e.g. NSE:INFY'),
21
+ });
22
+ const searchArg = z.object({
23
+ query: z.string().min(1).describe('Free-text query over trading symbol and name'),
24
+ exchange: z.string().optional().describe('Restrict to an exchange, e.g. NSE, NFO'),
25
+ type: z.string().optional().describe('Restrict to an instrument type, e.g. EQ, CE, PE, FUT'),
26
+ limit: z.number().int().positive().max(100).optional().describe('Maximum results (default 25)'),
27
+ });
28
+ /** Build one tool, deriving its advertised JSON Schema from the zod schema. */
29
+ function tool(name, description, schema, handler) {
30
+ return {
31
+ name,
32
+ description,
33
+ schema,
34
+ inputSchema: z.toJSONSchema(schema),
35
+ handler,
36
+ };
37
+ }
38
+ /** The read-only Kite surface exposed to an agent. */
39
+ function buildTools(ctx) {
40
+ const { api, instruments, signal } = ctx;
41
+ const noArgs = z.object({});
42
+ return [
43
+ tool('get_profile', 'The authenticated Kite user profile.', noArgs, () => api.getProfile(signal)),
44
+ tool('get_holdings', 'Long-term holdings with average price and P&L.', noArgs, () => api.getHoldings(signal)),
45
+ tool('get_positions', 'Open positions (intraday and overnight).', noArgs, () => api.getPositions(signal)),
46
+ tool('get_funds', 'Available margins and cash balance.', noArgs, () => api.getMargins(signal)),
47
+ tool('get_orders', "Today's orderbook, including status and fills.", noArgs, () => api.getOrders(signal)),
48
+ tool('get_trades', "Today's executed trades.", noArgs, () => api.getTrades(signal)),
49
+ tool('quote', 'Full market quotes for one or more instruments.', instrumentsArg, (a) => api.getQuote(a.instruments, signal)),
50
+ tool('ltp', 'Last traded price for one or more instruments.', instrumentsArg, (a) => api.getLtp(a.instruments, signal)),
51
+ tool('ohlc', 'Open/high/low/close plus last price for one or more instruments.', instrumentsArg, (a) => api.getOhlc(a.instruments, signal)),
52
+ tool('search_instruments', 'Find tradeable instruments by symbol or name.', searchArg, async (a) => {
53
+ await instruments.load({ signal });
54
+ return instruments.search(a.query, {
55
+ exchange: a.exchange,
56
+ type: a.type,
57
+ ...(a.limit !== undefined ? { limit: a.limit } : {}),
58
+ });
59
+ }),
60
+ ];
61
+ }
62
+ async function mcp(ctx) {
63
+ // A live session is required — every tool hits the API. Fail here (exit 3)
64
+ // rather than starting a server whose every call would 403.
65
+ ctx.requireSession();
66
+ const server = new McpServer({
67
+ name: 'kite-cli',
68
+ version: VERSION,
69
+ tools: buildTools(ctx),
70
+ signal: ctx.signal,
71
+ });
72
+ ctx.io.info('kite MCP server ready on stdio (read-only). Launch this from an MCP client; Ctrl-C to stop.');
73
+ if (ctx.env === 'sandbox')
74
+ ctx.io.info('Running against the sandbox — no real account.');
75
+ // stdio is the transport: the process's own stdin/stdout carry JSON-RPC.
76
+ await server.serve(process.stdin, process.stdout);
77
+ }
78
+ export const mcpCommands = (program, run) => {
79
+ program
80
+ .command('mcp')
81
+ .description('Run a read-only MCP server over stdio for LLM agents (Claude and others)')
82
+ .action(run(mcp));
83
+ };
@@ -5,7 +5,7 @@ import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js'
5
5
  import { TERMINAL_ORDER_STATUSES } from '../core/schemas.js';
6
6
  import { dateTime, money, quantity, rupees, timeOnly } from '../output/format.js';
7
7
  import { heading, printTable, renderKeyValue, renderTable } from '../output/table.js';
8
- import { assertTradingEnabled, buildOrderTag, confirmAction } from '../safety.js';
8
+ import { assertTradingEnabled, buildOrderTag, CLI_TAG_PREFIX, confirmAction } from '../safety.js';
9
9
  export const orderCommands = (program, run) => {
10
10
  const orders = program.command('orders').description('View and manage orders');
11
11
  orders
@@ -18,6 +18,11 @@ export const orderCommands = (program, run) => {
18
18
  .description('Show the full state history of one order')
19
19
  .argument('<order-id>')
20
20
  .action(run(getOrder));
21
+ orders
22
+ .command('reconcile')
23
+ .description('Check whether a tagged order reached Kite (recovery after an ambiguous failure)')
24
+ .argument('[tag]', 'Order tag to look up; omit to list the orders this CLI placed today')
25
+ .action(run(reconcileOrders));
21
26
  orders
22
27
  .command('place')
23
28
  .description('Place an order')
@@ -473,6 +478,75 @@ async function reconcileAfterFailure(ctx, tag, cause) {
473
478
  process.exitCode = ExitCode.Upstream;
474
479
  }
475
480
  }
481
+ /**
482
+ * Standalone, after-the-fact reconciliation for the no-idempotency problem.
483
+ *
484
+ * `orders place` reconciles automatically the instant a placement fails (see
485
+ * {@link reconcileAfterFailure}), but that check lives and dies with the
486
+ * process — a killed shell, a crashed script, a slept laptop and it is gone.
487
+ * This re-runs it on demand. Given the unique tag every order carries, it
488
+ * answers the only question that matters after an ambiguous failure — "did it
489
+ * actually reach Kite?" — so you know whether it is safe to place again.
490
+ *
491
+ * With a tag, it looks that tag up. With none, it lists the orders this CLI
492
+ * placed today (those carrying the {@link CLI_TAG_PREFIX} prefix), so you can
493
+ * still find one whose exact tag you did not capture. It is a query, not a
494
+ * mutation: it exits 0 on any clean answer, and the `--json` `placed` flag is
495
+ * the machine-readable verdict.
496
+ */
497
+ async function reconcileOrders(ctx, _opts, command) {
498
+ ctx.requireSession();
499
+ const { io } = ctx;
500
+ const tag = command.args[0];
501
+ if (tag) {
502
+ const matches = await ctx.api.findOrderByTag(tag, ctx.signal);
503
+ if (io.json) {
504
+ io.writeJson({ tag, placed: matches.length > 0, order_ids: matches.map((o) => o.order_id), orders: matches });
505
+ return;
506
+ }
507
+ if (matches.length === 0) {
508
+ io.warn(`No order found for tag ${io.bold(tag)}.`);
509
+ io.info('If a placement looked like it failed, it most likely did not reach Kite — but check `kite orders list` before retrying, in case it simply has not appeared yet.');
510
+ return;
511
+ }
512
+ io.success(`Found ${matches.length === 1 ? 'an order' : `${matches.length} orders`} for tag ${io.bold(tag)}:`);
513
+ io.line(renderTableFor(ctx, matches, reconcileColumns()));
514
+ io.info('If placing this order looked like it failed, it went through — do not place it again.');
515
+ return;
516
+ }
517
+ const mine = (await ctx.api.getOrders(ctx.signal)).filter(hasCliTag);
518
+ printTable(io, mine, reconcileColumns(), mine, {
519
+ compact: ctx.config.output.compact,
520
+ empty: `No orders tagged by this CLI today (looking for the \`${CLI_TAG_PREFIX}\` prefix).`,
521
+ });
522
+ if (mine.length > 0 && !io.json) {
523
+ io.info('Reconcile a specific one with `kite orders reconcile <tag>`.');
524
+ }
525
+ }
526
+ /** True if this order carries a CLI-generated tag (see {@link CLI_TAG_PREFIX}). */
527
+ function hasCliTag(order) {
528
+ if (order.tag?.startsWith(CLI_TAG_PREFIX))
529
+ return true;
530
+ return order.tags?.some((t) => t.startsWith(CLI_TAG_PREFIX)) ?? false;
531
+ }
532
+ function reconcileColumns() {
533
+ return [
534
+ { header: 'Time', value: (o) => timeOnly(o.order_timestamp) },
535
+ { header: 'Order ID', value: (o, io) => io.dim(o.order_id) },
536
+ { header: 'Symbol', value: (o, io) => io.bold(o.tradingsymbol ?? '—') },
537
+ {
538
+ header: 'Side',
539
+ value: (o, io) => o.transaction_type === 'BUY' ? io.green('BUY') : o.transaction_type === 'SELL' ? io.red('SELL') : '—',
540
+ },
541
+ {
542
+ header: 'Qty',
543
+ value: (o) => `${quantity(o.filled_quantity ?? 0)}/${quantity(o.quantity ?? 0)}`,
544
+ align: 'right',
545
+ },
546
+ { header: 'Status', value: (o, io) => colourStatus(io, o.status) },
547
+ { header: 'Tag', value: (o) => o.tag ?? '—' },
548
+ ];
549
+ }
476
550
  // ---------------------------------------------------------------------------
477
551
  // Modify / cancel
478
552
  // ---------------------------------------------------------------------------
@@ -49,6 +49,7 @@ export async function registerCommands(program, run) {
49
49
  const { configCommands } = await import('./config.js');
50
50
  const { doctorCommands } = await import('./doctor.js');
51
51
  const { completionCommands } = await import('./completion.js');
52
+ const { mcpCommands } = await import('./mcp.js');
52
53
  // commandsGroup applies to every command registered after it, so the group
53
54
  // is set immediately before each block. With ~25 commands this is the
54
55
  // difference between a readable --help and a wall of text.
@@ -68,6 +69,8 @@ export async function registerCommands(program, run) {
68
69
  marginCommands(program, run);
69
70
  program.commandsGroup('Streaming:');
70
71
  watchCommands(program, run);
72
+ program.commandsGroup('Integrations:');
73
+ mcpCommands(program, run);
71
74
  program.commandsGroup('Settings:');
72
75
  configCommands(program, run);
73
76
  doctorCommands(program, run);
@@ -0,0 +1,56 @@
1
+ import type { z } from 'zod';
2
+ /** A tool the agent may call. `schema` validates the arguments before dispatch. */
3
+ export interface McpTool<S extends z.ZodType = z.ZodType> {
4
+ name: string;
5
+ description: string;
6
+ schema: S;
7
+ /** JSON Schema for the arguments, advertised in `tools/list`. */
8
+ inputSchema: Record<string, unknown>;
9
+ handler: (args: z.infer<S>, signal?: AbortSignal) => Promise<unknown> | unknown;
10
+ }
11
+ export interface McpServerOptions {
12
+ name: string;
13
+ version: string;
14
+ tools: McpTool[];
15
+ /** Aborting this stops the serve loop after the in-flight message. */
16
+ signal?: AbortSignal | undefined;
17
+ }
18
+ interface JsonRpcRequest {
19
+ jsonrpc?: unknown;
20
+ id?: string | number | null;
21
+ method?: unknown;
22
+ params?: unknown;
23
+ }
24
+ interface JsonRpcResponse {
25
+ jsonrpc: '2.0';
26
+ id: string | number | null;
27
+ result?: unknown;
28
+ error?: {
29
+ code: number;
30
+ message: string;
31
+ data?: unknown;
32
+ };
33
+ }
34
+ export declare class McpServer {
35
+ private readonly tools;
36
+ private readonly name;
37
+ private readonly version;
38
+ private readonly signal;
39
+ constructor(opts: McpServerOptions);
40
+ /**
41
+ * Handle a single decoded JSON-RPC message.
42
+ *
43
+ * Returns the response to write back, or `null` for a notification (a request
44
+ * with no `id`), which by JSON-RPC must never be answered — even on error.
45
+ */
46
+ handle(request: JsonRpcRequest): Promise<JsonRpcResponse | null>;
47
+ private initializeResult;
48
+ private toolList;
49
+ private callTool;
50
+ /**
51
+ * Read newline-delimited JSON-RPC from `input` and write responses to
52
+ * `output`, until the input closes or the abort signal fires.
53
+ */
54
+ serve(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<void>;
55
+ }
56
+ export {};
@@ -0,0 +1,166 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { redact, redactString } from './redact.js';
3
+ /**
4
+ * A minimal Model Context Protocol (MCP) server over stdio.
5
+ *
6
+ * MCP lets an LLM agent (Claude and others) call tools exposed by a local
7
+ * process. This server exposes Kite's *read* endpoints so an agent can inspect a
8
+ * portfolio, quotes and the orderbook — never place, modify or cancel anything.
9
+ *
10
+ * Two deliberate design choices, both flowing from this repo's own principles:
11
+ *
12
+ * - **Hand-rolled, not the official SDK.** A minimal dependency tree is a
13
+ * security property here (see scripts/check-deps.mjs), and the SDK pulls a
14
+ * heavy transitive closure. The stdio transport is newline-delimited JSON-RPC
15
+ * 2.0 — small enough to implement directly and unit-test in full.
16
+ * - **Read-only.** A money-moving command must render the resolved order and
17
+ * obtain confirmation (see src/safety.ts), and an MCP server has no terminal
18
+ * to confirm at. Rather than weaken that invariant, writes are simply not
19
+ * exposed. An agent that wants to trade must hand back to a human at a TTY.
20
+ *
21
+ * The transport framing is newline-delimited JSON: one JSON message per line,
22
+ * no embedded newlines, no Content-Length headers (that is the HTTP transport,
23
+ * not stdio).
24
+ */
25
+ /** Latest protocol revision this server speaks. Echoed back if a client asks. */
26
+ const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
27
+ // Standard JSON-RPC 2.0 error codes.
28
+ const PARSE_ERROR = -32700;
29
+ const INVALID_REQUEST = -32600;
30
+ const METHOD_NOT_FOUND = -32601;
31
+ const INTERNAL_ERROR = -32603;
32
+ export class McpServer {
33
+ tools;
34
+ name;
35
+ version;
36
+ signal;
37
+ constructor(opts) {
38
+ this.tools = opts.tools;
39
+ this.name = opts.name;
40
+ this.version = opts.version;
41
+ this.signal = opts.signal;
42
+ }
43
+ /**
44
+ * Handle a single decoded JSON-RPC message.
45
+ *
46
+ * Returns the response to write back, or `null` for a notification (a request
47
+ * with no `id`), which by JSON-RPC must never be answered — even on error.
48
+ */
49
+ async handle(request) {
50
+ const isNotification = request?.id === undefined;
51
+ const id = request?.id ?? null;
52
+ if (request?.jsonrpc !== '2.0' || typeof request.method !== 'string') {
53
+ return isNotification ? null : error(id, INVALID_REQUEST, 'Invalid Request');
54
+ }
55
+ const { method, params } = request;
56
+ try {
57
+ switch (method) {
58
+ case 'initialize':
59
+ return ok(id, this.initializeResult(params));
60
+ case 'ping':
61
+ return ok(id, {});
62
+ case 'tools/list':
63
+ return ok(id, { tools: this.toolList() });
64
+ case 'tools/call':
65
+ return ok(id, await this.callTool(params));
66
+ default:
67
+ // Unknown notifications (e.g. notifications/initialized, .../cancelled)
68
+ // are ignored; unknown requests get a method-not-found error.
69
+ return isNotification ? null : error(id, METHOD_NOT_FOUND, `Method not found: ${method}`);
70
+ }
71
+ }
72
+ catch (err) {
73
+ // A thrown error here is an internal fault, not a tool-level failure (those
74
+ // are reported in-band via isError). Redact: the message could echo input.
75
+ return isNotification ? null : error(id, INTERNAL_ERROR, redactString(messageOf(err)));
76
+ }
77
+ }
78
+ initializeResult(params) {
79
+ // Speak the client's requested revision when it names one, so we interoperate
80
+ // with older clients; otherwise advertise our latest.
81
+ const requested = params?.protocolVersion;
82
+ return {
83
+ protocolVersion: typeof requested === 'string' ? requested : DEFAULT_PROTOCOL_VERSION,
84
+ capabilities: { tools: {} },
85
+ serverInfo: { name: this.name, version: this.version },
86
+ };
87
+ }
88
+ toolList() {
89
+ return this.tools.map((tool) => ({
90
+ name: tool.name,
91
+ description: tool.description,
92
+ inputSchema: tool.inputSchema,
93
+ }));
94
+ }
95
+ async callTool(params) {
96
+ const name = params?.name;
97
+ const tool = this.tools.find((t) => t.name === name);
98
+ if (!tool) {
99
+ return toolError(`Unknown tool: ${typeof name === 'string' ? name : String(name)}`);
100
+ }
101
+ const rawArgs = params.arguments ?? {};
102
+ const parsed = tool.schema.safeParse(rawArgs);
103
+ if (!parsed.success) {
104
+ return toolError(`Invalid arguments for ${tool.name}: ${redactString(parsed.error.message)}`);
105
+ }
106
+ try {
107
+ const data = await tool.handler(parsed.data, this.signal);
108
+ // Redact tool output too: this is a fresh egress channel, and a Kite
109
+ // response or error could carry a value we treat as secret. (Redaction
110
+ // invariant — see src/core/redact.ts.)
111
+ return { content: [{ type: 'text', text: JSON.stringify(redact(data)) }] };
112
+ }
113
+ catch (err) {
114
+ // Tool failures are surfaced to the model in-band, so it can react (retry a
115
+ // different symbol, tell the user to log in), not as a transport error.
116
+ return toolError(redactString(messageOf(err)));
117
+ }
118
+ }
119
+ /**
120
+ * Read newline-delimited JSON-RPC from `input` and write responses to
121
+ * `output`, until the input closes or the abort signal fires.
122
+ */
123
+ async serve(input, output) {
124
+ const rl = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
125
+ const onAbort = () => rl.close();
126
+ this.signal?.addEventListener('abort', onAbort, { once: true });
127
+ try {
128
+ for await (const line of rl) {
129
+ const trimmed = line.trim();
130
+ if (trimmed === '')
131
+ continue;
132
+ let response;
133
+ let request;
134
+ try {
135
+ request = JSON.parse(trimmed);
136
+ }
137
+ catch {
138
+ response = error(null, PARSE_ERROR, 'Parse error');
139
+ output.write(`${JSON.stringify(response)}\n`);
140
+ continue;
141
+ }
142
+ response = await this.handle(request);
143
+ if (response)
144
+ output.write(`${JSON.stringify(response)}\n`);
145
+ if (this.signal?.aborted)
146
+ break;
147
+ }
148
+ }
149
+ finally {
150
+ this.signal?.removeEventListener('abort', onAbort);
151
+ rl.close();
152
+ }
153
+ }
154
+ }
155
+ function ok(id, result) {
156
+ return { jsonrpc: '2.0', id, result };
157
+ }
158
+ function error(id, code, message) {
159
+ return { jsonrpc: '2.0', id, error: { code, message } };
160
+ }
161
+ function toolError(text) {
162
+ return { content: [{ type: 'text', text }], isError: true };
163
+ }
164
+ function messageOf(err) {
165
+ return err instanceof Error ? err.message : String(err);
166
+ }
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { type ClientOptions, KiteClient, setDispatcher, } from './core/client.js
11
11
  export { type Endpoints, type Environment, endpointsFor, SANDBOX_CREDENTIALS, } from './core/config.js';
12
12
  export { AuthRequiredError, ExitCode, KiteApiError, KiteCliError, type KiteErrorType, NetworkError, UsageError, } from './core/errors.js';
13
13
  export { InstrumentStore, parseInstrumentKey, parseInstrumentsCsv, } from './core/instruments.js';
14
+ export { McpServer, type McpServerOptions, type McpTool, } from './core/mcp.js';
14
15
  export { DEFAULT_PROFILE, getProfile, listProfileNames, type ResolvedProfile, resolveProfile, resolveTradingConfig, SANDBOX_PROFILE, storagePrefixFor, } from './core/profiles.js';
15
16
  export { ORDER_LIMITS, type RateCategory, RateLimiter, } from './core/ratelimit.js';
16
17
  export { maskSecret, redact, redactString, redactUrl, registerSecret, } from './core/redact.js';
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { KiteClient, setDispatcher, } from './core/client.js';
11
11
  export { endpointsFor, SANDBOX_CREDENTIALS, } from './core/config.js';
12
12
  export { AuthRequiredError, ExitCode, KiteApiError, KiteCliError, NetworkError, UsageError, } from './core/errors.js';
13
13
  export { InstrumentStore, parseInstrumentKey, parseInstrumentsCsv, } from './core/instruments.js';
14
+ export { McpServer, } from './core/mcp.js';
14
15
  export { DEFAULT_PROFILE, getProfile, listProfileNames, resolveProfile, resolveTradingConfig, SANDBOX_PROFILE, storagePrefixFor, } from './core/profiles.js';
15
16
  export { ORDER_LIMITS, RateLimiter, } from './core/ratelimit.js';
16
17
  export { maskSecret, redact, redactString, redactUrl, registerSecret, } from './core/redact.js';
package/dist/run.js CHANGED
@@ -35,7 +35,7 @@ export async function run(opts = {}) {
35
35
  let exitCode = ExitCode.Ok;
36
36
  program
37
37
  .name('kite')
38
- .description('Unofficial command-line interface for the Zerodha Kite Connect API (not affiliated with Zerodha)')
38
+ .description('Unofficial, safety-first CLI for the Zerodha Kite Connect v3 API (not affiliated with Zerodha)')
39
39
  .version(VERSION, '-v, --version')
40
40
  .showHelpAfterError('(run `kite --help` for usage)')
41
41
  .configureOutput({
package/dist/safety.d.ts CHANGED
@@ -76,6 +76,14 @@ export declare function confirmAction(ctx: Context, opts: ConfirmOptions): Promi
76
76
  *
77
77
  * Kite caps `tag` at 20 alphanumeric characters.
78
78
  */
79
+ /**
80
+ * Prefix on every tag the CLI generates for an order it places.
81
+ *
82
+ * It lets the orderbook be filtered back to orders this tool placed — which is
83
+ * what `orders reconcile` (with no tag) uses to surface CLI-issued orders. Only
84
+ * the auto-generated tag carries it; a user-supplied `--tag` is kept verbatim.
85
+ */
86
+ export declare const CLI_TAG_PREFIX = "kc";
79
87
  export declare function generateOrderTag(prefix?: string): string;
80
88
  /**
81
89
  * Build the tag actually sent to Kite, guaranteeing uniqueness even when the
package/dist/safety.js CHANGED
@@ -138,7 +138,15 @@ function accountLine(ctx) {
138
138
  *
139
139
  * Kite caps `tag` at 20 alphanumeric characters.
140
140
  */
141
- export function generateOrderTag(prefix = 'kc') {
141
+ /**
142
+ * Prefix on every tag the CLI generates for an order it places.
143
+ *
144
+ * It lets the orderbook be filtered back to orders this tool placed — which is
145
+ * what `orders reconcile` (with no tag) uses to surface CLI-issued orders. Only
146
+ * the auto-generated tag carries it; a user-supplied `--tag` is kept verbatim.
147
+ */
148
+ export const CLI_TAG_PREFIX = 'kc';
149
+ export function generateOrderTag(prefix = CLI_TAG_PREFIX) {
142
150
  const random = randomBytes(6).toString('hex');
143
151
  const stamp = Date.now().toString(36);
144
152
  return `${prefix}${stamp}${random}`.slice(0, 20);
package/package.json CHANGED
@@ -1,16 +1,21 @@
1
1
  {
2
2
  "name": "@pungoyal/kite-cli",
3
- "version": "0.4.0",
4
- "description": "Unofficial command-line interface for the Zerodha Kite Connect API — not affiliated with or endorsed by Zerodha",
3
+ "version": "0.5.0",
4
+ "description": "Unofficial, safety-first CLI (and library) for the Zerodha Kite Connect v3 API — scriptable JSON, live streaming, and confirmations on anything that moves money. Not affiliated with or endorsed by Zerodha.",
5
5
  "keywords": [
6
6
  "zerodha",
7
7
  "kite",
8
8
  "kiteconnect",
9
+ "kite-connect",
9
10
  "trading",
11
+ "trading-api",
12
+ "algo-trading",
10
13
  "stocks",
11
14
  "cli",
15
+ "terminal",
12
16
  "nse",
13
- "bse"
17
+ "bse",
18
+ "nifty"
14
19
  ],
15
20
  "license": "MIT",
16
21
  "author": "Puneet Goyal",