@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 +14 -0
- package/CLAUDE.md +1 -1
- package/dist/index.js +116 -88
- package/package.json +1 -1
- package/server.json +2 -2
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.
|
|
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.
|
|
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
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
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.
|
|
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.
|
|
16
|
+
"version": "2.4.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"runtimeArguments": [
|
|
19
19
|
{
|