@pickstar-2002/video-fetch-mcp
Version:
基于 MCP 协议的多平台视频下载服务端
412 lines (376 loc) • 11.4 kB
text/typescript
/**
* MCP 服务器实现
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { VideoDownloadRequest, MCPToolResponse } from '../types.js';
import YtDlpService from './ytdlp-service.js';
import { logger } from '../utils/logger.js';
import config from '../config.js';
import Joi from 'joi';
export class MCPVideoDownloaderServer {
private server: Server;
private ytdlpService: YtDlpService;
constructor() {
this.server = new Server(
{
name: config.mcp.name,
version: config.mcp.version,
},
{
capabilities: {
tools: {},
},
}
);
this.ytdlpService = new YtDlpService();
this.setupHandlers();
}
/**
* 设置 MCP 处理器
*/
private setupHandlers(): void {
// 列出可用工具
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'download_video',
description: '下载指定URL的视频到本地路径',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: '视频链接URL'
},
outputPath: {
type: 'string',
description: '本地保存路径'
},
quality: {
type: 'string',
description: '视频质量选项',
enum: ['best', 'worst', 'bestvideo', 'bestaudio'],
default: 'best'
},
outputTemplate: {
type: 'string',
description: '输出文件名模板',
default: '%(title)s.%(ext)s'
},
extractAudio: {
type: 'boolean',
description: '是否仅提取音频',
default: false
},
audioFormat: {
type: 'string',
description: '音频格式',
enum: ['mp3', 'aac', 'wav', 'flac'],
default: 'mp3'
},
downloadSubtitles: {
type: 'boolean',
description: '是否下载字幕',
default: false
},
subtitleLangs: {
type: 'array',
items: { type: 'string' },
description: '字幕语言列表',
default: ['zh-CN', 'en']
}
},
required: ['url', 'outputPath']
}
},
{
name: 'get_video_info',
description: '获取视频信息(不下载)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: '视频链接URL'
}
},
required: ['url']
}
},
{
name: 'get_task_status',
description: '获取下载任务状态',
inputSchema: {
type: 'object',
properties: {
taskId: {
type: 'string',
description: '任务ID'
}
},
required: ['taskId']
}
},
{
name: 'cancel_task',
description: '取消下载任务',
inputSchema: {
type: 'object',
properties: {
taskId: {
type: 'string',
description: '任务ID'
}
},
required: ['taskId']
}
},
{
name: 'list_tasks',
description: '列出所有下载任务',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'extract_video_urls',
description: '从网页中提取视频链接',
inputSchema: {
type: 'object',
properties: {
pageUrl: {
type: 'string',
description: '网页URL'
}
},
required: ['pageUrl']
}
}
]
};
});
// 处理工具调用
this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'download_video':
return await this.handleDownloadVideo(args);
case 'get_video_info':
return await this.handleGetVideoInfo(args);
case 'get_task_status':
return await this.handleGetTaskStatus(args);
case 'cancel_task':
return await this.handleCancelTask(args);
case 'list_tasks':
return await this.handleListTasks(args);
case 'extract_video_urls':
return await this.handleExtractVideoUrls(args);
default:
throw new Error(`未知工具: ${name}`);
}
} catch (error) {
logger.error(`工具调用失败 ${name}:`, error);
return {
content: [
{
type: 'text',
text: `错误: ${error instanceof Error ? error.message : '未知错误'}`
}
],
isError: true
};
}
});
}
/**
* 处理视频下载请求
*/
private async handleDownloadVideo(args: any) {
// 验证参数
const schema = Joi.object({
url: Joi.string().uri().required(),
outputPath: Joi.string().required(),
quality: Joi.string().valid('best', 'worst', 'bestvideo', 'bestaudio').default('best'),
outputTemplate: Joi.string().default('%(title)s.%(ext)s'),
extractAudio: Joi.boolean().default(false),
audioFormat: Joi.string().valid('mp3', 'aac', 'wav', 'flac').default('mp3'),
downloadSubtitles: Joi.boolean().default(false),
subtitleLangs: Joi.array().items(Joi.string()).default(['zh-CN', 'en'])
});
const { error, value } = schema.validate(args);
if (error) {
throw new Error(`参数验证失败: ${error.message}`);
}
const request: VideoDownloadRequest = value;
const taskId = await this.ytdlpService.downloadVideo(request);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
taskId,
message: '下载任务已创建',
request
}, null, 2)
}
]
};
}
/**
* 处理获取视频信息请求
*/
private async handleGetVideoInfo(args: any) {
const schema = Joi.object({
url: Joi.string().uri().required()
});
const { error, value } = schema.validate(args);
if (error) {
throw new Error(`参数验证失败: ${error.message}`);
}
const videoInfo = await this.ytdlpService.getVideoInfo(value.url);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
videoInfo
}, null, 2)
}
]
};
}
/**
* 处理获取任务状态请求
*/
private async handleGetTaskStatus(args: any) {
const schema = Joi.object({
taskId: Joi.string().required()
});
const { error, value } = schema.validate(args);
if (error) {
throw new Error(`参数验证失败: ${error.message}`);
}
const task = this.ytdlpService.getTaskStatus(value.taskId);
if (!task) {
throw new Error('任务不存在');
}
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
task
}, null, 2)
}
]
};
}
/**
* 处理取消任务请求
*/
private async handleCancelTask(args: any) {
const schema = Joi.object({
taskId: Joi.string().required()
});
const { error, value } = schema.validate(args);
if (error) {
throw new Error(`参数验证失败: ${error.message}`);
}
const success = this.ytdlpService.cancelTask(value.taskId);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success,
message: success ? '任务已取消' : '任务不存在或无法取消'
}, null, 2)
}
]
};
}
/**
* 处理列出所有任务请求
*/
private async handleListTasks(args: any) {
const tasks = this.ytdlpService.getAllTasks();
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
tasks,
count: tasks.length
}, null, 2)
}
]
};
}
/**
* 处理从网页提取视频链接请求
*/
private async handleExtractVideoUrls(args: any) {
const schema = Joi.object({
pageUrl: Joi.string().uri().required()
});
const { error, value } = schema.validate(args);
if (error) {
throw new Error(`参数验证失败: ${error.message}`);
}
const videoUrls = await this.ytdlpService.extractVideoUrls(value.pageUrl);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
pageUrl: value.pageUrl,
videoUrls,
count: videoUrls.length
}, null, 2)
}
]
};
}
/**
* 启动 MCP 服务器
*/
async start(): Promise<void> {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
// MCP服务器启动后不应该输出日志到stdout,因为会干扰MCP通信
// logger.info(`MCP 视频下载服务器已启动 (${config.mcp.name} v${config.mcp.version})`);
// 定期清理过期任务
const cleanupInterval = setInterval(() => {
this.ytdlpService.cleanupExpiredTasks();
}, 60000); // 每分钟清理一次
// 优雅关闭时清理定时器
process.on('SIGINT', () => {
clearInterval(cleanupInterval);
process.exit(0);
});
process.on('SIGTERM', () => {
clearInterval(cleanupInterval);
process.exit(0);
});
} catch (error) {
// 错误信息写入stderr,不影响MCP通信
console.error('MCP服务器启动失败:', error);
process.exit(1);
}
}
}
export default MCPVideoDownloaderServer;