@daghis/teamcity-mcp 2.3.1 → 2.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.4.0](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.3.2...teamcity-mcp-v2.4.0) (2026-03-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * add SSH key management tools for projects ([#407](https://github.com/Daghis/teamcity-mcp/issues/407)) ([#421](https://github.com/Daghis/teamcity-mcp/issues/421)) ([e00955c](https://github.com/Daghis/teamcity-mcp/commit/e00955c1d4c43955a734c806e42bd8ac4296d21d))
9
+
10
+ ## [2.3.2](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.3.1...teamcity-mcp-v2.3.2) (2026-03-10)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * write snapshot dependency options as properties, not XML options ([#406](https://github.com/Daghis/teamcity-mcp/issues/406)) ([#419](https://github.com/Daghis/teamcity-mcp/issues/419)) ([e13fa2d](https://github.com/Daghis/teamcity-mcp/commit/e13fa2dd1be5c387efd9b4fc0e38cb5e15634be4))
16
+
3
17
  ## [2.3.1](https://github.com/Daghis/teamcity-mcp/compare/teamcity-mcp-v2.3.0...teamcity-mcp-v2.3.1) (2026-03-10)
4
18
 
5
19
 
package/CLAUDE.md CHANGED
@@ -64,7 +64,7 @@ Type support includes: `password`, `text`, `checkbox`, `select` with spec format
64
64
 
65
65
  When Marc says he wants to "check-in with GitHub", run these steps:
66
66
 
67
- 1. Pull latest pulse stats (via project-pulse MCP, repo: `Daghis/teamcity-mcp`, npm: `@daghis/teamcity-mcp`)
67
+ 1. Snapshot pulse stats (via project-pulse MCP `snapshot_stats`, name: `teamcity-mcp`, repo: `Daghis/teamcity-mcp`, npm: `@daghis/teamcity-mcp`) — this records a data point for trend tracking
68
68
  2. Check new issues
69
69
  3. Check new PRs
70
70
  4. Check security alerts
package/dist/index.js CHANGED
@@ -1205,7 +1205,7 @@ function debug2(message, meta) {
1205
1205
  // package.json
1206
1206
  var package_default = {
1207
1207
  name: "@daghis/teamcity-mcp",
1208
- version: "2.3.1",
1208
+ version: "2.4.0",
1209
1209
  description: "Model Control Protocol server for TeamCity CI/CD integration with AI coding assistants",
1210
1210
  mcpName: "io.github.Daghis/teamcity",
1211
1211
  main: "dist/index.js",
@@ -2780,13 +2780,6 @@ var defaultTypeFor = (dependencyType) => {
2780
2780
  return void 0;
2781
2781
  }
2782
2782
  };
2783
- var SNAPSHOT_DEPENDENCY_OPTION_KEYS = /* @__PURE__ */ new Set([
2784
- "run-build-on-the-same-agent",
2785
- "sync-revisions",
2786
- "take-successful-builds-only",
2787
- "take-started-build-with-same-revisions",
2788
- "do-not-run-new-build-if-there-is-a-suitable-one"
2789
- ]);
2790
2783
  var toStringRecord2 = (input) => {
2791
2784
  if (!input) {
2792
2785
  return {};
@@ -2841,15 +2834,6 @@ var optionsToRecord = (options) => {
2841
2834
  }
2842
2835
  return map;
2843
2836
  };
2844
- var recordToOptions = (record) => {
2845
- const entries = Object.entries(record);
2846
- if (entries.length === 0) {
2847
- return void 0;
2848
- }
2849
- return {
2850
- option: entries.map(([name, value]) => ({ name, value }))
2851
- };
2852
- };
2853
2837
  var mergeRecords2 = (base, override) => {
2854
2838
  const merged = { ...base };
2855
2839
  for (const [key, value] of Object.entries(override)) {
@@ -2881,25 +2865,6 @@ var propertiesToXml2 = (properties) => {
2881
2865
  }
2882
2866
  return `<properties>${nodes.join("")}</properties>`;
2883
2867
  };
2884
- var optionsToXml = (options) => {
2885
- if (!options) {
2886
- return void 0;
2887
- }
2888
- const entries = options.option;
2889
- const list = Array.isArray(entries) ? entries : entries != null ? [entries] : [];
2890
- if (list.length === 0) {
2891
- return void 0;
2892
- }
2893
- const nodes = list.filter((item) => item?.name).map((item) => {
2894
- const name = item?.name ?? "";
2895
- const value = item?.value != null ? String(item.value) : "";
2896
- return `<option name="${escapeXml2(name)}" value="${escapeXml2(value)}"/>`;
2897
- });
2898
- if (nodes.length === 0) {
2899
- return void 0;
2900
- }
2901
- return `<options>${nodes.join("")}</options>`;
2902
- };
2903
2868
  var sourceBuildTypeToXml = (source) => {
2904
2869
  if (!source || typeof source !== "object") {
2905
2870
  return void 0;
@@ -2946,10 +2911,6 @@ var dependencyToXml = (dependencyType, payload) => {
2946
2911
  if (propertiesXml) {
2947
2912
  fragments.push(propertiesXml);
2948
2913
  }
2949
- const optionsXml = optionsToXml(payload.options);
2950
- if (optionsXml) {
2951
- fragments.push(optionsXml);
2952
- }
2953
2914
  return `<${root}${attributesToString2(attributes)}>${fragments.join("")}</${root}>`;
2954
2915
  };
2955
2916
  var prepareArtifactRequest = (payload) => ({
@@ -3076,42 +3037,19 @@ var BuildDependencyManager = class {
3076
3037
  }
3077
3038
  }
3078
3039
  buildPayload(dependencyType, existing, input) {
3079
- const existingSnapshot = existing;
3080
3040
  const baseProperties = propertiesToRecord2(existing?.properties);
3081
- const inputPropertyRecord = toStringRecord2(input.properties);
3082
- const inputExplicitOptions = toStringRecord2(input.options);
3083
- let optionOverrides = {};
3084
- let propertyOverrides = inputPropertyRecord;
3085
- let baseOptions = {};
3086
3041
  if (dependencyType === "snapshot") {
3087
- baseOptions = optionsToRecord(existingSnapshot?.options);
3088
- const knownOptionKeys = /* @__PURE__ */ new Set([
3089
- ...Object.keys(baseOptions),
3090
- ...Object.keys(inputExplicitOptions)
3091
- ]);
3092
- for (const key of SNAPSHOT_DEPENDENCY_OPTION_KEYS) {
3093
- knownOptionKeys.add(key);
3094
- }
3095
- const derivedOptionOverrides = { ...inputExplicitOptions };
3096
- const derivedPropertyOverrides = {};
3097
- for (const [key, value] of Object.entries(inputPropertyRecord)) {
3098
- if (knownOptionKeys.has(key)) {
3099
- derivedOptionOverrides[key] = value;
3100
- } else {
3101
- derivedPropertyOverrides[key] = value;
3102
- }
3103
- }
3104
- optionOverrides = derivedOptionOverrides;
3105
- propertyOverrides = derivedPropertyOverrides;
3106
- } else if (Object.keys(inputExplicitOptions).length > 0) {
3107
- optionOverrides = inputExplicitOptions;
3042
+ const existingOptions = optionsToRecord(
3043
+ existing?.options
3044
+ );
3045
+ Object.assign(baseProperties, existingOptions);
3108
3046
  }
3109
- const mergedProps = mergeRecords2(baseProperties, propertyOverrides);
3047
+ const inputOverrides = mergeRecords2(
3048
+ toStringRecord2(input.properties),
3049
+ toStringRecord2(input.options)
3050
+ );
3051
+ const mergedProps = mergeRecords2(baseProperties, inputOverrides);
3110
3052
  const properties = recordToProperties2(mergedProps);
3111
- let mergedOptions = {};
3112
- if (dependencyType === "snapshot") {
3113
- mergedOptions = mergeRecords2(baseOptions, optionOverrides);
3114
- }
3115
3053
  const resolvedType = input.type ?? existing?.type ?? defaultTypeFor(dependencyType);
3116
3054
  const payload = {
3117
3055
  ...existing ?? {},
@@ -3125,14 +3063,6 @@ var BuildDependencyManager = class {
3125
3063
  } else {
3126
3064
  delete payload.properties;
3127
3065
  }
3128
- if (dependencyType === "snapshot") {
3129
- const options = recordToOptions(mergedOptions);
3130
- if (options) {
3131
- payload.options = options;
3132
- } else {
3133
- delete payload.options;
3134
- }
3135
- }
3136
3066
  const dependsOn = input.dependsOn ?? existing?.["source-buildType"]?.id;
3137
3067
  if (dependsOn) {
3138
3068
  payload["source-buildType"] = { id: dependsOn };
@@ -4618,6 +4548,13 @@ var globalErrorHandler = GlobalErrorHandler.getInstance();
4618
4548
  function json(data) {
4619
4549
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], success: true };
4620
4550
  }
4551
+ var SECRET_KEY_PATTERN = /token|authorization|password|privateKey/i;
4552
+ var maskSecrets = (args) => typeof args === "object" && args != null ? Object.fromEntries(
4553
+ Object.entries(args).map(([k, v]) => [
4554
+ k,
4555
+ SECRET_KEY_PATTERN.test(k) ? "***" : v
4556
+ ])
4557
+ ) : {};
4621
4558
  async function runTool(toolName, schema, handler, rawArgs, context) {
4622
4559
  const logger2 = getLogger();
4623
4560
  const reqId = context?.requestId ?? logger2.generateRequestId();
@@ -4629,13 +4566,7 @@ async function runTool(toolName, schema, handler, rawArgs, context) {
4629
4566
  const success = result?.success !== false;
4630
4567
  logger2.logToolExecution(
4631
4568
  toolName,
4632
- // Avoid logging secrets by shallow masking of obvious keys
4633
- typeof args === "object" && args != null ? Object.fromEntries(
4634
- Object.entries(args).map(([k, v]) => [
4635
- k,
4636
- /token|authorization|password/i.test(k) ? "***" : v
4637
- ])
4638
- ) : {},
4569
+ maskSecrets(args),
4639
4570
  { success, error: result?.error },
4640
4571
  duration,
4641
4572
  { requestId: reqId }
@@ -4646,7 +4577,7 @@ async function runTool(toolName, schema, handler, rawArgs, context) {
4646
4577
  const msg = err instanceof Error ? err.message : String(err);
4647
4578
  logger2.logToolExecution(
4648
4579
  toolName,
4649
- typeof rawArgs === "object" && rawArgs != null ? rawArgs : {},
4580
+ maskSecrets(rawArgs),
4650
4581
  { success: false, error: msg },
4651
4582
  duration,
4652
4583
  { requestId: reqId }
@@ -43985,6 +43916,103 @@ var FULL_MODE_TOOLS = [
43985
43916
  );
43986
43917
  },
43987
43918
  mode: "full"
43919
+ },
43920
+ // === SSH Key Management ===
43921
+ {
43922
+ name: "list_project_ssh_keys",
43923
+ description: "List SSH keys configured for a project",
43924
+ inputSchema: {
43925
+ type: "object",
43926
+ properties: {
43927
+ projectId: { type: "string", description: "Project ID" }
43928
+ },
43929
+ required: ["projectId"]
43930
+ },
43931
+ handler: async (args) => {
43932
+ const typedArgs = args;
43933
+ const api = TeamCityAPI.getInstance();
43934
+ const response = await api.http.get(
43935
+ `/app/rest/projects/${encodeURIComponent(typedArgs.projectId)}/sshKeys`,
43936
+ { headers: { Accept: "application/json" } }
43937
+ );
43938
+ return json({
43939
+ success: true,
43940
+ action: "list_project_ssh_keys",
43941
+ projectId: typedArgs.projectId,
43942
+ sshKeys: response.data
43943
+ });
43944
+ },
43945
+ mode: "full"
43946
+ },
43947
+ {
43948
+ name: "upload_project_ssh_key",
43949
+ description: "Upload an SSH key to a project. Provide either privateKeyContent (raw PEM string) or privateKeyPath (path to key file), but not both.",
43950
+ inputSchema: {
43951
+ type: "object",
43952
+ properties: {
43953
+ projectId: { type: "string", description: "Project ID" },
43954
+ keyName: { type: "string", description: "Name for the SSH key" },
43955
+ privateKeyContent: {
43956
+ type: "string",
43957
+ description: "Raw private key content (PEM format)"
43958
+ },
43959
+ privateKeyPath: {
43960
+ type: "string",
43961
+ description: "Path to the private key file"
43962
+ }
43963
+ },
43964
+ required: ["projectId", "keyName"]
43965
+ },
43966
+ handler: async (args) => {
43967
+ const typedArgs = args;
43968
+ if (!typedArgs.privateKeyContent && !typedArgs.privateKeyPath) {
43969
+ throw new Error("Either privateKeyContent or privateKeyPath must be provided");
43970
+ }
43971
+ if (typedArgs.privateKeyContent && typedArgs.privateKeyPath) {
43972
+ throw new Error("Provide only one of privateKeyContent or privateKeyPath, not both");
43973
+ }
43974
+ const keyContent = typedArgs.privateKeyPath ? await import_node_fs2.promises.readFile(typedArgs.privateKeyPath, "utf-8") : typedArgs.privateKeyContent;
43975
+ const formData = new FormData();
43976
+ formData.append("privateKey", new Blob([keyContent]), "key");
43977
+ const api = TeamCityAPI.getInstance();
43978
+ await api.http.post(
43979
+ `/app/rest/projects/${encodeURIComponent(typedArgs.projectId)}/sshKeys?${new URLSearchParams({ name: typedArgs.keyName })}`,
43980
+ formData
43981
+ );
43982
+ return json({
43983
+ success: true,
43984
+ action: "upload_project_ssh_key",
43985
+ projectId: typedArgs.projectId,
43986
+ keyName: typedArgs.keyName
43987
+ });
43988
+ },
43989
+ mode: "full"
43990
+ },
43991
+ {
43992
+ name: "delete_project_ssh_key",
43993
+ description: "Delete an SSH key from a project",
43994
+ inputSchema: {
43995
+ type: "object",
43996
+ properties: {
43997
+ projectId: { type: "string", description: "Project ID" },
43998
+ keyName: { type: "string", description: "Name of the SSH key to delete" }
43999
+ },
44000
+ required: ["projectId", "keyName"]
44001
+ },
44002
+ handler: async (args) => {
44003
+ const typedArgs = args;
44004
+ const api = TeamCityAPI.getInstance();
44005
+ await api.http.delete(
44006
+ `/app/rest/projects/${encodeURIComponent(typedArgs.projectId)}/sshKeys?${new URLSearchParams({ name: typedArgs.keyName })}`
44007
+ );
44008
+ return json({
44009
+ success: true,
44010
+ action: "delete_project_ssh_key",
44011
+ projectId: typedArgs.projectId,
44012
+ keyName: typedArgs.keyName
44013
+ });
44014
+ },
44015
+ mode: "full"
43988
44016
  }
43989
44017
  ];
43990
44018
  function getAvailableTools() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daghis/teamcity-mcp",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "Model Control Protocol server for TeamCity CI/CD integration with AI coding assistants",
5
5
  "mcpName": "io.github.Daghis/teamcity",
6
6
  "main": "dist/index.js",
package/server.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "source": "github"
8
8
  },
9
9
  "websiteUrl": "https://github.com/Daghis/teamcity-mcp",
10
- "version": "2.3.1",
10
+ "version": "2.4.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "registryBaseUrl": "https://registry.npmjs.org",
15
15
  "identifier": "@daghis/teamcity-mcp",
16
- "version": "2.3.1",
16
+ "version": "2.4.0",
17
17
  "runtimeHint": "npx",
18
18
  "runtimeArguments": [
19
19
  {