contree-mcp 0.1.0__py3-none-any.whl

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.
Files changed (46) hide show
  1. contree_mcp/__init__.py +0 -0
  2. contree_mcp/__main__.py +25 -0
  3. contree_mcp/app.py +240 -0
  4. contree_mcp/arguments.py +35 -0
  5. contree_mcp/auth/__init__.py +2 -0
  6. contree_mcp/auth/registry.py +236 -0
  7. contree_mcp/backend_types.py +301 -0
  8. contree_mcp/cache.py +208 -0
  9. contree_mcp/client.py +711 -0
  10. contree_mcp/context.py +53 -0
  11. contree_mcp/docs.py +1203 -0
  12. contree_mcp/file_cache.py +381 -0
  13. contree_mcp/prompts.py +238 -0
  14. contree_mcp/py.typed +0 -0
  15. contree_mcp/resources/__init__.py +17 -0
  16. contree_mcp/resources/guide.py +715 -0
  17. contree_mcp/resources/image_lineage.py +46 -0
  18. contree_mcp/resources/image_ls.py +32 -0
  19. contree_mcp/resources/import_operation.py +52 -0
  20. contree_mcp/resources/instance_operation.py +52 -0
  21. contree_mcp/resources/read_file.py +33 -0
  22. contree_mcp/resources/static.py +12 -0
  23. contree_mcp/server.py +77 -0
  24. contree_mcp/tools/__init__.py +39 -0
  25. contree_mcp/tools/cancel_operation.py +36 -0
  26. contree_mcp/tools/download.py +128 -0
  27. contree_mcp/tools/get_guide.py +54 -0
  28. contree_mcp/tools/get_image.py +30 -0
  29. contree_mcp/tools/get_operation.py +26 -0
  30. contree_mcp/tools/import_image.py +99 -0
  31. contree_mcp/tools/list_files.py +80 -0
  32. contree_mcp/tools/list_images.py +50 -0
  33. contree_mcp/tools/list_operations.py +46 -0
  34. contree_mcp/tools/read_file.py +47 -0
  35. contree_mcp/tools/registry_auth.py +71 -0
  36. contree_mcp/tools/registry_token_obtain.py +80 -0
  37. contree_mcp/tools/rsync.py +46 -0
  38. contree_mcp/tools/run.py +97 -0
  39. contree_mcp/tools/set_tag.py +31 -0
  40. contree_mcp/tools/upload.py +50 -0
  41. contree_mcp/tools/wait_operations.py +79 -0
  42. contree_mcp-0.1.0.dist-info/METADATA +450 -0
  43. contree_mcp-0.1.0.dist-info/RECORD +46 -0
  44. contree_mcp-0.1.0.dist-info/WHEEL +4 -0
  45. contree_mcp-0.1.0.dist-info/entry_points.txt +2 -0
  46. contree_mcp-0.1.0.dist-info/licenses/LICENSE +176 -0
