mcp-server-code-assist 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.
@@ -0,0 +1,24 @@
1
+ import click
2
+ from pathlib import Path
3
+ import logging
4
+ import sys
5
+ from .server import serve
6
+
7
+ @click.command()
8
+ @click.option("--working-dir", "-w", type=Path, help="Working directory path")
9
+ @click.option("-v", "--verbose", count=True)
10
+ def main(working_dir: Path | None, verbose: bool) -> None:
11
+ """MCP Code Assist Server - Code operations for MCP"""
12
+ import asyncio
13
+
14
+ logging_level = logging.WARN
15
+ if verbose == 1:
16
+ logging_level = logging.INFO
17
+ elif verbose >= 2:
18
+ logging_level = logging.DEBUG
19
+
20
+ logging.basicConfig(level=logging_level, stream=sys.stderr)
21
+ asyncio.run(serve(working_dir))
22
+
23
+ if __name__ == "__main__":
24
+ main()
@@ -0,0 +1,3 @@
1
+ from mcp_server_code_assist import main
2
+
3
+ main()
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3
+ <xs:element name="instruction">
4
+ <xs:complexType>
5
+ <xs:sequence>
6
+ <xs:element name="function">
7
+ <xs:simpleType>
8
+ <xs:restriction base="xs:string">
9
+ <xs:enumeration value="create"/>
10
+ <xs:enumeration value="delete"/>
11
+ <xs:enumeration value="modify"/>
12
+ <xs:enumeration value="rewrite"/>
13
+ </xs:restriction>
14
+ </xs:simpleType>
15
+ </xs:element>
16
+ <xs:element name="path" type="xs:string"/>
17
+ <xs:element name="content" type="xs:string" minOccurs="0"/>
18
+ <xs:element name="replacements" minOccurs="0">
19
+ <xs:complexType>
20
+ <xs:sequence>
21
+ <xs:any processContents="skip" minOccurs="0" maxOccurs="unbounded"/>
22
+ </xs:sequence>
23
+ </xs:complexType>
24
+ </xs:element>
25
+ </xs:sequence>
26
+ </xs:complexType>
27
+ </xs:element>
28
+ </xs:schema>
@@ -0,0 +1,240 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Any
5
+ from enum import Enum
6
+ from mcp.server import Server
7
+ from mcp.server.session import ServerSession
8
+ from mcp.server.stdio import stdio_server
9
+ from mcp.types import ClientCapabilities, TextContent, Tool, ListRootsResult, RootsCapability
10
+ import git
11
+ from mcp_server_code_assist.tools.models import (
12
+ FileCreate, FileDelete, FileModify, FileRewrite,
13
+ GitBase, GitAdd, GitCommit, GitDiff, GitCreateBranch,
14
+ GitCheckout, GitShow, GitLog
15
+ )
16
+ from mcp_server_code_assist.tools.file_tools import FileTools
17
+ from mcp_server_code_assist.tools.git_functions import git_status, git_diff_unstaged, git_diff_staged, git_diff, git_log, git_show
18
+
19
+ class CodeAssistTools(str, Enum):
20
+ FILE_CREATE = "file_create"
21
+ FILE_DELETE = "file_delete"
22
+ FILE_MODIFY = "file_modify"
23
+ FILE_REWRITE = "file_rewrite"
24
+ GIT_STATUS = "git_status"
25
+ GIT_DIFF_UNSTAGED = "git_diff_unstaged"
26
+ GIT_DIFF_STAGED = "git_diff_staged"
27
+ GIT_DIFF = "git_diff"
28
+ GIT_COMMIT = "git_commit"
29
+ GIT_ADD = "git_add"
30
+ GIT_RESET = "git_reset"
31
+ GIT_LOG = "git_log"
32
+ GIT_CREATE_BRANCH = "git_create_branch"
33
+ GIT_CHECKOUT = "git_checkout"
34
+ GIT_SHOW = "git_show"
35
+
36
+ async def process_instruction(instruction: dict[str, Any], repo_path: Path) -> dict[str, Any]:
37
+ FileTools.init_allowed_paths([str(repo_path)])
38
+ try:
39
+ match instruction['type']:
40
+ case 'read_file':
41
+ path = str(repo_path / instruction['path'])
42
+ content = await FileTools.read_file(path)
43
+ return {'content': content}
44
+
45
+ case 'read_multiple':
46
+ paths = [str(repo_path / p) for p in instruction['paths']]
47
+ contents = await FileTools.read_multiple_files(paths)
48
+ contents_dict = {os.path.basename(k): v for k,v in contents.items()}
49
+ return {'contents': contents_dict}
50
+
51
+ case 'create_file':
52
+ path = str(repo_path / instruction['path'])
53
+ result = await FileTools.create_file(path, instruction.get('content', ''))
54
+ return {'result': result}
55
+
56
+ case 'modify_file':
57
+ path = str(repo_path / instruction['path'])
58
+ result = await FileTools.modify_file(path, instruction['replacements'])
59
+ return {'result': result}
60
+
61
+ case 'rewrite_file':
62
+ path = str(repo_path / instruction['path'])
63
+ result = await FileTools.rewrite_file(path, instruction['content'])
64
+ return {'result': result}
65
+
66
+ case 'delete_file':
67
+ path = str(repo_path / instruction['path'])
68
+ result = await FileTools.delete_file(path)
69
+ return {'result': result}
70
+
71
+ case _:
72
+ return {'error': 'Invalid instruction type'}
73
+
74
+ except Exception as e:
75
+ return {'error': str(e)}
76
+
77
+ async def serve(working_dir: Path | None) -> None:
78
+ logger = logging.getLogger(__name__)
79
+ server = Server("mcp-code-assist")
80
+
81
+ @server.list_tools()
82
+ async def list_tools() -> list[Tool]:
83
+ return [
84
+ Tool(
85
+ name=CodeAssistTools.FILE_CREATE,
86
+ description="Creates a new file with content",
87
+ inputSchema=FileCreate.schema(),
88
+ ),
89
+ Tool(
90
+ name=CodeAssistTools.FILE_DELETE,
91
+ description="Deletes a file",
92
+ inputSchema=FileDelete.schema(),
93
+ ),
94
+ Tool(
95
+ name=CodeAssistTools.FILE_MODIFY,
96
+ description="Modifies file content using search/replace",
97
+ inputSchema=FileModify.schema(),
98
+ ),
99
+ Tool(
100
+ name=CodeAssistTools.FILE_REWRITE,
101
+ description="Rewrites entire file content",
102
+ inputSchema=FileRewrite.schema(),
103
+ ),
104
+ Tool(
105
+ name=CodeAssistTools.GIT_STATUS,
106
+ description="Shows the working tree status",
107
+ inputSchema=GitBase.schema(),
108
+ ),
109
+ Tool(
110
+ name=CodeAssistTools.GIT_DIFF_UNSTAGED,
111
+ description="Shows changes in the working directory that are not yet staged",
112
+ inputSchema=GitBase.schema(),
113
+ ),
114
+ Tool(
115
+ name=CodeAssistTools.GIT_DIFF_STAGED,
116
+ description="Shows changes that are staged for commit",
117
+ inputSchema=GitBase.schema(),
118
+ ),
119
+ Tool(
120
+ name=CodeAssistTools.GIT_DIFF,
121
+ description="Shows differences between branches or commits",
122
+ inputSchema=GitDiff.schema(),
123
+ ),
124
+ Tool(
125
+ name=CodeAssistTools.GIT_COMMIT,
126
+ description="Records changes to the repository",
127
+ inputSchema=GitCommit.schema(),
128
+ ),
129
+ Tool(
130
+ name=CodeAssistTools.GIT_ADD,
131
+ description="Adds file contents to the staging area",
132
+ inputSchema=GitAdd.schema(),
133
+ ),
134
+ Tool(
135
+ name=CodeAssistTools.GIT_RESET,
136
+ description="Unstages all staged changes",
137
+ inputSchema=GitBase.schema(),
138
+ ),
139
+ Tool(
140
+ name=CodeAssistTools.GIT_LOG,
141
+ description="Shows the commit logs",
142
+ inputSchema=GitLog.schema(),
143
+ ),
144
+ Tool(
145
+ name=CodeAssistTools.GIT_CREATE_BRANCH,
146
+ description="Creates a new branch from an optional base branch",
147
+ inputSchema=GitCreateBranch.schema(),
148
+ ),
149
+ Tool(
150
+ name=CodeAssistTools.GIT_CHECKOUT,
151
+ description="Switches branches",
152
+ inputSchema=GitCheckout.schema(),
153
+ ),
154
+ Tool(
155
+ name=CodeAssistTools.GIT_SHOW,
156
+ description="Shows the contents of a commit",
157
+ inputSchema=GitShow.schema(),
158
+ ),
159
+ ]
160
+
161
+ @server.call_tool()
162
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
163
+ match name:
164
+ case CodeAssistTools.FILE_CREATE:
165
+ result = await FileTools.create_file(arguments["path"], arguments.get("content", ""))
166
+ return [TextContent(type="text", text=result)]
167
+
168
+ case CodeAssistTools.FILE_DELETE:
169
+ result = await FileTools.delete_file(arguments["path"])
170
+ return [TextContent(type="text", text=result)]
171
+
172
+ case CodeAssistTools.FILE_MODIFY:
173
+ result = await FileTools.modify_file(arguments["path"], arguments["replacements"])
174
+ return [TextContent(type="text", text=result)]
175
+
176
+ case CodeAssistTools.FILE_REWRITE:
177
+ result = await FileTools.rewrite_file(arguments["path"], arguments["content"])
178
+ return [TextContent(type="text", text=result)]
179
+
180
+ case CodeAssistTools.GIT_STATUS:
181
+ repo = git.Repo(arguments["repo_path"])
182
+ status = git_status(repo)
183
+ return [TextContent(type="text", text=f"Repository status:\n{status}")]
184
+
185
+ case CodeAssistTools.GIT_DIFF_UNSTAGED:
186
+ repo = git.Repo(arguments["repo_path"])
187
+ diff = git_diff_unstaged(repo)
188
+ return [TextContent(type="text", text=f"Unstaged changes:\n{diff}")]
189
+
190
+ case CodeAssistTools.GIT_DIFF_STAGED:
191
+ repo = git.Repo(arguments["repo_path"])
192
+ diff = git_diff_staged(repo)
193
+ return [TextContent(type="text", text=f"Staged changes:\n{diff}")]
194
+
195
+ case CodeAssistTools.GIT_DIFF:
196
+ repo = git.Repo(arguments["repo_path"])
197
+ diff = git_diff(repo, arguments["target"])
198
+ return [TextContent(type="text", text=f"Diff with {arguments['target']}:\n{diff}")]
199
+
200
+ case CodeAssistTools.GIT_COMMIT:
201
+ repo = git.Repo(arguments["repo_path"])
202
+ result = git_commit(repo, arguments["message"])
203
+ return [TextContent(type="text", text=result)]
204
+
205
+ case CodeAssistTools.GIT_ADD:
206
+ repo = git.Repo(arguments["repo_path"])
207
+ result = git_add(repo, arguments["files"])
208
+ return [TextContent(type="text", text=result)]
209
+
210
+ case CodeAssistTools.GIT_RESET:
211
+ repo = git.Repo(arguments["repo_path"])
212
+ result = git_reset(repo)
213
+ return [TextContent(type="text", text=result)]
214
+
215
+ case CodeAssistTools.GIT_LOG:
216
+ repo = git.Repo(arguments["repo_path"])
217
+ log = git_log(repo, arguments.get("max_count", 10))
218
+ return [TextContent(type="text", text="Commit history:\n" + "\n".join(log))]
219
+
220
+ case CodeAssistTools.GIT_CREATE_BRANCH:
221
+ repo = git.Repo(arguments["repo_path"])
222
+ result = git_create_branch(repo, arguments["branch_name"], arguments.get("base_branch"))
223
+ return [TextContent(type="text", text=result)]
224
+
225
+ case CodeAssistTools.GIT_CHECKOUT:
226
+ repo = git.Repo(arguments["repo_path"])
227
+ result = git_checkout(repo, arguments["branch_name"])
228
+ return [TextContent(type="text", text=result)]
229
+
230
+ case CodeAssistTools.GIT_SHOW:
231
+ repo = git.Repo(arguments["repo_path"])
232
+ result = git_show(repo, arguments["revision"])
233
+ return [TextContent(type="text", text=result)]
234
+
235
+ case _:
236
+ raise ValueError(f"Unknown tool: {name}")
237
+
238
+ options = server.create_initialization_options()
239
+ async with stdio_server() as (read_stream, write_stream):
240
+ await server.run(read_stream, write_stream, options, raise_exceptions=True)
File without changes
@@ -0,0 +1,220 @@
1
+ from pathlib import Path
2
+ import os
3
+ import shutil
4
+ import difflib
5
+ import fnmatch
6
+ import json
7
+ from typing import Union, Dict, Tuple, Optional, Set
8
+ import git
9
+
10
+ class FileTools:
11
+ _allowed_paths: list[str] = []
12
+
13
+ @classmethod
14
+ def init_allowed_paths(cls, paths: list[str]):
15
+ cls._allowed_paths = [os.path.abspath(p) for p in paths]
16
+
17
+ @classmethod
18
+ async def validate_path(cls, path: str) -> str:
19
+ abs_path = os.path.abspath(path)
20
+ if not any(abs_path.startswith(p) for p in cls._allowed_paths):
21
+ raise ValueError(f"Path {path} is outside allowed directories")
22
+ return abs_path
23
+
24
+ @classmethod
25
+ async def read_file(cls, path: str) -> str:
26
+ path = await cls.validate_path(path)
27
+ return Path(path).read_text()
28
+
29
+ @classmethod
30
+ async def write_file(cls, path: str, content: str) -> None:
31
+ path = await cls.validate_path(path)
32
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
33
+ Path(path).write_text(content)
34
+
35
+ @classmethod
36
+ async def read_multiple_files(cls, paths: list[str]) -> Dict[str, str]:
37
+ result = {}
38
+ for path in paths:
39
+ try:
40
+ content = await cls.read_file(path)
41
+ result[path] = content
42
+ except Exception as e:
43
+ result[path] = str(e)
44
+ return result
45
+
46
+ @classmethod
47
+ async def create_file(cls, path: str, content: str = "") -> str:
48
+ await cls.write_file(path, content)
49
+ return f"Created file: {path}"
50
+
51
+ @classmethod
52
+ async def delete_file(cls, path: str) -> str:
53
+ path = await cls.validate_path(path)
54
+ path_obj = Path(path)
55
+ if path_obj.is_file():
56
+ path_obj.unlink()
57
+ return f"Deleted file: {path}"
58
+ elif path_obj.is_dir():
59
+ shutil.rmtree(path)
60
+ return f"Deleted directory: {path}"
61
+ return f"Path not found: {path}"
62
+
63
+ @classmethod
64
+ async def modify_file(cls, path: str, replacements: Dict[str, str]) -> str:
65
+ path = await cls.validate_path(path)
66
+ content = await cls.read_file(path)
67
+ original = content
68
+
69
+ for old, new in replacements.items():
70
+ content = content.replace(old, new)
71
+
72
+ await cls.write_file(path, content)
73
+ return cls.generate_diff(original, content)
74
+
75
+ @classmethod
76
+ async def rewrite_file(cls, path: str, content: str) -> str:
77
+ path = await cls.validate_path(path)
78
+ original = await cls.read_file(path) if Path(path).exists() else ""
79
+ await cls.write_file(path, content)
80
+ return cls.generate_diff(original, content)
81
+
82
+ @staticmethod
83
+ def generate_diff(original: str, modified: str) -> str:
84
+ diff = difflib.unified_diff(
85
+ original.splitlines(keepends=True),
86
+ modified.splitlines(keepends=True),
87
+ fromfile='original',
88
+ tofile='modified'
89
+ )
90
+ return ''.join(diff)
91
+
92
+ @classmethod
93
+ async def list_directory(cls, path: str) -> list[str]:
94
+ path = await cls.validate_path(path)
95
+ entries = []
96
+ for item in Path(path).iterdir():
97
+ entries.append(str(item))
98
+ return entries
99
+
100
+ @classmethod
101
+ async def create_directory(cls, path: str) -> None:
102
+ path = await cls.validate_path(path)
103
+ Path(path).mkdir(parents=True, exist_ok=True)
104
+
105
+ @classmethod
106
+ def _load_gitignore(cls, path: str) -> list[str]:
107
+ gitignore_path = os.path.join(path, ".gitignore")
108
+ patterns = []
109
+ if os.path.exists(gitignore_path):
110
+ with open(gitignore_path, 'r') as f:
111
+ for line in f:
112
+ line = line.strip()
113
+ if line and not line.startswith('#'):
114
+ patterns.append(line)
115
+ return patterns
116
+
117
+ @classmethod
118
+ def _get_tracked_files(cls, repo_path: str) -> Optional[Set[str]]:
119
+ try:
120
+ repo = git.Repo(repo_path)
121
+ return set(repo.git.ls_files().splitlines())
122
+ except git.exc.InvalidGitRepositoryError:
123
+ return None
124
+
125
+ @classmethod
126
+ async def directory_tree(cls, path: str) -> Tuple[str, int, int]:
127
+ path = await cls.validate_path(path)
128
+ base_path = Path(path)
129
+
130
+ # Try git tracking first
131
+ tracked_files = cls._get_tracked_files(path)
132
+ gitignore = cls._load_gitignore(path) if tracked_files is None else []
133
+
134
+ def gen_tree(path: Path, prefix: str = "") -> Tuple[list[str], int, int]:
135
+ entries = []
136
+ dir_count = 0
137
+ file_count = 0
138
+
139
+ items = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name))
140
+ for i, item in enumerate(items):
141
+ rel_path = str(item.relative_to(base_path))
142
+
143
+ # Skip if file should be ignored
144
+ if tracked_files is not None:
145
+ if rel_path not in tracked_files and not any(str(p.relative_to(base_path)) in tracked_files for p in item.rglob("*") if p.is_file()):
146
+ continue
147
+ else:
148
+ # Use gitignore
149
+ if cls._should_ignore(rel_path, gitignore):
150
+ continue
151
+
152
+ is_last = i == len(items) - 1
153
+ curr_prefix = "└── " if is_last else "├── "
154
+ curr_line = prefix + curr_prefix + item.name
155
+
156
+ if item.is_dir():
157
+ next_prefix = prefix + (" " if is_last else "│ ")
158
+ subtree, sub_dirs, sub_files = gen_tree(item, next_prefix)
159
+ if tracked_files is not None and not subtree:
160
+ continue
161
+ entries.extend([curr_line] + subtree)
162
+ dir_count += 1 + sub_dirs
163
+ file_count += sub_files
164
+ else:
165
+ if tracked_files is not None and rel_path not in tracked_files:
166
+ continue
167
+ entries.append(curr_line)
168
+ file_count += 1
169
+
170
+ return entries, dir_count, file_count
171
+
172
+ tree_lines, total_dirs, total_files = gen_tree(Path(path))
173
+ return "\n".join(tree_lines), total_dirs, total_files
174
+
175
+ @classmethod
176
+ def _should_ignore(cls, path: str, patterns: list[str]) -> bool:
177
+ if not patterns:
178
+ return False
179
+
180
+ parts = Path(path).parts
181
+ for pattern in patterns:
182
+ pattern = pattern.strip()
183
+ if not pattern or pattern.startswith('#'):
184
+ continue
185
+
186
+ if pattern.endswith('/'):
187
+ pattern = pattern.rstrip('/')
188
+ if pattern in parts:
189
+ return True
190
+ else:
191
+ if fnmatch.fnmatch(parts[-1], pattern): # Match basename
192
+ return True
193
+ # Match full path
194
+ if fnmatch.fnmatch(path, pattern):
195
+ return True
196
+
197
+ return False
198
+
199
+ @classmethod
200
+ async def search_files(cls, path: str, pattern: str, excludes: Optional[list[str]] = None) -> list[str]:
201
+ path = await cls.validate_path(path)
202
+ gitignore = cls._load_gitignore(path)
203
+ if excludes:
204
+ gitignore.extend(excludes)
205
+
206
+ results = []
207
+ for root, _, files in os.walk(path):
208
+ rel_root = os.path.relpath(root, path)
209
+ if rel_root != "." and cls._should_ignore(rel_root, gitignore):
210
+ continue
211
+
212
+ for file in files:
213
+ rel_path = os.path.join(rel_root, file)
214
+ if rel_path != "." and cls._should_ignore(rel_path, gitignore):
215
+ continue
216
+
217
+ if pattern in file or fnmatch.fnmatch(file, pattern):
218
+ results.append(os.path.join(root, file))
219
+
220
+ return results
@@ -0,0 +1,25 @@
1
+ import git
2
+
3
+ def git_status(repo: git.Repo) -> str:
4
+ return repo.git.status()
5
+
6
+ def git_diff_unstaged(repo: git.Repo) -> str:
7
+ return repo.git.diff()
8
+
9
+ def git_diff_staged(repo: git.Repo) -> str:
10
+ return repo.git.diff("--cached")
11
+
12
+ def git_diff(repo: git.Repo, target: str) -> str:
13
+ return repo.git.diff(target)
14
+
15
+ def git_log(repo: git.Repo, max_count: int = 10) -> str:
16
+ commits = list(repo.iter_commits(max_count=max_count))
17
+ log = []
18
+ for commit in commits:
19
+ log.append(f"Commit: {commit.hexsha}\nAuthor: {commit.author}\nDate: {commit.authored_datetime}\nMessage: {commit.message}\n")
20
+ return "\n".join(log)
21
+
22
+ def git_show(repo: git.Repo, revision: str) -> str:
23
+ commit = repo.commit(revision)
24
+ output = [f"Commit: {commit.hexsha}\nAuthor: {commit.author}\nDate: {commit.authored_datetime}\nMessage: {commit.message}\n"]
25
+ return "".join(output)
@@ -0,0 +1,7 @@
1
+ """Function invocation helper."""
2
+ from typing import Dict, Any
3
+
4
+ def invoke_git(function: str, params: Dict[str, Any]) -> str:
5
+ """Invokes a git function with given parameters."""
6
+ # Functions will be invoked via the assistant
7
+ return f"git_{function}({', '.join(f'{k}={v!r}' for k,v in params.items())})"
@@ -0,0 +1,48 @@
1
+ from typing import Union, Optional
2
+ from pydantic import BaseModel
3
+ from pathlib import Path
4
+
5
+ class FileCreate(BaseModel):
6
+ path: Union[str, Path]
7
+ content: str = ""
8
+
9
+ class FileDelete(BaseModel):
10
+ path: Union[str, Path]
11
+
12
+ class FileModify(BaseModel):
13
+ path: Union[str, Path]
14
+ replacements: dict[str, str]
15
+
16
+ class FileRewrite(BaseModel):
17
+ path: Union[str, Path]
18
+ content: str
19
+
20
+ class GitBase(BaseModel):
21
+ repo_path: str
22
+
23
+ class GitAdd(GitBase):
24
+ files: list[str]
25
+
26
+ class GitCommit(GitBase):
27
+ message: str
28
+
29
+ class GitDiff(GitBase):
30
+ target: str
31
+
32
+ class GitCreateBranch(GitBase):
33
+ branch_name: str
34
+ base_branch: Optional[str] = None
35
+
36
+ class GitCheckout(GitBase):
37
+ branch_name: str
38
+
39
+ class GitShow(GitBase):
40
+ revision: str
41
+
42
+ class GitLog(GitBase):
43
+ max_count: int = 10
44
+
45
+ class RepositoryOperation(BaseModel):
46
+ path: str
47
+ content: Optional[str] = None
48
+ replacements: Optional[dict[str, str]] = None
@@ -0,0 +1,76 @@
1
+ from pathlib import Path
2
+ from git import Repo
3
+ import aiofiles
4
+ from typing import Optional
5
+
6
+ class RepositoryTools:
7
+ def __init__(self, repo_path: str):
8
+ self.path = Path(repo_path).resolve()
9
+ if not self.path.exists():
10
+ raise ValueError(f"Invalid repository path: {repo_path}")
11
+ self.repo = Repo(self.path)
12
+
13
+ async def read_file(self, path: str) -> str:
14
+ file_path = self.path / path
15
+ async with aiofiles.open(file_path, 'r') as f:
16
+ return await f.read()
17
+
18
+ async def read_multiple(self, paths: list[str]) -> dict[str, str]:
19
+ contents = {}
20
+ for path in paths:
21
+ contents[path] = await self.read_file(path)
22
+ return contents
23
+
24
+ async def create_file(self, path: str, content: str = '') -> None:
25
+ file_path = self.path / path
26
+ file_path.parent.mkdir(parents=True, exist_ok=True)
27
+ async with aiofiles.open(file_path, 'w') as f:
28
+ await f.write(content)
29
+
30
+ async def modify_file(self, path: str, replacements: dict[str, str]) -> None:
31
+ file_path = self.path / path
32
+ content = await self.read_file(path)
33
+ for old, new in replacements.items():
34
+ content = content.replace(old, new)
35
+ await self.rewrite_file(path, content)
36
+
37
+ async def rewrite_file(self, path: str, content: str) -> None:
38
+ file_path = self.path / path
39
+ async with aiofiles.open(file_path, 'w') as f:
40
+ await f.write(content)
41
+
42
+ async def delete_file(self, path: str) -> None:
43
+ file_path = self.path / path
44
+ file_path.unlink()
45
+
46
+ def status(self) -> str:
47
+ return self.repo.git.status()
48
+
49
+ def add(self, files: list[str]) -> str:
50
+ return self.repo.git.add(files)
51
+
52
+ def commit(self, message: str) -> str:
53
+ return self.repo.git.commit('-m', message)
54
+
55
+ def diff_unstaged(self) -> str:
56
+ return self.repo.git.diff()
57
+
58
+ def diff_staged(self) -> str:
59
+ return self.repo.git.diff('--cached')
60
+
61
+ def diff(self, target: str) -> str:
62
+ return self.repo.git.diff(target)
63
+
64
+ def checkout(self, branch_name: str) -> str:
65
+ return self.repo.git.checkout(branch_name)
66
+
67
+ def create_branch(self, branch_name: str, base_branch: Optional[str] = None) -> str:
68
+ if base_branch:
69
+ return self.repo.git.branch(branch_name, base_branch)
70
+ return self.repo.git.branch(branch_name)
71
+
72
+ def log(self, max_count: int = 10) -> str:
73
+ return self.repo.git.log(f'-n {max_count}')
74
+
75
+ def show(self, revision: str) -> str:
76
+ return self.repo.git.show(revision)
@@ -0,0 +1,48 @@
1
+ import xml.etree.ElementTree as ET
2
+ from pathlib import Path
3
+ import xmlschema
4
+ import re
5
+
6
+ class XMLProcessor:
7
+ def __init__(self):
8
+ schema_path = Path(__file__).parent / "schema.xsd"
9
+ self.validator = xmlschema.XMLSchema(schema_path)
10
+
11
+ def _normalize_text(self, text: str) -> str:
12
+ """Normalize whitespace in text content"""
13
+ return re.sub(r'\s+', ' ', text).strip()
14
+
15
+ def parse(self, xml_str: str) -> dict[str, str | dict[str, str]]:
16
+ root = ET.fromstring(self._normalize_text(xml_str))
17
+ self.validator.validate(root)
18
+
19
+ result = {
20
+ "function": self._normalize_text(root.find("function").text),
21
+ "path": self._normalize_text(root.find("path").text),
22
+ }
23
+
24
+ if (content_elem := root.find("content")) is not None:
25
+ result["content"] = self._normalize_text(content_elem.text)
26
+
27
+ if (replacements_elem := root.find("replacements")) is not None:
28
+ result["replacements"] = {
29
+ elem.tag: self._normalize_text(elem.text)
30
+ for elem in replacements_elem
31
+ }
32
+
33
+ return result
34
+
35
+ def generate(self, data: dict[str, str | dict[str, str]]) -> str:
36
+ root = ET.Element("instruction")
37
+ ET.SubElement(root, "function").text = data["function"]
38
+ ET.SubElement(root, "path").text = data["path"]
39
+
40
+ if "content" in data:
41
+ ET.SubElement(root, "content").text = data["content"]
42
+
43
+ if "replacements" in data:
44
+ replacements = ET.SubElement(root, "replacements")
45
+ for key, value in data["replacements"].items():
46
+ ET.SubElement(replacements, key).text = value
47
+
48
+ return f'<?xml version="1.0"?>\n{ET.tostring(root, encoding="unicode")}'
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-server-code-assist
3
+ Version: 0.1.0
4
+ Summary: MCP Code Assist Server
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: aiofiles>=24.0.0
7
+ Requires-Dist: gitpython>=3.1.40
8
+ Requires-Dist: pydantic>=2.0.0
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest-asyncio>=0.25.0; extra == 'test'
11
+ Requires-Dist: pytest>=8.0.0; extra == 'test'
@@ -0,0 +1,14 @@
1
+ mcp_server_code_assist/__init__.py,sha256=Aai1h9YUGDDWQn9rsHq5gCZXO7QCQs2pJtyGC9mYV0o,661
2
+ mcp_server_code_assist/__main__.py,sha256=lbLwix6a0gDF7YKoqbXF88oTdmr8KQGKWchLKEIn5SU,47
3
+ mcp_server_code_assist/schema.xsd,sha256=vtfQQDzCHvbILT78zF_53N-68GrJRbELg1Xgq3Uu9DE,1235
4
+ mcp_server_code_assist/server.py,sha256=qtz9pD3_JxIJGare5Sal25E8DafZY-DX4thrghulT1s,10012
5
+ mcp_server_code_assist/xml_parser.py,sha256=FMmcAk7PbnTNidBGUynq6ZbUs1O9OHHI298REAQEMqw,1827
6
+ mcp_server_code_assist/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ mcp_server_code_assist/tools/file_tools.py,sha256=iGdVGP9phD5wMZ5HxbRv3gbUpNx-QF27xDLrUI9wUkU,7978
8
+ mcp_server_code_assist/tools/git_functions.py,sha256=iGIhi8i7BxIrAL8JytWjfcZP7xgla0uVweazSKUNtB4,882
9
+ mcp_server_code_assist/tools/invoke.py,sha256=lYIu5bJYBaKDDVYz6HRqodpLBMnfVpcvgAref10LO2s,313
10
+ mcp_server_code_assist/tools/models.py,sha256=3ZUfzodSVbwSoIKG5oKiLbnjVeptT5QKLaQSwl7McJY,937
11
+ mcp_server_code_assist/tools/repository_tools.py,sha256=5OG4ecmqXVEeuX9U5jdegbQ2So4lxKkIjG3M3e5rgFI,2700
12
+ mcp_server_code_assist-0.1.0.dist-info/METADATA,sha256=JuQfHEwqOIt1x3KF7vY1hsGpmpoXWnGrTndqb-5L4a0,340
13
+ mcp_server_code_assist-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ mcp_server_code_assist-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any