Schwertlilien
As a recoder: notes and ideas.

2025-8-18

uv是什么?

uv 是一个现代的 Python 包管理器和构建工具,由 Astral 公司开发(该公司还开发了知名的 ruff 代码检查工具),旨在提供比传统 pip 更快的依赖管理体验。

uv 可以理解为「增强版的 pip + venv + pip-tools」,核心功能包括:

  1. 依赖管理:安装、升级、卸载 Python 包(类似 pip),但速度极快(比 pip 快 10-100 倍)。
  2. 虚拟环境管理:创建、激活虚拟环境(类似 python -m venv),命令更简洁(uv venv 替代 python -m venv .venv)。
  3. 项目初始化:通过 uv init 创建标准化的 Python 项目结构(生成 pyproject.toml 等配置文件)。
  4. 依赖锁定:自动生成 uv.lock 文件,精确记录依赖版本,确保环境一致性(类似 pip freeze 但更高效)。

简单说,uv 是一个「一站式工具」,用统一的命令解决 Python 项目的环境管理和依赖安装问题,主打速度快、体验统一

uv为什么能/要创造虚拟环境?

虚拟环境是 Python 开发的基础需求(隔离不同项目的依赖),传统上需要用 python -m venv 或第三方工具(如 virtualenv)。
uv 内置了虚拟环境功能,原因是:

  • 简化工作流:无需记住 python -m venv 这种冗长命令,用 uv venv 即可一键创建,与依赖管理命令(uv add)无缝衔接。
  • 提升效率:uv 的虚拟环境创建速度比 python -m venv 更快,且默认配置更合理(如自动使用当前 Python 版本)。

本质上,uv 创建的虚拟环境与 python -m venv 完全兼容,只是操作更简便。

与Conda的区别?

conda 相比,uv 更轻量、更专注 Python,而 conda 适合复杂的跨语言环境。uv创建虚拟环境是为了简化工作流,与传统 venv 兼容;集「虚拟环境 + 依赖管理」于一身。

维度 uv conda
定位 专注于 Python 包和虚拟环境管理 跨语言的包管理器(支持 Python、C++ 等)+ 环境管理工具
依赖来源 仅从 PyPI 安装 Python 包 从 Anaconda 仓库安装,支持非 Python 包(如 C 库、CUDA 等)
环境隔离级别 基于 Python 解释器的轻量级隔离 全环境隔离(包括系统级依赖),隔离更彻底但体积更大
速度 极快(用 Rust 编写,优化了依赖解析) 相对较慢(尤其解决依赖冲突时)
适用场景 纯 Python 项目、快速迭代的开发场景 多语言项目、需要系统级依赖(如数据科学、机器学习)
配置复杂度 简单(依赖 pyproject.toml 标准配置) 较复杂(有自己的配置体系和频道概念)

uvx?

1
uvx black --check my_script.py

uvx 会自动下载 black 到临时目录,执行命令后不会残留依赖,不污染当前项目环境。uvxuv 工具集中的一个子命令,专门用于临时运行 Python 包中的可执行程序,而无需手动安装包到当前环境

方式 特点 适用场景
uv add <package> 安装包到当前环境,长期可用 项目核心依赖、需要反复使用的工具
uvx <package> 临时下载并运行,不安装到环境 一次性使用、试用工具、避免污染环境

uvx典型使用场景

  1. 试用工具:快速验证某个包的功能,比如用 uvx pip-audit 临时检查项目依赖的安全漏洞,用完即走。
  2. 避免环境污染:对于偶尔使用的工具(如 pyright 类型检查、pdoc 文档生成),不需要长期安装在项目依赖中,用 uvx 临时调用更干净。
  3. 指定版本运行:可以通过 uvx <package>==<version> 强制使用特定版本,例如 uvx black==23.1.0 确保用旧版本格式化代码。
  4. 运行脚本:甚至可以直接执行远程脚本,例如 uvx https://example.com/script.py(会自动处理依赖)。

My MCP Server

