vibevibes-mcp-server 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 +96 -0
- package/dist/http-client.d.ts +21 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +79 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +818 -0
- package/dist/index.js.map +1 -0
- package/dist/supabase-client.d.ts +121 -0
- package/dist/supabase-client.d.ts.map +1 -0
- package/dist/supabase-client.js +420 -0
- package/dist/supabase-client.js.map +1 -0
- package/package.json +48 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { supabase, WEB_URL, SUPABASE_URL, listExperiences, getExperienceSource, getExperienceFiles, getExperienceFull, getRoomState, getRoomMetadata, getEventHistory, createRoom, countRooms, executeToolGate, webPost, joinRoom, watchRoom, leaveRoom, isJoined, getJoinedSession, getExperienceTools, getExperienceAgentHints, runExperienceTests, } from "./supabase-client.js";
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "vibevibes",
|
|
8
|
+
version: "0.2.0",
|
|
9
|
+
});
|
|
10
|
+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || "";
|
|
11
|
+
let GITHUB_OWNER = process.env.GITHUB_OWNER || "";
|
|
12
|
+
async function initGitHubAuth() {
|
|
13
|
+
if (!GITHUB_TOKEN) {
|
|
14
|
+
console.error("[vibevibes-mcp] WARNING: GITHUB_TOKEN not set. Experience write operations may be limited.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch("https://api.github.com/user", {
|
|
19
|
+
headers: {
|
|
20
|
+
Authorization: `Bearer ${GITHUB_TOKEN}`,
|
|
21
|
+
Accept: "application/vnd.github+json",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error(`[vibevibes-mcp] WARNING: GitHub token validation failed (HTTP ${res.status}).`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const user = (await res.json());
|
|
29
|
+
console.error(`[vibevibes-mcp] GitHub authenticated as: ${user.login}`);
|
|
30
|
+
if (!GITHUB_OWNER) {
|
|
31
|
+
GITHUB_OWNER = user.login;
|
|
32
|
+
console.error(`[vibevibes-mcp] GITHUB_OWNER derived from token: ${GITHUB_OWNER}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error(`[vibevibes-mcp] WARNING: GitHub auth check failed: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
server.tool("vibevibes_health", "Check health status of the vibe-vibe realtime server. Returns server status, active room count, and connection count.", {}, async () => {
|
|
40
|
+
try {
|
|
41
|
+
const roomCount = await countRooms();
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `Server: ok (Supabase-backed)\nRooms: ${roomCount}\nConnections: N/A (serverless)`,
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { content: [{ type: "text", text: `Health check failed: ${err.message}` }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
server.tool("vibevibes_diagnostics", "Get detailed diagnostics from the vibe-vibe realtime server including uptime, recent events (errors, tool calls, etc.), room count, and rate limit info. Use this to find bugs and monitor the app.", {}, async () => {
|
|
54
|
+
try {
|
|
55
|
+
const roomCount = await countRooms();
|
|
56
|
+
const { data: recentEvents } = await supabase
|
|
57
|
+
.from("room_events")
|
|
58
|
+
.select("id, room_id, ts, actor_id, tool, error")
|
|
59
|
+
.order("ts", { ascending: false })
|
|
60
|
+
.limit(20);
|
|
61
|
+
let eventLog = "No recent events.";
|
|
62
|
+
if (recentEvents && recentEvents.length > 0) {
|
|
63
|
+
eventLog = recentEvents
|
|
64
|
+
.map((e) => {
|
|
65
|
+
const time = new Date(e.ts).toISOString();
|
|
66
|
+
const level = e.error ? "ERROR" : "INFO";
|
|
67
|
+
return `[${time}] ${level} [${e.room_id}] ${e.actor_id} called ${e.tool}${e.error ? ` — ${e.error}` : ""}`;
|
|
68
|
+
})
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
content: [{
|
|
73
|
+
type: "text",
|
|
74
|
+
text: [
|
|
75
|
+
`Status: ok (Supabase-backed, serverless)`,
|
|
76
|
+
`Rooms: ${roomCount}`,
|
|
77
|
+
`Architecture: Supabase Edge Functions + Broadcast`,
|
|
78
|
+
``,
|
|
79
|
+
`=== Recent Events (${recentEvents?.length || 0}) ===`,
|
|
80
|
+
eventLog,
|
|
81
|
+
].join("\n"),
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return { content: [{ type: "text", text: `Diagnostics failed: ${err.message}` }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
server.tool("vibevibes_list_experiences", "List all available experiences saved in Supabase. Returns id, title, description, and version for each.", {}, async () => {
|
|
90
|
+
try {
|
|
91
|
+
const exps = await listExperiences();
|
|
92
|
+
if (exps.length === 0) {
|
|
93
|
+
return { content: [{ type: "text", text: "No experiences found in Supabase." }] };
|
|
94
|
+
}
|
|
95
|
+
const list = exps
|
|
96
|
+
.map((e) => {
|
|
97
|
+
const m = e.manifest || {};
|
|
98
|
+
return `- ${e.id} | "${m.title || e.name}" | ${m.description || "(no description)"} | v${m.version || "0.0.0"}`;
|
|
99
|
+
})
|
|
100
|
+
.join("\n");
|
|
101
|
+
return { content: [{ type: "text", text: `Found ${exps.length} experience(s):\n\n${list}` }] };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return { content: [{ type: "text", text: `Failed to list experiences: ${err.message}` }], isError: true };
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
server.tool("vibevibes_get_experience_source", "Get the full source code of a specific experience. Use this to understand an experience before modifying it.", {
|
|
108
|
+
experienceId: z.string().describe("ID of the experience (e.g. 'my-app' or 'username/repo')"),
|
|
109
|
+
}, async ({ experienceId }) => {
|
|
110
|
+
try {
|
|
111
|
+
const exp = await getExperienceSource(experienceId);
|
|
112
|
+
const sizeKB = ((exp.source_code?.length || 0) / 1024).toFixed(1);
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: exp.source_code + `\n\n--- Size: ${sizeKB}KB ---`,
|
|
117
|
+
}],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return { content: [{ type: "text", text: `Failed to get source: ${err.message}` }], isError: true };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.tool("vibevibes_get_experience_tools", "List all tools exported by a specific experience. Each tool has a name, description, input schema, and risk level.", {
|
|
125
|
+
experienceId: z.string().describe("ID of the experience"),
|
|
126
|
+
}, async ({ experienceId }) => {
|
|
127
|
+
try {
|
|
128
|
+
const exp = await getExperienceFull(experienceId);
|
|
129
|
+
if (!exp.server_bundled_code) {
|
|
130
|
+
return { content: [{ type: "text", text: "Experience has no server bundle. Re-save to generate it." }], isError: true };
|
|
131
|
+
}
|
|
132
|
+
const tools = getExperienceTools(exp.server_bundled_code);
|
|
133
|
+
if (tools.length === 0) {
|
|
134
|
+
return { content: [{ type: "text", text: "No tools found for this experience." }] };
|
|
135
|
+
}
|
|
136
|
+
const list = tools
|
|
137
|
+
.map((t) => {
|
|
138
|
+
const schema = t.input_schema ? JSON.stringify(t.input_schema) : "{}";
|
|
139
|
+
return `- ${t.name} (risk: ${t.risk})\n ${t.description}\n Input: ${schema}`;
|
|
140
|
+
})
|
|
141
|
+
.join("\n\n");
|
|
142
|
+
return { content: [{ type: "text", text: `${tools.length} tool(s):\n\n${list}` }] };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return { content: [{ type: "text", text: `Failed to get tools: ${err.message}` }], isError: true };
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
server.tool("vibevibes_save_experience", "Create or update an experience by saving source code. The source must be valid vibe-vibe experience code (using defineExperience, Canvas, and tools). The server will bundle and persist it automatically. When GITHUB_TOKEN is configured, the experience is committed to GitHub as the source of truth.", {
|
|
149
|
+
id: z.string().describe("Unique experience ID (e.g. 'my-app')"),
|
|
150
|
+
name: z.string().describe("Display name for the experience"),
|
|
151
|
+
sourceCode: z.string().describe("Complete TypeScript/React source code for the experience"),
|
|
152
|
+
}, async ({ id, name, sourceCode }) => {
|
|
153
|
+
try {
|
|
154
|
+
const res = await webPost("/api/experiences/save", {
|
|
155
|
+
id,
|
|
156
|
+
name,
|
|
157
|
+
source_code: sourceCode,
|
|
158
|
+
});
|
|
159
|
+
const manifest = res.manifest;
|
|
160
|
+
const warnings = res.warnings || [];
|
|
161
|
+
let text = `Experience saved successfully!\n\nManifest:\n${JSON.stringify(manifest, null, 2)}`;
|
|
162
|
+
if (warnings.length > 0) {
|
|
163
|
+
text += `\n\nValidation warnings:\n${warnings.map((w) => ` - ${w}`).join("\n")}`;
|
|
164
|
+
}
|
|
165
|
+
return { content: [{ type: "text", text }] };
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
let errorText = `Failed to save experience: ${err.message}`;
|
|
169
|
+
if (err.data?.errors?.length) {
|
|
170
|
+
errorText += `\n\nValidation errors:\n${err.data.errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
171
|
+
}
|
|
172
|
+
return { content: [{ type: "text", text: errorText }], isError: true };
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
server.tool("vibevibes_save_experience_files", "Create or update a multi-file experience. Accepts a map of file paths to source code content (e.g. { 'src/index.tsx': '...', 'src/components/Board.tsx': '...' }). The server bundles all files together, resolving relative imports between them, then validates and persists. Entry point must be src/index.tsx. Max total size: 2MB.", {
|
|
176
|
+
id: z.string().describe("Unique experience ID (e.g. 'my-app')"),
|
|
177
|
+
name: z.string().describe("Display name for the experience"),
|
|
178
|
+
files: z.record(z.string(), z.string()).describe("Map of file paths to source code content. Entry point: src/index.tsx. Example: { 'src/index.tsx': '...', 'src/components/Board.tsx': '...' }"),
|
|
179
|
+
}, async ({ id, name, files }) => {
|
|
180
|
+
try {
|
|
181
|
+
const fileCount = Object.keys(files).length;
|
|
182
|
+
const res = await webPost("/api/experiences/save", { id, name, files });
|
|
183
|
+
const manifest = res.manifest;
|
|
184
|
+
const warnings = res.warnings || [];
|
|
185
|
+
let text = `Multi-file experience saved successfully!\n\n${fileCount} files uploaded.\n\nManifest:\n${JSON.stringify(manifest, null, 2)}`;
|
|
186
|
+
if (warnings.length > 0) {
|
|
187
|
+
text += `\n\nValidation warnings:\n${warnings.map((w) => ` - ${w}`).join("\n")}`;
|
|
188
|
+
}
|
|
189
|
+
return { content: [{ type: "text", text }] };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
let errorText = `Failed to save multi-file experience: ${err.message}`;
|
|
193
|
+
if (err.data?.errors?.length) {
|
|
194
|
+
errorText += `\n\nValidation errors:\n${err.data.errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
195
|
+
}
|
|
196
|
+
return { content: [{ type: "text", text: errorText }], isError: true };
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
server.tool("vibevibes_get_experience_files", "Get all source files for a multi-file experience. Returns a map of file paths to content. Use this to understand the full codebase of an experience before editing individual files.", {
|
|
200
|
+
experienceId: z.string().describe("ID of the experience"),
|
|
201
|
+
}, async ({ experienceId }) => {
|
|
202
|
+
try {
|
|
203
|
+
const exp = await getExperienceFiles(experienceId);
|
|
204
|
+
const files = exp.source_files || {};
|
|
205
|
+
const paths = Object.keys(files);
|
|
206
|
+
if (paths.length === 0) {
|
|
207
|
+
return { content: [{ type: "text", text: "No source files found for this experience." }] };
|
|
208
|
+
}
|
|
209
|
+
const sections = paths.map((path) => {
|
|
210
|
+
const content = files[path];
|
|
211
|
+
const sizeLabel = ` (${(content.length / 1024).toFixed(1)}KB)`;
|
|
212
|
+
return `── ${path}${sizeLabel} ──\n${content}`;
|
|
213
|
+
});
|
|
214
|
+
const totalSize = paths.reduce((sum, p) => sum + files[p].length, 0);
|
|
215
|
+
const text = [
|
|
216
|
+
`Experience: ${exp.id}`,
|
|
217
|
+
`Name: ${exp.name}`,
|
|
218
|
+
`Files: ${paths.length}`,
|
|
219
|
+
`Total size: ${(totalSize / 1024).toFixed(1)}KB`,
|
|
220
|
+
``,
|
|
221
|
+
...sections,
|
|
222
|
+
].join("\n");
|
|
223
|
+
return { content: [{ type: "text", text }] };
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
return { content: [{ type: "text", text: `Failed to get files: ${err.message}` }], isError: true };
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
server.tool("vibevibes_edit_experience_file", "Update or delete specific files in a multi-file experience. Provide files to create/overwrite and/or file paths to delete. The server re-bundles the full experience after applying changes, validates it, and persists. Use this for incremental edits to large experiences — no need to re-upload unchanged files.", {
|
|
230
|
+
experienceId: z.string().describe("ID of the experience to edit"),
|
|
231
|
+
files: z.record(z.string(), z.string()).optional().describe("Map of file paths to new content (creates or overwrites). Example: { 'src/components/Board.tsx': '...' }"),
|
|
232
|
+
deleteFiles: z.array(z.string()).optional().describe("List of file paths to delete. Example: ['src/old-file.ts']"),
|
|
233
|
+
}, async ({ experienceId, files, deleteFiles }) => {
|
|
234
|
+
if (!files && !deleteFiles) {
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text: "Error: Must provide either 'files' (to update) or 'deleteFiles' (to remove)." }],
|
|
237
|
+
isError: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const exp = await getExperienceFiles(experienceId);
|
|
242
|
+
const currentFiles = exp.source_files || {};
|
|
243
|
+
const updatedFiles = { ...currentFiles };
|
|
244
|
+
if (files) {
|
|
245
|
+
Object.assign(updatedFiles, files);
|
|
246
|
+
}
|
|
247
|
+
if (deleteFiles) {
|
|
248
|
+
for (const f of deleteFiles) {
|
|
249
|
+
delete updatedFiles[f];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const res = await webPost("/api/experiences/save", {
|
|
253
|
+
id: experienceId,
|
|
254
|
+
name: exp.name,
|
|
255
|
+
files: updatedFiles,
|
|
256
|
+
});
|
|
257
|
+
const warnings = res.warnings || [];
|
|
258
|
+
const fileCount = Object.keys(updatedFiles).length;
|
|
259
|
+
let text = `Experience '${experienceId}' updated successfully!\n\nFiles: ${fileCount}`;
|
|
260
|
+
if (files)
|
|
261
|
+
text += `\nUpdated: ${Object.keys(files).join(", ")}`;
|
|
262
|
+
if (deleteFiles?.length)
|
|
263
|
+
text += `\nDeleted: ${deleteFiles.join(", ")}`;
|
|
264
|
+
if (warnings.length > 0) {
|
|
265
|
+
text += `\n\nValidation warnings:\n${warnings.map((w) => ` - ${w}`).join("\n")}`;
|
|
266
|
+
}
|
|
267
|
+
return { content: [{ type: "text", text }] };
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
let errorText = `Failed to edit experience files: ${err.message}`;
|
|
271
|
+
if (err.data?.errors?.length) {
|
|
272
|
+
errorText += `\n\nValidation errors:\n${err.data.errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
273
|
+
}
|
|
274
|
+
return { content: [{ type: "text", text: errorText }], isError: true };
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
server.tool("vibevibes_create_room", "Create a new room for a given experience. Returns the room ID and a browser URL to open the room.", {
|
|
278
|
+
experienceId: z.string().describe("ID of the experience to load in the room"),
|
|
279
|
+
}, async ({ experienceId }) => {
|
|
280
|
+
try {
|
|
281
|
+
const room = await createRoom(experienceId);
|
|
282
|
+
const url = `${WEB_URL}/room/${room.id}`;
|
|
283
|
+
return {
|
|
284
|
+
content: [{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: `Room created!\n\nRoom ID: ${room.id}\nExperience: ${experienceId}\nOpen in browser: ${url}`,
|
|
287
|
+
}],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return { content: [{ type: "text", text: `Failed to create room: ${err.message}` }], isError: true };
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
server.tool("vibevibes_join_room", "Join a room as a participant. The human in the browser will see you appear in the participants list. Use watch_room to long-poll for their actions. Your join stays alive as long as you keep calling watch_room (60s timeout otherwise).", {
|
|
295
|
+
roomId: z.string().describe("The room ID to join"),
|
|
296
|
+
actorId: z.string().optional().default("claude-code").describe("Your actor ID (deprecated, server assigns IDs now)"),
|
|
297
|
+
role: z.string().optional().describe("Your role in this room (e.g. 'assistant', 'moderator', 'player')"),
|
|
298
|
+
allowedTools: z.array(z.string()).optional().describe("If set, restricts this agent to only calling these tools. Other tools will be rejected."),
|
|
299
|
+
}, async ({ roomId, role, allowedTools }) => {
|
|
300
|
+
try {
|
|
301
|
+
const username = GITHUB_OWNER || "claude";
|
|
302
|
+
const agentMetadata = {
|
|
303
|
+
model: "claude",
|
|
304
|
+
role: role || "assistant",
|
|
305
|
+
provider: "anthropic",
|
|
306
|
+
};
|
|
307
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
308
|
+
agentMetadata.allowedTools = allowedTools;
|
|
309
|
+
}
|
|
310
|
+
const d = await joinRoom(roomId, username, role || "assistant", agentMetadata);
|
|
311
|
+
const toolsList = (d.tools || [])
|
|
312
|
+
.map((t) => ` - ${t.name} (${t.risk}): ${t.description}`)
|
|
313
|
+
.join("\n");
|
|
314
|
+
const stateStr = JSON.stringify(d.sharedState, null, 2);
|
|
315
|
+
const stateSummary = stateStr.length > 500 ? stateStr.slice(0, 500) + "\n ..." : stateStr;
|
|
316
|
+
return {
|
|
317
|
+
content: [{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: [
|
|
320
|
+
`Joined room ${roomId} as "${d.actorId}"`,
|
|
321
|
+
``,
|
|
322
|
+
`Experience: ${d.experienceId || "(none)"}`,
|
|
323
|
+
`Participants: ${(d.participants || []).join(", ")}`,
|
|
324
|
+
`Browser URL: ${d.browserUrl}`,
|
|
325
|
+
``,
|
|
326
|
+
`Available tools:`,
|
|
327
|
+
toolsList || " (none)",
|
|
328
|
+
``,
|
|
329
|
+
`Current state:`,
|
|
330
|
+
stateSummary,
|
|
331
|
+
``,
|
|
332
|
+
`Recent events: ${(d.events || []).length}`,
|
|
333
|
+
``,
|
|
334
|
+
`Use vibevibes_watch_room to long-poll for human activity.`,
|
|
335
|
+
`Use vibevibes_execute_tool to take actions.`,
|
|
336
|
+
`Use vibevibes_leave_room when done.`,
|
|
337
|
+
].join("\n"),
|
|
338
|
+
}],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
return { content: [{ type: "text", text: `Failed to join room: ${err.message}` }], isError: true };
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
server.tool("vibevibes_watch_room", "Long-poll for new activity in a joined room. Blocks until events arrive or timeout expires. Returns events from other participants (human actions) since your last check, plus current state. Also acts as a heartbeat to keep your join alive.", {
|
|
346
|
+
roomId: z.string().describe("The room ID to watch"),
|
|
347
|
+
timeout: z.number().optional().default(30000).describe("Max time to wait in ms (default 30000, max 55000). The request blocks until events arrive or this timeout expires."),
|
|
348
|
+
}, async ({ roomId, timeout }) => {
|
|
349
|
+
if (!isJoined(roomId)) {
|
|
350
|
+
return {
|
|
351
|
+
content: [{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: `Not joined to room ${roomId}. Call vibevibes_join_room first.`,
|
|
354
|
+
}],
|
|
355
|
+
isError: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const clampedTimeout = Math.min(Math.max(timeout, 0), 55000);
|
|
360
|
+
const d = await watchRoom(roomId, clampedTimeout);
|
|
361
|
+
if (d.events.length === 0) {
|
|
362
|
+
return {
|
|
363
|
+
content: [{
|
|
364
|
+
type: "text",
|
|
365
|
+
text: [
|
|
366
|
+
`No new activity in room ${roomId} (waited ${clampedTimeout}ms).`,
|
|
367
|
+
`Participants: ${d.participants.join(", ")}`,
|
|
368
|
+
].join("\n"),
|
|
369
|
+
}],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const eventLines = d.events
|
|
373
|
+
.map((e) => {
|
|
374
|
+
const time = new Date(e.ts).toISOString();
|
|
375
|
+
const inputStr = JSON.stringify(e.input);
|
|
376
|
+
const result = e.error ? `ERROR: ${e.error}` : JSON.stringify(e.output);
|
|
377
|
+
return `[${time}] ${e.actorId} called ${e.tool}(${inputStr}) => ${result}`;
|
|
378
|
+
})
|
|
379
|
+
.join("\n");
|
|
380
|
+
const stateStr = JSON.stringify(d.sharedState, null, 2);
|
|
381
|
+
const stateSummary = stateStr.length > 1000 ? stateStr.slice(0, 1000) + "\n..." : stateStr;
|
|
382
|
+
return {
|
|
383
|
+
content: [{
|
|
384
|
+
type: "text",
|
|
385
|
+
text: [
|
|
386
|
+
`${d.events.length} new event(s) in room ${roomId}:`,
|
|
387
|
+
``,
|
|
388
|
+
eventLines,
|
|
389
|
+
``,
|
|
390
|
+
`Current state:`,
|
|
391
|
+
stateSummary,
|
|
392
|
+
``,
|
|
393
|
+
`Participants: ${d.participants.join(", ")}`,
|
|
394
|
+
].join("\n"),
|
|
395
|
+
}],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
return { content: [{ type: "text", text: `Watch failed: ${err.message}` }], isError: true };
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
server.tool("vibevibes_leave_room", "Leave a room you previously joined. Removes you from the participants list.", {
|
|
403
|
+
roomId: z.string().describe("The room ID to leave"),
|
|
404
|
+
}, async ({ roomId }) => {
|
|
405
|
+
try {
|
|
406
|
+
const actorId = await leaveRoom(roomId);
|
|
407
|
+
return {
|
|
408
|
+
content: [{
|
|
409
|
+
type: "text",
|
|
410
|
+
text: `Left room ${roomId}. Removed "${actorId}" from participants.`,
|
|
411
|
+
}],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
return { content: [{ type: "text", text: `Leave failed: ${err.message}` }], isError: true };
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
server.tool("vibevibes_get_room_state", "Inspect the current shared state and participants of a room. Useful for debugging room state without opening a browser.", {
|
|
419
|
+
roomId: z.string().describe("The room ID to inspect"),
|
|
420
|
+
}, async ({ roomId }) => {
|
|
421
|
+
try {
|
|
422
|
+
const room = await getRoomMetadata(roomId);
|
|
423
|
+
const state = await getRoomState(roomId);
|
|
424
|
+
const session = getJoinedSession(roomId);
|
|
425
|
+
const participants = session ? session.participants : [];
|
|
426
|
+
const participantNote = session ? "" : " (join room for live participant tracking)";
|
|
427
|
+
return {
|
|
428
|
+
content: [{
|
|
429
|
+
type: "text",
|
|
430
|
+
text: [
|
|
431
|
+
`Room: ${room.id}`,
|
|
432
|
+
`Experience: ${room.experience_id || "(none)"}`,
|
|
433
|
+
`Participants: ${participants.length}${participantNote} — ${participants.join(", ") || "(none)"}`,
|
|
434
|
+
``,
|
|
435
|
+
`Shared State:`,
|
|
436
|
+
JSON.stringify(state, null, 2),
|
|
437
|
+
].join("\n"),
|
|
438
|
+
}],
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
return { content: [{ type: "text", text: `Failed to get room: ${err.message}` }], isError: true };
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
server.tool("vibevibes_room_screenshot", "Capture a screenshot of a room as it appears in the browser. Uses a headless browser to render the room and returns a PNG image. Use this to see the current visual state of an experience.", {
|
|
446
|
+
roomId: z.string().describe("The room ID to screenshot"),
|
|
447
|
+
width: z.number().optional().default(1280).describe("Viewport width in pixels"),
|
|
448
|
+
height: z.number().optional().default(800).describe("Viewport height in pixels"),
|
|
449
|
+
}, async () => {
|
|
450
|
+
return {
|
|
451
|
+
content: [{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: "Screenshots are not available in the serverless architecture. Use vibevibes_get_room_state to inspect room state, or open the room URL in a browser.",
|
|
454
|
+
}],
|
|
455
|
+
isError: true,
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
server.tool("vibevibes_execute_tool", "Execute a tool in a room. This calls the tool gate on the realtime server, which validates input, runs the handler, mutates shared state, and broadcasts updates to all connected clients.", {
|
|
459
|
+
roomId: z.string().describe("The room ID"),
|
|
460
|
+
toolName: z.string().describe("Fully-qualified tool name (e.g. 'counter.increment')"),
|
|
461
|
+
input: z.record(z.any()).describe("Tool input parameters as a JSON object"),
|
|
462
|
+
actorId: z.string().optional().default("mcp-client").describe("Actor ID for this tool call (deprecated, uses session ID from join)"),
|
|
463
|
+
owner: z.string().optional().describe("Who this action is on behalf of (e.g. a GitHub username)"),
|
|
464
|
+
}, async ({ roomId, toolName, input, actorId, owner }) => {
|
|
465
|
+
try {
|
|
466
|
+
const session = getJoinedSession(roomId);
|
|
467
|
+
const resolvedActorId = session?.actorId || actorId;
|
|
468
|
+
const result = await executeToolGate(roomId, toolName, input, resolvedActorId, owner);
|
|
469
|
+
return {
|
|
470
|
+
content: [{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: `Tool '${toolName}' executed successfully.\n\nOutput:\n${JSON.stringify(result.output, null, 2)}`,
|
|
473
|
+
}],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
return { content: [{ type: "text", text: `Tool call failed: ${err.message}` }], isError: true };
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
server.tool("vibevibes_edit_experience", "Create or modify an experience and assign it to a room. Supports full source code replacement or unified diff patching. This bundles, saves, invalidates cache, and hot-reloads the experience in the room.", {
|
|
481
|
+
roomId: z.string().describe("The room ID to update"),
|
|
482
|
+
sourceCode: z.string().optional().describe("Complete source code (for creating or full rewrite)"),
|
|
483
|
+
diff: z.string().optional().describe("Unified diff to apply to existing experience code (for incremental edits)"),
|
|
484
|
+
id: z.string().optional().describe("Experience ID (required when using diff mode to identify which experience to patch)"),
|
|
485
|
+
actorId: z.string().optional().default("mcp-client").describe("Actor ID (deprecated, uses session ID from join)"),
|
|
486
|
+
}, async ({ roomId, sourceCode, diff, id }) => {
|
|
487
|
+
if (!sourceCode && !diff) {
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: "Error: Must provide either 'sourceCode' or 'diff'." }],
|
|
490
|
+
isError: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const room = await getRoomMetadata(roomId);
|
|
495
|
+
const experienceId = id || room.experience_id;
|
|
496
|
+
let finalSource = sourceCode;
|
|
497
|
+
if (diff && !sourceCode) {
|
|
498
|
+
const exp = await getExperienceSource(experienceId);
|
|
499
|
+
finalSource = applyUnifiedDiff(exp.source_code, diff);
|
|
500
|
+
}
|
|
501
|
+
if (!finalSource) {
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: "Error: Could not determine source code." }],
|
|
504
|
+
isError: true,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const res = await webPost("/api/experiences/save", {
|
|
508
|
+
id: experienceId,
|
|
509
|
+
name: experienceId,
|
|
510
|
+
source_code: finalSource,
|
|
511
|
+
});
|
|
512
|
+
try {
|
|
513
|
+
const broadcastChannel = supabase.channel(`room:${roomId}`);
|
|
514
|
+
await new Promise((resolve) => {
|
|
515
|
+
broadcastChannel.subscribe((status) => {
|
|
516
|
+
if (status === "SUBSCRIBED") {
|
|
517
|
+
broadcastChannel.send({
|
|
518
|
+
type: "broadcast",
|
|
519
|
+
event: "experience_updated",
|
|
520
|
+
payload: { experienceId },
|
|
521
|
+
}).then(() => {
|
|
522
|
+
supabase.removeChannel(broadcastChannel);
|
|
523
|
+
resolve();
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
setTimeout(() => {
|
|
528
|
+
supabase.removeChannel(broadcastChannel);
|
|
529
|
+
resolve();
|
|
530
|
+
}, 3000);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
}
|
|
535
|
+
const warnings = res.warnings || [];
|
|
536
|
+
let text = `Experience updated and hot-reloaded!\n\nExperience ID: ${experienceId}\nOpen in browser: ${WEB_URL}/room/${roomId}`;
|
|
537
|
+
if (warnings.length > 0) {
|
|
538
|
+
text += `\n\nValidation warnings:\n${warnings.map((w) => ` - ${w}`).join("\n")}`;
|
|
539
|
+
}
|
|
540
|
+
return { content: [{ type: "text", text }] };
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
let errorText = `Edit failed: ${err.message}`;
|
|
544
|
+
if (err.data?.errors?.length) {
|
|
545
|
+
errorText += `\n\nValidation errors:\n${err.data.errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
546
|
+
}
|
|
547
|
+
return { content: [{ type: "text", text: errorText }], isError: true };
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
server.tool("vibevibes_sync_from_github", "Force re-sync an experience from its GitHub source. Use this after pushing changes directly to GitHub (outside of vibe-vibe). Re-fetches source code, re-bundles, and updates the cache.", {
|
|
551
|
+
experienceId: z.string().describe("ID of the experience to sync (e.g. 'my-app')"),
|
|
552
|
+
}, async ({ experienceId }) => {
|
|
553
|
+
try {
|
|
554
|
+
await webPost("/api/experiences/sync", { experienceId });
|
|
555
|
+
return {
|
|
556
|
+
content: [{ type: "text", text: `Experience '${experienceId}' synced from GitHub successfully.` }],
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
return { content: [{ type: "text", text: `Sync failed: ${err.message}` }], isError: true };
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
server.tool("vibevibes_run_tests", "Run the inline tests defined in an experience. Tests verify tool handlers by calling them with mock ToolCtx and checking results. Tests throw to fail; if they resolve, they pass. Returns pass/fail counts and error details.", {
|
|
564
|
+
experienceId: z.string().describe("ID of the experience to test"),
|
|
565
|
+
}, async ({ experienceId }) => {
|
|
566
|
+
try {
|
|
567
|
+
const exp = await getExperienceFull(experienceId);
|
|
568
|
+
if (!exp.server_bundled_code) {
|
|
569
|
+
return {
|
|
570
|
+
content: [{ type: "text", text: `Experience '${experienceId}' has no server bundle. Re-save to generate it.` }],
|
|
571
|
+
isError: true,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const d = await runExperienceTests(exp.server_bundled_code);
|
|
575
|
+
if (d.total === 0) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: `No tests defined for experience '${experienceId}'.\n\nTo add tests, include a \`tests\` array in your defineExperience() call using defineTest().`,
|
|
580
|
+
}],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const resultLines = d.results
|
|
584
|
+
.map((r) => {
|
|
585
|
+
const icon = r.passed ? "PASS" : "FAIL";
|
|
586
|
+
const detail = r.error ? ` -- ${r.error}` : "";
|
|
587
|
+
return ` ${icon} ${r.name} (${r.durationMs}ms)${detail}`;
|
|
588
|
+
})
|
|
589
|
+
.join("\n");
|
|
590
|
+
const summary = `${d.passed}/${d.total} passed${d.failed > 0 ? ` (${d.failed} failed)` : ""} in ${d.durationMs}ms`;
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: "text",
|
|
594
|
+
text: `Test results for '${experienceId}':\n\n${resultLines}\n\n${summary}`,
|
|
595
|
+
}],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
return { content: [{ type: "text", text: `Test execution failed: ${err.message}` }], isError: true };
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
server.tool("vibevibes_get_event_history", "Get persisted event history for a room. Returns paginated tool call events from the database. Use this to see what happened in a room over time, even across server restarts.", {
|
|
603
|
+
roomId: z.string().describe("The room ID to get history for"),
|
|
604
|
+
limit: z.number().optional().default(50).describe("Number of events to return (max 200)"),
|
|
605
|
+
since: z.number().optional().default(0).describe("Only return events after this timestamp (epoch ms)"),
|
|
606
|
+
offset: z.number().optional().default(0).describe("Pagination offset"),
|
|
607
|
+
}, async ({ roomId, limit, since, offset }) => {
|
|
608
|
+
try {
|
|
609
|
+
const d = await getEventHistory(roomId, { limit, since, offset });
|
|
610
|
+
const events = d.events;
|
|
611
|
+
if (events.length === 0) {
|
|
612
|
+
return {
|
|
613
|
+
content: [{
|
|
614
|
+
type: "text",
|
|
615
|
+
text: `No events found for room ${roomId}${since > 0 ? ` since ${new Date(since).toISOString()}` : ""}.`,
|
|
616
|
+
}],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const eventLines = events
|
|
620
|
+
.map((e) => {
|
|
621
|
+
const time = new Date(e.ts).toISOString();
|
|
622
|
+
const inputStr = JSON.stringify(e.input);
|
|
623
|
+
const result = e.error ? `ERROR: ${e.error}` : JSON.stringify(e.output);
|
|
624
|
+
return `[${time}] ${e.actor_id} called ${e.tool}(${inputStr}) => ${result}`;
|
|
625
|
+
})
|
|
626
|
+
.join("\n");
|
|
627
|
+
return {
|
|
628
|
+
content: [{
|
|
629
|
+
type: "text",
|
|
630
|
+
text: [
|
|
631
|
+
`Event history for room ${roomId}: ${events.length} event(s)${d.hasMore ? " (more available)" : ""}`,
|
|
632
|
+
``,
|
|
633
|
+
eventLines,
|
|
634
|
+
``,
|
|
635
|
+
d.hasMore ? `Use offset=${offset + events.length} to get the next page.` : `End of history.`,
|
|
636
|
+
].join("\n"),
|
|
637
|
+
}],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
return { content: [{ type: "text", text: `Failed to get event history: ${err.message}` }], isError: true };
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
server.tool("vibevibes_get_agent_hints", "Get declarative agent hints for an experience. These hints tell agents when to act, what tools to use, and under what conditions. Experience authors define these to guide agent behavior.", {
|
|
645
|
+
experienceId: z.string().describe("ID of the experience to get hints for"),
|
|
646
|
+
}, async ({ experienceId }) => {
|
|
647
|
+
try {
|
|
648
|
+
const exp = await getExperienceFull(experienceId);
|
|
649
|
+
if (!exp.server_bundled_code) {
|
|
650
|
+
return {
|
|
651
|
+
content: [{ type: "text", text: `Experience '${experienceId}' has no server bundle.` }],
|
|
652
|
+
isError: true,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const { hints, tools } = getExperienceAgentHints(exp.server_bundled_code);
|
|
656
|
+
if (hints.length === 0) {
|
|
657
|
+
const toolsList = tools.map((t) => ` - ${t.name}: ${t.description}`).join("\n");
|
|
658
|
+
return {
|
|
659
|
+
content: [{
|
|
660
|
+
type: "text",
|
|
661
|
+
text: [
|
|
662
|
+
`No agent hints defined for experience '${experienceId}'.`,
|
|
663
|
+
``,
|
|
664
|
+
`Available tools:`,
|
|
665
|
+
toolsList || " (none)",
|
|
666
|
+
``,
|
|
667
|
+
`The experience author has not specified when agents should act.`,
|
|
668
|
+
`Use your judgment based on the available tools and room state.`,
|
|
669
|
+
].join("\n"),
|
|
670
|
+
}],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const hintLines = hints
|
|
674
|
+
.map((h, i) => {
|
|
675
|
+
const parts = [
|
|
676
|
+
`${i + 1}. Trigger: ${h.trigger}`,
|
|
677
|
+
h.condition ? ` Condition: ${h.condition}` : null,
|
|
678
|
+
` Suggested tools: ${h.suggestedTools.join(", ")}`,
|
|
679
|
+
h.priority ? ` Priority: ${h.priority}` : null,
|
|
680
|
+
h.cooldownMs ? ` Cooldown: ${h.cooldownMs}ms` : null,
|
|
681
|
+
].filter(Boolean);
|
|
682
|
+
return parts.join("\n");
|
|
683
|
+
})
|
|
684
|
+
.join("\n\n");
|
|
685
|
+
const toolsList = tools.map((t) => ` - ${t.name}: ${t.description}`).join("\n");
|
|
686
|
+
return {
|
|
687
|
+
content: [{
|
|
688
|
+
type: "text",
|
|
689
|
+
text: [
|
|
690
|
+
`Agent hints for '${experienceId}' (${hints.length} hint(s)):`,
|
|
691
|
+
``,
|
|
692
|
+
hintLines,
|
|
693
|
+
``,
|
|
694
|
+
`Available tools:`,
|
|
695
|
+
toolsList || " (none)",
|
|
696
|
+
].join("\n"),
|
|
697
|
+
}],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
return { content: [{ type: "text", text: `Failed to get agent hints: ${err.message}` }], isError: true };
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
server.tool("vibevibes_discover_agents", "Discover other AI agents in a room. Returns agent actor IDs, roles, models, and tool restrictions. Use this to coordinate with other agents.", {
|
|
705
|
+
roomId: z.string().describe("The room ID to discover agents in"),
|
|
706
|
+
}, async ({ roomId }) => {
|
|
707
|
+
try {
|
|
708
|
+
const session = getJoinedSession(roomId);
|
|
709
|
+
if (!session) {
|
|
710
|
+
return {
|
|
711
|
+
content: [{
|
|
712
|
+
type: "text",
|
|
713
|
+
text: `Not joined to room ${roomId}. Join first to discover agents via Presence.`,
|
|
714
|
+
}],
|
|
715
|
+
isError: true,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const presenceState = session.channel.presenceState();
|
|
719
|
+
const agents = [];
|
|
720
|
+
for (const [key, presences] of Object.entries(presenceState)) {
|
|
721
|
+
const presence = presences[0];
|
|
722
|
+
if (presence?.actorType === "ai") {
|
|
723
|
+
agents.push({
|
|
724
|
+
actorId: key,
|
|
725
|
+
model: presence.model,
|
|
726
|
+
role: presence.role,
|
|
727
|
+
provider: presence.provider,
|
|
728
|
+
allowedTools: presence.allowedTools,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (agents.length === 0) {
|
|
733
|
+
return {
|
|
734
|
+
content: [{
|
|
735
|
+
type: "text",
|
|
736
|
+
text: `No AI agents currently in room ${roomId}.`,
|
|
737
|
+
}],
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const agentLines = agents
|
|
741
|
+
.map((a) => {
|
|
742
|
+
const parts = [
|
|
743
|
+
`- ${a.actorId}`,
|
|
744
|
+
a.model ? ` Model: ${a.model}` : null,
|
|
745
|
+
a.role ? ` Role: ${a.role}` : null,
|
|
746
|
+
a.provider ? ` Provider: ${a.provider}` : null,
|
|
747
|
+
a.allowedTools ? ` Allowed tools: ${a.allowedTools.join(", ")}` : ` Allowed tools: all`,
|
|
748
|
+
].filter(Boolean);
|
|
749
|
+
return parts.join("\n");
|
|
750
|
+
})
|
|
751
|
+
.join("\n\n");
|
|
752
|
+
return {
|
|
753
|
+
content: [{
|
|
754
|
+
type: "text",
|
|
755
|
+
text: [
|
|
756
|
+
`${agents.length} AI agent(s) in room ${roomId}:`,
|
|
757
|
+
``,
|
|
758
|
+
agentLines,
|
|
759
|
+
].join("\n"),
|
|
760
|
+
}],
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
return { content: [{ type: "text", text: `Failed to discover agents: ${err.message}` }], isError: true };
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
function applyUnifiedDiff(original, diff) {
|
|
768
|
+
const lines = original.split("\n");
|
|
769
|
+
const diffLines = diff.split("\n");
|
|
770
|
+
let outputLines = [...lines];
|
|
771
|
+
let offset = 0;
|
|
772
|
+
for (let i = 0; i < diffLines.length; i++) {
|
|
773
|
+
const hunkMatch = diffLines[i].match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
774
|
+
if (!hunkMatch)
|
|
775
|
+
continue;
|
|
776
|
+
const origStart = parseInt(hunkMatch[1]) - 1;
|
|
777
|
+
const removals = [];
|
|
778
|
+
const additions = [];
|
|
779
|
+
let j = i + 1;
|
|
780
|
+
let lineIdx = origStart;
|
|
781
|
+
while (j < diffLines.length && !diffLines[j].startsWith("@@") && !diffLines[j].startsWith("diff ")) {
|
|
782
|
+
const line = diffLines[j];
|
|
783
|
+
if (line.startsWith("-")) {
|
|
784
|
+
removals.push(lineIdx + offset);
|
|
785
|
+
lineIdx++;
|
|
786
|
+
}
|
|
787
|
+
else if (line.startsWith("+")) {
|
|
788
|
+
additions.push(line.slice(1));
|
|
789
|
+
}
|
|
790
|
+
else if (line.startsWith(" ")) {
|
|
791
|
+
lineIdx++;
|
|
792
|
+
}
|
|
793
|
+
j++;
|
|
794
|
+
}
|
|
795
|
+
for (const idx of removals.reverse()) {
|
|
796
|
+
outputLines.splice(idx, 1);
|
|
797
|
+
offset--;
|
|
798
|
+
}
|
|
799
|
+
const insertAt = origStart + offset + removals.length;
|
|
800
|
+
outputLines.splice(insertAt, 0, ...additions);
|
|
801
|
+
offset += additions.length;
|
|
802
|
+
}
|
|
803
|
+
return outputLines.join("\n");
|
|
804
|
+
}
|
|
805
|
+
async function main() {
|
|
806
|
+
await initGitHubAuth();
|
|
807
|
+
const transport = new StdioServerTransport();
|
|
808
|
+
await server.connect(transport);
|
|
809
|
+
console.error("[vibevibes-mcp] Server running on stdio (Supabase-backed)");
|
|
810
|
+
console.error(`[vibevibes-mcp] Supabase: ${SUPABASE_URL || "(NOT SET)"}`);
|
|
811
|
+
console.error(`[vibevibes-mcp] Web: ${WEB_URL}`);
|
|
812
|
+
console.error(`[vibevibes-mcp] GitHub: ${GITHUB_TOKEN ? GITHUB_OWNER : "NOT CONFIGURED (set GITHUB_TOKEN)"}`);
|
|
813
|
+
}
|
|
814
|
+
main().catch((error) => {
|
|
815
|
+
console.error("[vibevibes-mcp] Fatal error:", error);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
});
|
|
818
|
+
//# sourceMappingURL=index.js.map
|