@softeria/ms-365-mcp-server 0.28.2 → 0.29.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/.env.example CHANGED
@@ -4,7 +4,7 @@
4
4
  # Your Azure AD App Registration Client ID
5
5
  MS365_MCP_CLIENT_ID=your-azure-ad-app-client-id-here
6
6
 
7
- # Your Azure AD App Registration Client Secret
7
+ # Your Azure AD App Registration Client Secret (optional, for confidential clients)
8
8
  MS365_MCP_CLIENT_SECRET=your-azure-ad-app-client-secret-here
9
9
 
10
10
  # Tenant ID - use "common" for multi-tenant or your specific tenant ID
@@ -21,4 +21,24 @@ MS365_MCP_TENANT_ID=common
21
21
  # 5. Copy the Client ID from Overview page
22
22
  # 6. Go to Certificates & secrets → New client secret → Copy the secret value
23
23
  # 7. Replace the values above with your actual credentials
24
- # 8. Rename this file to .env
24
+ # 8. Rename this file to .env
25
+
26
+ # -------------------------------------------------------------------
27
+ # Azure Key Vault Integration (Optional)
28
+ # -------------------------------------------------------------------
29
+ # When set, secrets are fetched from Azure Key Vault instead of environment variables.
30
+ # This is useful for production deployments, especially with Azure Container Apps.
31
+ #
32
+ # MS365_MCP_KEYVAULT_URL=https://your-keyvault-name.vault.azure.net
33
+ #
34
+ # Key Vault secret names (store these in your Key Vault):
35
+ # - ms365-mcp-client-id (required)
36
+ # - ms365-mcp-tenant-id (optional, defaults to "common")
37
+ # - ms365-mcp-client-secret (optional)
38
+ #
39
+ # Authentication uses DefaultAzureCredential, which supports:
40
+ # - Managed Identity (recommended for Azure Container Apps)
41
+ # - Azure CLI credentials (for local development)
42
+ # - Environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
43
+ #
44
+ # See README.md for detailed Azure Key Vault setup instructions.
package/README.md CHANGED
@@ -395,6 +395,74 @@ Environment variables:
395
395
  - `MS365_MCP_CLIENT_ID`: Custom Azure app client ID (defaults to built-in app)
396
396
  - `MS365_MCP_TENANT_ID`: Custom tenant ID (defaults to 'common' for multi-tenant)
397
397
  - `MS365_MCP_OAUTH_TOKEN`: Pre-existing OAuth token for Microsoft Graph API (BYOT method)