方案一:同一个 Server 下的多个 Tools

适合场景:想做一个完整的视频处理流水线(下载 → 分离音频 → 语音转写),放在同一个 server。

  • 做法:把 download_videosplit_video_audiotranscribe_audio 都放在一个 MCP server 里,用同一个 FastMCP("video_pipeline") 实例。
  • 优点
    • 使用方便:只需要启动一个 server,所有功能都能调用;
    • 工具之间的数据流(下载 → 分离 → 转写)更连贯;
    • 管理简单,部署维护成本低。
  • 缺点
    • 如果某个功能出问题(例如 Whisper 模型加载太慢),可能影响整个 server 的可用性;
    • Server 会比较“臃肿”。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { exec, ExecOptions as ChildProcessExecOptions } from "child_process";
import { promisify } from "util";
import * as path from "path";
import * as fs from "fs/promises";
import * as os from "node:os";
import notifier from "node-notifier";

const execAsync = promisify(exec);

// Create an MCP server
const server = new McpServer({
name: "FFmpegProcessor",
version: "1.0.0"
});

// Define available resolutions
const RESOLUTIONS = {
"360p": { width: 640, height: 360 },
"480p": { width: 854, height: 480 },
"720p": { width: 1280, height: 720 },
"1080p": { width: 1920, height: 1080 }
};

/**
* Helper function to ask for permission using node-notifier
*/
async function askPermission(action: string): Promise<boolean> {
// Skip notification if DISABLE_NOTIFICATIONS is set
if (process.env.DISABLE_NOTIFICATIONS === 'true') {
console.log(`Auto-allowing action (notifications disabled): ${action}`);
return true;
}

return new Promise((resolve) => {
notifier.notify({
title: 'FFmpeg Processor Permission Request',
message: `${action}`,
wait: true,
timeout: 60,
actions: 'Allow',
closeLabel: 'Deny'
}, (err, response, metadata) => {
if (err) {
console.error('Error showing notification:', err);
resolve(false);
return;
}

const buttonPressed = metadata?.activationValue || response;
resolve(buttonPressed !== 'Deny');
});
});
}

/**
* Helper function to ensure output directories exist
*/
async function ensureDirectoriesExist() {
const outputDir = path.join(os.tmpdir(), 'ffmpeg-output');
try {
await fs.mkdir(outputDir, { recursive: true });
return outputDir;
} catch (error) {
console.error('Error creating output directory:', error);
return os.tmpdir();
}
}

