Skip to content

Commit aa8df76

Browse files
committed
feat: implement Ticket MCP integration with FastMCP client and add REST endpoints for ticket management
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent e3082ce commit aa8df76

3 files changed

Lines changed: 157 additions & 30 deletions

File tree

‎backend/agents.py‎

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,12 @@
4141

4242
# Local - Import operations registry for automatic tool discovery
4343
from api_decorators import get_langchain_tools
44-
4544
# Third-party - FastMCP client for external MCP servers
4645
from fastmcp import Client as MCPClient
4746
from langchain_core.tools import StructuredTool
48-
4947
# Third-party - LangChain and LangGraph
5048
from langchain_openai import AzureChatOpenAI
5149
from langgraph.prebuilt import create_react_agent
52-
5350
# Third-party - Pydantic for validation
5451
from pydantic import BaseModel, Field, create_model, field_validator
5552

@@ -143,8 +140,8 @@ class AgentResponse(BaseModel):
143140
AZURE_OPENAI_DEPLOYMENT = "gpt-5-mini"
144141
AZURE_OPENAI_API_VERSION = "2025-04-01-preview"
145142

146-
# External MCP server URL (hardcoded)
147-
MCP_SERVER_URL = "https://yodrrscbpxqnslgugwow.supabase.co/functions/v1/mcp"
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"
148145

149146

150147
# ============================================================================
@@ -262,48 +259,48 @@ def __init__(self):
262259
# These are the actual Python functions decorated with @tool
263260
self.tools = get_langchain_tools()
264261

265-
# MCP client state (lazy initialization)
266-
self._mcp_client: Optional[MCPClient] = None
267-
self._mcp_tools_loaded = False
262+
# Ticket MCP client state (lazy initialization)
263+
self._ticket_mcp_client: Optional[MCPClient] = None
264+
self._ticket_mcp_tools_loaded = False
268265

269-
async def _ensure_mcp_connection(self):
266+
async def _ensure_ticket_mcp_connection(self):
270267
"""
271-
Ensure MCP client is connected and tools are loaded.
268+
Ensure ticket MCP client is connected and tools are loaded.
272269
273-
Opens a persistent connection to the external MCP server and
270+
Opens a persistent connection to the external ticket MCP server and
274271
converts its tools to LangChain format. Called lazily on first
275272
agent run.
276273
"""
277-
if self._mcp_tools_loaded:
274+
if self._ticket_mcp_tools_loaded:
278275
return
279276

280277
try:
281-
# Create and connect MCP client (keep connection open)
282-
client = MCPClient(MCP_SERVER_URL)
278+
# Create and connect ticket MCP client (keep connection open)
279+
client = MCPClient(TICKET_MCP_SERVER_URL)
283280
await client.__aenter__()
284-
self._mcp_client = client
281+
self._ticket_mcp_client = client
285282

286-
# Fetch and convert MCP tools
283+
# Fetch and convert ticket MCP tools
287284
mcp_tools = await client.list_tools()
288285
for tool in mcp_tools:
289286
lc_tool = _mcp_tool_to_langchain(client, tool)
290287
self.tools.append(lc_tool)
291288

292-
print(f"DEBUG: Loaded {len(mcp_tools)} tools from MCP server {MCP_SERVER_URL}")
293-
self._mcp_tools_loaded = True
289+
print(f"DEBUG: Loaded {len(mcp_tools)} ticket tools from MCP server {TICKET_MCP_SERVER_URL}")
290+
self._ticket_mcp_tools_loaded = True
294291

295292
except Exception as e:
296-
print(f"WARNING: Failed to load MCP tools from {MCP_SERVER_URL}: {e}")
297-
# Continue without MCP tools - local tools still work
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
298295

299296
async def close(self):
300-
"""Close the MCP client connection."""
301-
if self._mcp_client:
297+
"""Close the ticket MCP client connection."""
298+
if self._ticket_mcp_client:
302299
try:
303-
await self._mcp_client.__aexit__(None, None, None)
300+
await self._ticket_mcp_client.__aexit__(None, None, None)
304301
except Exception:
305302
pass
306-
self._mcp_client = None
303+
self._ticket_mcp_client = None
307304

308305
async def run_agent(self, request: AgentRequest) -> AgentResponse:
309306
"""
@@ -329,8 +326,8 @@ async def run_agent(self, request: AgentRequest) -> AgentResponse:
329326
Raises:
330327
ValueError: If agent execution fails
331328
"""
332-
# Ensure MCP tools are loaded (lazy initialization)
333-
await self._ensure_mcp_connection()
329+
# Ensure ticket MCP tools are loaded (lazy initialization)
330+
await self._ensure_ticket_mcp_connection()
334331

335332
try:
336333
# Create ReAct agent with LangGraph tools

‎backend/app.py‎

Lines changed: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@
2929

3030
# Import unified operation system
3131
from api_decorators import operation
32-
from mcp import handle_mcp_request
33-
from ollama_service import ChatRequest, ChatResponse, ModelListResponse, OllamaService
32+
# FastMCP client for direct ticket MCP calls (no AI)
33+
from fastmcp import Client as MCPClient
34+
35+
# Ticket MCP server URL (same as in agents.py)
36+
TICKET_MCP_SERVER_URL = "https://yodrrscbpxqnslgugwow.supabase.co/functions/v1/mcp/a7f2b8c4-d3e9-4f1a-b5c6-e8d9f0123456"
37+
from mcp_handler import handle_mcp_request
38+
from ollama_service import (ChatRequest, ChatResponse, ModelListResponse,
39+
OllamaService)
3440
from pydantic import ValidationError
3541
from quart import Quart, jsonify, request, send_from_directory
3642
from quart_cors import cors
37-
3843
# Import Pydantic models and service
39-
from tasks import Task, TaskCreate, TaskFilter, TaskService, TaskStats, TaskUpdate
44+
from tasks import (Task, TaskCreate, TaskFilter, TaskService, TaskStats,
45+
TaskUpdate)
4046

