File size: 9,168 Bytes
b6fb194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import subprocess
import tempfile
from pathlib import Path
from typing import List, Optional

# MCP-standardized error handling
class MCPError(Exception):
    def __init__(self, message, stderr):
        self.message = message
        self.stderr = stderr
        super().__init__(self.message)

def _run_command(cmd: List[str]):
    """Helper function to run a command and handle exceptions."""
    try:
        command_str = " ".join(cmd)
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            check=True,
        )
        return {
            "command_executed": command_str,
            "stdout": result.stdout,
            "stderr": result.stderr,
        }
    except FileNotFoundError:
        raise MCPError(f"Command '{cmd[0]}' not found. Is arvados-python-client installed and in the PATH?", "")
    except subprocess.CalledProcessError as e:
        raise MCPError(f"Command '{' '.join(cmd)}' failed with exit code {e.returncode}", e.stderr)

from mcp.server.fastmcp import FastMCP

SERVER_NAME = 'local_arvados_python_client'
mcp = FastMCP(SERVER_NAME)

@mcp.tool()
def arv_get(
    object_locator: str,
    destination_path: Optional[Path] = None,
    filename: Optional[str] = None,
    force: bool = False,
    recursive: bool = False,
    progress: bool = False,
) -> dict:
    """
    Downloads an object from Arvados Keep to a local path or standard output.

    Args:
        object_locator: The UUID or portable data hash (PDH) of the object to download.
        destination_path: Local path to save the object. If a directory, the original filename is used. If not provided, content is written to stdout.
        filename: The name of the file to get from a collection.
        force: Overwrite existing files if they exist.
        recursive: Recursively download collections.
        progress: Show a progress bar during download.

    Returns:
        A dictionary containing the command executed, stdout, stderr, and a list of output files.
    """
    cmd = ["arv-get"]

    if force:
        cmd.append("--force")
    if recursive:
        cmd.append("--recursive")
    if progress:
        cmd.append("--progress")
    if filename:
        cmd.extend(["--filename", filename])

    cmd.append(object_locator)

    output_files = []
    if destination_path:
        # Ensure parent directory exists if a full path is given
        if destination_path.parent and not destination_path.parent.is_dir():
            destination_path.parent.mkdir(parents=True, exist_ok=True)
        cmd.append(str(destination_path))
        output_files.append(str(destination_path))

    try:
        result = _run_command(cmd)
        result["output_files"] = output_files
        return result
    except MCPError as e:
        return {
            "command_executed": " ".join(cmd),
            "stdout": "",
            "stderr": e.stderr,
            "error": e.message,
            "output_files": [],
        }

@mcp.tool()
def arv_put(
    paths: List[Path],
    name: Optional[str] = None,
    owner: Optional[str] = None,
    portable_data_hash: bool = False,
    storage_class: Optional[str] = None,
    num_retries: int = 5,
    progress: bool = False,
) -> dict:
    """
    Uploads local files or directories to Arvados Keep.

    Args:
        paths: One or more local file or directory paths to upload.
        name: Set the name of the new collection.
        owner: Set the owner project UUID for the new collection.
        portable_data_hash: Use portable data hash to identify the collection.
        storage_class: The storage class for the data (e.g., 'default', 'trash').
        num_retries: Number of times to retry a failed API call.
        progress: Show a progress bar during upload.

    Returns:
        A dictionary containing the command executed, stdout, and stderr.
    """
    if not paths:
        raise ValueError("At least one path must be provided for upload.")

    for p in paths:
        if not p.exists():
            raise FileNotFoundError(f"Input path does not exist: {p}")

    cmd = ["arv-put"]

    if name:
        cmd.extend(["--name", name])
    if owner:
        cmd.extend(["--owner", owner])
    if portable_data_hash:
        cmd.append("--portable-data-hash")
    if storage_class:
        cmd.extend(["--storage-class", storage_class])
    if progress:
        cmd.append("--progress")
    
    # num_retries is a standard arvados client option
    cmd.extend(["--num-retries", str(num_retries)])

    cmd.extend([str(p) for p in paths])

    try:
        result = _run_command(cmd)
        result["output_files"] = []
        return result
    except MCPError as e:
        return {
            "command_executed": " ".join(cmd),
            "stdout": "",
            "stderr": e.stderr,
            "error": e.message,
            "output_files": [],
        }

