Skip to main content
Glama
clipboard.ts10.3 kB
import { z } from 'zod'; import { exec } from 'child_process'; import { promisify } from 'util'; import os from 'os'; import fs from 'fs'; import path from 'path'; import { logAudit } from '../audit.js'; import { loadConfig } from '../config.js'; const execAsync = promisify(exec); const platform = os.platform(); const config = loadConfig(); // Schema definitions export const ClipboardReadSchema = { format: z.enum(['text', 'html', 'image']).optional().describe('Format to read (default: text)'), }; export const ClipboardWriteSchema = { content: z.string().describe('Content to write to clipboard'), format: z.enum(['text', 'html']).optional().describe('Format to write (default: text)'), }; export const ClipboardClearSchema = {}; export const ClipboardHasFormatSchema = { format: z.enum(['text', 'html', 'image', 'files']).describe('Format to check for'), }; // Tool handlers export async function handleClipboardRead(args: { format?: 'text' | 'html' | 'image' }) { try { const format = args.format || 'text'; const outputConfig = config.cliOutput ?? { maxOutputChars: 50000, warnAtChars: 10000, truncateMode: 'both' as const }; let content: string | null = null; if (platform === 'win32') { if (format === 'text') { const { stdout } = await execAsync('powershell -Command "Get-Clipboard"', { timeout: 5000 }); content = stdout.trim(); } else if (format === 'html') { const script = ` Add-Type -AssemblyName System.Windows.Forms $data = [System.Windows.Forms.Clipboard]::GetDataObject() if ($data.GetDataPresent([System.Windows.Forms.DataFormats]::Html)) { $data.GetData([System.Windows.Forms.DataFormats]::Html) } `; const { stdout } = await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { timeout: 5000 }); content = stdout.trim(); } else if (format === 'image') { const tempPath = path.join(os.tmpdir(), `clipboard_${Date.now()}.png`); const script = ` Add-Type -AssemblyName System.Windows.Forms $img = [System.Windows.Forms.Clipboard]::GetImage() if ($img) { $img.Save('${tempPath.replace(/\\/g, '\\\\')}') Write-Output '${tempPath.replace(/\\/g, '\\\\')}' } `; const { stdout } = await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { timeout: 5000 }); if (stdout.trim()) { const imageBuffer = fs.readFileSync(tempPath); content = imageBuffer.toString('base64'); fs.unlinkSync(tempPath); } } } else if (platform === 'darwin') { if (format === 'text') { const { stdout } = await execAsync('pbpaste', { timeout: 5000 }); content = stdout; } else if (format === 'html') { const { stdout } = await execAsync('osascript -e "the clipboard as «class HTML»"', { timeout: 5000 }); content = stdout; } } else { // Linux - use xclip or xsel if (format === 'text') { try { const { stdout } = await execAsync('xclip -selection clipboard -o', { timeout: 5000 }); content = stdout; } catch { const { stdout } = await execAsync('xsel --clipboard --output', { timeout: 5000 }); content = stdout; } } } // Apply truncation to text/html content (not images) let truncated = false; let originalLength = content?.length || 0; if (content && format !== 'image' && content.length > outputConfig.maxOutputChars) { originalLength = content.length; content = content.slice(0, outputConfig.maxOutputChars); content += `\n\n⚠️ CLIPBOARD TRUNCATED: Showing first ${outputConfig.maxOutputChars.toLocaleString()} of ${originalLength.toLocaleString()} characters.`; truncated = true; } await logAudit('clipboard_read', args, content ? `success${truncated ? ' (truncated)' : ''}` : 'empty'); return { content: [{ type: 'text', text: JSON.stringify({ format, hasContent: !!content, content: content || null, length: originalLength, truncated: truncated ? { originalLength, returnedLength: content?.length } : undefined }, null, 2) }], }; } catch (error: any) { await logAudit('clipboard_read', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } } export async function handleClipboardWrite(args: { content: string; format?: 'text' | 'html' }) { try { const format = args.format || 'text'; if (platform === 'win32') { if (format === 'text') { // Use stdin to handle special characters const escaped = args.content.replace(/"/g, '`"').replace(/\$/g, '`$'); await execAsync(`powershell -Command "Set-Clipboard -Value '${escaped.replace(/'/g, "''")}'"`); } else if (format === 'html') { const script = ` Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.Clipboard]::SetText('${args.content.replace(/'/g, "''")}', [System.Windows.Forms.TextDataFormat]::Html) `; await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { timeout: 5000 }); } } else if (platform === 'darwin') { // Write to temp file to handle special characters const tempFile = path.join(os.tmpdir(), `clipboard_${Date.now()}.txt`); fs.writeFileSync(tempFile, args.content); await execAsync(`cat "${tempFile}" | pbcopy`, { timeout: 5000 }); fs.unlinkSync(tempFile); } else { // Linux const tempFile = path.join(os.tmpdir(), `clipboard_${Date.now()}.txt`); fs.writeFileSync(tempFile, args.content); try { await execAsync(`cat "${tempFile}" | xclip -selection clipboard`, { timeout: 5000 }); } catch { await execAsync(`cat "${tempFile}" | xsel --clipboard --input`, { timeout: 5000 }); } fs.unlinkSync(tempFile); } await logAudit('clipboard_write', { format, length: args.content.length }, 'success'); return { content: [{ type: 'text', text: JSON.stringify({ written: true, format, length: args.content.length }, null, 2) }], }; } catch (error: any) { await logAudit('clipboard_write', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } } export async function handleClipboardClear() { try { if (platform === 'win32') { await execAsync('powershell -Command "Set-Clipboard -Value $null"', { timeout: 5000 }); } else if (platform === 'darwin') { await execAsync('pbcopy < /dev/null', { timeout: 5000 }); } else { try { await execAsync('xclip -selection clipboard < /dev/null', { timeout: 5000 }); } catch { await execAsync('xsel --clipboard --clear', { timeout: 5000 }); } } await logAudit('clipboard_clear', {}, 'success'); return { content: [{ type: 'text', text: JSON.stringify({ cleared: true }, null, 2) }], }; } catch (error: any) { await logAudit('clipboard_clear', {}, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } } export async function handleClipboardHasFormat(args: { format: 'text' | 'html' | 'image' | 'files' }) { try { let hasFormat = false; if (platform === 'win32') { const formatMap: Record<string, string> = { 'text': 'Text', 'html': 'Html', 'image': 'Bitmap', 'files': 'FileDrop', }; const script = ` Add-Type -AssemblyName System.Windows.Forms $data = [System.Windows.Forms.Clipboard]::GetDataObject() $data.GetDataPresent([System.Windows.Forms.DataFormats]::${formatMap[args.format]}) `; const { stdout } = await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { timeout: 5000 }); hasFormat = stdout.trim().toLowerCase() === 'true'; } else { // Simplified check for other platforms if (args.format === 'text') { try { const result = await handleClipboardRead({ format: 'text' }); hasFormat = !!(result.content[0] as any).text?.includes('"hasContent": true'); } catch { hasFormat = false; } } } await logAudit('clipboard_has_format', args, hasFormat); return { content: [{ type: 'text', text: JSON.stringify({ format: args.format, available: hasFormat }, null, 2) }], }; } catch (error: any) { await logAudit('clipboard_has_format', args, null, error.message); return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true, }; } }

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/Mnehmos/mnehmos.ooda.mcp'

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