reasonix 0.3.0-alpha.2 → 0.3.0-alpha.4
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 +24 -8
- package/dist/cli/index.js +357 -108
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +96 -2
- package/dist/index.js +216 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,12 +100,13 @@ the same Cache-First benefits. Two live runs, two data points:
|
|
|
100
100
|
|---|---:|---:|---:|---:|---:|
|
|
101
101
|
| bundled demo (`add` / `echo` / `get_time`) | 2 | 1 | **96.6%** (turn 2) | $0.000254 | −94.0% |
|
|
102
102
|
| official `@modelcontextprotocol/server-filesystem` | 5 | 4 | **96.7%** overall | $0.001235 | −97.0% |
|
|
103
|
+
| **both concurrently** (`demo_add` + `fs_write_file`) | 5 | 4 | **81.1%** | $0.001852 | −95.9% |
|
|
103
104
|
|
|
104
|
-
The
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
The third row is the ecosystem proof: two MCP servers running as
|
|
106
|
+
separate subprocesses, tools from both exercised in one conversation
|
|
107
|
+
(compute `17+25` with the demo server, write the result to a real file
|
|
108
|
+
via the filesystem server). **One single prefix hash across all 5
|
|
109
|
+
turns** — byte-stability survives concurrent MCP subprocesses.
|
|
109
110
|
|
|
110
111
|
**Reproduce without an API key** (replay the committed transcripts):
|
|
111
112
|
|
|
@@ -117,9 +118,24 @@ npx reasonix replay benchmarks/tau-bench/transcripts/mcp-filesystem.jsonl
|
|
|
117
118
|
**Reproduce with your own key** (live, ~$0.002):
|
|
118
119
|
|
|
119
120
|
```bash
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
# Don't know what MCP servers exist? Start here:
|
|
122
|
+
reasonix mcp list
|
|
123
|
+
# Prints a curated catalog (filesystem, fetch, github, sqlite, …) with
|
|
124
|
+
# ready-to-paste --mcp commands.
|
|
125
|
+
|
|
126
|
+
# One server:
|
|
127
|
+
reasonix chat --mcp "filesystem=npx -y @modelcontextprotocol/server-filesystem /tmp/safe"
|
|
128
|
+
|
|
129
|
+
# Multiple servers at once — each gets its own namespace prefix:
|
|
130
|
+
reasonix chat \
|
|
131
|
+
--mcp "fs=npx -y @modelcontextprotocol/server-filesystem /tmp/safe" \
|
|
132
|
+
--mcp "mem=npx -y @modelcontextprotocol/server-memory"
|
|
133
|
+
# Tools land in a shared registry as fs_read_file, mem_set, etc.
|
|
134
|
+
|
|
135
|
+
# Remote / hosted MCP server — pass an http(s) URL instead of a command.
|
|
136
|
+
# Reasonix opens an SSE stream and POSTs JSON-RPC to the endpoint the
|
|
137
|
+
# server advertises (MCP 2024-11-05 HTTP+SSE transport).
|
|
138
|
+
reasonix chat --mcp "kb=https://mcp.example.com/sse"
|
|
123
139
|
```
|
|
124
140
|
|
|
125
141
|
[mcp]: ./benchmarks/tau-bench/transcripts/mcp-demo.add.jsonl
|
package/dist/cli/index.js
CHANGED
|
@@ -2103,6 +2103,145 @@ function quoteArg(s, windows) {
|
|
|
2103
2103
|
return `"${s.replace(/"/g, '""')}"`;
|
|
2104
2104
|
}
|
|
2105
2105
|
|
|
2106
|
+
// src/mcp/sse.ts
|
|
2107
|
+
import { createParser as createParser2 } from "eventsource-parser";
|
|
2108
|
+
var SseTransport = class {
|
|
2109
|
+
url;
|
|
2110
|
+
headers;
|
|
2111
|
+
queue = [];
|
|
2112
|
+
waiters = [];
|
|
2113
|
+
controller = new AbortController();
|
|
2114
|
+
closed = false;
|
|
2115
|
+
postUrl = null;
|
|
2116
|
+
endpointReady;
|
|
2117
|
+
resolveEndpoint;
|
|
2118
|
+
rejectEndpoint;
|
|
2119
|
+
constructor(opts) {
|
|
2120
|
+
this.url = opts.url;
|
|
2121
|
+
this.headers = opts.headers ?? {};
|
|
2122
|
+
this.endpointReady = new Promise((resolve2, reject) => {
|
|
2123
|
+
this.resolveEndpoint = resolve2;
|
|
2124
|
+
this.rejectEndpoint = reject;
|
|
2125
|
+
});
|
|
2126
|
+
this.endpointReady.catch(() => void 0);
|
|
2127
|
+
void this.runStream();
|
|
2128
|
+
}
|
|
2129
|
+
async send(message) {
|
|
2130
|
+
if (this.closed) throw new Error("MCP SSE transport is closed");
|
|
2131
|
+
const postUrl = await this.endpointReady;
|
|
2132
|
+
const res = await fetch(postUrl, {
|
|
2133
|
+
method: "POST",
|
|
2134
|
+
headers: { "content-type": "application/json", ...this.headers },
|
|
2135
|
+
body: JSON.stringify(message),
|
|
2136
|
+
signal: this.controller.signal
|
|
2137
|
+
});
|
|
2138
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
2139
|
+
if (!res.ok) {
|
|
2140
|
+
throw new Error(`MCP SSE POST ${postUrl} failed: ${res.status} ${res.statusText}`);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
async *messages() {
|
|
2144
|
+
while (true) {
|
|
2145
|
+
if (this.queue.length > 0) {
|
|
2146
|
+
yield this.queue.shift();
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
if (this.closed) return;
|
|
2150
|
+
const next = await new Promise((resolve2) => {
|
|
2151
|
+
this.waiters.push(resolve2);
|
|
2152
|
+
});
|
|
2153
|
+
if (next === null) return;
|
|
2154
|
+
yield next;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
async close() {
|
|
2158
|
+
if (this.closed) return;
|
|
2159
|
+
this.closed = true;
|
|
2160
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2161
|
+
this.rejectEndpoint(new Error("MCP SSE transport closed before endpoint was ready"));
|
|
2162
|
+
try {
|
|
2163
|
+
this.controller.abort();
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
// ---------- internals ----------
|
|
2168
|
+
async runStream() {
|
|
2169
|
+
let res;
|
|
2170
|
+
try {
|
|
2171
|
+
res = await fetch(this.url, {
|
|
2172
|
+
method: "GET",
|
|
2173
|
+
headers: { accept: "text/event-stream", ...this.headers },
|
|
2174
|
+
signal: this.controller.signal
|
|
2175
|
+
});
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
this.failHandshake(`SSE connect to ${this.url} failed: ${err.message}`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (!res.ok || !res.body) {
|
|
2181
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2182
|
+
this.failHandshake(`SSE handshake ${this.url} \u2192 ${res.status} ${res.statusText}`);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const parser = createParser2({
|
|
2186
|
+
onEvent: (ev) => this.handleEvent(ev.event ?? "message", ev.data)
|
|
2187
|
+
});
|
|
2188
|
+
const decoder = new TextDecoder();
|
|
2189
|
+
try {
|
|
2190
|
+
for await (const chunk of res.body) {
|
|
2191
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
2192
|
+
}
|
|
2193
|
+
} catch (err) {
|
|
2194
|
+
if (!this.closed) {
|
|
2195
|
+
this.pushError(`SSE stream error: ${err.message}`);
|
|
2196
|
+
}
|
|
2197
|
+
} finally {
|
|
2198
|
+
this.markClosed();
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
handleEvent(type, data) {
|
|
2202
|
+
if (type === "endpoint") {
|
|
2203
|
+
if (this.postUrl) return;
|
|
2204
|
+
try {
|
|
2205
|
+
this.postUrl = new URL(data, this.url).toString();
|
|
2206
|
+
this.resolveEndpoint(this.postUrl);
|
|
2207
|
+
} catch (err) {
|
|
2208
|
+
this.failHandshake(`SSE endpoint event had bad URL "${data}": ${err.message}`);
|
|
2209
|
+
}
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (type === "message") {
|
|
2213
|
+
try {
|
|
2214
|
+
const parsed = JSON.parse(data);
|
|
2215
|
+
this.pushMessage(parsed);
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
failHandshake(reason) {
|
|
2222
|
+
this.rejectEndpoint(new Error(reason));
|
|
2223
|
+
this.pushError(reason);
|
|
2224
|
+
this.markClosed();
|
|
2225
|
+
}
|
|
2226
|
+
pushMessage(msg) {
|
|
2227
|
+
const waiter = this.waiters.shift();
|
|
2228
|
+
if (waiter) waiter(msg);
|
|
2229
|
+
else this.queue.push(msg);
|
|
2230
|
+
}
|
|
2231
|
+
pushError(message) {
|
|
2232
|
+
this.pushMessage({
|
|
2233
|
+
jsonrpc: "2.0",
|
|
2234
|
+
id: null,
|
|
2235
|
+
error: { code: -32e3, message }
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
markClosed() {
|
|
2239
|
+
if (this.closed) return;
|
|
2240
|
+
this.closed = true;
|
|
2241
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2106
2245
|
// src/mcp/registry.ts
|
|
2107
2246
|
async function bridgeMcpTools(client, opts = {}) {
|
|
2108
2247
|
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
@@ -2142,56 +2281,6 @@ function blockToString(block) {
|
|
|
2142
2281
|
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
2143
2282
|
}
|
|
2144
2283
|
|
|
2145
|
-
// src/config.ts
|
|
2146
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2147
|
-
import { homedir as homedir2 } from "os";
|
|
2148
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
2149
|
-
function defaultConfigPath() {
|
|
2150
|
-
return join2(homedir2(), ".reasonix", "config.json");
|
|
2151
|
-
}
|
|
2152
|
-
function readConfig(path = defaultConfigPath()) {
|
|
2153
|
-
try {
|
|
2154
|
-
const raw = readFileSync4(path, "utf8");
|
|
2155
|
-
const parsed = JSON.parse(raw);
|
|
2156
|
-
if (parsed && typeof parsed === "object") return parsed;
|
|
2157
|
-
} catch {
|
|
2158
|
-
}
|
|
2159
|
-
return {};
|
|
2160
|
-
}
|
|
2161
|
-
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2162
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
2163
|
-
writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2164
|
-
try {
|
|
2165
|
-
chmodSync2(path, 384);
|
|
2166
|
-
} catch {
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
function loadApiKey(path = defaultConfigPath()) {
|
|
2170
|
-
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
2171
|
-
return readConfig(path).apiKey;
|
|
2172
|
-
}
|
|
2173
|
-
function saveApiKey(key, path = defaultConfigPath()) {
|
|
2174
|
-
const cfg = readConfig(path);
|
|
2175
|
-
cfg.apiKey = key.trim();
|
|
2176
|
-
writeConfig(cfg, path);
|
|
2177
|
-
}
|
|
2178
|
-
function isPlausibleKey(key) {
|
|
2179
|
-
const trimmed = key.trim();
|
|
2180
|
-
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
2181
|
-
}
|
|
2182
|
-
function redactKey(key) {
|
|
2183
|
-
if (!key) return "";
|
|
2184
|
-
if (key.length <= 12) return "****";
|
|
2185
|
-
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// src/index.ts
|
|
2189
|
-
var VERSION = "0.3.0-alpha.2";
|
|
2190
|
-
|
|
2191
|
-
// src/cli/commands/chat.tsx
|
|
2192
|
-
import { render } from "ink";
|
|
2193
|
-
import React8, { useState as useState4 } from "react";
|
|
2194
|
-
|
|
2195
2284
|
// src/mcp/shell-split.ts
|
|
2196
2285
|
function shellSplit(input) {
|
|
2197
2286
|
const tokens = [];
|
|
@@ -2221,11 +2310,6 @@ function shellSplit(input) {
|
|
|
2221
2310
|
i++;
|
|
2222
2311
|
continue;
|
|
2223
2312
|
}
|
|
2224
|
-
if (ch === "\\" && i + 1 < s.length) {
|
|
2225
|
-
cur += s[i + 1];
|
|
2226
|
-
i += 2;
|
|
2227
|
-
continue;
|
|
2228
|
-
}
|
|
2229
2313
|
if (ch === " " || ch === " ") {
|
|
2230
2314
|
if (cur.length > 0) {
|
|
2231
2315
|
tokens.push(cur);
|
|
@@ -2246,6 +2330,81 @@ function shellSplit(input) {
|
|
|
2246
2330
|
return tokens;
|
|
2247
2331
|
}
|
|
2248
2332
|
|
|
2333
|
+
// src/mcp/spec.ts
|
|
2334
|
+
var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
|
|
2335
|
+
var HTTP_URL = /^https?:\/\//i;
|
|
2336
|
+
function parseMcpSpec(input) {
|
|
2337
|
+
const trimmed = input.trim();
|
|
2338
|
+
if (!trimmed) {
|
|
2339
|
+
throw new Error("empty MCP spec");
|
|
2340
|
+
}
|
|
2341
|
+
const nameMatch = NAME_PREFIX.exec(trimmed);
|
|
2342
|
+
const name = nameMatch ? nameMatch[1] : null;
|
|
2343
|
+
const body = (nameMatch ? nameMatch[2] : trimmed).trim();
|
|
2344
|
+
if (!body) {
|
|
2345
|
+
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
2346
|
+
}
|
|
2347
|
+
if (HTTP_URL.test(body)) {
|
|
2348
|
+
return { transport: "sse", name, url: body };
|
|
2349
|
+
}
|
|
2350
|
+
const argv = shellSplit(body);
|
|
2351
|
+
if (argv.length === 0) {
|
|
2352
|
+
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
2353
|
+
}
|
|
2354
|
+
const [command, ...args] = argv;
|
|
2355
|
+
return { transport: "stdio", name, command, args };
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/config.ts
|
|
2359
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
2360
|
+
import { homedir as homedir2 } from "os";
|
|
2361
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2362
|
+
function defaultConfigPath() {
|
|
2363
|
+
return join2(homedir2(), ".reasonix", "config.json");
|
|
2364
|
+
}
|
|
2365
|
+
function readConfig(path = defaultConfigPath()) {
|
|
2366
|
+
try {
|
|
2367
|
+
const raw = readFileSync4(path, "utf8");
|
|
2368
|
+
const parsed = JSON.parse(raw);
|
|
2369
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
2370
|
+
} catch {
|
|
2371
|
+
}
|
|
2372
|
+
return {};
|
|
2373
|
+
}
|
|
2374
|
+
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
2375
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2376
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
2377
|
+
try {
|
|
2378
|
+
chmodSync2(path, 384);
|
|
2379
|
+
} catch {
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function loadApiKey(path = defaultConfigPath()) {
|
|
2383
|
+
if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
|
|
2384
|
+
return readConfig(path).apiKey;
|
|
2385
|
+
}
|
|
2386
|
+
function saveApiKey(key, path = defaultConfigPath()) {
|
|
2387
|
+
const cfg = readConfig(path);
|
|
2388
|
+
cfg.apiKey = key.trim();
|
|
2389
|
+
writeConfig(cfg, path);
|
|
2390
|
+
}
|
|
2391
|
+
function isPlausibleKey(key) {
|
|
2392
|
+
const trimmed = key.trim();
|
|
2393
|
+
return /^sk-[A-Za-z0-9_-]{16,}$/.test(trimmed);
|
|
2394
|
+
}
|
|
2395
|
+
function redactKey(key) {
|
|
2396
|
+
if (!key) return "";
|
|
2397
|
+
if (key.length <= 12) return "****";
|
|
2398
|
+
return `${key.slice(0, 6)}\u2026${key.slice(-4)}`;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/index.ts
|
|
2402
|
+
var VERSION = "0.3.0-alpha.4";
|
|
2403
|
+
|
|
2404
|
+
// src/cli/commands/chat.tsx
|
|
2405
|
+
import { render } from "ink";
|
|
2406
|
+
import React8, { useState as useState4 } from "react";
|
|
2407
|
+
|
|
2249
2408
|
// src/cli/ui/App.tsx
|
|
2250
2409
|
import { Box as Box6, Static, Text as Text6, useApp } from "ink";
|
|
2251
2410
|
import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
@@ -2994,34 +3153,32 @@ function Root({ initialKey, tools, ...appProps }) {
|
|
|
2994
3153
|
async function chatCommand(opts) {
|
|
2995
3154
|
loadDotenv();
|
|
2996
3155
|
const initialKey = loadApiKey();
|
|
2997
|
-
|
|
3156
|
+
const mcpSpecs = opts.mcp ?? [];
|
|
3157
|
+
const clients = [];
|
|
2998
3158
|
let tools;
|
|
2999
|
-
if (
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
await mcp.initialize();
|
|
3014
|
-
const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
|
|
3015
|
-
tools = bridge.registry;
|
|
3016
|
-
process.stderr.write(
|
|
3017
|
-
`\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
|
|
3159
|
+
if (mcpSpecs.length > 0) {
|
|
3160
|
+
tools = new ToolRegistry();
|
|
3161
|
+
for (const raw of mcpSpecs) {
|
|
3162
|
+
try {
|
|
3163
|
+
const spec = parseMcpSpec(raw);
|
|
3164
|
+
const prefix = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3165
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3166
|
+
const mcp2 = new McpClient({ transport });
|
|
3167
|
+
await mcp2.initialize();
|
|
3168
|
+
const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix });
|
|
3169
|
+
const label = spec.name ?? "anon";
|
|
3170
|
+
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
3171
|
+
process.stderr.write(
|
|
3172
|
+
`\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
3018
3173
|
`
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3174
|
+
);
|
|
3175
|
+
clients.push(mcp2);
|
|
3176
|
+
} catch (err) {
|
|
3177
|
+
process.stderr.write(`MCP setup failed for "${raw}": ${err.message}
|
|
3022
3178
|
`);
|
|
3023
|
-
|
|
3024
|
-
|
|
3179
|
+
for (const c of clients) await c.close();
|
|
3180
|
+
process.exit(1);
|
|
3181
|
+
}
|
|
3025
3182
|
}
|
|
3026
3183
|
}
|
|
3027
3184
|
const { waitUntilExit } = render(/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, ...opts }), {
|
|
@@ -3030,7 +3187,7 @@ async function chatCommand(opts) {
|
|
|
3030
3187
|
try {
|
|
3031
3188
|
await waitUntilExit();
|
|
3032
3189
|
} finally {
|
|
3033
|
-
await
|
|
3190
|
+
for (const c of clients) await c.close();
|
|
3034
3191
|
}
|
|
3035
3192
|
}
|
|
3036
3193
|
|
|
@@ -3196,6 +3353,82 @@ markdown report written to ${opts.mdPath}`);
|
|
|
3196
3353
|
console.log(renderSummaryTable(report));
|
|
3197
3354
|
}
|
|
3198
3355
|
|
|
3356
|
+
// src/mcp/catalog.ts
|
|
3357
|
+
var MCP_CATALOG = [
|
|
3358
|
+
{
|
|
3359
|
+
name: "filesystem",
|
|
3360
|
+
summary: "read/write/search files inside a sandboxed directory",
|
|
3361
|
+
package: "@modelcontextprotocol/server-filesystem",
|
|
3362
|
+
userArgs: "<dir>",
|
|
3363
|
+
note: "the directory is a hard sandbox \u2014 the server refuses access outside it"
|
|
3364
|
+
},
|
|
3365
|
+
{
|
|
3366
|
+
name: "fetch",
|
|
3367
|
+
summary: "fetch URLs (markdown-friendly extraction, not a full browser)",
|
|
3368
|
+
package: "@modelcontextprotocol/server-fetch"
|
|
3369
|
+
},
|
|
3370
|
+
{
|
|
3371
|
+
name: "memory",
|
|
3372
|
+
summary: "persistent key-value memory across sessions",
|
|
3373
|
+
package: "@modelcontextprotocol/server-memory"
|
|
3374
|
+
},
|
|
3375
|
+
{
|
|
3376
|
+
name: "github",
|
|
3377
|
+
summary: "read issues, PRs, code search (needs GITHUB_PERSONAL_ACCESS_TOKEN)",
|
|
3378
|
+
package: "@modelcontextprotocol/server-github",
|
|
3379
|
+
note: "set GITHUB_PERSONAL_ACCESS_TOKEN in your env before spawning"
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
name: "sqlite",
|
|
3383
|
+
summary: "read/write a sqlite database file",
|
|
3384
|
+
package: "@modelcontextprotocol/server-sqlite",
|
|
3385
|
+
userArgs: "<db.sqlite>"
|
|
3386
|
+
},
|
|
3387
|
+
{
|
|
3388
|
+
name: "puppeteer",
|
|
3389
|
+
summary: "browser automation \u2014 take screenshots, click, type",
|
|
3390
|
+
package: "@modelcontextprotocol/server-puppeteer",
|
|
3391
|
+
note: "downloads Chromium on first run (~200 MB)"
|
|
3392
|
+
},
|
|
3393
|
+
{
|
|
3394
|
+
name: "everything",
|
|
3395
|
+
summary: "official test server \u2014 exercises every MCP feature",
|
|
3396
|
+
package: "@modelcontextprotocol/server-everything",
|
|
3397
|
+
note: "useful for debugging your Reasonix setup"
|
|
3398
|
+
}
|
|
3399
|
+
];
|
|
3400
|
+
function mcpCommandFor(entry) {
|
|
3401
|
+
const pkg = entry.package;
|
|
3402
|
+
const tail = entry.userArgs ? ` ${entry.userArgs}` : "";
|
|
3403
|
+
return `--mcp "${entry.name}=npx -y ${pkg}${tail}"`;
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
// src/cli/commands/mcp.ts
|
|
3407
|
+
function mcpListCommand(opts) {
|
|
3408
|
+
if (opts.json) {
|
|
3409
|
+
console.log(JSON.stringify(MCP_CATALOG, null, 2));
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
console.log("Popular MCP servers you can bridge into Reasonix:");
|
|
3413
|
+
console.log("");
|
|
3414
|
+
for (const entry of MCP_CATALOG) {
|
|
3415
|
+
console.log(` ${pad(entry.name, 12)} ${entry.summary}`);
|
|
3416
|
+
console.log(` ${mcpCommandFor(entry)}`);
|
|
3417
|
+
if (entry.note) console.log(` \xB7 ${entry.note}`);
|
|
3418
|
+
console.log("");
|
|
3419
|
+
}
|
|
3420
|
+
console.log("Usage: reasonix chat <one-of-the---mcp-lines-above>");
|
|
3421
|
+
console.log(
|
|
3422
|
+
"Docs: https://github.com/modelcontextprotocol/servers \u2014 Anthropic's official server repo"
|
|
3423
|
+
);
|
|
3424
|
+
console.log(
|
|
3425
|
+
" https://mcp.so \u2014 community-maintained catalog"
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
function pad(s, width) {
|
|
3429
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3199
3432
|
// src/cli/commands/replay.ts
|
|
3200
3433
|
import { render as render3 } from "ink";
|
|
3201
3434
|
import React13 from "react";
|
|
@@ -3386,29 +3619,31 @@ async function runCommand(opts) {
|
|
|
3386
3619
|
loadDotenv();
|
|
3387
3620
|
const apiKey = await ensureApiKey();
|
|
3388
3621
|
process.env.DEEPSEEK_API_KEY = apiKey;
|
|
3389
|
-
|
|
3622
|
+
const mcpSpecs = opts.mcp ?? [];
|
|
3623
|
+
const clients = [];
|
|
3390
3624
|
let tools;
|
|
3391
|
-
if (
|
|
3392
|
-
|
|
3393
|
-
const
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
`\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
|
|
3625
|
+
if (mcpSpecs.length > 0) {
|
|
3626
|
+
tools = new ToolRegistry();
|
|
3627
|
+
for (const raw of mcpSpecs) {
|
|
3628
|
+
try {
|
|
3629
|
+
const spec = parseMcpSpec(raw);
|
|
3630
|
+
const prefix2 = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
3631
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
3632
|
+
const mcp2 = new McpClient({ transport });
|
|
3633
|
+
await mcp2.initialize();
|
|
3634
|
+
const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix2 });
|
|
3635
|
+
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
3636
|
+
process.stderr.write(
|
|
3637
|
+
`\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
3405
3638
|
`
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3639
|
+
);
|
|
3640
|
+
clients.push(mcp2);
|
|
3641
|
+
} catch (err) {
|
|
3642
|
+
process.stderr.write(`MCP setup failed for "${raw}": ${err.message}
|
|
3409
3643
|
`);
|
|
3410
|
-
|
|
3411
|
-
|
|
3644
|
+
for (const c of clients) await c.close();
|
|
3645
|
+
process.exit(1);
|
|
3646
|
+
}
|
|
3412
3647
|
}
|
|
3413
3648
|
}
|
|
3414
3649
|
const client = new DeepSeekClient();
|
|
@@ -3470,7 +3705,7 @@ transcript: ${opts.transcript}
|
|
|
3470
3705
|
process.stdout.write(` \u2192 npx reasonix replay ${opts.transcript}
|
|
3471
3706
|
`);
|
|
3472
3707
|
}
|
|
3473
|
-
await
|
|
3708
|
+
for (const c of clients) await c.close();
|
|
3474
3709
|
}
|
|
3475
3710
|
|
|
3476
3711
|
// src/cli/commands/sessions.ts
|
|
@@ -3595,9 +3830,14 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
3595
3830
|
"--session <name>",
|
|
3596
3831
|
"Use a named session (default: 'default'). Resume the same session next time."
|
|
3597
3832
|
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
|
|
3598
|
-
"--mcp <
|
|
3599
|
-
'
|
|
3600
|
-
|
|
3833
|
+
"--mcp <spec>",
|
|
3834
|
+
'MCP server spec; repeatable. Forms: "name=cmd args..." (namespaced, tools get `name_` prefix) or "cmd args..." (anonymous). Example: --mcp "fs=npx -y @scope/fs /tmp" --mcp "gh=npx -y @scope/gh"',
|
|
3835
|
+
(value, previous = []) => [...previous, value],
|
|
3836
|
+
[]
|
|
3837
|
+
).option(
|
|
3838
|
+
"--mcp-prefix <str>",
|
|
3839
|
+
"Global prefix applied to every MCP tool (only honored when no per-spec name is set; avoids collisions with a single anonymous server)"
|
|
3840
|
+
).action(async (opts) => {
|
|
3601
3841
|
let session;
|
|
3602
3842
|
if (opts.session === false) {
|
|
3603
3843
|
session = void 0;
|
|
@@ -3625,9 +3865,14 @@ program.command("run <task>").description("Run a single task non-interactively,
|
|
|
3625
3865
|
"Self-consistency: run N parallel samples per turn and pick the most confident",
|
|
3626
3866
|
(v) => Number.parseInt(v, 10)
|
|
3627
3867
|
).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
|
|
3628
|
-
"--mcp <
|
|
3629
|
-
'
|
|
3630
|
-
|
|
3868
|
+
"--mcp <spec>",
|
|
3869
|
+
'MCP server spec; repeatable. "name=cmd args..." or "cmd args...".',
|
|
3870
|
+
(value, previous = []) => [...previous, value],
|
|
3871
|
+
[]
|
|
3872
|
+
).option(
|
|
3873
|
+
"--mcp-prefix <str>",
|
|
3874
|
+
"Global prefix (only honored when no per-spec name is set; for a single anonymous server)"
|
|
3875
|
+
).action(async (task, opts) => {
|
|
3631
3876
|
await runCommand({
|
|
3632
3877
|
task,
|
|
3633
3878
|
model: opts.model,
|
|
@@ -3668,6 +3913,10 @@ program.command("diff <a> <b>").description(
|
|
|
3668
3913
|
tui: !!opts.tui
|
|
3669
3914
|
});
|
|
3670
3915
|
});
|
|
3916
|
+
var mcp = program.command("mcp").description("Model Context Protocol helpers \u2014 discover servers, test your setup.");
|
|
3917
|
+
mcp.command("list").description("Show a curated catalog of popular MCP servers with ready-to-use --mcp commands.").option("--json", "Emit the catalog as JSON instead of the human-readable table").action((opts) => {
|
|
3918
|
+
mcpListCommand({ json: !!opts.json });
|
|
3919
|
+
});
|
|
3671
3920
|
program.command("version").description("Print Reasonix version.").action(versionCommand);
|
|
3672
3921
|
program.parseAsync(process.argv).catch((err) => {
|
|
3673
3922
|
console.error(err);
|