// Tool to check FFmpeg version
server.tool(
"get-ffmpeg-version",
"Get the version of FFmpeg installed on the system",
{},
async () => {
try {
const { stdout, stderr } = await execAsync('ffmpeg -version');

// Extract the version from the output
const versionMatch = stdout.match(/ffmpeg version (\S+)/);
const version = versionMatch ? versionMatch[1] : 'Unknown';

return {
content: [{
type: "text" as const,
text: `FFmpeg Version: ${version}\n\nFull version info:\n${stdout}`
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
isError: true,
content: [{
type: "text" as const,
text: `Error getting FFmpeg version: ${errorMessage}\n\nMake sure FFmpeg is installed and in your PATH.`
}]
};
}
}
);

// Tool to resize video
server.tool(
"resize-video",
"Resize a video to one or more standard resolutions",
{
videoPath: z.string().describe("Path to the video file to resize"),
resolutions: z.array(z.enum(["360p", "480p", "720p", "1080p"])).describe("Resolutions to convert the video to"),
outputDir: z.string().optional().describe("Optional directory to save the output files (defaults to a temporary directory)")
},
async ({ videoPath, resolutions, outputDir }) => {
try {
// Resolve the absolute path
const absVideoPath = path.resolve(videoPath);

// Check if file exists
try {
await fs.access(absVideoPath);
} catch (error) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Error: Video file not found at ${absVideoPath}`
}]
};
}

// Determine output directory
let outputDirectory = outputDir ? path.resolve(outputDir) : await ensureDirectoriesExist();

// Check if output directory exists and is writable
try {
await fs.access(outputDirectory, fs.constants.W_OK);
} catch (error) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Error: Output directory ${outputDirectory} does not exist or is not writable`
}]
};
}

// Format command for permission request
const resolutionsStr = resolutions.join(', ');
const permissionMessage = `Resize video ${path.basename(absVideoPath)} to ${resolutionsStr}`;

// Ask for permission
const permitted = await askPermission(permissionMessage);

if (!permitted) {
return {
isError: true,
content: [{
type: "text" as const,
text: "Permission denied by user"
}]
};
}

// Get video filename without extension
const videoFilename = path.basename(absVideoPath, path.extname(absVideoPath));

// Define the type for our results
type ResizeResult = {
resolution: "360p" | "480p" | "720p" | "1080p";
outputPath: string;
success: boolean;
error?: string;
};

// Process each resolution
const results: ResizeResult[] = [];

for (const resolution of resolutions) {
const { width, height } = RESOLUTIONS[resolution as keyof typeof RESOLUTIONS];
const outputFilename = `${videoFilename}_${resolution}${path.extname(absVideoPath)}`;
const outputPath = path.join(outputDirectory, outputFilename);

// Build FFmpeg command
const command = `ffmpeg -i "${absVideoPath}" -vf "scale=${width}:${height}" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k "${outputPath}"`;

try {
// Execute FFmpeg command
const { stdout, stderr } = await execAsync(command);

results.push({
resolution,
outputPath,
success: true
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);

results.push({
resolution,
outputPath,
success: false,
error: errorMessage
});
}
}

// Format results
const successCount = results.filter(r => r.success).length;
const failCount = results.length - successCount;

let resultText = `Processed ${results.length} resolutions (${successCount} successful, ${failCount} failed)\n\n`;

results.forEach(result => {
if (result.success) {
resultText += `✅ ${result.resolution}: ${result.outputPath}\n`;
} else {
resultText += `❌ ${result.resolution}: Failed - ${result.error}\n`;
}
});

return {
content: [{
type: "text" as const,
text: resultText
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
isError: true,
content: [{
type: "text" as const,
text: `Error resizing video: ${errorMessage}`
}]
};
}
}
);

// Tool to extract audio from video
server.tool(
"extract-audio",
"Extract audio from a video file",
{
videoPath: z.string().describe("Path to the video file to extract audio from"),
format: z.enum(["mp3", "aac", "wav", "ogg"]).default("mp3").describe("Audio format to extract"),
outputDir: z.string().optional().describe("Optional directory to save the output file (defaults to a temporary directory)")
},
async ({ videoPath, format, outputDir }) => {
try {
// Resolve the absolute path
const absVideoPath = path.resolve(videoPath);

// Check if file exists
try {
await fs.access(absVideoPath);
} catch (error) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Error: Video file not found at ${absVideoPath}`
}]
};
}

// Determine output directory
let outputDirectory = outputDir ? path.resolve(outputDir) : await ensureDirectoriesExist();

// Check if output directory exists and is writable
try {
await fs.access(outputDirectory, fs.constants.W_OK);
} catch (error) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Error: Output directory ${outputDirectory} does not exist or is not writable`
}]
};
}

// Format command for permission request
const permissionMessage = `Extract ${format} audio from video ${path.basename(absVideoPath)}`;

// Ask for permission
const permitted = await askPermission(permissionMessage);

if (!permitted) {
return {
isError: true,
content: [{
type: "text" as const,
text: "Permission denied by user"
}]
};
}

// Get video filename without extension
const videoFilename = path.basename(absVideoPath, path.extname(absVideoPath));
const outputFilename = `${videoFilename}.${format}`;
const outputPath = path.join(outputDirectory, outputFilename);

// Determine audio codec based on format
let audioCodec;
switch (format) {
case 'mp3':
audioCodec = 'libmp3lame';
break;
case 'aac':
audioCodec = 'aac';
break;
case 'wav':
audioCodec = 'pcm_s16le';
break;
case 'ogg':
audioCodec = 'libvorbis';
break;
default:
audioCodec = 'libmp3lame';
}

