Skip to content

Commit 681b85f

Browse files
authored
feat(cli): Add files_ls and files_rm tools to MCP server (#235)
* feat(cli): Add `files_ls` and `files_rm` tools to MCP server * docs(cli): Document `files_ls` and `files_rm` tools in cli.md * Auto generate docs --------- Co-authored-by: Davidyz <Davidyz@users.noreply.github.com>
1 parent a10ae92 commit 681b85f

4 files changed

Lines changed: 118 additions & 6 deletions

File tree

doc/VectorCode-cli.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,9 @@ features:
803803
- `ls`list local collections, similar to the `ls` subcommand in the CLI;
804804
- `query`query from a given collection, similar to the `query` subcommand in
805805
the CLI;
806-
- `vectorise`vectorise files into a given project.
806+
- `vectorise`vectorise files into a given project;
807+
- `files_ls`show files that have been indexed for the current project;
808+
- `files_rm`remove some files from the database for a project.
807809

808810
To try it out, install the `vectorcode[mcp]` dependency group and the MCP
809811
server is available in the shell as `vectorcode-mcp-server`.

docs/cli.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,9 @@ features:
726726
- `ls`: list local collections, similar to the `ls` subcommand in the CLI;
727727
- `query`: query from a given collection, similar to the `query` subcommand in
728728
the CLI;
729-
- `vectorise`: vectorise files into a given project.
729+
- `vectorise`: vectorise files into a given project;
730+
- `files_ls`: show files that have been indexed for the current project;
731+
- `files_rm`: remove some files from the database for a project.
730732
731733
To try it out, install the `vectorcode[mcp]` dependency group and the MCP server
732734
is available in the shell as `vectorcode-mcp-server`.

src/vectorcode/mcp_main.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
import sys
66
from dataclasses import dataclass
77
from pathlib import Path
8-
from typing import Optional
8+
from typing import Optional, cast
99

1010
import shtab
11+
from chromadb.types import Where
1112

1213
from vectorcode.subcommands.vectorise import (
1314
VectoriseStats,
@@ -33,11 +34,17 @@
3334
cleanup_path,
3435
config_logging,
3536
expand_globs,
37+
expand_path,
3638
find_project_config_dir,
3739
get_project_config,
3840
load_config_file,
3941
)
40-
from vectorcode.common import ClientManager, get_collection, get_collections
42+
from vectorcode.common import (
43+
ClientManager,
44+
get_collection,
45+
get_collections,
46+
list_collection_files,
47+
)
4148
from vectorcode.subcommands.prompt import prompt_by_categories
4249
from vectorcode.subcommands.query import get_query_result_files
4350

@@ -224,6 +231,34 @@ async def query_tool(
224231
)
225232

226233

234+
async def ls_files(project_root: str) -> list[str]:
235+
"""
236+
project_root: Directory to the repository. MUST be from the vectorcode `ls` tool or user input;
237+
"""
238+
configs = await get_project_config(expand_path(project_root, True))
239+
async with ClientManager().get_client(configs) as client:
240+
return await list_collection_files(await get_collection(client, configs, False))
241+
242+
243+
async def rm_files(files: list[str], project_root: str):
244+
"""
245+
files: list of paths of the files to be removed;
246+
project_root: Directory to the repository. MUST be from the vectorcode `ls` tool or user input;
247+
"""
248+
configs = await get_project_config(expand_path(project_root, True))
249+
async with ClientManager().get_client(configs) as client:
250+
try:
251+
collection = await get_collection(client, configs, False)
252+
files = [str(expand_path(i, True)) for i in files if os.path.isfile(i)]
253+
if files:
254+
await collection.delete(where=cast(Where, {"path": {"$in": files}}))
255+
else: # pragma: nocover
256+
logger.warning(f"All paths were invalid: {files}")
257+
except ValueError: # pragma: nocover
258+
logger.warning(f"Failed to find the collection at {configs.project_root}")
259+
return
260+
261+
227262
async def mcp_server():
228263
global default_config, default_project_root
229264

@@ -283,6 +318,18 @@ async def mcp_server():
283318
),
284319
)
285320

