@azure-devops/mcp 2.2.2-nightly.20251120 → 2.2.2-nightly.20251125

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/README.md CHANGED
@@ -42,113 +42,7 @@ The Azure DevOps MCP Server is built from tools that are concise, simple, focuse
42
42
 
43
43
  ## ⚙️ Supported Tools
44
44
 
45
- Interact with these Azure DevOps services:
46
-
47
- ### 🧿 Core
48
-
49
- - **core_list_project_teams**: Retrieve a list of teams for the specified Azure DevOps project.
50
- - **core_list_projects**: Retrieve a list of projects in your Azure DevOps organization.
51
- - **core_get_identity_ids**: Retrieve Azure DevOps identity IDs for a list of unique names.
52
-
53
- ### ⚒️ Work
54
-
55
- - **work_list_team_iterations**: Retrieve a list of iterations for a specific team in a project.
56
- - **work_list_iterations**: List all iterations in a specified Azure DevOps project.
57
- - **work_create_iterations**: Create new iterations in a specified Azure DevOps project.
58
- - **work_assign_iterations**: Assign existing iterations to a specific team in a project.
59
- - **work_get_team_capacity**: Get the team capacity of a specific team and iteration in a project.
60
- - **work_update_team_capacity**: Update the team capacity of a team member for a specific iteration in a project.
61
- - **work_get_iteration_capacities**: Get an iteration's capacity for all teams in iteration and project.
62
-
63
- ### 📅 Work Items
64
-
65
- - **wit_my_work_items**: Retrieve a list of work items relevant to the authenticated user.
66
- - **wit_list_backlogs**: Retrieve a list of backlogs for a given project and team.
67
- - **wit_list_backlog_work_items**: Retrieve a list of backlogs for a given project, team, and backlog category.
68
- - **wit_get_work_item**: Get a single work item by ID.
69
- - **wit_get_work_items_batch_by_ids**: Retrieve a list of work items by IDs in batch.
70
- - **wit_update_work_item**: Update a work item by ID with specified fields.
71
- - **wit_create_work_item**: Create a new work item in a specified project and work item type.
72
- - **wit_list_work_item_comments**: Retrieve a list of comments for a work item by ID.
73
- - **wit_list_work_item_revisions**: Retrieve a list of revisions for a work item by ID.
74
- - **wit_get_work_items_for_iteration**: Retrieve a list of work items for a specified iteration.
75
- - **wit_add_work_item_comment**: Add a comment to a work item by ID.
76
- - **wit_add_child_work_items**: Create one or more child work items of a specific work item type for the given parent ID.
77
- - **wit_link_work_item_to_pull_request**: Link a single work item to an existing pull request.
78
- - **wit_get_work_item_type**: Get a specific work item type.
79
- - **wit_get_query**: Get a query by its ID or path.
80
- - **wit_get_query_results_by_id**: Retrieve the results of a work item query given the query ID.
81
- - **wit_update_work_items_batch**: Update work items in batch.
82
- - **wit_work_items_link**: Link work items together in batch.
83
- - **wit_work_item_unlink**: Unlink one or many links from a work item.
84
- - **wit_add_artifact_link**: Link to artifacts like branch, pull request, commit, and build.
85
-
86
- ### 📁 Repositories
87
-
88
- - **repo_list_repos_by_project**: Retrieve a list of repositories for a given project.
89
- - **repo_list_pull_requests_by_repo_or_project**: Retrieve a list of pull requests for a given repository or project.
90
- - **repo_list_branches_by_repo**: Retrieve a list of branches for a given repository.
91
- - **repo_list_my_branches_by_repo**: Retrieve a list of your branches for a given repository ID.
92
- - **repo_list_pull_requests_by_commits**: List pull requests associated with commits.
93
- - **repo_list_pull_request_threads**: Retrieve a list of comment threads for a pull request.
94
- - **repo_list_pull_request_thread_comments**: Retrieve a list of comments in a pull request thread.
95
- - **repo_get_repo_by_name_or_id**: Get the repository by project and repository name or ID.
96
- - **repo_get_branch_by_name**: Get a branch by its name.
97
- - **repo_get_pull_request_by_id**: Get a pull request by its ID.
98
- - **repo_create_pull_request**: Create a new pull request.
99
- - **repo_create_branch**: Create a new branch in the repository.
100
- - **repo_update_pull_request**: Update various fields of an existing pull request (title, description, draft status, target branch).
101
- - **repo_update_pull_request_reviewers**: Add or remove reviewers for an existing pull request.
102
- - **repo_reply_to_comment**: Replies to a specific comment on a pull request.
103
- - **repo_resolve_comment**: Resolves a specific comment thread on a pull request.
104
- - **repo_search_commits**: Searches for commits.
105
- - **repo_create_pull_request_thread**: Creates a new comment thread on a pull request.
106
-
107
- ### 🚀 Pipelines
108
-
109
- - **pipelines_get_build_definitions**: Retrieve a list of build definitions for a given project.
110
- - **pipelines_get_build_definition_revisions**: Retrieve a list of revisions for a specific build definition.
111
- - **pipelines_get_builds**: Retrieve a list of builds for a given project.
112
- - **pipelines_get_build_log**: Retrieve the logs for a specific build.
113
- - **pipelines_get_build_log_by_id**: Get a specific build log by log ID.
114
- - **pipelines_get_build_changes**: Get the changes associated with a specific build.
115
- - **pipelines_get_build_status**: Fetch the status of a specific build.
116
- - **pipelines_update_build_stage**: Update the stage of a specific build.
117
- - **pipelines_create_pipeline**: Creates a pipeline definition with YAML configuration for a given project.
118
- - **pipelines_get_run**: Gets a run for a particular pipeline.
119
- - **pipelines_list_runs**: Gets top 10000 runs for a particular pipeline.
120
- - **pipelines_run_pipeline**: Starts a new run of a pipeline.
121
-
122
- ### Advanced Security
123
-
124
- - **advsec_get_alerts**: Retrieve Advanced Security alerts for a repository.
125
- - **advsec_get_alert_details**: Get detailed information about a specific Advanced Security alert.
126
-
127
- ### 🧪 Test Plans
128
-
129
- - **testplan_create_test_plan**: Create a new test plan in the project.
130
- - **testplan_create_test_case**: Create a new test case work item.
131
- - **testplan_update_test_case_steps**: Update an existing test case work item's steps.
132
- - **testplan_add_test_cases_to_suite**: Add existing test cases to a test suite.
133
- - **testplan_list_test_plans**: Retrieve a paginated list of test plans from an Azure DevOps project. Allows filtering for active plans and toggling detailed information.
134
- - **testplan_list_test_cases**: Get a list of test cases in the test plan.
135
- - **testplan_show_test_results_from_build_id**: Get a list of test results for a given project and build ID.
136
- - **testplan_create_test_suite**: Creates a new test suite in a test plan.
137
-
138
- ### 📖 Wiki
139
-
140
- - **wiki_list_wikis**: Retrieve a list of wikis for an organization or project.
141
- - **wiki_get_wiki**: Get the wiki by wikiIdentifier.
142
- - **wiki_list_pages**: Retrieve a list of wiki pages for a specific wiki and project.
143
- - **wiki_get_page**: Retrieve wiki page metadata by path.
144
- - **wiki_get_page_content**: Retrieve wiki page content by wikiIdentifier and path.
145
- - **wiki_create_or_update_page**: Create or update wiki pages with full content support.
146
-
147
- ### 🔎 Search
148
-
149
- - **search_code**: Get code search results for a given search text.
150
- - **search_wiki**: Get wiki search results for a given search text.
151
- - **search_workitem**: Get work item search results for a given search text.
45
+ See [TOOLSET.md](./docs/TOOLSET.md) for a comprehensive list.
152
46
 
