@azure-devops/mcp 2.9.0-nightly.20260823 → 2.9.0-nightly.20260901

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/dist/auth.js CHANGED
@@ -2,15 +2,41 @@
2
2
  // Licensed under the MIT License.
3
3
  import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
4
4
  import { PublicClientApplication } from "@azure/msal-node";
5
+ import { NativeBrokerPlugin } from "@azure/msal-node-extensions";
5
6
  import open from "open";
6
7
  import { logger } from "./logger.js";
7
8
  const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
9
+ const patAllowedHosts = new Set(["dev.azure.com", "vssps.dev.azure.com", "almsearch.dev.azure.com"]);
10
+ function isPatAllowedHost(hostname) {
11
+ const normalizedHostname = hostname.toLowerCase();
12
+ return patAllowedHosts.has(normalizedHostname) || normalizedHostname.endsWith(".visualstudio.com");
13
+ }
14
+ function installPatFetchInterceptor(basicValue) {
15
+ const originalFetch = globalThis.fetch;
16
+ const patBearerValue = `Bearer ${basicValue}`;
17
+ globalThis.fetch = async (input, init) => {
18
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
19
+ if (headers.get("Authorization") !== patBearerValue) {
20
+ return originalFetch(input, init);
21
+ }
22
+ const requestUrl = new URL(input instanceof Request ? input.url : input.toString());
23
+ if (requestUrl.protocol !== "https:" || !isPatAllowedHost(requestUrl.hostname)) {
24
+ throw new Error(`Refusing to send a Personal Access Token to untrusted destination '${requestUrl.origin}'`);
25
+ }
26
+ headers.set("Authorization", `Basic ${basicValue}`);
27
+ if (input instanceof Request) {
28
+ return originalFetch(new Request(input, { ...init, headers }));
29
+ }
30
+ return originalFetch(input, { ...init, headers });
31
+ };
32
+ }
8
33
  class OAuthAuthenticator {
9
34
  static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
10
35
  static defaultAuthority = "https://login.microsoftonline.com/common";
11
36
  static zeroTenantId = "00000000-0000-0000-0000-000000000000";
12
37
  accountId;
13
38
  publicClientApp;
39
+ publicClientAppFallback;
14
40
  constructor(tenantId) {
15
41
  this.accountId = null;
16
42
  let authority = OAuthAuthenticator.defaultAuthority;
@@ -26,6 +52,22 @@ class OAuthAuthenticator {
26
52
  clientId: OAuthAuthenticator.clientId,
27
53
  authority,
28
54
  },
55
+ broker: {
56
+ nativeBrokerPlugin: new NativeBrokerPlugin(),
57
+ },
58
+ system: {
59
+ loggerOptions: {
60
+ loggerCallback: (level, message) => {
61
+ logger.debug(`MSALClient[${level}]: ${message}`);
62
+ },
63
+ },
64
+ },
65
+ });
66
+ this.publicClientAppFallback = new PublicClientApplication({
67
+ auth: {
68
+ clientId: OAuthAuthenticator.clientId,
69
+ authority,
70
+ },
29
71
  });
30
72
  logger.debug(`OAuthAuthenticator: Initialized with clientId='${OAuthAuthenticator.clientId}'`);
31
73
  }
@@ -50,17 +92,35 @@ class OAuthAuthenticator {
50
92
  }
