@daghis/teamcity-mcp 1.6.0 → 1.8.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
+ ## [1.8.0](https://github.com/Daghis/teamcity-mcp/compare/v1.7.0...v1.8.0) (2025-09-20)
4
+
5
+
6
+ ### Features
7
+
8
+ * **tools:** support streaming artifacts in get_build_results ([#173](https://github.com/Daghis/teamcity-mcp/issues/173)) ([f95c5e4](https://github.com/Daghis/teamcity-mcp/commit/f95c5e4da9ffa80adc8b8f322c127a440c4524b5)), closes [#169](https://github.com/Daghis/teamcity-mcp/issues/169)
9
+
10
+ ## [1.7.0](https://github.com/Daghis/teamcity-mcp/compare/v1.6.0...v1.7.0) (2025-09-20)
11
+
12
+
13
+ ### Features
14
+
15
+ * **tools:** add streaming mode to fetch_build_log ([#171](https://github.com/Daghis/teamcity-mcp/issues/171)) ([1abce69](https://github.com/Daghis/teamcity-mcp/commit/1abce69b7fa3866ac289501e2369d5d06d57d57f))
16
+
3
17
  ## [1.6.0](https://github.com/Daghis/teamcity-mcp/compare/v1.5.0...v1.6.0) (2025-09-20)
4
18
 
5
19
 
package/dist/index.js CHANGED
@@ -1771,6 +1771,7 @@ var BuildResultsManager = class _BuildResultsManager {
1771
1771
  */
1772
1772
  async fetchArtifacts(buildId, options) {
1773
1773
  try {
1774
+ const encoding = options.artifactEncoding ?? "base64";
1774
1775
  const response = await this.client.modules.builds.getFilesListOfBuild(
1775
1776
  toBuildLocator(buildId)
1776
1777
  );
@@ -1783,6 +1784,7 @@ var BuildResultsManager = class _BuildResultsManager {
1783
1784
  artifacts.map(async (artifact) => {
1784
1785
  const artifactPath = artifact.fullName ?? artifact.name;
1785
1786
  const downloadHref = artifact.content?.href ?? `/app/rest/builds/id:${buildId}/artifacts/content/${artifactPath}`;
1787
+ const shouldInlineContent = encoding === "base64" && (options.downloadArtifacts?.length ? options.downloadArtifacts.includes(artifact.name) || options.downloadArtifacts.includes(artifactPath) : true);
1786
1788
  const artifactData = {
1787
1789
  name: artifact.name,
1788
1790
  path: artifactPath,
@@ -1790,7 +1792,7 @@ var BuildResultsManager = class _BuildResultsManager {
1790
1792
  modificationTime: artifact.modificationTime ?? "",
1791
1793
  downloadUrl: this.buildAbsoluteUrl(downloadHref)
1792
1794
  };
1793
- if (options.downloadArtifacts?.includes(artifact.name)) {
1795
+ if (shouldInlineContent) {
1794
1796
  const maxSize = options.maxArtifactSize ?? _BuildResultsManager.defaultMaxArtifactSize;
1795
1797
  if ((artifact.size ?? 0) <= maxSize) {
1796
1798
  try {
@@ -1799,6 +1801,16 @@ var BuildResultsManager = class _BuildResultsManager {
1799
1801
  } catch (err) {
1800
1802
  }
1801
1803
  }
1804
+ } else if (encoding === "stream") {
1805
+ artifactData.downloadHandle = {
1806
+ tool: "download_build_artifact",
1807
+ args: {
1808
+ buildId,
1809
+ artifactPath,
1810
+ encoding: "stream",
1811
+ ...options.maxArtifactSize ? { maxSize: options.maxArtifactSize } : {}
1812
+ }
1813
+ };
1802
1814
  }
1803
1815
  return artifactData;
1804
1816
  })
@@ -2117,6 +2129,7 @@ function createAdapterFromTeamCityAPI(api, options = {}) {
2117
2129
  builds: buildApi,
2118
2130
  listBuildArtifacts: (buildId, options2) => api.listBuildArtifacts(buildId, options2),
2119
2131
  downloadArtifactContent: (buildId, artifactPath, requestOptions) => api.downloadBuildArtifact(buildId, artifactPath, requestOptions),
2132
+ downloadBuildLogContent: (buildId, requestOptions) => api.downloadBuildLog(buildId, requestOptions),
2120
2133
  getBuildStatistics: (buildId, fields) => api.getBuildStatistics(buildId, fields),
2121
2134
  listChangesForBuild: (buildId, fields) => api.listChangesForBuild(buildId, fields),
2122
2135
  listSnapshotDependencies: (buildId) => api.listSnapshotDependencies(buildId),
@@ -36696,6 +36709,26 @@ var TeamCityAPI = class _TeamCityAPI {
36696
36709
  requestOptions
36697
36710
  );
36698
36711
  }
36712
+ async downloadBuildLog(buildId, options) {
36713
+ const rawParams = options?.params ?? void 0;
36714
+ const params = rawParams ? { ...rawParams } : {};
36715
+ if (!Object.prototype.hasOwnProperty.call(params, "plain")) {
36716
+ params["plain"] = true;
36717
+ }
36718
+ const rawHeaders = options?.headers ?? void 0;
36719
+ const headers = rawHeaders ? { ...rawHeaders } : {};
36720
+ const requestOptions = {
36721
+ ...options,
36722
+ params,
36723
+ headers: {
36724
+ Accept: "text/plain",
36725
+ ...headers
36726
+ },
36727
+ responseType: options?.responseType ?? "text",
36728
+ transformResponse: options?.transformResponse ?? [(data) => data]
36729
+ };
36730
+ return this.axiosInstance.get(`/app/rest/builds/id:${buildId}/log`, requestOptions);
36731
+ }
36699
36732
  async getBuildStatistics(buildId, fields) {
36700
36733
  return this.builds.getBuildStatisticValues(toBuildLocator(buildId), fields);
36701
36734
  }
@@ -36764,6 +36797,7 @@ var TeamCityAPI = class _TeamCityAPI {
36764
36797
  };
36765
36798
 
36766
36799
  // src/tools.ts
36800
+ var isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
36767
36801
  function getMCPMode2() {
36768
36802
  return getMCPMode();
36769
36803
  }
@@ -37160,7 +37194,17 @@ var DEV_TOOLS = [
37160
37194
  pageSize: { type: "number", description: "Lines per page (default 500)" },
37161
37195
  startLine: { type: "number", description: "0-based start line (overrides page)" },
37162
37196
  lineCount: { type: "number", description: "Max lines to return (overrides pageSize)" },
37163
- tail: { type: "boolean", description: "Tail mode: return last N lines" }
37197
+ tail: { type: "boolean", description: "Tail mode: return last N lines" },
37198
+ encoding: {
37199
+ type: "string",
37200
+ description: "Response encoding: 'text' (default) or 'stream'",
37201
+ enum: ["text", "stream"],
37202
+ default: "text"
37203
+ },
37204
+ outputPath: {
37205
+ type: "string",
37206
+ description: "Optional absolute path to write streamed logs; defaults to a temp file when streaming"
37207
+ }
37164
37208
  },
37165
37209
  required: []
37166
37210
  },
@@ -37173,9 +37217,24 @@ var DEV_TOOLS = [
37173
37217
  pageSize: import_zod4.z.number().int().min(1).max(5e3).optional(),
37174
37218
  startLine: import_zod4.z.number().int().min(0).optional(),
37175
37219
  lineCount: import_zod4.z.number().int().min(1).max(5e3).optional(),
37176
- tail: import_zod4.z.boolean().optional()
37177
- }).refine((v) => Boolean(v.buildId) || Boolean(v.buildNumber), {
37178
- message: "Provide either buildId or buildNumber"
37220
+ tail: import_zod4.z.boolean().optional(),
37221
+ encoding: import_zod4.z.enum(["text", "stream"]).default("text"),
37222
+ outputPath: import_zod4.z.string().min(1).optional()
37223
+ }).superRefine((value, ctx) => {
37224
+ if (!value.buildId && typeof value.buildNumber === "undefined") {
37225
+ ctx.addIssue({
37226
+ code: import_zod4.z.ZodIssueCode.custom,
37227
+ message: "Provide either buildId or buildNumber",
37228
+ path: ["buildId"]
37229
+ });
37230
+ }
37231
+ if (value.encoding === "stream" && value.tail) {
37232
+ ctx.addIssue({
37233
+ code: import_zod4.z.ZodIssueCode.custom,
37234
+ message: "Streaming mode does not support tail queries",
37235
+ path: ["tail"]
37236
+ });
37237
+ }
37179
37238
  });
37180
37239
  return runTool(
37181
37240
  "fetch_build_log",
@@ -37252,10 +37311,22 @@ var DEV_TOOLS = [
37252
37311
  }
37253
37312
  return false;
37254
37313
  };
37314
+ const normalizeError = (error2) => {
37315
+ if ((0, import_axios35.isAxiosError)(error2)) {
37316
+ const status = error2.response?.status;
37317
+ const statusText = (error2.response?.statusText ?? "").trim();
37318
+ const base = status ? `${status}${statusText ? ` ${statusText}` : ""}` : error2.message;
37319
+ return new Error(base || "Request failed");
37320
+ }
37321
+ if (error2 instanceof Error) {
37322
+ return error2;
37323
+ }
37324
+ return new Error(String(error2));
37325
+ };
37255
37326
  const wait = (ms) => new Promise((resolve) => {
37256
37327
  setTimeout(resolve, ms);
37257
37328
  });
37258
- const attemptFetch = async () => {
37329
+ const attemptBuffered = async () => {
37259
37330
  if (typed.tail) {
37260
37331
  const count = typed.lineCount ?? typed.pageSize ?? 500;
37261
37332
  const full = await adapter.getBuildLog(effectiveBuildId);
@@ -37303,16 +37374,52 @@ var DEV_TOOLS = [
37303
37374
  }
37304
37375
  });
37305
37376
  };
37306
- const maxAttempts = typed.tail ? 5 : 3;
37377
+ const attemptStream = async () => {
37378
+ const effectivePageSize = typed.lineCount ?? typed.pageSize ?? 500;
37379
+ const startLine = typeof typed.startLine === "number" ? typed.startLine : ((typed.page ?? 1) - 1) * effectivePageSize;
37380
+ const response = await adapter.downloadBuildLogContent(effectiveBuildId, {
37381
+ params: {
37382
+ start: startLine,
37383
+ count: effectivePageSize
37384
+ },
37385
+ responseType: "stream"
37386
+ });
37387
+ const stream = response.data;
37388
+ if (!isReadableStream(stream)) {
37389
+ throw new Error("Streaming log download did not return a readable stream");
37390
+ }
37391
+ const safeBuildId = effectiveBuildId.replace(/[^a-zA-Z0-9._-]/g, "_") || "build";
37392
+ const defaultFileName = `build-log-${safeBuildId}-${startLine}-${(0, import_node_crypto.randomUUID)()}.log`;
37393
+ const targetPath = typed.outputPath ?? (0, import_node_path.join)((0, import_node_os.tmpdir)(), defaultFileName);
37394
+ await import_node_fs.promises.mkdir((0, import_node_path.dirname)(targetPath), { recursive: true });
37395
+ await (0, import_promises.pipeline)(stream, (0, import_node_fs.createWriteStream)(targetPath));
37396
+ const stats = await import_node_fs.promises.stat(targetPath);
37397
+ const page = Math.floor(startLine / effectivePageSize) + 1;
37398
+ return json({
37399
+ encoding: "stream",
37400
+ outputPath: targetPath,
37401
+ bytesWritten: stats.size,
37402
+ meta: {
37403
+ buildId: effectiveBuildId,
37404
+ buildNumber: typeof typed.buildNumber !== "undefined" ? String(typed.buildNumber) : void 0,
37405
+ buildTypeId: typed.buildTypeId,
37406
+ page,
37407
+ pageSize: effectivePageSize,
37408
+ startLine
37409
+ }
37410
+ });
37411
+ };
37412
+ const isStream = typed.encoding === "stream";
37413
+ const maxAttempts = isStream ? 3 : typed.tail ? 5 : 3;
37307
37414
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
37308
37415
  try {
37309
- return await attemptFetch();
37416
+ return await (isStream ? attemptStream() : attemptBuffered());
37310
37417
  } catch (error2) {
37311
37418
  if (shouldRetry2(error2) && attempt < maxAttempts - 1) {
37312
37419
  await wait(500 * (attempt + 1));
37313
37420
  continue;
37314
37421
  }
37315
- throw error2;
37422
+ throw normalizeError(error2);
37316
37423
  }
37317
37424
  }
37318
37425
  throw new Error("Unable to fetch build log after retries");
@@ -38254,6 +38361,12 @@ var DEV_TOOLS = [
38254
38361
  maxArtifactSize: {
38255
38362
  type: "number",
38256
38363
  description: "Max artifact content size (bytes) when inlining"
38364
+ },
38365
+ artifactEncoding: {
38366
+ type: "string",
38367
+ description: "Encoding mode for artifacts when includeArtifacts is true",
38368
+ enum: ["base64", "stream"],
38369
+ default: "base64"
38257
38370
  }
38258
38371
  },
38259
38372
  required: ["buildId"]
@@ -38266,7 +38379,8 @@ var DEV_TOOLS = [
38266
38379
  includeChanges: import_zod4.z.boolean().optional(),
38267
38380
  includeDependencies: import_zod4.z.boolean().optional(),
38268
38381
  artifactFilter: import_zod4.z.string().min(1).optional(),
38269
- maxArtifactSize: import_zod4.z.number().int().min(1).optional()
38382
+ maxArtifactSize: import_zod4.z.number().int().min(1).optional(),
38383
+ artifactEncoding: import_zod4.z.enum(["base64", "stream"]).default("base64")
38270
38384
  });
38271
38385
  return runTool(
38272
38386
  "get_build_results",
@@ -38280,7 +38394,8 @@ var DEV_TOOLS = [
38280
38394
  includeChanges: typed.includeChanges,
38281
38395
  includeDependencies: typed.includeDependencies,
38282
38396
  artifactFilter: typed.artifactFilter,
38283
- maxArtifactSize: typed.maxArtifactSize
38397
+ maxArtifactSize: typed.maxArtifactSize,
38398
+ artifactEncoding: typed.artifactEncoding
38284
38399
  });
38285
38400
  return json(result);
38286
38401
  },
@@ -38321,7 +38436,6 @@ var DEV_TOOLS = [
38321
38436
  maxSize: import_zod4.z.number().int().positive().optional(),
38322
38437
  outputPath: import_zod4.z.string().min(1).optional()
38323
38438
  });
38324
- const isReadableStream = (value) => typeof value === "object" && value !== null && typeof value.pipe === "function";
38325
38439
  const toTempFilePath = (artifactName) => {
38326
38440
  const base = (0, import_node_path.basename)(artifactName || "artifact");
38327
38441
  const safeStem = base.replace(/[^a-zA-Z0-9._-]/g, "_") || "artifact";