NPTools / app.py
Nelson André
Add input validation and update documentation
9587243
Raw
History Blame Contribute Delete
13.8 kB
import gradio as gr
import httpx
import json
import re
BASE_URL = "https://nelsonandre.outsystemscloud.com/MCPServer/rest/MCP"
MAX_STRING_LENGTH = 500
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
def validate_employee_id(employee_id) -> int:
"""Validate and convert employee ID to a positive integer."""
try:
eid = int(employee_id)
if eid <= 0:
raise ValueError("Employee ID must be a positive integer")
return eid
except (TypeError, ValueError) as e:
raise ValueError(f"Invalid employee ID: {e}")
def validate_string(value: str, field_name: str, required: bool = False, max_length: int = MAX_STRING_LENGTH) -> str:
"""Validate and sanitize string input."""
if value is None:
value = ""
value = str(value).strip()
if required and not value:
raise ValueError(f"{field_name} is required")
if len(value) > max_length:
raise ValueError(f"{field_name} exceeds maximum length of {max_length}")
return value
def validate_date(value: str, field_name: str) -> str:
"""Validate date format (YYYY-MM-DD)."""
if not value:
return ""
if not DATE_PATTERN.match(value):
raise ValueError(f"{field_name} must be in YYYY-MM-DD format")
return value
def validate_nif(value) -> int:
"""Validate NIF as a non-negative integer."""
try:
nif = int(value) if value else 0
if nif < 0:
raise ValueError("NIF cannot be negative")
return nif
except (TypeError, ValueError):
raise ValueError("NIF must be a valid integer")
async def get_employees() -> list[dict]:
"""
Return a list of employees by calling an external web service.
Returns:
list[dict]: Response from the web service with columns:
Name (str): Name of the employee (required)
NIF (int): Tax identification number (defaults to 0)
DateOfBirth (str): Date of birth in YYYY-MM-DD format (defaults to empty string)
Address (str): Employee's address (defaults to empty string)
CreatedOn (date time): Date and time when the employee was created (defaults to empty string)
Phone (str): Employee's phone number (defaults to empty string)
"""
async with httpx.AsyncClient() as client:
try:
response = await client.get(f"{BASE_URL}/GetEmployees")
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
raise Exception(f"Failed to fetch employees (HTTP {e.response.status_code})")
except Exception:
raise Exception("Failed to connect to employee service")
async def get_employee_by_id(employee_id: int) -> dict:
"""
Return a specific employee by ID.
Args:
employee_id (int): The unique identifier of the employee
Returns:
dict: Employee data with fields:
Id (int): Employee ID
Name (str): Name of the employee
NIF (int): Tax identification number
DateOfBirth (str): Date of birth in YYYY-MM-DD format
Address (str): Employee's address
CreatedOn (datetime): Date and time when the employee was created
Phone (str): Employee's phone number
"""
validated_id = validate_employee_id(employee_id)
async with httpx.AsyncClient() as client:
try:
response = await client.get(f"{BASE_URL}/GetEmployeeBy", params={"EmployeeId": validated_id})
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
raise Exception("Employee not found")
raise Exception(f"Failed to fetch employee (HTTP {e.response.status_code})")
except ValueError as e:
raise Exception(str(e))
except Exception:
raise Exception("Failed to connect to employee service")
async def create_employee(name: str, nif: int = 0, date_of_birth: str = "", address: str = "") -> str:
"""
Create a new employee.
Args:
name (str): Name of the employee (required)
nif (int): Tax identification number (optional, defaults to 0)
date_of_birth (str): Date of birth in YYYY-MM-DD format (optional)
address (str): Employee's address (optional)
Returns:
str: Result message from the server
"""
# Validate all inputs
validated_name = validate_string(name, "Name", required=True)
validated_nif = validate_nif(nif)
validated_dob = validate_date(date_of_birth, "Date of birth")
validated_address = validate_string(address, "Address", required=False)
async with httpx.AsyncClient() as client:
try:
payload = {
"EmployeeName": validated_name,
"NIF": validated_nif,
"DateOfBirth": validated_dob,
"Address": validated_address
}
response = await client.post(f"{BASE_URL}/EmployeeCreate", json=payload)
response.raise_for_status()
return response.text
except httpx.HTTPStatusError as e:
raise Exception(f"Failed to create employee (HTTP {e.response.status_code})")
except ValueError as e:
raise Exception(str(e))
except Exception:
raise Exception("Failed to connect to employee service")
# MCP Protocol handler
async def handle_mcp_request(request_data: str) -> str:
"""Handle MCP protocol requests"""
try:
request = json.loads(request_data)
if request.get("method") == "tools/list":
# Return available tools
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {
"tools": [
{
"name": "get_employees",
"description": "Get list of all employees",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "get_employee_by_id",
"description": "Get a specific employee by their ID",
"inputSchema": {
"type": "object",
"properties": {
"employee_id": {
"type": "integer",
"description": "The unique identifier of the employee"
}
},
"required": ["employee_id"]
}
},
{
"name": "create_employee",
"description": "Create a new employee record",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the employee"
},
"nif": {
"type": "integer",
"description": "Tax identification number (optional)"
},
"date_of_birth": {
"type": "string",
"description": "Date of birth in YYYY-MM-DD format (optional)"
},
"address": {
"type": "string",
"description": "Employee's address (optional)"
}
},
"required": ["name"]
}
}
]
}
})
elif request.get("method") == "tools/call":
tool_name = request.get("params", {}).get("name")
arguments = request.get("params", {}).get("arguments", {})
if tool_name == "get_employees":
result = await get_employees()
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {
"content": [{
"type": "text",
"text": json.dumps(result, indent=2)
}]
}
})
elif tool_name == "get_employee_by_id":
employee_id = arguments.get("employee_id")
if employee_id is None:
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"error": {
"code": -32602,
"message": "Missing required parameter: employee_id"
}
})
try:
result = await get_employee_by_id(employee_id)
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {
"content": [{
"type": "text",
"text": json.dumps(result, indent=2)
}]
}
})
except Exception as e:
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"error": {
"code": -32602,
"message": str(e)
}
})
elif tool_name == "create_employee":
name = arguments.get("name")
if not name:
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"error": {
"code": -32602,
"message": "Missing required parameter: name"
}
})
try:
result = await create_employee(
name=name,
nif=arguments.get("nif", 0),
date_of_birth=arguments.get("date_of_birth", ""),
address=arguments.get("address", "")
)
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {
"content": [{
"type": "text",
"text": result
}]
}
})
except Exception as e:
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"error": {
"code": -32602,
"message": str(e)
}
})
# Default error response
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id"),
"error": {
"code": -32601,
"message": "Method not found"
}
})
except Exception as e:
return json.dumps({
"jsonrpc": "2.0",
"id": request.get("id", None),
"error": {
"code": -32603,
"message": f"Internal error: {str(e)}"
}
})
# Create interfaces
employees_interface = gr.Interface(
fn=get_employees,
inputs=None,
outputs="json",
title="Get Employees",
description="Return the list of all employees"
)
employee_by_id_interface = gr.Interface(
fn=get_employee_by_id,
inputs=gr.Number(label="Employee ID", precision=0),
outputs="json",
title="Get Employee by ID",
description="Return a specific employee by their ID"
)
create_employee_interface = gr.Interface(
fn=create_employee,
inputs=[
gr.Textbox(label="Name", placeholder="Employee name (required)"),
gr.Number(label="NIF", value=0, precision=0),
gr.Textbox(label="Date of Birth", placeholder="YYYY-MM-DD"),
gr.Textbox(label="Address", placeholder="Employee address")
],
outputs="text",
title="Create Employee",
description="Create a new employee record"
)
mcp_interface = gr.Interface(
fn=handle_mcp_request,
inputs=gr.Textbox(label="MCP Request JSON"),
outputs=gr.Textbox(label="MCP Response JSON"),
title="MCP Server",
description="MCP protocol endpoint"
)
# Combine interfaces
demo = gr.TabbedInterface(
[employees_interface, employee_by_id_interface, create_employee_interface, mcp_interface],
["Get All Employees", "Get Employee by ID", "Create Employee", "MCP Protocol"]
)
if __name__ == "__main__":
demo.launch(mcp_server=True, share=True)