alix-doc-vault-tool 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: alix-doc-vault-tool
3
+ Version: 0.1.0
4
+ Summary: Documents Vault MCP tool for Alix. Fetches files, files per estate and downloads them into temporary folder for next processing
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: mcp>=1.9.0
7
+ Requires-Dist: httpx>=0.27
8
+ Provides-Extra: dev
9
+ Requires-Dist: build; extra == "dev"
10
+ Requires-Dist: twine; extra == "dev"
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "alix-doc-vault-tool"
7
+ version = "0.1.0"
8
+ description = "Documents Vault MCP tool for Alix. Fetches files, files per estate and downloads them into temporary folder for next processing"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "mcp>=1.9.0",
12
+ "httpx>=0.27",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ dev = ["build", "twine"]
17
+
18
+ [project.scripts]
19
+ alix-doc-vault-tool = "alix_doc_vault_tool.mcp_server:main"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from alix_doc_vault_tool.mcp_server import (
4
+ run_get_estate_documents,
5
+ run_get_file,
6
+ )
7
+
8
+ __all__ = [
9
+ "__version__",
10
+ "run_get_estate_documents",
11
+ "run_get_file",
12
+ ]
@@ -0,0 +1,6 @@
1
+ """python -m alix_doc_vault_tool -> stdio MCP."""
2
+
3
+ from alix_doc_vault_tool.mcp_server import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,348 @@
1
+ """Doc Vault MCP server for retrieving documents from the Alix Doc Vault.
2
+
3
+ Env vars:
4
+ ALIX_API_URL required. Base URL (e.g. http://localhost:8080) or full
5
+ /graphql URL.
6
+ ALIX_API_KEY required. Master token sent as the Authorization header.
7
+ ALIX_GRAPHQL_TIMEOUT optional. Request timeout in seconds (default 30).
8
+ FILE_DOWNLOAD_TIMEOUT optional. Request timeout in seconds for downloading files (default 30).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from typing import Any, Literal, TypedDict
16
+
17
+ import httpx
18
+ from mcp.server.fastmcp import FastMCP
19
+
20
+ SERVER_NAME: str = "alix-doc-vault-tool"
21
+
22
+ EstateDocumentType = Literal[
23
+ "DecedentDeathCertificate",
24
+ "PreDeceasedContactDeathCertificate",
25
+ "Trust",
26
+ "Will",
27
+ "AmendmentToTrust",
28
+ "Codicil",
29
+ "CustomerID",
30
+ "DecedentID",
31
+ "FuneralBillReceipt",
32
+ "FamilyMemberDeathCertificate",
33
+ "NAICReport",
34
+ "SocialSecurityEarningsReport",
35
+ "BondCertificate",
36
+ "AuthorityDocument",
37
+ "BankStatement",
38
+ "Other",
39
+ ]
40
+
41
+ FileSource = Literal[
42
+ "Api",
43
+ "CustomerAppUpload",
44
+ "SpecialistEmUpload",
45
+ "ScanBox",
46
+ "SystemGenerated",
47
+ "EmailIntake",
48
+ ]
49
+
50
+ _FILE_SELECTION = """
51
+ id
52
+ key
53
+ fileName
54
+ fileSource
55
+ mime
56
+ size
57
+ public
58
+ downloadUrl
59
+ folder {
60
+ id
61
+ key
62
+ materializedPath
63
+ name
64
+ }
65
+ """
66
+
67
+ _GET_ESTATE_DOCUMENTS_QUERY = f"""
68
+ query GetEstateDocuments($where: EstateWhereUniqueInput!) {{
69
+ estate(where: $where) {{
70
+ id
71
+ documents {{
72
+ id
73
+ documentType
74
+ files {{
75
+ file {{
76
+ {_FILE_SELECTION}
77
+ }}
78
+ }}
79
+ }}
80
+ }}
81
+ }}
82
+ """
83
+
84
+ _GET_FILE_QUERY = f"""
85
+ query GetFile($where: FileWhereUniqueInput!) {{
86
+ file(where: $where) {{
87
+ id
88
+ {_FILE_SELECTION}
89
+ }}
90
+ }}
91
+ """
92
+
93
+ class Folder(TypedDict):
94
+ id: str
95
+ key: str | None
96
+ materializedPath: str | None
97
+ name: str
98
+
99
+ class File(TypedDict):
100
+ id: str
101
+ key: str
102
+ fileName: str
103
+ fileSource: FileSource | None
104
+ mime: str
105
+ size: int
106
+ public: bool
107
+ downloadUrl: str | None
108
+ folder: Folder
109
+
110
+ def _resolve_endpoint(raw: str) -> str:
111
+ url = raw.rstrip("/")
112
+ if url.endswith("/graphql"):
113
+ return url
114
+ return f"{url}/graphql"
115
+
116
+
117
+ def _read_alix_api_graphql_timeout() -> float:
118
+ raw = os.environ.get("ALIX_GRAPHQL_TIMEOUT")
119
+ if not raw:
120
+ return 30.0
121
+ try:
122
+ return float(raw)
123
+ except ValueError:
124
+ return 30.0
125
+
126
+ def _read_file_download_timeout() -> float:
127
+ raw = os.environ.get("FILE_DOWNLOAD_TIMEOUT")
128
+ if not raw:
129
+ return 30.0
130
+ try:
131
+ return float(raw)
132
+ except ValueError:
133
+ return 30.0
134
+
135
+
136
+ def _graphql_request(
137
+ *,
138
+ query: str,
139
+ variables: dict[str, Any],
140
+ operation_name: str,
141
+ ) -> dict[str, Any]:
142
+ api_url_raw = os.environ.get("ALIX_API_URL")
143
+ api_key = os.environ.get("ALIX_API_KEY")
144
+ if not api_url_raw:
145
+ return {"errors": [{"message": "ALIX_API_URL is not set."}]}
146
+ if not api_key:
147
+ return {"errors": [{"message": "ALIX_API_KEY is not set."}]}
148
+
149
+ endpoint = _resolve_endpoint(api_url_raw)
150
+ timeout = _read_alix_api_graphql_timeout()
151
+
152
+ payload: dict[str, Any] = {
153
+ "query": query,
154
+ "variables": variables,
155
+ "operationName": operation_name,
156
+ }
157
+ headers = {
158
+ "Content-Type": "application/json",
159
+ "Authorization": api_key,
160
+ }
161
+
162
+ try:
163
+ with httpx.Client(timeout=timeout) as client:
164
+ response = client.post(endpoint, json=payload, headers=headers)
165
+ except httpx.HTTPError as exc:
166
+ return {"errors": [{"message": f"Transport error: {exc.__class__.__name__}: {exc}"}]}
167
+
168
+ text = response.text
169
+ try:
170
+ body: Any = response.json()
171
+ except ValueError:
172
+ body = None
173
+
174
+ if response.is_error:
175
+ if isinstance(body, dict) and ("errors" in body or "data" in body):
176
+ return {
177
+ "data": body.get("data"),
178
+ "errors": body.get("errors")
179
+ or [{"message": f"HTTP {response.status_code}: {text or response.reason_phrase}"}],
180
+ "http_status": response.status_code,
181
+ }
182
+ return {
183
+ "data": None,
184
+ "errors": [{"message": f"HTTP {response.status_code}: {text or response.reason_phrase}"}],
185
+ "http_status": response.status_code,
186
+ }
187
+
188
+ if not isinstance(body, dict):
189
+ return {
190
+ "data": None,
191
+ "errors": [{"message": f"Non-JSON response from GraphQL endpoint: {text[:500]}"}],
192
+ "http_status": response.status_code,
193
+ }
194
+
195
+ return {
196
+ "data": body.get("data"),
197
+ "errors": body.get("errors"),
198
+ "extensions": body.get("extensions"),
199
+ "http_status": response.status_code,
200
+ }
201
+
202
+
203
+ def run_get_estate_documents(estateId: str) -> str:
204
+ if not estateId or not isinstance(estateId, str):
205
+ return json.dumps({"errors": [{"message": "estateId is required."}]})
206
+
207
+ result = _graphql_request(
208
+ query=_GET_ESTATE_DOCUMENTS_QUERY,
209
+ variables={"where": {"id": estateId}},
210
+ operation_name="GetEstateDocuments",
211
+ )
212
+
213
+ if result.get("errors"):
214
+ return json.dumps(result, default=str)
215
+
216
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
217
+ estate = data.get("estate") if isinstance(data, dict) else None
218
+
219
+ if not estate:
220
+ return json.dumps(
221
+ {
222
+ **result,
223
+ "data": {"documents": None},
224
+ },
225
+ default=str,
226
+ )
227
+ documents = estate.get("documents") or []
228
+
229
+ return json.dumps(
230
+ {
231
+ **result,
232
+ "data": {"documents": documents},
233
+ },
234
+ default=str,
235
+ )
236
+
237
+
238
+ def run_get_file(
239
+ fileId: str,
240
+ ) -> str:
241
+ if not fileId or not isinstance(fileId, str):
242
+ return json.dumps({"errors": [{"message": "fileId is required."}]})
243
+
244
+ where: dict[str, Any] = {
245
+ "id": fileId,
246
+ }
247
+
248
+ result = _graphql_request(
249
+ query=_GET_FILE_QUERY,
250
+ variables={"where": where},
251
+ operation_name="GetFile",
252
+ )
253
+
254
+ if result.get("errors"):
255
+ return json.dumps(result, default=str)
256
+
257
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
258
+ file = data.get("file") if isinstance(data, dict) else None
259
+
260
+ if not isinstance(file, dict):
261
+ return json.dumps(
262
+ {
263
+ **result,
264
+ "errors": [{"message": "Unexpected GraphQL response: `file` is missing."}],
265
+ },
266
+ default=str,
267
+ )
268
+
269
+ if not file:
270
+ return json.dumps(
271
+ {
272
+ **result,
273
+ "data": {"file": None},
274
+ },
275
+ default=str,
276
+ )
277
+
278
+ return json.dumps(
279
+ {
280
+ **result,
281
+ "data": {"file": file},
282
+ },
283
+ default=str,
284
+ )
285
+
286
+ def download_file_to_temp_folder(file: File, temp_folder_path: str) -> str:
287
+ if not file or not isinstance(file, dict):
288
+ return json.dumps({"errors": [{"message": "file is required."}]})
289
+
290
+ download_url = file.get("downloadUrl")
291
+ if not download_url or not isinstance(download_url, str):
292
+ return json.dumps({"errors": [{"message": "downloadUrl is required in file."}]})
293
+
294
+ if not temp_folder_path or not isinstance(temp_folder_path, str):
295
+ return json.dumps({"errors": [{"message": "temp_folder_path is required."}]})
296
+
297
+ file_name = file.get("fileName")
298
+ if not file_name or not isinstance(file_name, str):
299
+ return json.dumps({"errors": [{"message": "fileName is required in file."}]})
300
+
301
+ os.makedirs(temp_folder_path, exist_ok=True)
302
+ file_path = os.path.join(temp_folder_path, file_name)
303
+
304
+ timeout = _read_file_download_timeout()
305
+ try:
306
+ with httpx.stream("GET", download_url, timeout=timeout) as response:
307
+ response.raise_for_status()
308
+ with open(file_path, "wb") as output_file:
309
+ for chunk in response.iter_bytes():
310
+ output_file.write(chunk)
311
+ except httpx.HTTPError as exc:
312
+ return json.dumps({"errors": [{"message": f"Failed to download file: {exc}"}]})
313
+ except OSError as exc:
314
+ return json.dumps({"errors": [{"message": f"Failed to save file: {exc}"}]})
315
+
316
+ return json.dumps({"data": {"filePath": file_path}})
317
+
318
+ def main() -> None:
319
+ mcp = FastMCP(SERVER_NAME)
320
+
321
+ mcp.tool(
322
+ name="getEstateDocuments",
323
+ title="Get estate documents",
324
+ description=(
325
+ "Fetch all documents for an estate"
326
+ "Returns raw JSON containing `{data, errors, extensions, http_status}`."
327
+ ),
328
+ )(run_get_estate_documents)
329
+
330
+ mcp.tool(
331
+ name="getFile",
332
+ title="Get file",
333
+ description=(
334
+ "Fetch a file by id"
335
+ "Returns raw JSON containing `{data, errors, extensions, http_status}`."
336
+ ),
337
+ )(run_get_file)
338
+
339
+ mcp.tool(
340
+ name="downloadFileToTempFolder",
341
+ title="Download file to temporary folder",
342
+ description=(
343
+ "Download a file to a temporary folder and return the file path. Get file info via other tools first, for example 'getEstateDocuments'."
344
+ "Returns raw JSON containing `{data, errors, extensions, http_status}`."
345
+ ),
346
+ )(download_file_to_temp_folder)
347
+
348
+ mcp.run()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: alix-doc-vault-tool
3
+ Version: 0.1.0
4
+ Summary: Documents Vault MCP tool for Alix. Fetches files, files per estate and downloads them into temporary folder for next processing
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: mcp>=1.9.0
7
+ Requires-Dist: httpx>=0.27
8
+ Provides-Extra: dev
9
+ Requires-Dist: build; extra == "dev"
10
+ Requires-Dist: twine; extra == "dev"
@@ -0,0 +1,10 @@
1
+ pyproject.toml
2
+ src/alix_doc_vault_tool/__init__.py
3
+ src/alix_doc_vault_tool/__main__.py
4
+ src/alix_doc_vault_tool/mcp_server.py
5
+ src/alix_doc_vault_tool.egg-info/PKG-INFO
6
+ src/alix_doc_vault_tool.egg-info/SOURCES.txt
7
+ src/alix_doc_vault_tool.egg-info/dependency_links.txt
8
+ src/alix_doc_vault_tool.egg-info/entry_points.txt
9
+ src/alix_doc_vault_tool.egg-info/requires.txt
10
+ src/alix_doc_vault_tool.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ alix-doc-vault-tool = alix_doc_vault_tool.mcp_server:main
@@ -0,0 +1,6 @@
1
+ mcp>=1.9.0
2
+ httpx>=0.27
3
+
4
+ [dev]
5
+ build
6
+ twine
@@ -0,0 +1 @@
1
+ alix_doc_vault_tool