mcp-server-code-assist 0.1.2__tar.gz → 0.1.3__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/PKG-INFO +2 -1
  2. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/pyproject.toml +3 -2
  3. mcp_server_code_assist-0.1.3/res/cmds.md +27 -0
  4. mcp_server_code_assist-0.1.3/res/mcp-cli.md +45 -0
  5. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3/res}/plan.md +20 -13
  6. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/server.py +27 -2
  7. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/tools/file_tools.py +44 -5
  8. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/tools/models.py +8 -0
  9. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/tests/test_file_operations.py +31 -8
  10. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/uv.lock +24 -1
  11. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/.gitignore +0 -0
  12. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/Dockerfile +0 -0
  13. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/License +0 -0
  14. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/README.md +0 -0
  15. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3/res}/calude_prompt.txt +0 -0
  16. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/__init__.py +0 -0
  17. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/__main__.py +0 -0
  18. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/schema.xsd +0 -0
  19. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/tools/__init__.py +0 -0
  20. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/tools/git_functions.py +0 -0
  21. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/tools/invoke.py +0 -0
  22. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/tools/repository_tools.py +0 -0
  23. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/src/mcp_server_code_assist/xml_parser.py +0 -0
  24. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/tests/__init__.py +0 -0
  25. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/tests/test_repository_tools.py +0 -0
  26. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/tests/test_server.py +0 -0
  27. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/tests/test_tools.py +0 -0
  28. {mcp_server_code_assist-0.1.2 → mcp_server_code_assist-0.1.3}/tests/test_xml_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-server-code-assist
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: MCP Code Assist Server
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: aiofiles>=24.0.0
@@ -8,6 +8,7 @@ Requires-Dist: click>=8.1.7
8
8
  Requires-Dist: gitpython>=3.1.40
9
9
  Requires-Dist: mcp>=1.2.0
10
10
  Requires-Dist: pydantic>=2.0.0
11
+ Requires-Dist: xmlschema>=3.4.3
11
12
  Provides-Extra: test
12
13
  Requires-Dist: pytest-asyncio>=0.25.0; extra == 'test'
13
14
  Requires-Dist: pytest>=8.0.0; extra == 'test'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-server-code-assist"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "MCP Code Assist Server"
5
5
  requires-python = ">=3.11"
