diff --git a/README.md b/README.md index a702dc5..7799cfe 100644 --- a/README.md +++ b/README.md @@ -264,3 +264,25 @@ Parse the model from /info ```bash bsc raw -a=true -i=192.168.128.101 -p=ABC01A000001 -m=GET -r="info" | jq '.data.result.model' ``` + +## MCP Agent + +This package now includes a simple [Model Context Protocol](https://modelcontextprotocol.io) server. The agent exposes a few CLI commands as MCP tools so that other applications can trigger them. + +Start the server: + +```bash +npx bsc-agent +``` + +The agent currently provides tools to list players, fetch device info and reboot a player. + +## Interactive Menu + +Run an interactive menu for common tasks: + +```bash +npx bsc-menu +``` + +The menu lets you add players, pick a configured player and then run commands such as checking device status or rebooting. diff --git a/interactive-cli.mjs b/interactive-cli.mjs new file mode 100644 index 0000000..414d190 --- /dev/null +++ b/interactive-cli.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import inquirer from 'inquirer'; +import handlers from './bin/handlerFunctions.js'; + +const CONFIG_FILE_PATH = path.join(os.homedir(), '.bsc', 'players.json'); + +function readPlayers() { + try { + return JSON.parse(fs.readFileSync(CONFIG_FILE_PATH, 'utf8')); + } catch { + return {}; + } +} + +async function mainMenu() { + const answer = await inquirer.prompt([ + { + type: 'list', + name: 'action', + message: 'Select an option', + choices: ['List players', 'Add player', 'Use player', 'Exit'], + }, + ]); + switch (answer.action) { + case 'List players': + await handlers.listPlayers(); + return mainMenu(); + case 'Add player': + await addPlayerFlow(); + return mainMenu(); + case 'Use player': + await selectPlayer(); + return mainMenu(); + default: + console.log('Goodbye!'); + } +} + +async function addPlayerFlow() { + const answers = await inquirer.prompt([ + { type: 'input', name: 'playerName', message: 'Player name:' }, + { type: 'input', name: 'ipAddress', message: 'IP address or hostname:' }, + { type: 'input', name: 'username', message: 'Username:', default: 'admin' }, + { type: 'password', name: 'password', message: 'Password:' }, + { type: 'input', name: 'storage', message: 'Storage location:', default: 'sd' }, + ]); + await handlers.addPlayer({ ...answers, verbose: false }); + console.log(`Player ${answers.playerName} added.`); +} + +async function selectPlayer() { + const players = readPlayers(); + const names = Object.keys(players); + if (names.length === 0) { + console.log('No players configured. Add one first.'); + return; + } + const { playerName } = await inquirer.prompt([ + { type: 'list', name: 'playerName', message: 'Choose a player', choices: names }, + ]); + await playerMenu(playerName); +} + +async function playerMenu(playerName) { + const { action } = await inquirer.prompt([ + { + type: 'list', + name: 'action', + message: `Actions for ${playerName}`, + choices: ['Check device status', 'Reboot player', 'Back'], + }, + ]); + if (action === 'Check device status') { + await handlers.getDeviceInfo({ playerName, verbose: false, rawdata: false }); + return playerMenu(playerName); + } else if (action === 'Reboot player') { + await handlers.reboot({ playerName, verbose: false, rawdata: false }); + return playerMenu(playerName); + } +} + +mainMenu(); diff --git a/mcp-server.mjs b/mcp-server.mjs new file mode 100644 index 0000000..d636371 --- /dev/null +++ b/mcp-server.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import handlers from "./bin/handlerFunctions.js"; + +const server = new McpServer({ + name: "bsc-agent", + version: "1.0.0", +}); + +server.registerTool( + "listPlayers", + { + title: "List Players", + description: "List configured players", + inputSchema: {}, + }, + async () => { + await handlers.listPlayers(); + return { content: [{ type: "text", text: "Listed players" }] }; + } +); + +server.registerTool( + "getDeviceInfo", + { + title: "Get Device Info", + description: "Get device info for a player", + inputSchema: { playerName: z.string() }, + }, + async ({ playerName }) => { + await handlers.getDeviceInfo({ playerName, verbose: false, rawdata: true }); + return { content: [{ type: "text", text: "Fetched device info" }] }; + } +); + +server.registerTool( + "rebootPlayer", + { + title: "Reboot Player", + description: "Reboot a BrightSign player", + inputSchema: { playerName: z.string() }, + }, + async ({ playerName }) => { + await handlers.reboot({ playerName, verbose: false, rawdata: true }); + return { content: [{ type: "text", text: "Player rebooted" }] }; + } +); + +const transport = new StdioServerTransport(); +await server.connect(transport); + diff --git a/package.json b/package.json index 7d73c57..1d13d28 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,19 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { - "bsc": "./bin/index.js" + "bsc": "./bin/index.js", + "bsc-agent": "./mcp-server.mjs", + "bsc-menu": "./interactive-cli.mjs" }, "author": "support@brightsign.biz", "license": "ISC", "dependencies": { + "@modelcontextprotocol/sdk": "^1.13.0", "digest-fetch": "^2.0.3", "form-data": "^4.0.0", "http-status": "^1.6.2", "node-fetch": "^2.6.12", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "inquirer": "^9.2.16" } }