rhombus-node-mcp
Version:
MCP server for Rhombus API
126 lines (125 loc) • 5.56 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import path from "path";
import { createTracingProxy } from "./telemetry/tracingProxy.js";
import { resolveAccessibleApps } from "./api/get-accessible-apps.js";
import { logger } from "./logger.js";
import { createFilteringProxy } from "./util.js";
import getResources from "./resources/getResources.js";
import { getConsoleTools, getPartnerTools, getSharedTools, } from "./tools/getTools.js";
import { RhombusAppEnum } from "./types/schema.js";
let initiated = false;
let resources;
let sharedTools;
let consoleTools;
let partnerTools;
export async function serverInit() {
resources = await getResources();
logger.info(`📚 Found ${resources.length} resources`);
for (const resource of resources) {
logger.debug(`📕 - ${resource.name}`);
}
sharedTools = await getSharedTools();
consoleTools = await getConsoleTools();
partnerTools = await getPartnerTools();
logger.info(`🛠️ Found ${sharedTools.length} shared, ${consoleTools.length} console, ${partnerTools.length} partner tools`);
for (const tool of sharedTools)
logger.debug(`🔧 shared - ${tool.name}`);
for (const tool of consoleTools)
logger.debug(`🔧 console - ${tool.name}`);
for (const tool of partnerTools)
logger.debug(`🔧 partner - ${tool.name}`);
initiated = true;
}
/**
* Compose the tool set to register based on the caller's accessibleRhombusApps.
* Shared tools are always included.
*
* If **PARTNER** is among the caller's apps (including alongside CONSOLE), only
* **partner** tools are added — never the console-only set (partner capability
* is stricter).
*
* Else if **CONSOLE** is present, add **console** tools only.
*
* Otherwise (unresolved session, empty apps, or only other enums like
* RHOMBUS_KEY), fall back to the permissive union of console + partner sets.
*/
function pickToolsForSession(apps) {
if (apps !== null && apps.length > 0 && apps.includes(RhombusAppEnum.PARTNER)) {
return [...sharedTools, ...partnerTools];
}
if (apps !== null && apps.length > 0 && apps.includes(RhombusAppEnum.CONSOLE)) {
return [...sharedTools, ...consoleTools];
}
return [...sharedTools, ...consoleTools, ...partnerTools];
}
function isNodeDevEnvironment() {
return process.env.NODE_ENV === "development";
}
/**
* Human-readable caller + tool-set summary for dev logs (must stay aligned with
* {@link pickToolsForSession}).
*/
function describeCallerForDevLogs(apps) {
if (apps === null) {
return "caller=unknown (no session or unresolved apps); tool sets=shared + console + partner (permissive)";
}
if (apps.length === 0) {
return "caller=unknown (empty accessibleRhombusApps); tool sets=shared + console + partner (permissive)";
}
if (apps.includes(RhombusAppEnum.PARTNER)) {
const alsoConsole = apps.includes(RhombusAppEnum.CONSOLE);
return alsoConsole
? `caller=partner (PARTNER in [${apps.join(", ")}], console tools suppressed); tool sets=shared + partner`
: "caller=partner user; tool sets=shared + partner";
}
if (apps.includes(RhombusAppEnum.CONSOLE)) {
return "caller=console user; tool sets=shared + console";
}
return `caller=other (accessibleRhombusApps=[${apps.join(", ")}]); tool sets=shared + console + partner (permissive)`;
}
function logDevToolRegistration(sessionId, apps, toolsToRegister) {
if (!isNodeDevEnvironment())
return;
const names = toolsToRegister.map((t) => path.basename(t.name, ".js")).sort();
logger.info(`[dev MCP tools] session=${sessionId ?? "(none)"} — ${describeCallerForDevLogs(apps)} — registering ${names.length} tools`);
logger.info(`[dev MCP tools] tool names: ${names.join(", ")}`);
}
export default async function createServer({ sessionId } = {}) {
if (!initiated) {
await serverInit();
}
logger.info(`🖥️ Creating Server for session ${sessionId ?? "(stateless)"}`);
const server = new McpServer({
name: "rhombus-node-mcp",
version: "1.0.0",
}, {
capabilities: {
...(resources.length > 0 ? { resources: {} } : {}),
tools: {},
},
});
for (const resource of resources) {
resource.create(server);
}
logger.info(`📚 Registered ${resources.length} resources`);
const apps = await resolveAccessibleApps(sessionId);
const toolsToRegister = pickToolsForSession(apps);
logDevToolRegistration(sessionId, apps, toolsToRegister);
logger.info(`🔒 Session ${sessionId ?? "(none)"}: apps=[${apps?.join(", ") ?? "unknown"}] — registering ${toolsToRegister.length} tools`);
// Tracing wraps every tool handler; filtering wraps on top so handlers are
// timed after includeFields/filterBy are stripped (keeping them out of
// mcp.tool.arg_keys). Spans are no-ops unless a host registers an OTel SDK.
const filteredServer = createFilteringProxy(createTracingProxy(server), new Set(["time-tool", "count-tool", "time-conversion-tool"]));
for (const tool of toolsToRegister) {
try {
await tool.create(filteredServer);
}
catch (error) {
logger.error(`Failed to register tool ${tool.name}:`, error);
// Continue with other tools instead of failing completely
}
}
logger.info(`🛠️ Registered ${toolsToRegister.length} tools`);
logger.info(`✅ Server created`);
return server;
}