@azure-devops/mcp 2.10.0-nightly.20260927 → 2.10.0-nightly.20260928

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.
@@ -8,8 +8,23 @@ import { ConfigurationType, RepositoryType } from "azure-devops-node-api/interfa
8
8
  import { mkdirSync, createWriteStream } from "fs";
9
9
  import { createExternalContentResponse } from "../shared/content-safety.js";
10
10
  import { join, posix, resolve, win32 } from "path";
11
+ import { Readable, Transform } from "stream";
12
+ import { pipeline } from "stream/promises";
11
13
  import { pipelinesWriteShape } from "./pipelines.dto.js";
12
14
  const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
15
+ const MAX_ARTIFACT_FILE_BYTES = 1024 * 1024 * 1024;
16
+ const MAX_ARTIFACT_BASE64_BYTES = 10 * 1024 * 1024;
17
+ function getArtifactResponseStream(response) {
18
+ if (!response.body)
19
+ throw new Error("Artifact download returned an empty response body.");
20
+ return Readable.fromWeb(response.body);
21
+ }
22
+ function validateArtifactContentLength(response, maxBytes, limitDescription) {
23
+ const contentLength = Number(response.headers.get("content-length"));
24
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
25
+ throw new Error(`Artifact download exceeds the ${limitDescription} limit.`);
26
+ }
27
+ }
13
28
  async function runPipeline(args, connectionProvider) {
14
29
  if (!args.pipelineId)
15
30
  return errorResult("pipelineId is required for run_pipeline");
@@ -338,25 +353,47 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
338
353
  if (!artifact) {
339
354
  return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} not found in build ${buildId}.` }], isError: true };
340
355
  }
341
- const fileStream = await buildApi.getArtifactContentZip(project, buildId, resolvedArtifactName);
356
+ if (!artifact.resource?.downloadUrl) {
357
+ throw new Error(`Artifact ${resolvedArtifactName} does not have a download URL.`);
358
+ }
359
+ const accessToken = await tokenProvider();
360
+ const response = await fetch(artifact.resource.downloadUrl, {
361
+ headers: { "Authorization": `Bearer ${accessToken}`, "User-Agent": userAgentProvider() },
362
+ });
363
+ if (!response.ok) {
364
+ throw new Error(`Artifact download failed: ${response.status} ${response.statusText}`);
365
+ }
342
366
  if (destinationPath) {
367
+ validateArtifactContentLength(response, MAX_ARTIFACT_FILE_BYTES, "1 GB file size");
343
368
  const fullDestinationPath = resolve(destinationPath);
344
369
  mkdirSync(fullDestinationPath, { recursive: true });
345
370
  const fileDestinationPath = join(fullDestinationPath, `${resolvedArtifactName}.zip`);
346
371
  const writeStream = createWriteStream(fileDestinationPath);
347
- await new Promise((resolve, reject) => {
348
- fileStream.pipe(writeStream);
349
- fileStream.on("end", () => resolve());
350
- fileStream.on("error", (err) => reject(err));
372
+ let downloadedBytes = 0;
373
+ const sizeLimitStream = new Transform({
374
+ transform(chunk, _encoding, callback) {
375
+ downloadedBytes += chunk.length;
376
+ if (downloadedBytes > MAX_ARTIFACT_FILE_BYTES) {
377
+ callback(new Error("Artifact download exceeds the 1 GB file size limit."));
378
+ return;
379
+ }
380
+ callback(null, chunk);
381
+ },
351
382
  });
383
+ await pipeline(getArtifactResponseStream(response), sizeLimitStream, writeStream);
352
384
  return { content: [{ type: "text", text: `Artifact ${resolvedArtifactName} downloaded to ${destinationPath}.` }] };
353
385
  }
386
+ validateArtifactContentLength(response, MAX_ARTIFACT_BASE64_BYTES, "10 MB base64 response size");
354
387
  const chunks = [];
355
- await new Promise((resolve, reject) => {
356
- fileStream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
357
- fileStream.on("end", () => resolve());
358
- fileStream.on("error", (err) => reject(err));
359
- });
388
+ let downloadedBytes = 0;
389
+ for await (const chunk of getArtifactResponseStream(response)) {
390
+ const bufferChunk = Buffer.from(chunk);
391
+ downloadedBytes += bufferChunk.length;
392
+ if (downloadedBytes > MAX_ARTIFACT_BASE64_BYTES) {
393
+ throw new Error("Artifact download exceeds the 10 MB base64 response size limit.");
394
+ }
395
+ chunks.push(bufferChunk);
396
+ }
360
397
  const buffer = Buffer.concat(chunks);
361
398
  const base64Data = buffer.toString("base64");
362
399
  return {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.10.0-nightly.20260927";
1
+ export const packageVersion = "2.10.0-nightly.20260928";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.10.0-nightly.20260927",
3
+ "version": "2.10.0-nightly.20260928",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",