Skip to content

Commit 162649a

Browse files
Zhe YuDavidyz
authored andcommitted
feat(cli): Expose vectorise tool in MCP server
1 parent 9e7dad6 commit 162649a

3 files changed

Lines changed: 83 additions & 3 deletions

File tree

docs/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,8 @@ features:
706706
707707
- `ls`: list local collections, similar to the `ls` subcommand in the CLI;
708708
- `query`: query from a given collection, similar to the `query` subcommand in
709-
the CLI.
709+
the CLI;
710+
- `vectorise`: vectorise files into a given project.
710711
711712
To try it out, install the `vectorcode[mcp]` dependency group and the MCP server
712713
is available in the shell as `vectorcode-mcp-server`, and make sure you're using

src/vectorcode/mcp_main.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
from chromadb.api.models.AsyncCollection import AsyncCollection
1313
from chromadb.errors import InvalidCollectionException
1414

15+
from vectorcode.subcommands.vectorise import (
16+
VectoriseStats,
17+
chunked_add,
18+
exclude_paths_by_spec,
19+
find_exclude_specs,
20+
)
21+
1522
try: # pragma: nocover
1623
from mcp import ErrorData, McpError
1724
from mcp.server.fastmcp import FastMCP
@@ -26,6 +33,7 @@
2633
Config,
2734
cleanup_path,
2835
config_logging,
36+
expand_globs,
2937
find_project_config_dir,
3038
get_project_config,
3139
load_config_file,
@@ -89,6 +97,69 @@ async def list_collections() -> list[str]:
8997
return names
9098

9199

100+
async def vectorise_files(paths: list[str], project_root: str) -> dict[str, int]:
101+
logger.info(
102+
f"vectorise tool called with the following args: {paths=}, {project_root=}"
103+
)
104+
project_root = os.path.expanduser(project_root)
105+
if not os.path.isdir(project_root):
106+
logger.error(f"Invalid project root: {project_root}")
107+
raise McpError(
108+
ErrorData(code=1, message=f"{project_root} is not a valid path.")
109+
)
110+
config = await get_project_config(project_root)
111+
try:
112+
client = await get_client(config)
113+
collection = await get_collection(client, config, True)
114+
except Exception as e:
115+
logger.error("Failed to access collection at %s", project_root)
116+
raise McpError(
117+
ErrorData(
118+
code=1,
119+
message=f"{e.__class__.__name__}: Failed to create the collection at {project_root}.",
120+
)
121+
)
122+
if collection is None:
123+
raise McpError(
124+
ErrorData(
125+
code=1,
126+
message=f"Failed to access the collection at {project_root}. Use `list_collections` tool to get a list of valid paths for this field.",
127+
)
128+
)
129+
130+
paths = [os.path.expanduser(i) for i in await expand_globs(paths)]
131+
final_config = await config.merge_from(
132+
Config(files=[i for i in paths if os.path.isfile(i)], project_root=project_root)
133+
)
134+
for ignore_spec in find_exclude_specs(final_config):
135+
if os.path.isfile(ignore_spec):
136+
logger.info(f"Loading ignore specs from {ignore_spec}.")
137+
paths = exclude_paths_by_spec((str(i) for i in paths), ignore_spec)
138+
stats = VectoriseStats()
139+
collection_lock = asyncio.Lock()
140+
stats_lock = asyncio.Lock()
141+
max_batch_size = await client.get_max_batch_size()
142+
semaphore = asyncio.Semaphore(os.cpu_count() or 1)
143+
tasks = [
144+
asyncio.create_task(
145+
chunked_add(
146+
str(file),
147+
collection,
148+
collection_lock,
149+
stats,
150+
stats_lock,
151+
final_config,
152+
max_batch_size,
153+
semaphore,
154+
)
155+
)
156+
for file in paths
157+
]
158+
for i, task in enumerate(asyncio.as_completed(tasks), start=1):
159+
await task
160+
return stats.to_dict()
161+
162+
92163
async def query_tool(
93164
n_query: int, query_messages: list[str], project_root: str
94165
) -> list[str]:
@@ -199,6 +270,14 @@ async def mcp_server():
199270
),
200271
)
201272

273+
mcp.add_tool(
274+
fn=vectorise_files,
275+
name="vectorise",
276+
description="\n".join(
277+
prompt_by_categories["vectorise"] + prompt_by_categories["general"]
278+
),
279+
)
280+
202281
return mcp
203282

204283

tests/test_mcp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async def test_mcp_server():
188188

189189
await mcp_server()
190190

191-
assert mock_add_tool.call_count == 2
191+
assert mock_add_tool.call_count == 3
192192

193193

194194
@pytest.mark.asyncio
@@ -223,7 +223,7 @@ async def new_get_collections(clients):
223223

224224
await mcp_server()
225225

226-
assert mock_add_tool.call_count == 2
226+
assert mock_add_tool.call_count == 3
227227
mock_get_collections.assert_called()
228228

229229

0 commit comments

Comments
 (0)