#
tokens: 6442/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ![Node.js](https://img.shields.io/badge/Node.js-v16%2B-green)
  4 | ![TypeScript](https://img.shields.io/badge/TypeScript-4.x-blue)
  5 | ![MCP](https://img.shields.io/badge/MCP-orange)
  6 | ![License](https://img.shields.io/badge/License-MIT-yellow)
  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 | ![ Claude Server Tools](/images/Claude_Server_Tools_Setup.png)
111 | 
112 | - Let's test a prompt by invoking tool - get-user-info
113 | ![ Get User Tool Test](/images/get_user_tool_test.png)
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 | ![ Interceptor commange](/images/Interceptor.png)
121 | 
122 | - Go to 🔍  http://localhost:5173 🚀
123 | ![ Interceptor](/images/Inspector.png)
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 | 
```