@@ -0,0 +1,46 @@
1
+ import json
2
+ from urllib.parse import unquote
3
+
4
+ from contree_mcp.context import CLIENT
5
+
6
+
7
+ async def image_lineage(image: str) -> str:
8
+ """View image parent-child relationships and history.
9
+
10
+ View image parent-child relationships and history. Free (no VM).
11
+
12
+ URI: contree://image/{image}/lineage
13
+
14
+ Returns lineage information including:
15
+ - parent: Immediate parent image
16
+ - children: Direct children of this image
17
+ - ancestors: Full parent chain up to root
18
+ - root: Root imported image
19
+ - depth: Number of ancestors
20
+ - is_known: Whether the image is in our lineage database
21
+ - data: Stored metadata (command, operation_id, etc.)
22
+
23
+ Example: contree://image/abc-123-def/lineage
24
+ """
25
+ cache = CLIENT.get().cache
26
+ # URL-decode image since it may contain encoded characters
27
+ decoded_image = unquote(image)
28
+
29
+ entry = await cache.get("image", decoded_image)
30
+ ancestors = await cache.get_ancestors("image", decoded_image)
31
+ children = await cache.get_children("image", decoded_image)
32
+
33
+ # Extract root from ancestors (last one) or self if no ancestors
34
+ root = ancestors[-1].key if ancestors else (entry.key if entry else None)
35
+
36
+ lineage_data = {
37
+ "image": decoded_image,
38
+ "parent": entry.data.get("parent_image") if entry else None,
39
+ "children": [c.key for c in children],
40
+ "ancestors": [a.key for a in ancestors],
41
+ "root": root,
42
+ "depth": len(ancestors),
43
+ "is_known": entry is not None,
44
+ "data": dict(entry.data) if entry else None,
45
+ }
46
+ return json.dumps(lineage_data, indent=2)
@@ -0,0 +1,32 @@
1
+ from urllib.parse import unquote
2
+
3
+ from contree_mcp.context import CLIENT
4
+
5
+
6
+ async def image_ls(image: str, path: str) -> str:
7
+ """List files and directories in a container image.
8
+
9
+ List files and directories in a container image. Free (no VM).
10
+ Output is ls -alh style text format.
11
+
12
+ URI: contree://image/{image}/ls/{path}
13
+
14
+ Where:
15
+ - image: Image UUID or tag prefixed with "tag:" (e.g., "tag:alpine:latest")
16
+ - path: Directory path without leading slash (e.g., "etc" or "usr/bin")
17
+ - For root directory, use path "."
18
+
19
+ Examples:
20
+ - contree://image/abc-123-def/ls/.
21
+ - contree://image/abc-123-def/ls/etc
22
+ - contree://image/abc-123-def/ls/usr/local/bin
23
+ - contree://image/tag:alpine:latest/ls/etc
24
+ """
25
+
26
+ client = CLIENT.get()
27
+ # URL-decode image and path since they may contain encoded characters
28
+ decoded_image = unquote(image)
29
+ decoded_path = unquote(path)
30
+ image_uuid = await client.resolve_image(decoded_image)
31
+ dir_path = "/" if decoded_path in (".", "") else "/" + decoded_path
32
+ return await client.list_directory_text(image_uuid, dir_path)
@@ -0,0 +1,52 @@
1
+ from contree_mcp.backend_types import ImportImageMetadata, OperationKind
2
+ from contree_mcp.context import CLIENT
3
+
4
+
5
+ async def import_operation(operation_id: str) -> str:
6
+ """
7
+ Read image import operation details. Free (no VM).
8
+
9
+ Return text in format:
10
+ ```
11
+ STATE: SUCCESS
12
+ RESULT_IMAGE: <uuid>
13
+ RESULT_TAG: latest
14
+ REGISTRY_URL: registry.example.com/repo/image:tag
15
+ ERROR:
16
+ multiline error message if any
17
+ ```
18
+
19
+ Strings might absent in case it's not applicable.
20
+
21
+ URI: contree://operations/import/{operation_id}
22
+
23
+ Example: contree://operations/import/op-abc-123-def
24
+ Returns:
25
+ ```
26
+ STATE: SUCCESS
27
+ RESULT_IMAGE: 550e8400-e29b-41d4-a716-446655440000
28
+ RESULT_TAG: latest
29
+ REGISTRY_URL: registry.example.com/repo/image:tag
30
+ """
31
+ client = CLIENT.get()
32
+ op = await client.get_operation(operation_id)
33
+ if op.kind != OperationKind.IMAGE_IMPORT:
34
+ raise ValueError(f"Operation {operation_id} is not an import operation (kind={op.kind})")
35
+
36
+ result = f"STATE: {op.status.value}"
37
+
38
+ if op.result and op.result.image:
39
+ result += f"\nRESULT_IMAGE: {op.result.image}"
40
+ if op.result and op.result.tag:
41
+ result += f"\nRESULT_TAG: {op.result.tag}"
42
+
43
+ # Extract registry URL from metadata
44
+ if isinstance(op.metadata, ImportImageMetadata):
45
+ registry_url = str(op.metadata.registry.url) if op.metadata.registry else None
46
+ if registry_url:
47
+ result += f"\nREGISTRY_URL: {registry_url}"
48
+
49
+ if op.error:
50
+ result += f"\nERROR:\n{op.error}"
51
+
52
+ return result
@@ -0,0 +1,52 @@
1
+ import json
2
+
3
+ from contree_mcp.backend_types import InstanceMetadata, OperationKind
4
+ from contree_mcp.context import CLIENT
5
+
6
+
7
+ async def instance_operation(operation_id: str) -> str:
8
+ """Read instance (command execution) operation details.
9
+
10
+ Read instance (command execution) operation details. Free (no VM).
11
+
12
+ URI: contree://operations/instance/{operation_id}
13
+
14
+ Returns cached operation data including:
15
+ - state: Operation state (SUCCESS, FAILED, etc.)
16
+ - exit_code: Command exit code
17
+ - stdout/stderr: Command output
18
+ - result_image: Output image UUID (if disposable=false)
19
+ - resources: CPU/memory usage statistics
20
+
21
+ Example: contree://operations/instance/op-abc-123-def
22
+ """
23
+ client = CLIENT.get()
24
+ # Use get_operation which checks cache first, then fetches from API
25
+ op = await client.get_operation(operation_id)
26
+
27
+ if op.kind != OperationKind.INSTANCE:
28
+ raise ValueError(f"Operation {operation_id} is not an instance operation (kind={op.kind})")
29
+
30
+ result_data: dict[str, object] = {
31
+ "state": op.status.value,
32
+ }
33
+
34
+ if op.error:
35
+ result_data["error"] = op.error
36
+
37
+ if op.result:
38
+ result_data["result_image"] = op.result.image
39
+ if op.result.tag:
40
+ result_data["result_tag"] = op.result.tag
41
+
42
+ # Extract instance-specific metadata
43
+ if isinstance(op.metadata, InstanceMetadata) and op.metadata.result:
44
+ instance_result = op.metadata.result
45
+ result_data["exit_code"] = instance_result.state.exit_code
46
+ result_data["timed_out"] = instance_result.state.timed_out
47
+ result_data["stdout"] = instance_result.stdout.text() if instance_result.stdout else ""
48
+ result_data["stderr"] = instance_result.stderr.text() if instance_result.stderr else ""
49
+ if instance_result.resources:
50
+ result_data["resources"] = instance_result.resources.model_dump()
51
+
52
+ return json.dumps(result_data, indent=2)
@@ -0,0 +1,33 @@
1
+ import base64
2
+ from urllib.parse import unquote
3
+
4
+ from contree_mcp.context import CLIENT
5
+
6
+
7
+ async def read_file(image: str, path: str) -> str:
8
+ """Read a file from a container image.
9
+
10
+ Read text files, config files, scripts, etc. without incurring VM costs.
11
+ Binary files will be returned as BASE64-encoded strings prefixed with "base64:".
12
+
13
+ URI: contree://image/{image}/read/{path}
14
+
15
+ Where:
16
+ - image: Image UUID or tag prefixed with "tag:" (e.g., "tag:alpine:latest")
17
+ - path: File path without leading slash (e.g., "etc/passwd")
18
+
19
+ Examples:
20
+ - contree://image/abc-123-def/read/etc/passwd
21
+ - contree://image/abc-123-def/read/usr/local/bin/python
22
+ - contree://image/tag:alpine:latest/read/etc/alpine-release
23
+ """
24
+ client = CLIENT.get()
25
+ image_uuid = await client.resolve_image(image)
26
+ file_path = "/" + unquote(path).lstrip("/")
27
+ content = await client.read_file(image_uuid, file_path)
28
+
29
+ try:
30
+ return content.decode("utf-8")
31
+ except (UnicodeDecodeError, AttributeError):
32
+ b64_content = base64.b64encode(content).decode("utf-8")
33
+ return f"base64:{b64_content}"
@@ -0,0 +1,12 @@
1
+ from typing import Any
2
+
3
+ from mcp.server.fastmcp.resources import Resource
4
+
5
+
6
+ class StaticResource(Resource):
7
+ def __init__(self, content: str, /, **data: Any) -> None:
8
+ super().__init__(**data)
9
+ self._content = content
10
+
11
+ async def read(self) -> str | bytes:
12
+ return self._content
contree_mcp/server.py ADDED
@@ -0,0 +1,77 @@
1
+ import contextvars
2
+ import logging
3
+ from contextlib import AsyncExitStack
4
+ from functools import partial
5
+
6
+ import uvicorn
7
+ from starlette.requests import Request
8
+ from starlette.responses import HTMLResponse
9
+
10
+ from contree_mcp.app import create_mcp_app
11
+ from contree_mcp.arguments import Parser, ServerMode
12
+ from contree_mcp.cache import Cache
13
+ from contree_mcp.client import ContreeClient
14
+ from contree_mcp.context import CLIENT, FILES_CACHE, ContextMiddleware
15
+ from contree_mcp.docs import generate_docs_html
16
+ from contree_mcp.file_cache import FileCache
17
+ from contree_mcp.resources.guide import SECTIONS
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ async def index_page(docs_html: str, _: Request) -> HTMLResponse:
23
+ return HTMLResponse(docs_html)
24
+
25
+
26
+ async def amain(parser: Parser) -> None:
27
+ async with AsyncExitStack() as stack:
28
+ # Initialize all dependencies
29
+ files_cache = await stack.enter_async_context(FileCache(db_path=parser.cache.files.expanduser()))
30
+ general_cache = await stack.enter_async_context(
31
+ Cache(
32
+ db_path=parser.cache.general.expanduser(),
33
+ retention_days=parser.cache.prune_days,
34
+ )
35
+ )
36
+ client = await stack.enter_async_context(
37
+ ContreeClient(base_url=parser.url, token=parser.token, cache=general_cache)
38
+ )
39
+
40
+ CLIENT.set(client)
41
+ FILES_CACHE.set(files_cache)
42
+
43
+ mcp = create_mcp_app()
44
+
45
+ log.debug("MCP app initialized: %s", CLIENT.get())
46
+
47
+ if parser.mode == ServerMode.HTTP:
48
+ log.info("Starting MCP server on http://%s:%d", parser.http.listen, parser.http.port)
49
+
50
+ # Generate docs HTML
51
+ tools = await mcp.list_tools()
52
+ templates = await mcp.list_resource_templates()
53
+ docs_html = generate_docs_html(
54
+ server_instructions=mcp.instructions or "",
55
+ tools=tools,
56
+ templates=templates,
57
+ guides=SECTIONS,
58
+ http_port=parser.http.port,
59
+ )
60
+
61
+ app = mcp.streamable_http_app()
62
+ app.add_middleware(ContextMiddleware, ctx=contextvars.copy_context())
63
+ app.add_route("/", partial(index_page, docs_html), methods=["GET"])
64
+
65
+ config = uvicorn.Config(
66
+ app,
67
+ host=parser.http.listen,
68
+ port=parser.http.port,
69
+ log_level="info",
70
+ )
71
+ server = uvicorn.Server(config)
72
+ await server.serve()
73
+ elif parser.mode == ServerMode.STDIO:
74
+ log.info("Starting MCP server in stdio mode")
75
+ await mcp.run_stdio_async()
76
+ else:
77
+ raise ValueError(f"Unsupported server mode: {parser.mode}")
@@ -0,0 +1,39 @@
1
+ """Contree MCP tools."""
2
+
3
+ from .cancel_operation import cancel_operation
4
+ from .download import download
5
+ from .get_guide import get_guide
6
+ from .get_image import get_image
7
+ from .get_operation import get_operation
8
+ from .import_image import import_image
9
+ from .list_files import list_files
10
+ from .list_images import list_images
11
+ from .list_operations import list_operations
12
+ from .read_file import read_file
13
+ from .registry_auth import registry_auth
14
+ from .registry_token_obtain import registry_token_obtain
15
+ from .rsync import rsync
16
+ from .run import run
17
+ from .set_tag import set_tag
18
+ from .upload import upload
19
+ from .wait_operations import wait_operations
20
+
21
+ __all__ = [
22
+ "cancel_operation",
23
+ "download",
24
+ "get_guide",
25
+ "get_image",
26
+ "get_operation",
27
+ "import_image",
28
+ "list_files",
29
+ "list_images",
30
+ "list_operations",
31
+ "read_file",
32
+ "registry_auth",
33
+ "registry_token_obtain",
34
+ "rsync",
35
+ "run",
36
+ "set_tag",
37
+ "upload",
38
+ "wait_operations",
39
+ ]
@@ -0,0 +1,36 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from contree_mcp.context import CLIENT
4
+
5
+
6
+ class CancelOperationOutput(BaseModel):
7
+ cancelled: bool = Field(description="Whether cancellation succeeded")
8
+ operation_id: str = Field(description="UUID of the cancelled operation")
9
+
10
+
11
+ async def cancel_operation(operation_id: str) -> CancelOperationOutput:
12
+ """
13
+ Cancel a running operation. Free (no VM).
14
+
15
+ TL;DR:
16
+ - PURPOSE: Stop operations taking too long or no longer needed
17
+ - COST: Free (no VM) - saves resources by stopping unnecessary work
18
+
19
+ USAGE:
20
+ - Stop long-running commands that are taking too long
21
+ - Cancel image imports that are stuck or no longer needed
22
+
23
+ RETURNS: cancelled, operation_id
24
+
25
+ GUIDES:
26
+ - [USEFUL] contree://guide/async - Async execution and cancellation
27
+ """
28
+
29
+ client = CLIENT.get()
30
+ result_status = await client.cancel_operation(operation_id)
31
+ # Check if operation was cancelled (CANCELLED status)
32
+ cancelled = result_status == "CANCELLED"
33
+ return CancelOperationOutput(
34
+ cancelled=cancelled,
35
+ operation_id=operation_id,
36
+ )
@@ -0,0 +1,128 @@
1
+ import asyncio
2
+ import contextlib
3
+ import os
4
+ import platform
5
+ from collections.abc import AsyncIterable
6
+ from pathlib import Path
7
+ from queue import Queue
8
+ from tempfile import mktemp
9
+
10
+ from pydantic import BaseModel, ByteSize, Field
11
+
12
+ from contree_mcp.context import CLIENT
13
+
14
+
15
+ class DownloadSource(BaseModel):
16
+ image: str = Field(description="Image UUID")
17
+ path: str = Field(description="Path in container")
18
+
19
+
20
+ class DownloadOutput(BaseModel):
21
+ success: bool = Field(description="Whether download succeeded")
22
+ source: DownloadSource = Field(description="Source image and path")
23
+ destination: str = Field(description="Local path where file was saved")
24
+ size: ByteSize = Field(description="File size in bytes")
25
+ executable: bool = Field(description="Whether file was made executable")
26
+
27
+
28
+ async def async_file_writer(destination: Path, stream: AsyncIterable[bytes]) -> int:
29
+ queue: Queue[bytes | BaseException | None] = Queue(maxsize=16)
30
+ write_event = asyncio.Event()
31
+ loop = asyncio.get_running_loop()
32
+
33
+ def sync_writer(dest: Path) -> int:
34
+ total_bytes = 0
35
+ with dest.open("wb") as f:
36
+ while True:
37
+ match queue.get():
38
+ case None:
39
+ loop.call_soon_threadsafe(write_event.set)
40
+ return total_bytes
41
+ case BaseException():
42
+ # Error occurred, close file and remove temp
43
+ loop.call_soon_threadsafe(write_event.set)
44
+ f.close()
45
+ dest.unlink(missing_ok=True)
46
+ return -1
47
+ case bytes() as chunk:
48
+ loop.call_soon_threadsafe(write_event.set)
49
+ f.write(chunk)
50
+ queue.task_done()
51
+ total_bytes += len(chunk)
52
+
53
+ async def queue_waiter() -> None:
54
+ while queue.full():
55
+ write_event.clear()
56
+ # Circuit breaker to avoid deadlocks
57
+ with contextlib.suppress(asyncio.TimeoutError):
58
+ await asyncio.wait_for(write_event.wait(), timeout=1)
59
+
60
+ tmp_path = Path(mktemp(dir=destination.parent, prefix=".download-", suffix=".tmp"))
61
+ task = asyncio.create_task(asyncio.to_thread(sync_writer, tmp_path))
62
+ error: BaseException | None = None
63
+
64
+ try:
65
+ async for chunk in stream:
66
+ await queue_waiter()
67
+ queue.put_nowait(chunk)
68
+ except BaseException as e:
69
+ error = e
70
+ finally:
71
+ # Have to wait for queue to be drained before sending signal
72
+ await queue_waiter()
73
+ queue.put_nowait(error) # None for success, exception for error
74
+
75
+ await task
76
+ if error is not None:
77
+ raise error
78
+
79
+ await asyncio.to_thread(os.replace, tmp_path, destination)
80
+ return task.result()
81
+
82
+
83
+ async def download(
84
+ image: str,
85
+ path: str,
86
+ destination: str,
87
+ executable: bool = False,
88
+ ) -> DownloadOutput:
89
+ """
90
+ Download file from container image to local filesystem. Free (no VM).
91
+
92
+ TL;DR:
93
+ - PURPOSE: Extract files from container images to local filesystem
94
+ - EXECUTABLE: Set executable=true for binaries to run locally
95
+ - COST: Free (no VM)
96
+
97
+ USAGE:
98
+ - destination: Absolute path for downloaded file (~ expansion supported)
99
+ - Extract compiled binaries or build artifacts
100
+ - Save configuration files for local editing
101
+ - Retrieve logs or output files from completed runs
102
+
103
+ RETURNS: success, destination, size, size_human
104
+
105
+ GUIDES:
106
+ - [USEFUL] contree://guide/reference - Tool reference and resources
107
+ """
108
+ client = CLIENT.get()
109
+ image_uuid = await client.resolve_image(image)
110
+ dest_path = Path(os.path.expanduser(destination))
111
+ if not dest_path.is_absolute():
112
+ raise ValueError(f"destination must be an absolute path, got: {destination}")
113
+
114
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
115
+
116
+ async with client.stream_file(image_uuid, path) as chunks:
117
+ file_size = await async_file_writer(dest_path, chunks)
118
+
119
+ if executable and platform.system() != "Windows":
120
+ dest_path.chmod(0o755)
121
+
122
+ return DownloadOutput(
123
+ success=True,
124
+ source=DownloadSource(image=image_uuid, path=path),
125
+ destination=str(dest_path),
126
+ size=ByteSize(file_size),
127
+ executable=executable,
128
+ )
@@ -0,0 +1,54 @@
1
+ from pydantic import BaseModel
2
+
3
+ from contree_mcp.resources.guide import SECTIONS
4
+
5
+
6
+ class GetGuideOutput(BaseModel):
7
+ section: str
8
+ content: str
9
+ available_sections: list[str]
10
+
11
+
12
+ async def get_guide(section: str) -> GetGuideOutput:
13
+ """
14
+ Get agent guide sections for Contree best practices. Free (no VM).
15
+
16
+ TL;DR:
17
+ - PURPOSE: Access documentation and best practices for using Contree
18
+ - SECTIONS: workflow, reference, quickstart, state, async, tagging, errors
19
+ - COST: Free (no VM)
20
+
21
+ USAGE:
22
+ - Get workflow patterns: get_guide(section="workflow")
23
+ - Get tool reference: get_guide(section="reference")
24
+ - Get quick start examples: get_guide(section="quickstart")
25
+ - Get state management guide: get_guide(section="state")
26
+ - Get async execution guide: get_guide(section="async")
27
+ - Get tagging convention: get_guide(section="tagging")
28
+ - Get error handling guide: get_guide(section="errors")
29
+
30
+ RETURNS: section, content, available_sections
31
+
32
+ AVAILABLE SECTIONS:
33
+ - workflow: Complete workflow patterns with decision tree
34
+ - reference: Tool reference with parameters and data flow
35
+ - quickstart: Quick examples for common operations
36
+ - state: State management and rollback patterns
37
+ - async: Parallel execution and wait_operations
38
+ - tagging: Tagging convention for prepared environments
39
+ - errors: Error handling and debugging guide
40
+
41
+ RESOURCE ALTERNATIVE:
42
+ - contree://guide/{section} - Same content as MCP resource (if your agent supports resources)
43
+ """
44
+
45
+ available = sorted(SECTIONS.keys())
46
+
47
+ if section not in SECTIONS:
48
+ raise ValueError(f"Unknown guide section '{section}'. Available sections: {', '.join(available)}")
49
+
50
+ return GetGuideOutput(
51
+ section=section,
52
+ content=SECTIONS[section],
53
+ available_sections=available,
54
+ )
@@ -0,0 +1,30 @@
1
+ from contree_mcp.backend_types import Image
2
+ from contree_mcp.context import CLIENT
3
+
4
+
5
+ async def get_image(image: str) -> Image:
6
+ """
7
+ Get image details by UUID or tag. Free (no VM).
8
+
9
+ TL;DR:
10
+ - PURPOSE: Verify image exists or resolve tag to UUID
11
+ - FORMAT: Use "tag:python:3.11" to look up by tag
12
+ - COST: Free (no VM)
13
+
14
+ USAGE:
15
+ - Look up image metadata (UUID, tag, creation time)
16
+ - Verify image exists before running commands
17
+ - Resolve tag to underlying UUID
18
+ - Prefer verifying an existing image before using import_image
19
+
20
+ RETURNS: uuid, tag, created_at
21
+
22
+ GUIDES:
23
+ - [USEFUL] contree://guide/quickstart - UUIDs vs tags guidance
24
+ """
25
+ client = CLIENT.get()
26
+ if image.startswith("tag:"):
27
+ img = await client.get_image_by_tag(image[4:])
28
+ else:
29
+ img = await client.get_image(image)
30
+ return Image(uuid=img.uuid, tag=img.tag, created_at=img.created_at)
@@ -0,0 +1,26 @@
1
+ from contree_mcp.backend_types import OperationResponse
2
+ from contree_mcp.context import CLIENT
3
+
4
+
5
+ async def get_operation(operation_id: str) -> OperationResponse:
6
+ """
7
+ Get status and result of an operation. Free (no VM).
8
+
9
+ TL;DR:
10
+ - PURPOSE: Poll async operations launched with wait=false
11
+ - PREFER: Use wait_operations for multiple operations
12
+ - COST: Free (no VM)
13
+
14
+ USAGE:
15
+ - Check status of async operations started with wait=false
16
+ - Retrieve stdout/stderr from completed command executions
17
+ - Get result_image UUID from non-disposable command runs
18
+
19
+ RETURNS: state, stdout, stderr, exit_code, result_image
20
+
21
+ GUIDES:
22
+ - [ESSENTIAL] contree://guide/async - Async execution and polling
23
+ """
24
+
25
+ client = CLIENT.get()
26
+ return await client.get_operation(operation_id)