NurseCitizenDeveloper commited on
Commit
27c849b
·
1 Parent(s): 01e313b

fix: resolve remaining merge conflicts across all affected files

Browse files
Files changed (5) hide show
  1. MCP_SETUP.md +0 -70
  2. cached_legislation.py +0 -118
  3. lex_client.py +0 -1
  4. mcp_server.py +0 -178
  5. nursing_sections.json +0 -1
MCP_SETUP.md CHANGED
@@ -1,4 +1,3 @@
1
- <<<<<<< HEAD
2
  # 🏥 NurseLex MCP Server Setup Guide
3
 
4
  Welcome to the **NurseLex MCP Server**! This server uses the **Model Context Protocol (MCP)** to inject the entire i.AI Lex UK legislative database directly into your favorite AI Assistant (like Claude Desktop or Cursor).
@@ -66,72 +65,3 @@ If you are a Nurse Citizen Developer building applications in Cursor, you can ad
66
  5. Click **Save** and verify the connection is green.
67
 
68
  You can now ask Cursor Agent: *"Write a Python function that uses NurseLex to find the legal requirements for Section 136 of the MHA."*
69
- =======
70
- # 🏥 NurseLex MCP Server Setup Guide
71
-
72
- Welcome to the **NurseLex MCP Server**! This server uses the **Model Context Protocol (MCP)** to inject the entire i.AI Lex UK legislative database directly into your favorite AI Assistant (like Claude Desktop or Cursor).
73
-
74
- When connected, your AI assistant gains three powerful real-time tools:
75
-
76
- 1. **`search_local_nursing_cache`**: Instantly retrieves the exact statutory text for 1,128 critical nursing sections (Mental Health Act, Care Act, etc.).
77
- 2. **`get_official_explanatory_note`**: Fetches the official UK Government plain English explainer for a specific Act and section.
78
- 3. **`vector_search_lex_api`**: A semantic search engine that maps clinical, plain-English scenarios (e.g., "patient lacks capacity to consent to treatment") to relevant national laws.
79
-
80
- ---
81
-
82
- ## 🛠️ Prerequisites
83
-
84
- 1. You must have **Python 3.10+** installed.
85
- 2. Install the required dependencies:
86
- ```bash
87
- pip install mcp fastmcp httpx pandas
88
- ```
89
-
90
- ---
91
-
92
- ## 🔌 Connecting to Claude Desktop
93
-
94
- To let Claude Desktop use NurseLex as a knowledge tool, you need to edit your `claude_desktop_config.json` file.
95
-
96
- ### 1. Locate your Config File
97
- - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
98
- - **Mac:** `~/Library/Application Support/Claude/claude_desktop_config.json`
99
-
100
- ### 2. Add the NurseLex Server
101
- Open the file in a text editor and add the following configuration to your `mcpServers` block.
102
-
103
- *Make sure to replace `/path/to/NurseLex` with the actual folder path where you downloaded the project!*
104
-
105
- ```json
106
- {
107
- "mcpServers": {
108
- "nurselex": {
109
- "command": "python",
110
- "args": [
111
- "/path/to/NurseLex/mcp_server.py"
112
- ]
113
- }
114
- }
115
- }
116
- ```
117
-
118
- ### 3. Restart Claude
119
- Fully close and restart the Claude Desktop app. You should now see a 🔨 (hammer) icon in the chat bar indicating the tools are loaded!
120
-
121
- ---
122
-
123
- ## 🖱️ Connecting to Cursor IDE
124
-
125
- If you are a Nurse Citizen Developer building applications in Cursor, you can add NurseLex to help write legally compliant code!
126
-
127
- 1. Open Cursor Settings (**Cmd + Shift + J** on Mac, **Ctrl + Shift + J** on Windows).
128
- 2. Go to the **Features** -> **MCP** section.
129
- 3. Click **+ Add New MCP Server**.
130
- 4. Set the following:
131
- - **Name:** `NurseLex`
132
- - **Type:** `command`
133
- - **Command:** `python /path/to/NurseLex/mcp_server.py` (Use the absolute path to your folder).
134
- 5. Click **Save** and verify the connection is green.
135
-
136
- You can now ask Cursor Agent: *"Write a Python function that uses NurseLex to find the legal requirements for Section 136 of the MHA."*
137
- >>>>>>> a4e257b16d56f80612b7c9ac6d2e7c198fef5bb6
 
 
1
  # 🏥 NurseLex MCP Server Setup Guide
