3232# Standard library
3333import os
3434from datetime import datetime
35- from typing import Literal , Optional
35+ from typing import Any , Literal , Optional
36+
37+ # Load environment variables before anything else
38+ from dotenv import load_dotenv
39+
40+ load_dotenv ()
3641
3742# Local - Import operations registry for automatic tool discovery
3843from api_decorators import get_langchain_tools
39-
44+ # Third-party - FastMCP client for external MCP servers
45+ from fastmcp import Client as MCPClient
46+ from langchain_core .tools import StructuredTool
4047# Third-party - LangChain and LangGraph
41- from langchain_openai import ChatOpenAI
48+ from langchain_openai import AzureChatOpenAI
4249from langgraph .prebuilt import create_react_agent
43-
4450# Third-party - Pydantic for validation
45- from pydantic import BaseModel , Field , field_validator
51+ from pydantic import BaseModel , Field , create_model , field_validator
4652
4753# ============================================================================
4854# DATA MODELS - Pydantic for validation and schema generation
@@ -125,15 +131,84 @@ class AgentResponse(BaseModel):
125131
126132
127133# ============================================================================
128- # CONFIGURATION - Azure OpenAI settings from environment
134+ # CONFIGURATION - Azure OpenAI settings (hardcoded except API key)
135+ # ============================================================================
136+
137+ # Azure OpenAI configuration - only API key from environment
138+ AZURE_OPENAI_ENDPOINT = "https://can-i-haz-houze-resource.cognitiveservices.azure.com"
139+ AZURE_OPENAI_API_KEY = os .getenv ("AZURE_API_KEY" , "" )
140+ AZURE_OPENAI_DEPLOYMENT = "gpt-5-mini"
141+ AZURE_OPENAI_API_VERSION = "2025-04-01-preview"
142+
143+ # External MCP server URL for ticket management (hardcoded)
144+ TICKET_MCP_SERVER_URL = "https://yodrrscbpxqnslgugwow.supabase.co/functions/v1/mcp/a7f2b8c4-d3e9-4f1a-b5c6-e8d9f0123456"
145+
146+
147+ # ============================================================================
148+ # MCP TOOL CONVERSION HELPERS
129149# ============================================================================
130150
131- # Azure OpenAI configuration
132- # These should be set in .env file (see .env.example)
133- AZURE_OPENAI_ENDPOINT = os .getenv ("AZURE_OPENAI_ENDPOINT" , "" )
134- AZURE_OPENAI_API_KEY = os .getenv ("AZURE_OPENAI_API_KEY" , "" )
135- AZURE_OPENAI_DEPLOYMENT = os .getenv ("AZURE_OPENAI_DEPLOYMENT" , "gpt-4" )
136- AZURE_OPENAI_API_VERSION = os .getenv ("AZURE_OPENAI_API_VERSION" , "2024-02-15-preview" )
151+ def _json_type_to_python (json_type : str ) -> type :
152+ """Map JSON schema type to Python type."""
153+ mapping = {
154+ "string" : str ,
155+ "integer" : int ,
156+ "number" : float ,
157+ "boolean" : bool ,
158+ "array" : list ,
159+ "object" : dict ,
160+ }
161+ return mapping .get (json_type , str )
162+
163+
164+ def _schema_to_pydantic (name : str , schema : dict ) -> type [BaseModel ]:
165+ """Convert JSON schema to Pydantic model for LangChain tool args."""
166+ properties = schema .get ("properties" , {})
167+ required = set (schema .get ("required" , []))
168+
169+ fields : dict [str , Any ] = {}
170+ for field_name , field_schema in properties .items ():
171+ field_type = _json_type_to_python (field_schema .get ("type" , "string" ))
172+ field_desc = field_schema .get ("description" , f"{ field_name } parameter" )
173+
174+ if field_name in required :
175+ fields [field_name ] = (field_type , Field (description = field_desc ))
176+ else :
177+ default = field_schema .get ("default" )
178+ fields [field_name ] = (Optional [field_type ], Field (default = default , description = field_desc ))
179+
180+ model_name = f"{ name .title ().replace ('_' , '' ).replace ('-' , '' )} Args"
181+ return create_model (model_name , ** fields )
182+
183+
184+ def _mcp_tool_to_langchain (mcp_client : MCPClient , tool : Any ) -> StructuredTool :
185+ """
186+ Convert MCP tool to LangChain StructuredTool.
187+
188+ Creates a wrapper that calls the external MCP server via the persistent client.
189+ """
190+ tool_name = tool .name
191+ tool_desc = tool .description or f"MCP tool: { tool_name } "
192+ input_schema = tool .inputSchema if hasattr (tool , 'inputSchema' ) else {}
193+
194+ # Create async wrapper that calls MCP server
195+ async def call_mcp_tool (** kwargs ) -> str :
196+ result = await mcp_client .call_tool (tool_name , kwargs )
197+ # Extract text from MCP response
198+ if hasattr (result , 'content' ) and result .content :
199+ texts = [c .text for c in result .content if hasattr (c , 'text' )]
200+ return "\n " .join (texts ) if texts else str (result )
201+ return str (result )
202+
203+ # Build Pydantic model from input schema
204+ args_model = _schema_to_pydantic (tool_name , input_schema )
205+
206+ return StructuredTool (
207+ name = tool_name ,
208+ description = tool_desc ,
209+ coroutine = call_mcp_tool ,
210+ args_schema = args_model ,
211+ )
137212
138213
139214# ============================================================================
@@ -147,6 +222,7 @@ class AgentService:
147222 Consolidated operations:
148223 - Agent initialization with Azure OpenAI
149224 - Tool discovery from @operation registry
225+ - External MCP server tool integration (persistent connection)
150226 - ReAct agent execution with task tools
151227 - Response formatting and error handling
152228
@@ -165,24 +241,66 @@ def __init__(self):
165241 ValueError: If Azure OpenAI configuration is incomplete
166242 """
167243 # Validate configuration
168- if not AZURE_OPENAI_ENDPOINT or not AZURE_OPENAI_API_KEY :
244+ if not AZURE_OPENAI_API_KEY :
169245 raise ValueError (
170- "Azure OpenAI configuration is incomplete. "
171- "Please set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY "
172- "environment variables. See .env.example for template."
246+ "Azure OpenAI API key not set. "
247+ "Please set AZURE_API_KEY environment variable."
173248 )
174249
175- # Initialize ChatOpenAI with Azure endpoint using base_url pattern
176- self .llm = ChatOpenAI (
177- base_url = AZURE_OPENAI_ENDPOINT ,
250+ # Initialize AzureChatOpenAI with hardcoded endpoint
251+ self .llm = AzureChatOpenAI (
252+ azure_endpoint = AZURE_OPENAI_ENDPOINT ,
178253 api_key = AZURE_OPENAI_API_KEY , # type: ignore
179- model = AZURE_OPENAI_DEPLOYMENT ,
180- temperature = 0.7 ,
254+ azure_deployment = AZURE_OPENAI_DEPLOYMENT ,
255+ api_version = AZURE_OPENAI_API_VERSION ,
181256 )
182257
183258 # Load LangGraph tools from operation registry
184259 # These are the actual Python functions decorated with @tool
185260 self .tools = get_langchain_tools ()
261+
262+ # Ticket MCP client state (lazy initialization)
263+ self ._ticket_mcp_client : Optional [MCPClient ] = None
264+ self ._ticket_mcp_tools_loaded = False
265+
266+ async def _ensure_ticket_mcp_connection (self ):
267+ """
268+ Ensure ticket MCP client is connected and tools are loaded.
269+
270+ Opens a persistent connection to the external ticket MCP server and
271+ converts its tools to LangChain format. Called lazily on first
272+ agent run.
273+ """
274+ if self ._ticket_mcp_tools_loaded :
275+ return
276+
277+ try :
278+ # Create and connect ticket MCP client (keep connection open)
279+ client = MCPClient (TICKET_MCP_SERVER_URL )
280+ await client .__aenter__ ()
281+ self ._ticket_mcp_client = client
282+
283+ # Fetch and convert ticket MCP tools
284+ mcp_tools = await client .list_tools ()
285+ for tool in mcp_tools :
286+ lc_tool = _mcp_tool_to_langchain (client , tool )
287+ self .tools .append (lc_tool )
288+
289+ print (f"DEBUG: Loaded { len (mcp_tools )} ticket tools from MCP server { TICKET_MCP_SERVER_URL } " )
290+ self ._ticket_mcp_tools_loaded = True
291+
292+ except Exception as e :
293+ print (f"WARNING: Failed to load ticket MCP tools from { TICKET_MCP_SERVER_URL } : { e } " )
294+ # Continue without ticket MCP tools - local tools still work
295+
296+ async def close (self ):
297+ """Close the ticket MCP client connection."""
298+ if self ._ticket_mcp_client :
299+ try :
300+ await self ._ticket_mcp_client .__aexit__ (None , None , None )
301+ except Exception :
302+ pass
303+ self ._ticket_mcp_client = None
186304
187305 async def run_agent (self , request : AgentRequest ) -> AgentResponse :
188306 """
@@ -197,6 +315,7 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
197315
198316 All @operation decorated functions are automatically available as LangGraph tools.
199317 LangGraph calls the Python functions directly (not via MCP).
318+ External MCP tools are loaded from the configured MCP server.
200319
201320 Args:
202321 request: AgentRequest with prompt and agent type
@@ -207,6 +326,9 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
207326 Raises:
208327 ValueError: If agent execution fails
209328 """
329+ # Ensure ticket MCP tools are loaded (lazy initialization)
330+ await self ._ensure_ticket_mcp_connection ()
331+
210332 try :
211333 # Create ReAct agent with LangGraph tools
212334 # The tools are the actual Python functions with @tool decorator
0 commit comments