@pungoyal/kite-cli 0.3.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 +101 -9
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +74 -5
- package/dist/commands/completion.d.ts +35 -0
- package/dist/commands/completion.js +240 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +190 -0
- 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.d.ts +25 -0
- package/dist/commands/register.js +78 -0
- package/dist/core/auth.d.ts +9 -0
- package/dist/core/auth.js +39 -0
- package/dist/core/client.js +8 -1
- 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 +8 -45
- package/dist/safety.d.ts +8 -0
- package/dist/safety.js +9 -1
- package/package.json +8 -3
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { Command, CommanderError } from 'commander';
|
|
3
|
+
import { applyGlobalOptions, registerCommands } from './commands/register.js';
|
|
3
4
|
import { createContext } from './context.js';
|
|
4
5
|
import { AbortedError, ExitCode, KiteCliError } from './core/errors.js';
|
|
5
6
|
import { redactString } from './core/redact.js';
|
|
@@ -34,18 +35,8 @@ export async function run(opts = {}) {
|
|
|
34
35
|
let exitCode = ExitCode.Ok;
|
|
35
36
|
program
|
|
36
37
|
.name('kite')
|
|
37
|
-
.description('Unofficial
|
|
38
|
+
.description('Unofficial, safety-first CLI for the Zerodha Kite Connect v3 API (not affiliated with Zerodha)')
|
|
38
39
|
.version(VERSION, '-v, --version')
|
|
39
|
-
.option('--json', 'Emit JSON instead of formatted tables')
|
|
40
|
-
.option('--color <when>', 'Colour output: auto, always, or never')
|
|
41
|
-
// Deliberately no `-q` short form: it would shadow `-q, --quantity` on the
|
|
42
|
-
// order and GTT subcommands, which is typed far more often than --quiet.
|
|
43
|
-
.option('--quiet', 'Suppress informational messages')
|
|
44
|
-
.option('--debug', 'Print redacted request diagnostics to stderr')
|
|
45
|
-
.option('--env <env>', 'Environment: production or sandbox')
|
|
46
|
-
.option('--profile <name>', 'Account profile to use (see `kite profiles`)')
|
|
47
|
-
.option('-y, --yes', 'Skip confirmation prompts (use with care)')
|
|
48
|
-
.option('--dry-run', 'Show what would happen without sending anything to Kite')
|
|
49
40
|
.showHelpAfterError('(run `kite --help` for usage)')
|
|
50
41
|
.configureOutput({
|
|
51
42
|
writeOut: (str) => (opts.streams?.stdout ?? process.stdout).write(str),
|
|
@@ -54,6 +45,7 @@ export async function run(opts = {}) {
|
|
|
54
45
|
// Throw instead of calling process.exit, so `run()` stays testable and
|
|
55
46
|
// always returns its exit code.
|
|
56
47
|
.exitOverride();
|
|
48
|
+
applyGlobalOptions(program);
|
|
57
49
|
/** Wraps a handler with context construction and error reporting. */
|
|
58
50
|
const withContext = (handler) => async (...args) => {
|
|
59
51
|
// Commander passes (…arguments, options, command).
|
|
@@ -68,40 +60,11 @@ export async function run(opts = {}) {
|
|
|
68
60
|
exitCode = reportError(err, ctx.io);
|
|
69
61
|
}
|
|
70
62
|
};
|
|
71
|
-
// Registered
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const { marketCommands } = await import('./commands/market.js');
|
|
77
|
-
const { orderCommands } = await import('./commands/orders.js');
|
|
78
|
-
const { gttCommands } = await import('./commands/gtt.js');
|
|
79
|
-
const { alertCommands } = await import('./commands/alerts.js');
|
|
80
|
-
const { marginCommands } = await import('./commands/margins.js');
|
|
81
|
-
const { mfCommands } = await import('./commands/mf.js');
|
|
82
|
-
const { watchCommands } = await import('./commands/watch.js');
|
|
83
|
-
const { configCommands } = await import('./commands/config.js');
|
|
84
|
-
// commandsGroup applies to every command registered after it, so the group
|
|
85
|
-
// is set immediately before each block. With ~25 commands this is the
|
|
86
|
-
// difference between a readable --help and a wall of text.
|
|
87
|
-
program.commandsGroup('Account:');
|
|
88
|
-
authCommands(program, withContext);
|
|
89
|
-
profileCommands(program, withContext);
|
|
90
|
-
program.commandsGroup('Portfolio:');
|
|
91
|
-
portfolioCommands(program, withContext);
|
|
92
|
-
program.commandsGroup('Mutual funds:');
|
|
93
|
-
mfCommands(program, withContext);
|
|
94
|
-
program.commandsGroup('Market data:');
|
|
95
|
-
marketCommands(program, withContext);
|
|
96
|
-
program.commandsGroup('Trading:');
|
|
97
|
-
orderCommands(program, withContext);
|
|
98
|
-
gttCommands(program, withContext);
|
|
99
|
-
alertCommands(program, withContext);
|
|
100
|
-
marginCommands(program, withContext);
|
|
101
|
-
program.commandsGroup('Streaming:');
|
|
102
|
-
watchCommands(program, withContext);
|
|
103
|
-
program.commandsGroup('Settings:');
|
|
104
|
-
configCommands(program, withContext);
|
|
63
|
+
// Registered through the shared registration path (see commands/register.ts)
|
|
64
|
+
// so run() and `kite completion` build the exact same command tree. Modules
|
|
65
|
+
// are still imported lazily inside registerCommands, so a bare `kite quote`
|
|
66
|
+
// never pays to parse the dashboard renderer or the ticker up front.
|
|
67
|
+
await registerCommands(program, withContext);
|
|
105
68
|
program.addHelpText('after', `
|
|
106
69
|
Examples:
|
|
107
70
|
$ kite login Authenticate and store a session
|
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",
|