diffler-mcp 0.13.1 → 0.14.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 +23 -4
- package/index.js +347 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,10 +39,21 @@ directory it searched.
|
|
|
39
39
|
|
|
40
40
|
Resolution order (first match wins):
|
|
41
41
|
|
|
42
|
-
1.
|
|
43
|
-
2. `--
|
|
44
|
-
3.
|
|
45
|
-
|
|
42
|
+
1. `use_instance` called earlier in this session
|
|
43
|
+
2. `--url <url>` / `DIFFLER_MCP_URL`: full endpoint, e.g. `http://127.0.0.1:8417/mcp`
|
|
44
|
+
3. `--port <n>` / `DIFFLER_MCP_PORT` and `--host <h>` / `DIFFLER_MCP_HOST`
|
|
45
|
+
4. the live port in the nearest `.diffler/mcp.json`, searching the start
|
|
46
|
+
directory (`--repo <path>`, default: cwd) and then each parent, if that
|
|
47
|
+
diffler is still running
|
|
48
|
+
5. the per-user instance registry (`$XDG_STATE_HOME/diffler/instances`,
|
|
49
|
+
`~/.local/state` by default): the most recently started live diffler;
|
|
50
|
+
`use_instance` switches; several are listed by `list_instances`
|
|
51
|
+
|
|
52
|
+
`use_instance` connects before it answers, so a bad target never gets bound.
|
|
53
|
+
When diffler isn't running yet, the proxy keeps retrying quietly and announces
|
|
54
|
+
its tools once one appears, so a human can start Claude first and diffler
|
|
55
|
+
second. A call diffler doesn't answer within 110 s fails with that instance
|
|
56
|
+
named.
|
|
46
57
|
|
|
47
58
|
Discovery covers the normal case, so nothing needs configuring:
|
|
48
59
|
|
|
@@ -60,6 +71,14 @@ Discovery covers the normal case, so nothing needs configuring:
|
|
|
60
71
|
Reach for `--port`, `--host` or `--repo` when diffler runs somewhere the walk-up
|
|
61
72
|
cannot see, such as another machine over a tunnel.
|
|
62
73
|
|
|
74
|
+
A human's diffler and an agent's shell are often in different repos. Two
|
|
75
|
+
proxy-owned tools handle that, always available alongside diffler's own:
|
|
76
|
+
|
|
77
|
+
- **list_instances**: every running diffler this proxy can reach, across all
|
|
78
|
+
repos, from the registry.
|
|
79
|
+
- **use_instance**: point this proxy at one of them, by repo path (or a
|
|
80
|
+
unique directory-name suffix) or port, for the rest of this session.
|
|
81
|
+
|
|
63
82
|
## Prefer HTTP directly?
|
|
64
83
|
|
|
65
84
|
Claude Code speaks HTTP natively, so you can skip this proxy entirely:
|
package/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// stdio↔HTTP MCP proxy bridging Claude Code to a running diffler TUI, lazily
|
|
3
3
|
// (re)connecting so it survives diffler quitting and restarting on a new port.
|
|
4
4
|
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
6
7
|
import { dirname, join, resolve } from "node:path";
|
|
7
8
|
|
|
8
9
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -16,6 +17,34 @@ import {
|
|
|
16
17
|
|
|
17
18
|
const DEFAULT_HOST = "127.0.0.1";
|
|
18
19
|
const ENDPOINT_FILE = join(".diffler", "mcp.json");
|
|
20
|
+
// A missing diffler is common (a human starts Claude before the TUI), so the
|
|
21
|
+
// background retry below stops at a ceiling.
|
|
22
|
+
const RETRY_MS = Number(process.env.DIFFLER_MCP_RETRY_MS) || 3000;
|
|
23
|
+
const RETRY_MAX_MS = Number(process.env.DIFFLER_MCP_RETRY_MAX_MS) || 10 * 60 * 1000;
|
|
24
|
+
// Longer than a client's own request ceiling, so a hung diffler fails the
|
|
25
|
+
// call itself, with the diagnosis this proxy can give.
|
|
26
|
+
const CALL_DEADLINE_MS = Number(process.env.DIFFLER_MCP_CALL_DEADLINE_MS) || 110_000;
|
|
27
|
+
|
|
28
|
+
const PROXY_TOOLS = [
|
|
29
|
+
{
|
|
30
|
+
name: "list_instances",
|
|
31
|
+
description: "List every running diffler instance this proxy can reach, across all repos.",
|
|
32
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "use_instance",
|
|
36
|
+
description:
|
|
37
|
+
"Point this proxy at one diffler instance, by repo path (or a unique directory-name suffix) or port.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
repo: { type: "string" },
|
|
42
|
+
port: { type: "number" },
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
19
48
|
|
|
20
49
|
function parseArgs(argv) {
|
|
21
50
|
const opts = {};
|
|
@@ -41,39 +70,198 @@ function parseArgs(argv) {
|
|
|
41
70
|
return opts;
|
|
42
71
|
}
|
|
43
72
|
|
|
73
|
+
// A process that no longer exists (ESRCH) is dead; anything else (alive, or
|
|
74
|
+
// EPERM because it's owned by someone else) counts as alive. A missing pid
|
|
75
|
+
// means an older TUI that predates this field, which we can't disprove.
|
|
76
|
+
function pidAlive(pid) {
|
|
77
|
+
if (pid == null) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
process.kill(pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return err.code !== "ESRCH";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
// Each diffler publishes its live port under its own repo root, so the walk-up
|
|
45
|
-
// finds the instance owning the directory the editor launched from.
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
let dir = start;
|
|
89
|
+
// finds the instance owning the directory the editor launched from. Returns
|
|
90
|
+
// null both when nothing is found and when the closest file names a dead
|
|
91
|
+
// process, so the caller falls through to the cross-repo registry either way.
|
|
92
|
+
function findLocalEndpoint(from) {
|
|
93
|
+
let dir = resolve(from);
|
|
51
94
|
for (;;) {
|
|
52
95
|
try {
|
|
53
|
-
const { port } = JSON.parse(readFileSync(join(dir, ENDPOINT_FILE), "utf8"));
|
|
96
|
+
const { port, pid } = JSON.parse(readFileSync(join(dir, ENDPOINT_FILE), "utf8"));
|
|
54
97
|
if (typeof port === "number") {
|
|
55
|
-
return port;
|
|
98
|
+
return pidAlive(pid) ? port : null;
|
|
56
99
|
}
|
|
57
100
|
} catch {
|
|
58
101
|
// unreadable or malformed reads like absent: keep walking up
|
|
59
102
|
}
|
|
60
103
|
const parent = dirname(dir);
|
|
61
104
|
if (parent === dir) {
|
|
62
|
-
|
|
105
|
+
return null;
|
|
63
106
|
}
|
|
64
107
|
dir = parent;
|
|
65
108
|
}
|
|
66
109
|
}
|
|
67
110
|
|
|
111
|
+
// Mirrors how the diffler binary resolves XDG_STATE_HOME, so both sides agree
|
|
112
|
+
// on where the per-user instance registry lives.
|
|
113
|
+
function registryDir() {
|
|
114
|
+
const base = process.env.DIFFLER_STATE_DIR || process.env.XDG_STATE_HOME || join(homedir(), ".local", "state");
|
|
115
|
+
return join(base, "diffler", "instances");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Every running diffler that isn't the one found by the walk-up: read from
|
|
119
|
+
// the registry, dropping (and deleting) entries whose process has exited.
|
|
120
|
+
function liveRegistry() {
|
|
121
|
+
let files;
|
|
122
|
+
try {
|
|
123
|
+
files = readdirSync(registryDir());
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const live = [];
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
if (!file.endsWith(".json")) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const path = join(registryDir(), file);
|
|
133
|
+
let entry;
|
|
134
|
+
try {
|
|
135
|
+
entry = JSON.parse(readFileSync(path, "utf8"));
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!pidAlive(entry.pid)) {
|
|
140
|
+
try {
|
|
141
|
+
unlinkSync(path);
|
|
142
|
+
} catch {
|
|
143
|
+
// another proxy already pruned it
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
entry.mtimeMs = statSync(path).mtimeMs;
|
|
148
|
+
live.push(entry);
|
|
149
|
+
}
|
|
150
|
+
return live;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function describeInstances(instances) {
|
|
154
|
+
return instances.map((i) => `${i.repo} (port ${i.port})`).join(", ");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function matchInstances(instances, { repo, port }) {
|
|
158
|
+
if (port != null) {
|
|
159
|
+
return instances.filter((i) => i.port === Number(port));
|
|
160
|
+
}
|
|
161
|
+
if (!repo) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const exact = instances.filter((i) => i.repo === repo);
|
|
165
|
+
if (exact.length) {
|
|
166
|
+
return exact;
|
|
167
|
+
}
|
|
168
|
+
return instances.filter((i) => i.repo.endsWith(`/${repo}`) || i.repo.endsWith(`\\${repo}`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Best-effort label for an error message: which repo (and port) a URL names,
|
|
172
|
+
// falling back to the port alone when the registry doesn't know it.
|
|
173
|
+
function describeUrl(url) {
|
|
174
|
+
const match = liveRegistry().find((i) => i.url === url);
|
|
175
|
+
if (match) {
|
|
176
|
+
return `${match.repo} (port ${match.port})`;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
return `port ${new URL(url).port}`;
|
|
180
|
+
} catch {
|
|
181
|
+
return "diffler";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class CallDeadline extends Error {}
|
|
186
|
+
|
|
187
|
+
// Dropping the `upstream` reference alone leaves the transport's
|
|
188
|
+
// AbortController and reconnection timer alive, so every place that
|
|
189
|
+
// abandons a client closes it first.
|
|
190
|
+
function closeQuietly(client) {
|
|
191
|
+
if (!client) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
client.close().catch(() => {});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A call to the upstream that never answers must not hang the client
|
|
198
|
+
// forever; racing it against a timer is what lets the proxy give up and
|
|
199
|
+
// report it. `fn` is what to race (a tool call, a tools/list), so both share
|
|
200
|
+
// the same deadline and reconnect handling in `withUpstream` below.
|
|
201
|
+
function withDeadline(client, fn, meta) {
|
|
202
|
+
let timer;
|
|
203
|
+
const timeout = new Promise((_, reject) => {
|
|
204
|
+
timer = setTimeout(() => reject(new CallDeadline(meta)), CALL_DEADLINE_MS);
|
|
205
|
+
});
|
|
206
|
+
return Promise.race([fn(client), timeout]).finally(() => clearTimeout(timer));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function toolError(text) {
|
|
210
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function deadlineError(meta) {
|
|
214
|
+
const seconds = Math.round(CALL_DEADLINE_MS / 1000);
|
|
215
|
+
return toolError(`${meta || "diffler"} did not answer within ${seconds}s. Call again to reconnect.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function unreachableError(err) {
|
|
219
|
+
const instances = liveRegistry();
|
|
220
|
+
const extra = instances.length
|
|
221
|
+
? ` Other diffler instances running: ${describeInstances(instances)}.`
|
|
222
|
+
: "";
|
|
223
|
+
return toolError(`diffler isn't reachable. Is it running in this repo? (${err.message ?? err})${extra}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Set by the use_instance tool; overrides every other resolution source for
|
|
227
|
+
// the rest of this proxy process.
|
|
228
|
+
let overrideUrl = null;
|
|
229
|
+
|
|
230
|
+
// `ambiguous` marks the one resolution path with no explicit say-so from the
|
|
231
|
+
// human or the caller: more than one diffler is running and the newest was
|
|
232
|
+
// picked for them. `forwardCall` uses it to tell the agent once per bind.
|
|
68
233
|
function resolveUrl(opts) {
|
|
234
|
+
if (overrideUrl) {
|
|
235
|
+
return { url: overrideUrl, ambiguous: false };
|
|
236
|
+
}
|
|
69
237
|
const env = process.env;
|
|
70
238
|
const explicit = opts.url || env.DIFFLER_MCP_URL;
|
|
71
239
|
if (explicit) {
|
|
72
|
-
return explicit;
|
|
240
|
+
return { url: explicit, ambiguous: false };
|
|
73
241
|
}
|
|
74
242
|
const host = opts.host || env.DIFFLER_MCP_HOST || DEFAULT_HOST;
|
|
75
|
-
const
|
|
76
|
-
|
|
243
|
+
const explicitPort = opts.port || env.DIFFLER_MCP_PORT;
|
|
244
|
+
if (explicitPort) {
|
|
245
|
+
return { url: `http://${host}:${explicitPort}/mcp`, ambiguous: false };
|
|
246
|
+
}
|
|
247
|
+
const local = findLocalEndpoint(opts.repo || process.cwd());
|
|
248
|
+
if (local !== null) {
|
|
249
|
+
return { url: `http://${host}:${local}/mcp`, ambiguous: false };
|
|
250
|
+
}
|
|
251
|
+
const instances = liveRegistry();
|
|
252
|
+
if (instances.length === 1) {
|
|
253
|
+
return { url: instances[0].url, ambiguous: false };
|
|
254
|
+
}
|
|
255
|
+
if (instances.length > 1) {
|
|
256
|
+
const newest = instances.reduce((a, b) => (b.mtimeMs > a.mtimeMs ? b : a));
|
|
257
|
+
process.stderr.write(
|
|
258
|
+
`diffler-mcp: several diffler instances are running (${describeInstances(instances)}); binding the most recently started, ${newest.repo} (port ${newest.port}). Call use_instance to switch.\n`,
|
|
259
|
+
);
|
|
260
|
+
return { url: newest.url, ambiguous: true };
|
|
261
|
+
}
|
|
262
|
+
throw new Error(
|
|
263
|
+
`no diffler is running in ${resolve(opts.repo || process.cwd())} or any parent (no ${ENDPOINT_FILE})`,
|
|
264
|
+
);
|
|
77
265
|
}
|
|
78
266
|
|
|
79
267
|
async function main() {
|
|
@@ -84,13 +272,28 @@ async function main() {
|
|
|
84
272
|
);
|
|
85
273
|
|
|
86
274
|
let upstream = null;
|
|
275
|
+
let upstreamMeta = null;
|
|
87
276
|
let connecting = null;
|
|
277
|
+
let retryActive = false;
|
|
278
|
+
// Set on an ambiguous auto-bind (several instances, newest picked with no
|
|
279
|
+
// say-so); `forwardCall` reports it on the first result after and clears
|
|
280
|
+
// it. A resolution the caller or the human chose leaves this null.
|
|
281
|
+
let pendingBindNotice = null;
|
|
88
282
|
|
|
89
283
|
const connect = async () => {
|
|
284
|
+
const { url, ambiguous } = resolveUrl(opts);
|
|
90
285
|
const client = new Client({ name: "diffler-mcp-proxy", version: "0.1.0" });
|
|
91
|
-
await client.connect(new StreamableHTTPClientTransport(new URL(
|
|
286
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
287
|
+
// One round trip before caching: a registered pid can be alive while the
|
|
288
|
+
// MCP server behind it is not (mid-restart, a crashed listener), and this
|
|
289
|
+
// is what tells the two apart.
|
|
290
|
+
await client.listTools();
|
|
92
291
|
// cache synchronously after the await so a later close can't race ahead of it
|
|
93
292
|
upstream = client;
|
|
293
|
+
upstreamMeta = describeUrl(url);
|
|
294
|
+
pendingBindNotice = ambiguous
|
|
295
|
+
? `Bound to ${upstreamMeta} because several diffler instances are running; call use_instance to switch.`
|
|
296
|
+
: null;
|
|
94
297
|
const drop = () => {
|
|
95
298
|
if (upstream === client) {
|
|
96
299
|
upstream = null;
|
|
@@ -114,37 +317,149 @@ async function main() {
|
|
|
114
317
|
return connecting;
|
|
115
318
|
};
|
|
116
319
|
|
|
320
|
+
// Runs `fn` against the live upstream client under the deadline above; a
|
|
321
|
+
// deadline drops the upstream and fails outright, no second wait on the
|
|
322
|
+
// same call, while any other failure gets one reconnect-and-retry. Shared
|
|
323
|
+
// by tools/list and tools/call, so a hung diffler fails either one alike.
|
|
117
324
|
const withUpstream = async (fn) => {
|
|
325
|
+
let client;
|
|
326
|
+
const attempt = async () => {
|
|
327
|
+
client = await ensureUpstream();
|
|
328
|
+
const meta = upstreamMeta;
|
|
329
|
+
try {
|
|
330
|
+
return await withDeadline(client, fn, meta);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (err instanceof CallDeadline) {
|
|
333
|
+
closeQuietly(client);
|
|
334
|
+
upstream = null;
|
|
335
|
+
}
|
|
336
|
+
throw err;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
118
339
|
try {
|
|
119
|
-
return await
|
|
120
|
-
} catch {
|
|
340
|
+
return await attempt();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (err instanceof CallDeadline) {
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
closeQuietly(client);
|
|
121
346
|
upstream = null;
|
|
122
|
-
return await
|
|
347
|
+
return await attempt();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Lets a human start Claude first and diffler second: a `tools/list` that
|
|
352
|
+
// finds nothing arms this, and it keeps trying quietly until diffler
|
|
353
|
+
// appears (or ten minutes pass), announcing the new tools once it does.
|
|
354
|
+
const startRetryingUpstream = () => {
|
|
355
|
+
if (retryActive) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
retryActive = true;
|
|
359
|
+
const deadline = Date.now() + RETRY_MAX_MS;
|
|
360
|
+
const attempt = async () => {
|
|
361
|
+
try {
|
|
362
|
+
await ensureUpstream();
|
|
363
|
+
retryActive = false;
|
|
364
|
+
server.sendToolListChanged?.().catch(() => {});
|
|
365
|
+
return;
|
|
366
|
+
} catch {
|
|
367
|
+
// still unreachable
|
|
368
|
+
}
|
|
369
|
+
if (Date.now() >= deadline) {
|
|
370
|
+
retryActive = false;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
setTimeout(attempt, RETRY_MS);
|
|
374
|
+
};
|
|
375
|
+
setTimeout(attempt, RETRY_MS);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Forwards one tool call, appending the ambiguous-bind notice to the first
|
|
379
|
+
// result after such a bind (once only: it clears itself here).
|
|
380
|
+
const forwardCall = async (params) => {
|
|
381
|
+
try {
|
|
382
|
+
const result = await withUpstream((client) => client.callTool(params));
|
|
383
|
+
if (!pendingBindNotice) {
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
const notice = pendingBindNotice;
|
|
387
|
+
pendingBindNotice = null;
|
|
388
|
+
return { ...result, content: [...(result.content ?? []), { type: "text", text: notice }] };
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return err instanceof CallDeadline ? deadlineError(err.message) : unreachableError(err);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const currentUrl = () => {
|
|
395
|
+
try {
|
|
396
|
+
return resolveUrl(opts).url;
|
|
397
|
+
} catch {
|
|
398
|
+
return null;
|
|
123
399
|
}
|
|
124
400
|
};
|
|
125
401
|
|
|
402
|
+
const handleListInstances = () => {
|
|
403
|
+
const here = currentUrl();
|
|
404
|
+
const instances = liveRegistry().map((i) => ({
|
|
405
|
+
repo: i.repo,
|
|
406
|
+
port: i.port,
|
|
407
|
+
pid: i.pid,
|
|
408
|
+
current: i.url === here,
|
|
409
|
+
}));
|
|
410
|
+
return { content: [{ type: "text", text: JSON.stringify({ instances }) }] };
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const handleUseInstance = async (args) => {
|
|
414
|
+
const instances = liveRegistry();
|
|
415
|
+
const matches = matchInstances(instances, args);
|
|
416
|
+
if (matches.length === 0) {
|
|
417
|
+
return toolError(
|
|
418
|
+
`no running instance matches ${JSON.stringify(args)}. Choices: ${describeInstances(instances) || "none running"}.`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (matches.length > 1) {
|
|
422
|
+
return toolError(
|
|
423
|
+
`${JSON.stringify(args)} matches more than one instance: ${describeInstances(matches)}. Be more specific.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const target = matches[0];
|
|
427
|
+
const previousOverride = overrideUrl;
|
|
428
|
+
overrideUrl = target.url;
|
|
429
|
+
closeQuietly(upstream);
|
|
430
|
+
upstream = null;
|
|
431
|
+
try {
|
|
432
|
+
await ensureUpstream();
|
|
433
|
+
} catch (err) {
|
|
434
|
+
overrideUrl = previousOverride;
|
|
435
|
+
return toolError(`could not connect to ${target.repo} (port ${target.port}): ${err.message ?? err}`);
|
|
436
|
+
}
|
|
437
|
+
server.sendToolListChanged?.().catch(() => {});
|
|
438
|
+
return {
|
|
439
|
+
content: [
|
|
440
|
+
{ type: "text", text: JSON.stringify({ ok: true, repo: target.repo, port: target.port }) },
|
|
441
|
+
],
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
|
|
126
445
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
446
|
+
let upstreamTools = [];
|
|
127
447
|
try {
|
|
128
|
-
|
|
448
|
+
upstreamTools = (await withUpstream((client) => client.listTools())).tools ?? [];
|
|
129
449
|
} catch {
|
|
130
|
-
|
|
450
|
+
upstreamTools = [];
|
|
451
|
+
startRetryingUpstream();
|
|
131
452
|
}
|
|
453
|
+
return { tools: [...PROXY_TOOLS, ...upstreamTools] };
|
|
132
454
|
});
|
|
133
455
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
134
|
-
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
isError: true,
|
|
140
|
-
content: [
|
|
141
|
-
{
|
|
142
|
-
type: "text",
|
|
143
|
-
text: `diffler isn't reachable. Is it running in this repo? (${err.message ?? err})`,
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
};
|
|
456
|
+
if (request.params.name === "list_instances") {
|
|
457
|
+
return handleListInstances();
|
|
458
|
+
}
|
|
459
|
+
if (request.params.name === "use_instance") {
|
|
460
|
+
return handleUseInstance(request.params.arguments ?? {});
|
|
147
461
|
}
|
|
462
|
+
return forwardCall(request.params);
|
|
148
463
|
});
|
|
149
464
|
|
|
150
465
|
await server.connect(new StdioServerTransport());
|