// Build FFmpeg command
const command = `ffmpeg -i "${absVideoPath}" -vn -acodec ${audioCodec} "${outputPath}"`;

try {
// Execute FFmpeg command
const { stdout, stderr } = await execAsync(command);

return {
content: [{
type: "text" as const,
text: `Successfully extracted audio to: ${outputPath}`
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
isError: true,
content: [{
type: "text" as const,
text: `Error extracting audio: ${errorMessage}`
}]
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
isError: true,
content: [{
type: "text" as const,
text: `Error extracting audio: ${errorMessage}`
}]
};
}
}
);

// Tool to get video information
server.tool(
"get-video-info",
"Get detailed information about a video file",
{
videoPath: z.string().describe("Path to the video file to analyze")
},
async ({ videoPath }) => {
try {
// Resolve the absolute path
const absVideoPath = path.resolve(videoPath);

// Check if file exists
try {
await fs.access(absVideoPath);
} catch (error) {
return {
isError: true,
content: [{
type: "text" as const,
text: `Error: Video file not found at ${absVideoPath}`
}]
};
}

// Format command for permission request
const permissionMessage = `Analyze video file ${path.basename(absVideoPath)}`;

// Ask for permission
const permitted = await askPermission(permissionMessage);

if (!permitted) {
return {
isError: true,
content: [{
type: "text" as const,
text: "Permission denied by user"
}]
};
}

// Build FFprobe command to get video information in JSON format
const command = `ffprobe -v quiet -print_format json -show_format -show_streams "${absVideoPath}"`;

// Execute FFprobe command
const { stdout, stderr } = await execAsync(command);

// Parse the JSON output
const videoInfo = JSON.parse(stdout);

// Format the output in a readable way
let formattedInfo = `Video Information for: ${path.basename(absVideoPath)}\n\n`;

// Format information
if (videoInfo.format) {
formattedInfo += `Format: ${videoInfo.format.format_name}\n`;
formattedInfo += `Duration: ${videoInfo.format.duration} seconds\n`;
formattedInfo += `Size: ${(parseInt(videoInfo.format.size) / (1024 * 1024)).toFixed(2)} MB\n`;
formattedInfo += `Bitrate: ${(parseInt(videoInfo.format.bit_rate) / 1000).toFixed(2)} kbps\n\n`;
}

// Stream information
if (videoInfo.streams && videoInfo.streams.length > 0) {
formattedInfo += `Streams:\n`;

videoInfo.streams.forEach((stream: any, index: number) => {
formattedInfo += `\nStream #${index} (${stream.codec_type}):\n`;

if (stream.codec_type === 'video') {
formattedInfo += ` Codec: ${stream.codec_name}\n`;
formattedInfo += ` Resolution: ${stream.width}x${stream.height}\n`;
formattedInfo += ` Frame rate: ${stream.r_frame_rate}\n`;
if (stream.bit_rate) {
formattedInfo += ` Bitrate: ${(parseInt(stream.bit_rate) / 1000).toFixed(2)} kbps\n`;
}
} else if (stream.codec_type === 'audio') {
formattedInfo += ` Codec: ${stream.codec_name}\n`;
formattedInfo += ` Sample rate: ${stream.sample_rate} Hz\n`;
formattedInfo += ` Channels: ${stream.channels}\n`;
if (stream.bit_rate) {
formattedInfo += ` Bitrate: ${(parseInt(stream.bit_rate) / 1000).toFixed(2)} kbps\n`;
}
}
});
}

return {
content: [{
type: "text" as const,
text: formattedInfo
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
isError: true,
content: [{
type: "text" as const,
text: `Error getting video information: ${errorMessage}`
}]
};
}
}
);

// Start the server
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("FFmpeg MCP Server running");
} catch (error) {
console.error("Error starting server:", error);
process.exit(1);
}
}

main();
搜索
匹配结果数:
未搜索到匹配的文章。