@frustrated/ms-graph-mcp 0.1.8 → 0.1.11

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/src/index.ts CHANGED
@@ -1,80 +1,77 @@
1
- #!/usr/bin/env node
2
- import { Command } from "@commander-js/extra-typings";
3
- import { initAuth, revokeAuth, getAuthStatus } from "./auth.ts";
4
- import { getConfig, saveConfig } from "./config.ts";
5
- import { startMcpServer } from "./mcp-interface.ts";
6
-
7
- const program = new Command();
8
-
9
- program
10
- .name("ms-graph-mcp")
11
- .description("CLI for JSR-based Microsoft Graph MCP package")
12
- .version("0.1.0");
13
-
14
- program
15
- .command("init")
16
- .description("Initialize authentication with Microsoft Graph")
17
- .action(async () => {
18
- try {
19
- await initAuth();
20
- console.log("Authentication process completed.");
21
- } catch (error) {
22
- console.error("Authentication failed:", error instanceof Error ? error.message : String(error));
23
- process.exit(1);
24
- }
25
- });
26
-
27
- program
28
- .command("revoke")
29
- .description("Revoke Microsoft Graph authentication and clear tokens")
30
- .action(async () => {
31
- try {
32
- await revokeAuth();
33
- console.log("Authentication revoked successfully.");
34
- } catch (error) {
35
- console.error("Failed to revoke authentication:", error instanceof Error ? error.message : String(error));
36
- process.exit(1);
37
- }
38
- });
39
-
40
- program
41
- .command("permissions")
42
- .description(
43
- "Display current authentication status and configured permissions",
44
- )
45
- .action(async () => {
46
- try {
47
- const authStatus = await getAuthStatus();
48
- const config = getConfig();
49
- console.log("--- Microsoft Graph MCP Status ---");
50
- console.log(`Client ID: ${authStatus.clientId}`);
51
- console.log(`Tenant ID: ${authStatus.tenantId}`);
52
- console.log(
53
- `Authenticated: ${authStatus.isAuthenticated ? "Yes" : "No"}`,
54
- );
55
- console.log("\nEnabled Tools:");
56
- if (config.enabledTools.length > 0) {
57
- config.enabledTools.forEach((tool) => console.log(`- ${tool}`));
58
- } else {
59
- console.log(
60
- "No specific tools are explicitly enabled in config. All available tools will be used.",
61
- );
62
- }
63
- console.log("----------------------------------");
64
- } catch (error) {
65
- console.error("Failed to retrieve status:", error instanceof Error ? error.message : String(error));
66
- process.exit(1);
67
- }
68
- });
69
-
70
- program
71
- .command("run")
72
- .description("Start the MCP server to listen for commands via stdin/stdout")
73
- .action(async () => {
74
- console.log("MCP server started. Listening for commands on stdin...");
75
- // Placeholder for MCP interface logic
76
- // This will be implemented in src/mcp-interface.ts and called here
77
- startMcpServer();
78
- });
79
-
80
- program.parse(process.argv);
1
+ #!/usr/bin/env node
2
+ import { Command } from "@commander-js/extra-typings";
3
+ import { initAuth, revokeAuth, getAuthStatus } from "./auth.ts";
4
+ import { getConfig, saveConfig } from "./config.ts";
5
+ import { startMcpServer } from "./mcp-interface.ts";
6
+
7
+ const program = new Command();
8
+
9
+ program
10
+ .name("ms-graph-mcp")
11
+ .description("CLI for JSR-based Microsoft Graph MCP package")
12
+ .version("0.1.11");
13
+
14
+ program
15
+ .command("init")
16
+ .description("Initialize authentication with Microsoft Graph")
17
+ .action(async () => {
18
+ try {
19
+ await initAuth();
20
+ console.log("Authentication process completed.");
21
+ } catch (error) {
22
+ console.error("Authentication failed:", error instanceof Error ? error.message : String(error));
23
+ process.exit(1);
24
+ }
25
+ });
26
+
27
+ program
28
+ .command("revoke")
29
+ .description("Revoke Microsoft Graph authentication and clear tokens")
30
+ .action(async () => {
31
+ try {
32
+ await revokeAuth();
33
+ console.log("Authentication revoked successfully.");
34
+ } catch (error) {
35
+ console.error("Failed to revoke authentication:", error instanceof Error ? error.message : String(error));
36
+ process.exit(1);
37
+ }
38
+ });
39
+
40
+ program
41
+ .command("permissions")
42
+ .description(
43
+ "Display current authentication status and configured permissions",
44
+ )
45
+ .action(async () => {
46
+ try {
47
+ const authStatus = await getAuthStatus();
48
+ const config = getConfig();
49
+ console.log("--- Microsoft Graph MCP Status ---");
50
+ console.log(`Client ID: ${authStatus.clientId}`);
51
+ console.log(`Tenant ID: ${authStatus.tenantId}`);
52
+ console.log(
53
+ `Authenticated: ${authStatus.isAuthenticated ? "Yes" : "No"}`,
54
+ );
55
+ console.log("\nEnabled Tools:");
56
+ if (config.enabledTools.length > 0) {
57
+ config.enabledTools.forEach((tool) => console.log(`- ${tool}`));
58
+ } else {
59
+ console.log(
60
+ "No specific tools are explicitly enabled in config. All available tools will be used.",
61
+ );
62
+ }
63
+ console.log("----------------------------------");
64
+ } catch (error) {
65
+ console.error("Failed to retrieve status:", error instanceof Error ? error.message : String(error));
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ program
71
+ .command("run")
72
+ .description("Start the MCP server to listen for commands via stdin/stdout")
73
+ .action(async () => {
74
+ await startMcpServer();
75
+ });
76
+
77
+ program.parse(process.argv);
@@ -1,122 +1,142 @@
1
- import { getAccessToken } from './auth.ts';
2
- import { getConfig, isToolEnabled } from './config.ts';
3
- import { error, log } from './utils.ts';
4
- import { Client } from '@microsoft/microsoft-graph-client';
5
-
6
- // Define MCP message interfaces
7
- interface McpRequest {
8
- type: 'request';
9
- id: string;
10
- tool: string;
11
- input: any;
12
- }
13
-
14
- interface McpResponse {
15
- type: 'response';
16
- id: string;
17
- status: 'success' | 'error';
18
- output?: any;
19
- error?: { code: string; message: string };
20
- }
21
-
22
- // Placeholder for registered tools
23
- import { tools as registeredTools } from './tools/index.ts';
24
-
25
-
26
- async function processMcpRequest(request: McpRequest): Promise<McpResponse> {
27
- const config = getConfig();
28
-
29
- if (!isToolEnabled(request.tool)) {
30
- return {
31
- type: 'response',
32
- id: request.id,
33
- status: 'error',
34
- error: { code: 'TOOL_DISABLED', message: `Tool '${request.tool}' is disabled.` },
35
- };
36
- }
37
-
38
- const toolHandler = (registeredTools as Record<string, ((graphClient: Client, input: any) => Promise<any>) | undefined>)[request.tool];
39
- if (!toolHandler) {
40
- return {
41
- type: 'response',
42
- id: request.id,
43
- status: 'error',
44
- error: { code: 'TOOL_NOT_FOUND', message: `Tool '${request.tool}' not found.` },
45
- };
46
- }
47
-
48
- try {
49
- const accessToken = await getAccessToken();
50
- const graphClient = Client.init({
51
- authProvider: (done) => {
52
- done(null, accessToken);
53
- },
54
- });
55
-
56
- const output = await toolHandler(graphClient, request.input);
57
- return {
58
- type: 'response',
59
- id: request.id,
60
- status: 'success',
61
- output,
62
- };
63
- } catch (err: any) {
64
- error(`Error executing tool '${request.tool}':`, err);
65
- return {
66
- type: 'response',
67
- id: request.id,
68
- status: 'error',
69
- error: { code: err.code || 'TOOL_EXECUTION_ERROR', message: err.message || 'An unknown error occurred.' },
70
- };
71
- }
72
- }
73
-
74
- export function startMcpServer() {
75
- log('MCP server started. Listening for commands on stdin...');
76
-
77
- process.stdin.setEncoding('utf8');
78
-
79
- let buffer = '';
80
- process.stdin.on('data', async (chunk) => {
81
- buffer += chunk;
82
- let newlineIndex;
83
- while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
84
- const line = buffer.substring(0, newlineIndex).trim();
85
- buffer = buffer.substring(newlineIndex + 1);
86
-
87
- if (line) {
88
- try {
89
- const request: McpRequest = JSON.parse(line);
90
- if (request.type === 'request' && request.id && request.tool) {
91
- const response = await processMcpRequest(request);
92
- process.stdout.write(JSON.stringify(response) + '\n');
93
- } else {
94
- error('Invalid MCP request format:', new Error(line));
95
- process.stdout.write(JSON.stringify({
96
- type: 'response',
97
- id: request.id || 'unknown',
98
- status: 'error',
99
- error: { code: 'INVALID_REQUEST', message: 'Invalid MCP request format.' },
100
- }) + '\n');
101
- }
102
- } catch (parseError: any) {
103
- error('Failed to parse stdin input as JSON:', parseError);
104
- process.stdout.write(JSON.stringify({
105
- type: 'response',
106
- id: 'unknown',
107
- status: 'error',
108
- error: { code: 'JSON_PARSE_ERROR', message: 'Invalid JSON input.' },
109
- }) + '\n');
110
- }
111
- }
112
- }
113
- });
114
-
115
- process.stdin.on('end', () => {
116
- log('Stdin closed. MCP server shutting down.');
117
- });
118
-
119
- process.stdin.on('error', (err) => {
120
- error('Stdin error:', err);
121
- });
122
- }
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { getAccessToken } from './auth.ts';
5
+ import { isToolEnabled } from './config.ts';
6
+ import { log } from './utils.ts';
7
+ import { Client } from '@microsoft/microsoft-graph-client';
8
+ import * as mail from './tools/mail.ts';
9
+ import * as calendar from './tools/calendar.ts';
10
+ import * as onedrive from './tools/onedrive.ts';
11
+
12
+ function buildGraphClient(accessToken: string): Client {
13
+ return Client.init({
14
+ authProvider: (done) => done(null, accessToken),
15
+ });
16
+ }
17
+
18
+ export async function startMcpServer(): Promise<void> {
19
+ const server = new McpServer({
20
+ name: 'ms-graph-mcp',
21
+ version: '0.1.11',
22
+ });
23
+
24
+ if (isToolEnabled('mail.list_messages')) {
25
+ server.tool(
26
+ 'mail.list_messages',
27
+ 'List email messages from the signed-in user\'s mailbox',
28
+ {
29
+ folderId: z.string().optional().describe('Mail folder ID (defaults to Inbox)'),
30
+ top: z.number().int().min(1).optional().describe('Maximum number of messages to return'),
31
+ filter: z.string().optional().describe('OData $filter expression'),
32
+ },
33
+ async ({ folderId, top, filter }) => {
34
+ const token = await getAccessToken();
35
+ const result = await mail.listMessages(buildGraphClient(token), { folderId, top, filter });
36
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
37
+ },
38
+ );
39
+ }
40
+
41
+ if (isToolEnabled('calendar.create_event')) {
42
+ server.tool(
43
+ 'calendar.create_event',
44
+ 'Create a new event in the signed-in user\'s calendar',
45
+ {
46
+ subject: z.string().describe('Event title'),
47
+ start: z.object({
48
+ dateTime: z.string().describe('ISO 8601 date-time string'),
49
+ timeZone: z.string().describe('IANA timezone name, e.g. "America/New_York"'),
50
+ }),
51
+ end: z.object({
52
+ dateTime: z.string().describe('ISO 8601 date-time string'),
53
+ timeZone: z.string().describe('IANA timezone name'),
54
+ }),
55
+ content: z.string().optional().describe('HTML body of the event'),
56
+ attendees: z.array(z.object({
57
+ emailAddress: z.string().email(),
58
+ type: z.enum(['required', 'optional']),
59
+ })).optional().describe('List of attendees'),
60
+ location: z.string().optional().describe('Event location display name'),
61
+ },
62
+ async (input) => {
63
+ const token = await getAccessToken();
64
+ const result = await calendar.createEvent(buildGraphClient(token), input);
65
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
66
+ },
67
+ );
68
+ }
69
+
70
+ if (isToolEnabled('onedrive.list_items')) {
71
+ server.tool(
72
+ 'onedrive.list_items',
73
+ 'List files and folders from OneDrive root or a specific folder',
74
+ {
75
+ folderPath: z.string().trim().optional().describe('Folder path relative to OneDrive root, e.g. /Projects'),
76
+ itemId: z.string().trim().optional().describe('Drive item ID to list children from'),
77
+ top: z.number().int().min(1).max(999).optional().describe('Maximum number of items to return'),
78
+ select: z.string().trim().optional().describe('Optional OData $select clause'),
79
+ },
80
+ async (input) => {
81
+ const token = await getAccessToken();
82
+ const result = await onedrive.listItems(buildGraphClient(token), input);
83
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
84
+ },
85
+ );
86
+ }
87
+
88
+ if (isToolEnabled('onedrive.get_item')) {
89
+ server.tool(
90
+ 'onedrive.get_item',
91
+ 'Get metadata for a OneDrive file or folder by path or item ID',
92
+ {
93
+ path: z.string().trim().optional().describe('Item path relative to OneDrive root, e.g. /Projects/report.docx'),
94
+ itemId: z.string().trim().optional().describe('Drive item ID'),
95
+ select: z.string().trim().optional().describe('Optional OData $select clause'),
96
+ },
97
+ async (input) => {
98
+ const token = await getAccessToken();
99
+ const result = await onedrive.getItem(buildGraphClient(token), input);
100
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
101
+ },
102
+ );
103
+ }
104
+
105
+ if (isToolEnabled('onedrive.search_items')) {
106
+ server.tool(
107
+ 'onedrive.search_items',
108
+ 'Search OneDrive for files and folders',
109
+ {
110
+ query: z.string().trim().min(1).describe('Search query'),
111
+ top: z.number().int().min(1).max(999).optional().describe('Maximum number of items to return'),
112
+ select: z.string().trim().optional().describe('Optional OData $select clause'),
113
+ },
114
+ async (input) => {
115
+ const token = await getAccessToken();
116
+ const result = await onedrive.searchItems(buildGraphClient(token), input);
117
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
118
+ },
119
+ );
120
+ }
121
+
122
+ if (isToolEnabled('onedrive.create_folder')) {
123
+ server.tool(
124
+ 'onedrive.create_folder',
125
+ 'Create a folder in OneDrive root or under a specific parent',
126
+ {
127
+ name: z.string().trim().min(1).describe('Folder name'),
128
+ parentPath: z.string().trim().optional().describe('Parent folder path relative to OneDrive root'),
129
+ parentItemId: z.string().trim().optional().describe('Parent drive item ID'),
130
+ },
131
+ async (input) => {
132
+ const token = await getAccessToken();
133
+ const result = await onedrive.createFolder(buildGraphClient(token), input);
134
+ return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] };
135
+ },
136
+ );
137
+ }
138
+
139
+ const transport = new StdioServerTransport();
140
+ await server.connect(transport);
141
+ log('MCP server started (JSON-RPC 2.0 over stdio). Listening for commands...');
142
+ }
@@ -1,41 +1,56 @@
1
- import { Client } from '@microsoft/microsoft-graph-client';
2
- import { log, error } from '../utils.ts';
3
-
4
- export async function createEvent(graphClient: Client, input: {
5
- subject: string;
6
- start: { dateTime: string; timeZone: string };
7
- end: { dateTime: string; timeZone: string };
8
- content?: string;
9
- attendees?: Array<{ emailAddress: string; type: 'required' | 'optional' }>;
10
- location?: string;
11
- }): Promise<any> {
12
- try {
13
- const event = {
14
- subject: input.subject,
15
- start: input.start,
16
- end: input.end,
17
- body: input.content ? { contentType: 'HTML', content: input.content } : undefined,
18
- attendees: input.attendees?.map(att => ({
19
- emailAddress: { address: att.emailAddress },
20
- type: att.type,
21
- })),
22
- location: input.location ? { displayName: input.location } : undefined,
23
- };
24
-
25
- const response = await graphClient.api('/me/events').post(event);
26
- log(`Created calendar event: ${response.subject} (ID: ${response.id})`);
27
- return {
28
- id: response.id,
29
- webLink: response.webLink,
30
- status: 'created',
31
- };
32
- } catch (err: any) {
33
- error('Error creating calendar event:', err);
34
- return {
35
- id: null,
36
- webLink: null,
37
- status: 'failed',
38
- errorMessage: err.message,
39
- };
40
- }
41
- }
1
+ import { Client } from '@microsoft/microsoft-graph-client';
2
+ import { log, error, describeGraphError } from '../utils.ts';
3
+
4
+ function assertValidEventTimeRange(start: { dateTime: string; timeZone: string }, end: { dateTime: string; timeZone: string }): void {
5
+ const startDate = new Date(start.dateTime);
6
+ const endDate = new Date(end.dateTime);
7
+
8
+ if (Number.isNaN(startDate.getTime())) {
9
+ throw new Error(`Invalid start.dateTime value: ${start.dateTime}`);
10
+ }
11
+ if (Number.isNaN(endDate.getTime())) {
12
+ throw new Error(`Invalid end.dateTime value: ${end.dateTime}`);
13
+ }
14
+ if (endDate.getTime() <= startDate.getTime()) {
15
+ throw new Error('Event end time must be after the start time.');
16
+ }
17
+ if (!start.timeZone.trim() || !end.timeZone.trim()) {
18
+ throw new Error('Both start.timeZone and end.timeZone are required.');
19
+ }
20
+ }
21
+
22
+ export async function createEvent(graphClient: Client, input: {
23
+ subject: string;
24
+ start: { dateTime: string; timeZone: string };
25
+ end: { dateTime: string; timeZone: string };
26
+ content?: string;
27
+ attendees?: Array<{ emailAddress: string; type: 'required' | 'optional' }>;
28
+ location?: string;
29
+ }): Promise<any> {
30
+ try {
31
+ assertValidEventTimeRange(input.start, input.end);
32
+
33
+ const event = {
34
+ subject: input.subject,
35
+ start: input.start,
36
+ end: input.end,
37
+ body: input.content ? { contentType: 'HTML', content: input.content } : undefined,
38
+ attendees: input.attendees?.map(att => ({
39
+ emailAddress: { address: att.emailAddress },
40
+ type: att.type,
41
+ })),
42
+ location: input.location ? { displayName: input.location } : undefined,
43
+ };
44
+
45
+ const response = await graphClient.api('/me/events').post(event);
46
+ log(`Created calendar event: ${response.subject} (ID: ${response.id})`);
47
+ return {
48
+ id: response.id,
49
+ webLink: response.webLink,
50
+ status: 'created',
51
+ };
52
+ } catch (err: any) {
53
+ error('Error creating calendar event:', err);
54
+ throw new Error(`Failed to create calendar event: ${describeGraphError(err)}`);
55
+ }
56
+ }
@@ -1,9 +1,12 @@
1
- import * as mail from './mail.ts';
2
- import * as calendar from './calendar.ts';
3
- // import * as onedrive from './onedrive'; // Placeholder for future tools
4
-
5
- export const tools = {
6
- 'mail.list_messages': mail.listMessages,
7
- 'calendar.create_event': calendar.createEvent,
8
- // 'onedrive.list_files': onedrive.listFiles,
9
- };
1
+ import * as mail from './mail.ts';
2
+ import * as calendar from './calendar.ts';
3
+ import * as onedrive from './onedrive.ts';
4
+
5
+ export const tools = {
6
+ 'mail.list_messages': mail.listMessages,
7
+ 'calendar.create_event': calendar.createEvent,
8
+ 'onedrive.list_items': onedrive.listItems,
9
+ 'onedrive.get_item': onedrive.getItem,
10
+ 'onedrive.search_items': onedrive.searchItems,
11
+ 'onedrive.create_folder': onedrive.createFolder,
12
+ };
package/src/tools/mail.ts CHANGED
@@ -1,34 +1,60 @@
1
- import { Client } from '@microsoft/microsoft-graph-client';
2
- import { log, error } from '../utils.ts';
3
-
4
- export async function listMessages(graphClient: Client, input: { folderId?: string; top?: number; filter?: string }): Promise<any> {
5
- try {
6
- let request = graphClient.api(input.folderId ? `/me/mailFolders/${input.folderId}/messages` :
7
- '/me/messages');
8
-
9
- if (input.top) {
10
- request = request.top(input.top);
11
- }
12
- if (input.filter) {
13
- request = request.filter(input.filter);
14
- }
15
-
16
- const response = await request.select('id,subject,from,receivedDateTime,isRead,bodyPreview,webLink').get();
17
- log(`Listed ${response.value.length} messages.`);
18
- return {
19
- messages: response.value.map((msg: any) => ({
20
- id: msg.id,
21
- subject: msg.subject,
22
- from: msg.from,
23
- receivedDateTime: msg.receivedDateTime,
24
- isRead: msg.isRead,
25
- bodyPreview: msg.bodyPreview,
26
- webLink: msg.webLink,
27
- })),
28
- nextLink: response['@odata.nextLink'],
29
- };
30
- } catch (err: any) {
31
- error('Error listing messages:', err);
32
- throw err;
33
- }
34
- }
1
+ import { Client } from '@microsoft/microsoft-graph-client';
2
+ import { log, error, describeGraphError } from '../utils.ts';
3
+
4
+ const WELL_KNOWN_FOLDERS = new Map<string, string>([
5
+ ['inbox', 'inbox'],
6
+ ['sent', 'sentitems'],
7
+ ['sentitems', 'sentitems'],
8
+ ['drafts', 'drafts'],
9
+ ['deleted', 'deleteditems'],
10
+ ['deleteditems', 'deleteditems'],
11
+ ['archive', 'archive'],
12
+ ['junk', 'junkemail'],
13
+ ['junkemail', 'junkemail'],
14
+ ['outbox', 'outbox'],
15
+ ]);
16
+
17
+ function normalizeFolderId(folderId?: string): string | undefined {
18
+ if (!folderId) {
19
+ return undefined;
20
+ }
21
+
22
+ const trimmed = folderId.trim();
23
+ if (!trimmed) {
24
+ return undefined;
25
+ }
26
+
27
+ return WELL_KNOWN_FOLDERS.get(trimmed.toLowerCase()) ?? trimmed;
28
+ }
29
+
30
+ export async function listMessages(graphClient: Client, input: { folderId?: string; top?: number; filter?: string }): Promise<any> {
31
+ try {
32
+ const folderId = normalizeFolderId(input.folderId);
33
+ let request = graphClient.api(folderId ? `/me/mailFolders/${folderId}/messages` : '/me/messages');
34
+
35
+ if (input.top) {
36
+ request = request.top(input.top);
37
+ }
38
+ if (input.filter) {
39
+ request = request.filter(input.filter);
40
+ }
41
+
42
+ const response = await request.select('id,subject,from,receivedDateTime,isRead,bodyPreview,webLink').get();
43
+ log(`Listed ${response.value.length} messages.`);
44
+ return {
45
+ messages: response.value.map((msg: any) => ({
46
+ id: msg.id,
47
+ subject: msg.subject,
48
+ from: msg.from,
49
+ receivedDateTime: msg.receivedDateTime,
50
+ isRead: msg.isRead,
51
+ bodyPreview: msg.bodyPreview,
52
+ webLink: msg.webLink,
53
+ })),
54
+ nextLink: response['@odata.nextLink'],
55
+ };
56
+ } catch (err: any) {
57
+ error('Error listing messages:', err);
58
+ throw new Error(`Failed to list messages: ${describeGraphError(err)}`);
59
+ }
60
+ }