Skip to content

Commit 7cbd861

Browse files
authored
Fix search tools parameters (#314)
Search tools parameters were defined incorrectly, leading to either these parameters missing from MCP tools definition or to broken agentic workflows (in the case of wiki search). This also fixes the MCP inspector used through codespaces. This fixes #296 ## **Associated Risks** None ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [x] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** MCP inspector
1 parent 14a25b4 commit 7cbd861

File tree

2 files changed

+84
-86
lines changed

2 files changed

+84
-86
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"build": "tsc && shx chmod +x dist/*.js",
2727
"prepare": "npm run build",
2828
"watch": "tsc --watch",
29-
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
29+
"inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector node dist/index.js",
3030
"start": "node -r tsconfig-paths/register dist/index.js",
3131
"eslint": "eslint",
3232
"eslint-fix": "eslint --fix",

src/tools/search.ts

Lines changed: 83 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -18,51 +18,49 @@ const SEARCH_TOOLS = {
1818
};
1919

2020
function configureSearchTools(server: McpServer, tokenProvider: () => Promise<AccessToken>, connectionProvider: () => Promise<WebApi>, userAgentProvider: () => string) {
21-
/*
22-
CODE SEARCH
23-
Get the code search results for a given search text.
24-
*/
2521
server.tool(
2622
SEARCH_TOOLS.search_code,
27-
"Get the code search results for a given search text.",
23+
"Search Azure DevOps Repositories for a given search text",
2824
{
29-
searchRequest: z
30-
.object({
31-
searchText: z.string().describe("Search text to find in code"),
32-
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
33-
$top: z.number().default(5).describe("Number of results to return (for pagination)"),
34-
filters: z
35-
.object({
36-
Project: z.array(z.string()).optional().describe("Filter in these projects"),
37-
Repository: z.array(z.string()).optional().describe("Filter in these repositories"),
38-
Path: z.array(z.string()).optional().describe("Filter in these paths"),
39-
Branch: z.array(z.string()).optional().describe("Filter in these branches"),
40-
CodeElement: z.array(z.string()).optional().describe("Filter for these code elements (e.g., classes, functions, symbols)"),
41-
// Note: CodeElement is optional and can be used to filter results by specific code elements.
42-
// It can be a string or an array of strings.
43-
// If provided, the search will only return results that match the specified code elements.
44-
// This is useful for narrowing down the search to specific classes, functions, definitions, or symbols.
45-
// Example: CodeElement: ["MyClass", "MyFunction"]
46-
})
47-
.partial()
48-
.optional(),
49-
includeFacets: z.boolean().optional(),
50-
})
51-
.strict(),
25+
searchText: z.string().describe("Keywords to search for in code repositories"),
26+
project: z.array(z.string()).optional().describe("Filter by projects"),
27+
repository: z.array(z.string()).optional().describe("Filter by repositories"),
28+
path: z.array(z.string()).optional().describe("Filter by paths"),
29+
branch: z.array(z.string()).optional().describe("Filter by branches"),
30+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
31+
$skip: z.number().default(0).describe("Number of results to skip"),
32+
$top: z.number().default(5).describe("Maximum number of results to return"),
5233
},
53-
async ({ searchRequest }) => {
34+
async ({ searchText, project, repository, path, branch, includeFacets, $skip, $top }) => {
5435
const accessToken = await tokenProvider();
5536
const connection = await connectionProvider();
5637
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/codesearchresults?api-version=${apiVersion}`;
5738

39+
const requestBody: Record<string, unknown> = {
40+
searchText,
41+
includeFacets,
42+
$skip,
43+
$top,
44+
};
45+
46+
const filters: Record<string, string[]> = {};
47+
if (project && project.length > 0) filters.Project = project;
48+
if (repository && repository.length > 0) filters.Repository = repository;
49+
if (path && path.length > 0) filters.Path = path;
50+
if (branch && branch.length > 0) filters.Branch = branch;
51+
52+
if (Object.keys(filters).length > 0) {
53+
requestBody.filters = filters;
54+
}
55+
5856
const response = await fetch(url, {
5957
method: "POST",
6058
headers: {
6159
"Content-Type": "application/json",
6260
"Authorization": `Bearer ${accessToken.token}`,
6361
"User-Agent": userAgentProvider(),
6462
},
65-
body: JSON.stringify(searchRequest),
63+
body: JSON.stringify(requestBody),
6664
});
6765

6866
if (!response.ok) {
@@ -72,54 +70,53 @@ function configureSearchTools(server: McpServer, tokenProvider: () => Promise<Ac
7270
const resultText = await response.text();
7371
const resultJson = JSON.parse(resultText) as { results?: SearchResult[] };
7472

75-
const topResults: SearchResult[] = Array.isArray(resultJson.results) ? resultJson.results.slice(0, Math.min(searchRequest.$top, resultJson.results.length)) : [];
76-
7773
const gitApi = await connection.getGitApi();
78-
const combinedResults = await fetchCombinedResults(topResults, gitApi);
74+
const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
7975

8076
return {
8177
content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
8278
};
8379
}
8480
);
8581

86-
/*
87-
WIKI SEARCH
88-
Get wiki search results for a given search text.
89-
*/
9082
server.tool(
9183
SEARCH_TOOLS.search_wiki,
92-
"Get wiki search results for a given search text.",
84+
"Search Azure DevOps Wiki for a given search text",
9385
{
94-
searchRequest: z
95-
.object({
96-
searchText: z.string().describe("Search text to find in wikis"),
97-
$skip: z.number().default(0).describe("Number of results to skip (for pagination)"),
98-
$top: z.number().default(10).describe("Number of results to return (for pagination)"),
99-
filters: z
100-
.object({
101-
Project: z.array(z.string()).optional().describe("Filter in these projects"),
102-
Wiki: z.array(z.string()).optional().describe("Filter in these wiki names"),
103-
})
104-
.partial()
105-
.optional()
106-
.describe("Filters to apply to the search text"),
107-
includeFacets: z.boolean().optional(),
108-
})
109-
.strict(),
86+
searchText: z.string().describe("Keywords to search for wiki pages"),
87+
project: z.array(z.string()).optional().describe("Filter by projects"),
88+
wiki: z.array(z.string()).optional().describe("Filter by wiki names"),
89+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
90+
$skip: z.number().default(0).describe("Number of results to skip"),
91+
$top: z.number().default(10).describe("Maximum number of results to return"),
11092
},
111-
async ({ searchRequest }) => {
93+
async ({ searchText, project, wiki, includeFacets, $skip, $top }) => {
11294
const accessToken = await tokenProvider();
11395
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/wikisearchresults?api-version=${apiVersion}`;
11496

97+
const requestBody: Record<string, unknown> = {
98+
searchText,
99+
includeFacets,
100+
$skip,
101+
$top,
102+
};
103+
104+
const filters: Record<string, string[]> = {};
105+
if (project && project.length > 0) filters.Project = project;
106+
if (wiki && wiki.length > 0) filters.Wiki = wiki;
107+
108+
if (Object.keys(filters).length > 0) {
109+
requestBody.filters = filters;
110+
}
111+
115112
const response = await fetch(url, {
116113
method: "POST",
117114
headers: {
118115
"Content-Type": "application/json",
119116
"Authorization": `Bearer ${accessToken.token}`,
120117
"User-Agent": userAgentProvider(),
121118
},
122-
body: JSON.stringify(searchRequest),
119+
body: JSON.stringify(requestBody),
123120
});
124121

125122
if (!response.ok) {
@@ -133,45 +130,50 @@ function configureSearchTools(server: McpServer, tokenProvider: () => Promise<Ac
133130
}
134131
);
135132

136-
/*
137-
WORK ITEM SEARCH
138-
Get work item search results for a given search text.
139-
*/
140133
server.tool(
141134
SEARCH_TOOLS.search_workitem,
142-
"Get work item search results for a given search text.",
135+
"Get Azure DevOps Work Item search results for a given search text",
143136
{
144-
searchRequest: z
145-
.object({
146-
searchText: z.string().describe("Search text to find in work items"),
147-
$skip: z.number().default(0).describe("Number of results to skip for pagination"),
148-
$top: z.number().default(10).describe("Number of results to return"),
149-
filters: z
150-
.object({
151-
"System.TeamProject": z.array(z.string()).optional().describe("Filter by team project"),
152-
"System.AreaPath": z.array(z.string()).optional().describe("Filter by area path"),
153-
"System.WorkItemType": z.array(z.string()).optional().describe("Filter by work item type like Bug, Task, User Story"),
154-
"System.State": z.array(z.string()).optional().describe("Filter by state"),
155-
"System.AssignedTo": z.array(z.string()).optional().describe("Filter by assigned to"),
156-
})
157-
.partial()
158-
.optional(),
159-
includeFacets: z.boolean().optional(),
160-
})
161-
.strict(),
137+
searchText: z.string().describe("Search text to find in work items"),
138+
project: z.array(z.string()).optional().describe("Filter by projects"),
139+
areaPath: z.array(z.string()).optional().describe("Filter by area paths"),
140+
workItemType: z.array(z.string()).optional().describe("Filter by work item types"),
141+
state: z.array(z.string()).optional().describe("Filter by work item states"),
142+
assignedTo: z.array(z.string()).optional().describe("Filter by assigned to users"),
143+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
144+
$skip: z.number().default(0).describe("Number of results to skip for pagination"),
145+
$top: z.number().default(10).describe("Number of results to return"),
162146
},
163-
async ({ searchRequest }) => {
147+
async ({ searchText, project, areaPath, workItemType, state, assignedTo, includeFacets, $skip, $top }) => {
164148
const accessToken = await tokenProvider();
165149
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/workitemsearchresults?api-version=${apiVersion}`;
166150

151+
const requestBody: Record<string, unknown> = {
152+
searchText,
153+
includeFacets,
154+
$skip,
155+
$top,
156+
};
157+
158+
const filters: Record<string, unknown> = {};
159+
if (project && project.length > 0) filters["System.TeamProject"] = project;
160+
if (areaPath && areaPath.length > 0) filters["System.AreaPath"] = areaPath;
161+
if (workItemType && workItemType.length > 0) filters["System.WorkItemType"] = workItemType;
162+
if (state && state.length > 0) filters["System.State"] = state;
163+
if (assignedTo && assignedTo.length > 0) filters["System.AssignedTo"] = assignedTo;
164+
165+
if (Object.keys(filters).length > 0) {
166+
requestBody.filters = filters;
167+
}
168+
167169
const response = await fetch(url, {
168170
method: "POST",
169171
headers: {
170172
"Content-Type": "application/json",
171173
"Authorization": `Bearer ${accessToken.token}`,
172174
"User-Agent": userAgentProvider(),
173175
},
174-
body: JSON.stringify(searchRequest),
176+
body: JSON.stringify(requestBody),
175177
});
176178

177179
if (!response.ok) {
@@ -186,10 +188,6 @@ function configureSearchTools(server: McpServer, tokenProvider: () => Promise<Ac
186188
);
187189
}
188190

189-
/*
190-
Fetch git repo file content for top 5(default) search results.
191-
*/
192-
193191
interface SearchResult {
194192
project?: { id?: string };
195193
repository?: { id?: string };

0 commit comments

Comments
 (0)