@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.
- package/.github/workflows/build.yml +3 -0
- package/.github/workflows/npm-publish.yml +2 -0
- package/README.md +1 -1
- package/bin/generate-graph-client.mjs +59 -0
- package/bin/{download-openapi.mjs → modules/download-openapi.mjs} +10 -20
- package/bin/modules/extract-descriptions.mjs +48 -0
- package/bin/modules/generate-mcp-tools.mjs +36 -0
- package/bin/modules/simplified-openapi.mjs +78 -0
- package/dist/auth-tools.js +80 -0
- package/dist/auth.js +219 -0
- package/dist/cli.js +21 -0
- package/dist/endpoints.json +375 -0
- package/dist/generated/client.js +14683 -0
- package/dist/generated/endpoint-types.js +1 -0
- package/dist/generated/hack.js +37 -0
- package/dist/graph-client.js +254 -0
- package/dist/graph-tools.js +98 -0
- package/dist/index.js +39 -0
- package/dist/logger.js +33 -0
- package/dist/server.js +32 -0
- package/{src/version.mjs → dist/version.js} +0 -2
- package/package.json +12 -9
- package/src/{auth-tools.mjs → auth-tools.ts} +7 -5
- package/src/{auth.mjs → auth.ts} +60 -30
- package/src/{cli.mjs → cli.ts} +9 -1
- package/src/endpoints.json +375 -0
- package/src/generated/README.md +51 -0
- package/src/generated/client.ts +24916 -0
- package/src/generated/endpoint-types.ts +27 -0
- package/src/generated/hack.ts +50 -0
- package/src/{graph-client.mjs → graph-client.ts} +53 -18
- package/src/graph-tools.ts +174 -0
- package/{index.mjs → src/index.ts} +6 -6
- package/src/{logger.mjs → logger.ts} +1 -1
- package/src/{server.mjs → server.ts} +16 -9
- package/src/version.ts +9 -0
- package/test/{auth-tools.test.js → auth-tools.test.ts} +41 -38
- package/test/{cli.test.js → cli.test.ts} +3 -3
- package/test/{graph-api.test.js → graph-api.test.ts} +5 -5
- package/test/test-hack.ts +17 -0
- package/tsconfig.json +16 -0
- package/src/dynamic-tools.mjs +0 -442
- package/src/openapi-helpers.mjs +0 -187
- package/src/param-mapper.mjs +0 -30
- package/test/dynamic-tools.test.js +0 -852
- package/test/mappings.test.js +0 -29
- package/test/mcp-server.test.js +0 -36
- package/test/openapi-helpers.test.js +0 -210
- 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.
|
|
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
|
-
|
|
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
|
|
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 './
|
|
4
|
-
import logger from './
|
|
5
|
-
import AuthManager from './
|
|
6
|
-
import MicrosoftGraphServer from './
|
|
7
|
-
import { version } from './
|
|
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.
|
|
4
|
-
import { registerAuthTools } from './auth-tools.
|
|
5
|
-
import {
|
|
6
|
-
import GraphClient from './graph-client.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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(
|
|
65
|
-
callback(
|
|
66
|
-
|
|
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.
|
|
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.
|
|
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
|
+
});
|