"""
Construction Cost Calculator MCP Server
A no-auth MCP server that reads construction cost data from a public Google Sheet
and provides tools for searching, filtering, and calculating costs.
"""
import os
import time
from typing import Optional
from io import StringIO
import httpx
import pandas as pd
from fastmcp import FastMCP
# Configuration
GOOGLE_SHEET_ID = os.getenv(
"GOOGLE_SHEET_ID",
"1laH5l1FjwWC5HXLp8xkG1eolwScKHIlxB6PohBWQjh0"
)
SHEET_GID = os.getenv("SHEET_GID", "0")
CACHE_TTL_SECONDS = int(os.getenv("CACHE_TTL_SECONDS", "300")) # 5 minutes default
DEFAULT_LABOR_RATE = float(os.getenv("DEFAULT_LABOR_RATE", "75.0")) # $/hour
# Cache for sheet data
_cache = {
"data": None,
"last_fetch": 0
}
# Initialize FastMCP server with agent instructions
mcp = FastMCP(
"Construction Cost Calculator",
instructions="""You are a construction cost estimation assistant with access to a database of material and labor costs.
## Data Source
- Costs are loaded from a construction pricing database (Google Sheets)
- Default labor rate: $75/hour (can be overridden in any tool)
- All prices are in USD
- Data is cached for 5 minutes for performance
## Available Categories
- concrete: Slabs, foundations (various PSI ratings)
- framing: Wall framing, roof trusses, beams (2x4, 2x6, glulam)
- drywall: Standard drywall, textures (1/2", 5/8")
- paint: Interior/exterior, standard/premium, ceiling
- flooring: Carpet, vinyl plank, hardwood, tile
## Recommended Workflow
1. **Discovery**: Use `search_items` or `get_items_by_category` to find valid item codes
2. **Comparison**: Use `compare_options` to help users choose between quality/price levels
3. **Calculation**: Use `calculate_cost` for custom item lists, or `estimate_project` for standard project types
4. **Optimization**: Use `get_best_value` to find the most cost-effective options
## Project Types (for estimate_project tool)
- `room_finish`: Basic room finishing (drywall, paint, flooring)
- `basement_finish`: Full basement buildout including framing
- `addition_basic`: New addition with foundation, framing, and finishes
## Quality Levels
- `budget`: Most economical options
- `standard`: Mid-range materials (default)
- `premium`: High-end materials and finishes
## Key Tips
- Always verify item codes exist before calculating (use search_items first if unsure)
- Item codes are case-insensitive
- Use `convert_units` if user provides metric measurements
- The `markup_percent` parameter adds contractor profit margin to estimates
- For room estimates, `estimate_room_cost` automatically calculates wall/floor areas from dimensions
## Common Item Code Patterns
- Concrete: `concrete_slab_4000psi`, `concrete_slab_5000psi`
- Framing: `framing_wall_2x4`, `framing_wall_2x6`, `framing_roof_truss`
- Drywall: `drywall_standard_12`, `drywall_standard_58`, `drywall_texture_knockdown`
- Paint: `paint_interior_standard`, `paint_interior_premium`, `paint_ceiling`
- Flooring: `flooring_carpet_standard`, `flooring_vinyl_plank`, `flooring_hardwood_oak`
## Error Handling
- If an item code is not found, the tool returns available codes as suggestions
- If a category is not found, available categories are listed
- Always check the `errors` field in responses for partial failures
"""
)
def get_csv_url() -> str:
"""Get the CSV export URL for the Google Sheet."""
return f"https://docs.google.com/spreadsheets/d/{GOOGLE_SHEET_ID}/export?format=csv&gid={SHEET_GID}"
async def fetch_sheet_data() -> pd.DataFrame:
"""
Fetch data from Google Sheet with caching.
Returns cached data if still valid, otherwise fetches fresh data.
"""
current_time = time.time()
# Check if cache is still valid
if _cache["data"] is not None and (current_time - _cache["last_fetch"]) < CACHE_TTL_SECONDS:
return _cache["data"]
# Fetch fresh data
async with httpx.AsyncClient() as client:
response = await client.get(get_csv_url(), follow_redirects=True)
response.raise_for_status()
# Parse CSV
df = pd.read_csv(StringIO(response.text))
# Clean column names (remove any whitespace)
df.columns = df.columns.str.strip()
# Update cache
_cache["data"] = df
_cache["last_fetch"] = current_time
return df
def format_item(row: pd.Series, labor_rate: float = DEFAULT_LABOR_RATE) -> dict:
"""Format a single item row as a dictionary with computed total cost."""
material_cost = float(row["material_cost"])
labor_hours = float(row["labor_hours"])
total_cost_per_unit = material_cost + (labor_hours * labor_rate)
return {
"item_code": row["item_code"],
"description": row["description"],
"category": row["category"],
"material_cost": material_cost,
"labor_hours": labor_hours,
"unit": row["unit"],
"total_cost_per_unit": round(total_cost_per_unit, 2)
}
@mcp.tool()
async def list_all_items(labor_rate: Optional[float] = None) -> dict:
"""
List all available construction items from the cost database.
Args:
labor_rate: Hourly labor rate for total cost calculation (default: $75/hour)
Returns a list of all items with their codes, descriptions, categories,
material costs, labor hours, units, and computed total cost per unit.
"""
df = await fetch_sheet_data()
rate = labor_rate or DEFAULT_LABOR_RATE
items = [format_item(row, rate) for _, row in df.iterrows()]
categories = df["category"].unique().tolist()
return {
"total_items": len(items),
"categories": categories,
"labor_rate_used": rate,
"items": items
}
@mcp.tool()
async def get_item(item_code: str, labor_rate: Optional[float] = None) -> dict:
"""
Get detailed information for a specific construction item by its code.
Args:
item_code: The unique code for the item (e.g., 'concrete_slab_4000psi')
labor_rate: Hourly labor rate for total cost calculation (default: $75/hour)
Returns the item details including material cost, labor hours, unit, and total cost.
"""
df = await fetch_sheet_data()
rate = labor_rate or DEFAULT_LABOR_RATE
# Find the item
item_row = df[df["item_code"] == item_code]
if item_row.empty:
# Try case-insensitive search
item_row = df[df["item_code"].str.lower() == item_code.lower()]
if item_row.empty:
available_codes = df["item_code"].tolist()
return {
"error": f"Item '{item_code}' not found",
"suggestion": "Use list_all_items or search_items to find valid item codes",
"available_codes_sample": available_codes[:10]
}
return {
"found": True,
"item": format_item(item_row.iloc[0], rate)
}
@mcp.tool()
async def search_items(query: str, labor_rate: Optional[float] = None) -> dict:
"""
Search for construction items by keyword in their code or description.
Args:
query: Search term to find in item codes or descriptions
labor_rate: Hourly labor rate for total cost calculation (default: $75/hour)
Returns matching items sorted by relevance.
"""
df = await fetch_sheet_data()
query_lower = query.lower()
rate = labor_rate or DEFAULT_LABOR_RATE
# Search in both item_code and description
mask = (
df["item_code"].str.lower().str.contains(query_lower, na=False) |
df["description"].str.lower().str.contains(query_lower, na=False)
)
matches = df[mask]
items = [format_item(row, rate) for _, row in matches.iterrows()]
return {
"query": query,
"total_matches": len(items),
"labor_rate_used": rate,
"items": items
}
@mcp.tool()
async def get_items_by_category(category: str, labor_rate: Optional[float] = None) -> dict:
"""
Get all construction items in a specific category.
Args:
category: The category name (e.g., 'concrete', 'framing', 'finishes')
labor_rate: Hourly labor rate for total cost calculation (default: $75/hour)
Returns all items in the specified category sorted by total cost.
"""
df = await fetch_sheet_data()
category_lower = category.lower()
rate = labor_rate or DEFAULT_LABOR_RATE
# Filter by category (case-insensitive)
matches = df[df["category"].str.lower() == category_lower]
if matches.empty:
available_categories = df["category"].unique().tolist()
return {
"error": f"Category '{category}' not found",
"available_categories": available_categories
}
items = [format_item(row, rate) for _, row in matches.iterrows()]
# Sort by total cost
items.sort(key=lambda x: x["total_cost_per_unit"])
return {
"category": category,
"total_items": len(items),
"labor_rate_used": rate,
"items": items
}
@mcp.tool()
async def calculate_cost(
items: list[dict],
labor_rate: Optional[float] = None,
markup_percent: Optional[float] = None
) -> dict:
"""
Calculate the total cost for a list of construction items with quantities.
Args:
items: List of items with 'item_code' and 'quantity' keys.
Example: [{"item_code": "concrete_slab_4000psi", "quantity": 1000}]
labor_rate: Hourly labor rate in dollars (default: $75/hour)
markup_percent: Optional markup percentage for profit margin (e.g., 15 for 15%)
Returns itemized costs and total project cost.
Cost Formula:
Item Cost = (Material Cost × Quantity) + (Labor Hours × Quantity × Labor Rate)
Final Cost = Total Cost × (1 + Markup/100) if markup provided
"""
df = await fetch_sheet_data()
if labor_rate is None:
labor_rate = DEFAULT_LABOR_RATE
line_items = []
total_material_cost = 0.0
total_labor_cost = 0.0
total_labor_hours = 0.0
errors = []
for item_request in items:
item_code = item_request.get("item_code")
quantity = item_request.get("quantity", 0)
if not item_code:
errors.append({"error": "Missing item_code in request"})
continue
# Find the item
item_row = df[df["item_code"] == item_code]
if item_row.empty:
# Try case-insensitive
item_row = df[df["item_code"].str.lower() == item_code.lower()]
if item_row.empty:
errors.append({
"item_code": item_code,
"error": "Item not found"
})
continue
item_data = item_row.iloc[0]
material_cost = float(item_data["material_cost"])
labor_hours = float(item_data["labor_hours"])
# Calculate costs
item_material_cost = material_cost * quantity
item_labor_hours = labor_hours * quantity
item_labor_cost = item_labor_hours * labor_rate
item_total = item_material_cost + item_labor_cost
line_items.append({
"item_code": item_code,
"description": item_data["description"],
"quantity": quantity,
"unit": item_data["unit"],
"material_cost_per_unit": material_cost,
"labor_hours_per_unit": labor_hours,
"material_cost": round(item_material_cost, 2),
"labor_hours": round(item_labor_hours, 2),
"labor_cost": round(item_labor_cost, 2),
"total_cost": round(item_total, 2)
})
total_material_cost += item_material_cost
total_labor_cost += item_labor_cost
total_labor_hours += item_labor_hours
subtotal = total_material_cost + total_labor_cost
markup_amount = 0.0
grand_total = subtotal
if markup_percent:
markup_amount = subtotal * (markup_percent / 100)
grand_total = subtotal + markup_amount
return {
"labor_rate_used": labor_rate,
"line_items": line_items,
"summary": {
"total_material_cost": round(total_material_cost, 2),
"total_labor_hours": round(total_labor_hours, 2),
"total_labor_cost": round(total_labor_cost, 2),
"subtotal": round(subtotal, 2),
"markup_percent": markup_percent,
"markup_amount": round(markup_amount, 2) if markup_percent else None,
"grand_total": round(grand_total, 2)
},
"errors": errors if errors else None
}
@mcp.tool()
async def estimate_room_cost(
length_ft: float,
width_ft: float,
height_ft: float = 8.0,
floor_type: str = "flooring_vinyl_plank",
wall_type: str = "drywall_standard_12",
paint_type: str = "paint_interior_standard",
ceiling_paint: bool = True,
labor_rate: Optional[float] = None,
markup_percent: Optional[float] = None
) -> dict:
"""
Estimate the cost to finish a room given its dimensions.
Args:
length_ft: Room length in feet
width_ft: Room width in feet
height_ft: Room/ceiling height in feet (default: 8 ft)
floor_type: Item code for flooring (default: flooring_vinyl_plank)
wall_type: Item code for wall finish (default: drywall_standard_12)
paint_type: Item code for wall paint (default: paint_interior_standard)
ceiling_paint: Whether to include ceiling paint (default: True)
labor_rate: Hourly labor rate (default: $75/hour)
markup_percent: Optional markup percentage for profit
Returns detailed room cost breakdown.
"""
df = await fetch_sheet_data()
rate = labor_rate or DEFAULT_LABOR_RATE
# Calculate areas
floor_area = length_ft * width_ft
wall_area = 2 * (length_ft + width_ft) * height_ft
ceiling_area = floor_area
# Build items list
items_to_calculate = [
{"item_code": floor_type, "quantity": floor_area, "area_type": "floor"},
{"item_code": wall_type, "quantity": wall_area, "area_type": "walls"},
{"item_code": paint_type, "quantity": wall_area, "area_type": "wall_paint"},
]
if ceiling_paint:
items_to_calculate.append({
"item_code": "paint_ceiling",
"quantity": ceiling_area,
"area_type": "ceiling"
})
# Calculate costs
line_items = []
total_cost = 0.0
errors = []
for item_req in items_to_calculate:
item_code = item_req["item_code"]
quantity = item_req["quantity"]
item_row = df[df["item_code"] == item_code]
if item_row.empty:
item_row = df[df["item_code"].str.lower() == item_code.lower()]
if item_row.empty:
errors.append({"item_code": item_code, "error": "Item not found"})
continue
item_data = item_row.iloc[0]
material_cost = float(item_data["material_cost"]) * quantity
labor_cost = float(item_data["labor_hours"]) * quantity * rate
item_total = material_cost + labor_cost
line_items.append({
"area_type": item_req["area_type"],
"item_code": item_code,
"description": item_data["description"],
"area_sqft": round(quantity, 2),
"material_cost": round(material_cost, 2),
"labor_cost": round(labor_cost, 2),
"total": round(item_total, 2)
})
total_cost += item_total
# Apply markup
markup_amount = 0.0
final_total = total_cost
if markup_percent:
markup_amount = total_cost * (markup_percent / 100)
final_total = total_cost + markup_amount
return {
"room_dimensions": {
"length_ft": length_ft,
"width_ft": width_ft,
"height_ft": height_ft,
"floor_area_sqft": round(floor_area, 2),
"wall_area_sqft": round(wall_area, 2),
"ceiling_area_sqft": round(ceiling_area, 2)
},
"labor_rate_used": rate,
"line_items": line_items,
"subtotal": round(total_cost, 2),
"markup_percent": markup_percent,
"markup_amount": round(markup_amount, 2) if markup_percent else None,
"grand_total": round(final_total, 2),
"errors": errors if errors else None
}
@mcp.tool()
async def compare_options(
item_type: str,
quantity: float = 100,
labor_rate: Optional[float] = None
) -> dict:
"""
Compare different options/grades for a similar item type.
Args:
item_type: Base item type to compare (e.g., 'concrete_slab', 'drywall',
'paint', 'flooring', 'carpet')
quantity: Quantity to compare costs for (default: 100 units)
labor_rate: Hourly labor rate (default: $75/hour)
Returns comparison of all matching items with cost difference analysis.
"""
df = await fetch_sheet_data()
rate = labor_rate or DEFAULT_LABOR_RATE
# Search for matching items
mask = df["item_code"].str.lower().str.contains(item_type.lower(), na=False)
matches = df[mask]
if matches.empty:
return {
"error": f"No items found matching '{item_type}'",
"suggestion": "Try: concrete_slab, drywall, paint, flooring, carpet, framing"
}
options = []
for _, row in matches.iterrows():
material_cost = float(row["material_cost"])
labor_hours = float(row["labor_hours"])
total_per_unit = material_cost + (labor_hours * rate)
total_for_quantity = total_per_unit * quantity
options.append({
"item_code": row["item_code"],
"description": row["description"],
"unit": row["unit"],
"material_cost_per_unit": material_cost,
"labor_hours_per_unit": labor_hours,
"total_cost_per_unit": round(total_per_unit, 2),
"total_for_quantity": round(total_for_quantity, 2)
})
# Sort by total cost
options.sort(key=lambda x: x["total_cost_per_unit"])
# Calculate savings comparison
if len(options) > 1:
cheapest = options[0]["total_for_quantity"]
for opt in options:
opt["difference_from_cheapest"] = round(opt["total_for_quantity"] - cheapest, 2)
opt["percent_more_expensive"] = round(
((opt["total_for_quantity"] - cheapest) / cheapest) * 100, 1
) if cheapest > 0 else 0
return {
"item_type": item_type,
"quantity_compared": quantity,
"labor_rate_used": rate,
"total_options": len(options),
"cheapest_option": options[0]["item_code"] if options else None,
"most_expensive_option": options[-1]["item_code"] if options else None,
"potential_savings": round(options[-1]["total_for_quantity"] - options[0]["total_for_quantity"], 2) if len(options) > 1 else 0,
"options": options
}
@mcp.tool()
async def get_best_value(
category: Optional[str] = None,
labor_rate: Optional[float] = None
) -> dict:
"""
Find the cheapest (best value) option in each category or a specific category.
Args:
category: Optional category to filter (if not provided, returns best in each category)
labor_rate: Hourly labor rate (default: $75/hour)
Returns the most cost-effective items.
"""
df = await fetch_sheet_data()
rate = labor_rate or DEFAULT_LABOR_RATE
# Calculate total cost for each item
df_copy = df.copy()
df_copy["total_cost"] = df_copy["material_cost"] + (df_copy["labor_hours"] * rate)
if category:
df_copy = df_copy[df_copy["category"].str.lower() == category.lower()]
if df_copy.empty:
return {
"error": f"Category '{category}' not found",
"available_categories": df["category"].unique().tolist()
}
# Find cheapest in each category
best_values = []
for cat in df_copy["category"].unique():
cat_items = df_copy[df_copy["category"] == cat]
cheapest = cat_items.loc[cat_items["total_cost"].idxmin()]
best_values.append({
"category": cat,
"item_code": cheapest["item_code"],
"description": cheapest["description"],
"material_cost": float(cheapest["material_cost"]),
"labor_hours": float(cheapest["labor_hours"]),
"total_cost_per_unit": round(float(cheapest["total_cost"]), 2),
"unit": cheapest["unit"]
})
# Sort by category
best_values.sort(key=lambda x: x["category"])
return {
"labor_rate_used": rate,
"filter_category": category,
"best_value_items": best_values
}
@mcp.tool()
async def estimate_project(
project_type: str,
sqft: float,
quality: str = "standard",
labor_rate: Optional[float] = None,
markup_percent: Optional[float] = None
) -> dict:
"""
Estimate costs for a complete project based on type and square footage.
Args:
project_type: Type of project - 'room_finish', 'basement_finish',
'addition_basic', 'full_renovation'
sqft: Total square footage of the project
quality: Quality level - 'budget', 'standard', or 'premium'
labor_rate: Hourly labor rate (default: $75/hour)
markup_percent: Optional markup percentage for profit
Returns comprehensive project estimate.
"""
df = await fetch_sheet_data()
rate = labor_rate or DEFAULT_LABOR_RATE
# Define project templates
templates = {
"room_finish": {
"budget": [
("drywall_standard_12", 1.0, "walls"),
("paint_interior_standard", 1.0, "paint"),
("flooring_carpet_standard", 1.0, "floor"),
],
"standard": [
("drywall_standard_12", 1.0, "walls"),
("drywall_texture_knockdown", 1.0, "texture"),
("paint_interior_standard", 1.0, "paint"),
("flooring_vinyl_plank", 1.0, "floor"),
],
"premium": [
("drywall_standard_58", 1.0, "walls"),
("drywall_texture_knockdown", 1.0, "texture"),
("paint_interior_premium", 1.0, "paint"),
("flooring_hardwood_oak", 1.0, "floor"),
]
},
"basement_finish": {
"budget": [
("framing_wall_2x4", 0.5, "framing"),
("drywall_standard_12", 1.2, "walls"),
("paint_interior_standard", 1.2, "paint"),
("flooring_carpet_standard", 1.0, "floor"),
],
"standard": [
("framing_wall_2x4", 0.5, "framing"),
("drywall_standard_12", 1.2, "walls"),
("drywall_texture_knockdown", 1.2, "texture"),
("paint_interior_standard", 1.2, "paint"),
("flooring_vinyl_plank", 1.0, "floor"),
("paint_ceiling", 1.0, "ceiling"),
],
"premium": [
("framing_wall_2x6", 0.5, "framing"),
("drywall_standard_58", 1.2, "walls"),
("drywall_texture_knockdown", 1.2, "texture"),
("paint_interior_premium", 1.2, "paint"),
("flooring_hardwood_oak", 1.0, "floor"),
("paint_ceiling", 1.0, "ceiling"),
]
},
"addition_basic": {
"budget": [
("concrete_slab_4000psi", 1.0, "foundation"),
("framing_wall_2x4", 0.6, "framing"),
("framing_roof_truss", 1.0, "roof_frame"),
("drywall_standard_12", 1.3, "walls"),
("paint_interior_standard", 1.3, "paint"),
("flooring_carpet_standard", 1.0, "floor"),
],
"standard": [
("concrete_slab_4000psi", 1.0, "foundation"),
("framing_wall_2x6", 0.6, "framing"),
("framing_roof_truss", 1.0, "roof_frame"),
("drywall_standard_12", 1.3, "walls"),
("drywall_texture_knockdown", 1.3, "texture"),
("paint_interior_standard", 1.3, "paint"),
("flooring_vinyl_plank", 1.0, "floor"),
],
"premium": [
("concrete_slab_5000psi", 1.0, "foundation"),
("framing_wall_2x6", 0.6, "framing"),
("framing_roof_truss", 1.0, "roof_frame"),
("framing_beam_glulam", 0.1, "beams"),
("drywall_standard_58", 1.3, "walls"),
("drywall_texture_knockdown", 1.3, "texture"),
("paint_interior_premium", 1.3, "paint"),
("flooring_hardwood_oak", 1.0, "floor"),
]
}
}
if project_type not in templates:
return {
"error": f"Unknown project type '{project_type}'",
"available_types": list(templates.keys())
}
if quality not in templates[project_type]:
return {
"error": f"Unknown quality level '{quality}'",
"available_qualities": ["budget", "standard", "premium"]
}
template = templates[project_type][quality]
line_items = []
total_material = 0.0
total_labor = 0.0
errors = []
for item_code, multiplier, component in template:
quantity = sqft * multiplier
item_row = df[df["item_code"] == item_code]
if item_row.empty:
errors.append({"item_code": item_code, "error": "Not found"})
continue
item_data = item_row.iloc[0]
material = float(item_data["material_cost"]) * quantity
labor = float(item_data["labor_hours"]) * quantity * rate
line_items.append({
"component": component,
"item_code": item_code,
"description": item_data["description"],
"quantity": round(quantity, 2),
"unit": item_data["unit"],
"material_cost": round(material, 2),
"labor_cost": round(labor, 2),
"total": round(material + labor, 2)
})
total_material += material
total_labor += labor
subtotal = total_material + total_labor
markup_amount = 0.0
grand_total = subtotal
if markup_percent:
markup_amount = subtotal * (markup_percent / 100)
grand_total = subtotal + markup_amount
# Calculate per sqft costs
cost_per_sqft = grand_total / sqft if sqft > 0 else 0
return {
"project_type": project_type,
"quality_level": quality,
"square_footage": sqft,
"labor_rate_used": rate,
"line_items": line_items,
"summary": {
"total_material_cost": round(total_material, 2),
"total_labor_cost": round(total_labor, 2),
"subtotal": round(subtotal, 2),
"markup_percent": markup_percent,
"markup_amount": round(markup_amount, 2) if markup_percent else None,
"grand_total": round(grand_total, 2),
"cost_per_sqft": round(cost_per_sqft, 2)
},
"errors": errors if errors else None
}
@mcp.tool()
async def convert_units(
value: float,
from_unit: str,
to_unit: str
) -> dict:
"""
Convert between common construction measurement units.
Args:
value: The value to convert
from_unit: Source unit (sqft, sqm, lf, m, cuft, cum, inch, cm)
to_unit: Target unit
Returns the converted value.
"""
conversions = {
# Area
("sqft", "sqm"): 0.092903,
("sqm", "sqft"): 10.7639,
# Length
("lf", "m"): 0.3048,
("m", "lf"): 3.28084,
("ft", "m"): 0.3048,
("m", "ft"): 3.28084,
("inch", "cm"): 2.54,
("cm", "inch"): 0.393701,
# Volume
("cuft", "cum"): 0.0283168,
("cum", "cuft"): 35.3147,
("cuyd", "cum"): 0.764555,
("cum", "cuyd"): 1.30795,
}
key = (from_unit.lower(), to_unit.lower())
if key not in conversions:
return {
"error": f"Cannot convert from '{from_unit}' to '{to_unit}'",
"supported_conversions": [
"sqft <-> sqm (area)",
"lf/ft <-> m (length)",
"inch <-> cm (length)",
"cuft <-> cum (volume)",
"cuyd <-> cum (volume)"
]
}
result = value * conversions[key]
return {
"original_value": value,
"original_unit": from_unit,
"converted_value": round(result, 4),
"converted_unit": to_unit,
"conversion_factor": conversions[key]
}
# Run the server
if __name__ == "__main__":
port = int(os.getenv("PORT", "8000"))
mcp.run(transport="streamable-http", host="0.0.0.0", port=port)