Skip to main content
Glama
akitchin

Synology Download Station MCP Server

by akitchin
synology-client.ts13.2 kB
import axios, { AxiosInstance } from 'axios'; import { Logger } from 'winston'; export interface SynologyConfig { host: string; port: number; username: string; password: string; https?: boolean; } export interface ApiInfo { path: string; minVersion: number; maxVersion: number; } export interface Task { id: string; type: string; username: string; title: string; size: string; status: string; status_extra?: any; additional?: { detail?: TaskDetail; transfer?: TaskTransfer; file?: TaskFile[]; tracker?: TaskTracker[]; peer?: TaskPeer[]; }; } export interface TaskDetail { destination: string; uri: string; create_time: string; priority: string; total_peers: number; connected_seeders: number; connected_leechers: number; } export interface TaskTransfer { size_downloaded: string; size_uploaded: string; speed_download: number; speed_upload: number; } export interface TaskFile { filename: string; size: string; size_downloaded: string; priority: string; } export interface TaskTracker { url: string; status: string; update_timer: number; seeds: number; peers: number; } export interface TaskPeer { address: string; agent: string; progress: number; speed_download: number; speed_upload: number; } export interface SearchResult { title: string; size: string; date: string; download_uri: string; external_link: string; peers: number; seeds: number; leechs: number; module_id: string; module_title: string; } export interface SearchModule { id: string; title: string; enabled: boolean; } export interface Statistics { speed_download: number; speed_upload: number; emule_speed_download?: number; emule_speed_upload?: number; } export class SynologyClient { private axios: AxiosInstance; private config: SynologyConfig; private sid?: string; private apiInfo: Record<string, ApiInfo> = {}; private logger: Logger; constructor(config: SynologyConfig, logger: Logger) { this.config = config; this.logger = logger; const protocol = config.https ? 'https' : 'http'; const baseURL = `${protocol}://${config.host}:${config.port}/webapi`; this.axios = axios.create({ baseURL, timeout: 30000, validateStatus: () => true, // Don't throw on HTTP errors }); } async connect(): Promise<void> { this.logger.info('Connecting to Synology Download Station...'); // Get API info await this.getApiInfo(); // Login await this.login(); this.logger.info('Successfully connected to Synology Download Station'); } async disconnect(): Promise<void> { if (this.sid) { await this.logout(); } } private async getApiInfo(): Promise<void> { const apis = [ 'SYNO.API.Auth', 'SYNO.DownloadStation.Info', 'SYNO.DownloadStation.Schedule', 'SYNO.DownloadStation.Task', 'SYNO.DownloadStation.Statistic', 'SYNO.DownloadStation.RSS.Site', 'SYNO.DownloadStation.RSS.Feed', 'SYNO.DownloadStation.BTSearch' ]; const response = await this.axios.get('/query.cgi', { params: { api: 'SYNO.API.Info', version: 1, method: 'query', query: apis.join(',') } }); if (response.data.success) { this.apiInfo = response.data.data; this.logger.debug('Retrieved API info', { apis: Object.keys(this.apiInfo) }); } else { throw new Error(`Failed to get API info: ${JSON.stringify(response.data)}`); } } private async login(): Promise<void> { const authInfo = this.apiInfo['SYNO.API.Auth']; if (!authInfo) { throw new Error('Auth API not available'); } const response = await this.axios.get(`/${authInfo.path}`, { params: { api: 'SYNO.API.Auth', version: Math.min(3, authInfo.maxVersion), method: 'login', account: this.config.username, passwd: this.config.password, session: 'DownloadStation', format: 'sid' } }); if (response.data.success) { this.sid = response.data.data.sid; this.logger.debug('Login successful'); } else { const errorMessages: Record<number, string> = { 400: 'No such account or incorrect password', 401: 'Account disabled', 402: 'Permission denied', 403: '2-step verification code required', 404: 'Failed to authenticate 2-step verification code' }; const error = response.data.error; const errorCode = typeof error === 'object' ? error.code : error; const errorMsg = errorMessages[errorCode] || `Unknown error: ${errorCode}`; throw new Error(`Login failed: ${errorMsg}`); } } private async logout(): Promise<void> { const authInfo = this.apiInfo['SYNO.API.Auth']; if (!authInfo || !this.sid) return; await this.axios.get(`/${authInfo.path}`, { params: { api: 'SYNO.API.Auth', version: 1, method: 'logout', session: 'DownloadStation', _sid: this.sid } }); this.sid = undefined; this.logger.debug('Logout successful'); } private ensureAuthenticated(): void { if (!this.sid) { throw new Error('Not authenticated. Please call connect() first.'); } } // Task operations async listTasks(offset = 0, limit = -1, additional?: string[]): Promise<{ total: number; tasks: Task[] }> { this.ensureAuthenticated(); const taskInfo = this.apiInfo['SYNO.DownloadStation.Task']; if (!taskInfo) throw new Error('Task API not available'); const params: any = { api: 'SYNO.DownloadStation.Task', version: 1, method: 'list', offset, limit, _sid: this.sid }; if (additional) { params.additional = additional.join(','); } const response = await this.axios.get(`/${taskInfo.path}`, { params }); if (response.data.success) { return response.data.data; } else { throw new Error(`Failed to list tasks: ${JSON.stringify(response.data)}`); } } async getTaskInfo(ids: string[], additional?: string[]): Promise<Task[]> { this.ensureAuthenticated(); const taskInfo = this.apiInfo['SYNO.DownloadStation.Task']; if (!taskInfo) throw new Error('Task API not available'); const params: any = { api: 'SYNO.DownloadStation.Task', version: 1, method: 'getinfo', id: ids.join(','), _sid: this.sid }; if (additional) { params.additional = additional.join(','); } const response = await this.axios.get(`/${taskInfo.path}`, { params }); if (response.data.success) { return response.data.data.tasks; } else { throw new Error(`Failed to get task info: ${JSON.stringify(response.data)}`); } } async createTask(uri: string, destination?: string): Promise<void> { this.ensureAuthenticated(); const taskInfo = this.apiInfo['SYNO.DownloadStation.Task']; if (!taskInfo) throw new Error('Task API not available'); const params: any = { api: 'SYNO.DownloadStation.Task', version: 1, method: 'create', uri, _sid: this.sid }; if (destination) { params.destination = destination; } const response = await this.axios.get(`/${taskInfo.path}`, { params }); if (!response.data.success) { const errorMessages: Record<number, string> = { 400: 'File upload failed', 401: 'Max number of tasks reached', 402: 'Destination denied', 403: 'Destination does not exist', 406: 'No default destination', 408: 'File does not exist' }; const error = response.data.error; const errorCode = typeof error === 'object' ? error.code : error; const errorMsg = errorMessages[errorCode] || `Unknown error: ${errorCode}`; throw new Error(`Failed to create task: ${errorMsg}`); } } async pauseTasks(ids: string[]): Promise<void> { this.ensureAuthenticated(); const taskInfo = this.apiInfo['SYNO.DownloadStation.Task']; if (!taskInfo) throw new Error('Task API not available'); const response = await this.axios.get(`/${taskInfo.path}`, { params: { api: 'SYNO.DownloadStation.Task', version: 1, method: 'pause', id: ids.join(','), _sid: this.sid } }); if (!response.data.success) { throw new Error(`Failed to pause tasks: ${JSON.stringify(response.data)}`); } } async resumeTasks(ids: string[]): Promise<void> { this.ensureAuthenticated(); const taskInfo = this.apiInfo['SYNO.DownloadStation.Task']; if (!taskInfo) throw new Error('Task API not available'); const response = await this.axios.get(`/${taskInfo.path}`, { params: { api: 'SYNO.DownloadStation.Task', version: 1, method: 'resume', id: ids.join(','), _sid: this.sid } }); if (!response.data.success) { throw new Error(`Failed to resume tasks: ${JSON.stringify(response.data)}`); } } async deleteTasks(ids: string[], forceComplete = false): Promise<void> { this.ensureAuthenticated(); const taskInfo = this.apiInfo['SYNO.DownloadStation.Task']; if (!taskInfo) throw new Error('Task API not available'); const params: any = { api: 'SYNO.DownloadStation.Task', version: 1, method: 'delete', id: ids.join(','), _sid: this.sid }; if (forceComplete) { params.force_complete = true; } const response = await this.axios.get(`/${taskInfo.path}`, { params }); if (!response.data.success) { throw new Error(`Failed to delete tasks: ${JSON.stringify(response.data)}`); } } // BT Search operations async getSearchModules(): Promise<SearchModule[]> { this.ensureAuthenticated(); const btInfo = this.apiInfo['SYNO.DownloadStation.BTSearch']; if (!btInfo) throw new Error('BTSearch API not available'); const response = await this.axios.get(`/${btInfo.path}`, { params: { api: 'SYNO.DownloadStation.BTSearch', version: 1, method: 'getModule', _sid: this.sid } }); if (response.data.success) { return response.data.data.modules; } else { throw new Error(`Failed to get search modules: ${JSON.stringify(response.data)}`); } } async startSearch(keyword: string, module = 'enabled'): Promise<string> { this.ensureAuthenticated(); const btInfo = this.apiInfo['SYNO.DownloadStation.BTSearch']; if (!btInfo) throw new Error('BTSearch API not available'); const response = await this.axios.get(`/${btInfo.path}`, { params: { api: 'SYNO.DownloadStation.BTSearch', version: 1, method: 'start', keyword, module, _sid: this.sid } }); if (response.data.success) { return response.data.data.taskid; } else { throw new Error(`Failed to start search: ${JSON.stringify(response.data)}`); } } async getSearchResults( taskId: string, offset = 0, limit = 50, sortBy = 'seeds', sortDirection = 'DESC' ): Promise<{ finished: boolean; total: number; items: SearchResult[]; }> { this.ensureAuthenticated(); const btInfo = this.apiInfo['SYNO.DownloadStation.BTSearch']; if (!btInfo) throw new Error('BTSearch API not available'); const response = await this.axios.get(`/${btInfo.path}`, { params: { api: 'SYNO.DownloadStation.BTSearch', version: 1, method: 'list', taskid: taskId, offset, limit, sort_by: sortBy, sort_direction: sortDirection, _sid: this.sid } }); if (response.data.success) { return response.data.data; } else { throw new Error(`Failed to get search results: ${JSON.stringify(response.data)}`); } } async cleanSearch(taskId: string): Promise<void> { this.ensureAuthenticated(); const btInfo = this.apiInfo['SYNO.DownloadStation.BTSearch']; if (!btInfo) throw new Error('BTSearch API not available'); const response = await this.axios.get(`/${btInfo.path}`, { params: { api: 'SYNO.DownloadStation.BTSearch', version: 1, method: 'clean', taskid: taskId, _sid: this.sid } }); if (!response.data.success) { throw new Error(`Failed to clean search: ${JSON.stringify(response.data)}`); } } // Statistics async getStatistics(): Promise<Statistics> { this.ensureAuthenticated(); const statInfo = this.apiInfo['SYNO.DownloadStation.Statistic']; if (!statInfo) throw new Error('Statistic API not available'); const response = await this.axios.get(`/${statInfo.path}`, { params: { api: 'SYNO.DownloadStation.Statistic', version: 1, method: 'getinfo', _sid: this.sid } }); if (response.data.success) { return response.data.data; } else { throw new Error(`Failed to get statistics: ${JSON.stringify(response.data)}`); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/akitchin/synology-download-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server