51
93
  if (!authResult) {
52
94
  logger.debug(`OAuthAuthenticator: Starting interactive token acquisition`);
53
- authResult = await this.publicClientApp.acquireTokenInteractive({
95
+ try {
96
+ authResult = await this.publicClientApp.acquireTokenInteractive({
97
+ scopes,
98
+ openBrowser: async (url) => {
99
+ logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
100
+ open(url);
101
+ },
102
+ });
103
+ this.accountId = authResult.account;
104
+ logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
105
+ }
106
+ catch (error) {
107
+ const msalErrorMessage = error.platformBrokerError ? JSON.stringify(error.platformBrokerError) : "";
108
+ logger.debug(`OAuthAuthenticator: Interactive token acquisition failed: ${error instanceof Error ? error.message + msalErrorMessage : String(error)}`);
109
+ authResult = null;
110
+ }
111
+ }
112
+ if (!authResult) {
113
+ logger.debug(`OAuthAuthenticator: Starting interactive token acquisition without broker`);
114
+ authResult = await this.publicClientAppFallback.acquireTokenInteractive({
54
115
  scopes,
55
116
  openBrowser: async (url) => {
56
- logger.debug(`OAuthAuthenticator: Opening browser for authentication`);
117
+ logger.debug(`OAuthAuthenticator: Opening browser for authentication with target URL: ${url}`);
57
118
  open(url);
58
119
  },
59
120
  });
60
- this.accountId = authResult.account;
61
- logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
121
+ logger.debug(`OAuthAuthenticator: Successfully acquired token interactively without broker`);
62
122
  }
63
- if (!authResult.accessToken) {
123
+ if (!authResult?.accessToken) {
64
124
  logger.error(`OAuthAuthenticator: Authentication result contains no access token`);
65
125
  throw new Error("Failed to obtain Azure DevOps OAuth token.");
66
126
  }
@@ -126,4 +186,4 @@ function createAuthenticator(type, tenantId) {
126
186
  };
127
187
  }
128
188
  }
129
- export { createAuthenticator };
189
+ export { createAuthenticator, installPatFetchInterceptor };
package/dist/index.js CHANGED
@@ -5,13 +5,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { getBearerHandler, getPersonalAccessTokenHandler, WebApi } from "azure-devops-node-api";
7
7
  import yargs from "yargs";
8
- import { hideBin } from "yargs/helpers";
9
- import { createAuthenticator } from "./auth.js";
8
+ import { createAuthenticator, installPatFetchInterceptor } from "./auth.js";
10
9
  import { logger } from "./logger.js";
11
10
  import { getOrgTenant } from "./org-tenants.js";
12
11
  //import { configurePrompts } from "./prompts.js";
13
12
  import { configureAllTools } from "./tools.js";
14
13
  import { UserAgentComposer } from "./useragent.js";
14
+ import { getCliArgs } from "./utils.js";
15
15
  import { packageVersion } from "./version.js";
16
16
  import { DomainsManager } from "./shared/domains.js";
17
17
  function isGitHubCodespaceEnv() {
@@ -19,7 +19,7 @@ function isGitHubCodespaceEnv() {
19
19
  }
20
20
  const defaultAuthenticationType = isGitHubCodespaceEnv() ? "azcli" : "interactive";
21
21
  // Parse command line arguments using yargs
22
- const argv = yargs(hideBin(process.argv))
22
+ const argv = yargs(getCliArgs())
23
23
  .scriptName("mcp-server-azuredevops")
24
24
  .usage("Usage: $0 <organization> [options]")
25
25
  .version(packageVersion)
@@ -97,18 +97,7 @@ async function main() {
97
97
  const authenticator = createAuthenticator(argv.authentication, tenantId);
98
98
  if (argv.authentication === "pat") {
99
99
  const basicValue = await authenticator();
100
- // basicValue is already base64("{email}:{token}") — use it directly in the Authorization header
101
- const _originalFetch = globalThis.fetch;
102
- globalThis.fetch = async (input, init) => {
103
- if (init?.headers) {
104
- const headers = new Headers(init.headers);
105
- if (headers.get("Authorization")?.startsWith("Bearer ")) {
106
- headers.set("Authorization", `Basic ${basicValue}`);
107
- init = { ...init, headers };
108
- }
109
- }
110
- return _originalFetch(input, init);
111
- };
100
+ installPatFetchInterceptor(basicValue);
112
101
  logger.debug("PAT mode: global fetch interceptor installed to rewrite Bearer -> Basic auth headers");
113
102
  }
114
103
  // removing prompts until further notice
package/dist/logger.js CHANGED
File without changes
File without changes
package/dist/prompts.js CHANGED
File without changes
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
6
6
  import { extractAdoStreamError, getEnumKeys, streamToString, apiVersion } from "../utils.js";
7
7
  import { orgName } from "../index.js";
8
+ import { createExternalContentResponse } from "../shared/content-safety.js";
8
9
  const REPO_TOOLS = {
9
10
  repo_repository: "repo_repository",
10
11
  repo_pull_request: "repo_pull_request",
@@ -243,7 +244,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
243
244
  enhancedResponse = { ...enhancedResponse, changedFilesSummary: {} };
244
245
  }
245
246
  }
246
- return { content: [{ type: "text", text: JSON.stringify(enhancedResponse, null, 2) }] };
247
+ return createExternalContentResponse(enhancedResponse, "pull request");
247
248
  }
248
249
  if (action === "list") {
249
250
  if (!repositoryId && !project) {
@@ -457,7 +458,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
457
458
  if (streamError) {
458
459
  return { content: [{ type: "text", text: `Error getting file content for '${path}': ${streamError}` }], isError: true };
459
460
  }
460
- return { content: [{ type: "text", text: content }] };
461
+ return createExternalContentResponse(content, "repository file");
461
462
  }
462
463
  if (action === "list_directory") {
463
464
  const versionDescriptor = buildVersionDescriptor(version, versionType === "Commit" ? "Branch" : versionType);
@@ -750,20 +751,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
750
751
  // --- repo_pull_request_thread_write ----------------------------------------
751
752
  server.tool(REPO_TOOLS.repo_pull_request_thread_write, "Write operations for pull request comment threads. Use the action parameter to specify the operation.", {
752
753
  action: z
753
- .enum(["create", "reply", "update_status"])
754
- .describe("The action to perform. Options: create (create a new comment thread on a pull request), reply (reply to a comment in a thread), update_status (update the status of a comment thread)."),
754
+ .enum(["create", "reply", "update", "update_status"])
755
+ .describe("The action to perform. Options: create (create a new comment thread on a pull request), reply (reply to a comment in a thread), update (update an existing comment), update_status (update the status of a comment thread)."),
755
756
  repositoryId: z.string().describe("The ID or name of the repository. When using a name instead of a GUID, project must also be provided."),
756
757
  pullRequestId: z.coerce.number().min(1).describe("The ID of the pull request."),
757
758
  project: z.string().optional().describe("Project ID or project name. Required when repositoryId is a name instead of a GUID."),
758
- threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply and update_status."),
759
- content: z.string().optional().describe("The content of the comment. Required for create and reply."),
759
+ threadId: z.coerce.number().min(1).optional().describe("The ID of the thread. Required for reply, update, and update_status."),
760
+ commentId: z.coerce.number().min(1).optional().describe("The ID of the comment to update. Required for update."),
761
+ content: z.string().optional().describe("The content of the comment. Required for create, reply, and update."),
760
762
  status: z
761
763
  .enum(getEnumKeys(CommentThreadStatus))
762
764
  .optional()
763
765
  .default(CommentThreadStatus[CommentThreadStatus.Active])
764
766
  .describe("The thread status. Used for create (defaults to 'Active') and required for update_status."),
765
767
  filePath: z.string().optional().describe("The file path for the comment thread. Used for create."),
766
- fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply."),
768
+ fullResponse: z.boolean().optional().default(false).describe("Return full JSON response. Used for reply and update."),
767
769
  rightFileStartLine: z.coerce.number().min(1).optional().describe("Start line in the right file. Used for create."),
768
770
  rightFileStartOffset: z.number().optional().describe("Start character offset in the right file. Used for create."),
769
771
  rightFileEndLine: z.number().optional().describe("End line in the right file. Used for create."),
@@ -771,7 +773,7 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
771
773
  changeTrackingId: z.coerce.number().int().min(1).optional().describe("The file change tracking ID from the pull request iteration changes. Used for create."),
772
774
  firstComparingIteration: z.coerce.number().int().min(0).optional().describe("The iteration on the left side of the diff. Used for create."),
773
775
  secondComparingIteration: z.coerce.number().int().min(1).optional().describe("The iteration on the right side of the diff. Used for create."),
774
- }, async ({ action, repositoryId, pullRequestId, project, threadId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
776
+ }, async ({ action, repositoryId, pullRequestId, project, threadId, commentId, content, status, filePath, fullResponse, rightFileStartLine, rightFileStartOffset, rightFileEndLine, rightFileEndOffset, changeTrackingId, firstComparingIteration, secondComparingIteration, }) => {
775
777
  try {
776
778
  const connection = await connectionProvider();
777
779
  const gitApi = await connection.getGitApi();
@@ -847,6 +849,21 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
847
849
  return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
848
850
  return { content: [{ type: "text", text: `Comment successfully added to thread ${threadId}.` }] };
849
851
  }
852
+ if (action === "update") {
853
+ if (!threadId)
854
+ return { content: [{ type: "text", text: "threadId is required for update" }], isError: true };
855
+ if (!commentId)
856
+ return { content: [{ type: "text", text: "commentId is required for update" }], isError: true };
857
+ if (!content)
858
+ return { content: [{ type: "text", text: "content is required for update" }], isError: true };
859
+ const comment = await gitApi.updateComment({ content }, repositoryId, pullRequestId, threadId, commentId, project);
860
+ if (!comment) {
861
+ return { content: [{ type: "text", text: `Error: Failed to update comment ${commentId} in thread ${threadId}. The comment was not updated successfully.` }], isError: true };
862
+ }
863
+ if (fullResponse)
864
+ return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] };
865
+ return { content: [{ type: "text", text: `Comment ${commentId} successfully updated in thread ${threadId}.` }] };
866
+ }
850
867
  if (action === "update_status") {
851
868
  if (!threadId)
852
869
  return { content: [{ type: "text", text: "threadId is required for update_status" }], isError: true };
@@ -1,9 +1,10 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
3
4
  import { z } from "zod";
4
5
  import { apiVersion } from "../utils.js";
5
6
  import { orgName } from "../index.js";
6
- import { VersionControlRecursionType } from "azure-devops-node-api/interfaces/GitInterfaces.js";
7
+ import { createExternalContentResponse } from "../shared/content-safety.js";
7
8
  const SEARCH_TOOLS = {
8
9
  search_code: "search_code",
9
10
  search_wiki: "search_wiki",
@@ -60,9 +61,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
60
61
  const resultJson = JSON.parse(resultText);
61
62
  const gitApi = await connection.getGitApi();
62
63
  const combinedResults = await fetchCombinedResults(resultJson.results ?? [], gitApi);
63
- return {
64
- content: [{ type: "text", text: resultText + JSON.stringify(combinedResults) }],
65
- };
64
+ return createExternalContentResponse(resultText + JSON.stringify(combinedResults), "code search results");
66
65
  });
67
66
  server.tool(SEARCH_TOOLS.search_wiki, "Search Azure DevOps Wiki for a given search text", {
68
67
  searchText: z.string().describe("Keywords to search for wiki pages"),
@@ -101,9 +100,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
101
100
  throw new Error(`Azure DevOps Wiki Search API error: ${response.status} ${response.statusText}`);
102
101
  }
103
102
  const result = await response.text();
104
- return {
105
- content: [{ type: "text", text: result }],
106
- };
103
+ return createExternalContentResponse(result, "wiki search results");
107
104
  });
108
105
  server.tool(SEARCH_TOOLS.search_workitem, "Get Azure DevOps Work Item search results for a given search text", {
109
106
  searchText: z.string().describe("Search text to find in work items"),
@@ -151,9 +148,7 @@ function configureSearchTools(server, tokenProvider, connectionProvider, userAge
151
148
  throw new Error(`Azure DevOps Work Item Search API error: ${response.status} ${response.statusText}`);
152
149
  }
153
150
  const result = await response.text();
154
- return {
155
- content: [{ type: "text", text: result }],
156
- };
151
+ return createExternalContentResponse(result, "work item search results");
157
152
  });
158
153
  }
159
154
  async function fetchCombinedResults(topSearchResults, gitApi) {
@@ -170,7 +165,7 @@ async function fetchCombinedResults(topSearchResults, gitApi) {
170
165
  });
171
166
  continue;
172
167
  }
173
- const versionDescriptor = changeId ? { version: changeId, versionType: 2, versionOptions: 0 } : undefined;
168
+ const versionDescriptor = { version: changeId, versionType: 2, versionOptions: 0 };
174
169
  const item = await gitApi.getItem(repositoryId, filePath, projectId, undefined, VersionControlRecursionType.None, true, // includeContentMetadata
175
170
  false, // latestProcessedChange
176
171
  false, // download
@@ -458,9 +458,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
458
458
  }
459
459
  const mimeType = getMimeType(fileName);
460
460
  if (mimeType.startsWith("text/")) {
461
- return {
462
- content: [{ type: "text", text: buffer.toString("utf-8") }],
463
- };
461
+ return createExternalContentResponse(buffer.toString("utf-8"), "work item attachment");
464
462
  }
465
463
  const base64Data = buffer.toString("base64");
466
464
  return {
@@ -505,14 +503,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
505
503
  op: z
506
504
  .string()
507
505
  .transform((val) => val.toLowerCase())
508
- .pipe(z.enum(["add", "replace", "remove"]))
506
+ .pipe(z.enum(["add", "replace", "remove", "test"]))
509
507
  .default("add")
510
- .describe("The operation to perform."),
511
- path: z.string().describe("The field path, e.g. '/fields/System.Title'."),
512
- value: z.string().describe("The new value for the field."),
508
+ .describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
509
+ path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
510
+ value: z
511
+ .union([z.string(), z.number(), z.boolean(), z.null()])
512
+ .optional()
513
+ .describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
513
514
  }))
