@goforgeit/mcp-microsoft-365 0.4.11 → 0.4.19
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/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -9
- package/src/index.test.ts +319 -0
- package/src/index.ts +393 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +15 -0
package/dist/index.js
CHANGED
|
@@ -17,7 +17,9 @@ var ForgeOAuthTokensSchema = z.object({
|
|
|
17
17
|
connectedAt: z.string(),
|
|
18
18
|
scope: z.string().optional(),
|
|
19
19
|
email: z.string().optional(),
|
|
20
|
-
accountId: z.string().optional()
|
|
20
|
+
accountId: z.string().optional(),
|
|
21
|
+
/** User-level access token (Slack xoxp-* token for search operations) */
|
|
22
|
+
userAccessToken: z.string().optional()
|
|
21
23
|
});
|
|
22
24
|
var DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
23
25
|
function createForgeTokenManager(options) {
|
|
@@ -99,6 +101,12 @@ function createForgeTokenManager(options) {
|
|
|
99
101
|
}
|
|
100
102
|
return cachedTokens.accessToken;
|
|
101
103
|
},
|
|
104
|
+
async getUserAccessToken() {
|
|
105
|
+
if (!cachedTokens) {
|
|
106
|
+
await this.getAccessToken();
|
|
107
|
+
}
|
|
108
|
+
return cachedTokens?.userAccessToken ?? null;
|
|
109
|
+
},
|
|
102
110
|
getTokenInfo() {
|
|
103
111
|
return cachedTokens;
|
|
104
112
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../shared/src/mcp-auth/forge-token-manager.ts"],"sourcesContent":["/**\n * @goforgeit/mcp-microsoft-365 — MCP server for Microsoft 365\n *\n * Provides Outlook, Calendar, and OneDrive tools via stdio transport.\n * Uses Microsoft Graph REST API directly (no SDK dependency).\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport { createForgeTokenManager } from '@forge/shared/mcp-auth';\n\nconst MS_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';\nconst GRAPH_BASE = 'https://graph.microsoft.com/v1.0';\n\nexport interface Microsoft365MCPOptions {\n tokenPath: string;\n clientId: string;\n clientSecret: string;\n}\n\ninterface ToolHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;\n}\n\ninterface Microsoft365MCPServer {\n server: McpServer;\n getRegisteredTools(): Record<string, ToolHandler>;\n start(): Promise<void>;\n}\n\nexport function createMicrosoft365MCPServer(\n options: Microsoft365MCPOptions,\n): Microsoft365MCPServer {\n const tokenManager = createForgeTokenManager({\n tokenPath: options.tokenPath,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n tokenUrl: MS_TOKEN_URL,\n });\n\n const server = new McpServer({\n name: '@goforgeit/mcp-microsoft-365',\n version: '0.1.0',\n });\n\n const registeredTools: Record<string, ToolHandler> = {};\n\n function registerTool(\n name: string,\n description: string,\n schema: Record<string, z.ZodType>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,\n ) {\n registeredTools[name] = { handler };\n server.tool(name, description, schema, handler);\n }\n\n async function graphRequest(\n path: string,\n method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',\n body?: unknown,\n ): Promise<unknown> {\n const token = await tokenManager.getAccessToken();\n const options: RequestInit = {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n };\n if (body) options.body = JSON.stringify(body);\n\n const response = await fetch(`${GRAPH_BASE}${path}`, options);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Microsoft Graph API error (${response.status}): ${errorText}`);\n }\n\n if (response.status === 204) return {};\n return response.json();\n }\n\n // --- Outlook Tools ---\n\n registerTool(\n 'search_emails',\n 'Search Outlook emails',\n {\n query: z.string().describe('Search query (OData $search syntax)'),\n top: z.number().optional().default(10).describe('Maximum results'),\n },\n async (args: { query: string; top?: number }) => {\n const top = args.top || 10;\n const data = (await graphRequest(\n `/me/messages?$search=\"${encodeURIComponent(args.query)}\"&$top=${top}&$select=id,subject,from,receivedDateTime,bodyPreview,isRead`,\n )) as { value: unknown[] };\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ messages: data.value }),\n },\n ],\n };\n },\n );\n\n registerTool(\n 'get_email',\n 'Get full email details by message ID',\n {\n messageId: z.string().describe('Outlook message ID'),\n },\n async (args: { messageId: string }) => {\n const data = await graphRequest(`/me/messages/${args.messageId}`);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool('list_folders', 'List Outlook mail folders', {}, async () => {\n const data = (await graphRequest('/me/mailFolders?$top=50')) as { value: unknown[] };\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ folders: data.value }),\n },\n ],\n };\n });\n\n // --- Outlook Write Tools ---\n\n registerTool(\n 'create_draft',\n 'Create an Outlook draft email',\n {\n to: z.string().describe('Recipient email address'),\n subject: z.string().describe('Email subject'),\n body: z.string().describe('Email body (plain text)'),\n },\n async (args: { to: string; subject: string; body: string }) => {\n const data = await graphRequest('/me/messages', 'POST', {\n subject: args.subject,\n body: { contentType: 'Text', content: args.body },\n toRecipients: [{ emailAddress: { address: args.to } }],\n });\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool(\n 'archive_email',\n 'Move an Outlook email to Archive folder',\n {\n messageId: z.string().describe('Outlook message ID to archive'),\n },\n async (args: { messageId: string }) => {\n const data = await graphRequest(`/me/messages/${args.messageId}/move`, 'POST', {\n destinationId: 'archive',\n });\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n // --- Calendar Tools ---\n\n registerTool(\n 'list_events',\n 'List calendar events in a date range',\n {\n startDateTime: z.string().optional().describe('Start of range (ISO 8601)'),\n endDateTime: z.string().optional().describe('End of range (ISO 8601)'),\n top: z.number().optional().default(25).describe('Maximum events'),\n },\n async (args: { startDateTime?: string; endDateTime?: string; top?: number }) => {\n let path = '/me/events?$orderby=start/dateTime';\n path += `&$top=${args.top || 25}`;\n if (args.startDateTime) {\n path += `&$filter=start/dateTime ge '${args.startDateTime}'`;\n if (args.endDateTime) {\n path += ` and end/dateTime le '${args.endDateTime}'`;\n }\n }\n path +=\n '&$select=id,subject,start,end,location,attendees,bodyPreview,webLink,isAllDay,organizer';\n\n const data = (await graphRequest(path)) as { value: unknown[] };\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ events: data.value }),\n },\n ],\n };\n },\n );\n\n registerTool(\n 'get_event',\n 'Get a specific calendar event by ID',\n {\n eventId: z.string().describe('Calendar event ID'),\n },\n async (args: { eventId: string }) => {\n const data = await graphRequest(`/me/events/${args.eventId}`);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n // --- Calendar Write Tools ---\n\n registerTool(\n 'create_event',\n 'Create a new calendar event in Outlook',\n {\n subject: z.string().describe('Event title'),\n startDateTime: z.string().describe('Start time (ISO 8601)'),\n endDateTime: z.string().describe('End time (ISO 8601)'),\n body: z.string().optional().describe('Event description'),\n location: z.string().optional().describe('Event location'),\n attendeeEmails: z.array(z.string()).optional().describe('Attendee email addresses'),\n },\n async (args: {\n subject: string;\n startDateTime: string;\n endDateTime: string;\n body?: string;\n location?: string;\n attendeeEmails?: string[];\n }) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const event: Record<string, any> = {\n subject: args.subject,\n start: { dateTime: args.startDateTime, timeZone: 'UTC' },\n end: { dateTime: args.endDateTime, timeZone: 'UTC' },\n };\n if (args.body) event.body = { contentType: 'Text', content: args.body };\n if (args.location) event.location = { displayName: args.location };\n if (args.attendeeEmails) {\n event.attendees = args.attendeeEmails.map((email) => ({\n emailAddress: { address: email },\n type: 'required',\n }));\n }\n\n const data = await graphRequest('/me/events', 'POST', event);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool(\n 'update_event',\n 'Update an existing Outlook calendar event',\n {\n eventId: z.string().describe('Calendar event ID'),\n subject: z.string().optional().describe('New event title'),\n startDateTime: z.string().optional().describe('New start time (ISO 8601)'),\n endDateTime: z.string().optional().describe('New end time (ISO 8601)'),\n body: z.string().optional().describe('New event description'),\n location: z.string().optional().describe('New event location'),\n },\n async (args: {\n eventId: string;\n subject?: string;\n startDateTime?: string;\n endDateTime?: string;\n body?: string;\n location?: string;\n }) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const patch: Record<string, any> = {};\n if (args.subject) patch.subject = args.subject;\n if (args.body) patch.body = { contentType: 'Text', content: args.body };\n if (args.location) patch.location = { displayName: args.location };\n if (args.startDateTime) patch.start = { dateTime: args.startDateTime, timeZone: 'UTC' };\n if (args.endDateTime) patch.end = { dateTime: args.endDateTime, timeZone: 'UTC' };\n\n const data = await graphRequest(`/me/events/${args.eventId}`, 'PATCH', patch);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool(\n 'delete_event',\n 'Delete an Outlook calendar event',\n {\n eventId: z.string().describe('Calendar event ID to delete'),\n },\n async (args: { eventId: string }) => {\n await graphRequest(`/me/events/${args.eventId}`, 'DELETE');\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ deleted: true, eventId: args.eventId }),\n },\n ],\n };\n },\n );\n\n // --- OneDrive Tools ---\n\n registerTool(\n 'search_files',\n 'Search OneDrive files',\n {\n query: z.string().describe('Search query'),\n top: z.number().optional().default(20).describe('Maximum results'),\n },\n async (args: { query: string; top?: number }) => {\n const top = args.top || 20;\n const data = (await graphRequest(\n `/me/drive/root/search(q='${encodeURIComponent(args.query)}')?$top=${top}&$select=id,name,file,size,lastModifiedDateTime,webUrl`,\n )) as { value: unknown[] };\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ files: data.value }),\n },\n ],\n };\n },\n );\n\n registerTool(\n 'get_file_content',\n 'Get OneDrive file metadata by item ID',\n {\n itemId: z.string().describe('OneDrive item ID'),\n },\n async (args: { itemId: string }) => {\n const data = await graphRequest(`/me/drive/items/${args.itemId}`);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n return {\n server,\n getRegisteredTools() {\n return registeredTools;\n },\n async start() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n },\n };\n}\n\n// CLI entry point\nconst isMainModule =\n process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\\\/g, '/'));\nif (isMainModule || process.env.FORGE_MCP_START === 'true') {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath || !clientId || !clientSecret) {\n console.error(\n 'Required environment variables: FORGE_TOKEN_PATH, FORGE_CLIENT_ID, FORGE_CLIENT_SECRET',\n );\n process.exit(1);\n }\n\n const mcpServer = createMicrosoft365MCPServer({ tokenPath, clientId, clientSecret });\n mcpServer.start().catch((err) => {\n console.error('Failed to start MCP server:', err);\n process.exit(1);\n });\n}\n","/**\n * Forge Token Manager — reads Forge's OAuthTokens format and handles refresh.\n *\n * Used by @forge/mcp-* packages to get valid access tokens for API calls.\n * Reads from FORGE_TOKEN_PATH, validates with Zod, and refreshes when\n * the token is expired or within 5 minutes of expiry.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\n/** Zod schema for Forge's OAuth token file format */\nexport const ForgeOAuthTokensSchema = z.object({\n accessToken: z.string(),\n refreshToken: z.string().optional(),\n expiresAt: z.number(),\n providerId: z.string(),\n connectedAt: z.string(),\n scope: z.string().optional(),\n email: z.string().optional(),\n accountId: z.string().optional(),\n});\n\nexport type ForgeOAuthTokens = z.infer<typeof ForgeOAuthTokensSchema>;\n\nexport interface ForgeTokenManagerOptions {\n /** Path to the Forge OAuth token JSON file */\n tokenPath: string;\n /** OAuth client ID for token refresh */\n clientId: string;\n /** OAuth client secret for token refresh */\n clientSecret: string;\n /** OAuth token endpoint URL for refresh requests */\n tokenUrl: string;\n /** Buffer in ms before expiry to trigger refresh (default: 5 minutes) */\n refreshBufferMs?: number;\n}\n\nexport interface ForgeTokenManager {\n /** Returns a valid access token, refreshing if needed */\n getAccessToken(): Promise<string>;\n /** Returns the current token data, or null if not yet loaded */\n getTokenInfo(): ForgeOAuthTokens | null;\n /** Register a callback for when tokens are refreshed */\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void;\n}\n\nconst DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes\n\nexport function createForgeTokenManager(options: ForgeTokenManagerOptions): ForgeTokenManager {\n const {\n tokenPath,\n clientId,\n clientSecret,\n tokenUrl,\n refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS,\n } = options;\n\n let cachedTokens: ForgeOAuthTokens | null = null;\n const refreshCallbacks: Array<(tokens: ForgeOAuthTokens) => void> = [];\n\n function isExpiredOrNearExpiry(tokens: ForgeOAuthTokens): boolean {\n return Date.now() >= tokens.expiresAt - refreshBufferMs;\n }\n\n async function readTokenFile(): Promise<ForgeOAuthTokens> {\n let raw: string;\n try {\n raw = await fs.readFile(tokenPath, 'utf-8');\n } catch (err: unknown) {\n if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {\n throw new Error(`Token file not found: ${tokenPath}`);\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(`Invalid JSON in token file: ${tokenPath}`);\n }\n\n const result = ForgeOAuthTokensSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(`Invalid token file format: ${result.error.message}`);\n }\n\n return result.data;\n }\n\n async function refreshToken(tokens: ForgeOAuthTokens): Promise<ForgeOAuthTokens> {\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available — cannot refresh expired token');\n }\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: tokens.refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = await response.json();\n\n const refreshedTokens: ForgeOAuthTokens = {\n ...tokens,\n accessToken: data.access_token,\n refreshToken: data.refresh_token || tokens.refreshToken,\n expiresAt: Date.now() + data.expires_in * 1000,\n };\n\n // Write refreshed tokens back to file\n await fs.mkdir(path.dirname(tokenPath), { recursive: true });\n await fs.writeFile(tokenPath, JSON.stringify(refreshedTokens), { mode: 0o600 });\n\n // Notify listeners\n for (const cb of refreshCallbacks) {\n cb(refreshedTokens);\n }\n\n return refreshedTokens;\n }\n\n return {\n async getAccessToken(): Promise<string> {\n if (cachedTokens && !isExpiredOrNearExpiry(cachedTokens)) {\n return cachedTokens.accessToken;\n }\n\n const tokens = cachedTokens || (await readTokenFile());\n\n if (isExpiredOrNearExpiry(tokens)) {\n if (tokens.refreshToken) {\n cachedTokens = await refreshToken(tokens);\n } else {\n // No refresh token — return the access token as-is.\n // Some providers (e.g. Slack bot tokens) issue non-expiring tokens\n // without refresh tokens, so the expiresAt may be unreliable.\n cachedTokens = tokens;\n }\n } else {\n cachedTokens = tokens;\n }\n\n return cachedTokens.accessToken;\n },\n\n getTokenInfo(): ForgeOAuthTokens | null {\n return cachedTokens;\n },\n\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void {\n refreshCallbacks.push(callback);\n },\n };\n}\n\n/**\n * Create a ForgeTokenManager from standard environment variables.\n *\n * Expected env vars:\n * - FORGE_TOKEN_PATH: path to the OAuth token JSON file\n * - FORGE_CLIENT_ID: OAuth client ID\n * - FORGE_CLIENT_SECRET: OAuth client secret\n *\n * @param tokenUrl The provider's token endpoint URL\n */\nexport function createForgeTokenManagerFromEnv(tokenUrl: string): ForgeTokenManager {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath) throw new Error('FORGE_TOKEN_PATH environment variable is required');\n if (!clientId) throw new Error('FORGE_CLIENT_ID environment variable is required');\n if (!clientSecret) throw new Error('FORGE_CLIENT_SECRET environment variable is required');\n\n return createForgeTokenManager({ tokenPath, clientId, clientSecret, tokenUrl });\n}\n"],"mappings":"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","names":["z","path","options","z"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../../shared/src/mcp-auth/forge-token-manager.ts"],"sourcesContent":["/**\n * @goforgeit/mcp-microsoft-365 — MCP server for Microsoft 365\n *\n * Provides Outlook, Calendar, and OneDrive tools via stdio transport.\n * Uses Microsoft Graph REST API directly (no SDK dependency).\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { z } from 'zod';\nimport { createForgeTokenManager } from '@forge/shared/mcp-auth';\n\nconst MS_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';\nconst GRAPH_BASE = 'https://graph.microsoft.com/v1.0';\n\nexport interface Microsoft365MCPOptions {\n tokenPath: string;\n clientId: string;\n clientSecret: string;\n}\n\ninterface ToolHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;\n}\n\ninterface Microsoft365MCPServer {\n server: McpServer;\n getRegisteredTools(): Record<string, ToolHandler>;\n start(): Promise<void>;\n}\n\nexport function createMicrosoft365MCPServer(\n options: Microsoft365MCPOptions,\n): Microsoft365MCPServer {\n const tokenManager = createForgeTokenManager({\n tokenPath: options.tokenPath,\n clientId: options.clientId,\n clientSecret: options.clientSecret,\n tokenUrl: MS_TOKEN_URL,\n });\n\n const server = new McpServer({\n name: '@goforgeit/mcp-microsoft-365',\n version: '0.1.0',\n });\n\n const registeredTools: Record<string, ToolHandler> = {};\n\n function registerTool(\n name: string,\n description: string,\n schema: Record<string, z.ZodType>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,\n ) {\n registeredTools[name] = { handler };\n server.tool(name, description, schema, handler);\n }\n\n async function graphRequest(\n path: string,\n method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',\n body?: unknown,\n ): Promise<unknown> {\n const token = await tokenManager.getAccessToken();\n const options: RequestInit = {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n };\n if (body) options.body = JSON.stringify(body);\n\n const response = await fetch(`${GRAPH_BASE}${path}`, options);\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Microsoft Graph API error (${response.status}): ${errorText}`);\n }\n\n if (response.status === 204) return {};\n return response.json();\n }\n\n // --- Outlook Tools ---\n\n registerTool(\n 'search_emails',\n 'Search Outlook emails',\n {\n query: z.string().describe('Search query (OData $search syntax)'),\n top: z.number().optional().default(10).describe('Maximum results'),\n },\n async (args: { query: string; top?: number }) => {\n const top = args.top || 10;\n const data = (await graphRequest(\n `/me/messages?$search=\"${encodeURIComponent(args.query)}\"&$top=${top}&$select=id,subject,from,receivedDateTime,bodyPreview,isRead`,\n )) as { value: unknown[] };\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ messages: data.value }),\n },\n ],\n };\n },\n );\n\n registerTool(\n 'get_email',\n 'Get full email details by message ID',\n {\n messageId: z.string().describe('Outlook message ID'),\n },\n async (args: { messageId: string }) => {\n const data = await graphRequest(`/me/messages/${args.messageId}`);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool('list_folders', 'List Outlook mail folders', {}, async () => {\n const data = (await graphRequest('/me/mailFolders?$top=50')) as { value: unknown[] };\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ folders: data.value }),\n },\n ],\n };\n });\n\n // --- Outlook Write Tools ---\n\n registerTool(\n 'create_draft',\n 'Create an Outlook draft email',\n {\n to: z.string().describe('Recipient email address'),\n subject: z.string().describe('Email subject'),\n body: z.string().describe('Email body (plain text)'),\n },\n async (args: { to: string; subject: string; body: string }) => {\n const data = await graphRequest('/me/messages', 'POST', {\n subject: args.subject,\n body: { contentType: 'Text', content: args.body },\n toRecipients: [{ emailAddress: { address: args.to } }],\n });\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool(\n 'archive_email',\n 'Move an Outlook email to Archive folder',\n {\n messageId: z.string().describe('Outlook message ID to archive'),\n },\n async (args: { messageId: string }) => {\n const data = await graphRequest(`/me/messages/${args.messageId}/move`, 'POST', {\n destinationId: 'archive',\n });\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n // --- Calendar Tools ---\n\n registerTool(\n 'list_events',\n 'List calendar events in a date range',\n {\n startDateTime: z.string().optional().describe('Start of range (ISO 8601)'),\n endDateTime: z.string().optional().describe('End of range (ISO 8601)'),\n top: z.number().optional().default(25).describe('Maximum events'),\n },\n async (args: { startDateTime?: string; endDateTime?: string; top?: number }) => {\n let path = '/me/events?$orderby=start/dateTime';\n path += `&$top=${args.top || 25}`;\n if (args.startDateTime) {\n path += `&$filter=start/dateTime ge '${args.startDateTime}'`;\n if (args.endDateTime) {\n path += ` and end/dateTime le '${args.endDateTime}'`;\n }\n }\n path +=\n '&$select=id,subject,start,end,location,attendees,bodyPreview,webLink,isAllDay,organizer';\n\n const data = (await graphRequest(path)) as { value: unknown[] };\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ events: data.value }),\n },\n ],\n };\n },\n );\n\n registerTool(\n 'get_event',\n 'Get a specific calendar event by ID',\n {\n eventId: z.string().describe('Calendar event ID'),\n },\n async (args: { eventId: string }) => {\n const data = await graphRequest(`/me/events/${args.eventId}`);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n // --- Calendar Write Tools ---\n\n registerTool(\n 'create_event',\n 'Create a new calendar event in Outlook',\n {\n subject: z.string().describe('Event title'),\n startDateTime: z.string().describe('Start time (ISO 8601)'),\n endDateTime: z.string().describe('End time (ISO 8601)'),\n body: z.string().optional().describe('Event description'),\n location: z.string().optional().describe('Event location'),\n attendeeEmails: z.array(z.string()).optional().describe('Attendee email addresses'),\n },\n async (args: {\n subject: string;\n startDateTime: string;\n endDateTime: string;\n body?: string;\n location?: string;\n attendeeEmails?: string[];\n }) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const event: Record<string, any> = {\n subject: args.subject,\n start: { dateTime: args.startDateTime, timeZone: 'UTC' },\n end: { dateTime: args.endDateTime, timeZone: 'UTC' },\n };\n if (args.body) event.body = { contentType: 'Text', content: args.body };\n if (args.location) event.location = { displayName: args.location };\n if (args.attendeeEmails) {\n event.attendees = args.attendeeEmails.map((email) => ({\n emailAddress: { address: email },\n type: 'required',\n }));\n }\n\n const data = await graphRequest('/me/events', 'POST', event);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool(\n 'update_event',\n 'Update an existing Outlook calendar event',\n {\n eventId: z.string().describe('Calendar event ID'),\n subject: z.string().optional().describe('New event title'),\n startDateTime: z.string().optional().describe('New start time (ISO 8601)'),\n endDateTime: z.string().optional().describe('New end time (ISO 8601)'),\n body: z.string().optional().describe('New event description'),\n location: z.string().optional().describe('New event location'),\n },\n async (args: {\n eventId: string;\n subject?: string;\n startDateTime?: string;\n endDateTime?: string;\n body?: string;\n location?: string;\n }) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const patch: Record<string, any> = {};\n if (args.subject) patch.subject = args.subject;\n if (args.body) patch.body = { contentType: 'Text', content: args.body };\n if (args.location) patch.location = { displayName: args.location };\n if (args.startDateTime) patch.start = { dateTime: args.startDateTime, timeZone: 'UTC' };\n if (args.endDateTime) patch.end = { dateTime: args.endDateTime, timeZone: 'UTC' };\n\n const data = await graphRequest(`/me/events/${args.eventId}`, 'PATCH', patch);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n registerTool(\n 'delete_event',\n 'Delete an Outlook calendar event',\n {\n eventId: z.string().describe('Calendar event ID to delete'),\n },\n async (args: { eventId: string }) => {\n await graphRequest(`/me/events/${args.eventId}`, 'DELETE');\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ deleted: true, eventId: args.eventId }),\n },\n ],\n };\n },\n );\n\n // --- OneDrive Tools ---\n\n registerTool(\n 'search_files',\n 'Search OneDrive files',\n {\n query: z.string().describe('Search query'),\n top: z.number().optional().default(20).describe('Maximum results'),\n },\n async (args: { query: string; top?: number }) => {\n const top = args.top || 20;\n const data = (await graphRequest(\n `/me/drive/root/search(q='${encodeURIComponent(args.query)}')?$top=${top}&$select=id,name,file,size,lastModifiedDateTime,webUrl`,\n )) as { value: unknown[] };\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ files: data.value }),\n },\n ],\n };\n },\n );\n\n registerTool(\n 'get_file_content',\n 'Get OneDrive file metadata by item ID',\n {\n itemId: z.string().describe('OneDrive item ID'),\n },\n async (args: { itemId: string }) => {\n const data = await graphRequest(`/me/drive/items/${args.itemId}`);\n return {\n content: [{ type: 'text' as const, text: JSON.stringify(data) }],\n };\n },\n );\n\n return {\n server,\n getRegisteredTools() {\n return registeredTools;\n },\n async start() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n },\n };\n}\n\n// CLI entry point\nconst isMainModule =\n process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\\\/g, '/'));\nif (isMainModule || process.env.FORGE_MCP_START === 'true') {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath || !clientId || !clientSecret) {\n console.error(\n 'Required environment variables: FORGE_TOKEN_PATH, FORGE_CLIENT_ID, FORGE_CLIENT_SECRET',\n );\n process.exit(1);\n }\n\n const mcpServer = createMicrosoft365MCPServer({ tokenPath, clientId, clientSecret });\n mcpServer.start().catch((err) => {\n console.error('Failed to start MCP server:', err);\n process.exit(1);\n });\n}\n","/**\n * Forge Token Manager — reads Forge's OAuthTokens format and handles refresh.\n *\n * Used by @goforgeit/mcp-* packages to get valid access tokens for API calls.\n * Reads from FORGE_TOKEN_PATH, validates with Zod, and refreshes when\n * the token is expired or within 5 minutes of expiry.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\n/** Zod schema for Forge's OAuth token file format */\nexport const ForgeOAuthTokensSchema = z.object({\n accessToken: z.string(),\n refreshToken: z.string().optional(),\n expiresAt: z.number(),\n providerId: z.string(),\n connectedAt: z.string(),\n scope: z.string().optional(),\n email: z.string().optional(),\n accountId: z.string().optional(),\n /** User-level access token (Slack xoxp-* token for search operations) */\n userAccessToken: z.string().optional(),\n});\n\nexport type ForgeOAuthTokens = z.infer<typeof ForgeOAuthTokensSchema>;\n\nexport interface ForgeTokenManagerOptions {\n /** Path to the Forge OAuth token JSON file */\n tokenPath: string;\n /** OAuth client ID for token refresh */\n clientId: string;\n /** OAuth client secret for token refresh */\n clientSecret: string;\n /** OAuth token endpoint URL for refresh requests */\n tokenUrl: string;\n /** Buffer in ms before expiry to trigger refresh (default: 5 minutes) */\n refreshBufferMs?: number;\n}\n\nexport interface ForgeTokenManager {\n /** Returns a valid access token, refreshing if needed */\n getAccessToken(): Promise<string>;\n /** Returns the user-level access token if available (e.g., Slack xoxp-* for search) */\n getUserAccessToken(): Promise<string | null>;\n /** Returns the current token data, or null if not yet loaded */\n getTokenInfo(): ForgeOAuthTokens | null;\n /** Register a callback for when tokens are refreshed */\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void;\n}\n\nconst DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes\n\nexport function createForgeTokenManager(options: ForgeTokenManagerOptions): ForgeTokenManager {\n const {\n tokenPath,\n clientId,\n clientSecret,\n tokenUrl,\n refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS,\n } = options;\n\n let cachedTokens: ForgeOAuthTokens | null = null;\n const refreshCallbacks: Array<(tokens: ForgeOAuthTokens) => void> = [];\n\n function isExpiredOrNearExpiry(tokens: ForgeOAuthTokens): boolean {\n return Date.now() >= tokens.expiresAt - refreshBufferMs;\n }\n\n async function readTokenFile(): Promise<ForgeOAuthTokens> {\n let raw: string;\n try {\n raw = await fs.readFile(tokenPath, 'utf-8');\n } catch (err: unknown) {\n if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {\n throw new Error(`Token file not found: ${tokenPath}`);\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(`Invalid JSON in token file: ${tokenPath}`);\n }\n\n const result = ForgeOAuthTokensSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(`Invalid token file format: ${result.error.message}`);\n }\n\n return result.data;\n }\n\n async function refreshToken(tokens: ForgeOAuthTokens): Promise<ForgeOAuthTokens> {\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available — cannot refresh expired token');\n }\n\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: tokens.refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n });\n\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Token refresh failed (${response.status}): ${errorText}`);\n }\n\n const data = await response.json();\n\n const refreshedTokens: ForgeOAuthTokens = {\n ...tokens,\n accessToken: data.access_token,\n refreshToken: data.refresh_token || tokens.refreshToken,\n expiresAt: Date.now() + data.expires_in * 1000,\n };\n\n // Write refreshed tokens back to file\n await fs.mkdir(path.dirname(tokenPath), { recursive: true });\n await fs.writeFile(tokenPath, JSON.stringify(refreshedTokens), { mode: 0o600 });\n\n // Notify listeners\n for (const cb of refreshCallbacks) {\n cb(refreshedTokens);\n }\n\n return refreshedTokens;\n }\n\n return {\n async getAccessToken(): Promise<string> {\n if (cachedTokens && !isExpiredOrNearExpiry(cachedTokens)) {\n return cachedTokens.accessToken;\n }\n\n const tokens = cachedTokens || (await readTokenFile());\n\n if (isExpiredOrNearExpiry(tokens)) {\n if (tokens.refreshToken) {\n cachedTokens = await refreshToken(tokens);\n } else {\n // No refresh token — return the access token as-is.\n // Some providers (e.g. Slack bot tokens) issue non-expiring tokens\n // without refresh tokens, so the expiresAt may be unreliable.\n cachedTokens = tokens;\n }\n } else {\n cachedTokens = tokens;\n }\n\n return cachedTokens.accessToken;\n },\n\n async getUserAccessToken(): Promise<string | null> {\n // Ensure tokens are loaded\n if (!cachedTokens) {\n await this.getAccessToken();\n }\n return cachedTokens?.userAccessToken ?? null;\n },\n\n getTokenInfo(): ForgeOAuthTokens | null {\n return cachedTokens;\n },\n\n onRefresh(callback: (tokens: ForgeOAuthTokens) => void): void {\n refreshCallbacks.push(callback);\n },\n };\n}\n\n/**\n * Create a ForgeTokenManager from standard environment variables.\n *\n * Expected env vars:\n * - FORGE_TOKEN_PATH: path to the OAuth token JSON file\n * - FORGE_CLIENT_ID: OAuth client ID\n * - FORGE_CLIENT_SECRET: OAuth client secret\n *\n * @param tokenUrl The provider's token endpoint URL\n */\nexport function createForgeTokenManagerFromEnv(tokenUrl: string): ForgeTokenManager {\n const tokenPath = process.env.FORGE_TOKEN_PATH;\n const clientId = process.env.FORGE_CLIENT_ID;\n const clientSecret = process.env.FORGE_CLIENT_SECRET;\n\n if (!tokenPath) throw new Error('FORGE_TOKEN_PATH environment variable is required');\n if (!clientId) throw new Error('FORGE_CLIENT_ID environment variable is required');\n if (!clientSecret) throw new Error('FORGE_CLIENT_SECRET environment variable is required');\n\n return createForgeTokenManager({ tokenPath, clientId, clientSecret, tokenUrl });\n}\n"],"mappings":"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","names":["z","path","options","z"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goforgeit/mcp-microsoft-365",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Forge MCP server for Microsoft 365 (Outlook, Calendar, OneDrive)",
|
|
6
6
|
"bin": {
|
|
@@ -8,24 +8,17 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"types": "dist/index.d.ts",
|
|
11
|
-
"publishConfig": {
|
|
12
|
-
"access": "public"
|
|
13
|
-
},
|
|
14
|
-
"files": [
|
|
15
|
-
"dist",
|
|
16
|
-
"README.md"
|
|
17
|
-
],
|
|
18
11
|
"scripts": {
|
|
19
12
|
"build": "tsup",
|
|
20
13
|
"typecheck": "tsc --noEmit",
|
|
21
14
|
"test": "vitest run"
|
|
22
15
|
},
|
|
23
16
|
"dependencies": {
|
|
17
|
+
"@forge/shared": "workspace:*",
|
|
24
18
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
25
19
|
"zod": "^4.2.1"
|
|
26
20
|
},
|
|
27
21
|
"devDependencies": {
|
|
28
|
-
"@forge/shared": "workspace:*",
|
|
29
22
|
"@types/node": "^22.10.2",
|
|
30
23
|
"tsup": "^8.0.0",
|
|
31
24
|
"vitest": "^3.2.4"
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { createMicrosoft365MCPServer } from './index';
|
|
3
|
+
|
|
4
|
+
vi.mock('@forge/shared/mcp-auth', () => ({
|
|
5
|
+
createForgeTokenManager: () => ({
|
|
6
|
+
getAccessToken: vi.fn().mockResolvedValue('mock-ms-token'),
|
|
7
|
+
getTokenInfo: () => null,
|
|
8
|
+
onRefresh: vi.fn(),
|
|
9
|
+
}),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const mockFetch = vi.fn();
|
|
13
|
+
global.fetch = mockFetch;
|
|
14
|
+
|
|
15
|
+
describe('createMicrosoft365MCPServer', () => {
|
|
16
|
+
beforeEach(() => vi.clearAllMocks());
|
|
17
|
+
|
|
18
|
+
it('creates a server with registered tools', () => {
|
|
19
|
+
const server = createMicrosoft365MCPServer({
|
|
20
|
+
tokenPath: '/tmp/ms.json',
|
|
21
|
+
clientId: 'id',
|
|
22
|
+
clientSecret: 'secret',
|
|
23
|
+
});
|
|
24
|
+
expect(server).toBeDefined();
|
|
25
|
+
const tools = server.getRegisteredTools();
|
|
26
|
+
expect(tools).toHaveProperty('search_emails');
|
|
27
|
+
expect(tools).toHaveProperty('get_email');
|
|
28
|
+
expect(tools).toHaveProperty('list_folders');
|
|
29
|
+
expect(tools).toHaveProperty('create_draft');
|
|
30
|
+
expect(tools).toHaveProperty('archive_email');
|
|
31
|
+
expect(tools).toHaveProperty('list_events');
|
|
32
|
+
expect(tools).toHaveProperty('get_event');
|
|
33
|
+
expect(tools).toHaveProperty('create_event');
|
|
34
|
+
expect(tools).toHaveProperty('update_event');
|
|
35
|
+
expect(tools).toHaveProperty('delete_event');
|
|
36
|
+
expect(tools).toHaveProperty('search_files');
|
|
37
|
+
expect(tools).toHaveProperty('get_file_content');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Outlook Tools', () => {
|
|
42
|
+
beforeEach(() => vi.clearAllMocks());
|
|
43
|
+
|
|
44
|
+
it('search_emails queries Microsoft Graph', async () => {
|
|
45
|
+
mockFetch.mockResolvedValue({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: () =>
|
|
48
|
+
Promise.resolve({
|
|
49
|
+
value: [
|
|
50
|
+
{
|
|
51
|
+
id: 'msg-1',
|
|
52
|
+
subject: 'Test Email',
|
|
53
|
+
from: { emailAddress: { address: 'sender@example.com', name: 'Sender' } },
|
|
54
|
+
receivedDateTime: '2026-02-28T10:00:00Z',
|
|
55
|
+
bodyPreview: 'Preview text...',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const server = createMicrosoft365MCPServer({
|
|
62
|
+
tokenPath: '/tmp/ms.json',
|
|
63
|
+
clientId: 'id',
|
|
64
|
+
clientSecret: 'secret',
|
|
65
|
+
});
|
|
66
|
+
const tools = server.getRegisteredTools();
|
|
67
|
+
const result = await tools.search_emails.handler({ query: 'test', top: 10 });
|
|
68
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
69
|
+
expect(parsed.messages).toHaveLength(1);
|
|
70
|
+
expect(parsed.messages[0].subject).toBe('Test Email');
|
|
71
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
72
|
+
expect.stringContaining('graph.microsoft.com'),
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
headers: expect.objectContaining({
|
|
75
|
+
Authorization: 'Bearer mock-ms-token',
|
|
76
|
+
}),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('get_email returns full message details', async () => {
|
|
82
|
+
mockFetch.mockResolvedValue({
|
|
83
|
+
ok: true,
|
|
84
|
+
json: () =>
|
|
85
|
+
Promise.resolve({
|
|
86
|
+
id: 'msg-1',
|
|
87
|
+
subject: 'Detailed Email',
|
|
88
|
+
body: { content: '<p>Hello</p>', contentType: 'html' },
|
|
89
|
+
from: { emailAddress: { address: 'from@test.com', name: 'From' } },
|
|
90
|
+
toRecipients: [{ emailAddress: { address: 'to@test.com', name: 'To' } }],
|
|
91
|
+
receivedDateTime: '2026-02-28T10:00:00Z',
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const server = createMicrosoft365MCPServer({
|
|
96
|
+
tokenPath: '/tmp/ms.json',
|
|
97
|
+
clientId: 'id',
|
|
98
|
+
clientSecret: 'secret',
|
|
99
|
+
});
|
|
100
|
+
const tools = server.getRegisteredTools();
|
|
101
|
+
const result = await tools.get_email.handler({ messageId: 'msg-1' });
|
|
102
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
103
|
+
expect(parsed.subject).toBe('Detailed Email');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('list_folders returns mail folders', async () => {
|
|
107
|
+
mockFetch.mockResolvedValue({
|
|
108
|
+
ok: true,
|
|
109
|
+
json: () =>
|
|
110
|
+
Promise.resolve({
|
|
111
|
+
value: [
|
|
112
|
+
{ id: 'folder-1', displayName: 'Inbox', totalItemCount: 100 },
|
|
113
|
+
{ id: 'folder-2', displayName: 'Sent Items', totalItemCount: 50 },
|
|
114
|
+
],
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const server = createMicrosoft365MCPServer({
|
|
119
|
+
tokenPath: '/tmp/ms.json',
|
|
120
|
+
clientId: 'id',
|
|
121
|
+
clientSecret: 'secret',
|
|
122
|
+
});
|
|
123
|
+
const tools = server.getRegisteredTools();
|
|
124
|
+
const result = await tools.list_folders.handler({});
|
|
125
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
126
|
+
expect(parsed.folders).toHaveLength(2);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('Outlook Write Tools', () => {
|
|
131
|
+
beforeEach(() => vi.clearAllMocks());
|
|
132
|
+
|
|
133
|
+
it('create_draft creates an Outlook draft', async () => {
|
|
134
|
+
mockFetch.mockResolvedValue({
|
|
135
|
+
ok: true,
|
|
136
|
+
status: 201,
|
|
137
|
+
json: () => Promise.resolve({ id: 'draft-1', subject: 'Draft Test' }),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const server = createMicrosoft365MCPServer({
|
|
141
|
+
tokenPath: '/tmp/ms.json',
|
|
142
|
+
clientId: 'id',
|
|
143
|
+
clientSecret: 'secret',
|
|
144
|
+
});
|
|
145
|
+
const tools = server.getRegisteredTools();
|
|
146
|
+
const result = await tools.create_draft.handler({
|
|
147
|
+
to: 'user@example.com',
|
|
148
|
+
subject: 'Draft Test',
|
|
149
|
+
body: 'Body',
|
|
150
|
+
});
|
|
151
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
152
|
+
expect(parsed.id).toBe('draft-1');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('archive_email moves to archive folder', async () => {
|
|
156
|
+
mockFetch.mockResolvedValue({
|
|
157
|
+
ok: true,
|
|
158
|
+
status: 200,
|
|
159
|
+
json: () => Promise.resolve({ id: 'msg-1' }),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const server = createMicrosoft365MCPServer({
|
|
163
|
+
tokenPath: '/tmp/ms.json',
|
|
164
|
+
clientId: 'id',
|
|
165
|
+
clientSecret: 'secret',
|
|
166
|
+
});
|
|
167
|
+
const tools = server.getRegisteredTools();
|
|
168
|
+
await tools.archive_email.handler({ messageId: 'msg-1' });
|
|
169
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
170
|
+
expect.stringContaining('/me/messages/msg-1/move'),
|
|
171
|
+
expect.objectContaining({ method: 'POST' }),
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('Calendar Tools', () => {
|
|
177
|
+
beforeEach(() => vi.clearAllMocks());
|
|
178
|
+
|
|
179
|
+
it('list_events returns calendar events', async () => {
|
|
180
|
+
mockFetch.mockResolvedValue({
|
|
181
|
+
ok: true,
|
|
182
|
+
json: () =>
|
|
183
|
+
Promise.resolve({
|
|
184
|
+
value: [
|
|
185
|
+
{
|
|
186
|
+
id: 'evt-1',
|
|
187
|
+
subject: 'Team Standup',
|
|
188
|
+
start: { dateTime: '2026-02-28T09:00:00', timeZone: 'UTC' },
|
|
189
|
+
end: { dateTime: '2026-02-28T09:30:00', timeZone: 'UTC' },
|
|
190
|
+
location: { displayName: 'Zoom' },
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const server = createMicrosoft365MCPServer({
|
|
197
|
+
tokenPath: '/tmp/ms.json',
|
|
198
|
+
clientId: 'id',
|
|
199
|
+
clientSecret: 'secret',
|
|
200
|
+
});
|
|
201
|
+
const tools = server.getRegisteredTools();
|
|
202
|
+
const result = await tools.list_events.handler({
|
|
203
|
+
startDateTime: '2026-02-28T00:00:00Z',
|
|
204
|
+
endDateTime: '2026-02-28T23:59:59Z',
|
|
205
|
+
});
|
|
206
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
207
|
+
expect(parsed.events).toHaveLength(1);
|
|
208
|
+
expect(parsed.events[0].subject).toBe('Team Standup');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Calendar Write Tools', () => {
|
|
213
|
+
beforeEach(() => vi.clearAllMocks());
|
|
214
|
+
|
|
215
|
+
it('create_event creates a calendar event', async () => {
|
|
216
|
+
mockFetch.mockResolvedValue({
|
|
217
|
+
ok: true,
|
|
218
|
+
status: 201,
|
|
219
|
+
json: () =>
|
|
220
|
+
Promise.resolve({ id: 'evt-new', subject: 'Lunch', webLink: 'https://outlook/evt-new' }),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const server = createMicrosoft365MCPServer({
|
|
224
|
+
tokenPath: '/tmp/ms.json',
|
|
225
|
+
clientId: 'id',
|
|
226
|
+
clientSecret: 'secret',
|
|
227
|
+
});
|
|
228
|
+
const tools = server.getRegisteredTools();
|
|
229
|
+
const result = await tools.create_event.handler({
|
|
230
|
+
subject: 'Lunch',
|
|
231
|
+
startDateTime: '2026-03-01T12:00:00Z',
|
|
232
|
+
endDateTime: '2026-03-01T13:00:00Z',
|
|
233
|
+
});
|
|
234
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
235
|
+
expect(parsed.id).toBe('evt-new');
|
|
236
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
237
|
+
expect.stringContaining('/me/events'),
|
|
238
|
+
expect.objectContaining({ method: 'POST' }),
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('update_event patches an existing event', async () => {
|
|
243
|
+
mockFetch.mockResolvedValue({
|
|
244
|
+
ok: true,
|
|
245
|
+
status: 200,
|
|
246
|
+
json: () => Promise.resolve({ id: 'evt-1', subject: 'Updated' }),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const server = createMicrosoft365MCPServer({
|
|
250
|
+
tokenPath: '/tmp/ms.json',
|
|
251
|
+
clientId: 'id',
|
|
252
|
+
clientSecret: 'secret',
|
|
253
|
+
});
|
|
254
|
+
const tools = server.getRegisteredTools();
|
|
255
|
+
const result = await tools.update_event.handler({
|
|
256
|
+
eventId: 'evt-1',
|
|
257
|
+
subject: 'Updated',
|
|
258
|
+
});
|
|
259
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
260
|
+
expect(parsed.subject).toBe('Updated');
|
|
261
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
262
|
+
expect.stringContaining('/me/events/evt-1'),
|
|
263
|
+
expect.objectContaining({ method: 'PATCH' }),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('delete_event deletes a calendar event', async () => {
|
|
268
|
+
mockFetch.mockResolvedValue({ ok: true, status: 204 });
|
|
269
|
+
|
|
270
|
+
const server = createMicrosoft365MCPServer({
|
|
271
|
+
tokenPath: '/tmp/ms.json',
|
|
272
|
+
clientId: 'id',
|
|
273
|
+
clientSecret: 'secret',
|
|
274
|
+
});
|
|
275
|
+
const tools = server.getRegisteredTools();
|
|
276
|
+
const result = await tools.delete_event.handler({ eventId: 'evt-1' });
|
|
277
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
278
|
+
expect(parsed.deleted).toBe(true);
|
|
279
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
280
|
+
expect.stringContaining('/me/events/evt-1'),
|
|
281
|
+
expect.objectContaining({ method: 'DELETE' }),
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('OneDrive Tools', () => {
|
|
287
|
+
beforeEach(() => vi.clearAllMocks());
|
|
288
|
+
|
|
289
|
+
it('search_files searches OneDrive', async () => {
|
|
290
|
+
mockFetch.mockResolvedValue({
|
|
291
|
+
ok: true,
|
|
292
|
+
json: () =>
|
|
293
|
+
Promise.resolve({
|
|
294
|
+
value: [
|
|
295
|
+
{
|
|
296
|
+
id: 'file-1',
|
|
297
|
+
name: 'Budget.xlsx',
|
|
298
|
+
file: {
|
|
299
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
300
|
+
},
|
|
301
|
+
size: 12345,
|
|
302
|
+
lastModifiedDateTime: '2026-02-28T12:00:00Z',
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
}),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const server = createMicrosoft365MCPServer({
|
|
309
|
+
tokenPath: '/tmp/ms.json',
|
|
310
|
+
clientId: 'id',
|
|
311
|
+
clientSecret: 'secret',
|
|
312
|
+
});
|
|
313
|
+
const tools = server.getRegisteredTools();
|
|
314
|
+
const result = await tools.search_files.handler({ query: 'Budget' });
|
|
315
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
316
|
+
expect(parsed.files).toHaveLength(1);
|
|
317
|
+
expect(parsed.files[0].name).toBe('Budget.xlsx');
|
|
318
|
+
});
|
|
319
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @goforgeit/mcp-microsoft-365 — MCP server for Microsoft 365
|
|
3
|
+
*
|
|
4
|
+
* Provides Outlook, Calendar, and OneDrive tools via stdio transport.
|
|
5
|
+
* Uses Microsoft Graph REST API directly (no SDK dependency).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { createForgeTokenManager } from '@forge/shared/mcp-auth';
|
|
12
|
+
|
|
13
|
+
const MS_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
|
|
14
|
+
const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
|
|
15
|
+
|
|
16
|
+
export interface Microsoft365MCPOptions {
|
|
17
|
+
tokenPath: string;
|
|
18
|
+
clientId: string;
|
|
19
|
+
clientSecret: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ToolHandler {
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Microsoft365MCPServer {
|
|
28
|
+
server: McpServer;
|
|
29
|
+
getRegisteredTools(): Record<string, ToolHandler>;
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createMicrosoft365MCPServer(
|
|
34
|
+
options: Microsoft365MCPOptions,
|
|
35
|
+
): Microsoft365MCPServer {
|
|
36
|
+
const tokenManager = createForgeTokenManager({
|
|
37
|
+
tokenPath: options.tokenPath,
|
|
38
|
+
clientId: options.clientId,
|
|
39
|
+
clientSecret: options.clientSecret,
|
|
40
|
+
tokenUrl: MS_TOKEN_URL,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const server = new McpServer({
|
|
44
|
+
name: '@goforgeit/mcp-microsoft-365',
|
|
45
|
+
version: '0.1.0',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const registeredTools: Record<string, ToolHandler> = {};
|
|
49
|
+
|
|
50
|
+
function registerTool(
|
|
51
|
+
name: string,
|
|
52
|
+
description: string,
|
|
53
|
+
schema: Record<string, z.ZodType>,
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
handler: (args: any) => Promise<{ content: Array<{ type: 'text'; text: string }> }>,
|
|
56
|
+
) {
|
|
57
|
+
registeredTools[name] = { handler };
|
|
58
|
+
server.tool(name, description, schema, handler);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function graphRequest(
|
|
62
|
+
path: string,
|
|
63
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
|
|
64
|
+
body?: unknown,
|
|
65
|
+
): Promise<unknown> {
|
|
66
|
+
const token = await tokenManager.getAccessToken();
|
|
67
|
+
const options: RequestInit = {
|
|
68
|
+
method,
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${token}`,
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
if (body) options.body = JSON.stringify(body);
|
|
75
|
+
|
|
76
|
+
const response = await fetch(`${GRAPH_BASE}${path}`, options);
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
throw new Error(`Microsoft Graph API error (${response.status}): ${errorText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (response.status === 204) return {};
|
|
84
|
+
return response.json();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Outlook Tools ---
|
|
88
|
+
|
|
89
|
+
registerTool(
|
|
90
|
+
'search_emails',
|
|
91
|
+
'Search Outlook emails',
|
|
92
|
+
{
|
|
93
|
+
query: z.string().describe('Search query (OData $search syntax)'),
|
|
94
|
+
top: z.number().optional().default(10).describe('Maximum results'),
|
|
95
|
+
},
|
|
96
|
+
async (args: { query: string; top?: number }) => {
|
|
97
|
+
const top = args.top || 10;
|
|
98
|
+
const data = (await graphRequest(
|
|
99
|
+
`/me/messages?$search="${encodeURIComponent(args.query)}"&$top=${top}&$select=id,subject,from,receivedDateTime,bodyPreview,isRead`,
|
|
100
|
+
)) as { value: unknown[] };
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'text' as const,
|
|
106
|
+
text: JSON.stringify({ messages: data.value }),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
registerTool(
|
|
114
|
+
'get_email',
|
|
115
|
+
'Get full email details by message ID',
|
|
116
|
+
{
|
|
117
|
+
messageId: z.string().describe('Outlook message ID'),
|
|
118
|
+
},
|
|
119
|
+
async (args: { messageId: string }) => {
|
|
120
|
+
const data = await graphRequest(`/me/messages/${args.messageId}`);
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
registerTool('list_folders', 'List Outlook mail folders', {}, async () => {
|
|
128
|
+
const data = (await graphRequest('/me/mailFolders?$top=50')) as { value: unknown[] };
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: 'text' as const,
|
|
133
|
+
text: JSON.stringify({ folders: data.value }),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// --- Outlook Write Tools ---
|
|
140
|
+
|
|
141
|
+
registerTool(
|
|
142
|
+
'create_draft',
|
|
143
|
+
'Create an Outlook draft email',
|
|
144
|
+
{
|
|
145
|
+
to: z.string().describe('Recipient email address'),
|
|
146
|
+
subject: z.string().describe('Email subject'),
|
|
147
|
+
body: z.string().describe('Email body (plain text)'),
|
|
148
|
+
},
|
|
149
|
+
async (args: { to: string; subject: string; body: string }) => {
|
|
150
|
+
const data = await graphRequest('/me/messages', 'POST', {
|
|
151
|
+
subject: args.subject,
|
|
152
|
+
body: { contentType: 'Text', content: args.body },
|
|
153
|
+
toRecipients: [{ emailAddress: { address: args.to } }],
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
registerTool(
|
|
162
|
+
'archive_email',
|
|
163
|
+
'Move an Outlook email to Archive folder',
|
|
164
|
+
{
|
|
165
|
+
messageId: z.string().describe('Outlook message ID to archive'),
|
|
166
|
+
},
|
|
167
|
+
async (args: { messageId: string }) => {
|
|
168
|
+
const data = await graphRequest(`/me/messages/${args.messageId}/move`, 'POST', {
|
|
169
|
+
destinationId: 'archive',
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// --- Calendar Tools ---
|
|
178
|
+
|
|
179
|
+
registerTool(
|
|
180
|
+
'list_events',
|
|
181
|
+
'List calendar events in a date range',
|
|
182
|
+
{
|
|
183
|
+
startDateTime: z.string().optional().describe('Start of range (ISO 8601)'),
|
|
184
|
+
endDateTime: z.string().optional().describe('End of range (ISO 8601)'),
|
|
185
|
+
top: z.number().optional().default(25).describe('Maximum events'),
|
|
186
|
+
},
|
|
187
|
+
async (args: { startDateTime?: string; endDateTime?: string; top?: number }) => {
|
|
188
|
+
let path = '/me/events?$orderby=start/dateTime';
|
|
189
|
+
path += `&$top=${args.top || 25}`;
|
|
190
|
+
if (args.startDateTime) {
|
|
191
|
+
path += `&$filter=start/dateTime ge '${args.startDateTime}'`;
|
|
192
|
+
if (args.endDateTime) {
|
|
193
|
+
path += ` and end/dateTime le '${args.endDateTime}'`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
path +=
|
|
197
|
+
'&$select=id,subject,start,end,location,attendees,bodyPreview,webLink,isAllDay,organizer';
|
|
198
|
+
|
|
199
|
+
const data = (await graphRequest(path)) as { value: unknown[] };
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: 'text' as const,
|
|
204
|
+
text: JSON.stringify({ events: data.value }),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
registerTool(
|
|
212
|
+
'get_event',
|
|
213
|
+
'Get a specific calendar event by ID',
|
|
214
|
+
{
|
|
215
|
+
eventId: z.string().describe('Calendar event ID'),
|
|
216
|
+
},
|
|
217
|
+
async (args: { eventId: string }) => {
|
|
218
|
+
const data = await graphRequest(`/me/events/${args.eventId}`);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// --- Calendar Write Tools ---
|
|
226
|
+
|
|
227
|
+
registerTool(
|
|
228
|
+
'create_event',
|
|
229
|
+
'Create a new calendar event in Outlook',
|
|
230
|
+
{
|
|
231
|
+
subject: z.string().describe('Event title'),
|
|
232
|
+
startDateTime: z.string().describe('Start time (ISO 8601)'),
|
|
233
|
+
endDateTime: z.string().describe('End time (ISO 8601)'),
|
|
234
|
+
body: z.string().optional().describe('Event description'),
|
|
235
|
+
location: z.string().optional().describe('Event location'),
|
|
236
|
+
attendeeEmails: z.array(z.string()).optional().describe('Attendee email addresses'),
|
|
237
|
+
},
|
|
238
|
+
async (args: {
|
|
239
|
+
subject: string;
|
|
240
|
+
startDateTime: string;
|
|
241
|
+
endDateTime: string;
|
|
242
|
+
body?: string;
|
|
243
|
+
location?: string;
|
|
244
|
+
attendeeEmails?: string[];
|
|
245
|
+
}) => {
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
+
const event: Record<string, any> = {
|
|
248
|
+
subject: args.subject,
|
|
249
|
+
start: { dateTime: args.startDateTime, timeZone: 'UTC' },
|
|
250
|
+
end: { dateTime: args.endDateTime, timeZone: 'UTC' },
|
|
251
|
+
};
|
|
252
|
+
if (args.body) event.body = { contentType: 'Text', content: args.body };
|
|
253
|
+
if (args.location) event.location = { displayName: args.location };
|
|
254
|
+
if (args.attendeeEmails) {
|
|
255
|
+
event.attendees = args.attendeeEmails.map((email) => ({
|
|
256
|
+
emailAddress: { address: email },
|
|
257
|
+
type: 'required',
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const data = await graphRequest('/me/events', 'POST', event);
|
|
262
|
+
return {
|
|
263
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
registerTool(
|
|
269
|
+
'update_event',
|
|
270
|
+
'Update an existing Outlook calendar event',
|
|
271
|
+
{
|
|
272
|
+
eventId: z.string().describe('Calendar event ID'),
|
|
273
|
+
subject: z.string().optional().describe('New event title'),
|
|
274
|
+
startDateTime: z.string().optional().describe('New start time (ISO 8601)'),
|
|
275
|
+
endDateTime: z.string().optional().describe('New end time (ISO 8601)'),
|
|
276
|
+
body: z.string().optional().describe('New event description'),
|
|
277
|
+
location: z.string().optional().describe('New event location'),
|
|
278
|
+
},
|
|
279
|
+
async (args: {
|
|
280
|
+
eventId: string;
|
|
281
|
+
subject?: string;
|
|
282
|
+
startDateTime?: string;
|
|
283
|
+
endDateTime?: string;
|
|
284
|
+
body?: string;
|
|
285
|
+
location?: string;
|
|
286
|
+
}) => {
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
288
|
+
const patch: Record<string, any> = {};
|
|
289
|
+
if (args.subject) patch.subject = args.subject;
|
|
290
|
+
if (args.body) patch.body = { contentType: 'Text', content: args.body };
|
|
291
|
+
if (args.location) patch.location = { displayName: args.location };
|
|
292
|
+
if (args.startDateTime) patch.start = { dateTime: args.startDateTime, timeZone: 'UTC' };
|
|
293
|
+
if (args.endDateTime) patch.end = { dateTime: args.endDateTime, timeZone: 'UTC' };
|
|
294
|
+
|
|
295
|
+
const data = await graphRequest(`/me/events/${args.eventId}`, 'PATCH', patch);
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
registerTool(
|
|
303
|
+
'delete_event',
|
|
304
|
+
'Delete an Outlook calendar event',
|
|
305
|
+
{
|
|
306
|
+
eventId: z.string().describe('Calendar event ID to delete'),
|
|
307
|
+
},
|
|
308
|
+
async (args: { eventId: string }) => {
|
|
309
|
+
await graphRequest(`/me/events/${args.eventId}`, 'DELETE');
|
|
310
|
+
return {
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: 'text' as const,
|
|
314
|
+
text: JSON.stringify({ deleted: true, eventId: args.eventId }),
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// --- OneDrive Tools ---
|
|
322
|
+
|
|
323
|
+
registerTool(
|
|
324
|
+
'search_files',
|
|
325
|
+
'Search OneDrive files',
|
|
326
|
+
{
|
|
327
|
+
query: z.string().describe('Search query'),
|
|
328
|
+
top: z.number().optional().default(20).describe('Maximum results'),
|
|
329
|
+
},
|
|
330
|
+
async (args: { query: string; top?: number }) => {
|
|
331
|
+
const top = args.top || 20;
|
|
332
|
+
const data = (await graphRequest(
|
|
333
|
+
`/me/drive/root/search(q='${encodeURIComponent(args.query)}')?$top=${top}&$select=id,name,file,size,lastModifiedDateTime,webUrl`,
|
|
334
|
+
)) as { value: unknown[] };
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
content: [
|
|
338
|
+
{
|
|
339
|
+
type: 'text' as const,
|
|
340
|
+
text: JSON.stringify({ files: data.value }),
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
registerTool(
|
|
348
|
+
'get_file_content',
|
|
349
|
+
'Get OneDrive file metadata by item ID',
|
|
350
|
+
{
|
|
351
|
+
itemId: z.string().describe('OneDrive item ID'),
|
|
352
|
+
},
|
|
353
|
+
async (args: { itemId: string }) => {
|
|
354
|
+
const data = await graphRequest(`/me/drive/items/${args.itemId}`);
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
server,
|
|
363
|
+
getRegisteredTools() {
|
|
364
|
+
return registeredTools;
|
|
365
|
+
},
|
|
366
|
+
async start() {
|
|
367
|
+
const transport = new StdioServerTransport();
|
|
368
|
+
await server.connect(transport);
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// CLI entry point
|
|
374
|
+
const isMainModule =
|
|
375
|
+
process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, '/'));
|
|
376
|
+
if (isMainModule || process.env.FORGE_MCP_START === 'true') {
|
|
377
|
+
const tokenPath = process.env.FORGE_TOKEN_PATH;
|
|
378
|
+
const clientId = process.env.FORGE_CLIENT_ID;
|
|
379
|
+
const clientSecret = process.env.FORGE_CLIENT_SECRET;
|
|
380
|
+
|
|
381
|
+
if (!tokenPath || !clientId || !clientSecret) {
|
|
382
|
+
console.error(
|
|
383
|
+
'Required environment variables: FORGE_TOKEN_PATH, FORGE_CLIENT_ID, FORGE_CLIENT_SECRET',
|
|
384
|
+
);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const mcpServer = createMicrosoft365MCPServer({ tokenPath, clientId, clientSecret });
|
|
389
|
+
mcpServer.start().catch((err) => {
|
|
390
|
+
console.error('Failed to start MCP server:', err);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
393
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
target: 'node20',
|
|
7
|
+
outDir: 'dist',
|
|
8
|
+
clean: true,
|
|
9
|
+
dts: true,
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
noExternal: ['@forge/shared'],
|
|
12
|
+
banner: {
|
|
13
|
+
js: '#!/usr/bin/env node',
|
|
14
|
+
},
|
|
15
|
+
});
|