# 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: -------------------------------------------------------------------------------- ``` 1 | /node_modules 2 | /*.env 3 | /build 4 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Dynamics 365 MCP Server 🚀 2 | 3 |  4 |  5 |  6 |  7 | 8 | ## Overview 9 | 10 | 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**. 11 | 12 | This project uses the `@modelcontextprotocol/sdk` library to implement the MCP server and tools, and it integrates with Dynamics 365 APIs for data operations. 13 | 14 | --- 15 | 16 | ## List of Tools 🛠️ 17 | 18 | | **Tool Name** | **Description** | **Input** | **Output** | 19 | | ------------------------------ | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | 20 | | `get-user-info` | Fetches information about the currently authenticated user. | None | User details including name, user ID, and business unit ID. | 21 | | `fetch-accounts` | Fetches all accounts from Dynamics 365. | None | List of accounts in JSON format. | 22 | | `get-associated-opportunities` | Fetches opportunities associated with a given account. | `accountId` (string, required) | List of opportunities in JSON format. | 23 | | `create-account` | Creates a new account in Dynamics 365. | `accountData` (object, required) containing account details. | Details of the created account in JSON format. | 24 | | `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. | 25 | 26 | --- 27 | 28 | ## Prerequisites 📝 29 | 30 | Before setting up the project, ensure you have the following installed: 31 | 32 | - **Node.js** (v16 or higher) 33 | - **NPM** (Node Package Manager) 34 | - A Dynamics 365 instance with API access 35 | - Azure Active Directory (AAD) application configured for Dynamics 365 API access 36 | 37 | --- 38 | 39 | ## Configuration Steps ⚙️ 40 | 41 | Follow these steps to set up and run the project locally: 42 | 43 | ### 1. Clone the Repository 44 | 45 | ```sh 46 | git clone https://github.com/your-repo/dynamics365-mcp-server.git 47 | cd dynamics365-mcp-server 48 | ``` 49 | 50 | ### 2. Install Dependencies 51 | 52 | ```sh 53 | npm install 54 | ``` 55 | 56 | ### 3. Configure Environment Variables 57 | 58 | Create a .env file in the root of the project and add the following variables: 59 | 60 | ```sh 61 | CLIENT_ID=your-client-id 62 | CLIENT_SECRET=your-client-secret 63 | TENANT_ID=your-tenant-id 64 | D365_URL=https://your-org.crm.dynamics.com 65 | 66 | ``` 67 | 68 | ### 4. Compile TypeScript Files 69 | 70 | ```sh 71 | npm run build 72 | 73 | ``` 74 | 75 | ### 4. Run MCP Server 76 | 77 | ```sh 78 | node build\index.js 79 | ``` 80 | 81 | You should see the following output: 82 | 83 | ```plaintext 84 | Dynamics365 MCP server running on stdio... 85 | ``` 86 | ### 5. (Optional) Register your MCP Server with Claude Desktop 87 | - Install [Claude Desktop](https://claude.ai/download) 88 | - Navigate to Settings > Developer > Edit Config 89 | - Edit claude_desktop_config.json 90 | ```json 91 | { 92 | "mcpServers": { 93 | "Dynamics365": { 94 | "command": "node", 95 | "args": [ 96 | "<Path to your MCP server build file ex: rootfolder/build/index.js>" 97 | ], 98 | "env": { 99 | "CLIENT_ID": "<D365 Client Id>", 100 | "CLIENT_SECRET": "<D365 Client Secret>", 101 | "TENANT_ID": "<D365 Tenant ID>", 102 | "D365_URL": "Dynamics 365 url" 103 | } 104 | } 105 | } 106 | } 107 | ``` 108 | - Restart Claude Desktop 109 | - Now you should be able to see the server tools in the prompt window 110 |  111 | 112 | - Let's test a prompt by invoking tool - get-user-info 113 |  114 | 115 | ### 6. (Optional) Test tools using MCP Interceptor 116 | - Run following command in terminal 117 | ```json 118 | npx @modelcontextprotocol/inspector node build/index.js 119 | ``` 120 |  121 | 122 | - Go to 🔍 http://localhost:5173 🚀 123 |  124 | 125 | - Now you can connect to server and terst all the tools!! 126 | 127 | 128 | ## Debugging 🐛 129 | 130 | ## If you encounter issues, ensure the following: 131 | 132 | If you encounter issues, ensure the following: 133 | 134 | - The .env file is properly configured. 135 | - The Azure AD application has the necessary permissions for Dynamics 365 APIs. 136 | - The Dynamics 365 instance is accessible from - your environment. 137 | - You can also add debug logs in the code to trace issues. For example: 138 | 139 | ```sh 140 | console.error("Debugging: Loaded environment variables:", process.env); 141 | ``` 142 | 143 | ## Contributing 🤝 144 | 145 | Contributions are welcome! Feel free to submit a pull request or open an issue for any bugs or feature requests. 146 | 147 | To contribute: 148 | 149 | - Fork the repository. 150 | - Create a new branch for your feature or bug fix. 151 | - Commit your changes and submit a pull request. 152 | - We appreciate your contributions! 😊 153 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "sourceMap": true, 13 | "resolveJsonModule": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "dynamics365", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "bin": { 7 | "dynamics365": "./build/index.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js','755')\"" 11 | }, 12 | "files": [ 13 | "build" 14 | ], 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "description": "", 19 | "dependencies": { 20 | "@azure/msal-browser": "^4.8.0", 21 | "@azure/msal-node": "^3.4.0", 22 | "@modelcontextprotocol/sdk": "^1.7.0", 23 | "dotenv": "^16.4.7", 24 | "zod": "^3.24.2" 25 | }, 26 | "devDependencies": { 27 | "@types/express": "^5.0.1", 28 | "@types/node": "^22.13.11", 29 | "typescript": "^5.8.2" 30 | } 31 | } 32 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { Dynamics365 } from "./main.js"; 4 | import { registerTools } from "./tools.js"; 5 | import dotenv from "dotenv"; 6 | 7 | // Load environment variables from .env file 8 | dotenv.config(); 9 | 10 | // Create server instance 11 | const server = new McpServer({ 12 | name: "Dynamics365", 13 | version: "1.0.0.0", 14 | }); 15 | 16 | const clientId = process.env.CLIENT_ID; 17 | const clientSecret = process.env.CLIENT_SECRET; 18 | const tenantId = process.env.TENANT_ID; 19 | const D365_BASE_URL = process.env.D365_URL; 20 | 21 | if (!clientId || !clientSecret || !tenantId || !D365_BASE_URL) { 22 | console.error( 23 | "Missing required environment variables. Please check your .env file." 24 | ); 25 | process.exit(1); 26 | } 27 | 28 | const d365 = new Dynamics365(clientId, clientSecret, tenantId, D365_BASE_URL); 29 | 30 | // Register all tools 31 | registerTools(server, d365); 32 | 33 | // Start the server 34 | async function main() { 35 | const transport = new StdioServerTransport(); 36 | await server.connect(transport); 37 | console.error("Dynamics365 MCP server running on stdio..."); 38 | } 39 | 40 | main().catch((error) => { 41 | console.error("Fatal error in main():", error); 42 | process.exit(1); 43 | }); 44 | ``` -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | import { Dynamics365 } from "./main.js"; 4 | 5 | export function registerTools(server: McpServer, d365: Dynamics365) { 6 | // Register the "get-user-info" tool 7 | server.tool( 8 | "get-user-info", 9 | "Get user info from Dynamics 365", 10 | {}, 11 | async () => { 12 | try { 13 | const response = await d365.makeWhoAmIRequest(); 14 | return { 15 | content: [ 16 | { 17 | type: "text", 18 | text: `Hi ${response.FullName}, your user ID is ${response.UserId} and your business unit ID is ${response.BusinessUnitId}`, 19 | }, 20 | ], 21 | }; 22 | } catch (error) { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text: `Error: ${ 28 | error instanceof Error ? error.message : "Unknown error" 29 | }, please check your credentials and try again.`, 30 | }, 31 | ], 32 | isError: true, 33 | }; 34 | } 35 | } 36 | ); 37 | 38 | // Register the "fetch-accounts" tool 39 | server.tool( 40 | "fetch-accounts", 41 | "Fetch accounts from Dynamics 365", 42 | {}, 43 | async () => { 44 | try { 45 | const response = await d365.getAccounts(); 46 | const accounts = JSON.stringify(response.value, null, 2); 47 | return { 48 | content: [ 49 | { 50 | type: "text", 51 | text: accounts, 52 | }, 53 | ], 54 | }; 55 | } catch (error) { 56 | return { 57 | content: [ 58 | { 59 | type: "text", 60 | text: `Error: ${ 61 | error instanceof Error ? error.message : "Unknown error" 62 | }, please check your credentials and try again.`, 63 | }, 64 | ], 65 | isError: true, 66 | }; 67 | } 68 | } 69 | ); 70 | 71 | // Register the "get-associated-opportunities" tool 72 | server.tool( 73 | "get-associated-opportunities", 74 | "Fetch opportunities for a given account from Dynamics 365", 75 | { accountId: z.string() }, 76 | async (req) => { 77 | try { 78 | const response = await d365.getAssociatedOpportunities(req.accountId); 79 | return { 80 | content: [ 81 | { 82 | type: "text", 83 | text: JSON.stringify(response, null, 2), 84 | }, 85 | ], 86 | }; 87 | } catch (error) { 88 | return { 89 | content: [ 90 | { 91 | type: "text", 92 | text: `Error: ${ 93 | error instanceof Error ? error.message : "Unknown error" 94 | }, please check your input and try again.`, 95 | }, 96 | ], 97 | isError: true, 98 | }; 99 | } 100 | } 101 | ); 102 | 103 | // Register the "create-account" tool 104 | server.tool( 105 | "create-account", 106 | "Create a new account in Dynamics 365", 107 | { accountData: z.object({}) }, 108 | async (params) => { 109 | try { 110 | const { accountData } = params; 111 | const response = await d365.createAccount(accountData); 112 | return { 113 | content: [ 114 | { 115 | type: "text", 116 | text: JSON.stringify(response, null, 2), 117 | }, 118 | ], 119 | }; 120 | } catch (error) { 121 | return { 122 | content: [ 123 | { 124 | type: "text", 125 | text: `Error: ${ 126 | error instanceof Error ? error.message : "Unknown error" 127 | }, please check your input and try again.`, 128 | }, 129 | ], 130 | isError: true, 131 | }; 132 | } 133 | } 134 | ); 135 | 136 | // Register the "update-account" tool 137 | server.tool( 138 | "update-account", 139 | "Update an existing account in Dynamics 365", 140 | { 141 | accountId: z.string(), 142 | accountData: z.object({}), 143 | }, 144 | async (params) => { 145 | try { 146 | const { accountId, accountData } = params; 147 | const response = await d365.updateAccount(accountId, accountData); 148 | return { 149 | content: [ 150 | { 151 | type: "text", 152 | text: JSON.stringify(response, null, 2), 153 | }, 154 | ], 155 | }; 156 | } catch (error) { 157 | return { 158 | content: [ 159 | { 160 | type: "text", 161 | text: `Error: ${ 162 | error instanceof Error ? error.message : "Unknown error" 163 | }, please check your input and try again.`, 164 | }, 165 | ], 166 | isError: true, 167 | }; 168 | } 169 | } 170 | ); 171 | } 172 | ``` -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Functions for Dynamics 365 authentication and user identity 3 | */ 4 | 5 | // Import required types 6 | import { 7 | Configuration, 8 | ConfidentialClientApplication, 9 | ClientCredentialRequest, 10 | } from "@azure/msal-node"; 11 | import dotenv from "dotenv"; 12 | 13 | // Load environment variables from .env file 14 | dotenv.config(); 15 | 16 | export class Dynamics365 { 17 | private clientId: string; 18 | private clientSecret: string; 19 | private tenantId: string; 20 | private d365Url: string; 21 | private msalInstance: ConfidentialClientApplication; 22 | private accessToken: string | null = null; 23 | private tokenExpiration: number | null = null; 24 | 25 | /** 26 | * @param clientId - Azure AD application client ID 27 | * @param clientSecret - Azure AD application client secret 28 | * @param tenantId - Azure AD tenant ID 29 | * @param d365Url - Dynamics 365 instance URL (e.g., https://your-org.crm.dynamics.com) 30 | */ 31 | constructor( 32 | clientId: string, 33 | clientSecret: string, 34 | tenantId: string, 35 | d365Url: string 36 | ) { 37 | this.clientId = clientId; 38 | this.clientSecret = clientSecret; 39 | this.tenantId = tenantId; 40 | this.d365Url = d365Url; 41 | 42 | // Configure MSAL 43 | const msalConfig: Configuration = { 44 | auth: { 45 | clientId: this.clientId, 46 | authority: `https://login.microsoftonline.com/${this.tenantId}`, 47 | clientSecret: this.clientSecret, 48 | }, 49 | }; 50 | 51 | // Initialize MSAL client 52 | this.msalInstance = new ConfidentialClientApplication(msalConfig); 53 | } 54 | 55 | /** 56 | * @returns Promise resolving to authentication result with token 57 | */ 58 | private async authenticate(): Promise<string> { 59 | const tokenRequest: ClientCredentialRequest = { 60 | scopes: [`${new URL(this.d365Url).origin}/.default`], 61 | }; 62 | 63 | try { 64 | // Check if the token is expired or about to expire 65 | if (this.tokenExpiration && Date.now() < this.tokenExpiration) { 66 | return this.accessToken as string; 67 | } 68 | 69 | const response = await this.msalInstance.acquireTokenByClientCredential( 70 | tokenRequest 71 | ); 72 | if (response && response.accessToken) { 73 | this.accessToken = response.accessToken; 74 | if (response.expiresOn) { 75 | this.tokenExpiration = response.expiresOn.getTime() - 3 * 60 * 1000; 76 | } 77 | } else { 78 | throw new Error( 79 | "Token acquisition failed: response is null or invalid." 80 | ); 81 | } 82 | } catch (error) { 83 | console.error("Token acquisition failed:", error); 84 | throw new Error( 85 | `Failed to authenticate with Dynamics 365: ${ 86 | error instanceof Error ? error.message : String(error) 87 | }` 88 | ); 89 | } 90 | return this.accessToken; 91 | } 92 | 93 | /** 94 | * Makes an API request to Dynamics 365 95 | * @param endpoint - The API endpoint (relative to the base URL) 96 | * @param method - The HTTP method (e.g., "GET", "POST") 97 | * @param body - The request body (optional, for POST/PUT requests) 98 | * @param additionalHeaders - Additional headers to include in the request 99 | * @returns Promise resolving to the API response 100 | */ 101 | private async makeApiRequest( 102 | endpoint: string, 103 | method: string, 104 | body?: any, 105 | additionalHeaders?: Record<string, string> 106 | ): Promise<any> { 107 | const token = await this.authenticate(); 108 | const baseUrl = this.d365Url.endsWith("/") 109 | ? this.d365Url 110 | : `${this.d365Url}/`; 111 | const url = `${baseUrl}${endpoint}`; 112 | 113 | const headers: Record<string, string> = { 114 | Authorization: `Bearer ${token}`, 115 | Accept: "application/json", 116 | "Content-Type": "application/json", 117 | "OData-MaxVersion": "4.0", 118 | "OData-Version": "4.0", 119 | ...additionalHeaders, 120 | }; 121 | 122 | try { 123 | const response = await fetch(url, { 124 | method, 125 | headers, 126 | body: body ? JSON.stringify(body) : undefined, 127 | }); 128 | 129 | if (!response.ok) { 130 | throw new Error( 131 | `API request failed with status: ${ 132 | response.status 133 | }, message: ${await response.text()}` 134 | ); 135 | } 136 | 137 | return await response.json(); 138 | } catch (error) { 139 | console.error(`API request to ${url} failed:`, error); 140 | throw new Error( 141 | `Failed to make API request: ${ 142 | error instanceof Error ? error.message : String(error) 143 | }` 144 | ); 145 | } 146 | } 147 | 148 | /** 149 | * Makes a WhoAmI request to Dynamics 365 to get information about the currently logged-in user 150 | * @returns Promise resolving to the user's information 151 | */ 152 | public async makeWhoAmIRequest(): Promise<{ 153 | BusinessUnitId: string; 154 | UserId: string; 155 | OrganizationId: string; 156 | UserName?: string; 157 | FullName?: string; 158 | }> { 159 | const data = await this.makeApiRequest("api/data/v9.2/WhoAmI", "GET"); 160 | 161 | // If we want to get more details about the user, we can make an additional request 162 | if (data && data.UserId) { 163 | const userDetails = await this.makeApiRequest( 164 | `api/data/v9.2/systemusers(${data.UserId})`, 165 | "GET", 166 | undefined, 167 | { Prefer: 'odata.include-annotations="*"' } 168 | ); 169 | data.UserName = userDetails.domainname; 170 | data.FullName = userDetails.fullname; 171 | } 172 | 173 | return data; 174 | } 175 | 176 | /** 177 | * Fetches accounts from Dynamics 365 178 | * @returns Promise resolving to the list of accounts 179 | */ 180 | public async getAccounts(): Promise<any> { 181 | return this.makeApiRequest("api/data/v9.2/accounts", "GET"); 182 | } 183 | 184 | /** 185 | * Fetches contacts for a given account from Dynamics 365 186 | * @param accountId - The ID of the account for which to retrieve contacts 187 | * @returns Promise resolving to the list of contacts 188 | */ 189 | public async getAssociatedContacts(accountId: string): Promise<any> { 190 | if (!accountId) { 191 | throw new Error("Account ID is required to fetch contacts."); 192 | } 193 | 194 | const endpoint = `api/data/v9.2/contacts?$filter=_parentcustomerid_value eq ${accountId}`; 195 | return this.makeApiRequest(endpoint, "GET"); 196 | } 197 | /** 198 | * Fetches opportunities for a given account from Dynamics 365 199 | * @param accountId - The ID of the account for which to retrieve opportunities 200 | * @returns Promise resolving to the list of opportunities 201 | */ 202 | public async getAssociatedOpportunities(accountId: string): Promise<any> { 203 | if (!accountId) { 204 | throw new Error("Account ID is required to fetch opportunities."); 205 | } 206 | 207 | const endpoint = `api/data/v9.2/opportunities?$filter=_customerid_value eq ${accountId}`; 208 | return this.makeApiRequest(endpoint, "GET"); 209 | } 210 | /* create a new account in Dynamics 365 211 | * @param accountData - The data for the new account 212 | * @returns Promise resolving to the created account 213 | */ 214 | public async createAccount(accountData: any): Promise<any> { 215 | if (!accountData) { 216 | throw new Error("Account data is required to create an account."); 217 | } 218 | 219 | const endpoint = "api/data/v9.2/accounts"; 220 | return this.makeApiRequest(endpoint, "POST", accountData); 221 | } 222 | 223 | /** 224 | * Updates an existing account in Dynamics 365 225 | * @param accountId - The ID of the account to update 226 | * @param accountData - The updated data for the account 227 | * @returns Promise resolving to the updated account 228 | */ 229 | public async updateAccount( 230 | accountId: string, 231 | accountData: any 232 | ): Promise<any> { 233 | if (!accountId) { 234 | throw new Error("Account ID is required to update an account."); 235 | } 236 | 237 | if (!accountData) { 238 | throw new Error("Account data is required to update an account."); 239 | } 240 | 241 | const endpoint = `api/data/v9.2/accounts(${accountId})`; 242 | return this.makeApiRequest(endpoint, "PATCH", accountData); 243 | } 244 | } 245 | ```