398
+ - `MS365_MCP_KEYVAULT_URL`: Azure Key Vault URL for secrets management (see Azure Key Vault section)
399
+
400
+ ## Azure Key Vault Integration
401
+
402
+ For production deployments, you can store secrets in Azure Key Vault instead of environment variables. This is particularly useful for Azure Container Apps with managed identity.
403
+
404
+ ### Setup
405
+
406
+ 1. **Create a Key Vault** (if you don't have one):
407
+
408
+ ```bash
409
+ az keyvault create --name your-keyvault-name --resource-group your-rg --location eastus
410
+ ```
411
+
412
+ 2. **Add secrets to Key Vault**:
413
+
414
+ ```bash
415
+ az keyvault secret set --vault-name your-keyvault-name --name ms365-mcp-client-id --value "your-client-id"
416
+ az keyvault secret set --vault-name your-keyvault-name --name ms365-mcp-tenant-id --value "your-tenant-id"
417
+ # Optional: if using confidential client flow
418
+ az keyvault secret set --vault-name your-keyvault-name --name ms365-mcp-client-secret --value "your-secret"
419
+ ```
420
+
421
+ 3. **Grant access to Key Vault**:
422
+
423
+ For Azure Container Apps with managed identity:
424
+
425
+ ```bash
426
+ # Get the managed identity principal ID
427
+ PRINCIPAL_ID=$(az containerapp show --name your-app --resource-group your-rg --query identity.principalId -o tsv)
428
+
429
+ # Grant access to Key Vault secrets
430
+ az keyvault set-policy --name your-keyvault-name --object-id $PRINCIPAL_ID --secret-permissions get list
431
+ ```
432
+
433
+ For local development with Azure CLI:
434
+
435
+ ```bash
436
+ # Your Azure CLI identity already has access if you have appropriate RBAC roles
437
+ az login
438
+ ```
439
+
440
+ 4. **Configure the server**:
441
+ ```bash
442
+ MS365_MCP_KEYVAULT_URL=https://your-keyvault-name.vault.azure.net npx @softeria/ms-365-mcp-server
443
+ ```
444
+
445
+ ### Secret Name Mapping
446
+
447
+ | Key Vault Secret Name | Environment Variable | Required |
448
+ | ----------------------- | ----------------------- | ------------------------- |
449
+ | ms365-mcp-client-id | MS365_MCP_CLIENT_ID | Yes |
450
+ | ms365-mcp-tenant-id | MS365_MCP_TENANT_ID | No (defaults to 'common') |
451
+ | ms365-mcp-client-secret | MS365_MCP_CLIENT_SECRET | No |
452
+
453
+ ### Authentication
454
+
455
+ The Key Vault integration uses `DefaultAzureCredential` from the Azure Identity SDK, which automatically tries multiple authentication methods in order:
456
+
457
+ 1. Environment variables (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID)
458
+ 2. Managed Identity (recommended for Azure Container Apps)
459
+ 3. Azure CLI credentials (for local development)
460
+ 4. Visual Studio Code credentials
461
+ 5. Azure PowerShell credentials
462
+
463
+ ### Optional Dependencies
464
+
465
+ The Azure Key Vault packages (`@azure/identity` and `@azure/keyvault-secrets`) are optional dependencies. They are only loaded when `MS365_MCP_KEYVAULT_URL` is configured. If you don't use Key Vault, these packages are not required.
398
466
 
399
467
  ## Contributing
400
468
 
package/dist/auth.js CHANGED
@@ -3,6 +3,7 @@ import logger from "./logger.js";
3
3
  import fs, { existsSync, readFileSync } from "fs";
4
4
  import { fileURLToPath } from "url";
5
5
  import path from "path";
6
+ import { getSecrets } from "./secrets.js";
6
7
  let keytar = null;
7
8
  async function getKeytar() {
8
9
  if (keytar === void 0) {
@@ -34,12 +35,14 @@ const SELECTED_ACCOUNT_KEY = "selected-account";
34
35
  const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
35
36
  const FALLBACK_PATH = path.join(FALLBACK_DIR, "..", ".token-cache.json");
36
37
  const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, "..", ".selected-account.json");
37
- const DEFAULT_CONFIG = {
38
- auth: {
39
- clientId: process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e",
40
- authority: `https://login.microsoftonline.com/${process.env.MS365_MCP_TENANT_ID || "common"}`
41
- }
42
- };
38
+ function createMsalConfig(secrets) {
39
+ return {
40
+ auth: {
41
+ clientId: secrets.clientId || "084a3e9f-a9f4-43f7-89f9-d229cf97853e",
42
+ authority: `https://login.microsoftonline.com/${secrets.tenantId || "common"}`
43
+ }
44
+ };
45
+ }
43
46
  const SCOPE_HIERARCHY = {
44
47
  "Mail.ReadWrite": ["Mail.Read"],
45
48
  "Calendars.ReadWrite": ["Calendars.Read"],
@@ -87,7 +90,7 @@ function buildScopesFromEndpoints(includeWorkAccountScopes = false, enabledTools
87
90
  return scopes;
88
91
  }
89
92
  class AuthManager {
90
- constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
93
+ constructor(config, scopes = buildScopesFromEndpoints()) {
91
94
  logger.info(`And scopes are ${scopes.join(", ")}`, scopes);
92
95
  this.config = config;
93
96
  this.scopes = scopes;
@@ -99,6 +102,15 @@ class AuthManager {
99
102
  this.oauthToken = oauthTokenFromEnv ?? null;
100
103
  this.isOAuthMode = oauthTokenFromEnv != null;
101
104
  }
105
+ /**
106
+ * Creates an AuthManager instance with secrets loaded from the configured provider.
107
+ * Uses Key Vault if MS365_MCP_KEYVAULT_URL is set, otherwise environment variables.
108
+ */
109
+ static async create(scopes = buildScopesFromEndpoints()) {
110
+ const secrets = await getSecrets();
111
+ const config = createMsalConfig(secrets);
112
+ return new AuthManager(config, scopes);
113
+ }
102
114
  async loadTokenCache() {
103
115
  try {
104
116
  let cacheData;
@@ -2,11 +2,12 @@ import logger from "./logger.js";
2
2
  import { refreshAccessToken } from "./lib/microsoft-auth.js";
3
3
  import { encode as toonEncode } from "@toon-format/toon";
4
4
  class GraphClient {
5
- constructor(authManager, outputFormat = "json") {
5
+ constructor(authManager, secrets, outputFormat = "json") {
6
6
  this.accessToken = null;
7
7
  this.refreshToken = null;
8
8
  this.outputFormat = "json";
9
9
  this.authManager = authManager;
10
+ this.secrets = secrets;
10
11
  this.outputFormat = outputFormat;
11
12
  }
12
13
  setOAuthTokens(accessToken, refreshToken) {
@@ -72,9 +73,9 @@ class GraphClient {
72
73
  }
73
74
  }
74
75
  async refreshAccessToken(refreshToken) {
75
- const tenantId = process.env.MS365_MCP_TENANT_ID || "common";
76
- const clientId = process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e";
77
- const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
76
+ const tenantId = this.secrets.tenantId || "common";
77
+ const clientId = this.secrets.clientId;
78
+ const clientSecret = this.secrets.clientSecret;
78
79
  if (clientSecret) {
79
80
  logger.info("GraphClient: Refreshing token with confidential client");
80
81
  } else {
@@ -251,7 +251,10 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
251
251
  paramSchema,
252
252
  {
253
253
  title: tool.alias,
254
- readOnlyHint: tool.method.toUpperCase() === "GET"
254
+ readOnlyHint: tool.method.toUpperCase() === "GET",
255
+ destructiveHint: ["POST", "PATCH", "DELETE"].includes(tool.method.toUpperCase()),
256
+ openWorldHint: true
257
+ // All tools call Microsoft Graph API
255
258
  },
256
259
  async (params) => executeGraphTool(tool, endpointConfig, graphClient, params)
257
260
  );
@@ -295,7 +298,9 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
295
298
  },
296
299
  {
297
300
  title: "search-tools",
298
- readOnlyHint: true
301
+ readOnlyHint: true,
302
+ openWorldHint: true
303
+ // Searches Microsoft Graph API tools
299
304
  },
300
305
  async ({ query, category, limit = 20 }) => {
301
306
  const maxLimit = Math.min(limit, 50);
@@ -348,7 +353,11 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
348
353
  },
349
354
  {
350
355
  title: "execute-tool",
351
- readOnlyHint: false
356
+ readOnlyHint: false,
357
+ destructiveHint: true,
358
+ // Can execute any tool, including write operations
359
+ openWorldHint: true
360
+ // Executes against Microsoft Graph API
352
361
  },
353
362
  async ({ tool_name, parameters = {} }) => {
354
363
  const toolData = toolsRegistry.get(tool_name);
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ async function main() {
13
13
  logger.info("Organization mode enabled - including work account scopes");
14
14
  }
15
15
  const scopes = buildScopesFromEndpoints(includeWorkScopes, args.enabledTools);
16
- const authManager = new AuthManager(void 0, scopes);
16
+ const authManager = await AuthManager.create(scopes);
17
17
  await authManager.loadTokenCache();
18
18
  if (args.login) {
19
19
  await authManager.acquireTokenByDeviceCode();
@@ -1,9 +1,9 @@
1
1
  import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
2
2
  import logger from "./logger.js";
3
3
  class MicrosoftOAuthProvider extends ProxyOAuthServerProvider {
4
- constructor(authManager) {
5
- const tenantId = process.env.MS365_MCP_TENANT_ID || "common";
6
- const clientId = process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e";
4
+ constructor(authManager, secrets) {
5
+ const tenantId = secrets.tenantId || "common";
6
+ const clientId = secrets.clientId;
7
7
  super({
8
8
  endpoints: {
9
9
  authorizationUrl: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
@@ -0,0 +1,61 @@
1
+ import logger from "./logger.js";
2
+ class EnvironmentSecretsProvider {
3
+ async getSecrets() {
4
+ return {
5
+ clientId: process.env.MS365_MCP_CLIENT_ID || "",
6
+ tenantId: process.env.MS365_MCP_TENANT_ID || "common",
7
+ clientSecret: process.env.MS365_MCP_CLIENT_SECRET
8
+ };
9
+ }
10
+ }
11
+ class KeyVaultSecretsProvider {
12
+ constructor(vaultUrl) {
13
+ this.vaultUrl = vaultUrl;
14
+ }
15
+ async getSecrets() {
16
+ const { DefaultAzureCredential } = await import("@azure/identity");
17
+ const { SecretClient } = await import("@azure/keyvault-secrets");
18
+ const credential = new DefaultAzureCredential();
19
+ const client = new SecretClient(this.vaultUrl, credential);
20
+ logger.info(`Fetching secrets from Key Vault: ${this.vaultUrl}`);
21
+ const [clientIdSecret, tenantIdSecret, clientSecretResult] = await Promise.all([
22
+ client.getSecret("ms365-mcp-client-id"),
23
+ client.getSecret("ms365-mcp-tenant-id").catch(() => null),
24
+ client.getSecret("ms365-mcp-client-secret").catch(() => null)
25
+ ]);
26
+ if (!clientIdSecret.value) {
27
+ throw new Error("Required secret ms365-mcp-client-id not found in Key Vault");
28
+ }
29
+ logger.info("Successfully retrieved secrets from Key Vault");
30
+ return {
31
+ clientId: clientIdSecret.value,
32
+ tenantId: tenantIdSecret?.value || "common",
33
+ clientSecret: clientSecretResult?.value
34
+ };
35
+ }
36
+ }
37
+ function createSecretsProvider() {
38
+ const vaultUrl = process.env.MS365_MCP_KEYVAULT_URL;
39
+ if (vaultUrl) {
40
+ logger.info("Key Vault URL configured, using Azure Key Vault for secrets");
41
+ return new KeyVaultSecretsProvider(vaultUrl);
42
+ }
43
+ logger.info("Using environment variables for secrets");
44
+ return new EnvironmentSecretsProvider();
45
+ }
46
+ let cachedSecrets = null;
47
+ async function getSecrets() {
48
+ if (cachedSecrets) {
49
+ return cachedSecrets;
50
+ }
51
+ const provider = createSecretsProvider();
52
+ cachedSecrets = await provider.getSecrets();
53
+ return cachedSecrets;
54
+ }
55
+ function clearSecretsCache() {
56
+ cachedSecrets = null;
57
+ }
58
+ export {
59
+ clearSecretsCache,
60
+ getSecrets
61
+ };
package/dist/server.js CHANGED
@@ -3,7 +3,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
5
5
  import express from "express";
6
- import crypto from "crypto";
7
6
  import logger, { enableConsoleLogging } from "./logger.js";
8
7
  import { registerAuthTools } from "./auth-tools.js";
9
8
  import { registerGraphTools, registerDiscoveryTools } from "./graph-tools.js";
@@ -15,7 +14,7 @@ import {
15
14
  microsoftBearerTokenAuthMiddleware,
16
15
  refreshAccessToken
17
16
  } from "./lib/microsoft-auth.js";
18
- const registeredClients = /* @__PURE__ */ new Map();
17
+ import { getSecrets } from "./secrets.js";
19
18
  function parseHttpOption(httpOption) {
20
19
  if (typeof httpOption === "boolean") {
21
20
  return { host: void 0, port: 3e3 };
@@ -34,11 +33,14 @@ class MicrosoftGraphServer {
34
33
  constructor(authManager, options = {}) {
35
34
  this.authManager = authManager;
36
35
  this.options = options;
37
- const outputFormat = options.toon ? "toon" : "json";
38
- this.graphClient = new GraphClient(authManager, outputFormat);
36
+ this.graphClient = null;
39
37
  this.server = null;
38
+ this.secrets = null;
40
39
  }
41
40
  async initialize(version) {
41
+ this.secrets = await getSecrets();
42
+ const outputFormat = this.options.toon ? "toon" : "json";
43
+ this.graphClient = new GraphClient(this.authManager, this.secrets, outputFormat);
42
44
  this.server = new McpServer({
43
45
  name: "Microsoft365MCP",
44
46
  version
@@ -70,10 +72,10 @@ class MicrosoftGraphServer {
70
72
  enableConsoleLogging();
71
73
  }
72
74
  logger.info("Microsoft 365 MCP Server starting...");
73
- logger.info("Environment Variables Check:", {
74
- CLIENT_ID: process.env.MS365_MCP_CLIENT_ID ? `${process.env.MS365_MCP_CLIENT_ID.substring(0, 8)}...` : "NOT SET",
75
- CLIENT_SECRET: process.env.MS365_MCP_CLIENT_SECRET ? "SET" : "NOT SET",
76
- TENANT_ID: process.env.MS365_MCP_TENANT_ID || "NOT SET",
75
+ logger.info("Secrets Check:", {
76
+ CLIENT_ID: this.secrets?.clientId ? `${this.secrets.clientId.substring(0, 8)}...` : "NOT SET",
77
+ CLIENT_SECRET: this.secrets?.clientSecret ? "SET" : "NOT SET",
78
+ TENANT_ID: this.secrets?.tenantId || "NOT SET",
77
79
  NODE_ENV: process.env.NODE_ENV || "NOT SET"
78
80
  });
79
81
  if (this.options.readOnly) {
@@ -99,7 +101,7 @@ class MicrosoftGraphServer {
99
101
  }
100
102
  next();
101
103
  });
102
- const oauthProvider = new MicrosoftOAuthProvider(this.authManager);
104
+ const oauthProvider = new MicrosoftOAuthProvider(this.authManager, this.secrets);
103
105
  app.get("/.well-known/oauth-authorization-server", async (req, res) => {
104
106
  const protocol = req.secure ? "https" : "http";
105
107
  const url = new URL(`${protocol}://${req.get("host")}`);
@@ -108,7 +110,6 @@ class MicrosoftGraphServer {
108
110
  issuer: url.origin,
109
111
  authorization_endpoint: `${url.origin}/authorize`,
110
112
  token_endpoint: `${url.origin}/token`,
111
- registration_endpoint: `${url.origin}/register`,
112
113
  response_types_supported: ["code"],
113
114
  response_modes_supported: ["query"],
114
115
  grant_types_supported: ["authorization_code", "refresh_token"],
@@ -129,33 +130,10 @@ class MicrosoftGraphServer {
129
130
  resource_documentation: `${url.origin}`
130
131
  });
131
132
  });
132
- app.post("/register", async (req, res) => {
133
- const body = req.body;
134
- const clientId = crypto.randomUUID();
135
- registeredClients.set(clientId, {
136
- client_id: clientId,
137
- client_name: body.client_name || "MCP Client",
138
- redirect_uris: body.redirect_uris || [],
139
- grant_types: body.grant_types || ["authorization_code", "refresh_token"],
140
- response_types: body.response_types || ["code"],
141
- scope: body.scope,
142
- token_endpoint_auth_method: "none",
143
- created_at: Date.now()
144
- });
145
- res.status(201).json({
146
- client_id: clientId,
147
- client_name: body.client_name || "MCP Client",
148
- redirect_uris: body.redirect_uris || [],
149
- grant_types: body.grant_types || ["authorization_code", "refresh_token"],
150
- response_types: body.response_types || ["code"],
151
- scope: body.scope,
152
- token_endpoint_auth_method: "none"
153
- });
154
- });
155
133
  app.get("/authorize", async (req, res) => {
156
134
  const url = new URL(req.url, `${req.protocol}://${req.get("host")}`);
157
- const tenantId = process.env.MS365_MCP_TENANT_ID || "common";
158
- const clientId = process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e";
135
+ const tenantId = this.secrets?.tenantId || "common";
136
+ const clientId = this.secrets.clientId;
159
137
  const microsoftAuthUrl = new URL(
160
138
  `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`
161
139
  );
@@ -209,9 +187,9 @@ class MicrosoftGraphServer {
209
187
  return;
210
188
  }
211
189
  if (body.grant_type === "authorization_code") {
212
- const tenantId = process.env.MS365_MCP_TENANT_ID || "common";
213
- const clientId = process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e";
214
- const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
190
+ const tenantId = this.secrets?.tenantId || "common";
191
+ const clientId = this.secrets.clientId;
192
+ const clientSecret = this.secrets?.clientSecret;
215
193
  if (clientSecret) {
216
194
  logger.info("Token endpoint: Using confidential client with client_secret");
217
195
  } else {
@@ -227,9 +205,9 @@ class MicrosoftGraphServer {
227
205
  );
228
206
  res.json(result);
229
207
  } else if (body.grant_type === "refresh_token") {
230
- const tenantId = process.env.MS365_MCP_TENANT_ID || "common";
231
- const clientId = process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e";
232
- const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
208
+ const tenantId = this.secrets?.tenantId || "common";
209
+ const clientId = this.secrets.clientId;
210
+ const clientSecret = this.secrets?.clientSecret;
233
211
  if (clientSecret) {
234
212
  logger.info("Refresh endpoint: Using confidential client with client_secret");
235
213
  } else {
package/logs/error.log ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ 2025-12-30 13:54:31 INFO: Using environment variables for secrets
2
+ 2025-12-30 13:54:31 INFO: Using environment variables for secrets
3
+ 2025-12-30 13:54:31 INFO: Using environment variables for secrets
4
+ 2025-12-30 13:54:31 INFO: Using environment variables for secrets
5
+ 2025-12-30 13:54:31 INFO: Using environment variables for secrets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.28.2",
3
+ "version": "0.29.0",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,6 +44,8 @@
44
44
  "zod": "^3.24.2"
45
45
  },
46
46
  "optionalDependencies": {
47
+ "@azure/identity": "^4.5.0",
48
+ "@azure/keyvault-secrets": "^4.9.0",
47
49
  "keytar": "^7.9.0"
48
50
  },
49
51
  "devDependencies": {
@@ -56,6 +58,7 @@
56
58
  "@types/node": "^22.15.15",
57
59
  "@typescript-eslint/eslint-plugin": "^8.38.0",
58
60
  "@typescript-eslint/parser": "^8.38.0",
61
+ "@vitest/coverage-v8": "^3.2.4",
59
62
  "eslint": "^9.31.0",
60
63
  "globals": "^16.3.0",
61
64
  "patch-package": "^8.0.1",