@mcpcn/mcp-file-select
Version:
文件选择对话框MCP服务器
264 lines (263 loc) • 10 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 { platform } from 'os';
import { promisify } from 'util';
import { exec } from 'child_process';
const execAsync = promisify(exec);
/**
* Escapes special characters in strings for AppleScript
*/
function escapeString(str) {
// Escape for both AppleScript and shell
return str
.replace(/'/g, "'\\''")
.replace(/"/g, '\\"');
}
/**
* Validates file selection parameters
*/
function validateFileSelectParams(params) {
if (params.prompt && typeof params.prompt !== 'string') {
throw new Error('Prompt must be a string');
}
if (params.defaultLocation && typeof params.defaultLocation !== 'string') {
throw new Error('Default location must be a string');
}
if (params.multiple !== undefined && typeof params.multiple !== 'boolean') {
throw new Error('Multiple selection flag must be a boolean');
}
if (params.fileTypes && params.fileTypes.length > 0) {
if (!Array.isArray(params.fileTypes)) {
throw new Error('File types must be an array');
}
for (const fileType of params.fileTypes) {
if (typeof fileType !== 'string') {
throw new Error('File type must be a string');
}
}
}
}
function buildMacOSCommand(params) {
let script = 'choose file';
if (params.multiple)
script += ' with multiple selections allowed';
if (params.prompt)
script += ` with prompt "${escapeString(params.prompt)}"`;
if (params.defaultLocation)
script += ` default location "${escapeString(params.defaultLocation)}"`;
if (params.fileTypes) {
const extensions = Object.values(params.fileTypes).flat();
if (extensions.length > 0) {
script += ` of type {${extensions.map(ext => `"${ext}"`).join(', ')}}`;
}
}
// 处理多选和单选的路径转换
if (params.multiple) {
script = `set fileList to ${script}
set pathString to ""
repeat with aFile in fileList
if pathString is equal to "" then
set pathString to POSIX path of aFile
else
set pathString to pathString & "," & POSIX path of aFile
end if
end repeat
return pathString`;
}
else {
script = `POSIX path of (${script})`;
}
return `osascript -e '${script}'`;
}
function buildWindowsCommand(params) {
const psScript = `
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.OpenFileDialog
${params.prompt ? `$dialog.Title = "${escapeString(params.prompt)}"` : ''}
${params.multiple ? '$dialog.Multiselect = $true' : ''}
${params.defaultLocation ? `$dialog.InitialDirectory = "${escapeString(params.defaultLocation)}"` : ''}
$dialog.ShowDialog() | Out-Null
$dialog.FileNames -join ","
`;
return `powershell -Command "${psScript.replace(/\n\s+/g, ' ')}"`;
}
// 新增:检查并安装 zenity
async function ensureZenityInstalled() {
try {
// 检查 zenity 是否已安装
await execAsync('which zenity || zenity --version').catch(() => { });
}
catch {
try {
// 根据发行版选择包管理器
const { stdout: distro } = await execAsync('grep ^ID= /etc/os-release | cut -d= -f2');
const pkgManager = distro.trim() === 'debian' || distro.trim() === 'ubuntu' ? 'apt' : 'dnf';
await execAsync(`sudo ${pkgManager} install -y zenity`);
console.error('zenity 安装成功!');
}
catch (installError) {
throw new Error(`自动安装 zenity 失败,请手动运行以下命令安装:\n` +
` Ubuntu/Debian: sudo apt install zenity\n` +
` Fedora/RHEL: sudo dnf install zenity`);
}
}
}
// 修改 Linux 命令构建逻辑
async function buildLinuxCommand(params) {
await ensureZenityInstalled(); // 确保 zenity 存在
let cmd = 'zenity --file-selection';
if (params.multiple)
cmd += ' --multiple --separator=","';
if (params.prompt)
cmd += ` --title="${escapeString(params.prompt)}"`;
if (params.defaultLocation)
cmd += ` --filename="${escapeString(params.defaultLocation)}"`;
return cmd;
}
/**
* Prompts user to select file(s) using native file picker
*/
async function selectFile(params) {
validateFileSelectParams(params);
let command;
const os = platform();
switch (os) {
case 'darwin':
command = buildMacOSCommand(params);
break;
case 'win32':
command = buildWindowsCommand(params);
break;
case 'linux':
command = await buildLinuxCommand(params); // 注意改为异步
break;
default:
throw new Error(`Unsupported platform: ${os}`);
}
try {
const { stdout } = await execAsync(command);
const paths = stdout
.trim()
.split(',')
.map(path => path.trim())
.filter(path => path.length > 0);
return {
paths,
message: `成功选择了 ${paths.length} 个文件`
};
}
catch (error) {
const err = error;
// 检查是否为用户取消操作
if (err.message.includes('用户已取消') ||
err.message.includes('User canceled') ||
err.message.includes('cancelled') ||
err.message.includes('cancel') ||
err.message.includes('closed') ||
err.message.includes('-128')) {
return {
paths: [],
cancelled: true,
message: '用户取消了文件选择'
};
}
throw new Error(`Failed to select file: ${err.message}`);
}
}
class FileSelectServer {
constructor() {
this.server = new Server({
name: 'file-select-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: 'select_file',
description: '打开文件选择对话框,当需要操作一个文件或目录,却不知道具体路径时,可以调用该工具让用户选择',
inputSchema: {
type: 'object',
properties: {
prompt: {
type: 'string',
description: '可选的提示消息'
},
defaultLocation: {
type: 'string',
description: '可选的默认目录路径'
},
fileTypes: {
type: 'array',
items: {
type: 'string'
},
description: '可选的文件类型过滤器 (例如: ["png", "jpg"])'
},
multiple: {
type: 'boolean',
description: '是否允许多选'
}
},
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 'select_file': {
const { prompt, defaultLocation, fileTypes, multiple } = request.params.arguments;
const params = {
prompt: typeof prompt === 'string' ? prompt : undefined,
defaultLocation: typeof defaultLocation === 'string' ? defaultLocation : undefined,
fileTypes: Array.isArray(fileTypes) ? fileTypes : undefined,
multiple: typeof multiple === 'boolean' ? multiple : undefined
};
const result = await selectFile(params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
};
}
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('File Select MCP server running on stdio');
}
}
const server = new FileSelectServer();
server.run().catch(console.error);