321+
mcp.add_tool(
322+
fn=rm_files,
323+
name="files_rm",
324+
description="Remove files from VectorCode embedding database.",
325+
)
326+
327+
mcp.add_tool(
328+
fn=ls_files,
329+
name="files_ls",
330+
description="List files that have been indexed by VectorCode.",
331+
)
332+
286333
return mcp
287334

288335

tests/test_mcp.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
from mcp import McpError
88

99
from vectorcode.cli_utils import Config
10+
from vectorcode.common import ClientManager
1011
from vectorcode.mcp_main import (
1112
get_arg_parser,
1213
list_collections,
14+
ls_files,
1315
mcp_server,
1416
parse_cli_args,
1517
query_tool,
18+
rm_files,
1619
vectorise_files,
1720
)
1821

@@ -335,7 +338,7 @@ async def test_mcp_server():
335338

336339
await mcp_server()
337340

338-
assert mock_add_tool.call_count == 3
341+
assert mock_add_tool.call_count == 5
339342

340343

341344
@pytest.mark.asyncio
@@ -374,10 +377,68 @@ async def new_get_collections(clients):
374377

375378
await mcp_server()
376379

377-
assert mock_add_tool.call_count == 3
380+
assert mock_add_tool.call_count == 5
378381
mock_get_collections.assert_called()
379382

380383

384+
@pytest.mark.asyncio
385+
async def test_ls_files_success():
386+
ClientManager().clear()
387+
mock_client = MagicMock()
388+
mock_collection = MagicMock()
389+
expected_files = ["/test/project/file1.py", "/test/project/dir/file2.txt"]
390+
391+
with (
392+
patch("vectorcode.mcp_main.get_project_config") as mock_get_project_config,
393+
patch(
394+
"vectorcode.mcp_main.ClientManager._create_client", return_value=mock_client
395+
),
396+
patch("vectorcode.common.try_server", return_value=True),
397+
patch("vectorcode.mcp_main.get_collection", return_value=mock_collection),
398+
patch(
399+
"vectorcode.mcp_main.list_collection_files", return_value=expected_files
400+
) as mock_list_collection_files,
401+
patch(
402+
"vectorcode.cli_utils.expand_path", side_effect=lambda x, y: x
403+
), # Mock expand_path to return input
404+
):
405+
mock_get_project_config.return_value = Config(project_root="/test/project")
406+
result = await ls_files(project_root="/test/project")
407+
408+
assert result == expected_files
409+
mock_get_project_config.assert_called_once_with("/test/project")
410+
411+
mock_list_collection_files.assert_called_once_with(mock_collection)
412+
413+
414+
@pytest.mark.asyncio
415+
async def test_rm_files_success():
416+
ClientManager().clear()
417+
mock_client = MagicMock()
418+
mock_collection = MagicMock()
419+
files_to_remove = ["/test/project/file1.py", "/test/project/file2.txt"]
420+
421+
with (
422+
patch("os.path.isfile", side_effect=lambda x: x in files_to_remove),
423+
patch("vectorcode.mcp_main.get_project_config") as mock_get_project_config,
424+
patch(
425+
"vectorcode.mcp_main.ClientManager._create_client", return_value=mock_client
426+
),
427+
patch("vectorcode.common.try_server", return_value=True),
428+
patch("vectorcode.mcp_main.get_collection", return_value=mock_collection),
429+
patch("vectorcode.cli_utils.expand_path", side_effect=lambda x, y: x),
430+
):
431+
mock_get_project_config.return_value = Config(project_root="/test/project")
432+
mock_collection.delete = AsyncMock()
433+
434+
await rm_files(files=files_to_remove, project_root="/test/project")
435+
436+
mock_get_project_config.assert_called_once_with("/test/project")
437+
mock_collection.delete.assert_called_once_with(
438+
where={"path": {"$in": files_to_remove}}
439+
)
440+
441+
381442
def test_arg_parser():
382443
assert isinstance(get_arg_parser(), ArgumentParser)
383444

0 commit comments

Comments
 (0)