@topvisor/mcp-notifications
Advanced tools
| import fs from 'node:fs' | ||
| import os from 'node:os' | ||
| import path from 'node:path' | ||
| import notifier from 'node-notifier' | ||
| import {normalizeIconPath} from './utils.mjs' | ||
| const payloadBase64 = process.argv[2] || '' | ||
| if (!payloadBase64) process.exit(1) | ||
| let job | ||
| try { | ||
| job = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8')) | ||
| } catch { | ||
| process.exit(1) | ||
| } | ||
| const icon = prepareIconPowerShell(job.icon) | ||
| notifier.notify( | ||
| { | ||
| title: job.title, | ||
| message: job.message, | ||
| wait: job.wait, | ||
| sound: Boolean(job.sound), | ||
| icon, | ||
| appID: job.appID, | ||
| }, | ||
| (error) => process.exit(error ? 1 : 0), | ||
| ) | ||
| function prepareIconPowerShell(iconPath) { | ||
| if (!iconPath) return undefined | ||
| const resolved = normalizeIconPath(iconPath) | ||
| if (!fs.existsSync(resolved)) return undefined | ||
| if (!resolved.startsWith('\\\\wsl.localhost\\')) return resolved | ||
| const ext = path.extname(resolved) || '.png' | ||
| const target = path.join(os.tmpdir(), `mcp-notifications-icon${ext}`) | ||
| fs.copyFileSync(resolved, target) | ||
| return target | ||
| } |
| import path from 'node:path' | ||
| // Преобразует пути WSL (/mnt/c/...) в Windows-формат (C:\...). | ||
| export const normalizeIconPath = (iconPath) => { | ||
| const resolved = path.resolve(iconPath) | ||
| const mntMatch = resolved.match(/^\/mnt\/([a-zA-Z])\/(.*)$/) | ||
| if (mntMatch) { | ||
| const drive = mntMatch[1].toUpperCase() | ||
| const rest = mntMatch[2].replace(/\//g, '\\') | ||
| return `${drive}:\\${rest}` | ||
| } | ||
| return resolved | ||
| } | ||
| // Определяет запуск в WSL, чтобы выбрать отправку через PowerShell-обёртку. | ||
| export const isWsl = () => | ||
| process.platform === 'linux' && | ||
| (Boolean(process.env.WSL_DISTRO_NAME) || process.env.WSL_INTEROP !== undefined) |
+6
-1
| { | ||
| "name": "@topvisor/mcp-notifications", | ||
| "version": "1.0.3", | ||
| "version": "1.0.4", | ||
| "mcpName": "io.github.topvisor/mcp-notifications", | ||
@@ -35,2 +35,3 @@ "type": "module", | ||
| "scripts": { | ||
| "test": "vitest run", | ||
| "start": "node ./src/server.mjs", | ||
@@ -47,3 +48,7 @@ "start:bin": "node ./bin/mcp-notifications.mjs" | ||
| "zod": "^3.24.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^20.19.41", | ||
| "vitest": "^4.1.6" | ||
| } | ||
| } |
+9
-0
@@ -148,2 +148,11 @@ # mcp-notifications | ||
| ## Backend selection | ||
| You can select notification transport via env `MCP_NOTIFICATIONS_BACKEND`: | ||
| - `auto` (default): uses `powershell` in WSL if available, then `wsl-notify-send`, otherwise `node-notifier` | ||
| - `powershell`: force `powershell.exe` Windows toast transport (recommended for WSL) | ||
| - `wsl-notify-send`: force `wsl-notify-send` command | ||
| - `node-notifier`: force `node-notifier` package | ||
| ## Chat Prompts To Test In Codex | ||
@@ -150,0 +159,0 @@ |
+56
-47
@@ -1,61 +0,70 @@ | ||
| import notifier from 'node-notifier'; | ||
| import path from 'path'; | ||
| import { fileURLToPath } from 'url'; | ||
| import {spawn, spawnSync} from 'node:child_process' | ||
| import path from 'node:path' | ||
| import {fileURLToPath} from 'node:url' | ||
| import notifier from 'node-notifier' | ||
| import {isWsl, normalizeIconPath} from './utils.mjs' | ||
| const jobs = []; | ||
| let isProcessing = false; | ||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = path.dirname(__filename); | ||
| const DEFAULT_ICON_PATH = path.resolve(__dirname, '../assets/topvisor-robot.png'); | ||
| const jobs = [] | ||
| let isProcessing = false | ||
| const __filename = fileURLToPath(import.meta.url) | ||
| const __dirname = path.dirname(__filename) | ||
| const DEFAULT_ICON_PATH = path.resolve(__dirname, '../assets/topvisor-robot.png') | ||
| const WINDOWS_WRAPPER_PATH = path.resolve(__dirname, './send-via-powershell.mjs') | ||
| /** | ||
| * Добавить уведомление в очередь фоновой отправки. | ||
| * Tool-вызов не ждёт завершения системного API уведомлений. | ||
| */ | ||
| export const enqueueNotification = ({ title, message, playSound, icon, appId }) => { | ||
| export const enqueueNotification = ({title, message, sound, icon, appID}) => { | ||
| jobs.push({ | ||
| title, | ||
| message, | ||
| playSound, | ||
| icon: normalizeIcon(icon), | ||
| appId, | ||
| }); | ||
| processQueue(); | ||
| }; | ||
| sound, | ||
| icon: normalizeIconPath(icon || DEFAULT_ICON_PATH), | ||
| wait: false, | ||
| appID, | ||
| }) | ||
| processQueue() | ||
| } | ||
| const processQueue = () => { | ||
| if (isProcessing) return; | ||
| if (isProcessing) return | ||
| const job = jobs.shift() | ||
| if (!job) return | ||
| const job = jobs.shift(); | ||
| if (!job) return; | ||
| isProcessing = true | ||
| sendNotification(job, (error) => { | ||
| isProcessing = false | ||
| if (error) { | ||
| console.error('[notify] Ошибка отправки уведомления:', error.message) | ||
| } | ||
| setImmediate(processQueue) | ||
| }) | ||
| } | ||
| isProcessing = true; | ||
| const sendNotification = (job, done) => { | ||
| if (isWsl()) { | ||
| sendViaPowershellWrapper(job, done) | ||
| return | ||
| } | ||
| notifier.notify( | ||
| { | ||
| title: job.title, | ||
| message: job.message, | ||
| wait: false, | ||
| sound: job.playSound, | ||
| icon: job.icon, | ||
| appID: job.appId, | ||
| }, | ||
| (error) => { | ||
| isProcessing = false; | ||
| notifier.notify(job, (error) => done(error ?? null)) | ||
| } | ||
| if (error) { | ||
| console.error('[notify] Ошибка отправки уведомления:', error.message); | ||
| } | ||
| const sendViaPowershellWrapper = (job, done) => { | ||
| const wrapperWindowsPath = toWindowsPath(WINDOWS_WRAPPER_PATH) | ||
| if (!wrapperWindowsPath) { | ||
| done(new Error('Cannot resolve Windows path for notifier wrapper')) | ||
| return | ||
| } | ||
| setImmediate(processQueue); | ||
| }, | ||
| ); | ||
| }; | ||
| const payload = Buffer.from(JSON.stringify(job), 'utf8').toString('base64') | ||
| const command = `node "${wrapperWindowsPath}" "${payload}"` | ||
| const child = spawn('powershell.exe', ['-NoProfile', '-Command', command], {stdio: 'ignore'}) | ||
| child.on('error', (error) => done(error)) | ||
| child.on('exit', (code) => done(code === 0 ? null : new Error(`powershell exited with code ${code}`))) | ||
| } | ||
| const normalizeIcon = (icon) => { | ||
| if (!icon) { | ||
| return DEFAULT_ICON_PATH; | ||
| const toWindowsPath = (linuxPath) => { | ||
| const converted = spawnSync('wslpath', ['-w', linuxPath], {encoding: 'utf8'}) | ||
| if (converted.status !== 0) { | ||
| return null | ||
| } | ||
| return path.resolve(icon); | ||
| }; | ||
| return converted.stdout.trim() || null | ||
| } |
+4
-4
@@ -21,4 +21,4 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; | ||
| async ({ title, message, play_sound, icon, app_id }) => { | ||
| const playSound = play_sound ?? false; | ||
| const appId = app_id ?? process.env.MCP_NOTIFICATIONS_APP_ID; | ||
| const sound = play_sound ?? false; | ||
| const appID = app_id ?? process.env.MCP_NOTIFICATIONS_APP_ID; | ||
@@ -28,5 +28,5 @@ enqueueNotification({ | ||
| message, | ||
| playSound, | ||
| sound, | ||
| icon, | ||
| appId, | ||
| appID, | ||
| }); | ||
@@ -33,0 +33,0 @@ |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
304780
0.78%10
25%153
66.3%0
-100%199
4.74%2
Infinity%4
300%1
Infinity%