@mcp.tool()
def arv_ls(
    object_locator: str,
    path: Optional[str] = None,
    long_format: bool = False,
    recursive: bool = False,
    human_readable: bool = False,
) -> dict:
    """
    Lists the contents of a collection in Arvados Keep.

    Args:
        object_locator: The UUID or portable data hash (PDH) of the collection to list.
        path: A path within the collection to list.
        long_format: Use a long listing format (similar to ls -l).
        recursive: List subdirectories recursively.
        human_readable: With long_format, print sizes in human-readable format (e.g., 1K, 234M, 2G).

    Returns:
        A dictionary containing the command executed, stdout (the listing), and stderr.
    """
    cmd = ["arv-ls"]

    if long_format:
        cmd.append("-l")
    if recursive:
        cmd.append("-R")
    if human_readable:
        cmd.append("-h")

    cmd.append(object_locator)
    if path:
        cmd.append(path)

    try:
        result = _run_command(cmd)
        result["output_files"] = []
        return result
    except MCPError as e:
        return {
            "command_executed": " ".join(cmd),
            "stdout": "",
            "stderr": e.stderr,
            "error": e.message,
            "output_files": [],
        }

@mcp.tool()
def arv_copy(
    src_locators: List[str],
    dest_project_uuid: str,
    name: Optional[str] = None,
    num_retries: int = 5,
) -> dict:
    """
    Copies one or more objects to a destination project in Arvados.

    Args:
        src_locators: A list of source object UUIDs or portable data hashes (PDHs).
        dest_project_uuid: The UUID of the destination project.
        name: New name for the object(s). If copying multiple objects, this is ignored.
        num_retries: Number of times to retry a failed API call.

    Returns:
        A dictionary containing the command executed, stdout, and stderr.
    """
    if not src_locators:
        raise ValueError("At least one source locator must be provided.")
    if not dest_project_uuid:
        raise ValueError("A destination project UUID must be provided.")

    cmd = ["arv-copy"]

    if name and len(src_locators) == 1:
        cmd.extend(["--name", name])
    
    cmd.extend(["--num-retries", str(num_retries)])
    
    cmd.extend(src_locators)
    cmd.append(dest_project_uuid)

    try:
        result = _run_command(cmd)
        result["output_files"] = []
        return result
    except MCPError as e:
        return {
            "command_executed": " ".join(cmd),
            "stdout": "",
            "stderr": e.stderr,
            "error": e.message,
            "output_files": [],
        }

@mcp.tool()
def arv_tag(
    locator: str,
    tags_to_add: Optional[List[str]] = None,
    keys_to_remove: Optional[List[str]] = None,
    list_tags: bool = False,
    num_retries: int = 5,
) -> dict:
    """
    Manipulates tags on an Arvados object. It can list, add, or remove tags.

    Args:
        locator: The UUID or portable data hash (PDH) of the object to tag.
        tags_to_add: A list of tags to add, in 'key' or 'key=value' format.
        keys_to_remove: A list of tag keys to remove.
        list_tags: If True, lists the existing tags on the object.
        num_retries: Number of times to retry a failed API call.

    Returns:
        A dictionary containing the command executed, stdout, and stderr.
    """
    cmd = ["arv-tag"]
    
    cmd.extend(["--num-retries", str(num_retries)])

    if list_tags:
        cmd.append("--list")
    
    cmd.append(locator)

    if tags_to_add:
        for tag in tags_to_add:
            if "=" not in tag and " " in tag:
                raise ValueError(f"Invalid tag format: '{tag}'. Use 'key=value' or 'key'.")
        cmd.extend(tags_to_add)

    if keys_to_remove:
        for key in keys_to_remove:
            cmd.extend(["--remove", key])

    try:
        result = _run_command(cmd)
        result["output_files"] = []
        return result
    except MCPError as e:
        return {
            "command_executed": " ".join(cmd),
            "stdout": "",
            "stderr": e.stderr,
            "error": e.message,
            "output_files": [],
        }

if __name__ == "__main__":
    mcp.run(transport="stdio")