@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 +100 -8
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +83 -0
- package/dist/commands/orders.js +75 -1
- package/dist/commands/register.js +3 -0
- package/dist/core/mcp.d.ts +56 -0
- package/dist/core/mcp.js +166 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run.js +1 -1
- package/dist/safety.d.ts +8 -0
- package/dist/safety.js +9 -1
- package/package.json +8 -3
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
|
-
|
|
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,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
|
+
};
|
package/dist/commands/orders.js
CHANGED
|
@@ -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 {};
|
package/dist/core/mcp.js
ADDED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
"description": "Unofficial
|
|
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",
|