UNPKG

v0-mcp-server

Version:

Simplified MCP Server for v0.dev with auto-discovery and direct SDK dispatch

1,624 lines (1,607 loc) 78 kB
// src/index.ts import { config as loadEnv } from "dotenv"; import fs2 from "fs"; import path from "path"; import { fileURLToPath } from "url"; // src/server.ts import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { createClient as createV0Client } from "v0-sdk"; // src/types.ts var SdkNamespace = /* @__PURE__ */ ((SdkNamespace2) => { SdkNamespace2["CHATS"] = "chats"; SdkNamespace2["PROJECTS"] = "projects"; SdkNamespace2["DEPLOYMENTS"] = "deployments"; SdkNamespace2["HOOKS"] = "hooks"; SdkNamespace2["INTEGRATIONS"] = "integrations"; SdkNamespace2["RATE_LIMITS"] = "rateLimits"; SdkNamespace2["USER"] = "user"; return SdkNamespace2; })(SdkNamespace || {}); var SETUP_URL = "https://v0.dev/chat/settings/keys"; var PERFORMANCE_LIMITS = { MAX_DESCRIPTION_LENGTH: 5e3, MAX_CHANGES_LENGTH: 2e3, MAX_CHAT_ID_LENGTH: 100, REQUEST_TIMEOUT: 3e4, MAX_CONCURRENT_REQUESTS: 5 }; // src/utils/sdk-discovery.ts function introspectSdkClient(client) { const methods = []; const errors = []; for (const namespace of Object.values(SdkNamespace)) { try { const namespaceMethods = discoverNamespaceMethods(client, namespace); methods.push(...namespaceMethods); } catch (error) { errors.push(`Failed to discover ${namespace}: ${error}`); } } const additionalNamespaces = discoverAdditionalNamespaces(client); for (const namespace of additionalNamespaces) { try { const namespaceMethods = discoverNamespaceMethods(client, namespace); methods.push(...namespaceMethods); } catch (error) { errors.push(`Failed to discover ${namespace}: ${error}`); } } return { methods, totalDiscovered: methods.length, namespaces: [...Object.values(SdkNamespace), ...additionalNamespaces], errors }; } function discoverNamespaceMethods(client, namespace) { const methods = []; const namespaceObject = client[namespace]; if (!namespaceObject || typeof namespaceObject !== "object") { return methods; } if (namespace === "integrations") { return discoverNestedNamespaceMethods(client, namespace, namespaceObject); } for (const [methodName, methodFunction] of Object.entries(namespaceObject)) { if (typeof methodFunction === "function") { try { const discoveredMethod = createDiscoveredMethod( namespace, methodName, methodFunction, `client.${namespace}.${methodName}` ); methods.push(discoveredMethod); } catch (error) { console.warn(`Failed to introspect ${namespace}.${methodName}:`, error); } } } return methods; } function discoverNestedNamespaceMethods(client, namespace, namespaceObject) { const methods = []; function inspectNestedObject(obj, currentPath = [namespace]) { for (const [key, value] of Object.entries(obj)) { if (typeof value === "function") { try { const fullPath = currentPath.join("."); const methodPath = [...currentPath, key]; const discoveredMethod = createDiscoveredMethod( fullPath, key, value, `client.${methodPath.join(".")}` ); methods.push(discoveredMethod); } catch (error) { console.warn(`Failed to introspect nested method ${currentPath.join(".")}:`, error); } } else if (value && typeof value === "object" && currentPath.length < 3) { inspectNestedObject(value, currentPath); } } } inspectNestedObject(namespaceObject); return methods; } function discoverAdditionalNamespaces(client) { const knownNamespaces = new Set(Object.values(SdkNamespace)); const additionalNamespaces = []; for (const [key, value] of Object.entries(client)) { if (typeof value === "object" && value !== null && !knownNamespaces.has(key)) { const hasMethods = Object.values(value).some((prop) => typeof prop === "function"); if (hasMethods) { additionalNamespaces.push(key); } } } return additionalNamespaces; } function createDiscoveredMethod(namespace, method, sdkFunction, fullPath) { return { namespace, method, toolName: generateToolName(namespace, method), sdkFunction, parameters: extractMethodParameters(sdkFunction), returnType: inferReturnType(sdkFunction), fullPath }; } function generateToolName(namespace, method) { const normalizedNamespace = namespace.replace(/([A-Z])/g, "_$1").toLowerCase(); const normalizedMethod = method.replace(/([A-Z])/g, "_$1").toLowerCase(); return `${normalizedNamespace}_${normalizedMethod}`.replace(/^_/, ""); } function extractMethodParameters(fn) { try { const fnString = fn.toString(); const paramMatch = fnString.match(/\(([^)]*)\)/); if (!paramMatch || !paramMatch[1].trim()) { return []; } const paramString = paramMatch[1]; const parameters = []; const paramNames = paramString.split(",").map((param) => param.trim()).filter((param) => param.length > 0); for (const paramName of paramNames) { const cleanName = paramName.split("=")[0].split(":")[0].trim().replace(/[{}]/g, ""); if (cleanName && cleanName !== "...args") { parameters.push({ name: cleanName, type: inferParameterType(paramName), required: !paramName.includes("=") && !paramName.includes("?"), description: `Parameter for ${fn.name || "method"}` }); } } return parameters; } catch (error) { return [{ name: "params", type: "object", required: false, description: "Method parameters" }]; } } function inferParameterType(paramString) { if (paramString.includes(": string")) return "string"; if (paramString.includes(": number")) return "number"; if (paramString.includes(": boolean")) return "boolean"; if (paramString.includes("[]") || paramString.includes("Array")) return "array"; if (paramString.includes("{") || paramString.includes("object")) return "object"; return "object"; } function inferReturnType(fn) { const fnString = fn.toString(); if (fnString.includes("Promise<") || fnString.includes("async ")) { return "Promise<object>"; } return "object"; } // src/utils/tool-schema.ts function generateMcpToolSchema(method) { const inputSchema = generateInputSchema(method.parameters); return { name: method.toolName, description: generateMethodDescription(method), inputSchema }; } function generateInputSchema(parameters) { const properties = {}; const required = []; for (const param of parameters) { properties[param.name] = mapParameterToSchema(param); if (param.required) { required.push(param.name); } } return { type: "object", properties, required, additionalProperties: true // Allow additional properties for auto-discovered methods }; } function mapParameterToSchema(param) { const baseSchema = { type: mapTypeToMcpType(param.type), description: param.description || `${param.name} parameter` }; switch (param.type) { case "string": return { ...baseSchema, type: "string", minLength: inferMinLength(param.name), maxLength: inferMaxLength(param.name) }; case "number": return { ...baseSchema, type: "number", minimum: inferMinimumValue(param.name), maximum: inferMaximumValue(param.name) }; case "boolean": return { ...baseSchema, type: "boolean" }; case "object": return { ...baseSchema, type: "object" }; case "array": return { ...baseSchema, type: "array" }; default: return baseSchema; } } function mapTypeToMcpType(tsType) { switch (tsType.toLowerCase()) { case "string": return "string"; case "number": case "integer": return "number"; case "boolean": return "boolean"; case "array": return "array"; case "object": default: return "object"; } } function generateMethodDescription(method) { const { namespace, method: methodName } = method; const descriptions = { chats: { create: "Create a new chat conversation for component generation", find: "Search and list existing chat conversations", getById: "Retrieve a specific chat conversation by ID", sendMessage: "Send a message to an existing chat conversation", delete: "Delete a chat conversation", favorite: "Mark or unmark a chat as favorite", fork: "Create a copy of an existing chat conversation", update: "Update chat conversation properties" }, projects: { create: "Create a new project for organizing components", find: "Search and list existing projects", getById: "Retrieve a specific project by ID", update: "Update project properties and settings", assign: "Assign a chat conversation to a project" }, deployments: { create: "Deploy a component or project to hosting", find: "List existing deployments", getById: "Retrieve deployment details by ID", delete: "Delete a deployment", findLogs: "Retrieve deployment logs for debugging", findErrors: "Retrieve deployment errors and failures" }, hooks: { create: "Create a webhook for event notifications", find: "List configured webhooks", getById: "Retrieve webhook configuration by ID", update: "Update webhook settings and URL", delete: "Delete a webhook configuration" }, rateLimits: { find: "Check current API rate limit status and quotas" }, user: { get: "Retrieve current user profile information", getBilling: "Get user billing information and usage", getPlan: "Retrieve current subscription plan details", getScopes: "List available API scopes and permissions" } }; const namespaceDescriptions = descriptions[namespace]; if (namespaceDescriptions && namespaceDescriptions[methodName]) { return namespaceDescriptions[methodName]; } return generateGenericDescription(namespace, methodName); } function generateGenericDescription(namespace, methodName) { const namespaceLabel = formatNamespaceLabel(namespace); if (methodName.startsWith("create")) { return `Create a new ${namespaceLabel.slice(0, -1)}`; } if (methodName.startsWith("find") || methodName.startsWith("list")) { return `Find and list ${namespaceLabel}`; } if (methodName.startsWith("get")) { return `Retrieve ${namespaceLabel.slice(0, -1)} information`; } if (methodName.startsWith("update")) { return `Update ${namespaceLabel.slice(0, -1)} properties`; } if (methodName.startsWith("delete")) { return `Delete a ${namespaceLabel.slice(0, -1)}`; } return `Execute ${methodName} operation on ${namespaceLabel}`; } function formatNamespaceLabel(namespace) { const formatted = namespace.replace(/([A-Z])/g, " $1").toLowerCase().trim(); return formatted; } function mapSdkParamsToMcp(sdkParams, method) { if (!sdkParams || typeof sdkParams !== "object") { return sdkParams; } const mcpParams = { ...sdkParams }; if (method.namespace === "chats") { if (mcpParams.message && typeof mcpParams.message === "string") { mcpParams.message = mcpParams.message.trim(); } } if (method.namespace === "projects") { if (mcpParams.projectId && typeof mcpParams.projectId === "string") { mcpParams.projectId = mcpParams.projectId.trim(); } } return mcpParams; } function inferMinLength(paramName) { const lengthConstraints = { id: 1, chatId: 1, projectId: 1, deploymentId: 1, hookId: 1, message: 1, description: 10, name: 1, title: 1 }; return lengthConstraints[paramName]; } function inferMaxLength(paramName) { const lengthConstraints = { message: 5e4, description: 1e4, name: 100, title: 200, url: 2e3 }; return lengthConstraints[paramName]; } function inferMinimumValue(paramName) { if (paramName.includes("count") || paramName.includes("limit")) { return 1; } if (paramName.includes("page")) { return 1; } return void 0; } function inferMaximumValue(paramName) { if (paramName.includes("limit")) { return 100; } if (paramName.includes("page")) { return 1e3; } return void 0; } function inferParameterTypes(method) { return method.parameters.map((param) => ({ ...param, // Add any method-specific type refinements type: refineParameterType(param, method) })); } function refineParameterType(param, method) { if (param.name.endsWith("Id")) { return "string"; } if (param.name === "message" || param.name === "description" || param.name === "changes") { return "string"; } if (param.name.includes("count") || param.name.includes("limit") || param.name.includes("page")) { return "number"; } if (param.name.includes("enable") || param.name.includes("is") || param.name.includes("has")) { return "boolean"; } return param.type; } // src/utils/aliases.ts function createAliasMap() { const aliases = /* @__PURE__ */ new Map(); aliases.set("generate_component", { legacyName: "generate_component", newName: "chats_create", transformParams: transformGenerateComponentParams, transformResponse: transformGenerateComponentResponse }); aliases.set("iterate_component", { legacyName: "iterate_component", newName: "chats_send_message", transformParams: transformIterateComponentParams, transformResponse: transformIterateComponentResponse }); aliases.set("get_rate_limits", { legacyName: "get_rate_limits", newName: "rate_limits_find", transformParams: transformRateLimitsParams, transformResponse: transformRateLimitsResponse }); return aliases; } function transformGenerateComponentParams(params) { if (!params || typeof params !== "object") { throw new Error("Invalid generate_component parameters"); } const { description, options } = params; if (!description || typeof description !== "string") { throw new Error("generate_component requires a description parameter"); } let message = description.trim(); if (options) { if (options.framework && options.framework !== "react") { message += ` Framework: ${options.framework}`; } if (options.typescript === false) { message += "\n\nUse JavaScript (no TypeScript)"; } else { message += "\n\nUse TypeScript"; } if (options.styling) { switch (options.styling) { case "tailwind": message += "\n\nUse Tailwind CSS for styling"; break; case "styled-components": message += "\n\nUse styled-components for styling"; break; case "css": message += "\n\nUse plain CSS for styling"; break; } } } else { message += "\n\nUse TypeScript"; message += "\n\nUse Tailwind CSS for styling"; } return { message }; } function transformGenerateComponentResponse(response) { if (!response) { return { success: false, chatId: "", previewUrl: "", files: [], error: "No response from v0.dev API" }; } try { const chatId = response.id || ""; const previewUrl = response.webUrl || `https://v0.dev/chat/${chatId}`; const files = extractFilesFromResponse(response); return { success: true, chatId, previewUrl, files: files.map((file) => ({ path: file.name, content: file.content })), message: files.length > 0 ? `Successfully generated ${files.length} file(s). Use the Write tool to save these files to your project.` : "Component generated successfully. Check the preview URL for details.", formattedContent: formatFilesForClaudeCode(files, chatId, previewUrl) }; } catch (error) { return { success: false, chatId: response.id || "", previewUrl: response.webUrl || "", files: [], error: `Failed to process generate_component response: ${error instanceof Error ? error.message : "Unknown error"}` }; } } function transformIterateComponentParams(params) { if (!params || typeof params !== "object") { throw new Error("Invalid iterate_component parameters"); } const { chatId, changes } = params; if (!chatId || typeof chatId !== "string") { throw new Error("iterate_component requires a chatId parameter"); } if (!changes || typeof changes !== "string") { throw new Error("iterate_component requires a changes parameter"); } return { chatId: chatId.trim(), message: changes.trim() }; } function transformIterateComponentResponse(response) { if (!response) { return { success: false, chatId: "", previewUrl: "", files: [], error: "No response from v0.dev API" }; } try { return transformGenerateComponentResponse(response); } catch (error) { return { success: false, chatId: response.chatId || "", previewUrl: "", files: [], error: `Failed to process iterate_component response: ${error instanceof Error ? error.message : "Unknown error"}` }; } } function transformRateLimitsParams(params) { return params || {}; } function transformRateLimitsResponse(response) { if (!response) { return { success: false, rateLimits: [], error: "No rate limit data available" }; } try { return { success: true, rateLimits: response.rateLimits || [], message: "Rate limits retrieved successfully" }; } catch (error) { return { success: false, rateLimits: [], error: `Failed to process rate limits: ${error instanceof Error ? error.message : "Unknown error"}` }; } } function transformLegacyParams(toolName, params) { const aliases = createAliasMap(); const alias = aliases.get(toolName); if (!alias || !alias.transformParams) { return params; } return alias.transformParams(params); } function resolveLegacyResponse(toolName, response) { const aliases = createAliasMap(); const alias = aliases.get(toolName); if (!alias || !alias.transformResponse) { return response; } return alias.transformResponse(response); } function resolveLegacyToolName(toolName) { const aliases = createAliasMap(); const alias = aliases.get(toolName); return alias ? alias.newName : toolName; } function isLegacyTool(toolName) { const aliases = createAliasMap(); return aliases.has(toolName); } function getLegacyToolNames() { const aliases = createAliasMap(); return Array.from(aliases.keys()); } function extractFilesFromResponse(response) { console.error("[DEBUG-ALIAS] Extracting files from response:", JSON.stringify(response, null, 2)); if (response.latestVersion && response.latestVersion.files) { console.error("[DEBUG-ALIAS] Found files in latestVersion.files"); return response.latestVersion.files; } if (response.files) { console.error("[DEBUG-ALIAS] Found files in files"); return response.files; } if (response.version && response.version.files) { console.error("[DEBUG-ALIAS] Found files in version.files"); return response.version.files; } if (response.data) { console.error("[DEBUG-ALIAS] Found data wrapper, checking inside"); if (response.data.latestVersion && response.data.latestVersion.files) { console.error("[DEBUG-ALIAS] Found files in data.latestVersion.files"); return response.data.latestVersion.files; } if (response.data.files) { console.error("[DEBUG-ALIAS] Found files in data.files"); return response.data.files; } } console.error("[DEBUG-ALIAS] No files found in response"); return []; } function formatFilesForClaudeCode(files, chatId, previewUrl) { if (files.length === 0) { return `No files generated. Preview available at: ${previewUrl}`; } let formatted = `Generated ${files.length} file(s) from v0.dev (Chat ID: ${chatId}) `; for (const file of files) { formatted += `## ${file.name} \`\`\`${getFileExtension(file.name)} ${file.content} \`\`\` `; } formatted += `Preview: ${previewUrl} `; formatted += ` Use the Write tool to save these files to your project.`; return formatted; } function getFileExtension(filename) { const ext = filename.split(".").pop()?.toLowerCase(); const extensionMap = { "ts": "typescript", "tsx": "typescript", "js": "javascript", "jsx": "javascript", "css": "css", "scss": "scss", "json": "json", "md": "markdown", "html": "html" }; return extensionMap[ext || ""] || "text"; } // src/utils/validation.ts var left = (value) => ({ _tag: "Left", left: value }); var right = (value) => ({ _tag: "Right", right: value }); var sanitizeString = (value) => { if (typeof value !== "string") { return ""; } let sanitized = value.trim(); sanitized = sanitized.replace(/\s+/g, " "); sanitized = sanitized.replace(/\0/g, ""); sanitized = sanitized.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, ""); return sanitized; }; var detectSecurityIssues = (value) => { if (typeof value !== "string") { return []; } const issues = []; if (/<script\b[^>]*>/gi.test(value)) { issues.push("Script tags detected"); } if (/on\w+\s*=/gi.test(value)) { issues.push("Event handlers detected"); } if (/javascript:/gi.test(value)) { issues.push("JavaScript protocol detected"); } if (/\.\.\/|\.\.\\|%2e%2e%2f|%2e%2e%5c/i.test(value)) { issues.push("Path traversal attempt detected"); } if (/\0|%00/.test(value)) { issues.push("Null bytes detected"); } if (/[;&|`$(){}[\]<>]/.test(value)) { issues.push("Potential command injection characters detected"); } const specialCharCount = (value.match(/[^a-zA-Z0-9\s]/g) || []).length; const specialCharRatio = specialCharCount / value.length; if (specialCharRatio > 0.3 && value.length > 50) { issues.push("Excessive special characters detected"); } return issues; }; var validateSecureString = (fieldName, value) => { const issues = detectSecurityIssues(value); if (issues.length > 0) { return left({ field: fieldName, message: `${fieldName} contains potentially unsafe content: ${issues.join(", ")}`, code: "SECURITY_ISSUE" }); } return right(sanitizeString(value)); }; // src/utils/param-validator.ts var left2 = (value) => ({ _tag: "Left", left: value }); var right2 = (value) => ({ _tag: "Right", right: value }); function validateToolParameters(toolName, params, schema) { try { const { properties, required = [] } = schema.inputSchema; if (Object.keys(properties).length === 0 || required.length === 0) { if (!params || Object.keys(params).length === 0) { return right2({}); } } if (!params && required.length > 0) { return left2(new Error(`${toolName} requires parameters: ${required.join(", ")}`)); } if (params && typeof params !== "object") { if (required.length === 1) { params = { [required[0]]: params }; } else { return left2(new Error(`${toolName} requires an object with parameters`)); } } if (!params) { params = {}; } const validatedParams = {}; const errors = []; for (const requiredParam of required) { if (!(requiredParam in params)) { if (requiredParam === "params" && Object.keys(params).length > 0) { validatedParams[requiredParam] = params; continue; } errors.push(`Missing required parameter: ${requiredParam}`); } } for (const [paramName, paramValue] of Object.entries(params)) { const paramSchema = properties[paramName]; if (!paramSchema) { if (schema.inputSchema.additionalProperties !== false) { validatedParams[paramName] = paramValue; } continue; } const paramValidation = validateParameter(paramName, paramValue, paramSchema); if (paramValidation._tag === "Left") { errors.push(paramValidation.left.message); } else { validatedParams[paramName] = paramValidation.right; } } if (errors.length > 0) { return left2(new Error(`${toolName} parameter validation failed: ${errors.join(", ")}`)); } return right2(validatedParams); } catch (error) { return left2(new Error(`${toolName} validation error: ${error instanceof Error ? error.message : "Unknown error"}`)); } } function validateParameter(name, value, schema) { const typeValidation = validateParameterType(name, value, schema.type); if (typeValidation._tag === "Left") { return typeValidation; } switch (schema.type) { case "string": return validateStringParameter(name, value, schema); case "number": return validateNumberParameter(name, value, schema); case "boolean": return validateBooleanParameter(name, value); case "object": return validateObjectParameter(name, value); case "array": return validateArrayParameter(name, value); default: return right2(value); } } function validateParameterType(name, value, expectedType) { const actualType = Array.isArray(value) ? "array" : typeof value; if (expectedType === "number" && actualType === "string") { const numValue = Number(value); if (!isNaN(numValue)) { return right2(numValue); } } if (expectedType === "boolean" && actualType === "string") { if (value === "true") return right2(true); if (value === "false") return right2(false); } if (actualType !== expectedType) { return left2(new Error(`Parameter ${name} must be of type ${expectedType}, got ${actualType}`)); } return right2(value); } function validateStringParameter(name, value, schema) { if (schema.minLength && value.length < schema.minLength) { return left2(new Error(`Parameter ${name} must be at least ${schema.minLength} characters long`)); } if (schema.maxLength && value.length > schema.maxLength) { return left2(new Error(`Parameter ${name} must be at most ${schema.maxLength} characters long`)); } if (schema.enum && !schema.enum.includes(value)) { return left2(new Error(`Parameter ${name} must be one of: ${schema.enum.join(", ")}`)); } if (name === "description" || name === "changes") { const securityValidation = validateSecureString(name, value); if (securityValidation._tag === "Left") { return left2(new Error(`Parameter ${name} failed security validation: ${securityValidation.left.message}`)); } value = securityValidation.right; } if (name.endsWith("Id")) { const idValidation = validateIdFormat(value); if (idValidation._tag === "Left") { return idValidation; } } return right2(value.trim()); } function validateNumberParameter(name, value, schema) { if (!Number.isFinite(value)) { return left2(new Error(`Parameter ${name} must be a finite number`)); } if (schema.minimum !== void 0 && value < schema.minimum) { return left2(new Error(`Parameter ${name} must be at least ${schema.minimum}`)); } if (schema.maximum !== void 0 && value > schema.maximum) { return left2(new Error(`Parameter ${name} must be at most ${schema.maximum}`)); } return right2(value); } function validateBooleanParameter(name, value) { return right2(value); } function validateObjectParameter(name, value) { if (value === null) { return left2(new Error(`Parameter ${name} cannot be null`)); } return right2(value); } function validateArrayParameter(name, value) { return right2(value); } function validateIdFormat(id) { if (!id || id.trim().length === 0) { return left2(new Error("ID cannot be empty")); } const trimmedId = id.trim(); if (trimmedId.length < 3) { return left2(new Error("ID must be at least 3 characters long")); } if (trimmedId.length > 100) { return left2(new Error("ID must be at most 100 characters long")); } if (!/^[a-zA-Z0-9_-]+$/.test(trimmedId)) { return left2(new Error("ID can only contain letters, numbers, hyphens, and underscores")); } return right2(trimmedId); } function validateComponentDescription(description) { if (!description || typeof description !== "string") { return left2(new Error("Description must be a non-empty string")); } const trimmed = description.trim(); if (trimmed.length < 10) { return left2(new Error("Description must be at least 10 characters long")); } if (trimmed.length > 5e3) { return left2(new Error("Description must be at most 5000 characters long")); } const securityValidation = validateSecureString("description", trimmed); if (securityValidation._tag === "Left") { return left2(new Error(`Description failed security validation: ${securityValidation.left.message}`)); } return right2(securityValidation.right); return right2(trimmed); } function validateChatId(chatId) { if (!chatId || typeof chatId !== "string") { return left2(new Error("Chat ID must be a non-empty string")); } return validateIdFormat(chatId); } // src/utils/response-formatter.ts function formatSdkResponse(response, toolName) { if (!response) { return { success: false, error: "No response from SDK method", toolName }; } try { if (toolName.startsWith("chats_")) { return formatChatResponse(response, toolName); } if (toolName.startsWith("projects_")) { return formatProjectResponse(response, toolName); } if (toolName.startsWith("deployments_")) { return formatDeploymentResponse(response, toolName); } if (toolName.startsWith("rate_limits_")) { return formatRateLimitResponse(response, toolName); } if (toolName.startsWith("user_")) { return formatUserResponse(response, toolName); } return formatGenericResponse(response, toolName); } catch (error) { return { success: false, error: `Failed to format response: ${error instanceof Error ? error.message : "Unknown error"}`, toolName, rawResponse: response }; } } function formatChatResponse(response, toolName) { const formatted = { success: true, toolName }; if (response.id) { formatted.chatId = response.id; } if (response.webUrl) { formatted.previewUrl = response.webUrl; } if (response.apiUrl) { formatted.apiUrl = response.apiUrl; } const files = extractFilesFromChatResponse(response); if (files.length > 0) { formatted.files = files.map((file) => ({ path: file.name, content: file.content, locked: file.locked || false })); formatted.message = `Generated ${files.length} file(s)`; formatted.formattedContent = formatFilesForDisplay(files, response.id, response.webUrl); } if (response.name || response.title) { formatted.title = response.name || response.title; } if (response.createdAt) { formatted.createdAt = response.createdAt; } if (response.updatedAt) { formatted.updatedAt = response.updatedAt; } return formatted; } function formatProjectResponse(response, toolName) { const formatted = { success: true, toolName }; if (response.id) { formatted.projectId = response.id; } if (response.name) { formatted.name = response.name; } if (response.description) { formatted.description = response.description; } if (response.webUrl) { formatted.webUrl = response.webUrl; } if (Array.isArray(response)) { return { success: true, toolName, projects: response.map((project) => ({ id: project.id, name: project.name, description: project.description, webUrl: project.webUrl, createdAt: project.createdAt, updatedAt: project.updatedAt })), count: response.length }; } return formatted; } function formatDeploymentResponse(response, toolName) { const formatted = { success: true, toolName }; if (response.id) { formatted.deploymentId = response.id; } if (response.url) { formatted.deploymentUrl = response.url; } if (response.status) { formatted.status = response.status; } if (Array.isArray(response)) { return { success: true, toolName, deployments: response.map((deployment) => ({ id: deployment.id, url: deployment.url, status: deployment.status, createdAt: deployment.createdAt, updatedAt: deployment.updatedAt })), count: response.length }; } if (response.logs && Array.isArray(response.logs)) { formatted.logs = response.logs; formatted.logCount = response.logs.length; } return formatted; } function formatRateLimitResponse(response, toolName) { return { success: true, toolName, rateLimits: response.rateLimits || response, message: "Rate limits retrieved successfully" }; } function formatUserResponse(response, toolName) { const formatted = { success: true, toolName }; if (response.id) { formatted.userId = response.id; } if (response.email) { formatted.email = maskEmail(response.email); } if (response.name) { formatted.name = response.name; } if (response.billing) { formatted.billing = { plan: response.billing.plan, status: response.billing.status // Omit payment details, amounts, etc. }; } return formatted; } function formatGenericResponse(response, toolName) { return { success: true, toolName, data: response, message: `${toolName} executed successfully` }; } function formatErrorResponse(error, toolName, context) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, toolName, error: errorMessage, context: context || "tool_execution", timestamp: (/* @__PURE__ */ new Date()).toISOString() }; } function extractFilesFromChatResponse(response) { console.error("[DEBUG] Extracting files from response:", JSON.stringify(response, null, 2)); if (response.latestVersion && response.latestVersion.files) { console.error("[DEBUG] Found files in latestVersion.files"); return response.latestVersion.files; } if (response.files) { console.error("[DEBUG] Found files in files"); return response.files; } if (response.version && response.version.files) { console.error("[DEBUG] Found files in version.files"); return response.version.files; } if (response.data) { console.error("[DEBUG] Found data wrapper, checking inside"); if (response.data.latestVersion && response.data.latestVersion.files) { console.error("[DEBUG] Found files in data.latestVersion.files"); return response.data.latestVersion.files; } if (response.data.files) { console.error("[DEBUG] Found files in data.files"); return response.data.files; } } console.error("[DEBUG] No files found in response"); return []; } function formatFilesForDisplay(files, chatId, previewUrl) { if (files.length === 0) { return "No files generated"; } let formatted = `Generated ${files.length} file(s)`; if (chatId) { formatted += ` (Chat ID: ${chatId})`; } formatted += "\n\n"; for (const file of files) { const extension = getFileExtension2(file.name); formatted += `## ${file.name} \`\`\`${extension} ${file.content} \`\`\` `; } if (previewUrl) { formatted += `Preview: ${previewUrl} `; } formatted += "\nUse the Write tool to save these files to your project."; return formatted; } function getFileExtension2(filename) { const ext = filename.split(".").pop()?.toLowerCase(); const extensionMap = { "ts": "typescript", "tsx": "typescript", "js": "javascript", "jsx": "javascript", "css": "css", "scss": "scss", "json": "json", "md": "markdown", "html": "html", "py": "python", "java": "java", "cpp": "cpp", "c": "c" }; return extensionMap[ext || ""] || "text"; } function maskEmail(email) { const [username, domain] = email.split("@"); if (!username || !domain) return "***@***.***"; const maskedUsername = username.length > 2 ? username.substring(0, 2) + "*".repeat(username.length - 2) : "**"; return `${maskedUsername}@${domain}`; } function formatExecutionResult(result) { if (result.success && result.result) { if (result.result.formattedContent) { return result.result.formattedContent; } if (result.result.message) { return result.result.message; } } if (result.success && result.formattedResult) { if (typeof result.formattedResult === "string") { return result.formattedResult; } if (result.formattedResult.formattedContent) { return result.formattedResult.formattedContent; } if (result.formattedResult.message) { return result.formattedResult.message; } return JSON.stringify(result.formattedResult, null, 2); } if (!result.success && result.error) { return `Error: ${result.error}`; } return "Operation completed successfully"; } // src/utils/logs.ts import fs from "fs"; import { promises as fsPromises } from "fs"; var LogManager = class _LogManager { static instance; logs = []; maxLogs = 1e3; logFilePath = "/tmp/v0-mcp-debug.log"; constructor() { } static getInstance() { if (!_LogManager.instance) { _LogManager.instance = new _LogManager(); } return _LogManager.instance; } /** * Add a log entry to the in-memory buffer */ addLog(level, message, context) { const entry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), level, message, context }; this.logs.push(entry); if (this.logs.length > this.maxLogs) { this.logs = this.logs.slice(-this.maxLogs); } } /** * Get recent logs from memory */ getRecentLogs(limit = 100) { return this.logs.slice(-limit); } /** * Get logs filtered by level */ getLogsByLevel(level, limit = 100) { return this.logs.filter((log) => log.level === level).slice(-limit); } /** * Search logs by message content */ searchLogs(query, limit = 100) { const lowerQuery = query.toLowerCase(); return this.logs.filter((log) => log.message.toLowerCase().includes(lowerQuery)).slice(-limit); } /** * Read logs from the debug file */ async readDebugFile(lines = 100) { try { if (fs.existsSync(this.logFilePath)) { const content = await fsPromises.readFile(this.logFilePath, "utf-8"); const allLines = content.split("\n").filter((line) => line.trim()); return allLines.slice(-lines); } return ["Debug log file not found"]; } catch (error) { return [`Error reading debug log: ${error instanceof Error ? error.message : String(error)}`]; } } /** * Get a summary of current log state */ getLogSummary() { const levels = { info: 0, error: 0, debug: 0, warn: 0 }; for (const log of this.logs) { levels[log.level]++; } const recentErrors = this.logs.filter((log) => log.level === "error").slice(-5).map((log) => `${log.timestamp}: ${log.message}`); return { totalLogs: this.logs.length, levels, recentErrors, debugFileExists: fs.existsSync(this.logFilePath) }; } /** * Clear in-memory logs */ clearLogs() { this.logs = []; } }; var logManager = LogManager.getInstance(); function writeToDebugFile(message) { try { const logLine = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message} `; fs.appendFileSync("/tmp/v0-mcp-debug.log", logLine); } catch (error) { } } function logInfo(message, context) { logManager.addLog("info", message, context); const fullMessage = `[MCP] ${message}${context ? " " + JSON.stringify(context) : ""}`; console.error(fullMessage); writeToDebugFile(`INFO: ${message}${context ? " " + JSON.stringify(context) : ""}`); } function logError(message, context) { logManager.addLog("error", message, context); const fullMessage = `[MCP] ERROR: ${message}${context ? " " + JSON.stringify(context) : ""}`; console.error(fullMessage); writeToDebugFile(`ERROR: ${message}${context ? " " + JSON.stringify(context) : ""}`); } function logDebug(message, context) { logManager.addLog("debug", message, context); const fullMessage = `[MCP] DEBUG: ${message}${context ? " " + JSON.stringify(context) : ""}`; if (process.env.VERBOSE === "true") { console.error(fullMessage); } writeToDebugFile(`DEBUG: ${message}${context ? " " + JSON.stringify(context) : ""}`); } function logWarn(message, context) { logManager.addLog("warn", message, context); const fullMessage = `[MCP] WARN: ${message}${context ? " " + JSON.stringify(context) : ""}`; console.error(fullMessage); writeToDebugFile(`WARN: ${message}${context ? " " + JSON.stringify(context) : ""}`); } // src/tools/logs-tool.ts function createLogsTools() { return [ { namespace: "logs", method: "get", toolName: "logs_get", sdkFunction: async (params) => { const { limit = 100, level, search } = params; if (search) { return { logs: logManager.searchLogs(search, limit), summary: logManager.getLogSummary() }; } if (level && ["info", "error", "debug", "warn"].includes(level)) { return { logs: logManager.getLogsByLevel(level, limit), summary: logManager.getLogSummary() }; } return { logs: logManager.getRecentLogs(limit), summary: logManager.getLogSummary() }; }, parameters: [ { name: "limit", type: "number", required: false, description: "Maximum number of logs to return (default: 100)" }, { name: "level", type: "string", required: false, description: "Filter by log level (info, error, debug, warn)" }, { name: "search", type: "string", required: false, description: "Search logs by message content" } ], returnType: "object", fullPath: "logs.get" }, { namespace: "logs", method: "readDebugFile", toolName: "logs_read_debug_file", sdkFunction: async (params) => { const { lines = 100 } = params; const fileLines = await logManager.readDebugFile(lines); return { debugFile: "/tmp/v0-mcp-debug.log", lines: fileLines, totalLines: fileLines.length }; }, parameters: [ { name: "lines", type: "number", required: false, description: "Number of lines to read from the end of the file (default: 100)" } ], returnType: "object", fullPath: "logs.readDebugFile" }, { namespace: "logs", method: "summary", toolName: "logs_summary", sdkFunction: async () => { const summary = logManager.getLogSummary(); const envInfo = { apiKeyPresent: !!process.env.V0_API_KEY, apiKeyLength: process.env.V0_API_KEY?.length || 0, verbose: process.env.VERBOSE === "true", nodeVersion: process.version, platform: process.platform, cwd: process.cwd() }; return { ...summary, environment: envInfo, timestamp: (/* @__PURE__ */ new Date()).toISOString() }; }, parameters: [], returnType: "object", fullPath: "logs.summary" }, { namespace: "logs", method: "clear", toolName: "logs_clear", sdkFunction: async () => { logManager.clearLogs(); return { success: true, message: "In-memory logs cleared" }; }, parameters: [], returnType: "object", fullPath: "logs.clear" } ]; } // src/config.ts var DEFAULT_CONFIG = { name: "v0-mcp-server", version: "1.0.0", verbose: false }; var ENV_VARS = { API_KEY: "V0_API_KEY", VERBOSE: "V0_VERBOSE", TIMEOUT: "V0_TIMEOUT", MAX_MEMORY: "V0_MAX_MEMORY" }; var ConfigManager = class _ConfigManager { config; /** * Creates a new ConfigManager instance * @param initialConfig - Initial configuration values */ constructor(initialConfig = {}) { this.config = this.buildConfig(initialConfig); this.validateConfig(); } /** * Builds complete configuration from partial config and environment * @param partial - Partial configuration * @returns Complete configuration */ buildConfig(partial) { const getEnvString = (name, options) => process.env[name] || options?.defaultValue || ""; const getEnvBoolean = (name) => process.env[name] === "true"; const getEnvNumber = (name) => process.env[name] ? parseInt(process.env[name], 10) : void 0; return { name: partial.name || DEFAULT_CONFIG.name, version: partial.version || DEFAULT_CONFIG.version, apiKey: partial.apiKey || getEnvString(ENV_VARS.API_KEY, { defaultValue: "" }), verbose: partial.verbose ?? getEnvBoolean(ENV_VARS.VERBOSE) ?? DEFAULT_CONFIG.verbose, timeout: partial.timeout ?? getEnvNumber(ENV_VARS.TIMEOUT), maxMemoryMB: partial.maxMemoryMB ?? getEnvNumber(ENV_VARS.MAX_MEMORY) }; } /** * Validates the complete configuration * @throws Error if configuration is invalid */ validateConfig() { const result = this.validate(this.config); if (!result.valid) { throw new Error( `Configuration validation failed: ${result.errors.join("\n")}` ); } if (result.warnings.length > 0 && this.config.verbose) { console.error("Configuration warnings:"); result.warnings.forEach((warning) => console.error(` - ${warning}`)); } } /** * Gets the validated configuration * @returns Server configuration */ getConfig() { return { ...this.config }; } /** * Updates configuration with new values * @param updates - Configuration updates * @returns Updated configuration */ updateConfig(updates) { const newConfig = { ...this.config, ...updates }; const result = this.validate(newConfig); if (!result.valid) { throw new Error( `Configuration update failed: ${result.errors.join("\n")}` ); } this.config = newConfig; return { ...this.config }; } /** * Validates a configuration object * @param config - Configuration to validate * @returns Validation result */ validate(config) { const errors = []; const warnings = []; if (!config.name || typeof config.name !== "string") { errors.push("Server name is required and must be a string"); } else if (config.name.length === 0) { errors.push("Server name cannot be empty"); } if (!config.version || typeof config.version !== "string") { errors.push("Server version is required and must be a string"); } else if (!this.isValidVersion(config.version)) { warnings.push(`Version '${config.version}' does not follow semantic versioning`); } if (!config.apiKey || typeof config.apiKey !== "string") { errors.push("API key is required and must be a string"); } else if (config.apiKey.length < 10) { errors.push("API key appears to be invalid (too short)"); } if (config.verbose !== void 0 && typeof config.verbose !== "boolean") { errors.push("Verbose setting must be a boolean"); } if (config.timeout !== void 0) { if (typeof config.timeout !== "number" || config.timeout <= 0) { errors.push("Timeout must be a positive number"); } } if (config.maxMemoryMB !== void 0) { if (typeof config.maxMemoryMB !== "number" || config.maxMemoryMB <= 0) { errors.push("Max memory must be a positive number"); } } return { valid: errors.length === 0, errors, warnings, config: errors.length === 0 ? config : void 0 }; } /** * Creates configuration from environment variables * @returns Configuration object */ static fromEnvironment() { const getEnvString = (name, options) => process.env[name] || options?.defaultValue || ""; const getEnvBoolean = (name) => process.env[name] === "true"; const getEnvNumber = (name) => process.env[name] ? parseInt(process.env[name], 10) : void 0; const envConfig = { apiKey: getEnvString(ENV_VARS.API_KEY), verbose: getEnvBoolean(ENV_VARS.VERBOSE), timeout: getEnvNumber(ENV_VARS.TIMEOUT), maxMemoryMB: getEnvNumber(ENV_VARS.MAX_MEMORY) }; return new _ConfigManager(envConfig); } /** * Creates configuration from CLI arguments * @param args - CLI arguments * @returns Configuration object */ static fromCliArgs(args) { const cliConfig = { apiKey: args.apiKey, verbose: args.verbose, timeout: args.timeout, maxMemoryMB: args.maxMemory }; return new _ConfigManager(cliConfig); } /** * Loads package.json information for server metadata * @returns Package information or defaults */ static async loadPackageInfo() { try { const fs3 = await import("fs"); const path2 = await import("path"); const url = await import("url"); const __dirname = path2.dirname(url.fileURLToPath(import.meta.url)); const packagePath = path2.join(__dirname, "..", "package.json"); if (fs3.existsSync(packagePath)) { const packageJson = JSON.parse(fs3.readFileSync