Skip to content

Commit 3d414c7

Browse files
rodion-mclaude
andcommitted
Improve tool discoverability: expand docstrings and add Literal type for profile
- Expand semantic_search and grep_search docstrings with full Args, Returns, and Examples sections so agents see complete usage guidance in tool descriptions - Use Literal["callsOnly", "inheritanceOnly", "allRelevant", "referencesOnly"] for get_artifact_relationships profile parameter — generates JSON Schema enum so agents see valid values without guessing - Update e2e test for invalid profile to expect Pydantic validation error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 490ab92 commit 3d414c7

3 files changed

Lines changed: 104 additions & 11 deletions

File tree

src/tests/test_e2e_tools.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,7 @@ async def test_not_found(self):
10731073

10741074
@pytest.mark.asyncio
10751075
async def test_invalid_profile_returns_error(self):
1076+
"""Pydantic rejects invalid Literal values before the function body runs."""
10761077
mcp = _server({})
10771078
async with Client(mcp) as client:
10781079
result = await client.call_tool(
@@ -1082,9 +1083,10 @@ async def test_invalid_profile_returns_error(self):
10821083
)
10831084

10841085
text = _text(result)
1085-
data = json.loads(text)
1086-
assert "error" in data
1087-
assert "Unsupported profile" in data["error"]
1086+
# Pydantic Literal validation fires before the function body, producing
1087+
# a human-readable validation error (not our custom JSON).
1088+
assert "callsOnly" in text
1089+
assert "literal_error" in text or "Input should be" in text
10881090

10891091
@pytest.mark.asyncio
10901092
async def test_empty_identifier_returns_error(self):

src/tools/artifact_relationships.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Artifact relationships tool implementation."""
22

33
import json
4-
from typing import Any, Dict, List, Optional
4+
from typing import Any, Dict, List, Literal, Optional
55
from urllib.parse import urljoin
66

77
import httpx
@@ -34,7 +34,7 @@
3434
async def get_artifact_relationships(
3535
ctx: Context,
3636
identifier: str,
37-
profile: str = "callsOnly",
37+
profile: Literal["callsOnly", "inheritanceOnly", "allRelevant", "referencesOnly"] = "callsOnly",
3838
max_count_per_type: int = 50,
3939
) -> str:
4040
"""

src/tools/search.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,56 @@ async def semantic_search(
142142
max_results: Optional[int] = None,
143143
) -> str:
144144
"""
145-
Canonical semantic search across indexed repositories and workspaces.
146-
147-
Use this for natural-language exploration when you want relevant artifacts by meaning.
148-
For exact or regex matching, use `grep_search` instead.
145+
Search indexed code by meaning — the default discovery tool.
146+
147+
Use this for natural-language exploration when you want relevant artifacts
148+
by meaning: function names, concepts, architecture patterns, etc.
149+
For exact string or regex matching, use `grep_search` instead.
150+
151+
Args:
152+
query: Natural-language description of what you're looking for.
153+
Example: "authentication middleware", "database connection pooling",
154+
"JWT token validation"
155+
156+
data_sources: Repository or workspace names to search.
157+
Omit to use the API key's default data source.
158+
Call `get_data_sources` first to discover available names.
159+
Example: ["backend", "workspace:payments-team"]
160+
161+
paths: Restrict results to specific directory paths.
162+
Example: ["src/services", "src/domain"]
163+
164+
extensions: Restrict results to specific file extensions.
165+
Example: [".cs", ".py", ".ts"]
166+
167+
max_results: Maximum number of results to return (1–500).
168+
Omit for the server default.
169+
170+
Returns:
171+
Compact JSON: {"results": [...], "hint": "..."}
172+
173+
Each result contains:
174+
- path: file path within the repository
175+
- identifier: fully qualified artifact ID — pass this to `fetch_artifacts`
176+
- kind: "File", "Symbol", or "Chunk"
177+
- description: short triage summary (NOT the real source — see hint)
178+
- startLine/endLine: line range (for symbols)
179+
- contentByteSize: file size in bytes
180+
181+
The `hint` field reminds you to load real source code via
182+
`fetch_artifacts(identifier)` or local `Read(path)` before reasoning
183+
about the code.
184+
185+
Examples:
186+
1. Find authentication code:
187+
semantic_search(query="authentication middleware",
188+
data_sources=["backend"])
189+
190+
2. Narrow to Python files in a specific directory:
191+
semantic_search(query="database retry logic",
192+
data_sources=["backend"],
193+
paths=["src/services"],
194+
extensions=[".py"])
149195
"""
150196
tool_name = "semantic_search"
151197
query_error = _validate_query(query, tool_name)
@@ -197,9 +243,54 @@ async def grep_search(
197243
regex: bool = False,
198244
) -> str:
199245
"""
200-
Canonical exact/regex search across indexed repositories and workspaces.
246+
Search indexed code by exact text or regex pattern.
247+
248+
Use this when the literal string or pattern matters: function names, error
249+
messages, config keys, import paths, TODO comments, etc.
250+
For meaning-based exploration, use `semantic_search` instead.
251+
252+
Args:
253+
query: Exact text or regex pattern to match.
254+
Literal examples: "ConnectionString", "TODO: fix", "import numpy"
255+
Regex examples: "def test_.*async", "Status\\.(Alive|Failed)"
256+
257+
data_sources: Repository or workspace names to search.
258+
Omit to use the API key's default data source.
259+
Call `get_data_sources` first to discover available names.
260+
261+
paths: Restrict results to specific directory paths.
262+
Example: ["src/services"]
263+
264+
extensions: Restrict results to specific file extensions.
265+
Example: [".cs", ".py"]
266+
267+
max_results: Maximum number of results to return (1–500).
268+
269+
regex: If True, treat `query` as a regex pattern. Default: False (literal).
270+
271+
Returns:
272+
Compact JSON: {"results": [...], "hint": "..."}
273+
274+
Each result contains:
275+
- path: file path
276+
- identifier: pass to `fetch_artifacts` for full source
277+
- matchCount: total matches in this file
278+
- matches: array of line-level hits, each with:
279+
- lineNumber, startColumn, endColumn, lineText
280+
281+
The `hint` reminds you that line previews are evidence only — load
282+
full source via `fetch_artifacts` or local `Read()` before reasoning.
283+
284+
Examples:
285+
1. Find exact string:
286+
grep_search(query="ConnectionString",
287+
data_sources=["backend"])
201288
202-
Use this for literal string lookup or regex matching when the pattern itself matters.
289+
2. Regex search for test methods:
290+
grep_search(query="def test_.*auth",
291+
data_sources=["backend"],
292+
extensions=[".py"],
293+
regex=True)
203294
"""
204295
tool_name = "grep_search"
205296
query_error = _validate_query(query, tool_name)

0 commit comments

Comments
 (0)