xenfra-sdk 0.1.2__py3-none-any.whl → 0.1.3__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.
xenfra_sdk/exceptions.py CHANGED
@@ -1,19 +1,19 @@
1
- class XenfraError(Exception):
2
- """Base exception for all SDK errors."""
3
-
4
- pass
5
-
6
-
7
- class AuthenticationError(XenfraError):
8
- """Raised for issues related to authentication."""
9
-
10
- pass
11
-
12
-
13
- class XenfraAPIError(XenfraError):
14
- """Raised when the API returns a non-2xx status code."""
15
-
16
- def __init__(self, status_code: int, detail: str):
17
- self.status_code = status_code
18
- self.detail = detail
19
- super().__init__(f"API Error {status_code}: {detail}")
1
+ class XenfraError(Exception):
2
+ """Base exception for all SDK errors."""
3
+
4
+ pass
5
+
6
+
7
+ class AuthenticationError(XenfraError):
8
+ """Raised for issues related to authentication."""
9
+
10
+ pass
11
+
12
+
13
+ class XenfraAPIError(XenfraError):
14
+ """Raised when the API returns a non-2xx status code."""
15
+
16
+ def __init__(self, status_code: int, detail: str):
17
+ self.status_code = status_code
18
+ self.detail = detail
19
+ super().__init__(f"API Error {status_code}: {detail}")
xenfra_sdk/mcp_client.py CHANGED
@@ -1,154 +1,154 @@
1
- # src/xenfra/mcp_client.py
2
-
3
- import base64
4
- import json
5
- import os
6
- import subprocess
7
- import tempfile
8
- from pathlib import Path
9
-
10
-
11
- class MCPClient:
12
- """
13
- A client for communicating with a local github-mcp-server process.
14
-
15
- This client starts the MCP server as a subprocess and interacts with it
16
- over stdin and stdout to download a full repository to a temporary directory.
17
- """
18
-
19
- def __init__(self, mcp_server_path="github-mcp-server"):
20
- """
21
- Initializes the MCPClient.
22
-
23
- Args:
24
- mcp_server_path (str): The path to the github-mcp-server executable.
25
- Assumes it's in the system's PATH by default.
26
- """
27
- self.mcp_server_path = mcp_server_path
28
- self.process = None
29
-
30
- def _start_server(self):
31
- """Starts the github-mcp-server subprocess."""
32
- if self.process and self.process.poll() is None:
33
- return
34
- try:
35
- self.process = subprocess.Popen(
36
- [self.mcp_server_path],
37
- stdin=subprocess.PIPE,
38
- stdout=subprocess.PIPE,
39
- stderr=subprocess.PIPE,
40
- text=True,
41
- env=os.environ,
42
- )
43
- except FileNotFoundError:
44
- raise RuntimeError(
45
- f"'{self.mcp_server_path}' not found. Ensure github-mcp-server is installed and in your PATH."
46
- )
47
- except Exception as e:
48
- raise RuntimeError(f"Failed to start MCP server: {e}")
49
-
50
- def _stop_server(self):
51
- """Stops the MCP server process."""
52
- if self.process:
53
- self.process.terminate()
54
- self.process.wait()
55
- self.process = None
56
-
57
- def _send_request(self, method: str, params: dict) -> dict:
58
- """Sends a JSON-RPC request and returns the response."""
59
- if not self.process or self.process.poll() is not None:
60
- self._start_server()
61
-
62
- request = {"jsonrpc": "2.0", "id": os.urandom(4).hex(), "method": method, "params": params}
63
-
64
- try:
65
- self.process.stdin.write(json.dumps(request) + "\n")
66
- self.process.stdin.flush()
67
- response_line = self.process.stdout.readline()
68
- if not response_line:
69
- error_output = self.process.stderr.read()
70
- raise RuntimeError(f"MCP server closed stream unexpectedly. Error: {error_output}")
71
-
72
- response = json.loads(response_line)
73
- if "error" in response:
74
- raise RuntimeError(f"MCP server returned an error: {response['error']}")
75
- return response.get("result", {})
76
- except (BrokenPipeError, json.JSONDecodeError) as e:
77
- raise RuntimeError(f"Failed to communicate with MCP server: {e}")
78
-
79
- def download_repo_to_tempdir(self, repo_url: str, commit_sha: str = "HEAD") -> str:
80
- """
81
- Downloads an entire repository at a specific commit to a local temporary directory.
82
-
83
- Args:
84
- repo_url (str): The full URL of the GitHub repository.
85
- commit_sha (str): The commit SHA to download. Defaults to "HEAD".
86
-
87
- Returns:
88
- The path to the temporary directory containing the downloaded code.
89
- """
90
- try:
91
- parts = repo_url.strip("/").split("/")
92
- owner = parts[-2]
93
- repo_name = parts[-1].replace(".git", "")
94
- except IndexError:
95
- raise ValueError(
96
- "Invalid repository URL format. Expected format: https://github.com/owner/repo"
97
- )
98
-
99
- print(f" [MCP] Fetching file tree for {owner}/{repo_name} at {commit_sha}...")
100
- tree_result = self._send_request(
101
- method="git.get_repository_tree",
102
- params={"owner": owner, "repo": repo_name, "tree_sha": commit_sha, "recursive": True},
103
- )
104
-
105
- tree = tree_result.get("tree", [])
106
- if not tree:
107
- raise RuntimeError("Could not retrieve repository file tree.")
108
-
109
- temp_dir = tempfile.mkdtemp(prefix=f"xenfra_{repo_name}_")
110
- print(f" [MCP] Downloading to temporary directory: {temp_dir}")
111
-
112
- for item in tree:
113
- item_path = item.get("path")
114
- item_type = item.get("type")
115
-
116
- if not item_path or item_type != "blob": # Only handle files
117
- continue
118
-
119
- # For downloading content, we can use the commit_sha in the 'ref' parameter
120
- # to ensure we get the content from the correct version.
121
- content_result = self._send_request(
122
- method="repos.get_file_contents",
123
- params={"owner": owner, "repo": repo_name, "path": item_path, "ref": commit_sha},
124
- )
125
-
126
- content_b64 = content_result.get("content")
127
- if content_b64 is None:
128
- print(f" [MCP] [Warning] Could not get content for {item_path}")
129
- continue
130
-
131
- try:
132
- # Content is base64 encoded, with newlines.
133
- decoded_content = base64.b64decode(content_b64.replace("\n", ""))
134
- except (base64.binascii.Error, TypeError):
135
- print(f" [MCP] [Warning] Could not decode content for {item_path}")
136
- continue
137
-
138
- # Create file and parent directories in the temp location
139
- local_file_path = Path(temp_dir) / item_path
140
- local_file_path.parent.mkdir(parents=True, exist_ok=True)
141
-
142
- # Write the file content
143
- with open(local_file_path, "wb") as f:
144
- f.write(decoded_content)
145
-
146
- print(" [MCP] ✅ Repository download complete.")
147
- return temp_dir
148
-
149
- def __enter__(self):
150
- self._start_server()
151
- return self
152
-
153
- def __exit__(self, exc_type, exc_val, exc_tb):
154
- self._stop_server()
1
+ # src/xenfra/mcp_client.py
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+
11
+ class MCPClient:
12
+ """
13
+ A client for communicating with a local github-mcp-server process.
14
+
15
+ This client starts the MCP server as a subprocess and interacts with it
16
+ over stdin and stdout to download a full repository to a temporary directory.
17
+ """
18
+
19
+ def __init__(self, mcp_server_path="github-mcp-server"):
20
+ """
21
+ Initializes the MCPClient.
22
+
23
+ Args:
24
+ mcp_server_path (str): The path to the github-mcp-server executable.
25
+ Assumes it's in the system's PATH by default.
26
+ """
27
+ self.mcp_server_path = mcp_server_path
28
+ self.process = None
29
+
30
+ def _start_server(self):
31
+ """Starts the github-mcp-server subprocess."""
32
+ if self.process and self.process.poll() is None:
33
+ return
34
+ try:
35
+ self.process = subprocess.Popen(
36
+ [self.mcp_server_path],
37
+ stdin=subprocess.PIPE,
38
+ stdout=subprocess.PIPE,
39
+ stderr=subprocess.PIPE,
40
+ text=True,
41
+ env=os.environ,
42
+ )
43
+ except FileNotFoundError:
44
+ raise RuntimeError(
45
+ f"'{self.mcp_server_path}' not found. Ensure github-mcp-server is installed and in your PATH."
46
+ )
47
+ except Exception as e:
48
+ raise RuntimeError(f"Failed to start MCP server: {e}")
49
+
50
+ def _stop_server(self):
51
+ """Stops the MCP server process."""
52
+ if self.process:
53
+ self.process.terminate()
54
+ self.process.wait()
55
+ self.process = None
56
+
57
+ def _send_request(self, method: str, params: dict) -> dict:
58
+ """Sends a JSON-RPC request and returns the response."""
59
+ if not self.process or self.process.poll() is not None:
60
+ self._start_server()
61
+
62
+ request = {"jsonrpc": "2.0", "id": os.urandom(4).hex(), "method": method, "params": params}
63
+
64
+ try:
65
+ self.process.stdin.write(json.dumps(request) + "\n")
66
+ self.process.stdin.flush()
67
+ response_line = self.process.stdout.readline()
68
+ if not response_line:
69
+ error_output = self.process.stderr.read()
70
+ raise RuntimeError(f"MCP server closed stream unexpectedly. Error: {error_output}")
71
+
72
+ response = json.loads(response_line)
73
+ if "error" in response:
74
+ raise RuntimeError(f"MCP server returned an error: {response['error']}")
75
+ return response.get("result", {})
76
+ except (BrokenPipeError, json.JSONDecodeError) as e:
77
+ raise RuntimeError(f"Failed to communicate with MCP server: {e}")
78
+
79
+ def download_repo_to_tempdir(self, repo_url: str, commit_sha: str = "HEAD") -> str:
80
+ """
81
+ Downloads an entire repository at a specific commit to a local temporary directory.
82
+
83
+ Args:
84
+ repo_url (str): The full URL of the GitHub repository.
85
+ commit_sha (str): The commit SHA to download. Defaults to "HEAD".
86
+
87
+ Returns:
88
+ The path to the temporary directory containing the downloaded code.
89
+ """
90
+ try:
91
+ parts = repo_url.strip("/").split("/")
92
+ owner = parts[-2]
93
+ repo_name = parts[-1].replace(".git", "")
94
+ except IndexError:
95
+ raise ValueError(
96
+ "Invalid repository URL format. Expected format: https://github.com/owner/repo"
97
+ )
98
+
99
+ print(f" [MCP] Fetching file tree for {owner}/{repo_name} at {commit_sha}...")
100
+ tree_result = self._send_request(
101
+ method="git.get_repository_tree",
102
+ params={"owner": owner, "repo": repo_name, "tree_sha": commit_sha, "recursive": True},
103
+ )
104
+
105
+ tree = tree_result.get("tree", [])
106
+ if not tree:
107
+ raise RuntimeError("Could not retrieve repository file tree.")
108
+
109
+ temp_dir = tempfile.mkdtemp(prefix=f"xenfra_{repo_name}_")
110
+ print(f" [MCP] Downloading to temporary directory: {temp_dir}")
111
+
112
+ for item in tree:
113
+ item_path = item.get("path")
114
+ item_type = item.get("type")
115
+
116
+ if not item_path or item_type != "blob": # Only handle files
117
+ continue
118
+
119
+ # For downloading content, we can use the commit_sha in the 'ref' parameter
120
+ # to ensure we get the content from the correct version.
121
+ content_result = self._send_request(
122
+ method="repos.get_file_contents",
123
+ params={"owner": owner, "repo": repo_name, "path": item_path, "ref": commit_sha},
124
+ )
125
+
126
+ content_b64 = content_result.get("content")
127
+ if content_b64 is None:
128
+ print(f" [MCP] [Warning] Could not get content for {item_path}")
129
+ continue
130
+
131
+ try:
132
+ # Content is base64 encoded, with newlines.
133
+ decoded_content = base64.b64decode(content_b64.replace("\n", ""))
134
+ except (base64.binascii.Error, TypeError):
135
+ print(f" [MCP] [Warning] Could not decode content for {item_path}")
136
+ continue
137
+
138
+ # Create file and parent directories in the temp location
139
+ local_file_path = Path(temp_dir) / item_path
140
+ local_file_path.parent.mkdir(parents=True, exist_ok=True)
141
+
142
+ # Write the file content
143
+ with open(local_file_path, "wb") as f:
144
+ f.write(decoded_content)
145
+
146
+ print(" [MCP] ✅ Repository download complete.")
147
+ return temp_dir
148
+
149
+ def __enter__(self):
150
+ self._start_server()
151
+ return self
152
+
153
+ def __exit__(self, exc_type, exc_val, exc_tb):
154
+ self._stop_server()