#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
McpError,
ErrorCode,
} from '@modelcontextprotocol/sdk/types.js';
import axios, { AxiosInstance, AxiosError } from 'axios';
import { z } from 'zod';
// --- Environment Variable Check ---
const API_KEY = process.env.DEEPL_API_KEY;
if (!API_KEY) {
console.error('DEEPL_API_KEY environment variable is not set.');
process.exit(1);
}
// Detect if using free or pro API based on key format
const IS_FREE_API = API_KEY.endsWith(':fx');
const BASE_URL = IS_FREE_API ? 'https://api-free.deepl.com' : 'https://api.deepl.com';
// --- Input Schemas (using Zod) ---
const TranslateTextInputSchema = z.object({
text: z.union([z.string(), z.array(z.string())]).transform(val =>
Array.isArray(val) ? val : [val]
),
target_lang: z.string().describe('Target language code (e.g., DE, FR, ES, EN-US, EN-GB)'),
source_lang: z.string().optional().describe('Source language code; auto-detected if omitted'),
formality: z.enum(['default', 'more', 'less', 'prefer_more', 'prefer_less']).optional()
.describe('Formality level (supported for DE, FR, IT, ES, NL, PL, PT, JA, RU)'),
context: z.string().optional().describe('Additional context for better translation (not billed)'),
preserve_formatting: z.boolean().optional().describe('Preserve original formatting'),
glossary_id: z.string().optional().describe('Glossary ID for custom terminology'),
split_sentences: z.enum(['0', '1', 'nonewlines']).optional()
.describe('Sentence splitting: 0=none, 1=default, nonewlines=punctuation only'),
});
const RephraseTextInputSchema = z.object({
text: z.string().min(1, 'Text cannot be empty'),
target_lang: z.string().optional().describe('Target language for rephrasing (defaults to detected language)'),
});
const ListLanguagesInputSchema = z.object({
type: z.enum(['source', 'target']).optional().describe('Filter by source or target languages'),
});
// --- DeepL API Response Types ---
interface DeepLTranslation {
detected_source_language: string;
text: string;
}
interface DeepLLanguage {
language: string;
name: string;
supports_formality?: boolean;
}
interface DeepLUsage {
character_count: number;
character_limit: number;
}
interface DeepLRephrase {
text: string;
}
// --- Tool Definitions ---
const TOOLS = [
{
name: 'translate_text',
description: 'Translates text using DeepL API with optional formality control, context, and glossary support.',
inputSchema: {
type: 'object',
properties: {
text: {
oneOf: [
{ type: 'string' },
{ type: 'array', items: { type: 'string' } }
],
description: 'Text or array of texts to translate',
},
target_lang: {
type: 'string',
description: 'Target language code (e.g., DE, FR, ES, EN-US, EN-GB)',
},
source_lang: {
type: 'string',
description: 'Source language code; auto-detected if omitted',
},
formality: {
type: 'string',
enum: ['default', 'more', 'less', 'prefer_more', 'prefer_less'],
description: 'Formality level (supported for DE, FR, IT, ES, NL, PL, PT, JA, RU)',
},
context: {
type: 'string',
description: 'Additional context for better translation accuracy (not billed)',
},
preserve_formatting: {
type: 'boolean',
description: 'Preserve original formatting',
},
glossary_id: {
type: 'string',
description: 'Glossary ID for custom terminology (requires source_lang)',
},
split_sentences: {
type: 'string',
enum: ['0', '1', 'nonewlines'],
description: 'Sentence splitting: 0=none, 1=default, nonewlines=punctuation only',
},
},
required: ['text', 'target_lang'],
},
},
{
name: 'rephrase_text',
description: 'Rephrases/improves text using DeepL Write API for better clarity and style.',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Text to rephrase/improve',
},
target_lang: {
type: 'string',
description: 'Target language for rephrasing (defaults to detected language)',
},
},
required: ['text'],
},
},
{
name: 'get_source_languages',
description: 'Retrieves all available source languages for translation.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_target_languages',
description: 'Retrieves all available target languages for translation, including formality support info.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_usage',
description: 'Retrieves current API usage statistics (character count and limits).',
inputSchema: {
type: 'object',
properties: {},
},
},
];
// --- DeepL Server Class ---
class DeepLServer {
private server: Server;
private axiosInstance: AxiosInstance;
constructor() {
this.server = new Server(
{
name: 'deepl-mcp-server',
version: '0.2.0',
},
{
capabilities: {
tools: {},
},
}
);
this.axiosInstance = axios.create({
baseURL: BASE_URL,
headers: {
Authorization: `DeepL-Auth-Key ${API_KEY}`,
},
timeout: 30000,
});
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Server Error]', error);
process.on('SIGINT', async () => {
console.error('Received SIGINT, shutting down server...');
await this.server.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('Received SIGTERM, shutting down server...');
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
// --- ListTools Handler ---
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
// --- CallTool Handler ---
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
switch (name) {
case 'translate_text':
return await this.translateText(args);
case 'rephrase_text':
return await this.rephraseText(args);
case 'get_source_languages':
return await this.getLanguages('source');
case 'get_target_languages':
return await this.getLanguages('target');
case 'get_usage':
return await this.getUsage();
default:
throw new McpError(ErrorCode.MethodNotFound, `Tool '${name}' not found.`);
}
} catch (error) {
return this.handleError(error);
}
});
}
// --- Tool Implementations ---
private async translateText(args: unknown) {
const validated = TranslateTextInputSchema.parse(args);
const requestBody: Record<string, unknown> = {
text: validated.text,
target_lang: validated.target_lang,
};
if (validated.source_lang) requestBody.source_lang = validated.source_lang;
if (validated.formality) requestBody.formality = validated.formality;
if (validated.context) requestBody.context = validated.context;
if (validated.preserve_formatting !== undefined) requestBody.preserve_formatting = validated.preserve_formatting;
if (validated.glossary_id) {
if (!validated.source_lang) {
throw new McpError(ErrorCode.InvalidParams, 'source_lang is required when using glossary_id');
}
requestBody.glossary_id = validated.glossary_id;
}
if (validated.split_sentences) requestBody.split_sentences = validated.split_sentences;
const response = await this.axiosInstance.post<{ translations: DeepLTranslation[] }>(
'/v2/translate',
requestBody
);
return {
content: [{
type: 'text' as const,
text: JSON.stringify(response.data.translations, null, 2),
}],
};
}
private async rephraseText(args: unknown) {
const validated = RephraseTextInputSchema.parse(args);
const requestBody: Record<string, unknown> = {
text: validated.text,
};
if (validated.target_lang) requestBody.target_lang = validated.target_lang;
try {
const response = await this.axiosInstance.post<{ improvements: DeepLRephrase[] }>(
'/v2/write/rephrase',
requestBody
);
return {
content: [{
type: 'text' as const,
text: JSON.stringify(response.data, null, 2),
}],
};
} catch (error) {
// DeepL Write may not be available on all plans
if (axios.isAxiosError(error) && error.response?.status === 403) {
return {
content: [{
type: 'text' as const,
text: 'DeepL Write/Rephrase feature is not available on your current plan. This feature requires a DeepL Pro subscription.',
}],
isError: true,
};
}
throw error;
}
}
private async getLanguages(type: 'source' | 'target') {
const response = await this.axiosInstance.get<DeepLLanguage[]>(
'/v2/languages',
{ params: { type } }
);
// Format nicely for readability
const formatted = response.data.map(lang => ({
code: lang.language,
name: lang.name,
...(type === 'target' && lang.supports_formality !== undefined && {
supports_formality: lang.supports_formality
}),
}));
return {
content: [{
type: 'text' as const,
text: JSON.stringify(formatted, null, 2),
}],
};
}
private async getUsage() {
const response = await this.axiosInstance.get<DeepLUsage>('/v2/usage');
const usage = response.data;
const percentUsed = ((usage.character_count / usage.character_limit) * 100).toFixed(2);
const result = {
character_count: usage.character_count,
character_limit: usage.character_limit,
characters_remaining: usage.character_limit - usage.character_count,
percent_used: `${percentUsed}%`,
api_type: IS_FREE_API ? 'Free' : 'Pro',
};
return {
content: [{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
}],
};
}
// --- Error Handling ---
private handleError(error: unknown) {
let errorMessage: string;
let errorCode = ErrorCode.InternalError;
if (error instanceof McpError) {
throw error;
} else if (error instanceof z.ZodError) {
errorCode = ErrorCode.InvalidParams;
errorMessage = `Validation error: ${error.issues.map((e: z.ZodIssue) => `${e.path.join('.')}: ${e.message}`).join(', ')}`;
} else if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ message?: string }>;
const status = axiosError.response?.status;
const message = axiosError.response?.data?.message || axiosError.message;
if (status === 400) errorCode = ErrorCode.InvalidParams;
else if (status === 401 || status === 403) errorCode = ErrorCode.InvalidRequest;
else if (status === 429) errorCode = ErrorCode.InvalidRequest;
else if (status === 456) errorCode = ErrorCode.InvalidRequest; // Quota exceeded
const statusText = status ? ` (${status})` : '';
errorMessage = `DeepL API error${statusText}: ${message}`;
if (status === 456) {
errorMessage = 'DeepL API quota exceeded. Please check your usage limits.';
} else if (status === 429) {
errorMessage = 'DeepL API rate limit exceeded. Please try again later.';
}
} else {
errorMessage = error instanceof Error ? error.message : String(error);
}
console.error('[Tool Error]', errorMessage);
return {
content: [{
type: 'text' as const,
text: `Error: ${errorMessage}`,
}],
isError: true,
};
}
// --- Server Start ---
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error(`DeepL MCP Server v0.2.0 started (${IS_FREE_API ? 'Free' : 'Pro'} API)`);
}
}
// --- Initialize and Run ---
const server = new DeepLServer();
server.run().catch(error => {
console.error('Failed to start DeepL MCP Server:', error);
process.exit(1);
});