yontrack-mcp 1.0.0

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.
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import { gqlClient } from "../client.js";
3
+ const GET_PROMOTION_RUNS = `
4
+ query GetPromotionRuns(
5
+ $project: String!
6
+ $branch: String!
7
+ $build: String!
8
+ $promotion: String
9
+ ) {
10
+ builds(project: $project, branch: $branch, name: $build) {
11
+ promotionRuns(promotion: $promotion) {
12
+ id
13
+ description
14
+ creation { user time }
15
+ promotionLevel { id name }
16
+ }
17
+ }
18
+ }
19
+ `;
20
+ const PROMOTE_BUILD = `
21
+ mutation PromoteBuild(
22
+ $project: String!
23
+ $branch: String!
24
+ $build: String!
25
+ $promotion: String!
26
+ $description: String
27
+ ) {
28
+ createPromotionRun(input: {
29
+ project: $project
30
+ branch: $branch
31
+ build: $build
32
+ promotion: $promotion
33
+ description: $description
34
+ }) {
35
+ promotionRun {
36
+ id
37
+ description
38
+ creation { user time }
39
+ promotionLevel { id name }
40
+ }
41
+ userErrors { message }
42
+ }
43
+ }
44
+ `;
45
+ export function registerPromotionRunTools(server, allowMutations) {
46
+ server.tool("get_promotion_runs", "Get promotion runs for a build, optionally filtered by promotion level", {
47
+ project: z.string().describe("Project name"),
48
+ branch: z.string().describe("Branch name"),
49
+ build: z.string().describe("Build name"),
50
+ promotion: z.string().optional().describe("Filter by promotion level name"),
51
+ }, async ({ project, branch, build, promotion }) => {
52
+ const data = await gqlClient.request(GET_PROMOTION_RUNS, { project, branch, build, promotion });
53
+ const runs = data.builds?.[0]?.promotionRuns ?? [];
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(runs, null, 2) }],
56
+ };
57
+ });
58
+ if (allowMutations)
59
+ server.tool("promote_build", "Promote a build to a promotion level", {
60
+ project: z.string().describe("Project name"),
61
+ branch: z.string().describe("Branch name"),
62
+ build: z.string().describe("Build name"),
63
+ promotion: z.string().describe("Promotion level name"),
64
+ description: z.string().optional().describe("Optional description"),
65
+ }, async ({ project, branch, build, promotion, description }) => {
66
+ const data = await gqlClient.request(PROMOTE_BUILD, { project, branch, build, promotion, description });
67
+ const { promotionRun, userErrors } = data.createPromotionRun;
68
+ if (userErrors?.length) {
69
+ return {
70
+ isError: true,
71
+ content: [{ type: "text", text: userErrors.map((e) => e.message).join(", ") }],
72
+ };
73
+ }
74
+ return {
75
+ content: [{ type: "text", text: JSON.stringify(promotionRun, null, 2) }],
76
+ };
77
+ });
78
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createTestClient } from "../test/helpers.js";
3
+ import { registerPromotionRunTools } from "./promotion-runs.js";
4
+ vi.mock("../client.js", () => ({
5
+ gqlClient: { request: vi.fn() },
6
+ }));
7
+ const { gqlClient } = await import("../client.js");
8
+ const mockRequest = gqlClient.request;
9
+ beforeEach(() => {
10
+ mockRequest.mockReset();
11
+ });
12
+ describe("get_promotion_runs", () => {
13
+ it("returns promotion runs for a build", async () => {
14
+ const runs = [
15
+ { id: 1, description: null, creation: { user: "alice", time: "2024-01-01T00:00:00Z" }, promotionLevel: { id: 1, name: "BRONZE" } },
16
+ ];
17
+ mockRequest.mockResolvedValueOnce({ builds: [{ promotionRuns: runs }] });
18
+ const client = await createTestClient(registerPromotionRunTools);
19
+ const result = await client.callTool({
20
+ name: "get_promotion_runs",
21
+ arguments: { project: "myproject", branch: "main", build: "1.0.1" },
22
+ });
23
+ const text = result.content[0].text;
24
+ expect(JSON.parse(text)).toEqual(runs);
25
+ });
26
+ it("returns empty array when build not found", async () => {
27
+ mockRequest.mockResolvedValueOnce({ builds: [] });
28
+ const client = await createTestClient(registerPromotionRunTools);
29
+ const result = await client.callTool({
30
+ name: "get_promotion_runs",
31
+ arguments: { project: "myproject", branch: "main", build: "missing" },
32
+ });
33
+ const text = result.content[0].text;
34
+ expect(JSON.parse(text)).toEqual([]);
35
+ });
36
+ it("passes the promotion filter to the GraphQL client", async () => {
37
+ mockRequest.mockResolvedValueOnce({ builds: [{ promotionRuns: [] }] });
38
+ const client = await createTestClient(registerPromotionRunTools);
39
+ await client.callTool({
40
+ name: "get_promotion_runs",
41
+ arguments: { project: "myproject", branch: "main", build: "1.0.1", promotion: "GOLD" },
42
+ });
43
+ expect(mockRequest).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ promotion: "GOLD" }));
44
+ });
45
+ });
46
+ describe("promote_build", () => {
47
+ it("returns the created promotion run", async () => {
48
+ const run = { id: 5, description: null, creation: { user: "alice", time: "2024-01-01T00:00:00Z" }, promotionLevel: { id: 1, name: "BRONZE" } };
49
+ mockRequest.mockResolvedValueOnce({ createPromotionRun: { promotionRun: run, userErrors: [] } });
50
+ const client = await createTestClient(registerPromotionRunTools);
51
+ const result = await client.callTool({
52
+ name: "promote_build",
53
+ arguments: { project: "myproject", branch: "main", build: "1.0.1", promotion: "BRONZE" },
54
+ });
55
+ expect(result.isError).toBeFalsy();
56
+ const text = result.content[0].text;
57
+ expect(JSON.parse(text)).toEqual(run);
58
+ });
59
+ it("returns an error when userErrors is non-empty", async () => {
60
+ mockRequest.mockResolvedValueOnce({
61
+ createPromotionRun: { promotionRun: null, userErrors: [{ message: "Build not found" }] },
62
+ });
63
+ const client = await createTestClient(registerPromotionRunTools);
64
+ const result = await client.callTool({
65
+ name: "promote_build",
66
+ arguments: { project: "myproject", branch: "main", build: "9.9.9", promotion: "BRONZE" },
67
+ });
68
+ expect(result.isError).toBe(true);
69
+ const text = result.content[0].text;
70
+ expect(text).toContain("Build not found");
71
+ });
72
+ it("is not registered when mutations are disabled", async () => {
73
+ const client = await createTestClient(registerPromotionRunTools, false);
74
+ const { tools } = await client.listTools();
75
+ expect(tools.map((t) => t.name)).not.toContain("promote_build");
76
+ });
77
+ it("is registered when mutations are enabled", async () => {
78
+ const client = await createTestClient(registerPromotionRunTools, true);
79
+ const { tools } = await client.listTools();
80
+ expect(tools.map((t) => t.name)).toContain("promote_build");
81
+ });
82
+ });
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { gqlClient } from "../client.js";
3
+ const SEARCH = `
4
+ query Search($token: String!, $type: String, $size: Int) {
5
+ search(token: $token, type: $type, size: $size) {
6
+ pageItems {
7
+ type { id name description }
8
+ title
9
+ description
10
+ accuracy
11
+ }
12
+ pageInfo { totalSize }
13
+ }
14
+ }
15
+ `;
16
+ export function registerSearchTools(server) {
17
+ server.tool("search", "Full-text search across all Yontrack entities (projects, branches, builds, etc.)", {
18
+ token: z.string().describe("Search query"),
19
+ type: z.string().optional().describe("Entity type filter (e.g. 'project', 'build')"),
20
+ size: z.number().int().optional().default(10).describe("Max number of results"),
21
+ }, async ({ token, type, size }) => {
22
+ const data = await gqlClient.request(SEARCH, { token, type, size });
23
+ return {
24
+ content: [{ type: "text", text: JSON.stringify(data.search, null, 2) }],
25
+ };
26
+ });
27
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createTestClient } from "../test/helpers.js";
3
+ import { registerSearchTools } from "./search.js";
4
+ vi.mock("../client.js", () => ({
5
+ gqlClient: { request: vi.fn() },
6
+ }));
7
+ const { gqlClient } = await import("../client.js");
8
+ const mockRequest = gqlClient.request;
9
+ beforeEach(() => {
10
+ mockRequest.mockReset();
11
+ });
12
+ describe("search", () => {
13
+ it("returns search results", async () => {
14
+ const search = {
15
+ pageItems: [
16
+ { type: { id: "project", name: "Project", description: null }, title: "myproject", description: null, accuracy: 1.0 },
17
+ ],
18
+ pageInfo: { totalSize: 1 },
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);
28
+ });
29
+ it("returns empty results when nothing matches", async () => {
30
+ const search = { pageItems: [], pageInfo: { totalSize: 0 } };
31
+ mockRequest.mockResolvedValueOnce({ search });
32
+ const client = await createTestClient(registerSearchTools);
33
+ const result = await client.callTool({
34
+ name: "search",
35
+ arguments: { token: "zzznomatch" },
36
+ });
37
+ const text = result.content[0].text;
38
+ expect(JSON.parse(text)).toEqual(search);
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 },
46
+ });
47
+ expect(mockRequest).toHaveBeenCalledWith(expect.any(String), { token: "foo", type: "build", size: 5 });
48
+ });
49
+ });
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ import { gqlClient } from "../client.js";
3
+ const GET_VALIDATION_RUNS = `
4
+ query GetValidationRuns(
5
+ $project: String!
6
+ $branch: String!
7
+ $build: String!
8
+ $validationStamp: String
9
+ $count: Int
10
+ ) {
11
+ builds(project: $project, branch: $branch, name: $build) {
12
+ validationRuns(validationStamp: $validationStamp, count: $count) {
13
+ id
14
+ runOrder
15
+ description
16
+ lastStatus { statusID { id name } description }
17
+ creation { user time }
18
+ validationStamp { id name }
19
+ }
20
+ }
21
+ }
22
+ `;
23
+ const CREATE_VALIDATION_RUN = `
24
+ mutation CreateValidationRun(
25
+ $project: String!
26
+ $branch: String!
27
+ $build: String!
28
+ $validationStamp: String!
29
+ $status: String!
30
+ $description: String
31
+ ) {
32
+ createValidationRun(input: {
33
+ project: $project
34
+ branch: $branch
35
+ build: $build
36
+ validationStamp: $validationStamp
37
+ validationRunStatus: $status
38
+ description: $description
39
+ }) {
40
+ validationRun {
41
+ id
42
+ runOrder
43
+ lastStatus { statusID { id name } }
44
+ }
45
+ userErrors { message }
46
+ }
47
+ }
48
+ `;
49
+ export function registerValidationRunTools(server, allowMutations) {
50
+ server.tool("get_validation_runs", "Get validation runs for a build, optionally filtered by validation stamp", {
51
+ project: z.string().describe("Project name"),
52
+ branch: z.string().describe("Branch name"),
53
+ build: z.string().describe("Build name"),
54
+ validationStamp: z.string().optional().describe("Filter by validation stamp name"),
55
+ count: z.number().int().optional().default(10).describe("Max number of runs to return"),
56
+ }, async ({ project, branch, build, validationStamp, count }) => {
57
+ const data = await gqlClient.request(GET_VALIDATION_RUNS, { project, branch, build, validationStamp, count });
58
+ const runs = data.builds?.[0]?.validationRuns ?? [];
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(runs, null, 2) }],
61
+ };
62
+ });
63
+ if (allowMutations)
64
+ server.tool("create_validation_run", "Create a validation run for a build with a status", {
65
+ project: z.string().describe("Project name"),
66
+ branch: z.string().describe("Branch name"),
67
+ build: z.string().describe("Build name"),
68
+ validationStamp: z.string().describe("Validation stamp name"),
69
+ status: z
70
+ .enum(["PASSED", "FAILED", "WARNING", "DEFECTIVE", "INTERRUPTED", "INVESTIGATED"])
71
+ .describe("Validation status"),
72
+ description: z.string().optional().describe("Optional description or notes"),
73
+ }, async ({ project, branch, build, validationStamp, status, description }) => {
74
+ const data = await gqlClient.request(CREATE_VALIDATION_RUN, {
75
+ project,
76
+ branch,
77
+ build,
78
+ validationStamp,
79
+ status,
80
+ description,
81
+ });
82
+ const { validationRun, userErrors } = data.createValidationRun;
83
+ if (userErrors?.length) {
84
+ return {
85
+ isError: true,
86
+ content: [{ type: "text", text: userErrors.map((e) => e.message).join(", ") }],
87
+ };
88
+ }
89
+ return {
90
+ content: [{ type: "text", text: JSON.stringify(validationRun, null, 2) }],
91
+ };
92
+ });
93
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createTestClient } from "../test/helpers.js";
3
+ import { registerValidationRunTools } from "./validation-runs.js";
4
+ vi.mock("../client.js", () => ({
5
+ gqlClient: { request: vi.fn() },
6
+ }));
7
+ const { gqlClient } = await import("../client.js");
8
+ const mockRequest = gqlClient.request;
9
+ beforeEach(() => {
10
+ mockRequest.mockReset();
11
+ });
12
+ describe("get_validation_runs", () => {
13
+ it("returns validation runs for a build", async () => {
14
+ const runs = [
15
+ { id: 1, runOrder: 1, description: null, lastStatus: { statusID: { id: "PASSED", name: "Passed" }, description: null }, creation: { user: "alice", time: "2024-01-01T00:00:00Z" }, validationStamp: { id: 1, name: "CI" } },
16
+ ];
17
+ mockRequest.mockResolvedValueOnce({ builds: [{ validationRuns: runs }] });
18
+ const client = await createTestClient(registerValidationRunTools);
19
+ const result = await client.callTool({
20
+ name: "get_validation_runs",
21
+ arguments: { project: "myproject", branch: "main", build: "1.0.1" },
22
+ });
23
+ const text = result.content[0].text;
24
+ expect(JSON.parse(text)).toEqual(runs);
25
+ });
26
+ it("returns empty array when build not found", async () => {
27
+ mockRequest.mockResolvedValueOnce({ builds: [] });
28
+ const client = await createTestClient(registerValidationRunTools);
29
+ const result = await client.callTool({
30
+ name: "get_validation_runs",
31
+ arguments: { project: "myproject", branch: "main", build: "missing" },
32
+ });
33
+ const text = result.content[0].text;
34
+ expect(JSON.parse(text)).toEqual([]);
35
+ });
36
+ it("passes the validationStamp filter to the GraphQL client", async () => {
37
+ mockRequest.mockResolvedValueOnce({ builds: [{ validationRuns: [] }] });
38
+ const client = await createTestClient(registerValidationRunTools);
39
+ await client.callTool({
40
+ name: "get_validation_runs",
41
+ arguments: { project: "myproject", branch: "main", build: "1.0.1", validationStamp: "CI" },
42
+ });
43
+ expect(mockRequest).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ validationStamp: "CI" }));
44
+ });
45
+ });
46
+ describe("create_validation_run", () => {
47
+ it("returns the created validation run", async () => {
48
+ const run = { id: 10, runOrder: 1, lastStatus: { statusID: { id: "PASSED", name: "Passed" } } };
49
+ mockRequest.mockResolvedValueOnce({ createValidationRun: { validationRun: run, userErrors: [] } });
50
+ const client = await createTestClient(registerValidationRunTools);
51
+ const result = await client.callTool({
52
+ name: "create_validation_run",
53
+ arguments: { project: "myproject", branch: "main", build: "1.0.1", validationStamp: "CI", status: "PASSED" },
54
+ });
55
+ expect(result.isError).toBeFalsy();
56
+ const text = result.content[0].text;
57
+ expect(JSON.parse(text)).toEqual(run);
58
+ });
59
+ it("returns an error when userErrors is non-empty", async () => {
60
+ mockRequest.mockResolvedValueOnce({
61
+ createValidationRun: { validationRun: null, userErrors: [{ message: "Validation stamp not found" }] },
62
+ });
63
+ const client = await createTestClient(registerValidationRunTools);
64
+ const result = await client.callTool({
65
+ name: "create_validation_run",
66
+ arguments: { project: "myproject", branch: "main", build: "1.0.1", validationStamp: "MISSING", status: "FAILED" },
67
+ });
68
+ expect(result.isError).toBe(true);
69
+ const text = result.content[0].text;
70
+ expect(text).toContain("Validation stamp not found");
71
+ });
72
+ it("is not registered when mutations are disabled", async () => {
73
+ const client = await createTestClient(registerValidationRunTools, false);
74
+ const { tools } = await client.listTools();
75
+ expect(tools.map((t) => t.name)).not.toContain("create_validation_run");
76
+ });
77
+ it("is registered when mutations are enabled", async () => {
78
+ const client = await createTestClient(registerValidationRunTools, true);
79
+ const { tools } = await client.listTools();
80
+ expect(tools.map((t) => t.name)).toContain("create_validation_run");
81
+ });
82
+ });
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { gqlClient } from "../client.js";
3
+ import { resolveBranchId } from "../utils.js";
4
+ const LIST_VALIDATION_STAMPS = `
5
+ query ListValidationStamps($project: String!, $branch: String!, $name: String) {
6
+ branches(project: $project, name: $branch) {
7
+ validationStamps(name: $name) {
8
+ id
9
+ name
10
+ description
11
+ }
12
+ }
13
+ }
14
+ `;
15
+ const CREATE_VALIDATION_STAMP = `
16
+ mutation CreateValidationStampById(
17
+ $branchId: Int!
18
+ $name: String!
19
+ $description: String!
20
+ ) {
21
+ createValidationStampById(input: {
22
+ branchId: $branchId
23
+ name: $name
24
+ description: $description
25
+ }) {
26
+ validationStamp { id name description }
27
+ userErrors { message }
28
+ }
29
+ }
30
+ `;
31
+ export function registerValidationStampTools(server, allowMutations) {
32
+ server.tool("list_validation_stamps", "List validation stamps for a branch", {
33
+ project: z.string().describe("Project name"),
34
+ branch: z.string().describe("Branch name"),
35
+ name: z.string().optional().describe("Filter by stamp name"),
36
+ }, async ({ project, branch, name }) => {
37
+ const data = await gqlClient.request(LIST_VALIDATION_STAMPS, { project, branch, name });
38
+ const stamps = data.branches?.[0]?.validationStamps ?? [];
39
+ return {
40
+ content: [{ type: "text", text: JSON.stringify(stamps, null, 2) }],
41
+ };
42
+ });
43
+ if (allowMutations)
44
+ server.tool("create_validation_stamp", "Create a new validation stamp on a branch", {
45
+ project: z.string().describe("Project name"),
46
+ branch: z.string().describe("Branch name"),
47
+ name: z.string().describe("Validation stamp name"),
48
+ description: z.string().optional().default("").describe("Description"),
49
+ }, async ({ project, branch, name, description }) => {
50
+ const branchId = await resolveBranchId(project, branch);
51
+ const data = await gqlClient.request(CREATE_VALIDATION_STAMP, { branchId, name, description });
52
+ const { validationStamp, userErrors } = data.createValidationStampById;
53
+ if (userErrors?.length) {
54
+ return {
55
+ isError: true,
56
+ content: [{ type: "text", text: userErrors.map((e) => e.message).join(", ") }],
57
+ };
58
+ }
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(validationStamp, null, 2) }],
61
+ };
62
+ });
63
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createTestClient } from "../test/helpers.js";
3
+ import { registerValidationStampTools } from "./validation-stamps.js";
4
+ vi.mock("../client.js", () => ({
5
+ gqlClient: { request: vi.fn() },
6
+ }));
7
+ vi.mock("../utils.js", () => ({
8
+ resolveBranchId: vi.fn().mockResolvedValue(42),
9
+ }));
10
+ const { gqlClient } = await import("../client.js");
11
+ const mockRequest = gqlClient.request;
12
+ beforeEach(() => {
13
+ mockRequest.mockReset();
14
+ });
15
+ describe("list_validation_stamps", () => {
16
+ it("returns validation stamps for a branch", async () => {
17
+ const stamps = [
18
+ { id: 1, name: "CI", description: "Continuous integration" },
19
+ ];
20
+ mockRequest.mockResolvedValueOnce({ branches: [{ validationStamps: stamps }] });
21
+ const client = await createTestClient(registerValidationStampTools);
22
+ const result = await client.callTool({
23
+ name: "list_validation_stamps",
24
+ arguments: { project: "myproject", branch: "main" },
25
+ });
26
+ const text = result.content[0].text;
27
+ expect(JSON.parse(text)).toEqual(stamps);
28
+ });
29
+ it("returns empty array when branch not found", async () => {
30
+ mockRequest.mockResolvedValueOnce({ branches: [] });
31
+ const client = await createTestClient(registerValidationStampTools);
32
+ const result = await client.callTool({
33
+ name: "list_validation_stamps",
34
+ arguments: { project: "myproject", branch: "missing" },
35
+ });
36
+ const text = result.content[0].text;
37
+ expect(JSON.parse(text)).toEqual([]);
38
+ });
39
+ });
40
+ describe("create_validation_stamp", () => {
41
+ it("returns the created validation stamp", async () => {
42
+ const stamp = { id: 5, name: "SECURITY", description: "Security scan" };
43
+ mockRequest.mockResolvedValueOnce({ createValidationStampById: { validationStamp: stamp, userErrors: [] } });
44
+ const client = await createTestClient(registerValidationStampTools);
45
+ const result = await client.callTool({
46
+ name: "create_validation_stamp",
47
+ arguments: { project: "myproject", branch: "main", name: "SECURITY", description: "Security scan" },
48
+ });
49
+ expect(result.isError).toBeFalsy();
50
+ const text = result.content[0].text;
51
+ expect(JSON.parse(text)).toEqual(stamp);
52
+ });
53
+ it("returns an error when userErrors is non-empty", async () => {
54
+ mockRequest.mockResolvedValueOnce({
55
+ createValidationStampById: { validationStamp: null, userErrors: [{ message: "Stamp already exists" }] },
56
+ });
57
+ const client = await createTestClient(registerValidationStampTools);
58
+ const result = await client.callTool({
59
+ name: "create_validation_stamp",
60
+ arguments: { project: "myproject", branch: "main", name: "CI" },
61
+ });
62
+ expect(result.isError).toBe(true);
63
+ const text = result.content[0].text;
64
+ expect(text).toContain("Stamp already exists");
65
+ });
66
+ it("is not registered when mutations are disabled", async () => {
67
+ const client = await createTestClient(registerValidationStampTools, false);
68
+ const { tools } = await client.listTools();
69
+ expect(tools.map((t) => t.name)).not.toContain("create_validation_stamp");
70
+ });
71
+ it("is registered when mutations are enabled", async () => {
72
+ const client = await createTestClient(registerValidationStampTools, true);
73
+ const { tools } = await client.listTools();
74
+ expect(tools.map((t) => t.name)).toContain("create_validation_stamp");
75
+ });
76
+ });
package/build/utils.js ADDED
@@ -0,0 +1,16 @@
1
+ import { gqlClient } from "./client.js";
2
+ const GET_BRANCH_ID = `
3
+ query GetBranchId($project: String!, $name: String!) {
4
+ branches(project: $project, name: $name) {
5
+ id
6
+ }
7
+ }
8
+ `;
9
+ export async function resolveBranchId(project, branch) {
10
+ const data = await gqlClient.request(GET_BRANCH_ID, { project, name: branch });
11
+ const found = data.branches?.[0];
12
+ if (!found) {
13
+ throw new Error(`Branch '${branch}' not found in project '${project}'`);
14
+ }
15
+ return found.id;
16
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "yontrack-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Yontrack (Ontrack CI/CD monitoring platform)",
5
+ "type": "module",
6
+ "bin": {
7
+ "yontrack-mcp": "./build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && chmod 755 build/index.js",
11
+ "dev": "tsx watch src/index.ts",
12
+ "start": "node build/index.js",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest"
16
+ },
17
+ "files": [
18
+ "build",
19
+ "yontrack.graphql"
20
+ ],
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.10.2",
23
+ "express": "^5.2.1",
24
+ "graphql-request": "^7.1.2",
25
+ "zod": "^3.24.0"
26
+ },
27
+ "devDependencies": {
28
+ "@semantic-release/changelog": "^6.0.3",
29
+ "@semantic-release/git": "^10.0.1",
30
+ "@types/express": "^5.0.6",
31
+ "@types/node": "^22.0.0",
32
+ "semantic-release": "^24.2.9",
33
+ "tsx": "^4.19.0",
34
+ "typescript": "^5.7.0",
35
+ "vitest": "^3.2.4"
36
+ }
37
+ }