@vibevibes/mcp 0.1.0 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 vibevibes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @vibevibes/mcp
2
+
3
+ MCP server + runtime engine for [vibevibes](https://github.com/vibevibes/sdk) experiences.
4
+
5
+ Agents connect via MCP. Humans connect via browser. Same room, same state.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @vibevibes/mcp
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Start the server for an experience
17
+ npx vibevibes-serve ./my-experience
18
+
19
+ # Or use as MCP server (for AI agents)
20
+ npx vibevibes-mcp
21
+ ```
22
+
23
+ ## How It Works
24
+
25
+ 1. Define an experience using [@vibevibes/sdk](https://github.com/vibevibes/sdk)
26
+ 2. This server loads and runs it
27
+ 3. Agents connect via MCP tools (`connect`, `act`, `look`)
28
+ 4. Humans open the browser to see the canvas
29
+ 5. Everyone shares the same state — tools are the only mutation path
30
+
31
+ ## MCP Tools
32
+
33
+ | Tool | Purpose |
34
+ |------|---------|
35
+ | `connect` | Join the server, get room info |
36
+ | `act` | Execute a tool in the experience |
37
+ | `look` | Observe current state |
38
+ | `disconnect` | Leave room |
39
+
40
+ ## Ecosystem
41
+
42
+ | Package | Description |
43
+ |---------|-------------|
44
+ | [@vibevibes/sdk](https://github.com/vibevibes/sdk) | Define experiences — tools, canvas, state |
45
+ | **@vibevibes/mcp** (this) | Runtime server — MCP + WebSocket + browser viewer |
46
+ | [create-vibevibes](https://github.com/vibevibes/create) | `npx create-vibevibes my-exp` — scaffold in seconds |
47
+ | [experiences](https://github.com/vibevibes/experiences) | Example experiences — fork and remix |
48
+
49
+ ## License
50
+
51
+ MIT
package/bin/cli.js CHANGED
@@ -1,11 +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";
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,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall: writes .mcp.json into the project root so Claude Code
5
+ * can discover the vibevibes MCP server automatically.
6
+ *
7
+ * Skipped during self-install (e.g. when building the monorepo).
8
+ */
9
+
10
+ import { writeFileSync, existsSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+
13
+ // npm sets INIT_CWD to the directory where `npm install` was run
14
+ const projectRoot = process.env.INIT_CWD;
15
+ if (!projectRoot) process.exit(0);
16
+
17
+ // Skip if we're installing inside the vibevibes monorepo itself
18
+ const isMonorepo = existsSync(resolve(projectRoot, "sdk")) && existsSync(resolve(projectRoot, "mcp"));
19
+ if (isMonorepo) process.exit(0);
20
+
21
+ const mcpJsonPath = resolve(projectRoot, ".mcp.json");
22
+
23
+ // Don't overwrite an existing .mcp.json
24
+ if (existsSync(mcpJsonPath)) process.exit(0);
25
+
26
+ const config = {
27
+ mcpServers: {
28
+ vibevibes: {
29
+ command: "npx",
30
+ args: ["vibevibes-mcp"],
31
+ env: {
32
+ VIBEVIBES_SERVER_URL: "http://localhost:4321",
33
+ },
34
+ },
35
+ },
36
+ };
37
+
38
+ writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + "\n");
39
+ console.log("vibevibes: wrote .mcp.json for Claude Code");
package/bin/serve.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Start the vibevibes server for an experience.
5
+ *
6
+ * Usage:
7
+ * npx vibevibes-serve # serves from current directory
8
+ * npx vibevibes-serve ./my-exp # serves from a specific path
9
+ */
10
+
11
+ import { resolve } from "node:path";
12
+ import { writeFileSync, existsSync } from "node:fs";
13
+ import { execSync } from "node:child_process";
14
+ import { startServer } from "../dist/server.js";
15
+
16
+ const projectRoot = resolve(process.argv[2] || ".");
17
+ const port = parseInt(process.env.PORT || "4321", 10);
18
+
19
+ // Find git root (where Claude Code reads .mcp.json from)
20
+ let mcpJsonDir = projectRoot;
21
+ try {
22
+ mcpJsonDir = execSync("git rev-parse --show-toplevel", { cwd: projectRoot, encoding: "utf-8" }).trim();
23
+ } catch {}
24
+
25
+ // Auto-generate .mcp.json so Claude Code can connect via /mcp
26
+ const mcpJsonPath = resolve(mcpJsonDir, ".mcp.json");
27
+ if (!existsSync(mcpJsonPath)) {
28
+ const config = {
29
+ mcpServers: {
30
+ vibevibes: {
31
+ command: "npx",
32
+ args: ["vibevibes-mcp"],
33
+ env: { VIBEVIBES_SERVER_URL: `http://localhost:${port}` },
34
+ },
35
+ },
36
+ };
37
+ writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + "\n");
38
+ console.log(` .mcp.json: wrote ${mcpJsonPath}`);
39
+ }
40
+
41
+ startServer({ projectRoot, port });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Experience bundler — produces server and client bundles from src/index.tsx.
3
+ *
4
+ * Server bundle: CJS, eval'd via new Function() to extract tools + manifest.
5
+ * Client bundle: ESM, loaded in browser via blob URL + dynamic import().
6
+ *
7
+ * Extracted from create-experience/runtime/bundler.ts into @vibevibes/runtime.
8
+ */
9
+ /**
10
+ * Bundle for server-side tool execution (Node.js eval).
11
+ * Returns the raw ExperienceModule extracted via new Function().
12
+ */
13
+ export declare function bundleForServer(entryPath: string): Promise<string>;
14
+ /**
15
+ * Evaluate a server bundle and extract the ExperienceModule.
16
+ */
17
+ export declare function evalServerBundle(serverCode: string): Promise<unknown>;
18
+ /**
19
+ * Bundle for client-side Canvas rendering (browser).
20
+ * Returns pure ESM. External imports (react, zod, etc.) are left as-is —
21
+ * the viewer's import map resolves them in the browser.
22
+ */
23
+ export declare function bundleForClient(entryPath: string): Promise<string>;
24
+ /**
25
+ * Build both bundles from an entry file.
26
+ */
27
+ export declare function buildExperience(entryPath: string): Promise<{
28
+ serverCode: string;
29
+ clientCode: string;
30
+ }>;
31
+ /**
32
+ * Validate a client bundle for common issues.
33
+ * Lightweight — esbuild already validates syntax. This just checks the output exists.
34
+ * Returns null if OK, or an error message string.
35
+ */
36
+ export declare function validateClientBundle(code: string): string | null;
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Experience bundler — produces server and client bundles from src/index.tsx.
3
+ *
4
+ * Server bundle: CJS, eval'd via new Function() to extract tools + manifest.
5
+ * Client bundle: ESM, loaded in browser via blob URL + dynamic import().
6
+ *
7
+ * Extracted from create-experience/runtime/bundler.ts into @vibevibes/runtime.
8
+ */
9
+ import * as esbuild from "esbuild";
10
+ const EXTERNALS = ["react", "react/jsx-runtime", "react-dom", "react-dom/client", "yjs", "zod", "@vibevibes/sdk", "@vibevibes/runtime"];
11
+ // Additional externals for server-only bundles — heavy rendering libraries that
12
+ // aren't needed for tool/test execution. Canvas components are never called server-side.
13
+ const SERVER_ONLY_EXTERNALS = [
14
+ ...EXTERNALS,
15
+ "three", "three/*",
16
+ "@react-three/fiber", "@react-three/fiber/*",
17
+ "@react-three/drei", "@react-three/drei/*",
18
+ ];
19
+ /**
20
+ * Strip esbuild's CJS annotation: `0 && (module.exports = {...})`.
21
+ * Uses brace-depth counting to handle nested objects/functions in the annotation,
22
+ * unlike a simple `[^}]*` regex which breaks on nested braces.
23
+ */
24
+ function stripCjsAnnotation(code) {
25
+ const marker = /0\s*&&\s*\(module\.exports\s*=\s*\{/g;
26
+ let match;
27
+ let result = code;
28
+ while ((match = marker.exec(code)) !== null) {
29
+ const start = match.index;
30
+ let depth = 1; // we matched the opening `{`
31
+ let i = match.index + match[0].length;
32
+ while (i < code.length && depth > 0) {
33
+ if (code[i] === "{")
34
+ depth++;
35
+ else if (code[i] === "}")
36
+ depth--;
37
+ i++;
38
+ }
39
+ // Skip the closing `)` and optional `;`
40
+ if (i < code.length && code[i] === ")")
41
+ i++;
42
+ if (i < code.length && code[i] === ";")
43
+ i++;
44
+ result = result.slice(0, start) + "/* [vibevibes] stripped CJS annotation */" + result.slice(i);
45
+ break; // Only one annotation per bundle
46
+ }
47
+ return result;
48
+ }
49
+ /**
50
+ * Strip import/export statements for external packages.
51
+ * The runtime provides these via globalThis (browser) or function args (server).
52
+ */
53
+ /**
54
+ * Strip external imports from a CJS server bundle so it can be eval'd with new Function().
55
+ * Only used for server bundles — client bundles keep imports (resolved by browser import map).
56
+ */
57
+ function stripExternalImports(code, externals = EXTERNALS) {
58
+ let result = code;
59
+ for (const ext of externals) {
60
+ let escaped;
61
+ if (ext.endsWith("/*")) {
62
+ const base = ext.slice(0, -2).replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
63
+ escaped = `${base}\\/[^"']+`;
64
+ }
65
+ else {
66
+ escaped = ext.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
67
+ }
68
+ // CJS: var import_X = __toESM(require("pkg"), N); or var import_X = require("pkg");
69
+ result = result.replace(new RegExp(`var\\s+\\w+\\s*=\\s*(?:__toESM\\()?require\\(["']${escaped}["']\\)[^;]{0,500};`, "g"), "");
70
+ }
71
+ return result;
72
+ }
73
+ /**
74
+ * CJS shim definitions for server-side eval (new Function()).
75
+ * Maps esbuild-generated variable names to runtime-provided globals.
76
+ */
77
+ const SDK_CORE_SHIM = "{ defineExperience: defineExperience, defineTool: defineTool, defineTest: defineTest, defineStream: defineStream, createChatTools: createChatTools, default: { defineExperience: defineExperience, defineTool: defineTool, defineTest: defineTest, defineStream: defineStream, createChatTools: createChatTools } }";
78
+ const CJS_BASE_SHIMS = {
79
+ import_react: "{ default: React, __esModule: true, createElement: React.createElement, Fragment: React.Fragment, useState: React.useState, useEffect: React.useEffect, useCallback: React.useCallback, useMemo: React.useMemo, useRef: React.useRef, useContext: React.useContext, useReducer: React.useReducer, createContext: React.createContext, forwardRef: React.forwardRef, memo: React.memo }",
80
+ import_zod: "{ z: z, default: z }",
81
+ import_yjs: "{ default: Y }",
82
+ import_sdk: SDK_CORE_SHIM,
83
+ import_vibevibes_sdk: SDK_CORE_SHIM,
84
+ import_runtime: SDK_CORE_SHIM,
85
+ import_vibevibes_runtime: SDK_CORE_SHIM,
86
+ import_react_dom: "{ default: {}, __esModule: true }",
87
+ import_client: "{ default: {}, __esModule: true }",
88
+ // Proxy stubs for rendering libraries (server-side only — Canvas is never called)
89
+ import_three: "(new Proxy({}, { get: (_, p) => typeof p === 'string' ? function(){} : undefined }))",
90
+ import_fiber: "(new Proxy({}, { get: (_, p) => typeof p === 'string' ? function(){} : undefined }))",
91
+ import_drei: "(new Proxy({}, { get: (_, p) => typeof p === 'string' ? function(){} : undefined }))",
92
+ };
93
+ /**
94
+ * Inject CJS shim variables for server-side eval.
95
+ */
96
+ function injectCjsShims(code) {
97
+ const lines = [];
98
+ // Emit base shims
99
+ for (const [name, value] of Object.entries(CJS_BASE_SHIMS)) {
100
+ lines.push(`var ${name} = ${value};`);
101
+ }
102
+ // Scan for numbered variants (e.g. import_react2, import_zod3) and alias them
103
+ for (const baseName of Object.keys(CJS_BASE_SHIMS)) {
104
+ const pattern = new RegExp(`\\b(${baseName}(\\d+))\\b`, "g");
105
+ const seen = new Set();
106
+ let match;
107
+ while ((match = pattern.exec(code)) !== null) {
108
+ const numberedName = match[1];
109
+ if (!seen.has(numberedName)) {
110
+ seen.add(numberedName);
111
+ lines.push(`var ${numberedName} = ${baseName};`);
112
+ }
113
+ }
114
+ }
115
+ return lines.join("\n");
116
+ }
117
+ /**
118
+ * Format esbuild errors into actionable messages with file:line and suggestions.
119
+ */
120
+ function formatEsbuildError(err, target) {
121
+ const buildErr = err;
122
+ if (buildErr.errors && Array.isArray(buildErr.errors)) {
123
+ const formatted = buildErr.errors.map((e) => {
124
+ const loc = e.location
125
+ ? `${e.location.file}:${e.location.line}:${e.location.column}`
126
+ : "unknown location";
127
+ return ` ${loc}: ${e.text}`;
128
+ }).join("\n");
129
+ return new Error(`Build failed (${target} bundle):\n${formatted}\n\n` +
130
+ `Common fixes:\n` +
131
+ `- Check for syntax errors at the indicated location\n` +
132
+ `- Ensure all imports resolve to existing files in src/\n` +
133
+ `- Verify @vibevibes/sdk imports match the available exports`);
134
+ }
135
+ return err instanceof Error ? err : new Error(String(err));
136
+ }
137
+ /**
138
+ * Bundle for server-side tool execution (Node.js eval).
139
+ * Returns the raw ExperienceModule extracted via new Function().
140
+ */
141
+ export async function bundleForServer(entryPath) {
142
+ let result;
143
+ try {
144
+ result = await esbuild.build({
145
+ entryPoints: [entryPath],
146
+ bundle: true,
147
+ format: "cjs",
148
+ platform: "node",
149
+ target: "es2022",
150
+ write: false,
151
+ external: SERVER_ONLY_EXTERNALS,
152
+ jsx: "transform",
153
+ jsxFactory: "React.createElement",
154
+ jsxFragment: "React.Fragment",
155
+ logLevel: "silent",
156
+ });
157
+ }
158
+ catch (err) {
159
+ throw formatEsbuildError(err, "server");
160
+ }
161
+ const outputFile = result.outputFiles?.[0];
162
+ if (!outputFile)
163
+ throw new Error("esbuild produced no output files for server bundle");
164
+ let code = outputFile.text;
165
+ code = stripExternalImports(code, SERVER_ONLY_EXTERNALS);
166
+ // Strip user-code React hook destructuring (already provided by CJS shims)
167
+ code = code.replace(/(?:const|let|var)\s+\{[^}]*?\b(?:useState|useEffect|useCallback|useMemo|useRef|useContext|useReducer)\b[^}]*?\}\s*=\s*(?:React|import_react\w*)\s*;/g, "/* [vibevibes] stripped duplicate React destructuring */");
168
+ // Inject CJS shims for esbuild-generated variable references
169
+ // Pass code so we can detect numbered variants (import_react2, etc.)
170
+ code = injectCjsShims(code) + "\n" + code;
171
+ // Strip esbuild's CJS annotation `0 && (module.exports = {...})` — dead code that
172
+ // causes syntax errors when module.exports is replaced with var assignment.
173
+ // Uses brace-depth counting to handle nested objects/functions in the annotation.
174
+ code = stripCjsAnnotation(code);
175
+ // Replace module.exports/export default with variable assignment
176
+ code = code.replace(/module\.exports\s*=\s*/g, "var __experience_export__ = ");
177
+ code = code.replace(/exports\.default(?!\w)\s*=\s*/g, "var __experience_export__ = ");
178
+ return code;
179
+ }
180
+ /**
181
+ * Evaluate a server bundle and extract the ExperienceModule.
182
+ */
183
+ export async function evalServerBundle(serverCode) {
184
+ const sdk = await import("@vibevibes/sdk");
185
+ const { defineExperience, defineTool, defineTest, defineStream, createChatTools } = sdk;
186
+ const noop = () => null;
187
+ const stubReact = {
188
+ createElement: noop, Fragment: "Fragment",
189
+ useState: (init) => [typeof init === "function" ? init() : init, noop],
190
+ useEffect: noop, useCallback: (fn) => fn,
191
+ useMemo: (fn) => fn(), useRef: (init) => ({ current: init ?? null }),
192
+ useContext: noop, useReducer: noop,
193
+ createContext: noop, forwardRef: noop, memo: (x) => x,
194
+ };
195
+ const zodModule = await import("zod");
196
+ const z = zodModule.z ?? zodModule.default ?? zodModule;
197
+ const fn = new Function("globalThis", "process", "global", "React", "Y", "z", "defineExperience", "defineTool", "defineTest", "defineStream", "createChatTools", "require", "exports", "module", "console", `"use strict";\n${serverCode}\nreturn typeof __experience_export__ !== 'undefined' ? __experience_export__ : (typeof module !== 'undefined' ? module.exports : undefined);`);
198
+ const fakeModule = { exports: {} };
199
+ const sandboxGlobal = Object.create(null);
200
+ const sandboxProcess = { env: { NODE_ENV: "production" } };
201
+ const result = fn(sandboxGlobal, sandboxProcess, sandboxGlobal, stubReact, {}, z, defineExperience, defineTool, defineTest, defineStream, createChatTools, (id) => { throw new Error(`require('${id}') is not supported in the vibevibes server sandbox. Add '${id}' to EXTERNALS in bundler.ts.`); }, fakeModule.exports, fakeModule, console);
202
+ const exports = fakeModule.exports;
203
+ return result?.default ?? result ?? exports?.default ?? exports;
204
+ }
205
+ /**
206
+ * Bundle for client-side Canvas rendering (browser).
207
+ * Returns pure ESM. External imports (react, zod, etc.) are left as-is —
208
+ * the viewer's import map resolves them in the browser.
209
+ */
210
+ export async function bundleForClient(entryPath) {
211
+ let result;
212
+ try {
213
+ result = await esbuild.build({
214
+ entryPoints: [entryPath],
215
+ bundle: true,
216
+ format: "esm",
217
+ platform: "browser",
218
+ target: "es2020",
219
+ write: false,
220
+ external: EXTERNALS,
221
+ jsx: "transform",
222
+ jsxFactory: "React.createElement",
223
+ jsxFragment: "React.Fragment",
224
+ logLevel: "silent",
225
+ });
226
+ }
227
+ catch (err) {
228
+ throw formatEsbuildError(err, "client");
229
+ }
230
+ const clientOutputFile = result.outputFiles?.[0];
231
+ if (!clientOutputFile)
232
+ throw new Error("esbuild produced no output files for client bundle");
233
+ return clientOutputFile.text;
234
+ }
235
+ /**
236
+ * Build both bundles from an entry file.
237
+ */
238
+ export async function buildExperience(entryPath) {
239
+ const [serverCode, clientCode] = await Promise.all([
240
+ bundleForServer(entryPath),
241
+ bundleForClient(entryPath),
242
+ ]);
243
+ return { serverCode, clientCode };
244
+ }
245
+ /**
246
+ * Validate a client bundle for common issues.
247
+ * Lightweight — esbuild already validates syntax. This just checks the output exists.
248
+ * Returns null if OK, or an error message string.
249
+ */
250
+ export function validateClientBundle(code) {
251
+ if (!code || !code.trim())
252
+ return "Client bundle is empty";
253
+ return null;
254
+ }
package/dist/index.d.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  /**
2
- * vibevibes-mcp — standalone MCP server for joining vibevibes experiences.
2
+ * vibevibes-mcp — MCP server for agent participation in vibevibes experiences.
3
3
  *
4
- * Works with both local dev servers and remote shared tunnels.
4
+ * The `connect` tool is the single entry point: it joins the server, writes
5
+ * the state file (for the stop hook to poll), and returns room info.
5
6
  *
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
7
+ * 4 tools: connect, act, look, disconnect
12
8
  */
13
9
  export {};