#
tokens: 4675/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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 🚀

![Node.js](https://img.shields.io/badge/Node.js-v16%2B-green)
![TypeScript](https://img.shields.io/badge/TypeScript-4.x-blue)
![MCP](https://img.shields.io/badge/MCP-orange)
![License](https://img.shields.io/badge/License-MIT-yellow)

## 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
![ Claude Server Tools](/images/Claude_Server_Tools_Setup.png)

- Let's test a prompt by invoking tool - get-user-info
![ Get User Tool Test](/images/get_user_tool_test.png)

### 6. (Optional) Test tools using MCP Interceptor
- Run following command in terminal
```json
npx @modelcontextprotocol/inspector node build/index.js
```
![ Interceptor commange](/images/Interceptor.png)

- Go to 🔍  http://localhost:5173 🚀
![ Interceptor](/images/Inspector.png)

- 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);
  }
}

```