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
JavaScript
// 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