@softeria/ms-365-mcp-server 0.3.5 → 0.4.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.
Files changed (49) hide show
  1. package/.github/workflows/build.yml +3 -0
  2. package/.github/workflows/npm-publish.yml +2 -0
  3. package/README.md +1 -1
  4. package/bin/generate-graph-client.mjs +59 -0
  5. package/bin/{download-openapi.mjs → modules/download-openapi.mjs} +10 -20
  6. package/bin/modules/extract-descriptions.mjs +48 -0
  7. package/bin/modules/generate-mcp-tools.mjs +36 -0
  8. package/bin/modules/simplified-openapi.mjs +78 -0
  9. package/dist/auth-tools.js +80 -0
  10. package/dist/auth.js +219 -0
  11. package/dist/cli.js +21 -0
  12. package/dist/endpoints.json +375 -0
  13. package/dist/generated/client.js +14683 -0
  14. package/dist/generated/endpoint-types.js +1 -0
  15. package/dist/generated/hack.js +37 -0
  16. package/dist/graph-client.js +254 -0
  17. package/dist/graph-tools.js +98 -0
  18. package/dist/index.js +39 -0
  19. package/dist/logger.js +33 -0
  20. package/dist/server.js +32 -0
  21. package/{src/version.mjs → dist/version.js} +0 -2
  22. package/package.json +12 -9
  23. package/src/{auth-tools.mjs → auth-tools.ts} +7 -5
  24. package/src/{auth.mjs → auth.ts} +60 -30
  25. package/src/{cli.mjs → cli.ts} +9 -1
  26. package/src/endpoints.json +375 -0
  27. package/src/generated/README.md +51 -0
  28. package/src/generated/client.ts +24916 -0
  29. package/src/generated/endpoint-types.ts +27 -0
  30. package/src/generated/hack.ts +50 -0
  31. package/src/{graph-client.mjs → graph-client.ts} +53 -18
  32. package/src/graph-tools.ts +174 -0
  33. package/{index.mjs → src/index.ts} +6 -6
  34. package/src/{logger.mjs → logger.ts} +1 -1
  35. package/src/{server.mjs → server.ts} +16 -9
  36. package/src/version.ts +9 -0
  37. package/test/{auth-tools.test.js → auth-tools.test.ts} +41 -38
  38. package/test/{cli.test.js → cli.test.ts} +3 -3
  39. package/test/{graph-api.test.js → graph-api.test.ts} +5 -5
  40. package/test/test-hack.ts +17 -0
  41. package/tsconfig.json +16 -0
  42. package/src/dynamic-tools.mjs +0 -442
  43. package/src/openapi-helpers.mjs +0 -187
  44. package/src/param-mapper.mjs +0 -30
  45. package/test/dynamic-tools.test.js +0 -852
  46. package/test/mappings.test.js +0 -29
  47. package/test/mcp-server.test.js +0 -36
  48. package/test/openapi-helpers.test.js +0 -210
  49. package/test/param-mapper.test.js +0 -56
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+
3
+ export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
4
+
5
+ export type Parameter = {
6
+ name: string;
7
+ type: 'Query' | 'Path' | 'Body' | 'Header';
8
+ schema: z.ZodType<any>;
9
+ description?: string;
10
+ };
11
+
12
+ export type Endpoint = {
13
+ method: HttpMethod;
14
+ path: string;
15
+ alias: string;
16
+ description?: string;
17
+ requestFormat: 'json';
18
+ parameters?: Parameter[];
19
+ response: z.ZodType<any>;
20
+ errors?: Array<{
21
+ status: number;
22
+ description?: string;
23
+ schema?: z.ZodType<any>;
24
+ }>;
25
+ };
26
+
27
+ export type Endpoints = Endpoint[];
@@ -0,0 +1,50 @@
1
+ import { Endpoint, Parameter } from './endpoint-types.js';
2
+ import { z } from 'zod';
3
+
4
+ export function makeApi(endpoints: Endpoint[]) {
5
+ return endpoints;
6
+ }
7
+
8
+ export class Zodios {
9
+ endpoints: Endpoint[];
10
+
11
+ constructor(baseUrlOrEndpoints: Endpoint[] | string, endpoints?: any, options?: any) {
12
+ if (typeof baseUrlOrEndpoints === 'string') {
13
+ throw new Error('No such hack');
14
+ }
15
+ this.endpoints = baseUrlOrEndpoints.map((endpoint) => {
16
+ endpoint.parameters = endpoint.parameters || [];
17
+ for (const parameter of endpoint.parameters) {
18
+ // We need a hack since MCP won't support $ in parameter names
19
+ parameter.name = parameter.name.replace(/\$/g, '__');
20
+ }
21
+
22
+ const pathParamRegex = /:([a-zA-Z0-9]+)/g;
23
+ const pathParams = [];
24
+ let match;
25
+ while ((match = pathParamRegex.exec(endpoint.path)) !== null) {
26
+ pathParams.push(match[1]);
27
+ }
28
+
29
+ for (const pathParam of pathParams) {
30
+ const paramExists = endpoint.parameters.some(
31
+ (param) => param.name === pathParam || param.name === pathParam.replace(/\$/g, '__')
32
+ );
33
+
34
+ if (!paramExists) {
35
+ const newParam: Parameter = {
36
+ name: pathParam,
37
+ type: 'Path',
38
+ schema: z.string().describe(`Path parameter: ${pathParam}`),
39
+ description: `Path parameter: ${pathParam}`,
40
+ };
41
+ endpoint.parameters.push(newParam);
42
+ }
43
+ }
44
+
45
+ return endpoint;
46
+ });
47
+ }
48
+ }
49
+
50
+ export type ZodiosOptions = {};
@@ -1,12 +1,41 @@
1
- import logger from './logger.mjs';
1
+ import logger from './logger.js';
2
+ import AuthManager from './auth.js';
3
+
4
+ interface GraphRequestOptions {
5
+ excelFile?: string;
6
+ headers?: Record<string, string>;
7
+ method?: string;
8
+ body?: string;
9
+ rawResponse?: boolean;
10
+
11
+ [key: string]: any;
12
+ }
13
+
14
+ interface ContentItem {
15
+ type: 'text';
16
+ text: string;
17
+
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ interface McpResponse {
22
+ content: ContentItem[];
23
+ _meta?: Record<string, unknown>;
24
+ isError?: boolean;
25
+
26
+ [key: string]: unknown;
27
+ }
2
28
 
3
29
  class GraphClient {
4
- constructor(authManager) {
30
+ private authManager: AuthManager;
31
+ private sessions: Map<string, string>;
32
+
33
+ constructor(authManager: AuthManager) {
5
34
  this.authManager = authManager;
6
35
  this.sessions = new Map();
7
36
  }
8
37
 
9
- async createSession(filePath) {
38
+ async createSession(filePath: string): Promise<string | null> {
10
39
  try {
11
40
  if (!filePath) {
12
41
  logger.error('No file path provided for Excel session');
@@ -14,7 +43,7 @@ class GraphClient {
14
43
  }
15
44
 
16
45
  if (this.sessions.has(filePath)) {
17
- return this.sessions.get(filePath);
46
+ return this.sessions.get(filePath) || null;
18
47
  }
19
48
 
20
49
  logger.info(`Creating new Excel session for file: ${filePath}`);
@@ -49,12 +78,13 @@ class GraphClient {
49
78
  }
50
79
  }
51
80
 
52
- async graphRequest(endpoint, options = {}) {
81
+ async graphRequest(endpoint: string, options: GraphRequestOptions = {}): Promise<McpResponse> {
53
82
  try {
83
+ logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
54
84
  let accessToken = await this.authManager.getToken();
55
85
 
56
- let url;
57
- let sessionId = null;
86
+ let url: string;
87
+ let sessionId: string | null = null;
58
88
 
59
89
  if (
60
90
  options.excelFile &&
@@ -62,7 +92,7 @@ class GraphClient {
62
92
  !endpoint.startsWith('/users') &&
63
93
  !endpoint.startsWith('/me')
64
94
  ) {
65
- sessionId = this.sessions.get(options.excelFile);
95
+ sessionId = this.sessions.get(options.excelFile) || null;
66
96
 
67
97
  if (!sessionId) {
68
98
  sessionId = await this.createSession(options.excelFile);
@@ -87,12 +117,15 @@ class GraphClient {
87
117
  };
88
118
  }
89
119
 
90
- const headers = {
120
+ const headers: Record<string, string> = {
121
+ ...options.headers,
91
122
  Authorization: `Bearer ${accessToken}`,
92
123
  'Content-Type': 'application/json',
93
124
  ...(sessionId && { 'workbook-session-id': sessionId }),
94
- ...options.headers,
95
125
  };
126
+ delete options.headers;
127
+
128
+ logger.info(` ** Making request to ${url} with options: ${JSON.stringify(options)}`);
96
129
 
97
130
  const response = await fetch(url, {
98
131
  headers,
@@ -142,12 +175,13 @@ class GraphClient {
142
175
  } catch (error) {
143
176
  logger.error(`Error in Graph API request: ${error}`);
144
177
  return {
145
- content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
178
+ content: [{ type: 'text', text: JSON.stringify({ error: (error as Error).message }) }],
179
+ isError: true,
146
180
  };
147
181
  }
148
182
  }
149
183
 
150
- async formatResponse(response, rawResponse = false) {
184
+ async formatResponse(response: Response, rawResponse = false): Promise<McpResponse> {
151
185
  try {
152
186
  if (response.status === 204) {
153
187
  return {
@@ -188,7 +222,7 @@ class GraphClient {
188
222
 
189
223
  const result = await response.json();
190
224
 
191
- const removeODataProps = (obj) => {
225
+ const removeODataProps = (obj: any): void => {
192
226
  if (!obj || typeof obj !== 'object') return;
193
227
 
194
228
  if (Array.isArray(obj)) {
@@ -217,7 +251,7 @@ class GraphClient {
217
251
  }
218
252
  }
219
253
 
220
- async closeSession(filePath) {
254
+ async closeSession(filePath: string): Promise<McpResponse> {
221
255
  if (!filePath || !this.sessions.has(filePath)) {
222
256
  return {
223
257
  content: [
@@ -240,7 +274,7 @@ class GraphClient {
240
274
  headers: {
241
275
  Authorization: `Bearer ${accessToken}`,
242
276
  'Content-Type': 'application/json',
243
- 'workbook-session-id': sessionId,
277
+ 'workbook-session-id': sessionId!,
244
278
  },
245
279
  }
246
280
  );
@@ -267,14 +301,15 @@ class GraphClient {
267
301
  text: JSON.stringify({ error: `Failed to close session for ${filePath}` }),
268
302
  },
269
303
  ],
304
+ isError: true,
270
305
  };
271
306
  }
272
307
  }
273
308
 
274
- async closeAllSessions() {
275
- const results = [];
309
+ async closeAllSessions(): Promise<McpResponse> {
310
+ const results: McpResponse[] = [];
276
311
 
277
- for (const [filePath, _] of this.sessions) {
312
+ for (const [filePath] of this.sessions) {
278
313
  const result = await this.closeSession(filePath);
279
314
  results.push(result);
280
315
  }
@@ -0,0 +1,174 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import logger from './logger.js';
3
+ import GraphClient from './graph-client.js';
4
+ import { api } from './generated/client.js';
5
+ import { z } from 'zod';
6
+
7
+ type TextContent = {
8
+ type: 'text';
9
+ text: string;
10
+ [key: string]: unknown;
11
+ };
12
+
13
+ type ImageContent = {
14
+ type: 'image';
15
+ data: string;
16
+ mimeType: string;
17
+ [key: string]: unknown;
18
+ };
19
+
20
+ type AudioContent = {
21
+ type: 'audio';
22
+ data: string;
23
+ mimeType: string;
24
+ [key: string]: unknown;
25
+ };
26
+
27
+ type ResourceTextContent = {
28
+ type: 'resource';
29
+ resource: {
30
+ text: string;
31
+ uri: string;
32
+ mimeType?: string;
33
+ [key: string]: unknown;
34
+ };
35
+ [key: string]: unknown;
36
+ };
37
+
38
+ type ResourceBlobContent = {
39
+ type: 'resource';
40
+ resource: {
41
+ blob: string;
42
+ uri: string;
43
+ mimeType?: string;
44
+ [key: string]: unknown;
45
+ };
46
+ [key: string]: unknown;
47
+ };
48
+
49
+ type ResourceContent = ResourceTextContent | ResourceBlobContent;
50
+
51
+ type ContentItem = TextContent | ImageContent | AudioContent | ResourceContent;
52
+
53
+ interface CallToolResult {
54
+ content: ContentItem[];
55
+ _meta?: Record<string, unknown>;
56
+ isError?: boolean;
57
+
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ export function registerGraphTools(server: McpServer, graphClient: GraphClient): void {
62
+ for (const tool of api.endpoints) {
63
+ // Create a zod schema for the parameters
64
+ const paramSchema: Record<string, any> = {};
65
+ if (tool.parameters && tool.parameters.length > 0) {
66
+ for (const param of tool.parameters) {
67
+ // Use z.any() as a fallback schema if the parameter schema is not specified
68
+ paramSchema[param.name] = param.schema || z.any();
69
+ }
70
+ }
71
+
72
+ server.tool(
73
+ tool.alias,
74
+ tool.description ?? '',
75
+ paramSchema,
76
+ {
77
+ title: tool.alias,
78
+ readOnlyHint: tool.method.toUpperCase() === 'GET',
79
+ },
80
+ async (params, extra) => {
81
+ logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
82
+ try {
83
+ logger.info(`params: ${JSON.stringify(params)}`);
84
+
85
+ const parameterDefinitions = tool.parameters || [];
86
+
87
+ let path = tool.path;
88
+ const queryParams: Record<string, string> = {};
89
+ const headers: Record<string, string> = {};
90
+ let body: any = null;
91
+ for (let [paramName, paramValue] of Object.entries(params)) {
92
+ const fixedParamName = paramName.replace(/__/g, '$');
93
+ const paramDef = parameterDefinitions.find((p) => p.name === paramName);
94
+
95
+ if (paramDef) {
96
+ switch (paramDef.type) {
97
+ case 'Path':
98
+ path = path
99
+ .replace(`{${paramName}}`, encodeURIComponent(paramValue as string))
100
+ .replace(`:${paramName}`, encodeURIComponent(paramValue as string));
101
+ break;
102
+
103
+ case 'Query':
104
+ queryParams[fixedParamName] = `${paramValue}`;
105
+ break;
106
+
107
+ case 'Body':
108
+ body = paramValue;
109
+ break;
110
+
111
+ case 'Header':
112
+ headers[fixedParamName] = `${paramValue}`;
113
+ break;
114
+ }
115
+ } else if (paramName === 'body') {
116
+ body = paramValue;
117
+ logger.info(`Set legacy body param: ${JSON.stringify(body)}`);
118
+ }
119
+ }
120
+
121
+ if (Object.keys(queryParams).length > 0) {
122
+ const queryString = Object.entries(queryParams)
123
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
124
+ .join('&');
125
+ path = `${path}${path.includes('?') ? '&' : '?'}${queryString}`;
126
+ }
127
+
128
+ const options: any = {
129
+ method: tool.method.toUpperCase(),
130
+ headers,
131
+ };
132
+
133
+ if (options.method !== 'GET' && body) {
134
+ options.body = JSON.stringify(body);
135
+ }
136
+
137
+ logger.info(`Making graph request to ${path} with options: ${JSON.stringify(options)}`);
138
+ const response = await graphClient.graphRequest(path, options);
139
+
140
+ // Convert McpResponse to CallToolResult with the correct structure
141
+ const content: ContentItem[] = response.content.map((item) => {
142
+ // GraphClient only returns text content items, so create proper TextContent items
143
+ const textContent: TextContent = {
144
+ type: 'text',
145
+ text: item.text,
146
+ };
147
+ return textContent;
148
+ });
149
+
150
+ const result: CallToolResult = {
151
+ content,
152
+ _meta: response._meta,
153
+ isError: response.isError,
154
+ };
155
+
156
+ return result;
157
+ } catch (error) {
158
+ logger.error(`Error in tool ${tool.alias}: ${(error as Error).message}`);
159
+ const errorContent: TextContent = {
160
+ type: 'text',
161
+ text: JSON.stringify({
162
+ error: `Error in tool ${tool.alias}: ${(error as Error).message}`,
163
+ }),
164
+ };
165
+
166
+ return {
167
+ content: [errorContent],
168
+ isError: true,
169
+ };
170
+ }
171
+ }
172
+ );
173
+ }
174
+ }
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs } from './src/cli.mjs';
4
- import logger from './src/logger.mjs';
5
- import AuthManager from './src/auth.mjs';
6
- import MicrosoftGraphServer from './src/server.mjs';
7
- import { version } from './src/version.mjs';
3
+ import { parseArgs } from './cli.js';
4
+ import logger from './logger.js';
5
+ import AuthManager from './auth.js';
6
+ import MicrosoftGraphServer from './server.js';
7
+ import { version } from './version.js';
8
8
 
9
- async function main() {
9
+ async function main(): Promise<void> {
10
10
  try {
11
11
  const args = parseArgs();
12
12
 
@@ -31,7 +31,7 @@ const logger = winston.createLogger({
31
31
  ],
32
32
  });
33
33
 
34
- export const enableConsoleLogging = () => {
34
+ export const enableConsoleLogging = (): void => {
35
35
  logger.add(
36
36
  new winston.transports.Console({
37
37
  format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
@@ -1,29 +1,36 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import logger, { enableConsoleLogging } from './logger.mjs';
4
- import { registerAuthTools } from './auth-tools.mjs';
5
- import { registerDynamicTools } from './dynamic-tools.mjs';
6
- import GraphClient from './graph-client.mjs';
3
+ import logger, { enableConsoleLogging } from './logger.js';
4
+ import { registerAuthTools } from './auth-tools.js';
5
+ import { registerGraphTools } from './graph-tools.js';
6
+ import GraphClient from './graph-client.js';
7
+ import AuthManager from './auth.js';
8
+ import type { CommandOptions } from './cli.ts';
7
9
 
8
10
  class MicrosoftGraphServer {
9
- constructor(authManager, options = {}) {
11
+ private authManager: AuthManager;
12
+ private options: CommandOptions;
13
+ private graphClient: GraphClient;
14
+ private server: McpServer | null;
15
+
16
+ constructor(authManager: AuthManager, options: CommandOptions = {}) {
10
17
  this.authManager = authManager;
11
18
  this.options = options;
12
19
  this.graphClient = new GraphClient(authManager);
13
20
  this.server = null;
14
21
  }
15
22
 
16
- async initialize(version) {
23
+ async initialize(version: string): Promise<void> {
17
24
  this.server = new McpServer({
18
25
  name: 'Microsoft365MCP',
19
26
  version,
20
27
  });
21
28
 
22
29
  registerAuthTools(this.server, this.authManager);
23
- await registerDynamicTools(this.server, this.graphClient);
30
+ registerGraphTools(this.server, this.graphClient);
24
31
  }
25
32
 
26
- async start() {
33
+ async start(): Promise<void> {
27
34
  if (this.options.v) {
28
35
  enableConsoleLogging();
29
36
  }
@@ -31,7 +38,7 @@ class MicrosoftGraphServer {
31
38
  logger.info('Microsoft 365 MCP Server starting...');
32
39
 
33
40
  const transport = new StdioServerTransport();
34
- await this.server.connect(transport);
41
+ await this.server!.connect(transport);
35
42
  logger.info('Server connected to transport');
36
43
  }
37
44
  }
package/src/version.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { readFileSync } from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
7
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
8
+
9
+ export const version: string = packageJson.version;
@@ -1,94 +1,97 @@
1
1
  import { describe, expect, it, vi, beforeEach } from 'vitest';
2
2
 
3
- // Mock Zod
4
3
  vi.mock('zod', () => {
5
4
  const mockZod = {
6
5
  boolean: () => ({
7
6
  default: () => ({
8
- describe: () => 'mocked-zod-boolean'
9
- })
7
+ describe: () => 'mocked-zod-boolean',
8
+ }),
10
9
  }),
11
10
  object: () => ({
12
- strict: () => 'mocked-zod-object'
13
- })
11
+ strict: () => 'mocked-zod-object',
12
+ }),
14
13
  };
15
14
  return { z: mockZod };
16
15
  });
17
16
 
18
- import { registerAuthTools } from '../src/auth-tools.mjs';
17
+ import { registerAuthTools } from '../src/auth-tools.js';
19
18
 
20
19
  describe('Auth Tools', () => {
21
- let server;
22
- let authManager;
23
- let loginTool;
24
-
20
+ let server: any;
21
+ let authManager: any;
22
+ let loginTool: any;
23
+
25
24
  beforeEach(() => {
26
25
  loginTool = vi.fn();
27
-
26
+
28
27
  server = {
29
28
  tool: vi.fn((name, schema, handler) => {
30
29
  if (name === 'login') {
31
30
  loginTool = handler;
32
31
  }
33
- })
32
+ }),
34
33
  };
35
-
34
+
36
35
  authManager = {
37
36
  testLogin: vi.fn(),
38
- acquireTokenByDeviceCode: vi.fn()
37
+ acquireTokenByDeviceCode: vi.fn(),
39
38
  };
40
-
39
+
41
40
  registerAuthTools(server, authManager);
42
41
  });
43
-
42
+
44
43
  describe('login tool', () => {
45
44
  it('should check if already logged in when force=false', async () => {
46
45
  authManager.testLogin.mockResolvedValue({
47
46
  success: true,
48
- userData: { displayName: 'Test User' }
47
+ userData: { displayName: 'Test User' },
49
48
  });
50
-
49
+
51
50
  const result = await loginTool({ force: false });
52
-
51
+
53
52
  expect(authManager.testLogin).toHaveBeenCalled();
54
53
  expect(authManager.acquireTokenByDeviceCode).not.toHaveBeenCalled();
55
54
  expect(result.content[0].text).toContain('Already logged in');
56
55
  });
57
-
56
+
58
57
  it('should force login when force=true even if already logged in', async () => {
59
58
  authManager.testLogin.mockResolvedValue({
60
59
  success: true,
61
- userData: { displayName: 'Test User' }
60
+ userData: { displayName: 'Test User' },
62
61
  });
63
-
64
- authManager.acquireTokenByDeviceCode.mockImplementation(callback => {
65
- callback('Login instructions');
66
- return Promise.resolve();
67
- });
68
-
62
+
63
+ authManager.acquireTokenByDeviceCode.mockImplementation(
64
+ (callback: (text: string) => void) => {
65
+ callback('Login instructions');
66
+ return Promise.resolve();
67
+ }
68
+ );
69
+
69
70
  const result = await loginTool({ force: true });
70
-
71
+
71
72
  expect(authManager.testLogin).not.toHaveBeenCalled();
72
73
  expect(authManager.acquireTokenByDeviceCode).toHaveBeenCalled();
73
74
  expect(result.content[0].text).toBe('Login instructions');
74
75
  });
75
-
76
+
76
77
  it('should proceed with login when not already logged in', async () => {
77
78
  authManager.testLogin.mockResolvedValue({
78
79
  success: false,
79
- message: 'Not logged in'
80
- });
81
-
82
- authManager.acquireTokenByDeviceCode.mockImplementation(callback => {
83
- callback('Login instructions');
84
- return Promise.resolve();
80
+ message: 'Not logged in',
85
81
  });
86
-
82
+
83
+ authManager.acquireTokenByDeviceCode.mockImplementation(
84
+ (callback: (text: string) => void) => {
85
+ callback('Login instructions');
86
+ return Promise.resolve();
87
+ }
88
+ );
89
+
87
90
  const result = await loginTool({ force: false });
88
-
91
+
89
92
  expect(authManager.testLogin).toHaveBeenCalled();
90
93
  expect(authManager.acquireTokenByDeviceCode).toHaveBeenCalled();
91
94
  expect(result.content[0].text).toBe('Login instructions');
92
95
  });
93
96
  });
94
- });
97
+ });
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { parseArgs } from '../src/cli.mjs';
2
+ import { parseArgs } from '../src/cli.js';
3
3
 
4
4
  vi.mock('commander', () => {
5
5
  const mockCommand = {
@@ -16,7 +16,7 @@ vi.mock('commander', () => {
16
16
  };
17
17
  });
18
18
 
19
- vi.mock('../auth.mjs', () => {
19
+ vi.mock('../src/auth.js', () => {
20
20
  return {
21
21
  default: vi.fn().mockImplementation(() => ({
22
22
  getToken: vi.fn().mockResolvedValue('mock-token'),
@@ -42,4 +42,4 @@ describe('CLI Module', () => {
42
42
  expect(result).toEqual({ file: 'test.xlsx' });
43
43
  });
44
44
  });
45
- });
45
+ });