153
47
  ## 🔌 Installation & Getting Started
154
48
 
package/dist/auth.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { AzureCliCredential, ChainedTokenCredential, DefaultAzureCredential } from "@azure/identity";
4
4
  import { PublicClientApplication } from "@azure/msal-node";
5
5
  import open from "open";
6
+ import { logger } from "./logger.js";
6
7
  const scopes = ["499b84ac-1321-427f-aa17-267ca6975798/.default"];
7
8
  class OAuthAuthenticator {
8
9
  static clientId = "0d50963b-7bb9-4fe7-94c7-a99af00b5136";
@@ -15,6 +16,10 @@ class OAuthAuthenticator {
15
16
  let authority = OAuthAuthenticator.defaultAuthority;
16
17
  if (tenantId && tenantId !== OAuthAuthenticator.zeroTenantId) {
17
18
  authority = `https://login.microsoftonline.com/${tenantId}`;
19
+ logger.debug(`OAuthAuthenticator: Using tenant-specific authority for tenantId='${tenantId}'`);
20
+ }
21
+ else {
22
+ logger.debug(`OAuthAuthenticator: Using default common authority`);
18
23
  }
19
24
  this.publicClientApp = new PublicClientApplication({
20
25
  auth: {
@@ -22,49 +27,67 @@ class OAuthAuthenticator {
22
27
  authority,
23
28
  },
24
29
  });
30
+ logger.debug(`OAuthAuthenticator: Initialized with clientId='${OAuthAuthenticator.clientId}'`);
25
31
  }
26
32
  async getToken() {
27
33
  let authResult = null;
28
34
  if (this.accountId) {
35
+ logger.debug(`OAuthAuthenticator: Attempting silent token acquisition for cached account`);
29
36
  try {
30
37
  authResult = await this.publicClientApp.acquireTokenSilent({
31
38
  scopes,
32
39
  account: this.accountId,
33
40
  });
41
+ logger.debug(`OAuthAuthenticator: Successfully acquired token silently`);
34
42
  }
35
- catch {
43
+ catch (error) {
44
+ logger.debug(`OAuthAuthenticator: Silent token acquisition failed: ${error instanceof Error ? error.message : String(error)}`);
36
45
  authResult = null;
37
46
  }
38
47
  }
48
+ else {
49
+ logger.debug(`OAuthAuthenticator: No cached account available, interactive auth required`);
50
+ }
39
51
  if (!authResult) {
52
+ logger.debug(`OAuthAuthenticator: Starting interactive token acquisition`);
40
53
  authResult = await this.publicClientApp.acquireTokenInteractive({
41
54
  scopes,
42
55
  openBrowser: async (url) => {
56
+ logger.debug(`OAuthAuthenticator: Opening browser for authentication`);
43
57
  open(url);
44
58
  },
45
59
  });
46
60
  this.accountId = authResult.account;
61
+ logger.debug(`OAuthAuthenticator: Successfully acquired token interactively, account cached`);
47
62
  }
48
63
  if (!authResult.accessToken) {
64
+ logger.error(`OAuthAuthenticator: Authentication result contains no access token`);
49
65
  throw new Error("Failed to obtain Azure DevOps OAuth token.");
50
66
  }
67
+ logger.debug(`OAuthAuthenticator: Token obtained successfully`);
51
68
  return authResult.accessToken;
52
69
  }
53
70
  }
54
71
  function createAuthenticator(type, tenantId) {
72
+ logger.debug(`Creating authenticator of type '${type}' with tenantId='${tenantId ?? "undefined"}'`);
55
73
  switch (type) {
56
74
  case "envvar":
75
+ logger.debug(`Authenticator: Using environment variable authentication (ADO_MCP_AUTH_TOKEN)`);
57
76
  // Read token from fixed environment variable
58
77
  return async () => {
78
+ logger.debug(`${type}: Reading token from ADO_MCP_AUTH_TOKEN environment variable`);
59
79
  const token = process.env["ADO_MCP_AUTH_TOKEN"];
60
80
  if (!token) {
81
+ logger.error(`${type}: ADO_MCP_AUTH_TOKEN environment variable is not set or empty`);
61
82
  throw new Error("Environment variable 'ADO_MCP_AUTH_TOKEN' is not set or empty. Please set it with a valid Azure DevOps Personal Access Token.");
62
83
  }
84
+ logger.debug(`${type}: Successfully retrieved token from environment variable`);
63
85
  return token;
64
86
  };
65
87
  case "azcli":
66
88
  case "env":
67
89
  if (type !== "env") {
90
+ logger.debug(`${type}: Setting AZURE_TOKEN_CREDENTIALS to 'dev' for development credential chain`);
68
91
  process.env.AZURE_TOKEN_CREDENTIALS = "dev";
69
92
  }
70
93
  let credential = new DefaultAzureCredential(); // CodeQL [SM05138] resolved by explicitly setting AZURE_TOKEN_CREDENTIALS
@@ -76,11 +99,14 @@ function createAuthenticator(type, tenantId) {
76
99
  return async () => {
77
100
  const result = await credential.getToken(scopes);
78
101
  if (!result) {
102
+ logger.error(`${type}: Failed to obtain token - credential.getToken returned null/undefined`);
79
103
  throw new Error("Failed to obtain Azure DevOps token. Ensure you have Azure CLI logged or use interactive type of authentication.");
80
104
  }
105
+ logger.debug(`${type}: Successfully obtained Azure DevOps token`);
81
106
  return result.token;
82
107
  };
83
108
  default:
109
+ logger.debug(`Authenticator: Using OAuth interactive authentication (default)`);
84
110
  const authenticator = new OAuthAuthenticator(tenantId);
85
111
  return () => {
86
112
  return authenticator.getToken();
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { getBearerHandler, WebApi } from "azure-devops-node-api";
7
7
  import yargs from "yargs";
8
8
  import { hideBin } from "yargs/helpers";
9
9
  import { createAuthenticator } from "./auth.js";
10
+ import { logger } from "./logger.js";
10
11
  import { getOrgTenant } from "./org-tenants.js";
11
12
  //import { configurePrompts } from "./prompts.js";
12
13
  import { configureAllTools } from "./tools.js";
@@ -67,6 +68,16 @@ function getAzureDevOpsClient(getAzureDevOpsToken, userAgentComposer) {
67
68
  };
68
69
  }
69
70
  async function main() {
71
+ logger.info("Starting Azure DevOps MCP Server", {
72
+ organization: orgName,
73
+ organizationUrl: orgUrl,
74
+ authentication: argv.authentication,
75
+ tenant: argv.tenant,
76
+ domains: argv.domains,
77
+ enabledDomains: Array.from(enabledDomains),
78
+ version: packageVersion,
79
+ isCodespace: isGitHubCodespaceEnv(),
80
+ });
70
81
  const server = new McpServer({
71
82
  name: "Azure DevOps MCP Server",
72
83
  version: packageVersion,
@@ -89,6 +100,6 @@ async function main() {
89
100
  await server.connect(transport);
90
101
  }
91
102
  main().catch((error) => {
92
- console.error("Fatal error in main():", error);
103
+ logger.error("Fatal error in main():", error);
93
104
  process.exit(1);
94
105
  });
package/dist/logger.js ADDED
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import winston from "winston";
4
+ import { setLogLevel } from "@azure/logger";
5
+ const logLevel = process.env.LOG_LEVEL?.toLowerCase();
6
+ if (logLevel && ["verbose", "debug", "info", "warning", "error"].includes(logLevel)) {
7
+ // Map Winston log levels to Azure log levels
8
+ const logLevelMap = {
9
+ verbose: "verbose",
10
+ debug: "info",
11
+ info: "info",
12
+ warning: "warning",
13
+ error: "error",
14
+ };
15
+ const azureLogLevel = logLevelMap[logLevel];
16
+ setLogLevel(azureLogLevel);
17
+ }
18
+ /**
19
+ * Logger utility for MCP server
20
+ *
21
+ * Since MCP servers use stdio transport for communication on stdout,
22
+ * we log to stderr to avoid interfering with the MCP protocol.
23
+ */
24
+ export const logger = winston.createLogger({
25
+ level: process.env.LOG_LEVEL || "info",
26
+ format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()),
27
+ transports: [
28
+ new winston.transports.Stream({
29
+ stream: process.stderr,
30
+ }),
31
+ ],
32
+ // Prevent Winston from exiting on error
33
+ exitOnError: false,
34
+ });
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import { readFile, writeFile } from "fs/promises";
4
+ import { logger } from "./logger.js";
4
5
  import { homedir } from "os";
5
6
  import { join } from "path";
6
7
  const CACHE_FILE = join(homedir(), ".ado_orgs.cache");
@@ -20,7 +21,7 @@ async function trySavingCache(cache) {
20
21
  await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
21
22
  }
22
23
  catch (error) {
23
- console.error("Failed to save org tenants cache:", error);
24
+ logger.error("Failed to save org tenants cache:", error);
24
25
  }
25
26
  }
26
27
  async function fetchTenantFromApi(orgName) {
@@ -65,11 +66,11 @@ export async function getOrgTenant(orgName) {
65
66
  catch (error) {
66
67
  // If we have an expired cache entry, return it as fallback
67
68
  if (cachedEntry) {
68
- console.error(`Failed to fetch fresh tenant for ADO org ${orgName}, using expired cache entry:`, error);
69
+ logger.error(`Failed to fetch fresh tenant for ADO org ${orgName}, using expired cache entry:`, error);
69
70
  return cachedEntry.tenantId;
70
71
  }
71
72
  // No cache entry available, log and return empty result
72
- console.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
73
+ logger.error(`Failed to fetch tenant for ADO org ${orgName}:`, error);
73
74
  return undefined;
74
75
  }
75
76
  }
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { logger } from "../logger.js";
3
4
  /**
4
5
  * Available Azure DevOps MCP domains
5
6
  */
@@ -68,7 +69,7 @@ export class DomainsManager {
68
69
  this.enableAllDomains();
69
70
  }
70
71
  else {
71
- console.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
72
+ logger.error(`Error: Specified invalid domain '${domain}'. Please specify exactly as available domains: ${Object.values(Domain).join(", ")}`);
72
73
  }
73
74
  });
74
75
  if (this.enabledDomains.size === 0) {
@@ -65,8 +65,8 @@ function configurePipelineTools(server, tokenProvider, connectionProvider, userA
65
65
  }, async ({ project, name, folder, yamlPath, repositoryType, repositoryName, repositoryId, repositoryConnectionId }) => {
66
66
  const connection = await connectionProvider();
67
67
  const pipelinesApi = await connection.getPipelinesApi();
68
- let repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
69
- let repositoryPayload = {
68
+ const repositoryTypeEnumValue = safeEnumConvert(RepositoryType, repositoryType);
69
+ const repositoryPayload = {
70
70
  type: repositoryType,
71
71
  };
72
72
  if (repositoryTypeEnumValue === RepositoryType.AzureReposGit) {
@@ -94,6 +94,7 @@ function trimPullRequest(pr, includeDescription = false) {
94
94
  uniqueName: pr.createdBy?.uniqueName,
95
95
  },
96
96
  creationDate: pr.creationDate,
97
+ closedDate: pr.closedDate,
97
98
  title: pr.title,
98
99
  ...(includeDescription ? { description: pr.description ?? "" } : {}),
99
100
  isDraft: pr.isDraft,
@@ -472,14 +473,38 @@ function configureRepoTools(server, tokenProvider, connectionProvider, userAgent
472
473
  project: z.string().optional().describe("Project ID or project name (optional)"),
473
474
  iteration: z.number().optional().describe("The iteration ID for which to retrieve threads. Optional, defaults to the latest iteration."),
474
475
  baseIteration: z.number().optional().describe("The base iteration ID for which to retrieve threads. Optional, defaults to the latest base iteration."),
475
- top: z.number().default(100).describe("The maximum number of threads to return."),
476
- skip: z.number().default(0).describe("The number of threads to skip."),
476
+ top: z.number().default(100).describe("The maximum number of threads to return after filtering."),
477
+ skip: z.number().default(0).describe("The number of threads to skip after filtering."),
477
478
  fullResponse: z.boolean().optional().default(false).describe("Return full thread JSON response instead of trimmed data."),
478
- }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse }) => {
479
+ status: z
480
+ .enum(getEnumKeys(CommentThreadStatus))
481
+ .optional()
482
+ .describe("Filter threads by status. If not specified, returns threads of all statuses."),
483
+ authorEmail: z.string().optional().describe("Filter threads by the email of the thread author (first comment author)."),
484
+ authorDisplayName: z.string().optional().describe("Filter threads by the display name of the thread author (first comment author). Case-insensitive partial matching."),
485
+ }, async ({ repositoryId, pullRequestId, project, iteration, baseIteration, top, skip, fullResponse, status, authorEmail, authorDisplayName }) => {
479
486
  const connection = await connectionProvider();
480
487
  const gitApi = await connection.getGitApi();
481
488
  const threads = await gitApi.getThreads(repositoryId, pullRequestId, project, iteration, baseIteration);
482
- const paginatedThreads = threads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
489
+ let filteredThreads = threads;
490
+ if (status !== undefined) {
491
+ const statusValue = CommentThreadStatus[status];
492
+ filteredThreads = filteredThreads?.filter((thread) => thread.status === statusValue);
493
+ }
494
+ if (authorEmail !== undefined) {
495
+ filteredThreads = filteredThreads?.filter((thread) => {
496
+ const firstComment = thread.comments?.[0];
497
+ return firstComment?.author?.uniqueName?.toLowerCase() === authorEmail.toLowerCase();
498
+ });
499
+ }
500
+ if (authorDisplayName !== undefined) {
501
+ const lowerAuthorName = authorDisplayName.toLowerCase();
502
+ filteredThreads = filteredThreads?.filter((thread) => {
503
+ const firstComment = thread.comments?.[0];
504
+ return firstComment?.author?.displayName?.toLowerCase().includes(lowerAuthorName);
505
+ });
506
+ }
507
+ const paginatedThreads = filteredThreads?.sort((a, b) => (a.id ?? 0) - (b.id ?? 0)).slice(skip, skip + top);
483
508
  if (fullResponse) {
484
509
  return {
485
510
  content: [{ type: "text", text: JSON.stringify(paginatedThreads, null, 2) }],
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.2.2-nightly.20251120";
1
+ export const packageVersion = "2.2.2-nightly.20251125";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.2.2-nightly.20251120",
3
+ "version": "2.2.2-nightly.20251125",
4
4
  "description": "MCP server for interacting with Azure DevOps",
5
5
  "license": "MIT",
6
6
  "author": "Microsoft Corporation",
@@ -25,7 +25,7 @@
25
25
  "prebuild": "node -p \"'export const packageVersion = ' + JSON.stringify(require('./package.json').version) + ';\\n'\" > src/version.ts && prettier --write src/version.ts",
26
26
  "validate-tools": "tsc --noEmit && node scripts/build-validate-tools.js",
27
27
  "build": "tsc && shx chmod +x dist/*.js",
28
- "prepare": "npm run build",
28
+ "prepare": "npm run build && husky",
29
29
  "watch": "tsc --watch",
30
30
  "inspect": "ALLOWED_ORIGINS=http://127.0.0.1:6274 npx @modelcontextprotocol/inspector node dist/index.js",
31
31
  "start": "node -r tsconfig-paths/register dist/index.js",
@@ -43,6 +43,7 @@
43
43
  "azure-devops-extension-api": "^4.252.0",
44
44
  "azure-devops-extension-sdk": "^4.0.2",
45
45
  "azure-devops-node-api": "^15.1.0",
46
+ "winston": "^3.18.3",
46
47
  "yargs": "^18.0.0",
47
48
  "zod": "^3.25.63",
48
49
  "zod-to-json-schema": "^3.24.5"
@@ -50,17 +51,24 @@
50
51
  "devDependencies": {
51
52
  "@modelcontextprotocol/inspector": "^0.17.0",
52
53
  "@types/jest": "^30.0.0",
53
- "@types/node": "^22",
54
+ "@types/node": "^22.19.1",
54
55
  "eslint-config-prettier": "10.1.8",
55
56
  "eslint-plugin-header": "^3.1.1",
56
57
  "glob": "^11.0.3",
58
+ "husky": "^9.1.7",
57
59
  "jest": "^30.0.2",
58
60
  "jest-extended": "^7.0.0",
61
+ "lint-staged": "^16.2.7",
59
62
  "prettier": "3.6.2",
60
63
  "shx": "^0.4.0",
61
64
  "ts-jest": "^29.4.0",
62
65
  "tsconfig-paths": "^4.2.0",
63
66
  "typescript": "^5.9.3",
64
67
  "typescript-eslint": "^8.46.4"
68
+ },
69
+ "lint-staged": {
70
+ "**/*.(js|ts|jsx|tsx|json|css|md)": [
71
+ "npm run format"
72
+ ]
65
73
  }
66
74
  }