@daghis/teamcity-mcp 1.10.1 → 1.10.2

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.2"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.10.2](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.10.1...teamcity-mcp-v1.10.2) (2025-09-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **tools:** clone_build_config uses manager (215) ([b84d1f8](https://github.com/Daghis/teamcity-mcp/commit/b84d1f80a4233783a93dd1e3ede9a83a7cf57171))
9
+ * **tools:** clone_build_config uses manager (215) ([c4cd959](https://github.com/Daghis/teamcity-mcp/commit/c4cd959a9f35052bf95386162316a9ace5599eb6))
10
+
3
11
  ## [1.10.1](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v1.10.0...teamcity-mcp-v1.10.1) (2025-09-27)
4
12
 
5
13
 
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";
@@ -40712,22 +41090,88 @@ var FULL_MODE_TOOLS = [
40712
41090
  sourceBuildTypeId: { type: "string", description: "Source build type ID" },
40713
41091
  name: { type: "string", description: "New build configuration name" },
40714
41092
  id: { type: "string", description: "New build configuration ID" },
40715
- projectId: { type: "string", description: "Target project ID" }
41093
+ projectId: { type: "string", description: "Target project ID" },
41094
+ description: { type: "string", description: "Description for the cloned configuration" },
41095
+ parameters: {
41096
+ type: "object",
41097
+ description: "Optional parameter overrides to apply to the clone",
41098
+ additionalProperties: { type: "string" }
41099
+ },
41100
+ copyBuildCounter: {
41101
+ type: "boolean",
41102
+ description: "Copy the build number counter from the source configuration"
41103
+ }
40716
41104
  },
40717
41105
  required: ["sourceBuildTypeId", "name", "id"]
40718
41106
  },
40719
41107
  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 });
41108
+ const schema = import_zod4.z.object({
41109
+ sourceBuildTypeId: import_zod4.z.string().min(1),
41110
+ name: import_zod4.z.string().min(1),
41111
+ id: import_zod4.z.string().min(1),
41112
+ projectId: import_zod4.z.string().min(1).optional(),
41113
+ description: import_zod4.z.string().optional(),
41114
+ parameters: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string()).optional(),
41115
+ copyBuildCounter: import_zod4.z.boolean().optional()
41116
+ }).superRefine((value, ctx) => {
41117
+ if (value.id.trim() === "") {
41118
+ ctx.addIssue({
41119
+ code: import_zod4.z.ZodIssueCode.custom,
41120
+ message: "id must be a non-empty string.",
41121
+ path: ["id"]
41122
+ });
41123
+ }
41124
+ });
41125
+ return runTool(
41126
+ "clone_build_config",
41127
+ schema,
41128
+ async (typedArgs) => {
41129
+ const adapter = createAdapterFromTeamCityAPI(TeamCityAPI.getInstance());
41130
+ const manager = new BuildConfigurationCloneManager(adapter);
41131
+ const source = await manager.retrieveConfiguration(typedArgs.sourceBuildTypeId);
41132
+ if (!source) {
41133
+ return json({
41134
+ success: false,
41135
+ action: "clone_build_config",
41136
+ error: `Source build configuration not found: ${typedArgs.sourceBuildTypeId}`
41137
+ });
41138
+ }
41139
+ const targetProjectId = typedArgs.projectId ?? source.projectId;
41140
+ if (!targetProjectId) {
41141
+ return json({
41142
+ success: false,
41143
+ action: "clone_build_config",
41144
+ error: "projectId is required when the source configuration does not specify a project."
41145
+ });
41146
+ }
41147
+ try {
41148
+ const cloned = await manager.cloneConfiguration(source, {
41149
+ id: typedArgs.id,
41150
+ name: typedArgs.name,
41151
+ targetProjectId,
41152
+ description: typedArgs.description ?? source.description,
41153
+ parameters: typedArgs.parameters,
41154
+ copyBuildCounter: typedArgs.copyBuildCounter
41155
+ });
41156
+ return json({
41157
+ success: true,
41158
+ action: "clone_build_config",
41159
+ id: cloned.id,
41160
+ name: cloned.name,
41161
+ projectId: cloned.projectId,
41162
+ url: cloned.url,
41163
+ description: cloned.description
41164
+ });
41165
+ } catch (error2) {
41166
+ return json({
41167
+ success: false,
41168
+ action: "clone_build_config",
41169
+ error: error2 instanceof Error ? error2.message : "Failed to clone build configuration."
41170
+ });
41171
+ }
41172
+ },
41173
+ args
41174
+ );
40731
41175
  },
40732
41176
  mode: "full"
40733
41177
  },