buddy-mcp-proxy 0.1.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 +105 -0
- package/dist/index.js +515 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# buddy-mcp-proxy
|
|
2
|
+
|
|
3
|
+
MCP proxy server for buddy.nvim sessions. This proxy allows OpenCode (or any MCP client) to connect to multiple buddy.nvim instances and switch between them dynamically.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
1. **Sessions Registry**: Reads buddy.nvim session info from `~/.local/state/buddy/sessions.json` (or `$XDG_STATE_HOME/buddy/sessions.json`)
|
|
8
|
+
2. **Meta-tools**: Exposes tools to list, select, and manage sessions
|
|
9
|
+
3. **Tool Proxying**: Once a session is selected, proxies all its tools to the MCP client
|
|
10
|
+
|
|
11
|
+
## Meta-tools
|
|
12
|
+
|
|
13
|
+
- `buddy_sessions_list` - List all available buddy.nvim sessions
|
|
14
|
+
- `buddy_session_select` - Select a session by ID to connect to
|
|
15
|
+
- `buddy_session_info` - Get info about the currently selected session
|
|
16
|
+
- `buddy_refresh` - Refresh sessions list and reconnect if needed
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g buddy-mcp-proxy
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or use it directly with `npx` — no install needed (see Usage below).
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### With OpenCode
|
|
29
|
+
|
|
30
|
+
Add to `~/.config/opencode/opencode.jsonc`:
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
{
|
|
34
|
+
"mcp": {
|
|
35
|
+
"vim": {
|
|
36
|
+
"type": "local",
|
|
37
|
+
"command": ["npx", "buddy-mcp-proxy"]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### With Claude Desktop
|
|
44
|
+
|
|
45
|
+
Add to your Claude Desktop MCP config:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"vim": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["buddy-mcp-proxy"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### From Source
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd mcp/buddy-mcp-proxy
|
|
62
|
+
npm install
|
|
63
|
+
npm run build
|
|
64
|
+
node dist/index.js
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Run directly with bun (no build needed)
|
|
71
|
+
bun run dev
|
|
72
|
+
|
|
73
|
+
# Build for production
|
|
74
|
+
npm run build
|
|
75
|
+
|
|
76
|
+
# Type check
|
|
77
|
+
npm run typecheck
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Sessions Registry Format
|
|
81
|
+
|
|
82
|
+
The proxy understands buddy.nvim's format (recommended) and a legacy array format.
|
|
83
|
+
|
|
84
|
+
Recommended (buddy.nvim):
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"version": 1,
|
|
89
|
+
"sessions": {
|
|
90
|
+
"<session_id>": {
|
|
91
|
+
"host": "127.0.0.1",
|
|
92
|
+
"port": 52341,
|
|
93
|
+
"cwd": "/home/user/project-a",
|
|
94
|
+
"label": "project-a",
|
|
95
|
+
"pid": 12345,
|
|
96
|
+
"started_at": 1730000000,
|
|
97
|
+
"last_seen": 1730000030
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Logs
|
|
104
|
+
|
|
105
|
+
All logs are written to stderr only (as required by MCP stdio transport).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ListToolsRequestSchema
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
function log(level, msg, data) {
|
|
16
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
17
|
+
const line = `[${timestamp}] [${level.toUpperCase()}] ${msg}`;
|
|
18
|
+
if (data !== void 0) {
|
|
19
|
+
console.error(line, data);
|
|
20
|
+
} else {
|
|
21
|
+
console.error(line);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getSessionsPath() {
|
|
25
|
+
const xdgState = process.env.XDG_STATE_HOME;
|
|
26
|
+
if (xdgState) {
|
|
27
|
+
return path.join(xdgState, "nvim", "buddy", "sessions.json");
|
|
28
|
+
}
|
|
29
|
+
return path.join(os.homedir(), ".local", "state", "nvim", "buddy", "sessions.json");
|
|
30
|
+
}
|
|
31
|
+
function normalizeSessions(raw) {
|
|
32
|
+
if (!raw || typeof raw !== "object")
|
|
33
|
+
return [];
|
|
34
|
+
const maybeArray = raw;
|
|
35
|
+
if (Array.isArray(maybeArray.sessions)) {
|
|
36
|
+
return maybeArray.sessions.filter((s) => !!s && typeof s.id === "string").map((s) => ({
|
|
37
|
+
...s,
|
|
38
|
+
host: s.host || "127.0.0.1"
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const maybeV1 = raw;
|
|
42
|
+
if (maybeV1.sessions && typeof maybeV1.sessions === "object") {
|
|
43
|
+
return Object.entries(maybeV1.sessions).flatMap(([id, s]) => {
|
|
44
|
+
const host = s.host || "127.0.0.1";
|
|
45
|
+
const port = Number(s.port);
|
|
46
|
+
const cwd = String(s.cwd || "");
|
|
47
|
+
if (!id || !Number.isFinite(port) || !cwd)
|
|
48
|
+
return [];
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
id,
|
|
52
|
+
host,
|
|
53
|
+
port,
|
|
54
|
+
cwd,
|
|
55
|
+
label: s.label ?? null,
|
|
56
|
+
pid: typeof s.pid === "number" ? s.pid : void 0,
|
|
57
|
+
started_at: s.started_at,
|
|
58
|
+
last_seen: typeof s.last_seen === "number" ? s.last_seen : void 0
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
function loadSessions() {
|
|
66
|
+
const sessionsPath = getSessionsPath();
|
|
67
|
+
try {
|
|
68
|
+
if (!fs.existsSync(sessionsPath)) {
|
|
69
|
+
log("info", `Sessions file not found: ${sessionsPath}`);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const content = fs.readFileSync(sessionsPath, "utf-8");
|
|
73
|
+
const raw = JSON.parse(content);
|
|
74
|
+
return normalizeSessions(raw);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log("error", `Failed to load sessions from ${sessionsPath}`, err);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function fuzzyIncludes(haystack, needle) {
|
|
81
|
+
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
82
|
+
}
|
|
83
|
+
function resolveSession(sessions, selector) {
|
|
84
|
+
if (selector.id) {
|
|
85
|
+
return sessions.find((s) => s.id === selector.id) ?? null;
|
|
86
|
+
}
|
|
87
|
+
if (typeof selector.index === "number") {
|
|
88
|
+
const sorted = [...sessions].sort((a, b) => {
|
|
89
|
+
const aSeen = a.last_seen ?? 0;
|
|
90
|
+
const bSeen = b.last_seen ?? 0;
|
|
91
|
+
return bSeen - aSeen;
|
|
92
|
+
});
|
|
93
|
+
const idx = selector.index - 1;
|
|
94
|
+
return sorted[idx] ?? null;
|
|
95
|
+
}
|
|
96
|
+
if (selector.label) {
|
|
97
|
+
const matches = sessions.filter(
|
|
98
|
+
(s) => s.label ? fuzzyIncludes(String(s.label), selector.label) : false
|
|
99
|
+
);
|
|
100
|
+
if (matches.length === 1)
|
|
101
|
+
return matches[0];
|
|
102
|
+
if (matches.length > 1) {
|
|
103
|
+
matches.sort((a, b) => (b.last_seen ?? 0) - (a.last_seen ?? 0));
|
|
104
|
+
return matches[0];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (selector.cwd) {
|
|
108
|
+
const matches = sessions.filter((s) => fuzzyIncludes(s.cwd, selector.cwd));
|
|
109
|
+
if (matches.length === 1)
|
|
110
|
+
return matches[0];
|
|
111
|
+
if (matches.length > 1) {
|
|
112
|
+
matches.sort((a, b) => (b.last_seen ?? 0) - (a.last_seen ?? 0));
|
|
113
|
+
return matches[0];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
var META_TOOLS = [
|
|
119
|
+
{
|
|
120
|
+
name: "buddy_sessions_list",
|
|
121
|
+
description: "List all available buddy.nvim sessions from the sessions registry",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {},
|
|
125
|
+
required: []
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "buddy_session_select",
|
|
130
|
+
description: "Select a buddy.nvim session by ID to connect to. This will expose that session's tools.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
session_id: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "(Deprecated) Session ID to select"
|
|
137
|
+
},
|
|
138
|
+
id: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "Session ID to select"
|
|
141
|
+
},
|
|
142
|
+
label: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "Fuzzy match session label"
|
|
145
|
+
},
|
|
146
|
+
cwd: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Fuzzy match session cwd"
|
|
149
|
+
},
|
|
150
|
+
index: {
|
|
151
|
+
type: "number",
|
|
152
|
+
description: "1-based index (sorted by last_seen desc)"
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
required: []
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "buddy_session_info",
|
|
160
|
+
description: "Get information about the currently selected buddy.nvim session",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {},
|
|
164
|
+
required: []
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "buddy_refresh",
|
|
169
|
+
description: "Refresh the sessions list and reconnect to the current session if still available",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {},
|
|
173
|
+
required: []
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
];
|
|
177
|
+
var DownstreamManager = class {
|
|
178
|
+
client = null;
|
|
179
|
+
transport = null;
|
|
180
|
+
currentSession = null;
|
|
181
|
+
cachedTools = [];
|
|
182
|
+
async connect(session) {
|
|
183
|
+
await this.disconnect();
|
|
184
|
+
const url = `http://${session.host || "127.0.0.1"}:${session.port}/sse`;
|
|
185
|
+
log("info", `Connecting to downstream: ${url}`);
|
|
186
|
+
try {
|
|
187
|
+
this.transport = new SSEClientTransport(new URL(url));
|
|
188
|
+
this.client = new Client(
|
|
189
|
+
{ name: "buddy-mcp-proxy", version: "0.1.0" },
|
|
190
|
+
{ capabilities: {} }
|
|
191
|
+
);
|
|
192
|
+
await this.client.connect(this.transport);
|
|
193
|
+
this.currentSession = session;
|
|
194
|
+
await this.refreshTools();
|
|
195
|
+
log("info", `Connected to session ${session.id} on port ${session.port}`);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
log("error", `Failed to connect to ${url}`, err);
|
|
198
|
+
await this.disconnect();
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async disconnect() {
|
|
203
|
+
if (this.transport) {
|
|
204
|
+
try {
|
|
205
|
+
await this.transport.close();
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
this.client = null;
|
|
210
|
+
this.transport = null;
|
|
211
|
+
this.currentSession = null;
|
|
212
|
+
this.cachedTools = [];
|
|
213
|
+
}
|
|
214
|
+
async refreshTools() {
|
|
215
|
+
if (!this.client) {
|
|
216
|
+
this.cachedTools = [];
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.client.listTools();
|
|
221
|
+
this.cachedTools = result.tools || [];
|
|
222
|
+
log("info", `Fetched ${this.cachedTools.length} tools from downstream`);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
log("error", "Failed to fetch downstream tools", err);
|
|
225
|
+
this.cachedTools = [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async callTool(name, args) {
|
|
229
|
+
if (!this.client) {
|
|
230
|
+
throw new Error("No downstream session connected");
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const result = await this.client.callTool({ name, arguments: args });
|
|
234
|
+
if ("content" in result) {
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
if ("toolResult" in result) {
|
|
238
|
+
const toolResult = result.toolResult;
|
|
239
|
+
return {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: "text",
|
|
243
|
+
text: typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult, null, 2)
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
} catch (err) {
|
|
254
|
+
log("error", `Failed to call tool ${name}`, err);
|
|
255
|
+
await this.checkHealth();
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async checkHealth() {
|
|
260
|
+
if (!this.client || !this.currentSession) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
await this.client.listTools();
|
|
265
|
+
return true;
|
|
266
|
+
} catch {
|
|
267
|
+
log("warn", "Downstream connection is dead, clearing selection");
|
|
268
|
+
await this.disconnect();
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
getTools() {
|
|
273
|
+
return this.cachedTools;
|
|
274
|
+
}
|
|
275
|
+
getCurrentSession() {
|
|
276
|
+
return this.currentSession;
|
|
277
|
+
}
|
|
278
|
+
isConnected() {
|
|
279
|
+
return this.client !== null && this.currentSession !== null;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
async function main() {
|
|
283
|
+
log("info", "Starting buddy-mcp-proxy");
|
|
284
|
+
const downstream = new DownstreamManager();
|
|
285
|
+
const server = new Server(
|
|
286
|
+
{ name: "buddy-mcp-proxy", version: "0.1.0" },
|
|
287
|
+
{ capabilities: { tools: { listChanged: true } } }
|
|
288
|
+
);
|
|
289
|
+
const notifyToolsChanged = async () => {
|
|
290
|
+
try {
|
|
291
|
+
await server.notification({
|
|
292
|
+
method: "notifications/tools/list_changed"
|
|
293
|
+
});
|
|
294
|
+
log("info", "Sent tools/list_changed notification");
|
|
295
|
+
} catch (err) {
|
|
296
|
+
log("error", "Failed to send tools/list_changed notification", err);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
300
|
+
if (downstream.isConnected()) {
|
|
301
|
+
await downstream.checkHealth();
|
|
302
|
+
}
|
|
303
|
+
const downstreamTools = downstream.getTools();
|
|
304
|
+
const allTools = [...META_TOOLS, ...downstreamTools];
|
|
305
|
+
log(
|
|
306
|
+
"info",
|
|
307
|
+
`Returning ${allTools.length} tools (${META_TOOLS.length} meta + ${downstreamTools.length} downstream)`
|
|
308
|
+
);
|
|
309
|
+
return { tools: allTools };
|
|
310
|
+
});
|
|
311
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
312
|
+
const { name, arguments: args = {} } = request.params;
|
|
313
|
+
log("info", `Tool call: ${name}`);
|
|
314
|
+
if (name === "buddy_sessions_list") {
|
|
315
|
+
const sessions = loadSessions();
|
|
316
|
+
const currentId = downstream.getCurrentSession()?.id;
|
|
317
|
+
return {
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: JSON.stringify(
|
|
322
|
+
{
|
|
323
|
+
sessions: sessions.sort((a, b) => (b.last_seen ?? 0) - (a.last_seen ?? 0)).map((s) => ({
|
|
324
|
+
id: s.id,
|
|
325
|
+
label: s.label ?? null,
|
|
326
|
+
host: s.host,
|
|
327
|
+
port: s.port,
|
|
328
|
+
cwd: s.cwd,
|
|
329
|
+
pid: s.pid,
|
|
330
|
+
started_at: s.started_at,
|
|
331
|
+
last_seen: s.last_seen,
|
|
332
|
+
url: `http://${s.host}:${s.port}/sse`,
|
|
333
|
+
selected: s.id === currentId
|
|
334
|
+
})),
|
|
335
|
+
current_session_id: currentId || null
|
|
336
|
+
},
|
|
337
|
+
null,
|
|
338
|
+
2
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (name === "buddy_session_select") {
|
|
345
|
+
const selector = {
|
|
346
|
+
id: args.session_id || args.id || void 0,
|
|
347
|
+
label: args.label || void 0,
|
|
348
|
+
cwd: args.cwd || void 0,
|
|
349
|
+
index: typeof args.index === "number" ? args.index : void 0
|
|
350
|
+
};
|
|
351
|
+
if (!selector.id && !selector.label && !selector.cwd && !selector.index) {
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: "text",
|
|
356
|
+
text: "Error: provide one of: id, session_id, label, cwd, index"
|
|
357
|
+
}
|
|
358
|
+
],
|
|
359
|
+
isError: true
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const sessions = loadSessions();
|
|
363
|
+
const session = resolveSession(sessions, selector);
|
|
364
|
+
if (!session) {
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: "text",
|
|
369
|
+
text: `Error: Session not found for selector ${JSON.stringify(selector)}`
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
isError: true
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
await downstream.connect(session);
|
|
377
|
+
await notifyToolsChanged();
|
|
378
|
+
return {
|
|
379
|
+
content: [
|
|
380
|
+
{
|
|
381
|
+
type: "text",
|
|
382
|
+
text: `Connected to session '${session.id}' at http://${session.host}:${session.port}. ${downstream.getTools().length} tools now available.`
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
};
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return {
|
|
388
|
+
content: [
|
|
389
|
+
{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: `Error connecting to session '${session.id}': ${err instanceof Error ? err.message : String(err)}`
|
|
392
|
+
}
|
|
393
|
+
],
|
|
394
|
+
isError: true
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (name === "buddy_session_info") {
|
|
399
|
+
const session = downstream.getCurrentSession();
|
|
400
|
+
if (!session) {
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{ type: "text", text: "No session currently selected" }
|
|
404
|
+
]
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const tools = downstream.getTools();
|
|
408
|
+
return {
|
|
409
|
+
content: [
|
|
410
|
+
{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: JSON.stringify(
|
|
413
|
+
{
|
|
414
|
+
id: session.id,
|
|
415
|
+
port: session.port,
|
|
416
|
+
cwd: session.cwd,
|
|
417
|
+
pid: session.pid,
|
|
418
|
+
started_at: session.started_at,
|
|
419
|
+
tools_count: tools.length,
|
|
420
|
+
tools: tools.map((t) => t.name)
|
|
421
|
+
},
|
|
422
|
+
null,
|
|
423
|
+
2
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
if (name === "buddy_refresh") {
|
|
430
|
+
const currentSession = downstream.getCurrentSession();
|
|
431
|
+
const sessions = loadSessions();
|
|
432
|
+
if (currentSession) {
|
|
433
|
+
const stillExists = sessions.find((s) => s.id === currentSession.id);
|
|
434
|
+
if (stillExists) {
|
|
435
|
+
const healthy = await downstream.checkHealth();
|
|
436
|
+
if (!healthy) {
|
|
437
|
+
try {
|
|
438
|
+
await downstream.connect(stillExists);
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
await downstream.refreshTools();
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
await downstream.disconnect();
|
|
446
|
+
}
|
|
447
|
+
await notifyToolsChanged();
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: JSON.stringify(
|
|
454
|
+
{
|
|
455
|
+
sessions_count: sessions.length,
|
|
456
|
+
current_session: downstream.getCurrentSession()?.id || null,
|
|
457
|
+
connected: downstream.isConnected(),
|
|
458
|
+
tools_count: downstream.getTools().length
|
|
459
|
+
},
|
|
460
|
+
null,
|
|
461
|
+
2
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (!downstream.isConnected()) {
|
|
468
|
+
return {
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: `Error: No buddy.nvim session selected. Use buddy_sessions_list and buddy_session_select first.`
|
|
473
|
+
}
|
|
474
|
+
],
|
|
475
|
+
isError: true
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const result = await downstream.callTool(name, args);
|
|
480
|
+
return result;
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (!downstream.isConnected()) {
|
|
483
|
+
await notifyToolsChanged();
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
content: [
|
|
487
|
+
{
|
|
488
|
+
type: "text",
|
|
489
|
+
text: `Error calling tool '${name}': ${err instanceof Error ? err.message : String(err)}`
|
|
490
|
+
}
|
|
491
|
+
],
|
|
492
|
+
isError: true
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
const transport = new StdioServerTransport();
|
|
497
|
+
await server.connect(transport);
|
|
498
|
+
log("info", "buddy-mcp-proxy running on stdio");
|
|
499
|
+
process.on("SIGINT", async () => {
|
|
500
|
+
log("info", "Shutting down...");
|
|
501
|
+
await downstream.disconnect();
|
|
502
|
+
await server.close();
|
|
503
|
+
process.exit(0);
|
|
504
|
+
});
|
|
505
|
+
process.on("SIGTERM", async () => {
|
|
506
|
+
log("info", "Shutting down...");
|
|
507
|
+
await downstream.disconnect();
|
|
508
|
+
await server.close();
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
main().catch((err) => {
|
|
513
|
+
log("error", "Fatal error", err);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "buddy-mcp-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP proxy server for buddy.nvim — connect AI agents to Neovim sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"buddy-mcp-proxy": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=dist/index.js --banner:js='#!/usr/bin/env node' --packages=external",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"dev": "bun run src/index.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"neovim",
|
|
24
|
+
"nvim",
|
|
25
|
+
"buddy",
|
|
26
|
+
"ai",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"editor",
|
|
29
|
+
"proxy"
|
|
30
|
+
],
|
|
31
|
+
"author": "arismoko",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/arismoko/buddy.nvim.git",
|
|
36
|
+
"directory": "mcp/buddy-mcp-proxy"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/arismoko/buddy.nvim/tree/main/mcp/buddy-mcp-proxy",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/arismoko/buddy.nvim/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"esbuild": "^0.20.0",
|
|
51
|
+
"typescript": "^5.4.0"
|
|
52
|
+
}
|
|
53
|
+
}
|