UNPKG

@monitoro/herd

Version:

Automate your browser, build AI web tools and MCP servers with Monitoro Herd

167 lines (166 loc) 7.43 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { HerdClient } from "./HerdClient.js"; import express from "express"; export class HerdMcpServer { constructor(options) { this.devices = []; this.server = new McpServer(options.info, options.mcp); this.transportConfig = options.transport?.type === "sse" ? { type: "sse", port: options.transport.port || 3000, path: options.transport.path || "/messages" } : { type: "stdio" }; // Initialize transport based on options if (this.transportConfig.type === "sse") { this.expressApp = express(); // Enable CORS this.expressApp.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); // Allow all origins res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // Allow specific methods res.header("Access-Control-Allow-Headers", "Content-Type"); // Allow specific headers next(); }); // Set up the express routes this.expressApp.get("/sse", async (req, res) => { this.sseTransport = new SSEServerTransport(this.transportConfig.path, res); await this.server.connect(this.sseTransport); }); this.expressApp.post(this.transportConfig.path, async (req, res) => { if (this.sseTransport) { await this.sseTransport.handlePostMessage(req, res); } else { res.status(500).send("SSE transport not initialized"); } }); } else { // Default to stdio if not specified or if "stdio" is explicitly set this.transport = new StdioServerTransport(); } this.herd = new HerdClient({ token: options.herd?.token || process.env.HERD_TOKEN || "", baseUrl: options.herd?.baseUrl || "https://herd.garden" }); } async initializeDevices() { await this.herd.initialize(); this.devices = await this.herd.listDevices(); } resource(options, callback) { // Create a wrapper that will inject devices const wrapCallback = (callback) => { return async (...args) => { // Replace the last argument (extra) with devices const argsWithoutExtra = args.slice(0, -1); const value = await callback(...argsWithoutExtra, this.devices); return value; }; }; if (options.metadata) { // Case with metadata this.server.resource(options.name, options.uriOrTemplate, options.metadata, wrapCallback(callback)); } else { // Case without metadata this.server.resource(options.name, options.uriOrTemplate, wrapCallback(callback)); } } tool(options, callback) { // Create a wrapper that will inject devices const wrapCallback = (callback) => { return async (...args) => { // Replace the last argument (extra) with devices const argsWithoutExtra = args.slice(0, -1); const value = await callback(...argsWithoutExtra, this.devices); // const value = { content: [{ type: "text", text: "oh yeah" }] } as any; return value; }; }; // if (!options.description && !options.schema) { // // Zero-argument tool without description // this.server.tool(options.name, wrapCallback(callback)); // } else if (options.description && !options.schema) { // // Zero-argument tool with description // this.server.tool(options.name, options.description, wrapCallback(callback)); // } else if (!options.description && options.schema) { // // Tool with params schema but no description // this.server.tool(options.name, options.schema, wrapCallback(callback)); // } else if (options.description && options.schema) { // Tool with description, params schema, and callback this.server.tool(options.name, options.description || options.name, options.schema || {}, wrapCallback(callback)); // } } prompt(options, callback) { // Create a wrapper that will inject devices const wrapCallback = (callback) => { return async (...args) => { // Replace the last argument (extra) with devices const argsWithoutExtra = args.slice(0, -1); const value = await callback(...argsWithoutExtra, this.devices); return value; }; }; if (!options.description && !options.schema) { // Zero-argument prompt without description this.server.prompt(options.name, wrapCallback(callback)); } else if (options.description && !options.schema) { // Zero-argument prompt with description this.server.prompt(options.name, options.description, wrapCallback(callback)); } else if (!options.description && options.schema) { // Prompt with args schema but no description this.server.prompt(options.name, options.schema, wrapCallback(callback)); } else if (options.description && options.schema) { // Prompt with description, args schema, and callback this.server.prompt(options.name, options.description, options.schema, wrapCallback(callback)); } } async start() { try { await this.initializeDevices(); console.error(`Found ${this.devices.length} devices`); // Start the appropriate transport based on configuration if (this.transportConfig.type === "sse" && this.expressApp) { // Start the express server for SSE transport const port = this.transportConfig.port; this.httpServer = this.expressApp.listen(port, () => { console.error(`SSE server listening on port ${port}`); }); } else if (this.transport) { // For stdio transport, connect to the server await this.server.connect(this.transport); } } catch (error) { console.error("Failed to start Herd MCP server:", error); process.exit(1); } } async stop() { // Close the server connection await this.server.close(); // If using SSE, close the HTTP server if (this.httpServer) { return new Promise((resolve, reject) => { this.httpServer.close((err) => { if (err) { // console.error("Error closing HTTP server:", err); reject(err); } else { // console.log("HTTP server closed"); resolve(); } }); }); } } }