sdlc-agent / mcp /mcp_server.py
Veeru-c's picture
initial commit
84e4a04
import gradio as gr
from typing import Dict, List, Optional
import json
from datetime import datetime
import os
from difflib import SequenceMatcher
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# ===== Configuration =====
class Config:
# JIRA Configuration (optional - uses mock if not provided)
JIRA_URL = os.getenv("JIRA_URL", "")
JIRA_EMAIL = os.getenv("JIRA_EMAIL", "")
JIRA_API_TOKEN = os.getenv("JIRA_API_TOKEN", "")
JIRA_PROJECT_KEY = os.getenv("JIRA_PROJECT_KEY", "PROJ")
# RAG Configuration
RAG_ENABLED = os.getenv("RAG_ENABLED", "false").lower() == "true"
RAG_API_URL = os.getenv("RAG_API_URL", "")
VECTOR_DB_PATH = os.getenv("VECTOR_DB_PATH", "./data/vectordb")
# Fine-tuning Configuration
FINETUNED_MODEL_PATH = os.getenv("FINETUNED_MODEL_PATH", "")
FINETUNED_MODEL_API_URL = os.getenv("FINETUNED_MODEL_API_URL", "")
FINETUNED_MODEL_TYPE = os.getenv("FINETUNED_MODEL_TYPE", "general")
# MCP Server
MCP_PORT = int(os.getenv("MCP_PORT", "7860"))
config = Config()
# ===== Mock Data Storage =====
mock_epics = [
{
"key": "PROJ-100",
"summary": "User Authentication System",
"description": "Implement comprehensive user authentication with OAuth2, JWT tokens, and MFA",
"status": "In Progress",
"created": "2024-01-15"
},
{
"key": "PROJ-101",
"summary": "Payment Gateway Integration",
"description": "Integrate Stripe and PayPal payment gateways with webhook support",
"status": "Done",
"created": "2024-02-01"
},
{
"key": "PROJ-102",
"summary": "Real-time Notification System",
"description": "Build WebSocket-based notification system with push notifications",
"status": "To Do",
"created": "2024-03-10"
}
]
mock_user_stories = []
# ===== Helper Functions =====
def calculate_similarity(text1: str, text2: str) -> float:
"""Calculate similarity between two strings (0.0 to 1.0)"""
return SequenceMatcher(None, text1.lower(), text2.lower()).ratio()
def use_real_jira() -> bool:
"""Check if real JIRA credentials are configured"""
return bool(config.JIRA_URL and config.JIRA_EMAIL and config.JIRA_API_TOKEN)
# ===== RAG Functions =====
def query_rag(requirement: str) -> Dict:
"""
Query the RAG system for product specifications based on the requirement.
"""
print(f"[RAG] Querying with requirement: {requirement[:50]}...")
if config.RAG_ENABLED and config.RAG_API_URL:
try:
import requests
print(f"[RAG] Calling remote endpoint: {config.RAG_API_URL}")
response = requests.post(
config.RAG_API_URL,
json={"question": requirement, "top_k": 5},
headers={"Content-Type": "application/json"},
timeout=60
)
if response.ok:
result = response.json()
answer = result.get("answer", "")
sources = result.get("sources", [])
# Parse the answer to extract structured fields if possible
# For now, we'll wrap the answer in our standard structure
return {
"status": "success",
"specification": {
"title": "Product Specification (RAG Generated)",
"summary": answer[:200] + "...",
"features": [line.strip('- ') for line in answer.split('\n') if line.strip().startswith('-')],
"technical_requirements": ["Derived from product design docs"],
"acceptance_criteria": ["See detailed RAG answer"],
"estimated_effort": "TBD",
"full_answer": answer,
"context_retrieved": len(sources)
},
"source": "real_rag",
"timestamp": datetime.now().isoformat()
}
else:
print(f"[RAG] Error: {response.status_code} - {response.text}")
except Exception as e:
print(f"[RAG] Exception: {e}")
# Mock response fallback
print("[RAG] Using mock response")
# Simulate processing time
# time.sleep(1)
# Simple keyword matching for mock data
req_lower = requirement.lower()
spec = {
"title": "Auto Insurance Product Spec",
"summary": "Specification based on Tokyo market requirements.",
"features": [
"User registration and login",
"Policy selection interface",
"Premium calculation engine"
],
"technical_requirements": [
"Secure database for user data",
"Integration with payment gateway",
"Responsive web design"
],
"acceptance_criteria": [
"User can create an account",
"User can view policy details",
"Premium is calculated correctly"
],
"estimated_effort": "2 weeks"
}
if "mobile" in req_lower or "app" in req_lower:
spec["title"] = "Mobile App Specification"
spec["features"].append("Push notifications")
spec["technical_requirements"].append("iOS and Android support")
if "ai" in req_lower or "agent" in req_lower:
spec["title"] = "AI Agent Integration Spec"
spec["features"].append("Chat interface")
spec["technical_requirements"].append("LLM integration")
return {
"status": "success",
"specification": spec,
"source": "mock_rag",
"timestamp": datetime.now().isoformat()
}
# ===== Fine-tuning Functions =====
def query_finetuned_model(requirement: str, domain: str = "general") -> Dict:
"""
Query fine-tuned model for domain-specific insights.
Args:
requirement: User's requirement text
domain: Domain type (insurance, finance, healthcare, etc.)
Returns:
Dict with domain-specific recommendations and insights
"""
print(f"[Fine-tuning] Querying {domain} model with requirement: {requirement[:50]}...")
if config.FINETUNED_MODEL_API_URL:
try:
import requests
print(f"[Fine-tuning] Calling remote endpoint: {config.FINETUNED_MODEL_API_URL}")
# Map inputs to the API expected format
payload = {
"question": requirement,
"context": f"Domain: {domain}. Provide specific insights for this domain."
}
response = requests.post(
config.FINETUNED_MODEL_API_URL,
json=payload,
headers={"Content-Type": "application/json"},
timeout=60
)
if response.ok:
result = response.json()
answer = result.get("answer", "")
latency = result.get("latency_ms", 0)
return {
"status": "success",
"insights": {
"domain": domain,
"recommendations": [line.strip('- ') for line in answer.split('\n') if line.strip().startswith('-')],
"compliance_notes": ["Generated by fine-tuned model"],
"full_response": answer
},
"source": "real_finetuned_model",
"latency_ms": latency,
"timestamp": datetime.now().isoformat()
}
else:
print(f"[Fine-tuning] Error: {response.status_code} - {response.text}")
except Exception as e:
print(f"[Fine-tuning] Exception: {e}")
# Mock response fallback
print("[Fine-tuning] Using mock response")
insights = {
"domain": domain,
"recommendations": [
"Ensure GDPR compliance for user data",
"Implement audit logging for all transactions",
"Use industry-standard encryption"
],
"compliance_notes": [
"ISO 27001 certification recommended",
"Regular security assessments required"
]
}
if domain == "insurance":
insights["recommendations"] = [
"Verify policy holder identity (KYC)",
"Calculate risk score based on actuarial tables",
"Generate compliant policy documents"
]
insights["compliance_notes"].append("Comply with local insurance regulations")
elif domain == "finance":
insights["recommendations"] = [
"Implement PCI-DSS for payment processing",
"Real-time fraud detection",
"Double-entry bookkeeping"
]
insights["compliance_notes"].append("Financial Services Agency guidelines")
return {
"status": "success",
"insights": insights,
"source": "mock_finetuned_model",
"timestamp": datetime.now().isoformat()
}
from jira import JIRA
# ... (imports remain the same)
# ===== JIRA Functions =====
def get_jira_client():
"""Get authenticated JIRA client"""
if not use_real_jira():
return None
return JIRA(
server=config.JIRA_URL,
basic_auth=(config.JIRA_EMAIL, config.JIRA_API_TOKEN),
options={"rest_api_version": "3"}
)
def search_jira_epics(keywords: str, similarity_threshold: float = 0.6) -> Dict:
"""
Search for existing JIRA epics matching the keywords.
"""
print(f"[JIRA] Searching epics with keywords: {keywords}")
if use_real_jira():
try:
# Use direct REST API call to avoid deprecated GET endpoint
import requests
from requests.auth import HTTPBasicAuth
jql = f'project = "{config.JIRA_PROJECT_KEY}" AND issuetype = Epic AND (summary ~ "{keywords}" OR description ~ "{keywords}")'
print(f"[JIRA] JQL: {jql}")
# Ensure no trailing slash in base URL
base_url = config.JIRA_URL.rstrip('/')
# Try standard POST search endpoint first
api_url = f"{base_url}/rest/api/3/search"
auth = HTTPBasicAuth(config.JIRA_EMAIL, config.JIRA_API_TOKEN)
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
payload = {
"jql": jql,
"maxResults": 5,
"fields": ["summary", "description", "status", "created"]
}
print(f"[JIRA] POST to {api_url}")
response = requests.post(api_url, json=payload, headers=headers, auth=auth)
# If standard search fails with 410, try the specific endpoint mentioned in error
if response.status_code == 410:
print("[JIRA] 410 Error, trying /rest/api/3/search/jql endpoint...")
api_url = f"{base_url}/rest/api/3/search/jql"
# The /search/jql endpoint uses a slightly different payload structure
# It expects 'jql' as a query parameter or in body?
# Actually, strictly following the error message recommendation.
# Documentation says POST /rest/api/3/search/jql takes { "jql": "...", ... } just like search
print(f"[JIRA] POST to {api_url}")
response = requests.post(api_url, json=payload, headers=headers, auth=auth)
if not response.ok:
print(f"[JIRA] Error response: {response.text}")
response.raise_for_status()
data = response.json()
# /search/jql returns { "issues": [...] } just like /search?
# Or does it return a different structure?
# Standard /search returns { "issues": [...], "total": ... }
# Let's handle both cases safely
issues = data.get("issues", [])
if "issues" not in data and isinstance(data, list):
# Some endpoints return list directly
issues = data
matching_epics = []
for issue in issues:
fields = issue.get("fields", {})
# Calculate similarity for ranking
summary_text = fields.get("summary", "")
desc_text = fields.get("description", "")
# Description can be complex object in v3 (ADF), handle string or dict
if isinstance(desc_text, dict):
# Simplified handling for ADF - just use summary for similarity if desc is complex
desc_text = ""
summary_sim = calculate_similarity(keywords, summary_text)
desc_sim = calculate_similarity(keywords, str(desc_text))
max_sim = max(summary_sim, desc_sim)
matching_epics.append({
"key": issue.get("key"),
"summary": summary_text,
"description": str(desc_text)[:200] + "..." if desc_text else "",
"status": str(fields.get("status", {}).get("name", "Unknown")),
"created": fields.get("created"),
"url": f"{config.JIRA_URL}/browse/{issue.get('key')}",
"similarity_score": round(max_sim, 2)
})
# Sort by similarity
matching_epics.sort(key=lambda x: x["similarity_score"], reverse=True)
return {
"status": "success",
"count": len(matching_epics),
"epics": matching_epics,
"source": "real_jira",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
print(f"[JIRA] Search error: {e}")
return {"status": "error", "message": str(e)}
# Mock search - find similar epics
matching_epics = []
for epic in mock_epics:
# Calculate similarity with summary and description
summary_sim = calculate_similarity(keywords, epic["summary"])
desc_sim = calculate_similarity(keywords, epic["description"])
max_sim = max(summary_sim, desc_sim)
if max_sim >= similarity_threshold:
matching_epics.append({
**epic,
"similarity_score": round(max_sim, 2)
})
# Sort by similarity
matching_epics.sort(key=lambda x: x["similarity_score"], reverse=True)
return {
"status": "success",
"count": len(matching_epics),
"epics": matching_epics,
"source": "mock_jira",
"timestamp": datetime.now().isoformat()
}
# Helper for Atlassian Document Format (ADF)
def create_adf_description(text: str) -> Dict:
"""Convert plain text to Atlassian Document Format (ADF)"""
if not text:
return {
"version": 1,
"type": "doc",
"content": []
}
return {
"version": 1,
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": text
}
]
}
]
}
def create_jira_epic(summary: str, description: str, project_key: str = None) -> Dict:
"""
Create a new JIRA epic.
"""
project_key = project_key or config.JIRA_PROJECT_KEY
print(f"[JIRA] Creating epic: {summary}")
if use_real_jira():
try:
jira = get_jira_client()
# Use ADF format for description in API v3
description_adf = create_adf_description(description)
epic_dict = {
'project': {'key': project_key},
'summary': summary,
'description': description_adf,
'issuetype': {'name': 'Epic'},
}
new_issue = jira.create_issue(fields=epic_dict)
print(f"[JIRA] Created epic: {new_issue.key}")
return {
"status": "success",
"epic": {
"key": new_issue.key,
"summary": summary,
"description": description,
"url": f"{config.JIRA_URL}/browse/{new_issue.key}"
},
"source": "real_jira",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
print(f"[JIRA] Create error: {e}")
return {"status": "error", "message": str(e)}
# Mock epic creation
epic_key = f"{project_key}-{len(mock_epics) + 100}"
new_epic = {
"key": epic_key,
"summary": summary,
"description": description,
"status": "To Do",
"created": datetime.now().strftime("%Y-%m-%d"),
"url": f"{config.JIRA_URL or 'https://mock-jira.atlassian.net'}/browse/{epic_key}"
}
mock_epics.append(new_epic)
return {
"status": "success",
"epic": new_epic,
"source": "mock_jira",
"timestamp": datetime.now().isoformat()
}
def create_jira_user_story(epic_key: str, summary: str, description: str,
story_points: int = None) -> Dict:
"""
Create a new JIRA user story linked to an epic.
"""
print(f"[JIRA] Creating user story under {epic_key}: {summary}")
# Extract actual key if format is "KEY: Summary"
actual_epic_key = epic_key.split(':')[0].strip()
if use_real_jira():
try:
jira = get_jira_client()
# Use ADF format for description in API v3
description_adf = create_adf_description(description)
story_dict = {
'project': {'key': actual_epic_key.split('-')[0]},
'summary': summary,
'description': description_adf,
'issuetype': {'name': 'Story'},
# Link to Epic - field name varies by JIRA instance, usually 'parent' for Next-Gen or 'customfield_XXXXX'
# Trying standard 'parent' first for modern JIRA Cloud
'parent': {'key': actual_epic_key}
}
new_issue = jira.create_issue(fields=story_dict)
print(f"[JIRA] Created story: {new_issue.key}")
return {
"status": "success",
"story": {
"key": new_issue.key,
"summary": summary,
"url": f"{config.JIRA_URL}/browse/{new_issue.key}"
},
"source": "real_jira",
"timestamp": datetime.now().isoformat()
}
except Exception as e:
print(f"[JIRA] Create story error: {e}")
return {"status": "error", "message": str(e)}
# Mock story creation
story_key = f"{epic_key.split('-')[0]}-{len(mock_user_stories) + 200}"
new_story = {
"key": story_key,
"epic_key": epic_key,
"summary": summary,
"description": description,
"story_points": story_points,
"status": "To Do",
"created": datetime.now().strftime("%Y-%m-%d"),
"url": f"{config.JIRA_URL or 'https://mock-jira.atlassian.net'}/browse/{story_key}"
}
mock_user_stories.append(new_story)
return {
"status": "success",
"story": new_story,
"source": "mock_jira",
"timestamp": datetime.now().isoformat()
}
# ===== Helper Functions =====
def get_available_epics() -> List[str]:
"""Get list of available epics for dropdown"""
epics_list = []
if use_real_jira():
try:
# Use direct REST API call to avoid deprecated GET endpoint
import requests
from requests.auth import HTTPBasicAuth
# Ensure no trailing slash in base URL
base_url = config.JIRA_URL.rstrip('/')
api_url = f"{base_url}/rest/api/3/search"
auth = HTTPBasicAuth(config.JIRA_EMAIL, config.JIRA_API_TOKEN)
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
# Search for all epics in project
jql = f'project = "{config.JIRA_PROJECT_KEY}" AND issuetype = Epic ORDER BY created DESC'
payload = {
"jql": jql,
"maxResults": 20,
"fields": ["summary"]
}
response = requests.post(api_url, json=payload, headers=headers, auth=auth)
# Handle 410 fallback
if response.status_code == 410:
api_url = f"{base_url}/rest/api/3/search/jql"
response = requests.post(api_url, json=payload, headers=headers, auth=auth)
if response.ok:
data = response.json()
issues = data.get("issues", [])
if "issues" not in data and isinstance(data, list):
issues = data
for issue in issues:
key = issue.get("key")
summary = issue.get("fields", {}).get("summary", "")
epics_list.append(f"{key}: {summary}")
except Exception as e:
print(f"[JIRA] Error fetching epics: {e}")
else:
# Mock mode
for epic in mock_epics:
epics_list.append(f"{epic['key']}: {epic['summary']}")
return epics_list
def refresh_epics_dropdown():
"""Refresh the choices for the epic dropdown"""
choices = get_available_epics()
if not choices:
return gr.Dropdown.update(choices=[], value=None, label="No Epics Found - Please Create an Epic First")
return gr.Dropdown.update(choices=choices, value=choices[0] if choices else None, label="Select Epic")
# ===== Gradio Interface =====
def create_gradio_interface():
"""Create Gradio interface for MCP server"""
with gr.Blocks(title="AI Development Agent MCP Server") as app:
gr.Markdown("# πŸ€– AI Development Agent MCP Server")
gr.Markdown("Unified interface for RAG, Fine-tuning, and JIRA integration")
with gr.Tab("RAG Query"):
with gr.Row():
rag_input = gr.Textbox(
label="Requirement",
placeholder="Enter your requirement...",
lines=5
)
rag_btn = gr.Button("Query RAG System", variant="primary")
rag_output = gr.JSON(label="RAG Response")
rag_btn.click(query_rag, inputs=[rag_input], outputs=[rag_output])
with gr.Tab("Fine-tuned Model"):
with gr.Row():
ft_input = gr.Textbox(
label="Requirement",
placeholder="Enter your requirement...",
lines=5
)
ft_domain = gr.Dropdown(
choices=["general", "insurance", "finance", "healthcare"],
value="general",
label="Domain"
)
ft_btn = gr.Button("Query Fine-tuned Model", variant="primary")
ft_output = gr.JSON(label="Fine-tuned Model Response")
ft_btn.click(
query_finetuned_model,
inputs=[ft_input, ft_domain],
outputs=[ft_output]
)
with gr.Tab("JIRA - Search Epics"):
search_input = gr.Textbox(
label="Search Keywords",
placeholder="Enter keywords to search...",
lines=2
)
search_threshold = gr.Slider(
minimum=0.0,
maximum=1.0,
value=0.6,
step=0.1,
label="Similarity Threshold"
)
search_btn = gr.Button("Search Epics", variant="primary")
search_output = gr.JSON(label="Search Results")
search_btn.click(
search_jira_epics,
inputs=[search_input, search_threshold],
outputs=[search_output]
)
with gr.Tab("JIRA - Create Epic"):
epic_summary = gr.Textbox(label="Epic Summary", placeholder="Epic title...")
epic_desc = gr.Textbox(
label="Epic Description",
placeholder="Detailed description...",
lines=5
)
epic_project = gr.Textbox(
label="Project Key",
value=config.JIRA_PROJECT_KEY,
placeholder="PROJ"
)
create_epic_btn = gr.Button("Create Epic", variant="primary")
epic_output = gr.JSON(label="Created Epic")
create_epic_btn.click(
create_jira_epic,
inputs=[epic_summary, epic_desc, epic_project],
outputs=[epic_output]
)
with gr.Tab("JIRA - Create User Story"):
with gr.Row():
initial_epics = get_available_epics()
story_epic = gr.Dropdown(
choices=initial_epics,
value=initial_epics[0] if initial_epics else None,
label="Select Epic" if initial_epics else "No Epics Found - Please Create an Epic First",
allow_custom_value=True # Allow typing if needed, or strictly selection
)
refresh_btn = gr.Button("πŸ”„ Refresh Epics")
story_summary = gr.Textbox(label="Story Summary", placeholder="Story title...")
story_desc = gr.Textbox(
label="Story Description",
placeholder="Detailed description...",
lines=5
)
story_points = gr.Number(label="Story Points (optional)", value=None)
create_story_btn = gr.Button("Create User Story", variant="primary")
story_output = gr.JSON(label="Created Story")
refresh_btn.click(refresh_epics_dropdown, outputs=[story_epic])
create_story_btn.click(
create_jira_user_story,
inputs=[story_epic, story_summary, story_desc, story_points],
outputs=[story_output]
)
with gr.Tab("Configuration"):
gr.Markdown(f"""
### Current Configuration
**JIRA:**
- URL: `{config.JIRA_URL or 'Not configured (using mock)'}`
- Project: `{config.JIRA_PROJECT_KEY}`
- Mode: `{'Real JIRA' if use_real_jira() else 'Mock Mode'}`
**RAG:**
- Enabled: `{config.RAG_ENABLED}`
- Vector DB: `{config.VECTOR_DB_PATH}`
**Fine-tuned Model:**
- Path: `{config.FINETUNED_MODEL_PATH or 'Not configured (using mock)'}`
- Type: `{config.FINETUNED_MODEL_TYPE}`
**MCP Server:**
- Port: `{config.MCP_PORT}`
---
To enable real integrations, set environment variables:
```bash
export JIRA_URL="https://your-domain.atlassian.net"
export JIRA_EMAIL="your-email@example.com"
export JIRA_API_TOKEN="your-api-token"
export JIRA_PROJECT_KEY="PROJ"
export RAG_ENABLED="true"
export FINETUNED_MODEL_PATH="/path/to/model"
```
""")
return app
# ===== Main =====
if __name__ == "__main__":
print("πŸš€ Starting AI Development Agent MCP Server...")
print(f"πŸ“ Server URL: http://localhost:{config.MCP_PORT}")
print(f"πŸ”§ Mode: {'Real JIRA' if use_real_jira() else 'Mock Mode'}")
print(f"🧠 RAG: {'Enabled' if config.RAG_ENABLED else 'Mock'}")
print(f"🎯 Fine-tuned Model: {'Enabled' if config.FINETUNED_MODEL_API_URL else 'Mock'}")
app = create_gradio_interface()
app.launch(
server_name="0.0.0.0",
server_port=config.MCP_PORT,
share=False
)