UNPKG

@softeria/ms-365-mcp-server

Version:
169 lines (168 loc) 8.22 kB
import logger from './logger.js'; import { api } from './generated/client.js'; import { z } from 'zod'; export function registerGraphTools(server, graphClient, readOnly = false) { for (const tool of api.endpoints) { if (readOnly && tool.method.toUpperCase() !== 'GET') { logger.info(`Skipping write operation ${tool.alias} in read-only mode`); continue; } const paramSchema = {}; if (tool.parameters && tool.parameters.length > 0) { for (const param of tool.parameters) { if (param.type === 'Body' && param.schema) { paramSchema[param.name] = z.union([z.string(), param.schema]); } else { paramSchema[param.name] = param.schema || z.any(); } } } server.tool(tool.alias, tool.description ?? '', paramSchema, { title: tool.alias, readOnlyHint: tool.method.toUpperCase() === 'GET', }, async (params, extra) => { logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`); try { logger.info(`params: ${JSON.stringify(params)}`); const parameterDefinitions = tool.parameters || []; let path = tool.path; const queryParams = {}; const headers = {}; let body = null; for (let [paramName, paramValue] of Object.entries(params)) { // Ok, so, MCP clients (such as claude code) doesn't support $ in parameter names, // and others might not support __, so we strip them in hack.ts and restore them here const odataParams = [ 'filter', 'select', 'expand', 'orderby', 'skip', 'top', 'count', 'search', 'format', ]; const fixedParamName = odataParams.includes(paramName.toLowerCase()) ? `$${paramName.toLowerCase()}` : paramName; const paramDef = parameterDefinitions.find((p) => p.name === paramName); if (paramDef) { switch (paramDef.type) { case 'Path': path = path .replace(`{${paramName}}`, encodeURIComponent(paramValue)) .replace(`:${paramName}`, encodeURIComponent(paramValue)); break; case 'Query': queryParams[fixedParamName] = `${paramValue}`; break; case 'Body': if (typeof paramValue === 'string') { try { body = JSON.parse(paramValue); } catch (e) { body = paramValue; } } else { body = paramValue; } break; case 'Header': headers[fixedParamName] = `${paramValue}`; break; } } else if (paramName === 'body') { if (typeof paramValue === 'string') { try { body = JSON.parse(paramValue); } catch (e) { body = paramValue; } } else { body = paramValue; } logger.info(`Set legacy body param: ${JSON.stringify(body)}`); } } if (Object.keys(queryParams).length > 0) { const queryString = Object.entries(queryParams) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); path = `${path}${path.includes('?') ? '&' : '?'}${queryString}`; } const options = { method: tool.method.toUpperCase(), headers, }; if (options.method !== 'GET' && body) { options.body = typeof body === 'string' ? body : JSON.stringify(body); } const isProbablyMediaContent = tool.errors?.some((error) => error.description === 'Retrieved media content') || path.endsWith('/content'); if (isProbablyMediaContent) { options.rawResponse = true; } logger.info(`Making graph request to ${path} with options: ${JSON.stringify(options)}`); const response = await graphClient.graphRequest(path, options); if (response && response.content && response.content.length > 0) { const responseText = response.content[0].text; const responseSize = responseText.length; logger.info(`Response size: ${responseSize} characters`); try { const jsonResponse = JSON.parse(responseText); if (jsonResponse.value && Array.isArray(jsonResponse.value)) { logger.info(`Response contains ${jsonResponse.value.length} items`); if (jsonResponse.value.length > 0 && jsonResponse.value[0].body) { logger.info(`First item has body field with size: ${JSON.stringify(jsonResponse.value[0].body).length} characters`); } } if (jsonResponse['@odata.nextLink']) { logger.info(`Response has pagination nextLink: ${jsonResponse['@odata.nextLink']}`); } const preview = responseText.substring(0, 500); logger.info(`Response preview: ${preview}${responseText.length > 500 ? '...' : ''}`); } catch (e) { const preview = responseText.substring(0, 500); logger.info(`Response preview (non-JSON): ${preview}${responseText.length > 500 ? '...' : ''}`); } } // Convert McpResponse to CallToolResult with the correct structure const content = response.content.map((item) => { // GraphClient only returns text content items, so create proper TextContent items const textContent = { type: 'text', text: item.text, }; return textContent; }); const result = { content, _meta: response._meta, isError: response.isError, }; return result; } catch (error) { logger.error(`Error in tool ${tool.alias}: ${error.message}`); const errorContent = { type: 'text', text: JSON.stringify({ error: `Error in tool ${tool.alias}: ${error.message}`, }), }; return { content: [errorContent], isError: true, }; } }); } }