{
  "markdown": "# D365 Finance & Operations MCP Server\n\nAn MCP (Model Context Protocol) server that provides access to Microsoft Dynamics 365 Finance & Operations environments. Enables AI assistants like Claude to explore D365 metadata, query data, and perform write operations on non-production environments.\n\n## Features\n\n- **Multi-Environment Support** - Connect to multiple D365 environments (production, UAT, dev)\n- **Read/Write Operations** - Query data on all environments; create, update, delete on non-production only\n- **Production Safety** - Production environments are always read-only by design\n- **MCP Resources** for schema discovery and metadata exploration\n- **22 Specialized Tools** for flexible data access, aggregation, batch operations, and analysis\n- **Environment Dashboard** - Health monitoring, API statistics, and operation tracking\n- **Secure Authentication** via Azure AD client credentials\n- **Automatic Metadata Caching** (24-hour TTL, per-environment)\n\n## Architecture\n\n### Resources\n\n| Resource | URI | Purpose |\n|----------|-----|---------|\n| Entities List | `d365://entities?filter=<pattern>` | List all entities with optional wildcard filtering |\n| Entity Schema | `d365://entity/{entityName}` | Full schema for any entity (fields, keys, navigation properties) |\n| Navigation Properties | `d365://navigation/{entityName}` | Entity relationships and navigation properties |\n| Enum Definitions | `d365://enums` | All enum types with their values |\n| Saved Queries | `d365://queries` | List saved query templates |\n| Dashboard | `d365://dashboard` | JSON metrics for all environments (health, API stats, recent operations) |\n\n### Tools\n\nAll tools support an optional `environment` parameter to target specific D365 environments.\n\n| Tool | Purpose |\n|------|---------|\n| `list_environments` | List all configured D365 environments with connection status |\n| `set_environment` | Set the working environment for the current session |\n| `describe_entity` | Quick schema lookup for an entity |\n| `execute_odata` | Execute raw OData paths (queries, single records, counts) |\n| `aggregate` | Perform aggregations (SUM, AVG, COUNT, MIN, MAX, COUNTDISTINCT, percentiles) on entity data |\n| `get_related` | Follow entity relationships to retrieve related records |\n| `export` | Export query results to CSV, JSON, or TSV format |\n| `compare_periods` | YoY, QoQ, MoM period comparisons with change calculations |\n| `trending` | Time series analysis with growth rates and moving averages |\n| `save_query` | Save reusable query templates with parameter support |\n| `execute_saved_query` | Execute saved query templates with parameter substitution |\n| `delete_saved_query` | Delete saved query templates |\n| `join_entities` | Cross-entity joins using $expand or client-side join |\n| `batch_query` | Execute multiple queries in parallel |\n| `search_entity` | Robust entity search with automatic fallback strategies |\n| `analyze_customer` | Comprehensive single-call customer analysis |\n| `create_record` | Create new records (non-production environments only) |\n| `update_record` | Update existing records (non-production environments only) |\n| `delete_record` | Delete records (non-production environments only) |\n| `batch_crud` | Execute multiple create/update/delete operations in a single batch request (non-production only) |\n| `compare_schemas` | Compare entity schemas between two environments to detect schema drift |\n| `dashboard` | Display environment dashboard with health status, API statistics, and recent operations |\n\n## Installation\n\n### From npm (Recommended)\n\n```bash\nnpx @zhound/d365fo-mcp-server\n```\n\nOr install globally:\n\n```bash\nnpm install -g @zhound/d365fo-mcp-server\nd365fo-mcp\n```\n\n### From Source\n\n```bash\ngit clone https://github.com/zhound420/D365FO-claude-connector.git\ncd D365FO-claude-connector\nnpm install\nnpm run build\n```\n\n## Quick Start (Recommended)\n\nRun the interactive setup wizard:\n\n```bash\nnpm run setup\n```\n\nThe wizard will:\n1. Check prerequisites (Node.js 18+, dependencies)\n2. Guide you through D365 environment configuration\n3. Test connectivity to your D365 environments\n4. Generate configuration files\n5. Configure Claude Desktop and/or Claude Code\n\nAfter setup, restart Claude Desktop (Cmd+Q then reopen on macOS, or Ctrl+Q on Windows) or start a new Claude Code session.\n\n## Configuration\n\n### Multi-Environment Configuration (Recommended)\n\nCreate a `d365-environments.json` file in the project root or working directory:\n\n```json\n{\n  \"environments\": [\n    {\n      \"name\": \"production\",\n      \"displayName\": \"Production\",\n      \"type\": \"production\",\n      \"tenantId\": \"your-tenant-id\",\n      \"clientId\": \"your-client-id\",\n      \"clientSecret\": \"your-client-secret\",\n      \"environmentUrl\": \"https://your-company.operations.dynamics.com\",\n      \"default\": true\n    },\n    {\n      \"name\": \"uat\",\n      \"displayName\": \"UAT (Tier 2)\",\n      \"type\": \"non-production\",\n      \"tenantId\": \"your-tenant-id\",\n      \"clientId\": \"your-client-id\",\n      \"clientSecret\": \"your-client-secret\",\n      \"environmentUrl\": \"https://your-company-uat.sandbox.operations.dynamics.com\"\n    },\n    {\n      \"name\": \"dev\",\n      \"displayName\": \"Dev Sandbox\",\n      \"type\": \"non-production\",\n      \"tenantId\": \"your-tenant-id\",\n      \"clientId\": \"your-client-id\",\n      \"clientSecret\": \"your-client-secret\",\n      \"environmentUrl\": \"https://your-company-dev.sandbox.operations.dynamics.com\"\n    }\n  ]\n}\n```\n\n**Environment Types:**\n- `type: \"production\"` - Read-only access (all write operations are blocked)\n- `type: \"non-production\"` - Full read/write access (create, update, delete enabled)\n\nCopy `d365-environments.example.json` as a starting point.\n\n### Single Environment (Legacy)\n\nThe server also supports the following environment variables (fallback if no JSON config):\n\n| Variable | Description |\n|----------|-------------|\n| `D365_TENANT_ID` | Azure AD tenant ID |\n| `D365_CLIENT_ID` | Azure AD application (client) ID |\n| `D365_CLIENT_SECRET` | Azure AD client secret |\n| `D365_ENVIRONMENT_URL` | D365 F&O environment URL (e.g., `https://contoso.operations.dynamics.com`) |\n| `D365_ENVIRONMENT_TYPE` | Optional: \"production\" or \"non-production\" (defaults to \"production\" for safety) |\n\nOptional:\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `D365_TRANSPORT` | `stdio` | Transport mode (`stdio` or `http`) |\n| `D365_HTTP_PORT` | `3000` | HTTP port (when using http transport) |\n| `D365_LOG_LEVEL` | `info` | Logging level |\n| `D365_PAGINATION_TIMEOUT_MS` | `60000` | Timeout (ms) for paginated requests on large datasets |\n| `D365_CONFIG_FILE` | | Path to config file if not in default location |\n\n### Azure AD App Registration\n\n#### Step 1: Create Azure AD App\n\n1. Go to [Azure Portal](https://portal.azure.com) > Azure Active Directory > App registrations\n2. Click \"New registration\"\n3. Name it (e.g., \"D365 MCP Server\")\n4. Select \"Accounts in this organizational directory only\"\n5. Click Register\n\n#### Step 2: Configure API Permissions\n\n1. Go to \"API permissions\" > \"Add a permission\"\n2. Select \"Dynamics 365 Finance and Operations\"\n3. Choose \"Application permissions\" > `CustomService.ReadWrite.All`\n4. Click \"Grant admin consent for [your organization]\"\n\n#### Step 3: Create Client Secret\n\n1. Go to \"Certificates & secrets\" > \"New client secret\"\n2. Add a description and expiry period\n3. Copy the secret value immediately (shown only once)\n4. Note down:\n   - **Tenant ID**: Found on the Overview page\n   - **Client ID**: Application (client) ID on Overview page\n   - **Client Secret**: The value you just copied\n\n#### Step 4: Register App in D365 Environments\n\n**Important:** This step must be done in each D365 environment (Production, UAT, Dev) you want to connect to.\n\n1. In D365 F&O, navigate to:\n   **System Administration > Setup > Azure Active Directory applications**\n\n2. Click \"New\" to add a record:\n   | Field | Value |\n   |-------|-------|\n   | Client ID | The Application (client) ID from Azure AD |\n   | Name | Descriptive name (e.g., \"MCP Server Integration\") |\n   | User ID | A D365 user account for the app to run as |\n\n3. The **User ID** determines what data the app can access:\n   - Use a service account with appropriate security roles\n   - For read-only access: assign roles like \"View all data\"\n   - For write access on non-production: assign roles that allow create/update/delete\n\n4. Repeat for each environment you want to connect to\n\n> **Note:** If you skip this step, API calls will fail with 401 Unauthorized or 403 Forbidden errors even though Azure AD authentication succeeded.\n\n## Setup\n\n### Claude Desktop\n\nAdd to your Claude Desktop config file:\n\n**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`\n**Windows**: `%APPDATA%\\Claude\\claude_desktop_config.json`\n\n```json\n{\n  \"mcpServers\": {\n    \"Microsoft D365\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/d365fo-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"D365_TENANT_ID\": \"your-tenant-id\",\n        \"D365_CLIENT_ID\": \"your-client-id\",\n        \"D365_CLIENT_SECRET\": \"your-client-secret\",\n        \"D365_ENVIRONMENT_URL\": \"https://your-env.operations.dynamics.com\"\n      }\n    }\n  }\n}\n```\n\n### Claude Code (CLI)\n\nAdd to `~/.claude/settings.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"Microsoft D365\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/d365fo-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"D365_TENANT_ID\": \"your-tenant-id\",\n        \"D365_CLIENT_ID\": \"your-client-id\",\n        \"D365_CLIENT_SECRET\": \"your-client-secret\",\n        \"D365_ENVIRONMENT_URL\": \"https://your-env.operations.dynamics.com\"\n      }\n    }\n  }\n}\n```\n\nAfter adding the configuration, restart Claude Desktop or Claude Code.\n\n### Environment Visibility Configuration\n\nWhen using multiple D365 environments, you can configure how they appear in Claude:\n\n#### Option A: Separate Servers per Environment (Recommended)\n\nThis option shows each environment as a separate MCP server in Claude's sidebar:\n\n```json\n{\n  \"mcpServers\": {\n    \"D365-production\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/d365fo-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"D365_CONFIG_FILE\": \"/path/to/d365fo-mcp-server/d365-environments.json\",\n        \"D365_SINGLE_ENV\": \"production\"\n      }\n    },\n    \"D365-uat\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/d365fo-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"D365_CONFIG_FILE\": \"/path/to/d365fo-mcp-server/d365-environments.json\",\n        \"D365_SINGLE_ENV\": \"uat\"\n      }\n    },\n    \"D365-dev\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/d365fo-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"D365_CONFIG_FILE\": \"/path/to/d365fo-mcp-server/d365-environments.json\",\n        \"D365_SINGLE_ENV\": \"dev\"\n      }\n    }\n  }\n}\n```\n\n**Pros:**\n- Environment is immediately visible in Claude's sidebar\n- No ambiguity about which environment a query targets\n- Works reliably across all platforms\n\n**How it works:** The `D365_SINGLE_ENV` environment variable tells the server to load only that specific environment from `d365-environments.json`. The `D365_CONFIG_FILE` ensures the config is found regardless of working directory.\n\n#### Option B: Single Multi-Environment Server\n\nUse a single server with an `environment` parameter on each query:\n\n```json\n{\n  \"mcpServers\": {\n    \"d365\": {\n      \"command\": \"node\",\n      \"args\": [\"/path/to/d365fo-mcp-server/dist/index.js\"],\n      \"env\": {\n        \"D365_CONFIG_FILE\": \"/path/to/d365fo-mcp-server/d365-environments.json\"\n      }\n    }\n  }\n}\n```\n\nThen specify the environment in queries:\n```json\n{ \"entity\": \"CustomersV3\", \"top\": 10, \"environment\": \"uat\" }\n```\n\n**Pros:**\n- Single server process\n- Flexibility to query any environment in one session\n\nThe interactive setup script (`node setup.js`) can generate either configuration for you.\n\n## Talking to Claude - Example Prompts\n\nOnce configured, you can ask Claude natural language questions about your D365 environment. Here are examples organized by capability:\n\n### Discovering Entities\n\n> **You:** What customer-related entities are available in D365?\n\nClaude will use the `d365://entities?filter=*Cust*` resource to find matching entities.\n\n> **You:** Show me the schema for the CustomersV3 entity\n\nClaude will use `describe_entity` or the `d365://entity/CustomersV3` resource.\n\n### Querying Data\n\n> **You:** Get me the first 10 customers with their account numbers and names\n\nClaude will use `execute_odata` with path `CustomersV3?$top=10&$select=CustomerAccount,CustomerName`\n\n> **You:** How many sales orders are in the system?\n\nClaude will use `execute_odata` with path `SalesOrderHeaders/$count`\n\n> **You:** Find all customers in customer group \"US\" with credit limit over 50000\n\nClaude will construct an OData filter query automatically.\n\n### Aggregation & Analytics\n\n> **You:** Who are our top 20 customers by total spend?\n\nClaude will use `aggregate` with groupBy, orderBy, and top:\n```json\n{\n  \"entity\": \"SalesOrderLinesV2\",\n  \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}],\n  \"groupBy\": [\"OrderingCustomerAccountNumber\"],\n  \"orderBy\": \"sum_LineAmount desc\",\n  \"top\": 20\n}\n```\n\n> **You:** What's the median order value? Show me the 90th and 95th percentiles too\n\nClaude will use `aggregate` with percentile functions:\n```json\n{\n  \"entity\": \"SalesOrderLinesV2\",\n  \"aggregations\": [\n    {\"function\": \"P50\", \"field\": \"LineAmount\", \"alias\": \"median\"},\n    {\"function\": \"P90\", \"field\": \"LineAmount\"},\n    {\"function\": \"P95\", \"field\": \"LineAmount\"}\n  ],\n  \"accurate\": true\n}\n```\n\n> **You:** Break down total revenue by product category\n\nClaude will use `aggregate` with groupBy:\n```json\n{\n  \"entity\": \"SalesOrderLinesV2\",\n  \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}],\n  \"groupBy\": [\"ItemGroup\"]\n}\n```\n\n### Time-Based Analysis\n\n> **You:** Show me the monthly sales trend for the past 12 months with growth rates\n\nClaude will use `trending`:\n```json\n{\n  \"entity\": \"SalesOrderLinesV2\",\n  \"dateField\": \"CreatedDateTime\",\n  \"valueField\": \"LineAmount\",\n  \"granularity\": \"month\",\n  \"periods\": 12,\n  \"includeGrowthRate\": true\n}\n```\n\n> **You:** Compare this year's sales to last year\n\nClaude will use `compare_periods` with YoY comparison:\n```json\n{\n  \"entity\": \"SalesOrderLinesV2\",\n  \"dateField\": \"CreatedDateTime\",\n  \"comparisonType\": \"YoY\",\n  \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}]\n}\n```\n\n> **You:** How did Q4 sales compare to Q3?\n\nClaude will use `compare_periods` with QoQ comparison:\n```json\n{\n  \"entity\": \"SalesOrderLinesV2\",\n  \"dateField\": \"CreatedDateTime\",\n  \"comparisonType\": \"QoQ\",\n  \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}]\n}\n```\n\n### Customer Intelligence\n\n> **You:** Give me a complete analysis of customer US-001 - profile, orders, spend, and trends\n\nClaude will use `analyze_customer` for comprehensive single-call analysis:\n```json\n{\n  \"customerAccount\": \"US-001\",\n  \"includeOrders\": true,\n  \"includeSpend\": true,\n  \"includeTrending\": true\n}\n```\n\n> **You:** Find the customer named \"S&S Industries\"\n\nClaude will use `search_entity` which handles special characters that break standard OData:\n```json\n{\n  \"entity\": \"CustomersV3\",\n  \"searchTerm\": \"S&S Industries\",\n  \"searchField\": \"CustomerName\"\n}\n```\n\n### Multi-Query & Joins\n\n> **You:** Get me a dashboard view: total customers, total orders this month, and top 5 products by sales\n\nClaude will use `batch_query` to run all three queries in parallel:\n```json\n{\n  \"queries\": [\n    {\"name\": \"total_customers\", \"entity\": \"CustomersV3\", \"top\": 1},\n    {\"name\": \"orders_this_month\", \"entity\": \"SalesOrderHeadersV2\", \"filter\": \"OrderCreatedDateTime ge 2024-01-01\"},\n    {\"name\": \"top_products\", \"entity\": \"SalesOrderLinesV2\", \"top\": 5, \"orderby\": \"LineAmount desc\"}\n  ]\n}\n```\n\n> **You:** Show me recent orders with customer names and their customer groups\n\nClaude will use `join_entities` to correlate orders with customer details:\n```json\n{\n  \"primaryEntity\": \"SalesOrderHeadersV2\",\n  \"primaryKey\": \"OrderingCustomerAccountNumber\",\n  \"secondaryEntity\": \"CustomersV3\",\n  \"secondaryKey\": \"CustomerAccount\",\n  \"primarySelect\": [\"SalesOrderNumber\", \"OrderCreatedDateTime\"],\n  \"secondarySelect\": [\"CustomerName\", \"CustomerGroup\"]\n}\n```\n\n### Data Export\n\n> **You:** Export all customers with credit limit over $100K to CSV\n\nClaude will use `export` with format and filter:\n```json\n{\n  \"entity\": \"CustomersV3\",\n  \"format\": \"csv\",\n  \"filter\": \"CreditLimit gt 100000\",\n  \"select\": [\"CustomerAccount\", \"CustomerName\", \"CreditLimit\"]\n}\n```\n\n### Understanding Enums\n\n> **You:** What are the possible values for sales order status?\n\nClaude will check the `d365://enums` resource to find enum definitions.\n\n## Tips for Best Results\n\n1. **Ask business questions directly** - The MCP tools handle complexity for you. Just ask: \"Who are our top 20 customers by spend?\" or \"How did Q4 compare to Q3?\"\n\n2. **Use natural date formats** - Claude understands \"last month\", \"Q4 2024\", \"past 12 months\", or specific dates like \"January 1, 2024\"\n\n3. **Don't worry about special characters** - Searching for \"S&S Industries\" or \"O'Brien Corp\" works automatically. The tools have fallback strategies for characters that break standard OData.\n\n4. **Request trends and comparisons** - Built-in time intelligence handles the complexity: \"Show monthly sales trend with growth rates\" or \"Compare this year's revenue to last year\"\n\n5. **Combine multiple questions** - Ask for dashboard-style views: \"Get me total customers, orders this month, and top 5 products\" - queries run in parallel.\n\n6. **Export data when needed** - Request CSV, JSON, or TSV exports directly: \"Export all customers with credit limit over $100K to CSV\"\n\n7. **Ask for explanations** - If you want to learn OData syntax, ask Claude to explain the query: \"Show customers in group US and explain the OData query\"\n\n## API Reference\n\n### Resources\n\n#### `d365://entities`\n\nList available D365 entities with optional filtering.\n\n**Query Parameters:**\n- `filter` (optional): Wildcard pattern (`*` for any chars, `?` for single char)\n\n**Examples:**\n```\nd365://entities                    # List all entities\nd365://entities?filter=Cust*       # Entities starting with \"Cust\"\nd365://entities?filter=*Header*    # Entities containing \"Header\"\n```\n\n#### `d365://entity/{entityName}`\n\nGet the full schema for an entity.\n\n**Examples:**\n```\nd365://entity/CustomersV3\nd365://entity/SalesOrderHeaders\n```\n\n**Response includes:**\n- Entity name and description\n- Primary key fields\n- All fields with types, constraints, and enum references\n- Navigation properties (relationships)\n\n#### `d365://navigation/{entityName}`\n\nGet navigation properties (relationships) for an entity.\n\n**Examples:**\n```\nd365://navigation/SalesOrderHeadersV2\nd365://navigation/CustomersV3\n```\n\n**Response includes:**\n- Navigation property names\n- Target entity types\n- Relationship cardinality (one-to-many, many-to-one)\n\n#### `d365://enums`\n\nList all enum type definitions.\n\n**Response includes:**\n- Enum name and full namespace\n- All member values with their numeric codes\n\n### Tools\n\n#### `list_environments`\n\nList all configured D365 environments with their connection status and permissions.\n\n**Parameters:**\n- None required\n\n**Example:**\n```json\n{}\n```\n\n**Response includes:**\n- Environment name and display name\n- Type (production/non-production)\n- Connection status\n- Read/write permissions\n\n#### `set_environment`\n\nSet the working environment for the current session. Subsequent tool calls will use this environment by default.\n\n**Parameters:**\n- `environment` (string, required): Name of the environment to set as active\n\n**Example:**\n```json\n{\n  \"environment\": \"uat\"\n}\n```\n\n#### `describe_entity`\n\nGet entity schema in a human-readable format.\n\n**Parameters:**\n- `entity` (string, required): Entity name\n\n**Example:**\n```json\n{\n  \"entity\": \"CustomersV3\"\n}\n```\n\n#### `execute_odata`\n\nExecute a raw OData path against D365.\n\n**Parameters:**\n- `path` (string, required): OData path appended to `/data/`\n\n**Examples:**\n```json\n// Query with parameters\n{ \"path\": \"CustomersV3?$top=5&$select=CustomerAccount,CustomerName\" }\n\n// Single record by key\n{ \"path\": \"CustomersV3('US-001')\" }\n\n// Compound key\n{ \"path\": \"CustomersV3(DataAreaId='usmf',CustomerAccount='US-001')\" }\n\n// Count\n{ \"path\": \"CustomersV3/$count\" }\n\n// Filtered count\n{ \"path\": \"CustomersV3/$count?$filter=CustomerGroup eq 'US'\" }\n\n// With expansion\n{ \"path\": \"SalesOrderHeaders?$expand=SalesOrderLines&$top=3\" }\n```\n\n#### `aggregate`\n\nPerform aggregations on D365 entity data. Uses fast `/$count` for simple COUNT operations, client-side aggregation otherwise.\n\n**Parameters:**\n- `entity` (string, required): Entity name to aggregate\n- `aggregations` (array, required): Array of aggregation specs:\n  - `function`: \"SUM\" | \"AVG\" | \"COUNT\" | \"MIN\" | \"MAX\" | \"COUNTDISTINCT\" | \"P50\" | \"P90\" | \"P95\" | \"P99\"\n  - `field`: Field to aggregate (use \"*\" for COUNT)\n  - `alias` (optional): Custom result name\n- `filter` (string, optional): OData $filter expression\n- `groupBy` (array, optional): Fields to group by\n- `accurate` (boolean, optional): Fetch ALL records for exact totals (default: false)\n- `sampling` (boolean, optional): Use statistical sampling for fast estimates on very large datasets (default: false)\n- `orderBy` (string, optional): Sort results by aggregation alias (e.g., \"sum_LineAmount desc\")\n- `top` (number, optional): Return only top N results after sorting\n\n**Percentile functions:**\n- `P50` - Median (50th percentile)\n- `P90` - 90th percentile\n- `P95` - 95th percentile\n- `P99` - 99th percentile\n\n**Performance notes:**\n- Default mode caps at 5K records for quick estimates\n- `accurate=true` fetches ALL records with 60s timeout per page and automatic retry (2 retries with exponential backoff)\n- `sampling=true` uses ~10K record sample for statistical estimates on very large datasets (100K+ records)\n\n**Examples:**\n```json\n// Count all customers\n{ \"entity\": \"CustomersV3\", \"aggregations\": [{\"function\": \"COUNT\", \"field\": \"*\"}] }\n\n// Sum with filter\n{ \"entity\": \"SalesOrderLines\", \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}], \"filter\": \"SalesOrderNumber eq 'SO-001'\" }\n\n// Accurate mode for exact totals\n{ \"entity\": \"SalesOrderLines\", \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}], \"accurate\": true }\n\n// Group by\n{ \"entity\": \"SalesOrderLines\", \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}], \"groupBy\": [\"ItemNumber\"] }\n\n// Median order value (requires accurate=true for percentiles)\n{ \"entity\": \"SalesOrderLines\", \"aggregations\": [{\"function\": \"P50\", \"field\": \"LineAmount\"}], \"accurate\": true }\n\n// Fast estimate on very large dataset (100K+ records)\n{ \"entity\": \"BatchJobs\", \"aggregations\": [{\"function\": \"COUNT\", \"field\": \"*\"}], \"sampling\": true }\n\n// Top 20 customers by spend\n{ \"entity\": \"SalesOrderLines\", \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}], \"groupBy\": [\"CustomerAccount\"], \"orderBy\": \"sum_LineAmount desc\", \"top\": 20 }\n```\n\n#### `get_related`\n\nFollow entity relationships to retrieve related records in a single call.\n\n**Parameters:**\n- `entity` (string, required): Source entity name\n- `key` (string | object, required): Primary key of source record\n- `relationship` (string, required): Navigation property name to follow\n- `select` (string[], optional): Fields to include from related entity\n- `filter` (string, optional): Filter to apply to related records\n- `top` (number, optional): Maximum related records (default: 1000)\n\n**Examples:**\n```json\n// Get order lines for an order\n{ \"entity\": \"SalesOrderHeaders\", \"key\": \"SO-001\", \"relationship\": \"SalesOrderLines\" }\n\n// With compound key\n{ \"entity\": \"SalesOrderHeaders\", \"key\": {\"DataAreaId\": \"usmf\", \"SalesOrderNumber\": \"SO-001\"}, \"relationship\": \"SalesOrderLines\" }\n\n// With field selection and filter\n{ \"entity\": \"SalesOrderHeaders\", \"key\": \"SO-001\", \"relationship\": \"SalesOrderLines\", \"select\": [\"ItemNumber\", \"LineAmount\"], \"filter\": \"LineAmount gt 1000\" }\n```\n\n#### `export`\n\nExport D365 entity data to CSV, JSON, or TSV format.\n\n**Parameters:**\n- `entity` (string, required): Entity to export\n- `format` (\"json\" | \"csv\" | \"tsv\", optional): Output format (default: \"json\")\n- `select` (string[], optional): Fields to include\n- `filter` (string, optional): OData $filter expression\n- `orderBy` (string, optional): OData $orderby expression\n- `maxRecords` (number, optional): Maximum records (default: 10000)\n- `includeHeaders` (boolean, optional): Include header row for CSV/TSV (default: true)\n\n**Examples:**\n```json\n// JSON export with field selection\n{ \"entity\": \"CustomersV3\", \"format\": \"json\", \"select\": [\"CustomerAccount\", \"CustomerName\"] }\n\n// CSV export with filter\n{ \"entity\": \"SalesOrderLines\", \"format\": \"csv\", \"filter\": \"SalesOrderNumber eq 'SO-001'\" }\n\n// TSV with ordering and limit\n{ \"entity\": \"Products\", \"format\": \"tsv\", \"orderBy\": \"ProductName asc\", \"maxRecords\": 500 }\n```\n\n#### `compare_periods`\n\nCompare aggregations between two time periods (YoY, QoQ, MoM, or custom ranges).\n\n**Parameters:**\n- `entity` (string, required): Entity to analyze\n- `dateField` (string, required): Date/datetime field for filtering\n- `aggregations` (array, required): Same as aggregate tool\n- `comparisonType` (\"YoY\" | \"QoQ\" | \"MoM\" | \"custom\", required): Type of comparison\n- `referenceDate` (string, optional): Reference date for calculations (default: today)\n- `period1`, `period2` (objects, optional): Custom period ranges\n- `filter` (string, optional): Additional OData filter\n- `groupBy` (string[], optional): Fields to group by\n\n**Examples:**\n```json\n// Year-over-Year comparison\n{ \"entity\": \"SalesOrderLines\", \"dateField\": \"CreatedDateTime\", \"comparisonType\": \"YoY\", \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}] }\n\n// Month-over-Month with grouping\n{ \"entity\": \"SalesOrderLines\", \"dateField\": \"CreatedDateTime\", \"comparisonType\": \"MoM\", \"aggregations\": [{\"function\": \"COUNT\", \"field\": \"*\"}], \"groupBy\": [\"ItemGroup\"] }\n\n// Custom date ranges\n{ \"entity\": \"SalesOrderLines\", \"dateField\": \"CreatedDateTime\", \"comparisonType\": \"custom\", \"aggregations\": [{\"function\": \"SUM\", \"field\": \"LineAmount\"}], \"period1\": {\"start\": \"2024-01-01\", \"end\": \"2024-03-31\"}, \"period2\": {\"start\": \"2023-01-01\", \"end\": \"2023-03-31\"} }\n```\n\n#### `trending`\n\nTime series analysis with aggregation, growth rates, and moving averages.\n\n**Parameters:**\n- `entity` (string, required): Entity to analyze\n- `dateField` (string, required): Date/datetime field for bucketing\n- `valueField` (string, required): Numeric field to aggregate\n- `aggregation` (\"SUM\" | \"AVG\" | \"COUNT\" | \"MIN\" | \"MAX\", optional): Default: \"SUM\"\n- `granularity` (\"day\" | \"week\" | \"month\" | \"quarter\" | \"year\", optional): Default: \"month\"\n- `periods` (number, optional): Number of periods to analyze (default: 12)\n- `endDate` (string, optional): End date for analysis (default: today)\n- `filter` (string, optional): Additional OData filter\n- `movingAverageWindow` (number, optional): Window size for MA calculation\n- `includeGrowthRate` (boolean, optional): Include growth rates (default: true)\n\n**Examples:**\n```json\n// Monthly revenue trend\n{ \"entity\": \"SalesOrderLines\", \"dateField\": \"CreatedDateTime\", \"valueField\": \"LineAmount\", \"granularity\": \"month\", \"periods\": 12 }\n\n// Weekly order count with moving average\n{ \"entity\": \"SalesOrderHeaders\", \"dateField\": \"OrderDate\", \"valueField\": \"*\", \"aggregation\": \"COUNT\", \"granularity\": \"week\", \"movingAverageWindow\": 4 }\n\n// Quarterly with filter\n{ \"entity\": \"SalesOrderLines\", \"dateField\": \"CreatedDateTime\", \"valueField\": \"LineAmount\", \"granularity\": \"quarter\", \"filter\": \"ItemGroup eq 'Electronics'\" }\n```\n\n#### `save_query`\n\nSave a reusable query template for later execution. Use `{{paramName}}` for substitutable parameters.\n\n**Parameters:**\n- `name` (string, required): Unique name for the query\n- `description` (string, optional): Description of the query\n- `entity` (string, required): Entity to query\n- `select` (string[], optional): Fields to select\n- `filter` (string, optional): OData $filter (use `{{paramName}}` for parameters)\n- `orderBy` (string, optional): OData $orderby expression\n- `top` (number, optional): Maximum records\n- `expand` (string, optional): OData $expand expression\n\n**Examples:**\n```json\n// Basic query\n{ \"name\": \"active_customers\", \"entity\": \"CustomersV3\", \"filter\": \"IsActive eq true\" }\n\n// With parameters\n{ \"name\": \"customer_orders\", \"entity\": \"SalesOrderHeaders\", \"filter\": \"CustomerAccount eq '{{customerId}}'\" }\n\n// Complex query with description\n{ \"name\": \"recent_sales\", \"description\": \"Recent sales for analysis\", \"entity\": \"SalesOrderLines\", \"select\": [\"ItemNumber\", \"LineAmount\"], \"filter\": \"CreatedDateTime ge {{startDate}}\", \"orderBy\": \"CreatedDateTime desc\", \"top\": 100 }\n```\n\n#### `execute_saved_query`\n\nExecute a previously saved query template.\n\n**Parameters:**\n- `name` (string, required): Name of the saved query\n- `params` (object, optional): Parameter values to substitute\n- `fetchAll` (boolean, optional): Fetch all pages (default: false)\n- `maxRecords` (number, optional): Max records when fetchAll=true (default: 50000)\n\n**Examples:**\n```json\n// Simple execution\n{ \"name\": \"active_customers\" }\n\n// With parameters\n{ \"name\": \"customer_orders\", \"params\": {\"customerId\": \"US-001\"} }\n\n// Multiple parameters with pagination\n{ \"name\": \"date_range_sales\", \"params\": {\"startDate\": \"2024-01-01\", \"endDate\": \"2024-12-31\"}, \"fetchAll\": true }\n```\n\n#### `delete_saved_query`\n\nDelete a saved query template.\n\n**Parameters:**\n- `name` (string, required): Name of the query to delete\n\n#### `join_entities`\n\nCross-entity joins using OData $expand or client-side join.\n\n**Parameters:**\n- `primaryEntity` (string, required): Primary entity name\n- `primaryKey` (string, required): Primary key field to join on\n- `secondaryEntity` (string, required): Secondary entity name\n- `secondaryKey` (string, required): Secondary key field to join on\n- `primarySelect` (string[], optional): Fields from primary entity\n- `secondarySelect` (string[], optional): Fields from secondary entity\n- `primaryFilter` (string, optional): Filter for primary entity\n- `joinType` (\"inner\" | \"left\", optional): Join type (default: \"inner\")\n- `maxRecords` (number, optional): Maximum records (default: 5000)\n\n**Examples:**\n```json\n// Join orders with customers\n{ \"primaryEntity\": \"SalesOrderHeadersV2\", \"primaryKey\": \"OrderingCustomerAccountNumber\", \"secondaryEntity\": \"CustomersV3\", \"secondaryKey\": \"CustomerAccount\", \"primarySelect\": [\"SalesOrderNumber\", \"OrderCreatedDateTime\"], \"secondarySelect\": [\"CustomerName\", \"CustomerGroup\"] }\n```\n\n#### `batch_query`\n\nExecute multiple D365 OData queries in parallel, returning all results in a single response.\n\n**Parameters:**\n- `queries` (array, required): Array of query specs (1-10 queries):\n  - `name` (string, optional): Label for this query result\n  - `entity` (string, required): Entity name\n  - `filter` (string, optional): OData $filter expression\n  - `select` (string[], optional): Fields to include\n  - `top` (number, optional): Limit records (default: 100)\n  - `orderby` (string, optional): OData $orderby expression\n  - `fetchAll` (boolean, optional): Auto-paginate all pages\n  - `maxRecords` (number, optional): Max records when fetchAll=true\n- `stopOnError` (boolean, optional): Stop on first failure (default: false)\n\n**Examples:**\n```json\n// Multiple parallel queries\n{\n  \"queries\": [\n    { \"name\": \"recent_orders\", \"entity\": \"SalesOrderHeadersV2\", \"top\": 10, \"orderby\": \"CreatedDateTime desc\" },\n    { \"name\": \"customers\", \"entity\": \"CustomersV3\", \"filter\": \"CustomerGroup eq 'US'\", \"select\": [\"CustomerAccount\", \"CustomerName\"] },\n    { \"name\": \"all_invoices\", \"entity\": \"SalesInvoiceHeadersV2\", \"fetchAll\": true, \"maxRecords\": 1000 }\n  ]\n}\n```\n\n#### `search_entity`\n\nRobust entity search with automatic fallback strategies. Handles special characters (like `&` in company names) that cause issues with standard OData `contains()`.\n\n**Search Strategies (tried in order):**\n1. `contains()` - Standard OData text search (fastest)\n2. `startswith()` - Prefix matching (more reliable on D365)\n3. `exact` - Exact field match\n4. `client_filter` - Fetch + client-side filter (always works)\n\n**Parameters:**\n- `entity` (string, required): Entity to search\n- `searchTerm` (string, required): Text to search for\n- `searchField` (string, required): Field to search in\n- `select` (string[], optional): Fields to return in results\n- `top` (number, optional): Maximum results (default: 10)\n\n**Examples:**\n```json\n// Search customers with special characters\n{ \"entity\": \"CustomersV3\", \"searchTerm\": \"S&S\", \"searchField\": \"CustomerName\" }\n\n// Search with specific fields\n{ \"entity\": \"CustomersV3\", \"searchTerm\": \"Contoso\", \"searchField\": \"CustomerName\", \"select\": [\"CustomerAccount\", \"CustomerName\", \"CustomerGroup\"], \"top\": 5 }\n\n// Search vendors\n{ \"entity\": \"VendorsV3\", \"searchTerm\": \"Microsoft\", \"searchField\": \"VendorName\" }\n```\n\n#### `analyze_customer`\n\nComprehensive customer analysis in a single call. Runs parallel queries to gather profile, orders, spend, and trending data.\n\n**Features:**\n- Customer profile lookup (with fallback search strategies)\n- Order statistics (count, total spend, average order value)\n- Order date range (first and last order)\n- Recent orders list\n- Monthly order trending\n\nUses efficient aggregation at the line level (`SalesOrderLinesV2`) for accurate spend calculation, avoiding the $0 header total issue.\n\n**Parameters:**\n- `customerAccount` (string, optional): Customer account number\n- `customerName` (string, optional): Customer name to search (handles special characters)\n- `includeOrders` (boolean, optional): Include recent orders list (default: true)\n- `includeSpend` (boolean, optional): Include total spend calculation (default: true)\n- `includeTrending` (boolean, optional): Include monthly trend analysis (default: true)\n- `recentOrdersLimit` (number, optional): Number of recent orders to show (default: 10)\n- `trendPeriods` (number, optional): Number of months for trend (default: 12)\n\n**Examples:**\n```json\n// Analyze by account number\n{ \"customerAccount\": \"SS0011\" }\n\n// Analyze by name (handles special characters like &)\n{ \"customerName\": \"S&S\" }\n\n// Quick analysis without trending (faster)\n{ \"customerAccount\": \"US-001\", \"includeTrending\": false }\n\n// Full analysis with custom periods\n{ \"customerName\": \"Contoso\", \"recentOrdersLimit\": 20, \"trendPeriods\": 24 }\n```\n\n**Output includes:**\n- Customer profile (name, account, group, address)\n- Summary statistics (total orders, total spend, average order value, first/last order dates)\n- Recent orders list\n- Monthly order trend table with order counts and revenue\n\n#### `d365://queries`\n\nResource that lists all saved query templates.\n\n**Response includes:**\n- Query count and list\n- Each query's name, description, entity, and parameters\n- Usage instructions\n\n#### `dashboard`\n\nDisplay environment dashboard with health status, API statistics, and recent operations.\n\n**Parameters:**\n- `checkHealth` (boolean, optional): Perform live connectivity check (default: false)\n\n**Example:**\n```json\n{\n  \"checkHealth\": true\n}\n```\n\n**Response includes:**\n- Per-environment health status\n- API call statistics (total calls, success rate)\n- Recent operations log\n- Environment configuration summary\n\n## OData Query Syntax\n\n### Filter Examples\n\n```\n// Equality\n$filter=CustomerAccount eq 'US-001'\n\n// Comparison\n$filter=CreditLimit gt 10000\n\n// String functions\n$filter=startswith(CustomerName, 'Contoso')\n$filter=contains(CustomerName, 'Inc')\n\n// Logical operators\n$filter=CustomerGroup eq 'US' and CreditLimit gt 5000\n\n// Enum values\n$filter=Status eq Microsoft.Dynamics.DataEntities.SalesStatus'Invoiced'\n\n// Date comparison\n$filter=OrderDate gt 2024-01-01\n```\n\n### Select and Expand\n\n```\n// Select specific fields\n$select=CustomerAccount,CustomerName,CreditLimit\n\n// Expand navigation property\n$expand=SalesOrderLines\n\n// Expand with nested select\n$expand=SalesOrderLines($select=ItemId,Quantity)\n```\n\n### Ordering and Pagination\n\n```\n// Sort ascending\n$orderby=CustomerName asc\n\n// Sort descending\n$orderby=OrderDate desc\n\n// Multiple sort columns\n$orderby=CustomerGroup asc,CustomerName asc\n\n// Pagination\n$top=50&$skip=100\n```\n\n## Development\n\n```bash\n# Build\nnpm run build\n\n# Watch mode\nnpm run dev\n\n# Run directly (requires environment variables)\nnpm start\n\n# Run tests\nnpm test\n\n# Run tests in watch mode\nnpm run test:watch\n```\n\n## Project Structure\n\n```\nsrc/\n├── index.ts                # Entry point and server setup\n├── config-loader.ts        # Configuration loading (JSON + env var fallback)\n├── environment-manager.ts  # Multi-environment management and write guards\n├── auth.ts                 # Azure AD OAuth2 authentication\n├── d365-client.ts          # D365 OData API client with read/write methods\n├── metadata-cache.ts       # EDMX metadata parser and cache (24h TTL)\n├── progress.ts             # Progress reporting for long operations\n├── types.ts                # TypeScript type definitions\n├── metrics/\n│   ├── index.ts            # Metrics module exports\n│   ├── metrics-tracker.ts  # API call statistics tracking\n│   ├── health-checker.ts   # Environment connectivity health checks\n│   └── operation-log.ts    # Operation history tracking\n├── resources/\n│   ├── index.ts            # Resource registration\n│   ├── entities.ts         # d365://entities resource\n│   ├── entity.ts           # d365://entity/{name} resource\n│   ├── navigation.ts       # d365://navigation/{name} resource\n│   ├── enums.ts            # d365://enums resource\n│   ├── queries.ts          # d365://queries resource\n│   └── dashboard.ts        # d365://dashboard resource\n├── utils/\n│   ├── date-utils.ts       # Date period calculations\n│   ├── csv-utils.ts        # CSV/TSV formatting\n│   ├── env-utils.ts        # Environment variable parsing with validation\n│   └── pagination.ts       # Shared pagination utilities (fetchPageWithRetry, paginatedFetch)\n└── tools/\n    ├── index.ts            # Tool registration\n    ├── common.ts           # Shared tool utilities and error formatting\n    ├── list-environments.ts\n    ├── set-environment.ts\n    ├── describe-entity.ts\n    ├── execute-odata.ts\n    ├── aggregate.ts\n    ├── get-related.ts\n    ├── export.ts\n    ├── compare-periods.ts\n    ├── trending.ts\n    ├── saved-queries.ts    # save/execute/delete query templates\n    ├── join-entities.ts\n    ├── batch-query.ts\n    ├── batch-crud.ts       # Batch create/update/delete via $batch (non-production only)\n    ├── compare-schemas.ts  # Cross-environment schema comparison\n    ├── search-entity.ts\n    ├── analyze-customer.ts\n    ├── create-record.ts    # Write operation (non-production only)\n    ├── update-record.ts    # Write operation (non-production only)\n    ├── delete-record.ts    # Write operation (non-production only)\n    └── dashboard.ts\ntests/\n├── auth.test.ts            # Token caching, refresh dedup, invalidation\n├── d365-client.test.ts     # Retry logic, key formatting, CRUD operations\n├── config-loader.test.ts   # JSON loading, env var fallback, validation\n├── pagination.test.ts      # Shared pagination utilities\n└── env-utils.test.ts       # parseInt validation utility\n```\n\n## Write Operations (Non-Production Only)\n\nWrite operations are only available on environments with `type: \"non-production\"`. Production environments are always read-only.\n\n### `create_record`\n\nCreate a new record in a D365 entity.\n\n**Parameters:**\n- `entity` (string, required): Entity name\n- `data` (object, required): Field values for the new record\n- `environment` (string, optional): Target environment\n\n**Example:**\n```json\n{\n  \"entity\": \"CustomersV3\",\n  \"data\": {\n    \"CustomerAccount\": \"CUST-001\",\n    \"CustomerName\": \"Contoso Ltd\",\n    \"CustomerGroup\": \"US\"\n  },\n  \"environment\": \"uat\"\n}\n```\n\n### `update_record`\n\nUpdate an existing record.\n\n**Parameters:**\n- `entity` (string, required): Entity name\n- `key` (string | object, required): Record key\n- `data` (object, required): Field values to update\n- `etag` (string, optional): ETag for optimistic concurrency\n- `environment` (string, optional): Target environment\n\n**Example:**\n```json\n{\n  \"entity\": \"CustomersV3\",\n  \"key\": \"CUST-001\",\n  \"data\": {\n    \"CustomerName\": \"Contoso Corporation\"\n  },\n  \"environment\": \"dev\"\n}\n```\n\n### `delete_record`\n\nDelete a record from an entity.\n\n**Parameters:**\n- `entity` (string, required): Entity name\n- `key` (string | object, required): Record key\n- `etag` (string, optional): ETag for optimistic concurrency\n- `environment` (string, optional): Target environment\n\n**Example:**\n```json\n{\n  \"entity\": \"CustomersV3\",\n  \"key\": \"CUST-001\",\n  \"environment\": \"dev\"\n}\n```\n\n## Security\n\n### Credential Protection\n\nThis project implements multiple layers to protect your Azure AD credentials:\n\n| Protection | Description |\n|------------|-------------|\n| `.gitignore` | `.env` and `d365-environments.json` are excluded from version control |\n| `.gitattributes` | Sensitive files excluded from `git archive` exports |\n| Pre-commit hook | Scans staged files for secret patterns before allowing commits |\n| Sanitized errors | Azure AD error responses are logged internally but not exposed to callers |\n\n### Protected Files\n\nThe following files contain credentials and are protected:\n\n- `.env` - Environment variables (legacy single-environment config)\n- `d365-environments.json` - Multi-environment configuration with secrets\n- `*.local.json` - Local configuration overrides\n\n**Safe files** (contain placeholders, OK to commit):\n- `.env.example` - Template with placeholder values\n- `d365-environments.example.json` - Example configuration\n\n### If Credentials Are Exposed\n\nIf you accidentally commit or expose credentials:\n\n1. **Immediately rotate the Azure AD client secret:**\n   - Go to [Azure Portal](https://portal.azure.com) > Azure Active Directory > App registrations\n   - Select your D365 app registration\n   - Go to \"Certificates & secrets\"\n   - Create a new client secret\n   - Update your local `.env` or `d365-environments.json` with the new secret\n   - Delete the old secret from Azure AD\n\n2. **Review Azure AD sign-in logs:**\n   - Check for unauthorized access attempts\n   - Azure Portal > Azure AD > Sign-in logs > Filter by your app\n\n3. **If committed to git:**\n   - Even if you remove the secret in a new commit, it remains in git history\n   - Consider using `git filter-branch` or BFG Repo-Cleaner to purge history\n   - Force-push the cleaned repository (coordinate with collaborators)\n\n### Rotating Azure AD Secrets\n\nBest practice is to rotate secrets periodically (every 90-180 days):\n\n1. **Create new secret in Azure Portal** (before the old one expires)\n2. **Update your configuration files:**\n   ```bash\n   # Edit .env or d365-environments.json with new secret\n   ```\n3. **Test connectivity:**\n   ```bash\n   npm start  # Verify authentication works\n   ```\n4. **Delete old secret from Azure Portal**\n\n### Pre-commit Hook\n\nThe pre-commit hook scans for patterns like:\n- Azure AD client secrets (30+ character strings after `clientSecret`)\n- Tenant IDs (UUID format after `tenantId`)\n- Environment variable assignments with secrets\n\nTo bypass (for false positives only):\n```bash\ngit commit --no-verify\n```\n\n### Runtime Protections\n\n- **Production environments read-only**: Write operations are structurally blocked on production\n- **Non-production write access**: Create, update, delete only available on `type: \"non-production\"` environments\n- **No credential exposure**: Credentials are managed server-side\n- **OData injection prevention**: Parameters are properly encoded\n\n## Troubleshooting\n\n### MCP Servers Not Appearing\n\n1. **Restart Claude Desktop fully** - Cmd+Q on macOS (not just close window), then reopen. On Windows, use Ctrl+Q or exit from the system tray.\n\n2. **Check server configuration** - Verify the config file path is correct:\n   ```bash\n   D365_CONFIG_FILE=./d365-environments.json D365_SINGLE_ENV=uat node dist/index.js\n   ```\n\n3. **Verify config path** - Ensure `D365_CONFIG_FILE` in your Claude config points to the actual location of `d365-environments.json`.\n\n4. **Check Claude logs** - On macOS: `~/Library/Logs/Claude/`; on Windows: `%APPDATA%\\Claude\\logs\\`\n\n### Authentication Errors\n\n- Verify tenant ID, client ID, and secret are correct\n- Ensure the Azure AD app has the required API permissions\n- Check that admin consent has been granted\n\n### Entity Not Found\n\n- Use `d365://entities` to discover available entities\n- Entity names are case-sensitive\n- Some entities may not be exposed via OData\n\n### Timeout Errors\n\n- Reduce query scope with `$top` and `$filter`\n- For large datasets, use pagination with `$skip`\n- Use `batch_query` to run multiple queries in parallel\n\n**Large dataset aggregation improvements:**\n- Pagination requests now use 60s timeout with automatic retry (2 retries with exponential backoff)\n- Configure timeout via `D365_PAGINATION_TIMEOUT_MS` environment variable\n- For very large datasets (100K+ records), use `sampling=true` on the `aggregate` tool for fast statistical estimates\n- `accurate=true` mode now reports partial results if interrupted mid-pagination\n\n## License\n\nMIT\n",
  "bytes": 45108,
  "sha": "17d706feb42d07fea47ed16c2b4411aad52ad6acadf5e3955e4da9b95ac4f388",
  "repo_slug": "zhound420/d365fo-claude-connector",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_zhound420_d365fo_connector_ddcbb4a4/readme"
}