4147
# ============================================================================
4248
# APPLICATION SETUP
@@ -299,6 +305,130 @@ async def rest_list_ollama_models():
299305
return jsonify({"error": str(e)}), 500
300306

301307

308+
# ============================================================================
309+
# TICKET MCP EXAMPLE - Direct FastMCP client usage (no AI)
310+
# ============================================================================
311+
312+
async def _call_ticket_mcp_tool(tool_name: str, args: dict | None = None) -> list[dict]:
313+
"""
314+
Helper: Call a tool on the Ticket MCP server and extract results.
315+
316+
This demonstrates using FastMCP client programmatically without any AI.
317+
The connection is opened, tool is called, and connection is closed.
318+
319+
Args:
320+
tool_name: Name of the MCP tool to call (e.g., "list_tickets")
321+
args: Optional dict of arguments for the tool
322+
323+
Returns:
324+
List of parsed JSON results from the tool response
325+
"""
326+
args = args or {}
327+
results = []
328+
329+
async with MCPClient(TICKET_MCP_SERVER_URL) as client:
330+
response = await client.call_tool(tool_name, args)
331+
332+
# Extract text content from MCP response
333+
if hasattr(response, 'content') and response.content:
334+
for content_item in response.content:
335+
# Only process TextContent items (use getattr for type safety)
336+
text = getattr(content_item, 'text', None)
337+
if text is not None and isinstance(text, str):
338+
try:
339+
# Parse JSON if possible
340+
results.append(json.loads(text))
341+
except json.JSONDecodeError:
342+
results.append({"text": text})
343+
344+
return results
345+
346+
return results
347+
348+
349+
@app.route("/api/tickets", methods=["GET"])
350+
async def rest_list_tickets():
351+
"""
352+
List tickets from external Ticket MCP server.
353+
354+
Example of calling MCP tools directly via FastMCP client.
355+
No AI involved - just pure MCP protocol.
356+
357+
Query params:
358+
- status: Filter by status (new, assigned, in_progress, etc.)
359+
- priority: Filter by priority (critical, high, medium, low)
360+
- search: Full-text search in summary/description
361+
- page: Page number (default: 1)
362+
- page_size: Results per page (default: 20)
363+
"""
364+
try:
365+
# Build args from query params
366+
args = {}
367+
for param in ["status", "priority", "city", "service", "search"]:
368+
if val := request.args.get(param):
369+
args[param] = val
370+
for param in ["page", "page_size"]:
371+
if val := request.args.get(param):
372+
args[param] = int(val)
373+
374+
results = await _call_ticket_mcp_tool("list_tickets", args)
375+
return jsonify(results[0] if len(results) == 1 else results), 200
376+
except Exception as e:
377+
return jsonify({"error": str(e)}), 500
378+
379+
380+
@app.route("/api/tickets/<ticket_id>", methods=["GET"])
381+
async def rest_get_ticket(ticket_id: str):
382+
"""
383+
Get a single ticket by ID from the Ticket MCP server.
384+
385+
Demonstrates calling MCP tool with path parameter.
386+
"""
387+
try:
388+
results = await _call_ticket_mcp_tool("get_ticket", {"ticket_id": ticket_id})
389+
if not results:
390+
return jsonify({"error": "Ticket not found"}), 404
391+
return jsonify(results[0]), 200
392+
except Exception as e:
393+
return jsonify({"error": str(e)}), 500
394+
395+
396+
@app.route("/api/tickets/stats", methods=["GET"])
397+
async def rest_get_ticket_stats():
398+
"""
399+
Get ticket statistics from the Ticket MCP server.
400+
401+
Returns aggregated counts by status, priority, service, city.
402+
"""
403+
try:
404+
args = {}
405+
if time_from := request.args.get("time_from"):
406+
args["time_from"] = time_from
407+
if time_to := request.args.get("time_to"):
408+
args["time_to"] = time_to
409+
410+
results = await _call_ticket_mcp_tool("get_ticket_stats", args)
411+
return jsonify(results[0] if len(results) == 1 else results), 200
412+
except Exception as e:
413+
return jsonify({"error": str(e)}), 500
414+
415+
416+
@app.route("/api/tickets/search", methods=["POST"])
417+
async def rest_search_tickets():
418+
"""
419+
Advanced ticket search with multiple filters.
420+
421+
Body JSON:
422+
- query: Full-text search string
423+
- filters: {status: [...], priority: [...], city: [...], service: [...]}
424+
- limit: Max results
425+
"""
426+
try:
427+
data = await request.get_json() or {}
428+
results = await _call_ticket_mcp_tool("search_tickets", data)
429+
return jsonify(results[0] if len(results) == 1 else results), 200
430+
except Exception as e:
431+
return jsonify({"error": str(e)}), 500
302432

303433

304434
# ============================================================================
File renamed without changes.

0 commit comments

Comments
 (0)