@daghis/teamcity-mcp 1.10.1 → 1.10.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.10.1"
2
+ ".": "1.10.3"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.3](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.10.2...teamcity-mcp-v1.10.3) (2025-09-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **tools:** honor trigger_build branch overrides (210) ([#223](https://github.com/Daghis/teamcity-mcp/issues/223)) ([7222c28](https://github.com/Daghis/teamcity-mcp/commit/7222c28c4fc9a307222ee9a50fa518127f5187de))
9
+
10
+ ## [1.10.2](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.10.1...teamcity-mcp-v1.10.2) (2025-09-27)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **tools:** clone_build_config uses manager (215) ([b84d1f8](https://github.com/Daghis/teamcity-mcp/commit/b84d1f80a4233783a93dd1e3ede9a83a7cf57171))
16
+ * **tools:** clone_build_config uses manager (215) ([c4cd959](https://github.com/Daghis/teamcity-mcp/commit/c4cd959a9f35052bf95386162316a9ace5599eb6))
17
+
3
18
  ## [1.10.1](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.10.0...teamcity-mcp-v1.10.1) (2025-09-27)
4
19
 
5
20
 
package/dist/index.js CHANGED
@@ -1516,6 +1516,384 @@ var ArtifactManager = class _ArtifactManager {
1516
1516
  }
1517
1517
  };
1518
1518
 
1519
+ // src/teamcity/types/api-responses.ts
1520
+ function isBuildTypeData(data) {
1521
+ return typeof data === "object" && data !== null && ("id" in data || "name" in data || "projectId" in data);
1522
+ }
1523
+ function isVcsRootsResponse(data) {
1524
+ return typeof data === "object" && data !== null && "vcs-root" in data;
1525
+ }
1526
+
1527
+ // src/teamcity/build-configuration-clone-manager.ts
1528
+ var BuildConfigurationCloneManager = class {
1529
+ client;
1530
+ constructor(client) {
1531
+ this.client = client;
1532
+ }
1533
+ /**
1534
+ * Retrieve complete build configuration from TeamCity
1535
+ */
1536
+ async retrieveConfiguration(configId) {
1537
+ try {
1538
+ const response = await this.client.modules.buildTypes.getBuildType(
1539
+ configId,
1540
+ "$long,steps($long),triggers($long),features($long),artifact-dependencies($long),snapshot-dependencies($long),parameters($long),vcs-root-entries($long)"
1541
+ );
1542
+ if (response.data == null || !isBuildTypeData(response.data)) {
1543
+ return null;
1544
+ }
1545
+ const config2 = response.data;
1546
+ let vcsRootId;
1547
+ const vcsRootEntries = config2["vcs-root-entries"];
1548
+ if (vcsRootEntries?.["vcs-root-entry"] && vcsRootEntries["vcs-root-entry"].length > 0) {
1549
+ const firstEntry = vcsRootEntries["vcs-root-entry"][0];
1550
+ if (firstEntry?.["vcs-root"]?.id) {
1551
+ vcsRootId = firstEntry["vcs-root"].id;
1552
+ }
1553
+ }
1554
+ const parameters = {};
1555
+ if (config2.parameters?.property) {
1556
+ for (const param of config2.parameters.property) {
1557
+ if (param.name && param.value) {
1558
+ parameters[param.name] = param.value;
1559
+ }
1560
+ }
1561
+ }
1562
+ const cfgId = config2.id;
1563
+ const cfgName = config2.name;
1564
+ if (!cfgId || !cfgName) {
1565
+ throw new Error("Source configuration missing id or name");
1566
+ }
1567
+ return {
1568
+ id: cfgId,
1569
+ name: cfgName,
1570
+ projectId: config2.projectId ?? config2.project?.id ?? "",
1571
+ description: config2.description,
1572
+ vcsRootId,
1573
+ parameters,
1574
+ templateId: config2.templates?.buildType?.[0]?.id,
1575
+ steps: config2.steps?.step,
1576
+ triggers: config2.triggers?.trigger,
1577
+ features: config2.features?.feature,
1578
+ artifactDependencies: config2["artifact-dependencies"]?.["artifact-dependency"],
1579
+ snapshotDependencies: config2["snapshot-dependencies"]?.["snapshot-dependency"],
1580
+ buildNumberCounter: (() => {
1581
+ const counterProp = config2.settings?.property?.find(
1582
+ (p) => p.name === "buildNumberCounter"
1583
+ );
1584
+ return counterProp?.value ? parseInt(counterProp.value, 10) : void 0;
1585
+ })(),
1586
+ buildNumberFormat: config2.settings?.property?.find(
1587
+ (p) => p.name === "buildNumberPattern"
1588
+ )?.value
1589
+ };
1590
+ } catch (err) {
1591
+ const axiosError = err;
1592
+ if (axiosError.response?.status === 404) {
1593
+ debug("Build configuration not found", { configId });
1594
+ return null;
1595
+ }
1596
+ if (axiosError.response?.status === 403) {
1597
+ throw new Error("Permission denied: No access to source configuration");
1598
+ }
1599
+ throw err;
1600
+ }
1601
+ }
1602
+ /**
1603
+ * Validate target project exists and user has permissions
1604
+ */
1605
+ async validateTargetProject(projectId) {
1606
+ try {
1607
+ const response = await this.client.modules.projects.getProject(projectId, "$short");
1608
+ const id = response.data?.id;
1609
+ const name = response.data?.name;
1610
+ if (id && name) {
1611
+ return { id, name };
1612
+ }
1613
+ return null;
1614
+ } catch (err) {
1615
+ const axiosError = err;
1616
+ if (axiosError.response?.status === 404) {
1617
+ debug("Target project not found", { projectId });
1618
+ return null;
1619
+ }
1620
+ if (axiosError.response?.status === 403) {
1621
+ debug("No permission to access target project", { projectId });
1622
+ return null;
1623
+ }
1624
+ throw err;
1625
+ }
1626
+ }
1627
+ /**
1628
+ * Handle VCS root cloning or reuse
1629
+ */
1630
+ async handleVcsRoot(vcsRootId, handling, targetProjectId) {
1631
+ if (handling === "reuse") {
1632
+ return { id: vcsRootId, name: "Reused VCS Root" };
1633
+ }
1634
+ try {
1635
+ const vcsRootsResponse = await this.client.modules.vcsRoots.getAllVcsRoots(
1636
+ `id:${vcsRootId}`,
1637
+ "$long,vcsRoot($long,properties($long))"
1638
+ );
1639
+ if (vcsRootsResponse.data == null || !isVcsRootsResponse(vcsRootsResponse.data)) {
1640
+ throw new Error("Invalid VCS root response");
1641
+ }
1642
+ const vcsRoots = vcsRootsResponse.data["vcs-root"] ?? [];
1643
+ if (vcsRoots.length === 0) {
1644
+ throw new Error("VCS root not found");
1645
+ }
1646
+ const sourceVcsRoot = vcsRoots[0];
1647
+ if (sourceVcsRoot == null) {
1648
+ throw new Error("VCS root data is invalid");
1649
+ }
1650
+ const clonedVcsRootName = `${sourceVcsRoot.name}_Clone_${Date.now()}`;
1651
+ const clonedVcsRoot = {
1652
+ name: clonedVcsRootName,
1653
+ vcsName: sourceVcsRoot.vcsName,
1654
+ project: {
1655
+ id: targetProjectId
1656
+ },
1657
+ properties: sourceVcsRoot.properties
1658
+ };
1659
+ const createResponse = await this.client.modules.vcsRoots.addVcsRoot(
1660
+ void 0,
1661
+ clonedVcsRoot
1662
+ );
1663
+ const newId = createResponse.data.id;
1664
+ const newName = createResponse.data.name;
1665
+ if (!newId || !newName) {
1666
+ throw new Error("Failed to obtain cloned VCS root id/name");
1667
+ }
1668
+ return { id: newId, name: newName };
1669
+ } catch (err) {
1670
+ error("Failed to clone VCS root", err);
1671
+ throw new Error(`Failed to clone VCS root: ${err.message}`);
1672
+ }
1673
+ }
1674
+ /**
1675
+ * Apply parameter overrides to configuration
1676
+ */
1677
+ async applyParameterOverrides(sourceParameters, overrides) {
1678
+ const mergedParameters = { ...sourceParameters };
1679
+ for (const [key, value] of Object.entries(overrides)) {
1680
+ if (!this.isValidParameterName(key)) {
1681
+ throw new Error(`Invalid parameter name: ${key}`);
1682
+ }
1683
+ mergedParameters[key] = value;
1684
+ }
1685
+ return mergedParameters;
1686
+ }
1687
+ /**
1688
+ * Clone the build configuration
1689
+ */
1690
+ async cloneConfiguration(source, options) {
1691
+ const configId = options.id ?? this.generateBuildConfigId(options.targetProjectId, options.name);
1692
+ const configPayload = {
1693
+ id: configId,
1694
+ name: options.name,
1695
+ project: {
1696
+ id: options.targetProjectId
1697
+ }
1698
+ };
1699
+ if (options.description) {
1700
+ configPayload.description = options.description;
1701
+ }
1702
+ if (source.templateId) {
1703
+ configPayload.templates = {
1704
+ buildType: [{ id: source.templateId }]
1705
+ };
1706
+ }
1707
+ if (options.vcsRootId) {
1708
+ configPayload["vcs-root-entries"] = {
1709
+ "vcs-root-entry": [
1710
+ {
1711
+ "vcs-root": { id: options.vcsRootId },
1712
+ "checkout-rules": ""
1713
+ }
1714
+ ]
1715
+ };
1716
+ }
1717
+ if (source.steps && source.steps.length > 0) {
1718
+ configPayload.steps = {
1719
+ step: this.cloneBuildSteps(source.steps)
1720
+ };
1721
+ }
1722
+ if (source.triggers && source.triggers.length > 0) {
1723
+ configPayload.triggers = {
1724
+ trigger: this.cloneTriggers(source.triggers)
1725
+ };
1726
+ }
1727
+ if (source.features && source.features.length > 0) {
1728
+ configPayload.features = {
1729
+ feature: source.features.map((f) => this.deepCloneConfiguration(f))
1730
+ };
1731
+ }
1732
+ if (source.artifactDependencies && source.artifactDependencies.length > 0) {
1733
+ configPayload["artifact-dependencies"] = {
1734
+ "artifact-dependency": this.updateDependencyReferences(
1735
+ source.artifactDependencies,
1736
+ source.id,
1737
+ configId
1738
+ )
1739
+ };
1740
+ }
1741
+ if (source.snapshotDependencies && source.snapshotDependencies.length > 0) {
1742
+ configPayload["snapshot-dependencies"] = {
1743
+ "snapshot-dependency": this.updateDependencyReferences(
1744
+ source.snapshotDependencies,
1745
+ source.id,
1746
+ configId
1747
+ )
1748
+ };
1749
+ }
1750
+ if (options.parameters && Object.keys(options.parameters).length > 0) {
1751
+ configPayload.parameters = {
1752
+ property: Object.entries(options.parameters).map(([name, value]) => ({
1753
+ name,
1754
+ value
1755
+ }))
1756
+ };
1757
+ }
1758
+ if (options.copyBuildCounter && source.buildNumberCounter) {
1759
+ if (configPayload.settings == null) {
1760
+ configPayload.settings = { property: [] };
1761
+ }
1762
+ configPayload.settings.property?.push({
1763
+ name: "buildNumberCounter",
1764
+ value: source.buildNumberCounter.toString()
1765
+ });
1766
+ }
1767
+ if (source.buildNumberFormat) {
1768
+ if (configPayload.settings == null) {
1769
+ configPayload.settings = { property: [] };
1770
+ }
1771
+ configPayload.settings.property?.push({
1772
+ name: "buildNumberPattern",
1773
+ value: source.buildNumberFormat
1774
+ });
1775
+ }
1776
+ try {
1777
+ const response = await this.client.modules.buildTypes.createBuildType(
1778
+ void 0,
1779
+ this.prepareBuildTypePayload(configPayload)
1780
+ );
1781
+ const teamcityUrl = getTeamCityUrl();
1782
+ const id = response.data.id;
1783
+ const name = response.data.name;
1784
+ if (!id || !name) {
1785
+ throw new Error("Clone response missing id or name");
1786
+ }
1787
+ const result = {
1788
+ id,
1789
+ name,
1790
+ projectId: response.data.projectId ?? options.targetProjectId,
1791
+ description: response.data.description,
1792
+ vcsRootId: options.vcsRootId,
1793
+ parameters: options.parameters,
1794
+ url: `${teamcityUrl}/viewType.html?buildTypeId=${id}`
1795
+ };
1796
+ info("Build configuration cloned", {
1797
+ id: result.id,
1798
+ name: result.name,
1799
+ sourceId: source.id
1800
+ });
1801
+ return result;
1802
+ } catch (err) {
1803
+ const error2 = err;
1804
+ if (error2.response?.status === 409) {
1805
+ throw new Error(`Build configuration already exists with ID: ${configId}`);
1806
+ }
1807
+ if (error2.response?.status === 403) {
1808
+ throw new Error("Permission denied: You need project edit permissions");
1809
+ }
1810
+ if (error2.response?.status === 400) {
1811
+ const message = error2.response?.data?.message ?? "Invalid configuration";
1812
+ throw new Error(`Invalid configuration: ${message}`);
1813
+ }
1814
+ error(
1815
+ "Failed to clone build configuration",
1816
+ error2 instanceof Error ? error2 : new Error(String(error2))
1817
+ );
1818
+ throw error2;
1819
+ }
1820
+ }
1821
+ /**
1822
+ * Normalize cloned payload into the generated BuildType shape expected by the API
1823
+ */
1824
+ prepareBuildTypePayload(payload) {
1825
+ const clone = typeof structuredClone === "function" ? structuredClone(payload) : JSON.parse(JSON.stringify(payload));
1826
+ if (typeof clone.id !== "string" || typeof clone.name !== "string") {
1827
+ throw new Error("Invalid build configuration payload: missing id or name");
1828
+ }
1829
+ if (typeof clone.project?.id !== "string") {
1830
+ throw new Error("Invalid build configuration payload: missing project id");
1831
+ }
1832
+ return clone;
1833
+ }
1834
+ /**
1835
+ * Deep clone configuration object and remove server-generated fields
1836
+ */
1837
+ deepCloneConfiguration(config2) {
1838
+ const cloned = JSON.parse(JSON.stringify(config2));
1839
+ delete cloned.href;
1840
+ delete cloned.webUrl;
1841
+ delete cloned.locator;
1842
+ delete cloned.uuid;
1843
+ delete cloned.links;
1844
+ delete cloned._links;
1845
+ return cloned;
1846
+ }
1847
+ /**
1848
+ * Clone build steps with new IDs
1849
+ */
1850
+ cloneBuildSteps(steps) {
1851
+ return steps.map((step, index) => {
1852
+ const clonedStep = this.deepCloneConfiguration(step);
1853
+ clonedStep.id = `RUNNER_${index + 1}`;
1854
+ return clonedStep;
1855
+ });
1856
+ }
1857
+ /**
1858
+ * Clone triggers with new IDs
1859
+ */
1860
+ cloneTriggers(triggers) {
1861
+ return triggers.map((trigger, index) => {
1862
+ const clonedTrigger = this.deepCloneConfiguration(trigger);
1863
+ clonedTrigger.id = `TRIGGER_${index + 1}`;
1864
+ return clonedTrigger;
1865
+ });
1866
+ }
1867
+ /**
1868
+ * Update internal references in dependencies
1869
+ */
1870
+ updateDependencyReferences(dependencies, oldId, newId) {
1871
+ return dependencies.map((dep) => {
1872
+ const clonedDep = this.deepCloneConfiguration(dep);
1873
+ if (clonedDep.sourceBuildTypeId === oldId) {
1874
+ clonedDep.sourceBuildTypeId = newId;
1875
+ }
1876
+ if (clonedDep.dependsOnBuildTypeId === oldId) {
1877
+ clonedDep.dependsOnBuildTypeId = newId;
1878
+ }
1879
+ return clonedDep;
1880
+ });
1881
+ }
1882
+ /**
1883
+ * Generate a unique build configuration ID
1884
+ */
1885
+ generateBuildConfigId(projectId, name) {
1886
+ const cleanName = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1887
+ return `${projectId}_${cleanName}`;
1888
+ }
1889
+ /**
1890
+ * Validate parameter name according to TeamCity rules
1891
+ */
1892
+ isValidParameterName(name) {
1893
+ return /^[a-zA-Z0-9._-]+$/.test(name);
1894
+ }
1895
+ };
1896
+
1519
1897
  // src/teamcity/build-configuration-update-manager.ts
1520
1898
  var ARTIFACT_RULES_SETTINGS_FIELD = "settings/artifactRules";
1521
1899
  var ARTIFACT_RULES_LEGACY_FIELD = "artifactRules";
@@ -38171,7 +38549,12 @@ var DEV_TOOLS = [
38171
38549
  properties: {
38172
38550
  buildTypeId: { type: "string", description: "Build type ID to trigger" },
38173
38551
  branchName: { type: "string", description: "Branch to build (optional)" },
38174
- comment: { type: "string", description: "Build comment (optional)" }
38552
+ comment: { type: "string", description: "Build comment (optional)" },
38553
+ properties: {
38554
+ type: "object",
38555
+ description: "Optional build parameters to set when triggering the build",
38556
+ additionalProperties: { type: "string" }
38557
+ }
38175
38558
  },
38176
38559
  required: ["buildTypeId"]
38177
38560
  },
@@ -38179,35 +38562,84 @@ var DEV_TOOLS = [
38179
38562
  const schema = import_zod4.z.object({
38180
38563
  buildTypeId: import_zod4.z.string().min(1),
38181
38564
  branchName: import_zod4.z.string().min(1).max(255).optional(),
38182
- comment: import_zod4.z.string().max(500).optional()
38565
+ comment: import_zod4.z.string().max(500).optional(),
38566
+ properties: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string()).optional()
38183
38567
  });
38184
38568
  return runTool(
38185
38569
  "trigger_build",
38186
38570
  schema,
38187
38571
  async (typed) => {
38188
38572
  const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
38189
- try {
38190
- const build = await adapter.triggerBuild(
38191
- typed.buildTypeId,
38192
- typed.branchName,
38193
- typed.comment
38573
+ const directBranch = typed.branchName?.trim();
38574
+ const normalizedDirectBranch = directBranch && directBranch.length > 0 ? directBranch : void 0;
38575
+ const rawPropertyBranch = typed.properties?.["teamcity.build.branch"];
38576
+ const trimmedPropertyBranch = rawPropertyBranch?.trim();
38577
+ const normalizedPropertyBranch = trimmedPropertyBranch && trimmedPropertyBranch.length > 0 ? trimmedPropertyBranch : void 0;
38578
+ const branchName = normalizedDirectBranch ?? normalizedPropertyBranch;
38579
+ if (normalizedDirectBranch && normalizedPropertyBranch && normalizedDirectBranch !== normalizedPropertyBranch) {
38580
+ const errorPayload = {
38581
+ success: false,
38582
+ action: "trigger_build",
38583
+ error: `Conflicting branch overrides: branchName='${normalizedDirectBranch}' vs properties.teamcity.build.branch='${normalizedPropertyBranch}'.`
38584
+ };
38585
+ return {
38586
+ success: false,
38587
+ error: errorPayload.error,
38588
+ content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }]
38589
+ };
38590
+ }
38591
+ const propertyEntries = typed.properties ? Object.entries(typed.properties).map(([name, value]) => ({
38592
+ name,
38593
+ value: name === "teamcity.build.branch" && normalizedPropertyBranch ? normalizedPropertyBranch : value
38594
+ })) : [];
38595
+ const propertiesPayload = propertyEntries.length > 0 ? { property: propertyEntries } : void 0;
38596
+ const buildRequest = {
38597
+ buildType: { id: typed.buildTypeId }
38598
+ };
38599
+ if (branchName) {
38600
+ buildRequest.branchName = branchName;
38601
+ }
38602
+ const commentText = typed.comment?.trim();
38603
+ if (commentText && commentText.length > 0) {
38604
+ buildRequest.comment = { text: commentText };
38605
+ }
38606
+ if (propertiesPayload) {
38607
+ buildRequest.properties = propertiesPayload;
38608
+ }
38609
+ const sendXmlFallback = async (error2) => {
38610
+ const escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
38611
+ const branchPart = branchName ? `<branchName>${escapeXml(branchName)}</branchName>` : "";
38612
+ const commentPart = commentText ? `<comment><text>${escapeXml(commentText)}</text></comment>` : "";
38613
+ const propertiesPart = propertiesPayload ? `<properties>${propertiesPayload.property.map(
38614
+ (prop) => `<property name="${escapeXml(prop.name)}" value="${escapeXml(prop.value)}"/>`
38615
+ ).join("")}</properties>` : "";
38616
+ const xml = `<?xml version="1.0" encoding="UTF-8"?><build><buildType id="${escapeXml(
38617
+ typed.buildTypeId
38618
+ )}"/>${branchPart}${commentPart}${propertiesPart}</build>`;
38619
+ const response = await adapter.modules.buildQueue.addBuildToQueue(
38620
+ false,
38621
+ xml,
38622
+ {
38623
+ headers: { "Content-Type": "application/xml", Accept: "application/json" }
38624
+ }
38194
38625
  );
38626
+ const build = response.data;
38195
38627
  return json({
38196
38628
  success: true,
38197
38629
  action: "trigger_build",
38198
38630
  buildId: String(build.id ?? ""),
38199
38631
  state: build.state ?? void 0,
38200
- status: build.status ?? void 0
38632
+ status: build.status ?? void 0,
38633
+ branchName: build.branchName ?? branchName,
38634
+ fallback: { mode: "xml", reason: error2?.message }
38201
38635
  });
38202
- } catch (e) {
38203
- const branchPart = typed.branchName ? `<branchName>${typed.branchName}</branchName>` : "";
38204
- const commentPart = typed.comment ? `<comment><text>${typed.comment.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</text></comment>` : "";
38205
- const xml = `<?xml version="1.0" encoding="UTF-8"?><build><buildType id="${typed.buildTypeId}"/>${branchPart}${commentPart}</build>`;
38636
+ };
38637
+ try {
38206
38638
  const response = await adapter.modules.buildQueue.addBuildToQueue(
38207
38639
  false,
38208
- xml,
38640
+ buildRequest,
38209
38641
  {
38210
- headers: { "Content-Type": "application/xml", Accept: "application/json" }
38642
+ headers: { "Content-Type": "application/json", Accept: "application/json" }
38211
38643
  }
38212
38644
  );
38213
38645
  const build = response.data;
@@ -38216,8 +38648,11 @@ var DEV_TOOLS = [
38216
38648
  action: "trigger_build",
38217
38649
  buildId: String(build.id ?? ""),
38218
38650
  state: build.state ?? void 0,
38219
- status: build.status ?? void 0
38651
+ status: build.status ?? void 0,
38652
+ branchName: build.branchName ?? branchName
38220
38653
  });
38654
+ } catch (error2) {
38655
+ return sendXmlFallback(error2);
38221
38656
  }
38222
38657
  },
38223
38658
  args
@@ -40712,22 +41147,88 @@ var FULL_MODE_TOOLS = [
40712
41147
  sourceBuildTypeId: { type: "string", description: "Source build type ID" },
40713
41148
  name: { type: "string", description: "New build configuration name" },
40714
41149
  id: { type: "string", description: "New build configuration ID" },
40715
- projectId: { type: "string", description: "Target project ID" }
41150
+ projectId: { type: "string", description: "Target project ID" },
41151
+ description: { type: "string", description: "Description for the cloned configuration" },
41152
+ parameters: {
41153
+ type: "object",
41154
+ description: "Optional parameter overrides to apply to the clone",
41155
+ additionalProperties: { type: "string" }
41156
+ },
41157
+ copyBuildCounter: {
41158
+ type: "boolean",
41159
+ description: "Copy the build number counter from the source configuration"
41160
+ }
40716
41161
  },
40717
41162
  required: ["sourceBuildTypeId", "name", "id"]
40718
41163
  },
40719
41164
  handler: async (args) => {
40720
- const typedArgs = args;
40721
- const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
40722
- const source = await adapter.getBuildType(typedArgs.sourceBuildTypeId);
40723
- const buildType = {
40724
- ...source,
40725
- name: typedArgs.name,
40726
- id: typedArgs.id,
40727
- project: { id: typedArgs.projectId ?? source.project?.id ?? "_Root" }
40728
- };
40729
- const response = await adapter.modules.buildTypes.createBuildType(void 0, buildType);
40730
- return json({ success: true, action: "clone_build_config", id: response.data.id });
41165
+ const schema = import_zod4.z.object({
41166
+ sourceBuildTypeId: import_zod4.z.string().min(1),
41167
+ name: import_zod4.z.string().min(1),
41168
+ id: import_zod4.z.string().min(1),
41169
+ projectId: import_zod4.z.string().min(1).optional(),
41170
+ description: import_zod4.z.string().optional(),
41171
+ parameters: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string()).optional(),
41172
+ copyBuildCounter: import_zod4.z.boolean().optional()
41173
+ }).superRefine((value, ctx) => {
41174
+ if (value.id.trim() === "") {
41175
+ ctx.addIssue({
41176
+ code: import_zod4.z.ZodIssueCode.custom,
41177
+ message: "id must be a non-empty string.",
41178
+ path: ["id"]
41179
+ });
41180
+ }
41181
+ });
41182
+ return runTool(
41183
+ "clone_build_config",
41184
+ schema,
41185
+ async (typedArgs) => {
41186
+ const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
41187
+ const manager = new BuildConfigurationCloneManager(adapter);
41188
+ const source = await manager.retrieveConfiguration(typedArgs.sourceBuildTypeId);
41189
+ if (!source) {
41190
+ return json({
41191
+ success: false,
41192
+ action: "clone_build_config",
41193
+ error: `Source build configuration not found: ${typedArgs.sourceBuildTypeId}`
41194
+ });
41195
+ }
41196
+ const targetProjectId = typedArgs.projectId ?? source.projectId;
41197
+ if (!targetProjectId) {
41198
+ return json({
41199
+ success: false,
41200
+ action: "clone_build_config",
41201
+ error: "projectId is required when the source configuration does not specify a project."
41202
+ });
41203
+ }
41204
+ try {
41205
+ const cloned = await manager.cloneConfiguration(source, {
41206
+ id: typedArgs.id,
41207
+ name: typedArgs.name,
41208
+ targetProjectId,
41209
+ description: typedArgs.description ?? source.description,
41210
+ parameters: typedArgs.parameters,
41211
+ copyBuildCounter: typedArgs.copyBuildCounter
41212
+ });
41213
+ return json({
41214
+ success: true,
41215
+ action: "clone_build_config",
41216
+ id: cloned.id,
41217
+ name: cloned.name,
41218
+ projectId: cloned.projectId,
41219
+ url: cloned.url,
41220
+ description: cloned.description
41221
+ });
41222
+ } catch (error2) {
41223
+ return json({
41224
+ success: false,
41225
+ action: "clone_build_config",
41226
+ error: error2 instanceof Error ? error2.message : "Failed to clone build configuration."
41227
+ });
41228
+ }
41229
+ },
41230
+ args
41231
+ );
40731
41232
  },
40732
41233
  mode: "full"
40733
41234
  },