reasonix 0.3.0-alpha.3 → 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 CHANGED
@@ -131,6 +131,11 @@ reasonix chat \
131
131
  --mcp "fs=npx -y @modelcontextprotocol/server-filesystem /tmp/safe" \
132
132
  --mcp "mem=npx -y @modelcontextprotocol/server-memory"
133
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"
134
139
  ```
135
140
 
136
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.3";
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 = [];
@@ -2243,6 +2332,7 @@ function shellSplit(input) {
2243
2332
 
2244
2333
  // src/mcp/spec.ts
2245
2334
  var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
2335
+ var HTTP_URL = /^https?:\/\//i;
2246
2336
  function parseMcpSpec(input) {
2247
2337
  const trimmed = input.trim();
2248
2338
  if (!trimmed) {
@@ -2250,15 +2340,71 @@ function parseMcpSpec(input) {
2250
2340
  }
2251
2341
  const nameMatch = NAME_PREFIX.exec(trimmed);
2252
2342
  const name = nameMatch ? nameMatch[1] : null;
2253
- const body = nameMatch ? nameMatch[2] : trimmed;
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
+ }
2254
2350
  const argv = shellSplit(body);
2255
2351
  if (argv.length === 0) {
2256
2352
  throw new Error(`MCP spec has name but no command: ${input}`);
2257
2353
  }
2258
2354
  const [command, ...args] = argv;
2259
- return { name, command, args };
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)}`;
2260
2399
  }
2261
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
+
2262
2408
  // src/cli/ui/App.tsx
2263
2409
  import { Box as Box6, Static, Text as Text6, useApp } from "ink";
2264
2410
  import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
@@ -3016,13 +3162,14 @@ async function chatCommand(opts) {
3016
3162
  try {
3017
3163
  const spec = parseMcpSpec(raw);
3018
3164
  const prefix = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3019
- const transport = new StdioTransport({ command: spec.command, args: spec.args });
3165
+ const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
3020
3166
  const mcp2 = new McpClient({ transport });
3021
3167
  await mcp2.initialize();
3022
3168
  const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix });
3023
3169
  const label = spec.name ?? "anon";
3170
+ const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
3024
3171
  process.stderr.write(
3025
- `\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${spec.command} ${spec.args.join(" ")}
3172
+ `\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
3026
3173
  `
3027
3174
  );
3028
3175
  clients.push(mcp2);
@@ -3481,13 +3628,13 @@ async function runCommand(opts) {
3481
3628
  try {
3482
3629
  const spec = parseMcpSpec(raw);
3483
3630
  const prefix2 = spec.name ? `${spec.name}_` : mcpSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
3484
- const mcp2 = new McpClient({
3485
- transport: new StdioTransport({ command: spec.command, args: spec.args })
3486
- });
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 });
3487
3633
  await mcp2.initialize();
3488
3634
  const bridge = await bridgeMcpTools(mcp2, { registry: tools, namePrefix: prefix2 });
3635
+ const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
3489
3636
  process.stderr.write(
3490
- `\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${spec.command} ${spec.args.join(" ")}
3637
+ `\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
3491
3638
  `
3492
3639
  );
3493
3640
  clients.push(mcp2);