Skip to content

Commit eabfde9

Browse files
Zhe YuDavidyz
authored andcommitted
feat(mcp): Add command-line options for MCP.
1 parent 970dc66 commit eabfde9

3 files changed

Lines changed: 119 additions & 6 deletions

File tree

docs/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,10 @@ is available in the shell as `vectorcode-mcp-server`, and make sure you're using
636636
a [standalone chromadb server](#chromadb) configured in the [JSON](#configuring-vectorcode)
637637
via the `host` and `port` options.
638638

639+
The MCP server entry point (`vectorcode-mcp-server`) provides some CLI options
640+
that you can use to customise the default behaviour of the server. To view the
641+
supported options, run `vectorcode-mcp-server -h` in your shell.
642+
639643
### Writing Prompts
640644

641645
If you want to integrate VectorCode in your LLM application, you may want to

src/vectorcode/mcp_main.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import argparse
12
import asyncio
23
import logging
34
import os
45
import sys
6+
from dataclasses import dataclass
57
from pathlib import Path
68
from typing import Optional
79

@@ -31,7 +33,33 @@
3133
from vectorcode.subcommands.query import get_query_result_files
3234

3335
logger = logging.getLogger(name=__name__)
34-
mcp = FastMCP("VectorCode", instructions="\n".join(prompt_strings))
36+
37+
38+
@dataclass
39+
class MCPConfig:
40+
n_results: int = 10
41+
ls_on_start: bool = False
42+
43+
44+
mcp_config = MCPConfig()
45+
46+
47+
def get_arg_parser():
48+
parser = argparse.ArgumentParser(prog="vectorcode-mcp-server")
49+
parser.add_argument(
50+
"--number",
51+
"-n",
52+
type=int,
53+
default=10,
54+
help="Default number of files to retrieve.",
55+
)
56+
parser.add_argument(
57+
"--ls-on-start",
58+
action="store_true",
59+
default=False,
60+
help="Whether to include the output of `vectorcode ls` in the tool description.",
61+
)
62+
return parser
3563

3664

3765
default_config: Optional[Config] = None
@@ -114,6 +142,7 @@ async def query_tool(
114142

115143
async def mcp_server():
116144
global default_config, default_client, default_collection
145+
117146
local_config_dir = await find_project_config_dir(".")
118147

119148
if local_config_dir is not None:
@@ -128,9 +157,23 @@ async def mcp_server():
128157
try:
129158
default_collection = await get_collection(default_client, default_config)
130159
logger.info("Collection initialised for %s.", project_root)
131-
except InvalidCollectionException:
160+
except InvalidCollectionException: # pragma: nocover
132161
default_collection = None
133162

163+
default_instructions = "\n".join(prompt_strings)
164+
if default_client is None:
165+
if mcp_config.ls_on_start: # pragma: nocover
166+
logger.warning(
167+
"Failed to initialise a chromadb client. Ignoring --ls-on-start flag."
168+
)
169+
else:
170+
if mcp_config.ls_on_start:
171+
logger.info("Adding available collections to the server instructions.")
172+
default_instructions += "\nYou have access to the following collections:\n"
173+
for name in await list_collections():
174+
default_instructions += f"<collection>{name}</collection>"
175+
176+
mcp = FastMCP("VectorCode", instructions=default_instructions)
134177
mcp.add_tool(
135178
fn=list_collections,
136179
name="ls",
@@ -140,24 +183,36 @@ async def mcp_server():
140183
mcp.add_tool(
141184
fn=query_tool,
142185
name="query",
143-
description="""
186+
description=f"""
144187
Use VectorCode to perform vector similarity search on repositories and return a list of relevant file paths and contents.
145188
Make sure `project_root` is one of the values from the `ls` tool.
189+
Unless the user requested otherwise, start your retrievals by {mcp_config.n_results} files.
146190
The result contains the relative paths for the files and their corresponding contents.
147191
""",
148192
)
149193

150194
return mcp
151195

152196

197+
def parse_cli_args(args: Optional[list[str]] = None) -> MCPConfig:
198+
parser = get_arg_parser()
199+
parsed_args = parser.parse_args(args or sys.argv)
200+
return MCPConfig(n_results=parsed_args.number, ls_on_start=parsed_args.ls_on_start)
201+
202+
153203
async def run_server(): # pragma: nocover
154204
mcp = await mcp_server()
155205
await mcp.run_stdio_async()
156206
return 0
157207

158208

159209
def main(): # pragma: nocover
210+
global mcp_config
160211
config_logging("vectorcode-mcp-server", stdio=False)
212+
mcp_config = parse_cli_args()
213+
assert mcp_config.n_results > 0 and mcp_config.n_results % 1 == 0, (
214+
"--number must be used with a positive integer!"
215+
)
161216
return asyncio.run(run_server())
162217

163218

tests/test_mcp.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
from argparse import ArgumentParser
12
from unittest.mock import AsyncMock, MagicMock, patch
23

34
import pytest
45
from mcp import McpError
56

67
from vectorcode.cli_utils import Config
7-
from vectorcode.mcp_main import list_collections, mcp_server, query_tool
8+
from vectorcode.mcp_main import (
9+
get_arg_parser,
10+
list_collections,
11+
mcp_server,
12+
parse_cli_args,
13+
query_tool,
14+
)
815

916

1017
@pytest.mark.asyncio
@@ -44,11 +51,11 @@ async def test_list_collections_no_metadata():
4451
mock_collection2 = AsyncMock()
4552
mock_collection2.metadata = None
4653

47-
async def async_generator():
54+
async def async_generator(cli):
4855
yield mock_collection1
4956
yield mock_collection2
5057

51-
mock_get_collections.return_value = async_generator()
58+
mock_get_collections.side_effect = async_generator
5259

5360
result = await list_collections()
5461
assert result == ["path1"]
@@ -182,3 +189,50 @@ async def test_mcp_server():
182189
await mcp_server()
183190

184191
assert mock_add_tool.call_count == 2
192+
193+
194+
@pytest.mark.asyncio
195+
async def test_mcp_server_ls_on_start():
196+
with (
197+
patch(
198+
"vectorcode.mcp_main.find_project_config_dir"
199+
) as mock_find_project_config_dir,
200+
patch("vectorcode.mcp_main.load_config_file") as mock_load_config_file,
201+
patch("vectorcode.mcp_main.get_client") as mock_get_client,
202+
patch("vectorcode.mcp_main.get_collection") as mock_get_collection,
203+
patch(
204+
"vectorcode.mcp_main.get_collections", spec=AsyncMock
205+
) as mock_get_collections,
206+
patch("mcp.server.fastmcp.FastMCP.add_tool") as mock_add_tool,
207+
):
208+
from vectorcode.mcp_main import mcp_config
209+
210+
mcp_config.ls_on_start = True
211+
mock_find_project_config_dir.return_value = "/path/to/config"
212+
mock_load_config_file.return_value = Config(project_root="/path/to/project")
213+
mock_client = AsyncMock()
214+
mock_get_client.return_value = mock_client
215+
mock_collection = AsyncMock()
216+
mock_collection.metadata = {"path": "/path/to/project"}
217+
mock_get_collection.return_value = mock_collection
218+
219+
async def new_get_collections(clients):
220+
yield mock_collection
221+
222+
mock_get_collections.side_effect = new_get_collections
223+
224+
await mcp_server()
225+
226+
assert mock_add_tool.call_count == 2
227+
mock_get_collections.assert_called()
228+
229+
230+
def test_arg_parser():
231+
assert isinstance(get_arg_parser(), ArgumentParser)
232+
233+
234+
def test_args_parsing():
235+
args = ["--number", "15", "--ls-on-start"]
236+
parsed = parse_cli_args(args)
237+
assert parsed.n_results == 15
238+
assert parsed.ls_on_start

0 commit comments

Comments
 (0)