514
515
  .optional()
515
- .describe("Field updates for a single work item. Required for: update."),
516
+ .describe('Field updates for a single work item. Required for: update. For a safe read-modify-write, prepend a test operation on "/rev" with value set to the numeric revision returned by the preceding read; Azure DevOps rejects the entire update if the current revision differs.'),
516
517
  batchUpdates: z
517
518
  .array(z.object({
518
519
  op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
@@ -575,6 +576,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
575
576
  return { content: [{ type: "text", text: "id is required for update" }], isError: true };
576
577
  if (!updates || updates.length === 0)
577
578
  return { content: [{ type: "text", text: "updates is required for update" }], isError: true };
579
+ const updateWithoutValue = updates.find((update) => update.op !== "remove" && update.value === undefined);
580
+ if (updateWithoutValue) {
581
+ return { content: [{ type: "text", text: `value is required for ${updateWithoutValue.op}` }], isError: true };
582
+ }
578
583
  const workItemApi = await connection.getWorkItemTrackingApi();
579
584
  const apiUpdates = updates.map((update) => ({ ...update, op: update.op }));
580
585
  const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
@@ -703,9 +708,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
703
708
  }
704
709
  catch (error) {
705
710
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
711
+ const statusCode = typeof error === "object" && error !== null && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : undefined;
712
+ const statusText = statusCode === 409 ? " Conflict" : statusCode === 412 ? " Precondition Failed" : "";
713
+ const updateStatus = statusCode !== undefined ? ` [HTTP ${statusCode}${statusText}]` : "";
706
714
  const msgs = {
707
715
  create: `Error creating work item: ${errorMessage}`,
708
- update: `Error updating work item: ${errorMessage}`,
716
+ update: `Error updating work item${updateStatus}: ${errorMessage}`,
709
717
  update_batch: `Error updating work items in batch: ${errorMessage}`,
710
718
  add_child: `Error creating child work items: ${errorMessage}`,
711
719
  };
package/dist/tools.js CHANGED
File without changes
package/dist/useragent.js CHANGED
File without changes
package/dist/utils.js CHANGED
@@ -3,6 +3,19 @@
3
3
  export const apiVersion = "7.2-preview.1";
4
4
  export const batchApiVersion = "5.0";
5
5
  export const markdownCommentsApiVersion = "7.2-preview.4";
6
+ /**
7
+ * Returns the user-supplied CLI arguments.
8
+ *
9
+ * The server is always started script-style — `[runtime, scriptPath, ...args]` — on Node
10
+ * and on Electron hosts alike, so the first two entries are always dropped.
11
+ *
12
+ * Do not replace this with yargs' `hideBin`: it drops a single entry whenever
13
+ * `process.versions.electron` is set and `process.defaultApp` is not, so on an Electron
14
+ * host the script path survives and is parsed as the organization name.
15
+ */
16
+ export function getCliArgs(argv = process.argv) {
17
+ return argv.slice(2);
18
+ }
6
19
  export function createEnumMapping(enumObject) {
7
20
  const mapping = {};
8
21
  for (const [key, value] of Object.entries(enumObject)) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.9.0-nightly.20260823";
1
+ export const packageVersion = "2.9.0-nightly.20260901";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.9.0-nightly.20260823",
3
+ "version": "2.9.0-nightly.20260901",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -37,12 +37,15 @@
37
37
  "test": "jest"
38
38
  },
39
39
  "dependencies": {
40
- "@azure/identity": "^4.10.0",
41
- "@azure/msal-node": "^5.0.6",
40
+ "@azure/identity": "^4.13.0",
41
+ "@azure/logger": "^1.3.0",
42
+ "@azure/msal-node": "^5.5.0",
43
+ "@azure/msal-node-extensions": "^5.3.5",
42
44
  "@modelcontextprotocol/sdk": "1.29.0",
43
45
  "azure-devops-extension-api": "^5.272.3",
44
46
  "azure-devops-extension-sdk": "^4.0.2",
45
47
  "azure-devops-node-api": "^15.1.2",
48
+ "open": "^10.2.0",
46
49
  "winston": "^3.18.3",
47
50
  "yargs": "^18.0.0",
48
51
  "zod": "^3.25.63",
@@ -69,5 +72,9 @@
69
72
  "**/*.(js|ts|jsx|tsx|json|css|md)": [
70
73
  "npm run format"
71
74
  ]
75
+ },
76
+ "allowScripts": {
77
+ "keytar@7.9.0": true,
78
+ "@azure/msal-node-extensions@5.3.5": true
72
79
  }
73
80
  }