diffler-mcp 0.2.3 → 0.2.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.
Files changed (2) hide show
  1. package/index.js +70 -35
  2. package/package.json +2 -1
package/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // stdio↔HTTP MCP proxy: Claude Code spawns this over stdio, and it forwards
3
- // every tool call to the streamable-HTTP MCP server embedded in a running
4
- // diffler TUI. diffler's MCP serves the live review state, so the proxy never
5
- // owns state — it only bridges transports.
2
+ // stdio↔HTTP MCP proxy bridging Claude Code to a running diffler TUI, lazily
3
+ // (re)connecting so it survives diffler quitting and restarting on a new port.
6
4
 
7
5
  import { readFileSync } from "node:fs";
8
6
  import { join } from "node:path";
@@ -22,9 +20,8 @@ const DEFAULT_PORT = 8417;
22
20
  function parseArgs(argv) {
23
21
  const opts = {};
24
22
  for (let i = 0; i < argv.length; i += 1) {
25
- const arg = argv[i];
26
23
  const take = () => argv[(i += 1)];
27
- switch (arg) {
24
+ switch (argv[i]) {
28
25
  case "--url":
29
26
  opts.url = take();
30
27
  break;
@@ -38,27 +35,22 @@ function parseArgs(argv) {
38
35
  opts.repo = take();
39
36
  break;
40
37
  default:
41
- // unknown flags are ignored so future diffler args don't break older proxies
42
38
  break;
43
39
  }
44
40
  }
45
41
  return opts;
46
42
  }
47
43
 
48
- // The port may differ from the configured one after an ephemeral fallback, so
49
- // diffler publishes the live endpoint to .diffler/mcp.json in the repo.
50
44
  function discoverPort(repo) {
51
45
  try {
52
- const raw = readFileSync(join(repo, ".diffler", "mcp.json"), "utf8");
53
- const port = JSON.parse(raw).port;
46
+ const port = JSON.parse(readFileSync(join(repo, ".diffler", "mcp.json"), "utf8")).port;
54
47
  return typeof port === "number" ? port : undefined;
55
48
  } catch {
56
49
  return undefined;
57
50
  }
58
51
  }
59
52
 
60
- function resolveUrl() {
61
- const opts = parseArgs(process.argv.slice(2));
53
+ function resolveUrl(opts) {
62
54
  const env = process.env;
63
55
  const explicit = opts.url || env.DIFFLER_MCP_URL;
64
56
  if (explicit) {
@@ -66,40 +58,83 @@ function resolveUrl() {
66
58
  }
67
59
  const host = opts.host || env.DIFFLER_MCP_HOST || DEFAULT_HOST;
68
60
  const repo = opts.repo || process.cwd();
69
- const port =
70
- opts.port || env.DIFFLER_MCP_PORT || discoverPort(repo) || DEFAULT_PORT;
61
+ const port = opts.port || env.DIFFLER_MCP_PORT || discoverPort(repo) || DEFAULT_PORT;
71
62
  return `http://${host}:${port}/mcp`;
72
63
  }
73
64
 
74
65
  async function main() {
75
- const url = resolveUrl();
76
-
77
- const client = new Client({ name: "diffler-mcp-proxy", version: "0.1.0" });
78
- try {
79
- await client.connect(new StreamableHTTPClientTransport(new URL(url)));
80
- } catch (err) {
81
- process.stderr.write(
82
- `diffler-mcp: cannot reach a diffler MCP server at ${url}\n` +
83
- `Is diffler running in this repo? (${err.message ?? err})\n`,
84
- );
85
- process.exit(1);
86
- }
87
-
66
+ const opts = parseArgs(process.argv.slice(2));
88
67
  const server = new Server(
89
68
  { name: "diffler", version: "0.1.0" },
90
69
  { capabilities: { tools: {} } },
91
70
  );
92
- server.setRequestHandler(ListToolsRequestSchema, () => client.listTools());
93
- server.setRequestHandler(CallToolRequestSchema, (request) =>
94
- client.callTool(request.params),
95
- );
96
71
 
97
- // when diffler exits the HTTP side drops; tear down so Claude sees the close
98
- client.onclose = () => {
99
- void server.close().finally(() => process.exit(0));
72
+ let upstream = null;
73
+ let connecting = null;
74
+
75
+ const connect = async () => {
76
+ const client = new Client({ name: "diffler-mcp-proxy", version: "0.1.0" });
77
+ await client.connect(new StreamableHTTPClientTransport(new URL(resolveUrl(opts))));
78
+ // cache synchronously after the await so a later close can't race ahead of it
79
+ upstream = client;
80
+ const drop = () => {
81
+ if (upstream === client) {
82
+ upstream = null;
83
+ }
84
+ };
85
+ client.onclose = drop;
86
+ client.onerror = drop;
87
+ server.sendToolListChanged?.().catch(() => {});
88
+ return client;
89
+ };
90
+
91
+ const ensureUpstream = () => {
92
+ if (upstream) {
93
+ return Promise.resolve(upstream);
94
+ }
95
+ if (!connecting) {
96
+ connecting = connect().finally(() => {
97
+ connecting = null;
98
+ });
99
+ }
100
+ return connecting;
100
101
  };
101
102
 
103
+ const withUpstream = async (fn) => {
104
+ try {
105
+ return await fn(await ensureUpstream());
106
+ } catch {
107
+ upstream = null;
108
+ return await fn(await ensureUpstream());
109
+ }
110
+ };
111
+
112
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
113
+ try {
114
+ return await withUpstream((client) => client.listTools());
115
+ } catch {
116
+ return { tools: [] };
117
+ }
118
+ });
119
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
120
+ try {
121
+ return await withUpstream((client) => client.callTool(request.params));
122
+ } catch (err) {
123
+ upstream = null;
124
+ return {
125
+ isError: true,
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `diffler isn't reachable — is it running in this repo? (${err.message ?? err})`,
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ });
135
+
102
136
  await server.connect(new StdioServerTransport());
137
+ void ensureUpstream().catch(() => {});
103
138
  }
104
139
 
105
140
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffler-mcp",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "stdio↔HTTP MCP proxy bridging Claude Code (or any stdio MCP client) to a running diffler review session",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "claude"
28
28
  ],
29
29
  "scripts": {
30
+ "test": "node --test",
30
31
  "prepublishOnly": "node --check index.js"
31
32
  },
32
33
  "publishConfig": {