2
 
3
  Welcome to the **NurseLex MCP Server**! This server uses the **Model Context Protocol (MCP)** to inject the entire i.AI Lex UK legislative database directly into your favorite AI Assistant (like Claude Desktop or Cursor).
 
65
  5. Click **Save** and verify the connection is green.
66
 
67
  You can now ask Cursor Agent: *"Write a Python function that uses NurseLex to find the legal requirements for Section 136 of the MHA."*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cached_legislation.py CHANGED
@@ -1,4 +1,3 @@
1
- <<<<<<< HEAD
2
  """
3
  NurseLex — Pre-cached legislation for offline lookup.
4
  Loads full section text from nursing_sections.json (1,000+ sections).
@@ -114,120 +113,3 @@ def search_cached(query: str, max_results: int = 5) -> list:
114
  break
115
 
116
  return results[:max_results]
117
- =======
118
- """
119
- NurseLex — Pre-cached legislation for offline lookup.
120
- Loads full section text from nursing_sections.json (1,000+ sections).
121
- Source: legislation.gov.uk (Crown Copyright, OGL v3.0) via i.AI Lex API.
122
- """
123
- import os
124
- import json
125
- import logging
126
-
127
- logger = logging.getLogger(__name__)
128
-
129
- # --- Configuration ---
130
- JSON_PATH = os.path.join(os.path.dirname(__file__), "nursing_sections.json")
131
-
132
- # --- Load Sections ---
133
- CACHED_SECTIONS = []
134
- try:
135
- if os.path.exists(JSON_PATH):
136
- with open(JSON_PATH, "r", encoding="utf-8") as f:
137
- CACHED_SECTIONS = json.load(f)
138
- logger.info(f"Loaded {len(CACHED_SECTIONS)} sections from {JSON_PATH}")
139
- else:
140
- logger.warning(f"Metadata file {JSON_PATH} not found.")
141
- except Exception as e:
142
- logger.error(f"Error loading {JSON_PATH}: {e}")
143
-
144
- # --- Keyword Map for Natural Language Shortcuts ---
145
- # Direct section links for common nursing search terms
146
- KEYWORD_MAP = {
147
- "nurse holding power": ("ukpga/1983/20", "5(4)"),
148
- "doctor holding power": ("ukpga/1983/20", "5(2)"),
149
- "section 2": ("ukpga/1983/20", "2"),
150
- "section 3": ("ukpga/1983/20", "3"),
151
- "section 4": ("ukpga/1983/20", "4"),
152
- "aftercare": ("ukpga/1983/20", "117"),
153
- "section 117": ("ukpga/1983/20", "117"),
154
- "leave of absence": ("ukpga/1983/20", "17"),
155
- "section 17": ("ukpga/1983/20", "17"),
156
- "place of safety": ("ukpga/1983/20", "136"),
157
- "section 136": ("ukpga/1983/20", "136"),
158
- "section 135": ("ukpga/1983/20", "135"),
159
- "best interests": ("ukpga/2005/9", "4"),
160
- "capacity test": ("ukpga/2005/9", "3"),
161
- "functional test": ("ukpga/2005/9", "3"),
162
- "mca principles": ("ukpga/2005/9", "1"),
163
- "safeguarding": ("ukpga/2014/23", "42"),
164
- "section 42": ("ukpga/2014/23", "42"),
165
- "advocacy": ("ukpga/2014/23", "67"),
166
- }
167
-
168
- def search_cached(query: str, max_results: int = 5) -> list:
169
- """
170
- Search local sections by keyword, title, or legislation ID.
171
- Returns a list of section dictionaries.
172
- """
173
- if not query:
174
- return []
175
-
176
- query = query.lower().strip()
177
- results = []
178
-
179
- # 1. Check Keyword Map First (High Precision)
180
- for kw, (leg_id, sec_num) in KEYWORD_MAP.items():
181
- if kw in query:
182
- # Find the specific section in our list
183
- for s in CACHED_SECTIONS:
184
- if s.get("legislation_id") == leg_id and str(s.get("number")) == sec_num:
185
- if s not in results:
186
- results.append(s)
187
- # Find closest matches if exact number not found (e.g. 5(4) vs 5)
188
- if not results:
189
- for s in CACHED_SECTIONS:
190
- if s.get("legislation_id") == leg_id and str(s.get("number")).startswith(sec_num.split('(')[0]):
191
- if s not in results:
192
- results.append(s)
193
-
194
- # 2. Text-based Search
195
- # Sort sections by relevance (title match > text match)
196
- scored_results = []
197
- for s in CACHED_SECTIONS:
198
- score = 0
199
- title = s.get("title", "").lower()
200
- text = s.get("text", "").lower()
201
- leg_id = s.get("legislation_id", "").lower()
202
- num = str(s.get("number", "")).lower()
203
-
204
- # Exact section reference (e.g. "Section 5")
205
- if f"section {num}" in query or f"s.{num}" in query or f"s {num}" in query:
206
- score += 100
207
-
208
- # Title matches
209
- if query in title:
210
- score += 50
211
-
212
- # Word matches in title
213
- for word in query.split():
214
- if len(word) > 3 and word in title:
215
- score += 10
216
-
217
- # Content matches
218
- if query in text:
219
- score += 5
220
-
221
- if score > 0:
222
- scored_results.append((score, s))
223
-
224
- # Sort and add to results
225
- scored_results.sort(key=lambda x: x[0], reverse=True)
226
- for _, s in scored_results:
227
- if s not in results:
228
- results.append(s)
229
- if len(results) >= max_results:
230
- break
231
-
232
- return results[:max_results]
233
- >>>>>>> a4e257b16d56f80612b7c9ac6d2e7c198fef5bb6
 
 
1
  """
