#!/usr/bin/env node
import { config } from "dotenv";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { VerblazeApiService } from "./services/verblazeApi.js";
import { z } from "zod";
// Load environment variables from .env file
config();
// Create MCP server
const server = new McpServer({
name: "verblaze-localization-server",
version: "1.0.0",
});
const verblazeApi = new VerblazeApiService();
// Helper function to safely extract arguments
const getArguments = (request: any) => {
if (!request) {
throw new Error("Request object is undefined");
}
// New MCP SDK structure: arguments are directly in the request
if (request.arguments) {
return request.arguments;
}
// Fallback: if no arguments property, the request itself might be the arguments
if (typeof request === "object" && Object.keys(request).length > 0) {
console.error(
"Using request as arguments directly:",
JSON.stringify(request, null, 2)
);
return request;
}
console.error("Request structure:", JSON.stringify(request, null, 2));
throw new Error("Request arguments are undefined");
};
// Helper function to generate fileKey from fileTitle
const generateFileKey = (fileTitle: string): string => {
return fileTitle
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "") // Remove special characters
.replace(/\s+/g, "_") // Replace spaces with underscores
.trim();
};
// Smart parameter extraction for each tool
const extractToolParameters = (args: any, toolName: string) => {
switch (toolName) {
case "addScreen":
let { fileKey, fileTitle } = args;
// Smart parameter extraction: generate fileKey from fileTitle if missing
if (!fileKey && fileTitle) {
fileKey = generateFileKey(fileTitle);
}
if (!fileKey) {
throw new Error(
`addScreen requires either fileKey or fileTitle. Received: ${JSON.stringify(
args
)}. Example: { "fileKey": "home_screen" } or { "fileTitle": "Home Screen" }`
);
}
return { fileKey, fileTitle };
case "translateValues":
const { fileKey: tvFileKey, values } = args;
if (!tvFileKey) {
throw new Error(
`translateValues requires fileKey parameter. Received: ${JSON.stringify(
args
)}. Example: { "fileKey": "home_screen", "values": { "welcome": "Hello" } }`
);
}
if (
!values ||
typeof values !== "object" ||
Object.keys(values).length === 0
) {
throw new Error(
`translateValues requires values object with at least one key-value pair. Received: ${JSON.stringify(
args
)}. Example: { "fileKey": "home_screen", "values": { "welcome": "Hello", "login": "Sign In" } }`
);
}
return { fileKey: tvFileKey, values };
case "addLanguage":
case "removeLanguage":
case "changeBaseLanguage":
const { languageCode } = args;
if (!languageCode) {
throw new Error(
`${toolName} requires languageCode parameter. Received: ${JSON.stringify(
args
)}. Example: { "languageCode": "tr-TR" }`
);
}
return { languageCode };
case "removeScreen":
case "deleteValue":
const { fileKey: rfFileKey } = args;
if (!rfFileKey) {
throw new Error(
`${toolName} requires fileKey parameter. Received: ${JSON.stringify(
args
)}. Example: { "fileKey": "home_screen" }`
);
}
if (toolName === "deleteValue") {
const { valueKey } = args;
if (!valueKey) {
throw new Error(
`deleteValue requires both fileKey and valueKey parameters. Received: ${JSON.stringify(
args
)}. Example: { "fileKey": "home_screen", "valueKey": "welcome" }`
);
}
return { fileKey: rfFileKey, valueKey };
}
return { fileKey: rfFileKey };
default:
return args;
}
};
// Register tools using the latest SDK syntax with Zod schemas
server.registerTool(
"translateValues",
{
title: "Translate Values",
description:
"Translate string values to all supported languages in a Verblaze project. REQUIRED: fileKey and values object. Example: { fileKey: 'home_screen', values: { welcome: 'Hello', login: 'Sign In' } }",
inputSchema: {
fileKey: z
.string()
.describe(
"REQUIRED: File key for the translations (e.g., 'home_screen', 'settings_page')"
),
values: z
.record(z.string())
.describe(
"REQUIRED: Key-value pairs of strings to translate. Must contain at least one key-value pair."
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { fileKey, values } = extractToolParameters(
args,
"translateValues"
);
const response = await verblazeApi.translateValues({
fileKey,
values,
});
return {
content: [
{
type: "text",
text: `Successfully translated ${
Object.keys(values).length
} values to ${
(response as any).translatedLanguages?.length || 0
} languages.\n\nOriginal values:\n${JSON.stringify(
values,
null,
2
)}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error translating values: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"addLanguage",
{
title: "Add Language",
description:
"Add a new language to a Verblaze project. REQUIRED: languageCode. Example: { languageCode: 'en-US' } for Turkish, { languageCode: 'es-ES' } for Spanish",
inputSchema: {
languageCode: z
.string()
.describe(
"REQUIRED: Language code to add (e.g., 'en-US' for English, 'es-ES' for Spanish, 'fr-FR' for French, 'de-DE' for German)"
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { languageCode } = extractToolParameters(args, "addLanguage");
const response = await verblazeApi.addLanguage({
languageCode,
});
// Debug: Log the actual response
console.error(
"DEBUG - addLanguage response:",
JSON.stringify(response, null, 2)
);
// Check if response has the expected structure
if (!response) {
throw new Error(`Response is null or undefined`);
}
// Check if response has statusCode (backend response structure)
if ((response as any).statusCode && (response as any).language) {
// This is the backend response structure
const language = (response as any).language;
const languageText = language
? `${language.general} (${language.code})`
: "Unknown";
return {
content: [
{
type: "text",
text: `Successfully added language: ${languageText} to the project.`,
},
],
};
}
// Fallback: try to access directly
if (!(response as any).general || !(response as any).code) {
throw new Error(
`Invalid response structure: ${JSON.stringify(response)}`
);
}
const languageText = `${(response as any).general} (${
(response as any).code
})`;
return {
content: [
{
type: "text",
text: `Successfully added language: ${languageText} to the project.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error adding language: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"removeLanguage",
{
title: "Remove Language",
description:
"Remove a language from a Verblaze project. REQUIRED: languageCode. Example: { languageCode: 'es-ES' } to remove Spanish",
inputSchema: {
languageCode: z
.string()
.describe(
"REQUIRED: Language code to remove (e.g., 'en-US ' for English, 'es-ES' for Spanish, 'fr-FR' for French)"
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { languageCode } = extractToolParameters(args, "removeLanguage");
await verblazeApi.removeLanguage({
languageCode,
});
return {
content: [
{
type: "text",
text: `Successfully removed language: ${languageCode} from the project.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error removing language: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"addScreen",
{
title: "Add Screen",
description:
"Add a new screen/file to all languages in a Verblaze project. REQUIRED: fileKey OR fileTitle. If only fileTitle is provided, fileKey will be generated automatically. Example: { fileKey: 'home_screen' } or { fileTitle: 'Home Screen' }",
inputSchema: {
fileKey: z
.string()
.optional()
.describe(
"File key for the screen (e.g., 'home_screen', 'settings_page'). If not provided, will be generated from fileTitle."
),
fileTitle: z
.string()
.optional()
.describe(
"Display title for the screen (e.g., 'Home Screen', 'Settings Page'). If fileKey is not provided, this will be used to generate fileKey."
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { fileKey, fileTitle } = extractToolParameters(args, "addScreen");
const response = await verblazeApi.addScreen({
fileKey,
fileTitle,
});
return {
content: [
{
type: "text",
text: `Successfully added screen: ${
(response as any).fileTitle || (response as any).fileKey
} (${(response as any).fileKey}) to all languages in the project.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error adding screen: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"removeScreen",
{
title: "Remove Screen",
description:
"Remove a screen/file from all languages in a Verblaze project. REQUIRED: fileKey. Example: { fileKey: 'home_screen' }",
inputSchema: {
fileKey: z
.string()
.describe(
"REQUIRED: File key of the screen to remove (e.g., 'home_screen', 'settings_page')"
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { fileKey } = extractToolParameters(args, "removeScreen");
const response = await verblazeApi.removeScreen({
fileKey,
});
return {
content: [
{
type: "text",
text: `Successfully removed screen: ${
(response as any).fileKey
} from ${(response as any).deletedCount} languages in the project.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error removing screen: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"deleteValue",
{
title: "Delete Value",
description:
"Delete a specific translation value from all languages in a Verblaze project. REQUIRED: fileKey and valueKey. Example: { fileKey: 'home_screen', valueKey: 'welcome' }",
inputSchema: {
fileKey: z
.string()
.describe(
"REQUIRED: File key containing the value (e.g., 'home_screen', 'settings_page')"
),
valueKey: z
.string()
.describe(
"REQUIRED: Key of the value to delete (e.g., 'welcome', 'login_button')"
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { fileKey, valueKey } = extractToolParameters(args, "deleteValue");
const response = await verblazeApi.deleteValue({
fileKey,
valueKey,
});
return {
content: [
{
type: "text",
text: `Successfully deleted value: ${
(response as any).valueKey
} from file: ${(response as any).fileKey} in ${
(response as any).deletedCount
} languages.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deleting value: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"changeBaseLanguage",
{
title: "Change Base Language",
description:
"Change the base language of a Verblaze project. REQUIRED: languageCode. Example: { languageCode: 'en-US' } to set English as base language",
inputSchema: {
languageCode: z
.string()
.describe(
"REQUIRED: New base language code (must be supported in the project). Examples: 'en-US' for English, 'es-ES' for Spanish, 'fr-FR' for French"
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { languageCode } = extractToolParameters(
args,
"changeBaseLanguage"
);
const response = await verblazeApi.changeBaseLanguage({
languageCode,
});
// Debug: Log the actual response
console.error(
"DEBUG - changeBaseLanguage response:",
JSON.stringify(response, null, 2)
);
// Check if response has the expected structure
if (!response) {
throw new Error(`Response is null or undefined`);
}
// Check if response has statusCode (backend response structure)
if ((response as any).statusCode && (response as any).newBaseLanguage) {
// This is the backend response structure
const newBaseLanguage = (response as any).newBaseLanguage;
const baseLanguageText = newBaseLanguage
? `${newBaseLanguage.general} (${newBaseLanguage.code})`
: "Unknown";
return {
content: [
{
type: "text",
text: `Successfully changed base language to: ${baseLanguageText}.`,
},
],
};
}
// Fallback: try to access directly
if (!(response as any).newBaseLanguage) {
throw new Error(
`Invalid response structure: ${JSON.stringify(response)}`
);
}
const baseLanguageText = (response as any).newBaseLanguage
? `${(response as any).newBaseLanguage.general} (${
(response as any).newBaseLanguage.code
})`
: "Unknown";
return {
content: [
{
type: "text",
text: `Successfully changed base language to: ${baseLanguageText}.`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error changing base language: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
// New read-only tools
server.registerTool(
"listLanguages",
{
title: "List Languages",
description:
"List all supported languages in a Verblaze project. No parameters required.",
inputSchema: {},
},
async (request: any) => {
try {
const response = await verblazeApi.listLanguages();
// Debug: Log the actual response
console.error(
"DEBUG - listLanguages response:",
JSON.stringify(response, null, 2)
);
// Check if response has the expected structure
if (!response) {
throw new Error(`Response is null or undefined`);
}
// Check if response has statusCode (backend response structure)
if ((response as any).statusCode && (response as any).languages) {
// This is the backend response structure
const languagesList = (response as any).languages
.map((lang: any) => `${lang.general} (${lang.code})`)
.join(", ");
const baseLanguageText = (response as any).baseLanguage
? `${(response as any).baseLanguage.general} (${
(response as any).baseLanguage.code
})`
: "Unknown";
return {
content: [
{
type: "text",
text: `Project supports ${
(response as any).languages.length
} languages:\n\n${languagesList}\n\nBase language: ${baseLanguageText}`,
},
],
};
}
// Fallback: try to access directly
if (!response.languages || !Array.isArray(response.languages)) {
throw new Error(
`Invalid response structure: ${JSON.stringify(response)}`
);
}
const languagesList = response.languages
.map((lang) => `${lang.general} (${lang.code})`)
.join(", ");
const baseLanguageText = response.baseLanguage
? `${response.baseLanguage.general} (${response.baseLanguage.code})`
: "Unknown";
return {
content: [
{
type: "text",
text: `Project supports ${response.languages.length} languages:\n\n${languagesList}\n\nBase language: ${baseLanguageText}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing languages: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"listScreens",
{
title: "List Screens",
description:
"List all screens/files in a Verblaze project. No parameters required.",
inputSchema: {},
},
async (request: any) => {
try {
const response = await verblazeApi.listScreens();
// Debug: Log the actual response
console.error(
"DEBUG - listScreens response:",
JSON.stringify(response, null, 2)
);
// Check if response has the expected structure
if (!response) {
throw new Error(`Response is null or undefined`);
}
// Check if response has statusCode (backend response structure)
if ((response as any).statusCode && (response as any).screens) {
// This is the backend response structure
if ((response as any).screens.length === 0) {
return {
content: [
{
type: "text",
text: "No screens found in this project.",
},
],
};
}
const screensList = (response as any).screens
.map(
(screen: any) =>
`• ${screen.fileTitle} (${screen.fileKey}) - ${screen.valueCount} values`
)
.join("\n");
return {
content: [
{
type: "text",
text: `Found ${
(response as any).screens.length
} screens in the project:\n\n${screensList}`,
},
],
};
}
// Fallback: try to access directly
if (!response.screens || !Array.isArray(response.screens)) {
throw new Error(
`Invalid response structure: ${JSON.stringify(response)}`
);
}
if (response.screens.length === 0) {
return {
content: [
{
type: "text",
text: "No screens found in this project.",
},
],
};
}
const screensList = response.screens
.map(
(screen) =>
`• ${screen.fileTitle} (${screen.fileKey}) - ${screen.valueCount} values`
)
.join("\n");
return {
content: [
{
type: "text",
text: `Found ${response.screens.length} screens in the project:\n\n${screensList}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error listing screens: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
server.registerTool(
"getScreenValues",
{
title: "Get Screen Values",
description:
"Get all translation values for a specific screen in a specific language. REQUIRED: languageCode and fileKey. Example: { languageCode: 'en-US', fileKey: 'home_screen' }",
inputSchema: {
languageCode: z
.string()
.describe(
"REQUIRED: Language code to get values for (e.g., 'en-US' for English, 'es-ES' for Spanish, 'fr-FR' for French)"
),
fileKey: z
.string()
.describe(
"REQUIRED: File key of the screen to get values for (e.g., 'home_screen', 'settings_page')"
),
},
},
async (request: any) => {
try {
const args = getArguments(request);
const { languageCode, fileKey } = args;
if (!languageCode) {
throw new Error("languageCode is required");
}
if (!fileKey) {
throw new Error("fileKey is required");
}
const response = await verblazeApi.getScreenValues({
languageCode,
fileKey,
});
// Access response directly, not response.data
const valuesList = Object.entries(response.values || {})
.map(([key, value]) => `• ${key}: "${value}"`)
.join("\n");
return {
content: [
{
type: "text",
text: `Screen values for ${response.fileTitle} (${
response.fileKey
}) in ${response.languageCode}:\n\n${
valuesList || "No values found"
}\n\nTotal values: ${response.valueCount}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error getting screen values: ${
error instanceof Error ? error.message : "Unknown error"
}`,
},
],
};
}
}
);
// Start server with stdio transport
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Verblaze MCP Server started successfully");
console.error("Available tools:");
console.error(
"- translateValues: Translate string values to all supported languages"
);
console.error("- addLanguage: Add a new language to the project");
console.error("- removeLanguage: Remove a language from the project");
console.error("- addScreen: Add a new screen to all languages");
console.error("- removeScreen: Remove a screen from all languages");
console.error("- deleteValue: Delete a value from all languages");
console.error(
"- changeBaseLanguage: Change the base language of the project"
);
console.error("- listLanguages: List all supported languages");
console.error("- listScreens: List all screens in the project");
console.error(
"- getScreenValues: Get values for a specific screen and language"
);
} catch (error) {
console.error("Failed to start Verblaze MCP Server:", error);
process.exit(1);
}
}
main();