supOS MCP Server

#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import fetch from "node-fetch"; import { z } from "zod"; import fs, { readFileSync } from "fs"; import _ from "lodash"; import mqtt from "mqtt"; import { pathToFileURL } from "url"; import { createFilePath } from "./utils.js"; let SUPOS_API_URL = process.env.SUPOS_API_URL || "http://office.unibutton.com:11488/"; let SUPOS_API_KEY = process.env.SUPOS_API_KEY || "4174348a-9222-4e81-b33e-5d72d2fd7f1e"; let SUPOS_MQTT_URL = process.env.SUPOS_MQTT_URL || "tcp://office.unibutton.com:11483/mqtt"; // Command line argument parsing if (process.argv.length >= 5) { const args = process.argv.slice(-3); if (args.length !== 0) { SUPOS_API_URL = args?.[0]; SUPOS_API_KEY = args?.[1]; SUPOS_MQTT_URL = args?.[2]; } } if (!SUPOS_API_URL) { console.error("SUPOS_API_URL environment variable is not set"); process.exit(1); } if (!SUPOS_API_KEY) { console.error("SUPOS_API_KEY environment variable is not set"); process.exit(1); } const filePath = createFilePath(); const fileUri = pathToFileURL(filePath).href; async function getModelTopicDetail(topic: string): Promise<any> { const url = `${SUPOS_API_URL}/open-api/supos/uns/model?topic=${encodeURIComponent( topic )}`; const response = await fetch(url, { headers: { apiKey: `${SUPOS_API_KEY}`, }, }); if (!response.ok) { throw new Error(`SupOS API error: ${response.statusText}`); } return await response.json(); } async function getModelTopicTree(params: { key?: string; showRec?: string; type?: string; }): Promise<any> { let searchParams = {}; if (params) { searchParams = _.pickBy(params, !_.isNil); } const paramsUrl = new URLSearchParams(""); _.forOwn(searchParams, function (value, key) { paramsUrl.append(key, encodeURIComponent(value)); }); const url = `${SUPOS_API_URL}/open-api/supos/uns/tree?${paramsUrl.toString()}`; const response = await fetch(url, { headers: { apiKey: `${SUPOS_API_KEY}`, }, }); if (!response.ok) { throw new Error(`SupOS API error: ${response.statusText}`); } return await response.json(); } function getAllTopicRealtimeData() { // 缓存实时数据,定时写入缓存文件 const cache = new Map(); let timer: any = null; const options = { clean: true, connectTimeout: 4000, clientId: "emqx_topic_all", rejectUnauthorized: false, reconnectPeriod: 0, // 不进行重连 }; const connectUrl = SUPOS_MQTT_URL; if (!connectUrl) { return; } const client = mqtt.connect(connectUrl, options); client.on("connect", function () { client.subscribe("#", function (err) { // console.log("err", err); }); }); client.on("message", function (topic, message) { cache.set(topic, message.toString()); }); client.on("error", function (error) { // console.log("error", error); }); client.on("close", function () { if (timer) { clearInterval(timer); } }); // 每 5 秒批量写入一次 timer = setInterval(() => { const cacheJson = JSON.stringify( Object.fromEntries(Array.from(cache)), null, 2 ); // 将更新后的数据写入 JSON 文件 fs.writeFile( filePath, cacheJson, { encoding: "utf-8", }, (error) => { if (error) { fs.writeFile( filePath, JSON.stringify({ msg: "写入数据失败" }, null, 2), { encoding: "utf-8" }, () => {} ); } } ); }, 5000); } function getTopicRealtimeData(subscribeTopic: string) { return new Promise((resolve) => { const options = { clean: true, connectTimeout: 4000, clientId: "emqx_topic_single", rejectUnauthorized: false, reconnectPeriod: 0, // 不进行重连 }; const connectUrl = SUPOS_MQTT_URL; if (!connectUrl) { resolve(`订阅${subscribeTopic}失败`); return; } const client = mqtt.connect(connectUrl, options); client.on("connect", function () { client.subscribe(`${subscribeTopic}`, function (err) { if (err) { resolve(`订阅${subscribeTopic}失败`); } }); }); client.on("message", function (topic, message) { // console.log({ topic, message: message.toString() }); resolve(message.toString()); client.end(); }); client.on("error", function (error) { // console.log("error", error); resolve(`MQTT 错误: ${error.message}`); client.end(); }); client.on("close", function () { // console.log("close"); resolve(`订阅${subscribeTopic}失败`); }); setTimeout(() => { resolve(`订阅超时: ${subscribeTopic}可能不存在或者其他异常`); client.end(); }, 30000); }); } function createMcpServer() { const server = new McpServer( { name: "mcp-server-supos", version: "0.0.1", }, { capabilities: { tools: {}, }, } ); // Static resource server.resource("all-topic-realtime-data", fileUri, async (uri) => ({ contents: [ { uri: uri.href, text: readFileSync(filePath, { encoding: "utf-8" }), }, ], })); server.tool( "get-model-topic-tree", { key: z .string() .optional() .describe("Fuzzy search keyword for child nodes"), showRec: z.boolean().optional().describe("Is show recommend topic"), type: z .string() .optional() .describe("Search type: 1--Text search, 2--Tag search"), }, async (args: any) => { const trees = await getModelTopicTree({ key: _.isNil(args.key) ? "" : `${args.key}`, showRec: _.isNil(args.showRec) ? "false" : `${args.showRec}`, type: _.isNil(args.type) ? "1" : `${args.type}`, }); return { content: [{ type: "text", text: JSON.stringify(trees) }], }; } ); server.tool( "get-model-topic-detail", { topic: z.string() }, async (args: any) => { const detail = await getModelTopicDetail(args.topic); return { content: [{ type: "text", text: `${JSON.stringify(detail)}` }], }; } ); server.tool("get-all-topic-realtime-data", {}, async () => { return { content: [ { type: "text", text: readFileSync(filePath, { encoding: "utf-8" }), }, ], }; }); server.tool( "get-topic-realtime-data", { topic: z.string() }, async (args: any) => { const realtimeData = await getTopicRealtimeData(`${args.topic}`); return { content: [{ type: "text", text: `${realtimeData}` }], }; } ); async function runServer() { const transport = new StdioServerTransport(); const serverConnect = await server.connect(transport); console.error("SupOS MCP Server running on stdio"); return serverConnect; } runServer().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); } async function main() { try { createMcpServer(); getAllTopicRealtimeData(); } catch (error) { console.error("Error in main():", error); process.exit(1); } } main();