#!/usr/bin/env node
/**
* CalcsLive MCP Server
* Enables AI agents to perform unit-aware engineering calculations
* via Model Context Protocol (MCP)
*/
import { config } from "dotenv";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { existsSync } from "fs";
import { resolve } from "path";
// Load environment variables from .env file only if it exists and API key not already set
const envPath = resolve(process.cwd(), ".env");
if (!process.env.CALCSLIVE_API_KEY && existsSync(envPath)) {
config({ path: envPath });
}
const CALCSLIVE_API_BASE = process.env.CALCSLIVE_API_URL || "https://www.calcs.live";
const API_KEY = process.env.CALCSLIVE_API_KEY;
if (!API_KEY) {
console.error("Error: CALCSLIVE_API_KEY environment variable is required");
console.error("Please set your CalcsLive MCP API key:");
console.error(" export CALCSLIVE_API_KEY=your_api_key_here");
process.exit(1);
}
const server = new Server(
{
name: "calcslive-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
/**
* List available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "calcslive_calculate",
description: "Perform unit-aware engineering calculations using CalcsLive articles. Automatically handles unit conversions and dependency calculations. Example: Calculate hydro power with flow rate 150 m³/s and head 25m. Returns calculated outputs with values and units.",
inputSchema: {
type: "object",
properties: {
articleId: {
type: "string",
description: "Article Short ID (e.g., 'pump-calc-abc'). Use calcslive_validate to discover available articles."
},
inputs: {
type: "object",
description: "Input PQ values with units. Example: {velocity: {value: 25, unit: 'm/s'}, mass: {value: 1500, unit: 'kg'}}",
additionalProperties: {
type: "object",
properties: {
value: {
type: "number",
description: "Numeric value for this input"
},
unit: {
type: "string",
description: "Unit for this value (e.g., 'm/s', 'kg', 'kW'). Supports both superscript (m³) and caret (m^3) notation."
}
},
required: ["value", "unit"]
}
},
outputs: {
type: "object",
description: "Optional output unit preferences. Example: {distance: {unit: 'km'}}",
additionalProperties: {
type: "object",
properties: {
unit: {
type: "string",
description: "Desired output unit (optional)"
}
}
}
}
},
required: ["articleId", "inputs"]
}
},
{
name: "calcslive_run_script",
description: "Run stateless unit-aware calculations from Physical Quantity (PQ) script definitions. Define inputs and outputs as PQ objects with symbols, values, units, and expressions. No article creation needed - fully stateless. Automatically handles unit conversions, dependency graphs, and Greek letters. Example: Calculate circle area from radius using expression 'pi * r^2'.",
inputSchema: {
type: "object",
properties: {
pqs: {
type: "array",
description: "Array of Physical Quantity definitions. Each PQ can be an input (with value) or output (with expression).",
items: {
type: "object",
properties: {
sym: {
type: "string",
description: "Symbol name for this PQ (e.g., 'r', 'v', 'A'). Supports Greek letters (α, β, η, ρ, etc.)."
},
description: {
type: "string",
description: "Human-readable description (e.g., 'radius', 'velocity', 'area')"
},
value: {
type: "number",
description: "Numeric value for input PQs. Omit for calculated outputs."
},
unit: {
type: "string",
description: "Unit (e.g., 'm', 'kg', 'kW'). Supports superscript (m³) and caret (m^3) notation."
},
expression: {
type: "string",
description: "Mathematical expression for calculated PQs (e.g., 'pi * r^2', 'v / t'). References other PQ symbols."
},
calcId: {
type: "string",
description: "Optional namespace for multi-calculation contexts (default: 'CA0')"
},
decimalPlaces: {
type: "number",
description: "Decimal places for display (default: 3)"
}
},
required: ["sym", "unit"]
},
minItems: 1
},
inputs: {
type: "object",
description: "Optional: Override input PQ values. Example: {r: {value: 5, unit: 'cm'}}",
additionalProperties: {
type: "object",
properties: {
value: { type: "number" },
unit: { type: "string" }
},
required: ["value", "unit"]
}
},
outputs: {
type: "object",
description: "Optional: Specify output unit preferences. Example: {A: {unit: 'mm²'}}",
additionalProperties: {
type: "object",
properties: {
unit: { type: "string" }
}
}
}
},
required: ["pqs"]
}
},
{
name: "calcslive_validate",
description: "Discover available inputs and outputs for a calculation article. Use this before calculate to understand what parameters are available and their units. Returns article metadata including all input/output PQs with descriptions, units, and available unit options.",
inputSchema: {
type: "object",
properties: {
articleId: {
type: "string",
description: "Article Short ID to validate (e.g., 'hydro-power-calc')"
}
},
required: ["articleId"]
}
}
]
};
});
/**
* Execute tool calls
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calcslive_calculate") {
const { articleId, inputs, outputs = {} } = request.params.arguments as any;
try {
const response = await fetch(`${CALCSLIVE_API_BASE}/api/v1/calculate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
articleId,
apiKey: API_KEY,
inputs,
outputs
}),
});
if (!response.ok) {
const errorData = await response.json() as any;
throw new Error(errorData.error?.message || `API error: ${response.status}`);
}
const result = await response.json() as any;
// Format response for MCP
return {
content: [
{
type: "text",
text: JSON.stringify(result.data.calculation, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error performing calculation: ${error.message}`,
},
],
isError: true,
};
}
}
if (request.params.name === "calcslive_run_script") {
const { pqs, inputs = {}, outputs = {} } = request.params.arguments as any;
try {
const response = await fetch(`${CALCSLIVE_API_BASE}/api/v1/articles/uac-script/run`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
apiKey: API_KEY,
pqs,
inputs,
outputs
}),
});
if (!response.ok) {
const errorData = await response.json() as any;
throw new Error(errorData.error?.message || `API error: ${response.status}`);
}
const result = await response.json() as any;
// Format response for MCP - use humanReadable for better AI consumption
const humanReadable = result.data.humanReadable;
const calculation = result.data.calculation;
return {
content: [
{
type: "text",
text: `${humanReadable.summary}\n\nResults:\n${humanReadable.results.map((r: any) =>
`• ${r.description} (${r.symbol}): ${r.result}${r.expression ? ` = ${r.expression}` : ''}`
).join('\n')}\n\nDetailed calculation:\n${JSON.stringify(calculation, null, 2)}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error running script calculation: ${error.message}`,
},
],
isError: true,
};
}
}
if (request.params.name === "calcslive_validate") {
const { articleId } = request.params.arguments as any;
try {
const response = await fetch(
`${CALCSLIVE_API_BASE}/api/v1/validate?articleId=${articleId}&apiKey=${API_KEY}`
);
if (!response.ok) {
const errorData = await response.json() as any;
throw new Error(errorData.error?.message || `API error: ${response.status}`);
}
const result = await response.json() as any;
return {
content: [
{
type: "text",
text: JSON.stringify(result.data.article, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: `Error validating article: ${error.message}`,
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
/**
* Start server
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("CalcsLive MCP server running on stdio");
console.error(`API Base: ${CALCSLIVE_API_BASE}`);
console.error("Ready to perform unit-aware calculations for AI agents!");
}
main().catch((error) => {
console.error("Fatal error starting MCP server:", error);
process.exit(1);
});