Nelson André Claude Opus 4.5 commited on
Commit
9587243
·
1 Parent(s): dd1f139

Add input validation and update documentation

Browse files

Security improvements:
- Add validation for employee_id (positive integer)
- Add validation for strings (max length, trimming)
- Add validation for date format (YYYY-MM-DD)
- Add validation for NIF (non-negative integer)
- Replace verbose error messages with generic ones

Documentation:
- Document all MCP tools and parameters
- Add MCP configuration example
- List security features

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +68 -2
  2. app.py +120 -43
README.md CHANGED
@@ -8,7 +8,73 @@ sdk_version: 5.43.1
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
- short_description: NP MCP test
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
11
+ short_description: MCP Server for Employee Management
12
  ---
13
 
14
+ # NPTools - Employee Management MCP Server
15
+
16
+ An MCP (Model Context Protocol) server that provides tools for managing employee records via OutSystems REST API.
17
+
18
+ ## Available Tools
19
+
20
+ | Tool | Description | Parameters |
21
+ |------|-------------|------------|
22
+ | `get_employees` | Get list of all employees | None |
23
+ | `get_employee_by_id` | Get a specific employee by ID | `employee_id` (required, integer) |
24
+ | `create_employee` | Create a new employee record | `name` (required), `nif`, `date_of_birth`, `address` |
25
+
26
+ ## Tool Details
27
+
28
+ ### get_employees
29
+ Returns a list of all employees with fields: Id, Name, NIF, DateOfBirth, Address, CreatedOn, Phone.
30
+
31
+ ### get_employee_by_id
32
+ Fetches a single employee by their unique identifier.
33
+
34
+ **Parameters:**
35
+ - `employee_id` (integer, required): The unique identifier of the employee
36
+
37
+ ### create_employee
38
+ Creates a new employee record in the system.
39
+
40
+ **Parameters:**
41
+ - `name` (string, required): Employee name (max 500 characters)
42
+ - `nif` (integer, optional): Tax identification number
43
+ - `date_of_birth` (string, optional): Date of birth in YYYY-MM-DD format
44
+ - `address` (string, optional): Employee address (max 500 characters)
45
+
46
+ ## MCP Configuration
47
+
48
+ To use this MCP server with Claude Desktop or other MCP clients, add to your configuration:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "nptools": {
54
+ "url": "https://nelsondiasandre-nptools.hf.space/gradio_api/mcp/sse"
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Web Interface
61
+
62
+ The Gradio interface provides tabs for testing each endpoint:
63
+ - **Get All Employees**: Fetch all employee records
64
+ - **Get Employee by ID**: Look up a specific employee
65
+ - **Create Employee**: Add a new employee record
66
+ - **MCP Protocol**: Raw MCP JSON-RPC interface for testing
67
+
68
+ ## Backend API
69
+
70
+ This MCP server connects to an OutSystems REST API at:
71
+ `https://nelsonandre.outsystemscloud.com/MCPServer/rest/MCP`
72
+
73
+ ## Security Features
74
+
75
+ - Input validation for all parameters
76
+ - String length limits (500 characters max)
77
+ - Date format validation (YYYY-MM-DD)
78
+ - Positive integer validation for IDs
79
+ - Non-negative validation for NIF
80
+ - Generic error messages (no internal details leaked)
app.py CHANGED
@@ -1,10 +1,54 @@
1
  import gradio as gr
2
  import httpx
3
  import json
4
- from typing import Any, Dict, List
5
- import asyncio
6
 
7
  BASE_URL = "https://nelsonandre.outsystemscloud.com/MCPServer/rest/MCP"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  async def get_employees() -> list[dict]:
10
  """
@@ -25,9 +69,9 @@ async def get_employees() -> list[dict]:
25
  response.raise_for_status()
26
  return response.json()
27
  except httpx.HTTPStatusError as e:
28
- raise Exception(f"HTTP Error {e.response.status_code}: {e.response.text}")
29
- except Exception as e:
30
- raise Exception(f"Error: {str(e)}")
31
 
32
  async def get_employee_by_id(employee_id: int) -> dict:
33
  """
@@ -46,15 +90,20 @@ async def get_employee_by_id(employee_id: int) -> dict:
46
  CreatedOn (datetime): Date and time when the employee was created
47
  Phone (str): Employee's phone number
48
  """
 
49
  async with httpx.AsyncClient() as client:
50
  try:
51
- response = await client.get(f"{BASE_URL}/GetEmployeeBy", params={"EmployeeId": employee_id})
52
  response.raise_for_status()
53
  return response.json()
54
  except httpx.HTTPStatusError as e:
55
- raise Exception(f"HTTP Error {e.response.status_code}: {e.response.text}")
56
- except Exception as e:
57
- raise Exception(f"Error: {str(e)}")
 
 
 
 
58
 
59
  async def create_employee(name: str, nif: int = 0, date_of_birth: str = "", address: str = "") -> str:
60
  """
@@ -69,21 +118,29 @@ async def create_employee(name: str, nif: int = 0, date_of_birth: str = "", addr
69
  Returns:
70
  str: Result message from the server
71
  """
 
 
 
 
 
 
72
  async with httpx.AsyncClient() as client:
73
  try:
74
  payload = {
75
- "EmployeeName": name,
76
- "NIF": nif,
77
- "DateOfBirth": date_of_birth,
78
- "Address": address
79
  }
80
  response = await client.post(f"{BASE_URL}/EmployeeCreate", json=payload)
81
  response.raise_for_status()
82
  return response.text
83
  except httpx.HTTPStatusError as e:
84
- raise Exception(f"HTTP Error {e.response.status_code}: {e.response.text}")
85
- except Exception as e:
86
- raise Exception(f"Error: {str(e)}")
 
 
87
 
88
  # MCP Protocol handler
89
  async def handle_mcp_request(request_data: str) -> str:
@@ -179,17 +236,27 @@ async def handle_mcp_request(request_data: str) -> str:
179
  "message": "Missing required parameter: employee_id"
180
  }
181
  })
182
- result = await get_employee_by_id(int(employee_id))
183
- return json.dumps({
184
- "jsonrpc": "2.0",
185
- "id": request.get("id"),
186
- "result": {
187
- "content": [{
188
- "type": "text",
189
- "text": json.dumps(result, indent=2)
190
- }]
191
- }
192
- })
 
 
 
 
 
 
 
 
 
 
193
 
194
  elif tool_name == "create_employee":
195
  name = arguments.get("name")
@@ -202,22 +269,32 @@ async def handle_mcp_request(request_data: str) -> str:
202
  "message": "Missing required parameter: name"
203
  }
204
  })
205
- result = await create_employee(
206
- name=name,
207
- nif=arguments.get("nif", 0),
208
- date_of_birth=arguments.get("date_of_birth", ""),
209
- address=arguments.get("address", "")
210
- )
211
- return json.dumps({
212
- "jsonrpc": "2.0",
213
- "id": request.get("id"),
214
- "result": {
215
- "content": [{
216
- "type": "text",
217
- "text": result
218
- }]
219
- }
220
- })
 
 
 
 
 
 
 
 
 
 
221
 
222
  # Default error response
223
  return json.dumps({
 
1
  import gradio as gr
2
  import httpx
3
  import json
4
+ import re
 
5
 
6
  BASE_URL = "https://nelsonandre.outsystemscloud.com/MCPServer/rest/MCP"
7
+ MAX_STRING_LENGTH = 500
8
+ DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
9
+
10
+
11
+ def validate_employee_id(employee_id) -> int:
12
+ """Validate and convert employee ID to a positive integer."""
13
+ try:
14
+ eid = int(employee_id)
15
+ if eid <= 0:
16
+ raise ValueError("Employee ID must be a positive integer")
17
+ return eid
18
+ except (TypeError, ValueError) as e:
19
+ raise ValueError(f"Invalid employee ID: {e}")
20
+
21
+
22
+ def validate_string(value: str, field_name: str, required: bool = False, max_length: int = MAX_STRING_LENGTH) -> str:
23
+ """Validate and sanitize string input."""
24
+ if value is None:
25
+ value = ""
26
+ value = str(value).strip()
27
+ if required and not value:
28
+ raise ValueError(f"{field_name} is required")
29
+ if len(value) > max_length:
30
+ raise ValueError(f"{field_name} exceeds maximum length of {max_length}")
31
+ return value
32
+
33
+
34
+ def validate_date(value: str, field_name: str) -> str:
35
+ """Validate date format (YYYY-MM-DD)."""
36
+ if not value:
37
+ return ""
38
+ if not DATE_PATTERN.match(value):
39
+ raise ValueError(f"{field_name} must be in YYYY-MM-DD format")
40
+ return value
41
+
42
+
43
+ def validate_nif(value) -> int:
44
+ """Validate NIF as a non-negative integer."""
45
+ try:
46
+ nif = int(value) if value else 0
47
+ if nif < 0:
48
+ raise ValueError("NIF cannot be negative")
49
+ return nif
50
+ except (TypeError, ValueError):
51
+ raise ValueError("NIF must be a valid integer")
52
 
53
  async def get_employees() -> list[dict]:
54
  """
 
69
  response.raise_for_status()
70
  return response.json()
71
  except httpx.HTTPStatusError as e:
72
+ raise Exception(f"Failed to fetch employees (HTTP {e.response.status_code})")
73
+ except Exception:
74
+ raise Exception("Failed to connect to employee service")
75
 
76
  async def get_employee_by_id(employee_id: int) -> dict:
77
  """
 
90
  CreatedOn (datetime): Date and time when the employee was created
91
  Phone (str): Employee's phone number
92
  """
93
+ validated_id = validate_employee_id(employee_id)
94
  async with httpx.AsyncClient() as client:
95
  try:
96
+ response = await client.get(f"{BASE_URL}/GetEmployeeBy", params={"EmployeeId": validated_id})
97
  response.raise_for_status()
98
  return response.json()
99
  except httpx.HTTPStatusError as e:
100
+ if e.response.status_code == 404:
101
+ raise Exception("Employee not found")
102
+ raise Exception(f"Failed to fetch employee (HTTP {e.response.status_code})")
103
+ except ValueError as e:
104
+ raise Exception(str(e))
105
+ except Exception:
106
+ raise Exception("Failed to connect to employee service")
107
 
108
  async def create_employee(name: str, nif: int = 0, date_of_birth: str = "", address: str = "") -> str:
109
  """
 
118
  Returns:
119
  str: Result message from the server
120
  """
121
+ # Validate all inputs
122
+ validated_name = validate_string(name, "Name", required=True)
123
+ validated_nif = validate_nif(nif)
124
+ validated_dob = validate_date(date_of_birth, "Date of birth")
125
+ validated_address = validate_string(address, "Address", required=False)
126
+
127
  async with httpx.AsyncClient() as client:
128
  try:
129
  payload = {
130
+ "EmployeeName": validated_name,
131
+ "NIF": validated_nif,
132
+ "DateOfBirth": validated_dob,
133
+ "Address": validated_address
134
  }
135
  response = await client.post(f"{BASE_URL}/EmployeeCreate", json=payload)
136
  response.raise_for_status()
137
  return response.text
138
  except httpx.HTTPStatusError as e:
139
+ raise Exception(f"Failed to create employee (HTTP {e.response.status_code})")
140
+ except ValueError as e:
141
+ raise Exception(str(e))
142
+ except Exception:
143
+ raise Exception("Failed to connect to employee service")
144
 
145
  # MCP Protocol handler
146
  async def handle_mcp_request(request_data: str) -> str:
 
236
  "message": "Missing required parameter: employee_id"
237
  }
238
  })
