@monitoro/herd
Version:
Automate your browser, build AI web tools and MCP servers with Monitoro Herd
167 lines (166 loc) • 7.43 kB
JavaScript
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();
}
});
});
}
}
}