2
  NurseLex — Pre-cached legislation for offline lookup.
3
  Loads full section text from nursing_sections.json (1,000+ sections).
 
113
  break
114
 
115
  return results[:max_results]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
lex_client.py CHANGED
@@ -1,4 +1,3 @@
1
- <<<<<<< HEAD
2
  """
3
  NurseLex — Lex API Client
4
  Wraps the i.AI Lex API for nursing-focused UK legislation search.
 
 
1
  """
2
  NurseLex — Lex API Client
3
  Wraps the i.AI Lex API for nursing-focused UK legislation search.
mcp_server.py CHANGED
@@ -1,4 +1,3 @@
1
- <<<<<<< HEAD
2
  import asyncio
3
  import httpx
4
  import pandas as pd
@@ -174,180 +173,3 @@ async def get_official_explanatory_note(act_name: str, section_number: str) -> s
174
  if __name__ == "__main__":
175
  # Ensure this runs correctly when started via cursor/claude
176
  mcp.run()
177
- =======
178
- import asyncio
179
- import httpx
180
- import pandas as pd
181
- import json
182
- import os
183
- from mcp.server.fastmcp import FastMCP
184
-
185
- # Initialize FastMCP server
186
- mcp = FastMCP("NurseLex-LexAPI")
187
-
188
- # Core Constants
189
- BASE_URL = 'https://lex.lab.i.ai.gov.uk'
190
- NURSING_ACTS = {
191
- "Mental Health Act 1983": "ukpga/1983/20",
192
- "Mental Capacity Act 2005": "ukpga/2005/9",
193
- "Care Act 2014": "ukpga/2014/23",
194
- "Human Rights Act 1998": "ukpga/1998/42",
195
- "Equality Act 2010": "ukpga/2010/15",
196
- "Health and Social Care Act 2012": "ukpga/2012/7",
197
- "Mental Health Units (Use of Force) Act 2018": "ukpga/2018/27",
198
- "Autism Act 2009": "ukpga/2009/15",
199
- "Children Act 1989": "ukpga/1989/41",
200
- "Children Act 2004": "ukpga/2004/31",
201
- "Safeguarding Vulnerable Groups Act 2006": "ukpga/2006/47",
202
- "Health and Care Act 2022": "ukpga/2022/31",
203
- }
204
- REVERSE_ACTS = {v: k for k, v in NURSING_ACTS.items()}
205
-
206
- # Load Cache (we need absolute paths since MCP might run from a different CWD)
207
- DB_DIR = os.path.dirname(os.path.abspath(__file__))
208
- CACHE_FILE = os.path.join(DB_DIR, "nursing_sections.json")
209
-
210
- def _load_sections():
211
- try:
212
- with open(CACHE_FILE, 'r', encoding='utf-8') as f:
213
- return json.load(f)
214
- except FileNotFoundError:
215
- return []
216
-
217
- SECTIONS_CACHE = _load_sections()
218
-
219
- @mcp.tool()
220
- def search_local_nursing_cache(query: str, limit: int = 5) -> str:
221
- """
222
- Search the local, curated cache of 1,128 critical nursing legislation sections
223
- (from the Mental Health Act, Care Act, etc.) for a specific keyword or section number.
224
- Returns the exact statutory text.
225
- """
226
- if not SECTIONS_CACHE:
227
- return "Error: Local cache not found."
228
-
229
- query_lower = query.lower()
230
- results = []
231
-
232
- for section in SECTIONS_CACHE:
233
- act_name = section.get('act_name', '').lower()
234
- title = section.get('title', '').lower()
235
- text = section.get('text', '').lower()
236
- num_str = str(section.get('number', ''))
237
-
238
- score = 0
239
- if query_lower in act_name: score += 1
240
- if query_lower in title: score += 3
241
- if query_lower in text: score += 1
242
- if query_lower == f"section {num_str}" or query_lower == num_str: score += 5
243
-
244
- if score > 0:
245
- results.append((score, section))
246
-
247
- # Sort and take top matches
248
- results.sort(key=lambda x: x[0], reverse=True)
249
-
250
- if not results:
251
- return "No sections found matching the query in the local cache."
252
-
253
- out = f"## 📚 Local Cache Results for '{query}'\n\n"
254
- for r in results[:limit]:
255
- sec = r[1]
256
- out += f"**{sec.get('act_name')} — Section {sec.get('number')}**\n"
257
- out += f"*{sec.get('title')}*\n"
258
- out += f"{sec.get('text')}\n\n---\n\n"
259
-
260
- return out
261
-
262
- @mcp.tool()
263
- async def vector_search_lex_api(clinical_scenario: str) -> str:
264
- """
265
- Translates a plain-English clinical scenario (e.g. 'Patient wants to leave but lacks capacity')
266
- into relevant UK legislation by querying the i.AI Lex API semantic vector search engine.
267
- This searches across the entire legislation database, not just the local cache.
268
- """
269
- url = f'{BASE_URL}/legislation/section/search'
270
- payload = {
271
- 'query': clinical_scenario,
272
- 'limit': 5
273
- }
274
-
275
- try:
276
- async with httpx.AsyncClient() as client:
277
- r = await client.post(url, json=payload, timeout=15.0)
278
- if r.status_code != 200:
279
- return f"Lex API Vector Search Failed: Status Code {r.status_code}"
280
-
281
- data = r.json()
282
- if not isinstance(data, list) or not data:
283
- return "No semantic matches found for this scenario."
284
-
285
- out = f"## ⚖️ Vector Matches for Scenario:\n*{clinical_scenario}*\n\n"
286
- for i, n in enumerate(data, 1):
287
- leg_id = n.get("legislation_id", "")
288
-
289
- # 1. Use the act_name from the API response if available
290
- act_name = n.get("act_name", "")
291
-
292
- # 2. If not, try our known mapping
293
- if not act_name:
294
- for known_id, known_name in REVERSE_ACTS.items():
295
- if known_id in leg_id:
296
- act_name = known_name
297
- break
298
-
299
- # 3. Final fallback: extract from the legislation_id URL
300
- if not act_name:
301
- act_name = leg_id.split("/id/")[-1] if "/id/" in leg_id else leg_id or "Legislation"
302
-
303
- sec_num = n.get("number", "??")
304
- title = n.get("title", "Untitled Section")
305
- text = n.get("text", "")
306
-
307
- out += f"### {i}. {act_name} — Section {sec_num}: {title}\n"
308
- out += f"{text[:600]}...\n\n"
309
- out += f"Source URI: {n.get('uri', f'https://www.legislation.gov.uk/id/{leg_id}/section/{sec_num}')}\n\n"
310
-
311
- return out
312
- except Exception as e:
313
- return f"Error querying Lex Vector API: {str(e)}"
314
-
315
- @mcp.tool()
316
- async def get_official_explanatory_note(act_name: str, section_number: str) -> str:
317
- """
318
- Dynamically fetches the Official Government Explanatory Note for a specific Act and section.
319
- Explanatory Notes are plain English explainers written by the government.
320
- Note: Acts passed prior to 1999 (e.g., Mental Health Act 1983) generally do not have these.
321
-
322
- Args:
323
- act_name: The full name of the Act (e.g., 'Mental Capacity Act 2005').
324
- section_number: The specific section number as a string (e.g., '3').
325
- """
326
- url = f'{BASE_URL}/explanatory_note/section/search'
327
- payload = {
328
- 'query': f'"{act_name}" Section {section_number}',
329
- 'limit': 3
330
- }
331
-
332
- try:
333
- async with httpx.AsyncClient() as client:
334
- r = await client.post(url, json=payload, timeout=10.0)
335
- if r.status_code == 200:
336
- data = r.json()
337
- if isinstance(data, list):
338
- parent_id = NURSING_ACTS.get(act_name, "")
339
- for note in data:
340
- # Match the parent ID to ensure this note belongs to the right Act
341
- if parent_id and parent_id in note.get('legislation_id', ''):
342
- text = note.get('text', '')
343
- if text:
344
- return f"### Official Explanatory Note ({act_name} S.{section_number})\n\n{text}"
345
-
346
- return f"No Official Explanatory Note could be found for '{act_name}' Section {section_number}. The Act may pre-date the 1999 introduction of Explanatory Notes."
347
- except Exception as e:
348
- return f"Error fetching Explanatory Note: {str(e)}"
349
-
350
- if __name__ == "__main__":
351
- # Ensure this runs correctly when started via cursor/claude
352
- mcp.run()
353
- >>>>>>> a4e257b16d56f80612b7c9ac6d2e7c198fef5bb6
 
 
1
  import asyncio
2
  import httpx
3
  import pandas as pd
 
173
  if __name__ == "__main__":
174
  # Ensure this runs correctly when started via cursor/claude
175
  mcp.run()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
nursing_sections.json CHANGED
@@ -1,4 +1,3 @@
1
- <<<<<<< HEAD
2
  [
3
  {
4
  "created_at": "2025-10-17T20:10:21.475763Z",
 
 
1
  [
2
  {
3
  "created_at": "2025-10-17T20:10:21.475763Z",