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.
- alix_doc_vault_tool-0.1.0/PKG-INFO +10 -0
- alix_doc_vault_tool-0.1.0/pyproject.toml +22 -0
- alix_doc_vault_tool-0.1.0/setup.cfg +4 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool/__init__.py +12 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool/__main__.py +6 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool/mcp_server.py +348 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool.egg-info/PKG-INFO +10 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool.egg-info/SOURCES.txt +10 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool.egg-info/dependency_links.txt +1 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool.egg-info/entry_points.txt +2 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool.egg-info/requires.txt +6 -0
- alix_doc_vault_tool-0.1.0/src/alix_doc_vault_tool.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
alix_doc_vault_tool
|