@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 ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @vibevibes/mcp CLI
5
+ *
6
+ * Usage:
7
+ * npx @vibevibes/mcp # localhost:4321
8
+ * npx @vibevibes/mcp https://xyz.trycloudflare.com # remote shared room
9
+ */
10
+
11
+ import "../dist/index.js";
@@ -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
+ }