-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
190 lines (155 loc) · 7.3 KB
/
server.py
File metadata and controls
190 lines (155 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
"""
MCP Server using FastMCP that adjusts pipette settings based on liquid type and environment
"""
from mcp.server.fastmcp import FastMCP
from typing import Optional
import os
import json
import glob
import math
import re
# Initialize the FastMCP server
mcp = FastMCP(
name="LiquidLib",
description="This MCP server provides pipette dispense speed recommendations based on liquid type, temperature, and relative humidity."
)
# User-specific label (optional)
USER_NAME = "LiquidLab User"
# Directory where the liquid JSON files are stored
LIQUIDS_DIR = "liquids"
# @mcp.tool()
# def adjust_dispense_volume(
# temperature_c: float,
# humidity_percent: float,
# liquid_name: str
# ) -> str:
# """
# Adjusts pipette dispense volume based on temperature (°C), humidity (% RH), and liquid type.
# Extracts density from structured PubChem-style JSON if available.
# """
# # === Input validation ===
# if not (isinstance(temperature_c, (int, float)) and math.isfinite(temperature_c)):
# return "Error: Temperature must be a finite number between 0 and 100 °C."
# if not (isinstance(humidity_percent, (int, float)) and math.isfinite(humidity_percent)):
# return "Error: Relative humidity must be a finite number between 0% and 100%."
# if not (0 <= temperature_c <= 32.2):
# return "Error: Temperature must be between 10 and 32.2 °C."
# if not (0 <= humidity_percent <= 100):
# return "Error: Relative humidity must be between 0% and 100%."
# # === File matching ===
# liquid_name = liquid_name.strip().lower()
# pattern = os.path.join(LIQUIDS_DIR, f"*{liquid_name}*.json")
# matching_files = glob.glob(pattern)
# if not matching_files:
# return f"Error: No data file found for liquid containing '{liquid_name}'. (Pattern: {pattern})"
# if len(matching_files) > 1:
# matched = ", ".join(os.path.basename(f) for f in matching_files)
# return f"Error: Multiple files matched for '{liquid_name}': {matched}. Please be more specific."
# filename = matching_files[0]
# # === Parse structured JSON ===
# try:
# with open(filename, 'r') as f:
# data = json.load(f)
# if not isinstance(data, list):
# return f"Error: Expected a list of sections in the JSON structure."
# density_val = None
# # Look for "Density" section
# for section in data:
# if section.get("TOCHeading", "").lower() == "density":
# for info in section.get("Information", []):
# val_obj = info.get("Value", {})
# for string_entry in val_obj.get("StringWithMarkup", []):
# s = string_entry.get("String", "")
# match = re.search(r"([0-9.]+)\s*g/(?:cu\s*cm|cm3|cc|mL)", s, re.IGNORECASE)
# if match:
# density_val = float(match.group(1))
# break
# if density_val is not None:
# break
# if density_val is not None:
# break
# if density_val is None:
# return f"Error: Could not extract numeric density from '{os.path.basename(filename)}'."
# # Estimate pipette base volume from density
# base = density_val * 1000 # µL (arbitrary scaling factor)
# temp_coeff = 0.01
# hum_coeff = -0.002
# except Exception as e:
# return f"Error reading or parsing JSON file '{os.path.basename(filename)}': {e}"
# # === Volume adjustment ===
# adjusted_volume = base + (temperature_c * temp_coeff) + (humidity_percent * hum_coeff)
# return (
# f"Matched file: {os.path.basename(filename)}\n"
# f"Extracted density: {density_val:.4f} g/cm³\n"
# f"Base volume estimate: {base:.2f} µL\n"
# f"Adjusted for {temperature_c}°C and {humidity_percent}% RH: {adjusted_volume:.2f} µL"
# )
@mcp.tool()
def adjust_dispense_speed(
temperature_c: float,
humidity_percent: float,
liquid_name: str
) -> str:
"""
Adjusts pipette dispense speed (µL/s) based on temperature (°C), humidity (% RH), and liquid type.
Extracts density from structured JSON. Higher density → slower speed. Higher temp → faster speed.
"""
# === Input validation ===
if not (isinstance(temperature_c, (int, float)) and math.isfinite(temperature_c)):
return "Error: Temperature must be a finite number between 0 and 32.2 °C."
if not (isinstance(humidity_percent, (int, float)) and math.isfinite(humidity_percent)):
return "Error: Relative humidity must be a finite number between 0% and 100%."
if not (0 <= temperature_c <= 32.2):
return "Error: Temperature must be between 0 and 32.2 °C."
if not (0 <= humidity_percent <= 100):
return "Error: Relative humidity must be between 0% and 100%."
# === File matching ===
liquid_name = liquid_name.strip().lower()
pattern = os.path.join(LIQUIDS_DIR, f"*{liquid_name}*.json")
matching_files = glob.glob(pattern)
if not matching_files:
return f"Error: No data file found for liquid containing '{liquid_name}'. (Pattern: {pattern})"
if len(matching_files) > 1:
matched = ", ".join(os.path.basename(f) for f in matching_files)
return f"Error: Multiple files matched for '{liquid_name}': {matched}. Please be more specific."
filename = matching_files[0]
# === Parse structured JSON ===
try:
with open(filename, 'r') as f:
data = json.load(f)
if not isinstance(data, list):
return f"Error: Expected a list of sections in the JSON structure."
density_val = None
for section in data:
if section.get("TOCHeading", "").lower() == "density":
for info in section.get("Information", []):
val_obj = info.get("Value", {})
for string_entry in val_obj.get("StringWithMarkup", []):
s = string_entry.get("String", "")
match = re.search(r"([0-9.]+)\s*g/(?:cu\s*cm|cm3|cc|mL)", s, re.IGNORECASE)
if match:
density_val = float(match.group(1))
break
if density_val is not None:
break
if density_val is not None:
break
if density_val is None:
return f"Error: Could not extract numeric density from '{os.path.basename(filename)}'."
except Exception as e:
return f"Error reading or parsing JSON file '{os.path.basename(filename)}': {e}"
# === Dispense speed model ===
base_speed = 50.0 # µL/s default
# Higher density → lower speed; higher temp → higher speed
# Simple model: inverse proportional to density, direct to temp
speed_factor = (temperature_c + 1) / density_val # Avoid division by zero
adjusted_speed = base_speed * speed_factor
return (
f"Matched file: {os.path.basename(filename)}\n"
f"Extracted density: {density_val:.4f} g/cm³\n"
f"Base speed: {base_speed:.2f} µL/s\n"
f"Adjusted speed for {temperature_c}°C and {humidity_percent}% RH: {adjusted_speed:.2f} µL/s"
)
if __name__ == "__main__":
# Run the server
mcp.run(transport='stdio')