239
+ try:
240
+ result = await get_employee_by_id(employee_id)
241
+ return json.dumps({
242
+ "jsonrpc": "2.0",
243
+ "id": request.get("id"),
244
+ "result": {
245
+ "content": [{
246
+ "type": "text",
247
+ "text": json.dumps(result, indent=2)
248
+ }]
249
+ }
250
+ })
251
+ except Exception as e:
252
+ return json.dumps({
253
+ "jsonrpc": "2.0",
254
+ "id": request.get("id"),
255
+ "error": {
256
+ "code": -32602,
257
+ "message": str(e)
258
+ }
259
+ })
260
 
261
  elif tool_name == "create_employee":
262
  name = arguments.get("name")
 
269
  "message": "Missing required parameter: name"
270
  }
271
  })
272
+ try:
273
+ result = await create_employee(
274
+ name=name,
275
+ nif=arguments.get("nif", 0),
276
+ date_of_birth=arguments.get("date_of_birth", ""),
277
+ address=arguments.get("address", "")
278
+ )
279
+ return json.dumps({
280
+ "jsonrpc": "2.0",
281
+ "id": request.get("id"),
282
+ "result": {
283
+ "content": [{
284
+ "type": "text",
285
+ "text": result
286
+ }]
287
+ }
288
+ })
289
+ except Exception as e:
290
+ return json.dumps({
291
+ "jsonrpc": "2.0",
292
+ "id": request.get("id"),
293
+ "error": {
294
+ "code": -32602,
295
+ "message": str(e)
296
+ }
297
+ })
298
 
299
  # Default error response
300
  return json.dumps({