@mcpcn/mcp-screenshot
Version:
截图MCP服务器
368 lines (363 loc) • 12.9 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import { stat } from 'fs/promises';
import { join } from 'path';
export const execAsync = promisify(exec);
/**
* Escapes special characters in strings for AppleScript
*/
export function escapeString(str) {
// Escape for both AppleScript and shell
return str
.replace(/'/g, "'\\''")
.replace(/"/g, '\\"');
}
// 检测操作系统
function getOS() {
const currentPlatform = process.platform;
if (currentPlatform === 'win32')
return 'windows';
if (currentPlatform === 'darwin')
return 'macos';
return 'linux';
}
/**
* Validates screenshot parameters
*/
function validateScreenshotParams(params) {
if (!params.path || typeof params.path !== 'string') {
throw new Error('Path is required and must be a string');
}
if (!params.type || !['fullscreen', 'window', 'selection'].includes(params.type)) {
throw new Error('Type must be one of: fullscreen, window, selection');
}
if (params.format && !['png', 'jpg', 'jpeg', 'pdf', 'tiff'].includes(params.format)) {
throw new Error('Format must be one of: png, jpg, jpeg, pdf, tiff');
}
if (params.hideCursor !== undefined && typeof params.hideCursor !== 'boolean') {
throw new Error('HideCursor must be a boolean');
}
if (params.timestamp !== undefined && typeof params.timestamp !== 'boolean') {
throw new Error('Timestamp must be a boolean');
}
}
/**
* Builds the screencapture command for macOS
*/
function buildMacOSScreenshotCommand(params) {
let command = 'screencapture';
// Screenshot type
switch (params.type) {
case 'window':
command += ' -w'; // Capture window
break;
case 'selection':
command += ' -s'; // Interactive selection
break;
// fullscreen is default, no flag needed
}
// Optional flags
if (params.format) {
command += ` -t ${params.format}`;
}
if (params.hideCursor) {
command += ' -C'; // Hide cursor
}
command += ` "${escapeString(params.path)}"`;
return command;
}
/**
* Builds the PowerShell screenshot command for Windows
*/
function buildWindowsScreenshotCommand(params) {
const { path, type, format = 'png' } = params;
// 映射格式
const formatMap = {
'png': 'Png',
'jpg': 'Jpeg',
'jpeg': 'Jpeg',
'bmp': 'Bmp',
'gif': 'Gif'
};
const imageFormat = formatMap[format.toLowerCase()] || 'Png';
if (type === 'selection') {
// Windows 没有直接的区域选择,使用 Snipping Tool
return `start ms-screenclip:`;
}
// PowerShell 截图脚本
const script = `
Add-Type -AssemblyName System.Windows.Forms;
Add-Type -AssemblyName System.Drawing;
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen;
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height;
$graphics = [System.Drawing.Graphics]::FromImage($bitmap);
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size);
$bitmap.Save('${escapeString(path)}', [System.Drawing.Imaging.ImageFormat]::${imageFormat});
$graphics.Dispose();
$bitmap.Dispose();
`;
return `powershell -Command "${script.replace(/\n\s+/g, ' ')}"`;
}
/**
* Builds the screenshot command for Linux
*/
function buildLinuxScreenshotCommand(params) {
const { path, type, format = 'png', hideCursor } = params;
// 首选方案:gnome-screenshot (GNOME 桌面默认有)
// 备选方案:scrot, import (ImageMagick), xwd
const gnomeCommand = buildGnomeScreenshotCommand(params, path);
const scrotCommand = buildScrotCommand(params, path);
const importCommand = buildImageMagickCommand(params, path);
const xwdCommand = buildXwdCommand(params, path);
// 按优先级尝试不同工具
return `bash -c '
if command -v gnome-screenshot >/dev/null 2>&1; then
${gnomeCommand}
elif command -v scrot >/dev/null 2>&1; then
${scrotCommand}
elif command -v import >/dev/null 2>&1; then
${importCommand}
elif command -v xwd >/dev/null 2>&1; then
${xwdCommand}
else
echo "No screenshot tool available. Please install gnome-screenshot, scrot, or imagemagick" >&2
exit 1
fi
'`;
}
/**
* Build gnome-screenshot command
*/
function buildGnomeScreenshotCommand(params, path) {
let cmd = 'gnome-screenshot';
switch (params.type) {
case 'window':
cmd += ' --window';
break;
case 'selection':
cmd += ' --area';
break;
// fullscreen is default
}
cmd += ` --file="${escapeString(path)}"`;
return cmd;
}
/**
* Build scrot command
*/
function buildScrotCommand(params, path) {
let cmd = 'scrot';
switch (params.type) {
case 'window':
cmd += ' --focused';
break;
case 'selection':
cmd += ' --select';
break;
// fullscreen is default
}
if (params.hideCursor) {
cmd += ' --pointer';
}
cmd += ` "${escapeString(path)}"`;
return cmd;
}
/**
* Build ImageMagick import command
*/
function buildImageMagickCommand(params, path) {
let cmd = 'import';
switch (params.type) {
case 'window':
cmd += ' -window root';
break;
case 'selection':
// import 默认就是区域选择
break;
case 'fullscreen':
cmd += ' -window root';
break;
}
cmd += ` "${escapeString(path)}"`;
return cmd;
}
/**
* Build xwd command (X Window Dump)
*/
function buildXwdCommand(params, path) {
let cmd = 'xwd';
switch (params.type) {
case 'window':
cmd += ' -id $(xdotool getwindowfocus)';
break;
case 'selection':
// xwd 默认点击选择窗口
break;
case 'fullscreen':
cmd += ' -root';
break;
}
// xwd 输出需要转换为常见格式
const tempFile = `/tmp/screenshot_${Date.now()}.xwd`;
cmd += ` -out "${tempFile}"`;
// 转换为目标格式 (需要 ImageMagick)
cmd += ` && convert "${tempFile}" "${escapeString(path)}" && rm "${tempFile}"`;
return cmd;
}
/**
* Takes a screenshot using platform-appropriate tools
*/
export async function takeScreenshot(params) {
try {
validateScreenshotParams(params);
// 检查path是否是目录,如果是则生成时间戳文件名
let finalPath = params.path;
try {
const pathStat = await stat(params.path);
if (pathStat.isDirectory()) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const format = params.format || 'png';
const filename = `screenshot-${timestamp}.${format}`;
finalPath = join(params.path, filename);
}
}
catch (error) {
// 如果路径不存在,保持原样(可能是要创建新文件)
}
// 使用最终路径创建新的参数对象
const finalParams = { ...params, path: finalPath };
const os = getOS();
let command;
switch (os) {
case 'macos':
command = buildMacOSScreenshotCommand(finalParams);
break;
case 'windows':
command = buildWindowsScreenshotCommand(finalParams);
break;
case 'linux':
command = buildLinuxScreenshotCommand(finalParams);
break;
default:
throw new Error(`Unsupported platform: ${os}`);
}
await execAsync(command);
return finalPath;
}
catch (error) {
const err = error;
if (err.message.includes('execution error')) {
throw new Error('Failed to capture screenshot');
}
else if (err.message.includes('permission')) {
throw new Error('Permission denied when trying to capture screenshot');
}
else {
throw new Error(`Unexpected error: ${err.message}`);
}
}
}
class ScreenshotServer {
constructor() {
this.server = new Server({
name: 'screenshot-mcp',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'take_screenshot',
description: '使用系统原生工具截图',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: '保存截图的路径,可以是完整的文件路径,也可以是目录路径(会自动生成时间戳文件名)',
},
type: {
type: 'string',
enum: ['fullscreen', 'window', 'selection'],
description: '截图类型,fullscreen表示全屏截图,window表示截取指定窗口,selection表示选区截图',
},
format: {
type: 'string',
enum: ['png', 'jpg', 'pdf', 'tiff'],
description: '图片格式',
},
hideCursor: {
type: 'boolean',
description: '是否隐藏鼠标光标',
},
timestamp: {
type: 'boolean',
description: '是否在文件名添加时间戳',
}
},
required: ['path', 'type'],
additionalProperties: false,
},
},
],
}));
// Handle tool execution
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments || typeof request.params.arguments !== 'object') {
throw new McpError(ErrorCode.InvalidParams, 'Invalid parameters');
}
switch (request.params.name) {
case 'take_screenshot': {
const { path, type, format, hideCursor, timestamp } = request.params.arguments;
const params = {
path: path,
type: type,
format: format,
hideCursor: typeof hideCursor === 'boolean' ? hideCursor : undefined,
timestamp: typeof timestamp === 'boolean' ? timestamp : undefined
};
const savePath = await takeScreenshot(params);
return {
content: [
{
type: 'text',
text: `截图保存成功,保存在${savePath}`,
},
],
};
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
}
catch (error) {
throw error;
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Screenshot MCP server running on stdio');
}
}
const server = new ScreenshotServer();
server.run().catch(console.error);