6
6
  dependencies = [
@@ -8,7 +8,8 @@ dependencies = [
8
8
  "gitpython>=3.1.40",
9
9
  "pydantic>=2.0.0",
10
10
  "click>=8.1.7",
11
- "mcp>=1.2.0"
11
+ "mcp>=1.2.0",
12
+ "xmlschema>=3.4.3"
12
13
  ]
13
14
 
14
15
  [project.scripts]
@@ -0,0 +1,27 @@
1
+
2
+ # Run locally
3
+
4
+
5
+
6
+ ```
7
+ source .venv/bin/activate
8
+ noglob uv pip install -e .[test]
9
+ uv run mcp-server-code-assist
10
+ ```
11
+
12
+
13
+ # Install uvx
14
+
15
+ ```
16
+ uvx mcp-server-code-assist
17
+ ```
18
+
19
+ # Build
20
+
21
+ ```
22
+ noglob uv pip install -e .[test]
23
+ uv sync
24
+ uv pip install build twine
25
+ python -m build
26
+ python -m twine upload dist/*
27
+ ```
@@ -0,0 +1,45 @@
1
+ # Quickstart
2
+
3
+ Let's create a simple MCP server that exposes a calculator tool and some data:
4
+
5
+ # server.py
6
+ ```python
7
+ from mcp.server.fastmcp import FastMCP
8
+ ```
9
+
10
+
11
+ # Create an MCP server
12
+ ```python
13
+ mcp = FastMCP("Demo")
14
+ ```
15
+
16
+
17
+ # Add an addition tool
18
+ ```python
19
+ @mcp.tool()
20
+ def add(a: int, b: int) -> int:
21
+ """Add two numbers"""
22
+ return a + b
23
+ ```
24
+
25
+
26
+ # Add a dynamic greeting resource
27
+ ```python
28
+ @mcp.resource("greeting://{name}")
29
+ def get_greeting(name: str) -> str:
30
+ """Get a personalized greeting"""
31
+ return f"Hello, {name}!"
32
+ ```
33
+
34
+
35
+ You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running:
36
+
37
+ ```bash
38
+ mcp install server.py
39
+ ```
40
+
41
+ Alternatively, you can test it with the MCP Inspector:
42
+
43
+ ```bash
44
+ mcp dev server.py
45
+ ```
@@ -16,15 +16,20 @@
16
16
  - [x] Implement parse/generate methods
17
17
  - [x] Add tests
18
18
 
19
- - [x] File Tools
20
- - [x] create function
21
- - [x] delete function
22
- - [x] modify with search/replace
23
- - [x] rewrite function
24
- - [x] diff generation
25
- - [x] directory tree with gitignore
26
- - [x] file search
27
- - [x] file operations tests
19
+ - [ ] File Tools
20
+ - [ ] Read Operations
21
+ - [x] read_file function
22
+ - [x] list_directory function
23
+ - [ ] file_tree function (with .gitignore support)
24
+ - [x] Write Operations
25
+ - [x] create function
26
+ - [x] delete function
27
+ - [x] modify with search/replace
28
+ - [x] rewrite function
29
+ - [x] diff generation
30
+ - [x] directory tree with gitignore
31
+ - [x] file search
32
+ - [x] file operations tests
28
33
 
29
34
  - [x] Git Operations
30
35
  - [x] Repository tools implementation
@@ -38,6 +43,7 @@
38
43
  - [ ] OpenRouter integration
39
44
  - [ ] Command line interface
40
45
  - [ ] XML instruction processing
46
+ - [ ] Read operations implementation
41
47
  - [x] Basic request handlers
42
48
  - [x] Error handling
43
49
  - [x] Request validation
@@ -68,10 +74,11 @@ Proposed Solutions:
68
74
  - Design instruction format to be resumable
69
75
 
70
76
  ## Current Task
71
- Phase 1: Read-only operations
72
- - File read operations complete
73
- - Git read operations complete
74
- - File write operations to be implemented directly
77
+ Phase 1: Complete Implementation
78
+ - File read operations (read_file, list_directory, file_tree)
79
+ - File write operations
80
+ - Git operations
81
+ - XML instruction processing
75
82
 
76
83
  Phase 2:
77
84
  - OpenRouter integration
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import json
3
4
  from pathlib import Path
4
5
  from typing import Any
5
6
  from enum import Enum
@@ -9,18 +10,20 @@ from mcp.server.stdio import stdio_server
9
10
  from mcp.types import ClientCapabilities, TextContent, Tool, ListRootsResult, RootsCapability
10
11
  import git
11
12
  from mcp_server_code_assist.tools.models import (
12
- FileCreate, FileDelete, FileModify, FileRewrite,
13
+ FileCreate, FileDelete, FileModify, FileRewrite, FileRead,
13
14
  GitBase, GitAdd, GitCommit, GitDiff, GitCreateBranch,
14
- GitCheckout, GitShow, GitLog
15
+ GitCheckout, GitShow, GitLog, ListDirectory
15
16
  )
16
17
  from mcp_server_code_assist.tools.file_tools import FileTools
17
18
  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
 
19
20
  class CodeAssistTools(str, Enum):
21
+ LIST_DIRECTORY = "list_directory"
20
22
  FILE_CREATE = "file_create"
21
23
  FILE_DELETE = "file_delete"
22
24
  FILE_MODIFY = "file_modify"
23
25
  FILE_REWRITE = "file_rewrite"
26
+ FILE_READ = "file_read"
24
27
  GIT_STATUS = "git_status"
25
28
  GIT_DIFF_UNSTAGED = "git_diff_unstaged"
26
29
  GIT_DIFF_STAGED = "git_diff_staged"
@@ -81,6 +84,11 @@ async def serve(working_dir: Path | None) -> None:
81
84
  @server.list_tools()
82
85
  async def list_tools() -> list[Tool]:
83
86
  return [
87
+ Tool(
88
+ name=CodeAssistTools.LIST_DIRECTORY,
89
+ description="Lists contents of a directory",
90
+ inputSchema=ListDirectory.schema(),
91
+ ),
84
92
  Tool(
85
93
  name=CodeAssistTools.FILE_CREATE,
86
94
  description="Creates a new file with content",
@@ -101,6 +109,11 @@ async def serve(working_dir: Path | None) -> None:
101
109
  description="Rewrites entire file content",
102
110
  inputSchema=FileRewrite.schema(),
103
111
  ),
112
+ Tool(
113
+ name=CodeAssistTools.FILE_READ,
114
+ description="Reads contents of a file",
115
+ inputSchema=FileRead.schema(),
116
+ ),
104
117
  Tool(
105
118
  name=CodeAssistTools.GIT_STATUS,
106
119
  description="Shows the working tree status",
@@ -161,6 +174,14 @@ async def serve(working_dir: Path | None) -> None:
161
174
  @server.call_tool()
162
175
  async def call_tool(name: str, arguments: dict) -> list[TextContent]:
163
176
  match name:
177
+ case CodeAssistTools.LIST_DIRECTORY:
178
+ result = await FileTools.list_directory(
179
+ arguments["path"],
180
+ arguments.get("recursive", False),
181
+ arguments.get("include_hidden", False)
182
+ )
183
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
184
+
164
185
  case CodeAssistTools.FILE_CREATE:
165
186
  result = await FileTools.create_file(arguments["path"], arguments.get("content", ""))
166
187
  return [TextContent(type="text", text=result)]
@@ -177,6 +198,10 @@ async def serve(working_dir: Path | None) -> None:
177
198
  result = await FileTools.rewrite_file(arguments["path"], arguments["content"])
178
199
  return [TextContent(type="text", text=result)]
179
200
 
201
+ case CodeAssistTools.FILE_READ:
202
+ content = await FileTools.read_file(arguments["path"])
203
+ return [TextContent(type="text", text=content)]
204
+
180
205
  case CodeAssistTools.GIT_STATUS:
181
206
  repo = git.Repo(arguments["repo_path"])
182
207
  status = git_status(repo)
@@ -90,12 +90,51 @@ class FileTools:
90
90
  return ''.join(diff)
91
91
 
92
92
  @classmethod
93
- async def list_directory(cls, path: str) -> list[str]:
93
+ async def list_directory(cls, path: str, recursive: bool = False, include_hidden: bool = False) -> list[dict]:
94
+ """List contents of a directory.
95
+
96
+ Args:
97
+ path: Directory path to list
98
+ recursive: Whether to list subdirectories recursively
99
+ include_hidden: Whether to include hidden files/directories
100
+
101
+ Returns:
102
+ List of dicts with file/directory info:
103
+ {
104
+ "name": str,
105
+ "path": str,
106
+ "type": "file"|"directory",
107
+ "size": int, # for files only
108
+ "children": list # for directories when recursive=True
109
+ }
110
+ """
94
111
  path = await cls.validate_path(path)
95
- entries = []
96
- for item in Path(path).iterdir():
97
- entries.append(str(item))
98
- return entries
112
+ if not os.path.isdir(path):
113
+ raise ValueError(f"Path {path} is not a directory")
114
+
115
+ result = []
116
+ for entry in os.scandir(path):
117
+ if not include_hidden and entry.name.startswith('.'):
118
+ continue
119
+
120
+ info = {
121
+ "name": entry.name,
122
+ "path": str(Path(entry.path).relative_to(path)),
123
+ "type": "directory" if entry.is_dir() else "file",
124
+ }
125
+
126
+ if entry.is_file():
127
+ info["size"] = entry.stat().st_size
128
+ elif recursive and entry.is_dir():
129
+ info["children"] = await cls.list_directory(
130
+ entry.path,
131
+ recursive=True,
132
+ include_hidden=include_hidden
133
+ )
134
+
135
+ result.append(info)
136
+
137
+ return result
99
138
 
100
139
  @classmethod
101
140
  async def create_directory(cls, path: str) -> None:
@@ -13,6 +13,9 @@ class FileModify(BaseModel):
13
13
  path: Union[str, Path]
14
14
  replacements: dict[str, str]
15
15
 
16
+ class FileRead(BaseModel):
17
+ path: Union[str, Path]
18
+
16
19
  class FileRewrite(BaseModel):
17
20
  path: Union[str, Path]
18
21
  content: str
@@ -42,6 +45,11 @@ class GitShow(GitBase):
42
45
  class GitLog(GitBase):
43
46
  max_count: int = 10
44
47
 
48
+ class ListDirectory(BaseModel):
49
+ path: Union[str, Path]
50
+ recursive: bool = False
51
+ include_hidden: bool = False
52
+
45
53
  class RepositoryOperation(BaseModel):
46
54
  path: str
47
55
  content: Optional[str] = None
@@ -51,14 +51,37 @@ async def test_read_multiple_files():
51
51
 
52
52
  @pytest.mark.asyncio
53
53
  async def test_list_directory():
54
- test_file = TEST_DIR / "test.txt"
55
- test_dir = TEST_DIR / "test_dir"
56
- await FileTools.write_file(str(test_file), "content")
57
- test_dir.mkdir()
58
-
59
- entries = await FileTools.list_directory(str(TEST_DIR))
60
- assert any(entry.endswith("test.txt") for entry in entries)
61
- assert any(entry.endswith("test_dir") for entry in entries)
54
+ # Create test structure
55
+ (TEST_DIR / "dir1").mkdir()
56
+ (TEST_DIR / "dir1" / "file1.txt").write_text("content1")
57
+ (TEST_DIR / "dir1" / "file2.txt").write_text("content2")
58
+ (TEST_DIR / "dir1" / "subdir").mkdir()
59
+ (TEST_DIR / "dir1" / "subdir" / "file3.txt").write_text("content3")
60
+ (TEST_DIR / ".hidden_dir").mkdir()
61
+ (TEST_DIR / ".hidden_file").write_text("hidden")
62
+
63
+ # Test basic listing
64
+ result = await FileTools.list_directory(str(TEST_DIR / "dir1"), recursive=False, include_hidden=False)
65
+ assert len(result) == 3
66
+ assert any(item["name"] == "file1.txt" and item["type"] == "file" for item in result)
67
+ assert any(item["name"] == "file2.txt" and item["type"] == "file" for item in result)
68
+ assert any(item["name"] == "subdir" and item["type"] == "directory" for item in result)
69
+
70
+ # Test recursive listing
71
+ result = await FileTools.list_directory(str(TEST_DIR / "dir1"), recursive=True, include_hidden=False)
72
+ assert len(result) == 3
73
+ subdir = next(item for item in result if item["name"] == "subdir")
74
+ assert "children" in subdir
75
+ assert len(subdir["children"]) == 1
76
+ assert subdir["children"][0]["name"] == "file3.txt"
77
+
78
+ # Test hidden files
79
+ result = await FileTools.list_directory(str(TEST_DIR), recursive=False, include_hidden=True)
80
+ assert any(item["name"].startswith(".hidden") for item in result)
81
+
82
+ # Test error on non-directory
83
+ with pytest.raises(ValueError):
84
+ await FileTools.list_directory(str(TEST_DIR / "dir1" / "file1.txt"))
62
85
 
63
86
  @pytest.mark.asyncio
64
87
  async def test_directory_tree():
@@ -63,6 +63,15 @@ wheels = [
63
63
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
64
64
  ]
65
65
 
66
+ [[package]]
67
+ name = "elementpath"
68
+ version = "4.7.0"
69
+ source = { registry = "https://pypi.org/simple" }
70
+ sdist = { url = "https://files.pythonhosted.org/packages/3e/44/7a10be09cb79d4e6900fc1f2a7db6fb7639c6b05f88b61106650b618fc52/elementpath-4.7.0.tar.gz", hash = "sha256:a2029dc8752fcfec49663d1ed1b412c6daf278c0c91938f50f63c4fe9ed1848e", size = 357225 }
71
+ wheels = [
72
+ { url = "https://files.pythonhosted.org/packages/b8/eb/c4fac84745a84d3bc0fd481cf8c204f60bce367a19d8f9182a195ed6d42a/elementpath-4.7.0-py3-none-any.whl", hash = "sha256:607804a1b4250ac448c1e2bfaec4ee1c980b0a07cfdb0d9057b57102038ed480", size = 240649 },
73
+ ]
74
+
66
75
  [[package]]
67
76
  name = "gitdb"
68
77
  version = "4.0.12"
@@ -172,7 +181,7 @@ wheels = [
172
181
 
173
182
  [[package]]
174
183
  name = "mcp-server-code-assist"
175
- version = "0.1.2"
184
+ version = "0.1.3"
176
185
  source = { editable = "." }
177
186
  dependencies = [
178
187
  { name = "aiofiles" },
@@ -180,6 +189,7 @@ dependencies = [
180
189
  { name = "gitpython" },
181
190
  { name = "mcp" },
182
191
  { name = "pydantic" },
192
+ { name = "xmlschema" },
183
193
  ]
184
194
 
185
195
  [package.optional-dependencies]
@@ -197,6 +207,7 @@ requires-dist = [
197
207
  { name = "pydantic", specifier = ">=2.0.0" },
198
208
  { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" },
199
209
  { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.25.0" },
210
+ { name = "xmlschema", specifier = ">=3.4.3" },
200
211
  ]
201
212
 
202
213
  [[package]]
@@ -397,3 +408,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec
397
408
  wheels = [
398
409
  { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
399
410
  ]
411
+
412
+ [[package]]
413
+ name = "xmlschema"
414
+ version = "3.4.3"
415
+ source = { registry = "https://pypi.org/simple" }
416
+ dependencies = [
417
+ { name = "elementpath" },
418
+ ]
419
+ sdist = { url = "https://files.pythonhosted.org/packages/11/ca/56579e6b4558c3c06902fe6647280345da8087f080c86e85166fd2131f8d/xmlschema-3.4.3.tar.gz", hash = "sha256:0c638dac81c7d6c9da9a8d7544402c48cffe7ee0e13cc47fc0c18794d1395dfb", size = 585144 }
420
+ wheels = [
421
+ { url = "https://files.pythonhosted.org/packages/5a/49/ff75976757b23d6345ec1021fd0d2480d7ceed66d285e31556ac1147858d/xmlschema-3.4.3-py3-none-any.whl", hash = "sha256:eea4e5a1aac041b546ebe7b2eb68eb5eaebf5c5258e573cfc182375676b2e4e3", size = 417847 },
422
+ ]