# Directory Structure ``` ├── .gitignore ├── images │ ├── Claude_Server_Tools_Setup.png │ ├── get_user_tool_test.png │ ├── Inspector.png │ └── Interceptor.png ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ ├── main.ts │ └── tools.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` /node_modules /*.env /build ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Dynamics 365 MCP Server 🚀     ## Overview The **Microsoft Dynamics 365 MCP Server** is a MCP server that provides tools to interact with Microsoft Dynamics 365 using the **Model Context Protocol(MCP)** by Anthorpic. It allows users to perform various operations such as retrieving user information, accounts, opportunities associated with an account, create and update accounts from **Claude Desktop**. This project uses the `@modelcontextprotocol/sdk` library to implement the MCP server and tools, and it integrates with Dynamics 365 APIs for data operations. --- ## List of Tools 🛠️ | **Tool Name** | **Description** | **Input** | **Output** | | ------------------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `get-user-info` | Fetches information about the currently authenticated user. | None | User details including name, user ID, and business unit ID. | | `fetch-accounts` | Fetches all accounts from Dynamics 365. | None | List of accounts in JSON format. | | `get-associated-opportunities` | Fetches opportunities associated with a given account. | `accountId` (string, required) | List of opportunities in JSON format. | | `create-account` | Creates a new account in Dynamics 365. | `accountData` (object, required) containing account details. | Details of the created account in JSON format. | | `update-account` | Updates an existing account in Dynamics 365. | `accountId` (string, required), `accountData` (object, required) containing updated details. | Details of the updated account in JSON format. | --- ## Prerequisites 📝 Before setting up the project, ensure you have the following installed: - **Node.js** (v16 or higher) - **NPM** (Node Package Manager) - A Dynamics 365 instance with API access - Azure Active Directory (AAD) application configured for Dynamics 365 API access --- ## Configuration Steps ⚙️ Follow these steps to set up and run the project locally: ### 1. Clone the Repository ```sh git clone https://github.com/your-repo/dynamics365-mcp-server.git cd dynamics365-mcp-server ``` ### 2. Install Dependencies ```sh npm install ``` ### 3. Configure Environment Variables Create a .env file in the root of the project and add the following variables: ```sh CLIENT_ID=your-client-id CLIENT_SECRET=your-client-secret TENANT_ID=your-tenant-id D365_URL=https://your-org.crm.dynamics.com ``` ### 4. Compile TypeScript Files ```sh npm run build ``` ### 4. Run MCP Server ```sh node build\index.js ``` You should see the following output: ```plaintext Dynamics365 MCP server running on stdio... ``` ### 5. (Optional) Register your MCP Server with Claude Desktop - Install [Claude Desktop](https://claude.ai/download) - Navigate to Settings > Developer > Edit Config - Edit claude_desktop_config.json ```json { "mcpServers": { "Dynamics365": { "command": "node", "args": [ "<Path to your MCP server build file ex: rootfolder/build/index.js>" ], "env": { "CLIENT_ID": "<D365 Client Id>", "CLIENT_SECRET": "<D365 Client Secret>", "TENANT_ID": "<D365 Tenant ID>", "D365_URL": "Dynamics 365 url" } } } } ``` - Restart Claude Desktop - Now you should be able to see the server tools in the prompt window  - Let's test a prompt by invoking tool - get-user-info  ### 6. (Optional) Test tools using MCP Interceptor - Run following command in terminal ```json npx @modelcontextprotocol/inspector node build/index.js ```  - Go to 🔍 http://localhost:5173 🚀  - Now you can connect to server and terst all the tools!! ## Debugging 🐛 ## If you encounter issues, ensure the following: If you encounter issues, ensure the following: - The .env file is properly configured. - The Azure AD application has the necessary permissions for Dynamics 365 APIs. - The Dynamics 365 instance is accessible from - your environment. - You can also add debug logs in the code to trace issues. For example: ```sh console.error("Debugging: Loaded environment variables:", process.env); ``` ## Contributing 🤝 Contributions are welcome! Feel free to submit a pull request or open an issue for any bugs or feature requests. To contribute: - Fork the repository. - Create a new branch for your feature or bug fix. - Commit your changes and submit a pull request. - We appreciate your contributions! 😊 ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "sourceMap": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "dynamics365", "version": "1.0.0", "main": "index.js", "type": "module", "bin": { "dynamics365": "./build/index.js" }, "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js','755')\"" }, "files": [ "build" ], "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@azure/msal-browser": "^4.8.0", "@azure/msal-node": "^3.4.0", "@modelcontextprotocol/sdk": "^1.7.0", "dotenv": "^16.4.7", "zod": "^3.24.2" }, "devDependencies": { "@types/express": "^5.0.1", "@types/node": "^22.13.11", "typescript": "^5.8.2" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Dynamics365 } from "./main.js"; import { registerTools } from "./tools.js"; import dotenv from "dotenv"; // Load environment variables from .env file dotenv.config(); // Create server instance const server = new McpServer({ name: "Dynamics365", version: "1.0.0.0", }); const clientId = process.env.CLIENT_ID; const clientSecret = process.env.CLIENT_SECRET; const tenantId = process.env.TENANT_ID; const D365_BASE_URL = process.env.D365_URL; if (!clientId || !clientSecret || !tenantId || !D365_BASE_URL) { console.error( "Missing required environment variables. Please check your .env file." ); process.exit(1); } const d365 = new Dynamics365(clientId, clientSecret, tenantId, D365_BASE_URL); // Register all tools registerTools(server, d365); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Dynamics365 MCP server running on stdio..."); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { Dynamics365 } from "./main.js"; export function registerTools(server: McpServer, d365: Dynamics365) { // Register the "get-user-info" tool server.tool( "get-user-info", "Get user info from Dynamics 365", {}, async () => { try { const response = await d365.makeWhoAmIRequest(); return { content: [ { type: "text", text: `Hi ${response.FullName}, your user ID is ${response.UserId} and your business unit ID is ${response.BusinessUnitId}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : "Unknown error" }, please check your credentials and try again.`, }, ], isError: true, }; } } ); // Register the "fetch-accounts" tool server.tool( "fetch-accounts", "Fetch accounts from Dynamics 365", {}, async () => { try { const response = await d365.getAccounts(); const accounts = JSON.stringify(response.value, null, 2); return { content: [ { type: "text", text: accounts, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : "Unknown error" }, please check your credentials and try again.`, }, ], isError: true, }; } } ); // Register the "get-associated-opportunities" tool server.tool( "get-associated-opportunities", "Fetch opportunities for a given account from Dynamics 365", { accountId: z.string() }, async (req) => { try { const response = await d365.getAssociatedOpportunities(req.accountId); return { content: [ { type: "text", text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : "Unknown error" }, please check your input and try again.`, }, ], isError: true, }; } } ); // Register the "create-account" tool server.tool( "create-account", "Create a new account in Dynamics 365", { accountData: z.object({}) }, async (params) => { try { const { accountData } = params; const response = await d365.createAccount(accountData); return { content: [ { type: "text", text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : "Unknown error" }, please check your input and try again.`, }, ], isError: true, }; } } ); // Register the "update-account" tool server.tool( "update-account", "Update an existing account in Dynamics 365", { accountId: z.string(), accountData: z.object({}), }, async (params) => { try { const { accountId, accountData } = params; const response = await d365.updateAccount(accountId, accountData); return { content: [ { type: "text", text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : "Unknown error" }, please check your input and try again.`, }, ], isError: true, }; } } ); } ``` -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- ```typescript /** * Functions for Dynamics 365 authentication and user identity */ // Import required types import { Configuration, ConfidentialClientApplication, ClientCredentialRequest, } from "@azure/msal-node"; import dotenv from "dotenv"; // Load environment variables from .env file dotenv.config(); export class Dynamics365 { private clientId: string; private clientSecret: string; private tenantId: string; private d365Url: string; private msalInstance: ConfidentialClientApplication; private accessToken: string | null = null; private tokenExpiration: number | null = null; /** * @param clientId - Azure AD application client ID * @param clientSecret - Azure AD application client secret * @param tenantId - Azure AD tenant ID * @param d365Url - Dynamics 365 instance URL (e.g., https://your-org.crm.dynamics.com) */ constructor( clientId: string, clientSecret: string, tenantId: string, d365Url: string ) { this.clientId = clientId; this.clientSecret = clientSecret; this.tenantId = tenantId; this.d365Url = d365Url; // Configure MSAL const msalConfig: Configuration = { auth: { clientId: this.clientId, authority: `https://login.microsoftonline.com/${this.tenantId}`, clientSecret: this.clientSecret, }, }; // Initialize MSAL client this.msalInstance = new ConfidentialClientApplication(msalConfig); } /** * @returns Promise resolving to authentication result with token */ private async authenticate(): Promise<string> { const tokenRequest: ClientCredentialRequest = { scopes: [`${new URL(this.d365Url).origin}/.default`], }; try { // Check if the token is expired or about to expire if (this.tokenExpiration && Date.now() < this.tokenExpiration) { return this.accessToken as string; } const response = await this.msalInstance.acquireTokenByClientCredential( tokenRequest ); if (response && response.accessToken) { this.accessToken = response.accessToken; if (response.expiresOn) { this.tokenExpiration = response.expiresOn.getTime() - 3 * 60 * 1000; } } else { throw new Error( "Token acquisition failed: response is null or invalid." ); } } catch (error) { console.error("Token acquisition failed:", error); throw new Error( `Failed to authenticate with Dynamics 365: ${ error instanceof Error ? error.message : String(error) }` ); } return this.accessToken; } /** * Makes an API request to Dynamics 365 * @param endpoint - The API endpoint (relative to the base URL) * @param method - The HTTP method (e.g., "GET", "POST") * @param body - The request body (optional, for POST/PUT requests) * @param additionalHeaders - Additional headers to include in the request * @returns Promise resolving to the API response */ private async makeApiRequest( endpoint: string, method: string, body?: any, additionalHeaders?: Record<string, string> ): Promise<any> { const token = await this.authenticate(); const baseUrl = this.d365Url.endsWith("/") ? this.d365Url : `${this.d365Url}/`; const url = `${baseUrl}${endpoint}`; const headers: Record<string, string> = { Authorization: `Bearer ${token}`, Accept: "application/json", "Content-Type": "application/json", "OData-MaxVersion": "4.0", "OData-Version": "4.0", ...additionalHeaders, }; try { const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw new Error( `API request failed with status: ${ response.status }, message: ${await response.text()}` ); } return await response.json(); } catch (error) { console.error(`API request to ${url} failed:`, error); throw new Error( `Failed to make API request: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Makes a WhoAmI request to Dynamics 365 to get information about the currently logged-in user * @returns Promise resolving to the user's information */ public async makeWhoAmIRequest(): Promise<{ BusinessUnitId: string; UserId: string; OrganizationId: string; UserName?: string; FullName?: string; }> { const data = await this.makeApiRequest("api/data/v9.2/WhoAmI", "GET"); // If we want to get more details about the user, we can make an additional request if (data && data.UserId) { const userDetails = await this.makeApiRequest( `api/data/v9.2/systemusers(${data.UserId})`, "GET", undefined, { Prefer: 'odata.include-annotations="*"' } ); data.UserName = userDetails.domainname; data.FullName = userDetails.fullname; } return data; } /** * Fetches accounts from Dynamics 365 * @returns Promise resolving to the list of accounts */ public async getAccounts(): Promise<any> { return this.makeApiRequest("api/data/v9.2/accounts", "GET"); } /** * Fetches contacts for a given account from Dynamics 365 * @param accountId - The ID of the account for which to retrieve contacts * @returns Promise resolving to the list of contacts */ public async getAssociatedContacts(accountId: string): Promise<any> { if (!accountId) { throw new Error("Account ID is required to fetch contacts."); } const endpoint = `api/data/v9.2/contacts?$filter=_parentcustomerid_value eq ${accountId}`; return this.makeApiRequest(endpoint, "GET"); } /** * Fetches opportunities for a given account from Dynamics 365 * @param accountId - The ID of the account for which to retrieve opportunities * @returns Promise resolving to the list of opportunities */ public async getAssociatedOpportunities(accountId: string): Promise<any> { if (!accountId) { throw new Error("Account ID is required to fetch opportunities."); } const endpoint = `api/data/v9.2/opportunities?$filter=_customerid_value eq ${accountId}`; return this.makeApiRequest(endpoint, "GET"); } /* create a new account in Dynamics 365 * @param accountData - The data for the new account * @returns Promise resolving to the created account */ public async createAccount(accountData: any): Promise<any> { if (!accountData) { throw new Error("Account data is required to create an account."); } const endpoint = "api/data/v9.2/accounts"; return this.makeApiRequest(endpoint, "POST", accountData); } /** * Updates an existing account in Dynamics 365 * @param accountId - The ID of the account to update * @param accountData - The updated data for the account * @returns Promise resolving to the updated account */ public async updateAccount( accountId: string, accountData: any ): Promise<any> { if (!accountId) { throw new Error("Account ID is required to update an account."); } if (!accountData) { throw new Error("Account data is required to update an account."); } const endpoint = `api/data/v9.2/accounts(${accountId})`; return this.makeApiRequest(endpoint, "PATCH", accountData); } } ```