bantamkit-mcp 0.28.0 → 0.29.1
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 +22 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +66 -2
- package/dist/hostinstall.d.ts +36 -0
- package/dist/hostinstall.js +247 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,7 +40,28 @@ float* below — it is the failure this package makes easiest to hit and hardest
|
|
|
40
40
|
|
|
41
41
|
### Connect it to a host
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
**One command, and it writes the entry for you:**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx -y bantamkit-mcp --install claude # Claude Code
|
|
47
|
+
npx -y bantamkit-mcp --install claude-desktop # Claude Desktop
|
|
48
|
+
npx -y bantamkit-mcp --install copilot # GitHub Copilot in VS Code
|
|
49
|
+
npx -y bantamkit-mcp --install cursor # Cursor
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`--install claude` runs `claude mcp add` rather than editing `~/.claude.json` directly:
|
|
53
|
+
that file is the host's, and it carries state that is not MCP configuration. What happens on
|
|
54
|
+
a second run there is Claude Code's decision, not this command's, and `--force` does not
|
|
55
|
+
reach it.
|
|
56
|
+
|
|
57
|
+
**For the other three**, which are edited directly: it never prompts. A second run that
|
|
58
|
+
finds its own entry says so and changes nothing; an entry that differs is refused, printed
|
|
59
|
+
beside the one it would write, and replaced only with `--force`. Every write backs the file
|
|
60
|
+
up first as `<name>.backup-<date>`, preserves the file's permissions, and a file that does
|
|
61
|
+
not parse is refused rather than replaced.
|
|
62
|
+
|
|
63
|
+
The rest of this section is what those commands write, for anyone who would rather do it by
|
|
64
|
+
hand. Every host runs the same command; only the file and the key around it change.
|
|
44
65
|
|
|
45
66
|
**Claude Code** — one command, no file to edit. `-s user` makes it available in every
|
|
46
67
|
project; drop it for this project only.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type Host } from './hostinstall.js';
|
|
2
3
|
export interface Options {
|
|
3
4
|
k: number;
|
|
4
5
|
indexBudget: number;
|
|
@@ -7,6 +8,8 @@ export interface Options {
|
|
|
7
8
|
assetsRoot: boolean;
|
|
8
9
|
mcpReport: boolean;
|
|
9
10
|
statusline: boolean;
|
|
11
|
+
install: Host | null;
|
|
12
|
+
force: boolean;
|
|
10
13
|
}
|
|
11
14
|
/** `_parse_args`, arm for arm, including the mutually exclusive group and `-h`. */
|
|
12
15
|
export declare function parseArgs(argv: readonly string[]): Options;
|
package/dist/cli.js
CHANGED
|
@@ -60,7 +60,9 @@ import { DEFAULT_INDEX_BUDGET } from './memory/store.js';
|
|
|
60
60
|
import { packFileCount } from './mcp/identity.js';
|
|
61
61
|
import { buildServer } from './mcp/server.js';
|
|
62
62
|
import { RawStdioTransport } from './mcp/transport.js';
|
|
63
|
-
import { ArgvError, formatHelp, formatUsageBlock, HelpRequested, helpWidth, parseArgs as parseWithSpec, pyIntStrict, } from './pyargparse.js';
|
|
63
|
+
import { ArgumentTypeError, ArgvError, formatHelp, formatUsageBlock, HelpRequested, helpWidth, parseArgs as parseWithSpec, pyIntStrict, } from './pyargparse.js';
|
|
64
|
+
import { pyRepr } from './memory/pyfs.js';
|
|
65
|
+
import { HOSTS, install as installHost, InstallError, thisCommand } from './hostinstall.js';
|
|
64
66
|
import { statusLine } from './statusline.js';
|
|
65
67
|
/** `SystemExit("...")`: the message on stderr, exit 1. */
|
|
66
68
|
class Refusal extends Error {
|
|
@@ -151,6 +153,40 @@ const PARSER = {
|
|
|
151
153
|
help: 'print one status line for a host status bar, then exit',
|
|
152
154
|
defaultValue: false,
|
|
153
155
|
},
|
|
156
|
+
// THE SAME PLACE AND THE SAME REASON AS THE TWO ABOVE, and the position is copied from
|
|
157
|
+
// the reference rather than chosen: argparse prints optionals in registration order, so
|
|
158
|
+
// this line decides where `[--install {...}]` sits in the generated usage. After
|
|
159
|
+
// `--statusline` leaves the FIRST line of the 80-column usage — pinned by the `cli`
|
|
160
|
+
// conformance suite — byte-identical.
|
|
161
|
+
//
|
|
162
|
+
// `choices` is NOT ported for ordinary options (see pyargparse.ts's not-ported list), and
|
|
163
|
+
// porting it for one flag would be a feature grown to serve a caller. The two things
|
|
164
|
+
// `choices` is visible through are reproduced directly instead: the metavar argparse
|
|
165
|
+
// builds, spelled out here, and the invalid-value sentence, thrown as an
|
|
166
|
+
// `ArgumentTypeError` so it prints VERBATIM after `argument --install: ` exactly as
|
|
167
|
+
// `_check_value` does.
|
|
168
|
+
{
|
|
169
|
+
optionStrings: ['--install'],
|
|
170
|
+
dest: 'install',
|
|
171
|
+
metavar: `{${HOSTS.join(',')}}`,
|
|
172
|
+
kind: 'store',
|
|
173
|
+
help: "wire this server into a host's MCP configuration, then exit",
|
|
174
|
+
convert: (raw) => {
|
|
175
|
+
if (!HOSTS.includes(raw)) {
|
|
176
|
+
throw new ArgumentTypeError(`invalid choice: ${pyRepr(raw)} (choose from ${HOSTS.join(', ')})`);
|
|
177
|
+
}
|
|
178
|
+
return raw;
|
|
179
|
+
},
|
|
180
|
+
typeName: 'str',
|
|
181
|
+
defaultValue: null,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
optionStrings: ['--force'],
|
|
185
|
+
dest: 'force',
|
|
186
|
+
kind: 'storeTrue',
|
|
187
|
+
help: 'with --install, replace an existing bantamkit entry',
|
|
188
|
+
defaultValue: false,
|
|
189
|
+
},
|
|
154
190
|
{
|
|
155
191
|
optionStrings: ['--store'],
|
|
156
192
|
dest: 'store',
|
|
@@ -166,7 +202,10 @@ const PARSER = {
|
|
|
166
202
|
defaultValue: null,
|
|
167
203
|
},
|
|
168
204
|
],
|
|
169
|
-
|
|
205
|
+
// `--store`/`--start` moved from 6/7 to 8/9 when `--install` and `--force` were added
|
|
206
|
+
// ahead of them. These are POSITIONS, not names, so adding an action above the group and
|
|
207
|
+
// leaving this line alone would silently make two unrelated flags mutually exclusive.
|
|
208
|
+
groups: [[8, 9]],
|
|
170
209
|
};
|
|
171
210
|
/** `_parse_args`, arm for arm, including the mutually exclusive group and `-h`. */
|
|
172
211
|
export function parseArgs(argv) {
|
|
@@ -179,6 +218,8 @@ export function parseArgs(argv) {
|
|
|
179
218
|
assetsRoot: values['assets_root'],
|
|
180
219
|
mcpReport: values['mcp_report'],
|
|
181
220
|
statusline: values['statusline'],
|
|
221
|
+
install: values['install'],
|
|
222
|
+
force: values['force'],
|
|
182
223
|
};
|
|
183
224
|
}
|
|
184
225
|
/** `_build_memory`, including the two refusals argparse cannot express. */
|
|
@@ -236,6 +277,29 @@ async function main(argv) {
|
|
|
236
277
|
process.stdout.write(`${statusLine(process.env, options.store, options.start)}\n`);
|
|
237
278
|
return 0;
|
|
238
279
|
}
|
|
280
|
+
// ORDER IS OBSERVABLE, and it is the reference's order. `main` in `mcpserver.py` checks
|
|
281
|
+
// assets_root, then mcp_report, then statusline, then install, then force — so
|
|
282
|
+
// `--mcp-report --install cursor` prints a report and writes NOTHING. Dispatching install
|
|
283
|
+
// earlier here made the same argv write a file on one runtime and not the other: one
|
|
284
|
+
// command line, two different states on the user's disk. Reviewed and moved.
|
|
285
|
+
if (options.install !== null && options.install !== undefined) {
|
|
286
|
+
// Before any store or transport exists, the shape `--assets-root` established.
|
|
287
|
+
const { command, args } = thisCommand();
|
|
288
|
+
try {
|
|
289
|
+
process.stdout.write(`${installHost(options.install, command, args, options.force)}\n`);
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
if (!(e instanceof InstallError))
|
|
293
|
+
throw e;
|
|
294
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
// `--force` alone is a typo with a plausible reading — somebody meant to install and
|
|
300
|
+
// dropped the flag that says where. Refusing names the missing half.
|
|
301
|
+
if (options.force)
|
|
302
|
+
throw new Refusal('--force is only meaningful with --install');
|
|
239
303
|
const memory = buildMemory(options);
|
|
240
304
|
const wire = new RawStdioTransport();
|
|
241
305
|
const server = buildServer(memory, wire, version());
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The four hosts, in the order they print in `--help`. */
|
|
2
|
+
export declare const HOSTS: readonly ["claude", "claude-desktop", "copilot", "cursor"];
|
|
3
|
+
export type Host = (typeof HOSTS)[number];
|
|
4
|
+
/** The entry name written into every host. */
|
|
5
|
+
export declare const ENTRY = "bantamkit";
|
|
6
|
+
/** A refusal, carrying the sentence the operator should read. Never a stack. */
|
|
7
|
+
export declare class InstallError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The file `--install <host>` would write, on THIS platform.
|
|
11
|
+
*
|
|
12
|
+
* Resolved from `homedir()` and `APPDATA` and nothing else — point `HOME`/`USERPROFILE` at a
|
|
13
|
+
* temporary directory and every path moves with it, which is what makes the tests possible.
|
|
14
|
+
*
|
|
15
|
+
* `claude` has no entry here: its arm shells out to `claude mcp add` and never names a path.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hostConfigPath(host: Host): string;
|
|
18
|
+
/** `servers` in VS Code, `mcpServers` everywhere else — the word that silently loses entries. */
|
|
19
|
+
export declare function configKey(host: Host): string;
|
|
20
|
+
/** The server entry, in the shape the host expects. `type` only where the host documents it. */
|
|
21
|
+
export declare function entryFor(host: Host, command: string, args: string[]): Record<string, unknown>;
|
|
22
|
+
/** Register this server with `host`. Returns the report to print; throws `InstallError`. */
|
|
23
|
+
export declare function install(host: Host, command: string, args: string[], force?: boolean): string;
|
|
24
|
+
/**
|
|
25
|
+
* What a host should run to get THIS server.
|
|
26
|
+
*
|
|
27
|
+
* `npx -y bantamkit-mcp` and not this process's own path, and the two are not the same
|
|
28
|
+
* thing: a global install, an `npx` cache entry and a checkout all have different paths,
|
|
29
|
+
* while the published name resolves the same everywhere. `-y` because `npx` historically
|
|
30
|
+
* prompts before installing a package it has not seen, and stdin here is the JSON-RPC
|
|
31
|
+
* channel — a prompt that ate one frame would look like a server that lost a request.
|
|
32
|
+
*/
|
|
33
|
+
export declare function thisCommand(): {
|
|
34
|
+
command: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire this server into an MCP host's configuration — `--install <host>`.
|
|
3
|
+
*
|
|
4
|
+
* The Node half of `runtime-py/src/bantamkit/hostinstall.py`, sentence for sentence. Read
|
|
5
|
+
* that file for WHY each decision is what it is; this header records only what porting it
|
|
6
|
+
* cost.
|
|
7
|
+
*
|
|
8
|
+
* EVERY OPERATOR-VISIBLE STRING IS COMPARED BYTE FOR BYTE against the reference, so two
|
|
9
|
+
* things that look like implementation detail are not:
|
|
10
|
+
*
|
|
11
|
+
* 1. The JSON in the conflict message goes through `dumpJson(fromJs(...), {sortKeys})`,
|
|
12
|
+
* never `JSON.stringify`. Python writes `{"args": [], "command": "OLD"}` and
|
|
13
|
+
* `JSON.stringify` writes `{"args":[],"command":"OLD"}` — same object, different
|
|
14
|
+
* bytes, and the separators are the whole difference.
|
|
15
|
+
* 2. The file is written with the same helper at `indent: 2`, which also reproduces
|
|
16
|
+
* `ensure_ascii`. A home directory with a non-ASCII name would otherwise make the two
|
|
17
|
+
* runtimes write different bytes for the same install.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IS RULED DIFFERENT, and it is one field: the COMMAND. This side records
|
|
20
|
+
* `npx -y bantamkit-mcp`; the reference records its own console script. The thing installed
|
|
21
|
+
* should be the thing that answers, and neither side should send a host looking for the
|
|
22
|
+
* other's runtime. `docs/porting.md` carries the row.
|
|
23
|
+
*/
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
import { dumpJson, fromJs } from './pyjson.js';
|
|
29
|
+
/** The four hosts, in the order they print in `--help`. */
|
|
30
|
+
export const HOSTS = ['claude', 'claude-desktop', 'copilot', 'cursor'];
|
|
31
|
+
/** The entry name written into every host. */
|
|
32
|
+
export const ENTRY = 'bantamkit';
|
|
33
|
+
/** A refusal, carrying the sentence the operator should read. Never a stack. */
|
|
34
|
+
export class InstallError extends Error {
|
|
35
|
+
}
|
|
36
|
+
const dumps = (value, indent = null, sortKeys = false) => dumpJson(fromJs(value), { indent, sortKeys });
|
|
37
|
+
/**
|
|
38
|
+
* The file `--install <host>` would write, on THIS platform.
|
|
39
|
+
*
|
|
40
|
+
* Resolved from `homedir()` and `APPDATA` and nothing else — point `HOME`/`USERPROFILE` at a
|
|
41
|
+
* temporary directory and every path moves with it, which is what makes the tests possible.
|
|
42
|
+
*
|
|
43
|
+
* `claude` has no entry here: its arm shells out to `claude mcp add` and never names a path.
|
|
44
|
+
*/
|
|
45
|
+
export function hostConfigPath(host) {
|
|
46
|
+
const home = homedir();
|
|
47
|
+
const appdata = process.env.APPDATA;
|
|
48
|
+
const roaming = appdata !== undefined && appdata !== '' ? appdata : join(home, 'AppData', 'Roaming');
|
|
49
|
+
if (host === 'claude-desktop') {
|
|
50
|
+
if (process.platform === 'darwin') {
|
|
51
|
+
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
52
|
+
}
|
|
53
|
+
if (process.platform === 'win32')
|
|
54
|
+
return join(roaming, 'Claude', 'claude_desktop_config.json');
|
|
55
|
+
return join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
56
|
+
}
|
|
57
|
+
if (host === 'copilot') {
|
|
58
|
+
if (process.platform === 'darwin') {
|
|
59
|
+
return join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
60
|
+
}
|
|
61
|
+
if (process.platform === 'win32')
|
|
62
|
+
return join(roaming, 'Code', 'User', 'mcp.json');
|
|
63
|
+
return join(home, '.config', 'Code', 'User', 'mcp.json');
|
|
64
|
+
}
|
|
65
|
+
if (host === 'cursor')
|
|
66
|
+
return join(home, '.cursor', 'mcp.json');
|
|
67
|
+
throw new InstallError(`no configuration file is written for ${host}`);
|
|
68
|
+
}
|
|
69
|
+
/** `servers` in VS Code, `mcpServers` everywhere else — the word that silently loses entries. */
|
|
70
|
+
export function configKey(host) {
|
|
71
|
+
return host === 'copilot' ? 'servers' : 'mcpServers';
|
|
72
|
+
}
|
|
73
|
+
/** The server entry, in the shape the host expects. `type` only where the host documents it. */
|
|
74
|
+
export function entryFor(host, command, args) {
|
|
75
|
+
return host === 'copilot'
|
|
76
|
+
? { type: 'stdio', command, args: [...args] }
|
|
77
|
+
: { command, args: [...args] };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `type(x).__name__` for the values `json.loads` can return.
|
|
81
|
+
*
|
|
82
|
+
* The reference prints CPython's type name and this sentence is compared byte for byte, so
|
|
83
|
+
* `typeof` is wrong in five of the six cases: `string`/`str`, `number`/`int`, `boolean`/`bool`.
|
|
84
|
+
* The first version of this translated only `list` and `NoneType` — and both unit tests used
|
|
85
|
+
* `[1, 2, 3]`, the ONE input where the two vocabularies happen to agree, so neither suite
|
|
86
|
+
* could see it. Review measured `"hello"` and `5`.
|
|
87
|
+
*
|
|
88
|
+
* `int` vs `float` follows `json`'s own split: a JSON number with no fraction and no exponent
|
|
89
|
+
* is decoded by `int`, everything else by `float`.
|
|
90
|
+
*/
|
|
91
|
+
function pyTypeName(value) {
|
|
92
|
+
if (value === null)
|
|
93
|
+
return 'NoneType';
|
|
94
|
+
if (Array.isArray(value))
|
|
95
|
+
return 'list';
|
|
96
|
+
if (typeof value === 'string')
|
|
97
|
+
return 'str';
|
|
98
|
+
if (typeof value === 'boolean')
|
|
99
|
+
return 'bool';
|
|
100
|
+
if (typeof value === 'number')
|
|
101
|
+
return Number.isInteger(value) ? 'int' : 'float';
|
|
102
|
+
return typeof value;
|
|
103
|
+
}
|
|
104
|
+
function readConfig(path) {
|
|
105
|
+
if (!existsSync(path))
|
|
106
|
+
return {};
|
|
107
|
+
let text;
|
|
108
|
+
try {
|
|
109
|
+
text = readFileSync(path, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
throw new InstallError(`cannot read ${path}: ${e.message}`);
|
|
113
|
+
}
|
|
114
|
+
if (text.trim() === '')
|
|
115
|
+
return {};
|
|
116
|
+
let loaded;
|
|
117
|
+
try {
|
|
118
|
+
loaded = JSON.parse(text);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
// The reference reports CPython's message, line and column. `JSON.parse` gives neither
|
|
122
|
+
// the same wording nor a position, so this is a RULED difference rather than a silent
|
|
123
|
+
// one: both refuse, both name the file, and the reason after the colon differs.
|
|
124
|
+
throw new InstallError(`${path} is not valid JSON, so this refuses to touch it: ${e.message}`);
|
|
125
|
+
}
|
|
126
|
+
if (loaded === null || typeof loaded !== 'object' || Array.isArray(loaded)) {
|
|
127
|
+
throw new InstallError(`${path} holds ${pyTypeName(loaded)}, not an object; refusing to touch it`);
|
|
128
|
+
}
|
|
129
|
+
return loaded;
|
|
130
|
+
}
|
|
131
|
+
function writeConfig(path, data) {
|
|
132
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
133
|
+
const tmp = `${path}.bantamkit-tmp`;
|
|
134
|
+
// The mode of the file being replaced, carried onto its replacement. A host config holds
|
|
135
|
+
// API keys in per-server `env` blocks, and a user who chmod'ed theirs to 0600 had it come
|
|
136
|
+
// back 0644, because a fresh temp file gets the process umask. Measured on both runtimes
|
|
137
|
+
// before this line existed.
|
|
138
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o7777 : null;
|
|
139
|
+
try {
|
|
140
|
+
writeFileSync(tmp, `${dumps(data, 2)}\n`, 'utf8');
|
|
141
|
+
if (mode !== null)
|
|
142
|
+
chmodSync(tmp, mode);
|
|
143
|
+
renameSync(tmp, path);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
rmSync(tmp, { force: true });
|
|
147
|
+
throw new InstallError(`cannot write ${path}: ${e.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** `date.today().isoformat()` — the LOCAL date, which is what the reference takes. */
|
|
151
|
+
function today() {
|
|
152
|
+
const now = new Date();
|
|
153
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
154
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
155
|
+
}
|
|
156
|
+
function backup(path) {
|
|
157
|
+
if (!existsSync(path))
|
|
158
|
+
return null;
|
|
159
|
+
const target = `${path}.backup-${today()}`;
|
|
160
|
+
try {
|
|
161
|
+
copyFileSync(path, target);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
throw new InstallError(`cannot back up ${path}: ${e.message}`);
|
|
165
|
+
}
|
|
166
|
+
return target;
|
|
167
|
+
}
|
|
168
|
+
function installViaClaudeCli(command, args) {
|
|
169
|
+
const argv = ['mcp', 'add', ENTRY, '-s', 'user', '--', command, ...args];
|
|
170
|
+
// `shell: true` ON WINDOWS ONLY. An npm-installed `claude` is a `claude.cmd` shim, and
|
|
171
|
+
// `spawnSync` refuses to launch a batch file without a shell — it returns ENOENT, which
|
|
172
|
+
// this function would report as "not on PATH", the one message guaranteed to send someone
|
|
173
|
+
// looking in the wrong place. The reference has no such problem: `shutil.which` honours
|
|
174
|
+
// PATHEXT and `CreateProcess` runs the shim. Arguments are quoted because `shell: true`
|
|
175
|
+
// hands the string to `cmd.exe`, and a path with a space is the normal case on Windows.
|
|
176
|
+
const win = process.platform === 'win32';
|
|
177
|
+
const quoted = win ? argv.map((a) => (/[\s"^&|<>]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)) : argv;
|
|
178
|
+
const done = spawnSync('claude', quoted, { encoding: 'utf8', shell: win });
|
|
179
|
+
if (done.error) {
|
|
180
|
+
const code = done.error.code;
|
|
181
|
+
if (code === 'ENOENT') {
|
|
182
|
+
throw new InstallError('the `claude` command is not on PATH, so this cannot register with Claude Code. ' +
|
|
183
|
+
'Install Claude Code, or add the entry by hand — `docs/install.md` gives the shape.');
|
|
184
|
+
}
|
|
185
|
+
throw new InstallError(`could not run claude: ${done.error.message}`);
|
|
186
|
+
}
|
|
187
|
+
if (done.status !== 0) {
|
|
188
|
+
const detail = (done.stderr || done.stdout || '').trim();
|
|
189
|
+
throw new InstallError(`\`claude mcp add\` failed (exit ${done.status})${detail ? `\n${detail}` : ''}`);
|
|
190
|
+
}
|
|
191
|
+
return argv;
|
|
192
|
+
}
|
|
193
|
+
/** Register this server with `host`. Returns the report to print; throws `InstallError`. */
|
|
194
|
+
export function install(host, command, args, force = false) {
|
|
195
|
+
if (!HOSTS.includes(host)) {
|
|
196
|
+
throw new InstallError(`unknown host '${host}'; choose one of: ${HOSTS.join(', ')}`);
|
|
197
|
+
}
|
|
198
|
+
if (host === 'claude') {
|
|
199
|
+
const argv = installViaClaudeCli(command, args);
|
|
200
|
+
return `installed bantamkit into claude\n ran : claude ${argv.join(' ')}`;
|
|
201
|
+
}
|
|
202
|
+
const path = hostConfigPath(host);
|
|
203
|
+
const key = configKey(host);
|
|
204
|
+
const wanted = entryFor(host, command, args);
|
|
205
|
+
const data = readConfig(path);
|
|
206
|
+
const raw = data[key];
|
|
207
|
+
if (raw !== undefined && (raw === null || typeof raw !== 'object' || Array.isArray(raw))) {
|
|
208
|
+
throw new InstallError(`${path} has a '${key}' that is not an object; refusing to touch it`);
|
|
209
|
+
}
|
|
210
|
+
const servers = (raw ?? {});
|
|
211
|
+
const existing = servers[ENTRY];
|
|
212
|
+
if (existing !== undefined && dumps(existing, null, true) === dumps(wanted, null, true)) {
|
|
213
|
+
return `bantamkit is already installed in ${host} and matches\n file : ${path}`;
|
|
214
|
+
}
|
|
215
|
+
if (existing !== undefined && !force) {
|
|
216
|
+
throw new InstallError(`${host} already has a bantamkit entry with different settings\n` +
|
|
217
|
+
` file : ${path}\n` +
|
|
218
|
+
` current : ${dumps(existing, null, true)}\n` +
|
|
219
|
+
` proposed: ${dumps(wanted, null, true)}\n` +
|
|
220
|
+
' re-run with --force to replace it');
|
|
221
|
+
}
|
|
222
|
+
const copied = backup(path);
|
|
223
|
+
servers[ENTRY] = wanted;
|
|
224
|
+
data[key] = servers;
|
|
225
|
+
writeConfig(path, data);
|
|
226
|
+
const lines = [
|
|
227
|
+
`installed bantamkit into ${host}`,
|
|
228
|
+
` file : ${path}`,
|
|
229
|
+
` key : ${key}`,
|
|
230
|
+
` command: ${command} ${args.join(' ')}`.replace(/\s+$/, ''),
|
|
231
|
+
];
|
|
232
|
+
if (copied !== null)
|
|
233
|
+
lines.push(` backup : ${copied}`);
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* What a host should run to get THIS server.
|
|
238
|
+
*
|
|
239
|
+
* `npx -y bantamkit-mcp` and not this process's own path, and the two are not the same
|
|
240
|
+
* thing: a global install, an `npx` cache entry and a checkout all have different paths,
|
|
241
|
+
* while the published name resolves the same everywhere. `-y` because `npx` historically
|
|
242
|
+
* prompts before installing a package it has not seen, and stdin here is the JSON-RPC
|
|
243
|
+
* channel — a prompt that ate one frame would look like a server that lost a request.
|
|
244
|
+
*/
|
|
245
|
+
export function thisCommand() {
|
|
246
|
+
return { command: 'npx', args: ['-y', 'bantamkit-mcp'] };
|
|
247
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bantamkit-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "bantamkit MCP server — pure Node, runnable with npx",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"assets"
|
|
21
21
|
],
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=20"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsc -p tsconfig.json",
|