yontrack-mcp 1.14.2 → 1.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/graphql-documents.test.js +1 -4
- package/build/tools/search.js +32 -28
- package/build/tools/search.test.js +38 -32
- package/package.json +1 -1
|
@@ -12,10 +12,7 @@ const schemas = {
|
|
|
12
12
|
// Yontrack 6 only: never sent to a v5 server (see docs/adr/0001-agent-context-tools-v6-only.md)
|
|
13
13
|
const V6_ONLY = new Set(["src/tools/agent-context.ts", "src/tools/agent-context-support.ts"]);
|
|
14
14
|
// Known invalid documents, as "version file NAME": these must be fixed, then removed from here
|
|
15
|
-
const KNOWN_INVALID = new Set([
|
|
16
|
-
// search(type:) is required on v5, but the `search` tool sends it only when given
|
|
17
|
-
"v5 src/tools/search.ts SEARCH",
|
|
18
|
-
]);
|
|
15
|
+
const KNOWN_INVALID = new Set([]);
|
|
19
16
|
const sourceFiles = [
|
|
20
17
|
"src/capabilities.ts",
|
|
21
18
|
"src/utils.ts",
|
package/build/tools/search.js
CHANGED
|
@@ -1,67 +1,71 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { gqlClient } from "../client.js";
|
|
3
|
+
// `type` is required on v5 (and deprecated, but still accepted, on v6)
|
|
3
4
|
const SEARCH = `
|
|
4
|
-
query Search($token: String!, $type: String
|
|
5
|
+
query Search($token: String!, $type: String!, $size: Int) {
|
|
5
6
|
search(token: $token, type: $type, size: $size) {
|
|
6
7
|
pageItems {
|
|
7
8
|
type { id name description }
|
|
8
9
|
title
|
|
9
10
|
description
|
|
10
11
|
accuracy
|
|
12
|
+
data
|
|
11
13
|
}
|
|
12
14
|
pageInfo { totalSize }
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
`;
|
|
16
|
-
const
|
|
17
|
-
query
|
|
18
|
-
|
|
19
|
-
pageItems {
|
|
20
|
-
type { id name description }
|
|
21
|
-
title
|
|
22
|
-
description
|
|
23
|
-
accuracy
|
|
24
|
-
data
|
|
25
|
-
}
|
|
26
|
-
pageInfo { totalSize }
|
|
27
|
-
}
|
|
18
|
+
const SEARCH_RESULT_TYPES = `
|
|
19
|
+
query SearchResultTypes {
|
|
20
|
+
searchResultTypes { id }
|
|
28
21
|
}
|
|
29
22
|
`;
|
|
23
|
+
async function searchType(token, type, size) {
|
|
24
|
+
const data = await gqlClient.request(SEARCH, { token, type, size });
|
|
25
|
+
return data.search ?? { pageItems: [], pageInfo: { totalSize: 0 } };
|
|
26
|
+
}
|
|
27
|
+
/** Searches each type of result, keeping the best `size` results overall. */
|
|
28
|
+
async function searchAllTypes(token, size) {
|
|
29
|
+
const { searchResultTypes } = await gqlClient.request(SEARCH_RESULT_TYPES);
|
|
30
|
+
const pages = await Promise.all(searchResultTypes.flatMap(({ id }) => (id ? [searchType(token, id, size)] : [])));
|
|
31
|
+
return {
|
|
32
|
+
pageItems: pages
|
|
33
|
+
.flatMap((page) => page.pageItems)
|
|
34
|
+
.sort((a, b) => (b.accuracy ?? 0) - (a.accuracy ?? 0))
|
|
35
|
+
.slice(0, size),
|
|
36
|
+
pageInfo: { totalSize: pages.reduce((total, page) => total + (page.pageInfo?.totalSize ?? 0), 0) },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
30
39
|
export function registerSearchTools(server) {
|
|
31
40
|
server.tool("search", "Full-text search across all Yontrack entities (projects, branches, builds, etc.)", {
|
|
32
41
|
token: z.string().describe("Search query"),
|
|
33
|
-
type: z
|
|
42
|
+
type: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Type of result to search for (e.g. 'project', 'build'); all types when omitted"),
|
|
34
46
|
size: z.number().int().optional().default(10).describe("Max number of results"),
|
|
35
47
|
}, async ({ token, type, size }) => {
|
|
36
|
-
const
|
|
48
|
+
const search = type ? await searchType(token, type, size) : await searchAllTypes(token, size);
|
|
37
49
|
return {
|
|
38
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
50
|
+
content: [{ type: "text", text: JSON.stringify(search, null, 2) }],
|
|
39
51
|
};
|
|
40
52
|
});
|
|
41
53
|
server.tool("search_commits", "Search for SCM commits by keyword or commit message", {
|
|
42
54
|
token: z.string().describe("Search query (keyword or commit message fragment)"),
|
|
43
55
|
size: z.number().int().optional().default(10).describe("Max number of results"),
|
|
44
56
|
}, async ({ token, size }) => {
|
|
45
|
-
const
|
|
46
|
-
token,
|
|
47
|
-
type: "scm-commit",
|
|
48
|
-
size,
|
|
49
|
-
});
|
|
57
|
+
const search = await searchType(token, "scm-commit", size);
|
|
50
58
|
return {
|
|
51
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
59
|
+
content: [{ type: "text", text: JSON.stringify(search, null, 2) }],
|
|
52
60
|
};
|
|
53
61
|
});
|
|
54
62
|
server.tool("search_issues", "Search for SCM issues by keyword or issue key", {
|
|
55
63
|
token: z.string().describe("Search query (keyword or issue key)"),
|
|
56
64
|
size: z.number().int().optional().default(10).describe("Max number of results"),
|
|
57
65
|
}, async ({ token, size }) => {
|
|
58
|
-
const
|
|
59
|
-
token,
|
|
60
|
-
type: "scm-issue",
|
|
61
|
-
size,
|
|
62
|
-
});
|
|
66
|
+
const search = await searchType(token, "scm-issue", size);
|
|
63
67
|
return {
|
|
64
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify(search, null, 2) }],
|
|
65
69
|
};
|
|
66
70
|
});
|
|
67
71
|
}
|
|
@@ -9,41 +9,47 @@ const mockRequest = gqlClient.request;
|
|
|
9
9
|
beforeEach(() => {
|
|
10
10
|
mockRequest.mockReset();
|
|
11
11
|
});
|
|
12
|
+
const result = (id, title, accuracy) => ({
|
|
13
|
+
type: { id, name: id, description: null },
|
|
14
|
+
title,
|
|
15
|
+
description: null,
|
|
16
|
+
accuracy,
|
|
17
|
+
data: null,
|
|
18
|
+
});
|
|
19
|
+
async function search(args) {
|
|
20
|
+
const client = await createTestClient(registerSearchTools);
|
|
21
|
+
const result = await client.callTool({ name: "search", arguments: args });
|
|
22
|
+
return JSON.parse(result.content[0].text);
|
|
23
|
+
}
|
|
12
24
|
describe("search", () => {
|
|
13
|
-
it("
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
mockRequest.mockResolvedValueOnce({ search });
|
|
21
|
-
const client = await createTestClient(registerSearchTools);
|
|
22
|
-
const result = await client.callTool({
|
|
23
|
-
name: "search",
|
|
24
|
-
arguments: { token: "myproject" },
|
|
25
|
-
});
|
|
26
|
-
const text = result.content[0].text;
|
|
27
|
-
expect(JSON.parse(text)).toEqual(search);
|
|
25
|
+
it("searches the given type only", async () => {
|
|
26
|
+
const page = { pageItems: [result("build", "1.0.0", 1.0)], pageInfo: { totalSize: 1 } };
|
|
27
|
+
mockRequest.mockResolvedValueOnce({ search: page });
|
|
28
|
+
expect(await search({ token: "foo", type: "build", size: 5 })).toEqual(page);
|
|
29
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(mockRequest).toHaveBeenCalledWith(expect.any(String), { token: "foo", type: "build", size: 5 });
|
|
28
31
|
});
|
|
29
|
-
it("
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
it("searches every type when none is given, best results first", async () => {
|
|
33
|
+
mockRequest
|
|
34
|
+
.mockResolvedValueOnce({ searchResultTypes: [{ id: "project" }, { id: "build" }] })
|
|
35
|
+
.mockResolvedValueOnce({
|
|
36
|
+
search: { pageItems: [result("project", "p1", 0.5), result("project", "p2", 0.1)], pageInfo: { totalSize: 2 } },
|
|
37
|
+
})
|
|
38
|
+
.mockResolvedValueOnce({
|
|
39
|
+
search: { pageItems: [result("build", "b1", 0.9)], pageInfo: { totalSize: 7 } },
|
|
36
40
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
it("passes type and size filters to the GraphQL client", async () => {
|
|
41
|
-
mockRequest.mockResolvedValueOnce({ search: { pageItems: [], pageInfo: { totalSize: 0 } } });
|
|
42
|
-
const client = await createTestClient(registerSearchTools);
|
|
43
|
-
await client.callTool({
|
|
44
|
-
name: "search",
|
|
45
|
-
arguments: { token: "foo", type: "build", size: 5 },
|
|
41
|
+
expect(await search({ token: "foo", size: 2 })).toEqual({
|
|
42
|
+
pageItems: [result("build", "b1", 0.9), result("project", "p1", 0.5)],
|
|
43
|
+
pageInfo: { totalSize: 9 },
|
|
46
44
|
});
|
|
47
|
-
expect(mockRequest).toHaveBeenCalledWith(expect.
|
|
45
|
+
expect(mockRequest).toHaveBeenCalledWith(expect.stringContaining("searchResultTypes"));
|
|
46
|
+
expect(mockRequest).toHaveBeenCalledWith(expect.any(String), { token: "foo", type: "project", size: 2 });
|
|
47
|
+
expect(mockRequest).toHaveBeenCalledWith(expect.any(String), { token: "foo", type: "build", size: 2 });
|
|
48
|
+
});
|
|
49
|
+
it("returns empty results when nothing matches", async () => {
|
|
50
|
+
mockRequest
|
|
51
|
+
.mockResolvedValueOnce({ searchResultTypes: [{ id: "project" }] })
|
|
52
|
+
.mockResolvedValueOnce({ search: { pageItems: [], pageInfo: { totalSize: 0 } } });
|
|
53
|
+
expect(await search({ token: "zzznomatch" })).toEqual({ pageItems: [], pageInfo: { totalSize: 0 } });
|
|
48
54
|
});
|
|
49
55
|
});
|