mcp-server-code-assist 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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