@vibevibes/mcp 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/bin/cli.js +11 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +319 -0
- package/package.json +33 -0
package/bin/cli.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibevibes-mcp — standalone MCP server for joining vibevibes experiences.
|
|
3
|
+
*
|
|
4
|
+
* Works with both local dev servers and remote shared tunnels.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx vibevibes-mcp # defaults to http://localhost:4321
|
|
8
|
+
* npx vibevibes-mcp https://xyz.trycloudflare.com # join a shared room
|
|
9
|
+
* VIBEVIBES_SERVER_URL=https://... npx vibevibes-mcp
|
|
10
|
+
*
|
|
11
|
+
* 5 tools: connect, watch, act, memory, screenshot
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibevibes-mcp — standalone MCP server for joining vibevibes experiences.
|
|
3
|
+
*
|
|
4
|
+
* Works with both local dev servers and remote shared tunnels.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx vibevibes-mcp # defaults to http://localhost:4321
|
|
8
|
+
* npx vibevibes-mcp https://xyz.trycloudflare.com # join a shared room
|
|
9
|
+
* VIBEVIBES_SERVER_URL=https://... npx vibevibes-mcp
|
|
10
|
+
*
|
|
11
|
+
* 5 tools: connect, watch, act, memory, screenshot
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
// Resolve server URL: CLI arg > env var > localhost default
|
|
17
|
+
const RAW_SERVER_URL = process.argv[2] ||
|
|
18
|
+
process.env.VIBEVIBES_SERVER_URL ||
|
|
19
|
+
"http://localhost:4321";
|
|
20
|
+
// Extract room token from the URL query param (e.g. https://xyz.trycloudflare.com?token=abc123)
|
|
21
|
+
const parsedUrl = new URL(RAW_SERVER_URL);
|
|
22
|
+
const ROOM_TOKEN = parsedUrl.searchParams.get("token");
|
|
23
|
+
// Strip the token param from the base URL so it isn't duplicated in request paths
|
|
24
|
+
parsedUrl.searchParams.delete("token");
|
|
25
|
+
const SERVER_URL = parsedUrl.toString().replace(/\/$/, ""); // remove trailing slash
|
|
26
|
+
// ── State ──────────────────────────────────────────────────
|
|
27
|
+
let currentActorId = null;
|
|
28
|
+
let lastEventTs = 0;
|
|
29
|
+
let connected = false;
|
|
30
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
31
|
+
async function fetchJSON(path, opts) {
|
|
32
|
+
const headers = {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
...(opts?.headers || {}),
|
|
35
|
+
};
|
|
36
|
+
// Attach room token as Authorization header when available
|
|
37
|
+
if (ROOM_TOKEN) {
|
|
38
|
+
headers["Authorization"] = `Bearer ${ROOM_TOKEN}`;
|
|
39
|
+
}
|
|
40
|
+
const res = await fetch(`${SERVER_URL}${path}`, {
|
|
41
|
+
...opts,
|
|
42
|
+
headers,
|
|
43
|
+
});
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error(`Server returned non-JSON: ${text.slice(0, 200)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function formatToolList(tools) {
|
|
53
|
+
if (!tools?.length)
|
|
54
|
+
return "No tools available.";
|
|
55
|
+
return tools
|
|
56
|
+
.map((t) => {
|
|
57
|
+
const schema = t.input_schema?.properties
|
|
58
|
+
? Object.entries(t.input_schema.properties)
|
|
59
|
+
.map(([k, v]) => `${k}: ${v.type || "any"}`)
|
|
60
|
+
.join(", ")
|
|
61
|
+
: "{}";
|
|
62
|
+
return ` ${t.name} (${t.risk || "low"}) — ${t.description}\n input: { ${schema} }`;
|
|
63
|
+
})
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|
|
66
|
+
async function joinRoom() {
|
|
67
|
+
const join = await fetchJSON("/join", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: JSON.stringify({ username: "claude", actorType: "ai" }),
|
|
70
|
+
});
|
|
71
|
+
if (join.error)
|
|
72
|
+
throw new Error(join.error);
|
|
73
|
+
currentActorId = join.actorId;
|
|
74
|
+
lastEventTs = Date.now();
|
|
75
|
+
connected = true;
|
|
76
|
+
return join;
|
|
77
|
+
}
|
|
78
|
+
async function ensureConnected() {
|
|
79
|
+
if (connected)
|
|
80
|
+
return;
|
|
81
|
+
await joinRoom();
|
|
82
|
+
}
|
|
83
|
+
// ── MCP Server ─────────────────────────────────────────────
|
|
84
|
+
const server = new McpServer({
|
|
85
|
+
name: "vibevibes",
|
|
86
|
+
version: "0.1.0",
|
|
87
|
+
});
|
|
88
|
+
// ── Tool: connect ──────────────────────────────────────────
|
|
89
|
+
server.tool("connect", `Connect to the running experience.
|
|
90
|
+
|
|
91
|
+
Returns: available tools, current state, participants, and the browser URL.
|
|
92
|
+
|
|
93
|
+
Call this first before using watch or act.`, {}, async () => {
|
|
94
|
+
try {
|
|
95
|
+
const join = await joinRoom();
|
|
96
|
+
const output = [
|
|
97
|
+
`Connected as ${currentActorId}`,
|
|
98
|
+
`Experience: ${join.experienceId}`,
|
|
99
|
+
`Browser: ${join.browserUrl}`,
|
|
100
|
+
`Server: ${SERVER_URL}`,
|
|
101
|
+
``,
|
|
102
|
+
`State: ${JSON.stringify(join.sharedState, null, 2)}`,
|
|
103
|
+
`Participants: ${join.participants?.join(", ")}`,
|
|
104
|
+
``,
|
|
105
|
+
`Tools:`,
|
|
106
|
+
formatToolList(join.tools),
|
|
107
|
+
].join("\n");
|
|
108
|
+
return { content: [{ type: "text", text: output }] };
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: `Failed to connect to ${SERVER_URL}.\n\nIs the dev server running? (npm run dev)\n\nError: ${err.message}`,
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// ── Tool: watch ────────────────────────────────────────────
|
|
120
|
+
server.tool("watch", `Wait for activity in the experience. Blocks until events arrive or timeout.
|
|
121
|
+
|
|
122
|
+
Use predicate to wait for a condition, e.g. "state.count > 5".
|
|
123
|
+
Use filterTools to only wake for specific tools, e.g. ["pixel.place"].
|
|
124
|
+
Use filterActors to only wake for specific actors.
|
|
125
|
+
|
|
126
|
+
Auto-connects if not already connected.`, {
|
|
127
|
+
timeout: z.number().optional().describe("Max wait ms (default 30000, max 55000)"),
|
|
128
|
+
predicate: z.string().optional().describe('JS expression, e.g. "state.count > 5"'),
|
|
129
|
+
filterTools: z.array(z.string()).optional().describe("Only wake for these tools"),
|
|
130
|
+
filterActors: z.array(z.string()).optional().describe("Only wake for these actors"),
|
|
131
|
+
}, async ({ timeout, predicate, filterTools, filterActors }) => {
|
|
132
|
+
try {
|
|
133
|
+
await ensureConnected();
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return { content: [{ type: "text", text: `Not connected: ${err.message}` }] };
|
|
137
|
+
}
|
|
138
|
+
const t = Math.min(timeout || 30000, 55000);
|
|
139
|
+
// Check if predicate already matches
|
|
140
|
+
if (predicate) {
|
|
141
|
+
try {
|
|
142
|
+
const current = await fetchJSON("/state");
|
|
143
|
+
const fn = new Function("state", "actorId", `return ${predicate}`);
|
|
144
|
+
if (fn(current.sharedState, currentActorId)) {
|
|
145
|
+
return {
|
|
146
|
+
content: [{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: [
|
|
149
|
+
`Predicate already true: ${predicate}`,
|
|
150
|
+
`State: ${JSON.stringify(current.sharedState, null, 2)}`,
|
|
151
|
+
`Participants: ${current.participants?.join(", ")}`,
|
|
152
|
+
].join("\n"),
|
|
153
|
+
}],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Predicate eval failed, continue to long-poll
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Long-poll for events
|
|
162
|
+
const data = await fetchJSON(`/events?since=${lastEventTs}&timeout=${t}`);
|
|
163
|
+
let events = data.events || [];
|
|
164
|
+
if (filterTools?.length) {
|
|
165
|
+
events = events.filter((e) => filterTools.includes(e.tool));
|
|
166
|
+
}
|
|
167
|
+
if (filterActors?.length) {
|
|
168
|
+
events = events.filter((e) => filterActors.includes(e.actorId));
|
|
169
|
+
}
|
|
170
|
+
if (events.length > 0) {
|
|
171
|
+
lastEventTs = Math.max(...events.map((e) => e.ts));
|
|
172
|
+
}
|
|
173
|
+
let predicateMatched = false;
|
|
174
|
+
if (predicate) {
|
|
175
|
+
try {
|
|
176
|
+
const fn = new Function("state", "actorId", `return ${predicate}`);
|
|
177
|
+
predicateMatched = !!fn(data.sharedState, currentActorId);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const parts = [];
|
|
184
|
+
if (events.length > 0) {
|
|
185
|
+
parts.push(`${events.length} event(s):`);
|
|
186
|
+
for (const e of events) {
|
|
187
|
+
parts.push(` [${e.actorId}] ${e.tool}(${JSON.stringify(e.input)}) → ${e.error ? `ERROR: ${e.error}` : JSON.stringify(e.output)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
parts.push("No new events (timeout).");
|
|
192
|
+
}
|
|
193
|
+
parts.push(`State: ${JSON.stringify(data.sharedState, null, 2)}`);
|
|
194
|
+
parts.push(`Participants: ${data.participants?.join(", ")}`);
|
|
195
|
+
if (predicate) {
|
|
196
|
+
parts.push(`Predicate "${predicate}": ${predicateMatched}`);
|
|
197
|
+
}
|
|
198
|
+
return { content: [{ type: "text", text: parts.join("\n") }] };
|
|
199
|
+
});
|
|
200
|
+
// ── Tool: act ──────────────────────────────────────────────
|
|
201
|
+
server.tool("act", `Execute a tool to mutate shared state. All state changes go through tools.
|
|
202
|
+
|
|
203
|
+
Example: act(toolName="counter.increment", input={amount: 2})
|
|
204
|
+
|
|
205
|
+
Auto-connects if not already connected.`, {
|
|
206
|
+
toolName: z.string().describe("Tool to call, e.g. 'counter.increment'"),
|
|
207
|
+
input: z.record(z.any()).optional().describe("Tool input parameters"),
|
|
208
|
+
}, async ({ toolName, input }) => {
|
|
209
|
+
try {
|
|
210
|
+
await ensureConnected();
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
return { content: [{ type: "text", text: `Not connected: ${err.message}` }] };
|
|
214
|
+
}
|
|
215
|
+
const result = await fetchJSON(`/tools/${toolName}`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
actorId: currentActorId || "mcp-client",
|
|
219
|
+
input: input || {},
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
if (result.error) {
|
|
223
|
+
return { content: [{ type: "text", text: `Tool error: ${result.error}` }] };
|
|
224
|
+
}
|
|
225
|
+
const state = await fetchJSON("/state");
|
|
226
|
+
const output = [
|
|
227
|
+
`${toolName} → ${JSON.stringify(result.output)}`,
|
|
228
|
+
`State: ${JSON.stringify(state.sharedState, null, 2)}`,
|
|
229
|
+
].join("\n");
|
|
230
|
+
return { content: [{ type: "text", text: output }] };
|
|
231
|
+
});
|
|
232
|
+
// ── Tool: memory ───────────────────────────────────────────
|
|
233
|
+
server.tool("memory", `Persistent agent memory (per-session). Survives across tool calls.
|
|
234
|
+
|
|
235
|
+
Actions:
|
|
236
|
+
get — Retrieve current memory
|
|
237
|
+
set — Merge updates into memory`, {
|
|
238
|
+
action: z.enum(["get", "set"]).describe("What to do"),
|
|
239
|
+
updates: z.record(z.any()).optional().describe("Key-value pairs to merge (for set)"),
|
|
240
|
+
}, async ({ action, updates }) => {
|
|
241
|
+
const key = currentActorId
|
|
242
|
+
? `local:${currentActorId}`
|
|
243
|
+
: "default";
|
|
244
|
+
if (action === "get") {
|
|
245
|
+
const data = await fetchJSON(`/memory?key=${encodeURIComponent(key)}`);
|
|
246
|
+
return {
|
|
247
|
+
content: [{
|
|
248
|
+
type: "text",
|
|
249
|
+
text: `Memory: ${JSON.stringify(data, null, 2)}`,
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (action === "set") {
|
|
254
|
+
if (!updates || Object.keys(updates).length === 0) {
|
|
255
|
+
return { content: [{ type: "text", text: "No updates provided." }] };
|
|
256
|
+
}
|
|
257
|
+
await fetchJSON("/memory", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify({ key, updates }),
|
|
260
|
+
});
|
|
261
|
+
return { content: [{ type: "text", text: `Memory updated: ${JSON.stringify(updates)}` }] };
|
|
262
|
+
}
|
|
263
|
+
return { content: [{ type: "text", text: `Unknown action: ${action}` }] };
|
|
264
|
+
});
|
|
265
|
+
// ── Tool: screenshot ──────────────────────────────────────
|
|
266
|
+
server.tool("screenshot", `Capture a screenshot of the experience as seen in the browser.
|
|
267
|
+
|
|
268
|
+
Returns the current visual state as a PNG image.
|
|
269
|
+
Requires the browser viewer to be open.
|
|
270
|
+
|
|
271
|
+
Use this to see what the user sees — inspect paintings, check layouts, read rendered text, etc.`, {
|
|
272
|
+
timeout: z.number().optional().describe("Max wait ms (default 10000)"),
|
|
273
|
+
}, async ({ timeout }) => {
|
|
274
|
+
try {
|
|
275
|
+
const t = Math.min(timeout || 10000, 30000);
|
|
276
|
+
const screenshotHeaders = {};
|
|
277
|
+
if (ROOM_TOKEN) {
|
|
278
|
+
screenshotHeaders["Authorization"] = `Bearer ${ROOM_TOKEN}`;
|
|
279
|
+
}
|
|
280
|
+
const res = await fetch(`${SERVER_URL}/screenshot?timeout=${t}`, {
|
|
281
|
+
headers: screenshotHeaders,
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok) {
|
|
284
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
285
|
+
return {
|
|
286
|
+
content: [{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `Screenshot failed: ${err.error || "Unknown error"}`,
|
|
289
|
+
}],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
293
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: "image",
|
|
297
|
+
data: base64,
|
|
298
|
+
mimeType: "image/png",
|
|
299
|
+
}],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
return {
|
|
304
|
+
content: [{
|
|
305
|
+
type: "text",
|
|
306
|
+
text: `Screenshot failed: ${err.message}. Is the dev server running? Is the browser open?`,
|
|
307
|
+
}],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// ── Start ──────────────────────────────────────────────────
|
|
312
|
+
async function main() {
|
|
313
|
+
const transport = new StdioServerTransport();
|
|
314
|
+
await server.connect(transport);
|
|
315
|
+
}
|
|
316
|
+
main().catch((err) => {
|
|
317
|
+
console.error("MCP server failed:", err);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vibevibes/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for joining vibevibes experiences — local or remote",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"vibevibes-mcp": "./bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"dev": "tsx src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
18
|
+
"zod": "^3.22.4"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"tsx": "^4.7.0",
|
|
22
|
+
"typescript": "^5.3.3",
|
|
23
|
+
"@types/node": "^20.11.5"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"bin"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|