Skip to main content
Glama
puran-water

Corrosion Engineering MCP Server

by puran-water
material_screening.py12.3 kB
""" Tier 0 Tool: Material Compatibility Screening Uses semantic search on corrosion_kb to quickly screen materials for compatibility with specified environments. Example Query: "316 stainless steel compatibility with seawater at 60°C" Returns: - Compatibility rating (acceptable/marginal/not_recommended) - Typical rate ranges from handbooks - Warnings and recommendations - Source citations """ from typing import Dict, Any, List, Optional from core.schemas import MaterialCompatibility, ProvenanceMetadata, ConfidenceLevel from core.interfaces import HandbookLookup import logging # This will be imported from MCP client context # For now, we'll define the interface try: from mcp import search_corrosion_kb # Will be available via MCP server context MCP_AVAILABLE = True except ImportError: MCP_AVAILABLE = False logging.warning("MCP corrosion_kb not available - using placeholder") class MaterialScreeningLookup(HandbookLookup): """ Material compatibility screening via semantic search. Queries corrosion handbooks for material-environment compatibility, typical corrosion rates, and usage recommendations. """ def __init__(self, mcp_search_function=None): """ Initialize material screening tool. Args: mcp_search_function: Function to call corrosion_kb semantic search (will be injected by MCP server context) """ self._logger = logging.getLogger(__name__) self._mcp_search = mcp_search_function def query( self, query_text: str, filters: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """ Query handbook for material compatibility. Args: query_text: Natural language query describing material and environment filters: Optional filters (not used in current implementation) Returns: Dictionary matching MaterialCompatibility schema """ try: # Call semantic search if self._mcp_search: search_results = self._mcp_search( query=query_text, mode="rerank", top_k=5, ) else: # Placeholder for development without MCP server search_results = self._placeholder_search(query_text) # Parse results into structured format parsed = self._parse_results(search_results, query_text) return parsed except Exception as e: self._logger.error(f"Material screening query failed: {e}") raise RuntimeError(f"Material screening error: {e}") def _parse_results( self, search_results: List[Dict[str, Any]], query_text: str, ) -> Dict[str, Any]: """ Parse semantic search results into MaterialCompatibility schema. Extracts: - Compatibility rating from handbook text - Typical rate ranges - Recommendations and warnings """ # Extract material and environment from query # (Simplified - would use NLP in production) material, environment = self._extract_material_environment(query_text) # Analyze top results for compatibility indicators compatibility = self._assess_compatibility(search_results) rate_range = self._extract_rate_range(search_results) notes = self._compile_notes(search_results) sources = self._extract_sources(search_results) return { "material": material, "environment": environment, "compatibility": compatibility, "typical_rate_range": rate_range, "notes": notes, "provenance": { "model": "kb.material_screening", "version": "1.0.0", "validation_dataset": None, "confidence": self._assess_confidence(search_results), "sources": sources, "assumptions": ["Handbook data represents typical service conditions"], "warnings": self._extract_warnings(search_results), } } def _extract_material_environment(self, query_text: str) -> tuple[str, str]: """Extract material and environment from query text""" # Simplified extraction - would use NLP/regex in production query_lower = query_text.lower() # Common materials materials = { "316": "316L", "304": "304", "carbon steel": "CS", "duplex": "duplex", "super duplex": "super-duplex", } material = "unknown" for key, val in materials.items(): if key in query_lower: material = val break # Environment is the full query for now environment = query_text return material, environment def _assess_compatibility(self, results: List[Dict[str, Any]]) -> str: """ Assess compatibility from search results. Returns: "acceptable", "marginal", or "not_recommended" """ # Analyze top results for positive/negative indicators positive_indicators = ["acceptable", "suitable", "recommended", "good performance", "resistant"] negative_indicators = ["not recommended", "avoid", "unsuitable", "severe", "rapid corrosion", "failure"] marginal_indicators = ["marginal", "limited", "with caution", "requires monitoring"] text_combined = " ".join([r.get("text", "") for r in results[:3]]).lower() # Count indicators negative_count = sum(1 for ind in negative_indicators if ind in text_combined) positive_count = sum(1 for ind in positive_indicators if ind in text_combined) marginal_count = sum(1 for ind in marginal_indicators if ind in text_combined) if negative_count > positive_count: return "not_recommended" elif marginal_count > 0 or (positive_count == negative_count): return "marginal" else: return "acceptable" def _extract_rate_range(self, results: List[Dict[str, Any]]) -> Optional[tuple[float, float]]: """ Extract typical corrosion rate range from results. Looks for patterns like "0.1-0.3 mm/y" or "2-5 mpy" in text. """ import re # Pattern for rate ranges (simplified) pattern = r'(\d+\.?\d*)\s*-\s*(\d+\.?\d*)\s*(mm/y|mpy|ipy)' for result in results[:5]: text = result.get("text", "") match = re.search(pattern, text) if match: min_rate = float(match.group(1)) max_rate = float(match.group(2)) unit = match.group(3) # Convert to mm/y if needed # mpy (mils/year): 1 mil = 0.0254 mm, so mpy / 39.37 = mm/y # ipy (inches/year): 1 inch = 25.4 mm, so ipy * 25.4 = mm/y if unit == "mpy": min_rate /= 39.37 max_rate /= 39.37 elif unit == "ipy": # FIXED: was /= 39.37 (wrong by 1000×) min_rate *= 25.4 max_rate *= 25.4 return (min_rate, max_rate) return None def _compile_notes(self, results: List[Dict[str, Any]]) -> str: """Compile detailed notes from top results""" notes = [] for i, result in enumerate(results[:3], 1): text = result.get("text", "")[:200] # First 200 chars notes.append(f"[Source {i}] {text}...") return "\n\n".join(notes) def _extract_sources(self, results: List[Dict[str, Any]]) -> List[str]: """Extract source citations from results""" sources = [] for result in results[:5]: path = result.get("path", "") if path: # Extract filename and offset source = f"{path}" if "offset" in result: source += f" (offset {result['offset']})" sources.append(source) return sources[:3] # Top 3 sources def _extract_warnings(self, results: List[Dict[str, Any]]) -> List[str]: """Extract warnings from search results""" warnings = [] warning_keywords = ["warning", "caution", "avoid", "not recommended", "severe"] for result in results[:5]: text = result.get("text", "").lower() for keyword in warning_keywords: if keyword in text: warnings.append(f"Handbook mentions: {keyword}") break return warnings def _assess_confidence(self, results: List[Dict[str, Any]]) -> str: """Assess confidence based on search result quality""" if not results: return "low" # Check if top results have high relevance scores top_score = results[0].get("score", 0) if results else 0 if top_score > 0.8: return "high" elif top_score > 0.6: return "medium" else: return "low" def _placeholder_search(self, query_text: str) -> List[Dict[str, Any]]: """Placeholder search for development without MCP server""" return [ { "text": "316 stainless steel shows excellent resistance to seawater corrosion at ambient temperatures. " "Typical corrosion rates range from 0.007-0.02 ipy (0.0002-0.0005 mm/y) in quiescent seawater. " "However, at elevated temperatures (>50°C) or in high-velocity conditions, pitting may occur.", "score": 0.85, "path": "handbook_of_corrosion_engineering.pdf", "offset": 1234, }, { "text": "At temperatures above 60°C, chloride pitting becomes a concern for 316 SS in seawater. " "Consider upgrading to duplex or super-duplex grades for high-temperature seawater service.", "score": 0.78, "path": "the_corrosion_handbook.pdf", "offset": 5678, }, ] # ============================================================================ # MCP Tool Function # ============================================================================ def material_screening_query( environment: str, candidates: List[str], application: Optional[str] = None, mcp_search_function=None, ) -> MaterialCompatibility: """ Screen materials for compatibility with specified environment. This is the main MCP tool function that will be registered with FastMCP. Args: environment: Environment description (e.g., "seawater, 35 g/L Cl, 25°C") candidates: List of material identifiers to screen (e.g., ["CS", "316L", "duplex"]) application: Optional application description (e.g., "heat_exchanger_tubes") mcp_search_function: Injected semantic search function Returns: MaterialCompatibility object with screening results Example: result = material_screening_query( environment="CO2-rich brine, 60°C, 35 g/L Cl, pCO2=0.5 bar", candidates=["CS", "316L", "duplex"], application="piping" ) print(result.compatibility) # "acceptable", "marginal", or "not_recommended" """ lookup = MaterialScreeningLookup(mcp_search_function) # Screen all candidates and return results for best/most compatible option # For now, combine all candidates in a single query to get comprehensive coverage materials_str = ", ".join(candidates) if candidates else "carbon steel" query_text = f"{materials_str} corrosion in {environment}" if application: query_text += f" for {application} application" result_dict = lookup.query(query_text) # Override material field to show all candidates screened if candidates: result_dict["material"] = materials_str result_dict["environment"] = f"{materials_str} corrosion in {environment}" # Convert to Pydantic model return MaterialCompatibility(**result_dict)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/puran-water/corrosion-engineering-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server