mcp-dataverse 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/CAPABILITIES.md +992 -0
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/config.example.json +11 -0
- package/dist/auth/auth-provider.factory.d.ts +4 -0
- package/dist/auth/auth-provider.factory.d.ts.map +1 -0
- package/dist/auth/auth-provider.factory.js +15 -0
- package/dist/auth/auth-provider.factory.js.map +1 -0
- package/dist/auth/auth-provider.interface.d.ts +21 -0
- package/dist/auth/auth-provider.interface.d.ts.map +1 -0
- package/dist/auth/auth-provider.interface.js +2 -0
- package/dist/auth/auth-provider.interface.js.map +1 -0
- package/dist/auth/msal-auth-provider.d.ts +14 -0
- package/dist/auth/msal-auth-provider.d.ts.map +1 -0
- package/dist/auth/msal-auth-provider.js +62 -0
- package/dist/auth/msal-auth-provider.js.map +1 -0
- package/dist/auth/pac-auth-provider.d.ts +19 -0
- package/dist/auth/pac-auth-provider.d.ts.map +1 -0
- package/dist/auth/pac-auth-provider.js +153 -0
- package/dist/auth/pac-auth-provider.js.map +1 -0
- package/dist/config/config.loader.d.ts +3 -0
- package/dist/config/config.loader.d.ts.map +1 -0
- package/dist/config/config.loader.js +52 -0
- package/dist/config/config.loader.js.map +1 -0
- package/dist/config/config.schema.d.ts +34 -0
- package/dist/config/config.schema.d.ts.map +1 -0
- package/dist/config/config.schema.js +25 -0
- package/dist/config/config.schema.js.map +1 -0
- package/dist/dataverse/dataverse-client-advanced.d.ts +47 -0
- package/dist/dataverse/dataverse-client-advanced.d.ts.map +1 -0
- package/dist/dataverse/dataverse-client-advanced.js +147 -0
- package/dist/dataverse/dataverse-client-advanced.js.map +1 -0
- package/dist/dataverse/dataverse-client.d.ts +49 -0
- package/dist/dataverse/dataverse-client.d.ts.map +1 -0
- package/dist/dataverse/dataverse-client.js +313 -0
- package/dist/dataverse/dataverse-client.js.map +1 -0
- package/dist/dataverse/dataverse-client.metadata.d.ts +40 -0
- package/dist/dataverse/dataverse-client.metadata.d.ts.map +1 -0
- package/dist/dataverse/dataverse-client.metadata.js +121 -0
- package/dist/dataverse/dataverse-client.metadata.js.map +1 -0
- package/dist/dataverse/dataverse-client.utils.d.ts +14 -0
- package/dist/dataverse/dataverse-client.utils.d.ts.map +1 -0
- package/dist/dataverse/dataverse-client.utils.js +65 -0
- package/dist/dataverse/dataverse-client.utils.js.map +1 -0
- package/dist/dataverse/http-client.d.ts +36 -0
- package/dist/dataverse/http-client.d.ts.map +1 -0
- package/dist/dataverse/http-client.js +103 -0
- package/dist/dataverse/http-client.js.map +1 -0
- package/dist/dataverse/types.d.ts +68 -0
- package/dist/dataverse/types.d.ts.map +1 -0
- package/dist/dataverse/types.js +2 -0
- package/dist/dataverse/types.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +165 -0
- package/dist/server.js.map +1 -0
- package/dist/setup-auth.d.ts +2 -0
- package/dist/setup-auth.d.ts.map +1 -0
- package/dist/setup-auth.js +29 -0
- package/dist/setup-auth.js.map +1 -0
- package/dist/tools/actions.tools.d.ts +170 -0
- package/dist/tools/actions.tools.d.ts.map +1 -0
- package/dist/tools/actions.tools.js +179 -0
- package/dist/tools/actions.tools.js.map +1 -0
- package/dist/tools/annotations.tools.d.ts +82 -0
- package/dist/tools/annotations.tools.d.ts.map +1 -0
- package/dist/tools/annotations.tools.js +180 -0
- package/dist/tools/annotations.tools.js.map +1 -0
- package/dist/tools/audit.tools.d.ts +45 -0
- package/dist/tools/audit.tools.d.ts.map +1 -0
- package/dist/tools/audit.tools.js +163 -0
- package/dist/tools/audit.tools.js.map +1 -0
- package/dist/tools/auth.tools.d.ts +17 -0
- package/dist/tools/auth.tools.d.ts.map +1 -0
- package/dist/tools/auth.tools.js +30 -0
- package/dist/tools/auth.tools.js.map +1 -0
- package/dist/tools/batch.tools.d.ts +45 -0
- package/dist/tools/batch.tools.d.ts.map +1 -0
- package/dist/tools/batch.tools.js +71 -0
- package/dist/tools/batch.tools.js.map +1 -0
- package/dist/tools/crud.tools.d.ts +206 -0
- package/dist/tools/crud.tools.d.ts.map +1 -0
- package/dist/tools/crud.tools.js +213 -0
- package/dist/tools/crud.tools.js.map +1 -0
- package/dist/tools/customization.tools.d.ts +75 -0
- package/dist/tools/customization.tools.d.ts.map +1 -0
- package/dist/tools/customization.tools.js +187 -0
- package/dist/tools/customization.tools.js.map +1 -0
- package/dist/tools/environment.tools.d.ts +40 -0
- package/dist/tools/environment.tools.d.ts.map +1 -0
- package/dist/tools/environment.tools.js +145 -0
- package/dist/tools/environment.tools.js.map +1 -0
- package/dist/tools/file.tools.d.ts +61 -0
- package/dist/tools/file.tools.d.ts.map +1 -0
- package/dist/tools/file.tools.js +142 -0
- package/dist/tools/file.tools.js.map +1 -0
- package/dist/tools/impersonate.tools.d.ts +37 -0
- package/dist/tools/impersonate.tools.d.ts.map +1 -0
- package/dist/tools/impersonate.tools.js +85 -0
- package/dist/tools/impersonate.tools.js.map +1 -0
- package/dist/tools/metadata.tools.d.ts +156 -0
- package/dist/tools/metadata.tools.d.ts.map +1 -0
- package/dist/tools/metadata.tools.js +200 -0
- package/dist/tools/metadata.tools.js.map +1 -0
- package/dist/tools/org.tools.d.ts +26 -0
- package/dist/tools/org.tools.d.ts.map +1 -0
- package/dist/tools/org.tools.js +57 -0
- package/dist/tools/org.tools.js.map +1 -0
- package/dist/tools/quality.tools.d.ts +30 -0
- package/dist/tools/quality.tools.d.ts.map +1 -0
- package/dist/tools/quality.tools.js +69 -0
- package/dist/tools/quality.tools.js.map +1 -0
- package/dist/tools/query.tools.d.ts +120 -0
- package/dist/tools/query.tools.d.ts.map +1 -0
- package/dist/tools/query.tools.js +182 -0
- package/dist/tools/query.tools.js.map +1 -0
- package/dist/tools/relations.tools.d.ts +65 -0
- package/dist/tools/relations.tools.d.ts.map +1 -0
- package/dist/tools/relations.tools.js +64 -0
- package/dist/tools/relations.tools.js.map +1 -0
- package/dist/tools/search.tools.d.ts +68 -0
- package/dist/tools/search.tools.d.ts.map +1 -0
- package/dist/tools/search.tools.js +134 -0
- package/dist/tools/search.tools.js.map +1 -0
- package/dist/tools/solution.tools.d.ts +95 -0
- package/dist/tools/solution.tools.d.ts.map +1 -0
- package/dist/tools/solution.tools.js +130 -0
- package/dist/tools/solution.tools.js.map +1 -0
- package/dist/tools/teams.tools.d.ts +27 -0
- package/dist/tools/teams.tools.d.ts.map +1 -0
- package/dist/tools/teams.tools.js +67 -0
- package/dist/tools/teams.tools.js.map +1 -0
- package/dist/tools/trace.tools.d.ts +63 -0
- package/dist/tools/trace.tools.d.ts.map +1 -0
- package/dist/tools/trace.tools.js +218 -0
- package/dist/tools/trace.tools.js.map +1 -0
- package/dist/tools/tracking.tools.d.ts +35 -0
- package/dist/tools/tracking.tools.d.ts.map +1 -0
- package/dist/tools/tracking.tools.js +40 -0
- package/dist/tools/tracking.tools.js.map +1 -0
- package/dist/tools/users.tools.d.ts +57 -0
- package/dist/tools/users.tools.d.ts.map +1 -0
- package/dist/tools/users.tools.js +146 -0
- package/dist/tools/users.tools.js.map +1 -0
- package/dist/tools/views.tools.d.ts +30 -0
- package/dist/tools/views.tools.d.ts.map +1 -0
- package/dist/tools/views.tools.js +84 -0
- package/dist/tools/views.tools.js.map +1 -0
- package/package.json +81 -0
- package/server.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MCP Dataverse Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# MCP Dataverse Server
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
    
|
|
6
|
+
|
|
7
|
+
MCP server that exposes the Microsoft Dataverse Web API as **39 AI-callable tools** — enabling GitHub Copilot, Claude, and other MCP clients to query, create, and manage Dataverse records without hallucinating schema.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
### One-click (VS Code)
|
|
12
|
+
|
|
13
|
+
[Install in VS Code](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522mcp-dataverse%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522mcp-dataverse%2522%255D%257D)
|
|
14
|
+
|
|
15
|
+
### Manual (npx)
|
|
16
|
+
|
|
17
|
+
Add to your `.vscode/mcp.json` (or user settings):
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"servers": {
|
|
22
|
+
"dataverse": {
|
|
23
|
+
"type": "stdio",
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["-y", "mcp-dataverse"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- Node.js 20+
|
|
34
|
+
- PAC CLI installed & authenticated → [aka.ms/PowerAppsCLI](https://aka.ms/PowerAppsCLI)
|
|
35
|
+
- VS Code + GitHub Copilot (Agent mode)
|
|
36
|
+
|
|
37
|
+
## Quick Start (< 5 min)
|
|
38
|
+
|
|
39
|
+
### 1. Clone & install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone <repo-url> mcp-dataverse && cd mcp-dataverse
|
|
43
|
+
npm install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Configure
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cp config.example.json config.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Edit `config.json`:
|
|
53
|
+
|
|
54
|
+
| Field | Description |
|
|
55
|
+
|-------|-------------|
|
|
56
|
+
| `environmentUrl` | Your org URL, e.g. `https://yourorg.crm.dynamics.com` |
|
|
57
|
+
| `authMode` | `"pac"` (recommended) or `"msal"` |
|
|
58
|
+
| `pacProfileName` | PAC CLI profile name (default: `"default"`) |
|
|
59
|
+
| `requestTimeoutMs` | HTTP timeout in ms (default: `30000`) |
|
|
60
|
+
| `maxRetries` | Retry count on transient errors (default: `3`) |
|
|
61
|
+
|
|
62
|
+
### 3. Authenticate
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run auth:setup
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Runs device code authentication. Follow the URL printed to the terminal — sign in with your Power Platform account. Token is cached in `.msal-cache.json` for silent reuse.
|
|
69
|
+
|
|
70
|
+
> Only required for `authMode: "pac"`. Skip if PAC CLI (`pac auth create`) is already authenticated.
|
|
71
|
+
|
|
72
|
+
### 4. Build
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run build
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. Verify the connection
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx tsx tests/live/test-whoami.ts
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Expected output:
|
|
85
|
+
```
|
|
86
|
+
WhoAmI result: { UserId: 'xxxxxxxx-...', BusinessUnitId: 'xxxxxxxx-...', OrganizationId: 'xxxxxxxx-...' }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 6. Configure VS Code
|
|
90
|
+
|
|
91
|
+
`.vscode/mcp.json` is already present in this repo. If you need to add it manually:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"servers": {
|
|
96
|
+
"dataverse": {
|
|
97
|
+
"type": "stdio",
|
|
98
|
+
"command": "node",
|
|
99
|
+
"args": ["${workspaceFolder}/dist/server.js"],
|
|
100
|
+
"env": {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
1. Restart VS Code
|
|
107
|
+
2. Open GitHub Copilot chat → switch to **Agent mode** (⚡)
|
|
108
|
+
3. Test: _"List the Dataverse tables in my environment"_
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Tools (48)
|
|
113
|
+
|
|
114
|
+
| Tool | Category | Description |
|
|
115
|
+
|------|----------|-------------|
|
|
116
|
+
| `dataverse_whoami` | Auth | Verify connection; returns UserId, BusinessUnitId, OrgId |
|
|
117
|
+
| `dataverse_list_tables` | Metadata | List all tables (`customOnly` filter available) |
|
|
118
|
+
| `dataverse_get_table_metadata` | Metadata | Full schema: columns, types, logical names |
|
|
119
|
+
| `dataverse_get_relationships` | Metadata | All 1:N, N:1, N:N relationships for a table; filter by `relationshipType` |
|
|
120
|
+
| `dataverse_list_global_option_sets` | Metadata | All global option sets in the environment |
|
|
121
|
+
| `dataverse_get_option_set` | Metadata | Options and values for a specific option set |
|
|
122
|
+
| `dataverse_get_entity_key` | Metadata | Alternate key definitions for a table (fields, index status, customizable flag) |
|
|
123
|
+
| `dataverse_query` | Query | OData query with `$select`, `$filter`, `$orderby`, `$expand`, `$count` |
|
|
124
|
+
| `dataverse_execute_fetchxml` | Query | Raw FetchXML for aggregations and complex joins |
|
|
125
|
+
| `dataverse_retrieve_multiple_with_paging` | Query | Paginated query following `@odata.nextLink`, with configurable `maxTotal` cap |
|
|
126
|
+
| `dataverse_get` | CRUD | Retrieve a single record by GUID |
|
|
127
|
+
| `dataverse_create` | CRUD | Create a record; returns the new GUID |
|
|
128
|
+
| `dataverse_update` | CRUD | Patch a record — only specified fields are changed |
|
|
129
|
+
| `dataverse_delete` | CRUD | Delete a record (requires explicit confirm) |
|
|
130
|
+
| `dataverse_upsert` | CRUD | Create-or-update via alternate key |
|
|
131
|
+
| `dataverse_associate` | Relations | Associate two records via a named relationship |
|
|
132
|
+
| `dataverse_disassociate` | Relations | Remove an association between two records |
|
|
133
|
+
| `dataverse_execute_action` | Actions | Execute a global (unbound) Dataverse action |
|
|
134
|
+
| `dataverse_execute_function` | Actions | Execute a global read-only function (e.g. `WhoAmI`) |
|
|
135
|
+
| `dataverse_execute_bound_action` | Actions | Execute an action bound to a specific record |
|
|
136
|
+
| `dataverse_execute_bound_function` | Actions | Execute an OData bound function on a specific record |
|
|
137
|
+
| `dataverse_retrieve_dependencies_for_delete` | Actions | Check what components block deletion of a Dataverse component |
|
|
138
|
+
| `dataverse_list_dependencies` | Actions | List component dependencies before modifying or deleting |
|
|
139
|
+
| `dataverse_batch_execute` | Batch | Up to 1000 operations in a single HTTP batch request; optional atomic changeset |
|
|
140
|
+
| `dataverse_change_detection` | Tracking | Delta tracking using change tokens to detect record changes since last sync |
|
|
141
|
+
| `dataverse_solution_components` | Solution | List all components in a named solution; filter by component type code |
|
|
142
|
+
| `dataverse_publish_customizations` | Solution | Publish pending customizations (all or targeted entities/web resources/option sets) |
|
|
143
|
+
| `dataverse_impersonate` | Impersonation | Execute any tool on behalf of another Dataverse user via `MSCRMCallerId` |
|
|
144
|
+
| `dataverse_list_custom_actions` | Customization | Lists custom actions (custom API / SDK messages) in the environment |
|
|
145
|
+
| `dataverse_list_plugin_steps` | Customization | Lists plugin step registrations with stage, mode, entity, and state |
|
|
146
|
+
| `dataverse_get_environment_variable` | Environment | Retrieve an environment variable definition and current value |
|
|
147
|
+
| `dataverse_set_environment_variable` | Environment | Set or update an environment variable value |
|
|
148
|
+
| `dataverse_get_plugin_trace_logs` | Trace | Retrieve plugin execution trace logs for debugging |
|
|
149
|
+
| `dataverse_get_workflow_trace_logs` | Trace | Retrieve async workflow/system job execution logs |
|
|
150
|
+
| `dataverse_search` | Search | Full-text Relevance Search across all configured tables |
|
|
151
|
+
| `dataverse_get_audit_log` | Audit | Retrieve audit trail for a record showing change history |
|
|
152
|
+
| `dataverse_detect_duplicates` | Quality | Check for potential duplicates before creating a record |
|
|
153
|
+
| `dataverse_get_annotations` | Annotations | Retrieve notes and file attachments linked to a record |
|
|
154
|
+
| `dataverse_list_users` | Users | Search system users by name or email with BU filtering |
|
|
155
|
+
| `dataverse_get_user_roles` | Users | Security roles assigned to a system user |
|
|
156
|
+
| `dataverse_create_annotation` | Annotations | Create a note or file attachment linked to a record |
|
|
157
|
+
| `dataverse_get_attribute_option_set` | Metadata | Local/entity-specific option set values (statecode, statuscode, picklist) |
|
|
158
|
+
| `dataverse_list_solutions` | Solution | List all solutions in the environment |
|
|
159
|
+
| `dataverse_set_workflow_state` | Customization | Enable or disable a workflow or process |
|
|
160
|
+
| `dataverse_list_views` | Views | List system and personal saved views for a table |
|
|
161
|
+
| `dataverse_upload_file_column` | Files | Upload binary content to a file/image column on a record |
|
|
162
|
+
| `dataverse_download_file_column` | Files | Download binary content from a file/image column on a record |
|
|
163
|
+
| `dataverse_list_business_units` | Org | List business units in the environment (org hierarchy) |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Advanced Configuration (MSAL)
|
|
168
|
+
|
|
169
|
+
For service-principal auth (CI/CD, unattended), set `authMode: "msal"` in `config.json`:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"environmentUrl": "https://yourorg.crm.dynamics.com",
|
|
174
|
+
"authMode": "msal",
|
|
175
|
+
"tenantId": "<azure-ad-tenant-id>",
|
|
176
|
+
"clientId": "<app-registration-client-id>",
|
|
177
|
+
"clientSecret": "<client-secret>"
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> The app registration must have the **Dynamics CRM → user_impersonation** API permission and the corresponding Dataverse security role.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Scripts
|
|
186
|
+
|
|
187
|
+
| Command | Description |
|
|
188
|
+
|---------|-------------|
|
|
189
|
+
| `npm run build` | Compile TypeScript → `dist/` |
|
|
190
|
+
| `npm run dev` | Watch mode — no build step needed |
|
|
191
|
+
| `npm start` | Start the compiled server |
|
|
192
|
+
| `npm run auth:setup` | One-time device code authentication |
|
|
193
|
+
| `npm run typecheck` | Type-check without emitting output |
|
|
194
|
+
| `npm run lint` | ESLint on `src/` |
|
|
195
|
+
| `npm run test:unit` | Unit tests only |
|
|
196
|
+
| `npm run test:integration` | Integration tests |
|
|
197
|
+
| `npm test` | All tests |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Architecture
|
|
202
|
+
|
|
203
|
+
TypeScript MCP server over **stdio** transport. An `AuthProvider` (PAC CLI or MSAL) injects Bearer tokens into a native-`fetch`-based `HttpClient` wrapped by `DataverseClient`. Each tool module registers handlers with the MCP `Server` instance.
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
GitHub Copilot → stdio → MCP Server → Tool Router → DataverseClient → Dataverse Web API v9.2
|
|
207
|
+
└── AuthProvider (PAC CLI | MSAL)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Security
|
|
213
|
+
|
|
214
|
+
- **Never commit `config.json`** or `.msal-cache.json` — both are in `.gitignore`
|
|
215
|
+
- Tokens are never logged; only diagnostic messages are written to stderr
|
|
216
|
+
- PAC CLI tokens are scoped to the authenticated user — no privilege escalation
|
|
217
|
+
- Service principal (`msal`) should be assigned the least-privilege Dataverse security role
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Docker
|
|
222
|
+
|
|
223
|
+
The server ships as a single-process stdio MCP server — no HTTP port is exposed.
|
|
224
|
+
**MSAL client credentials** is the recommended auth mode for containers; `PAC_CLI` mode requires an interactive device-code flow and is not suitable for container environments.
|
|
225
|
+
|
|
226
|
+
### Build the image
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
docker build -t mcp-dataverse .
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Run with MSAL client credentials
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
docker run --rm -i \
|
|
236
|
+
-e ENVIRONMENT_URL=https://yourorg.crm.dynamics.com \
|
|
237
|
+
-e CLIENT_ID=your-client-id \
|
|
238
|
+
-e CLIENT_SECRET=your-client-secret \
|
|
239
|
+
-e TENANT_ID=your-tenant-id \
|
|
240
|
+
mcp-dataverse
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
> The app registration must have the **Dynamics CRM → user_impersonation** API permission and a Dataverse security role assigned.
|
|
244
|
+
|
|
245
|
+
### Claude Desktop config (Docker)
|
|
246
|
+
|
|
247
|
+
Add the following to your Claude Desktop `claude_desktop_config.json`:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"mcpServers": {
|
|
252
|
+
"dataverse": {
|
|
253
|
+
"command": "docker",
|
|
254
|
+
"args": [
|
|
255
|
+
"run", "--rm", "-i",
|
|
256
|
+
"-e", "ENVIRONMENT_URL=https://yourorg.crm.dynamics.com",
|
|
257
|
+
"-e", "CLIENT_ID=your-client-id",
|
|
258
|
+
"-e", "CLIENT_SECRET=your-client-secret",
|
|
259
|
+
"-e", "TENANT_ID=your-tenant-id",
|
|
260
|
+
"mcp-dataverse"
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Troubleshooting
|
|
270
|
+
|
|
271
|
+
| Symptom | Fix |
|
|
272
|
+
|---------|-----|
|
|
273
|
+
| `No MSAL accounts found` | Run `npm run auth:setup` to re-authenticate |
|
|
274
|
+
| `"https://" is required` | Check `environmentUrl` in `config.json` — must start with `https://` |
|
|
275
|
+
| `pac: command not found` | Install PAC CLI and run `pac auth create --environment <url>` |
|
|
276
|
+
| Server not appearing in Copilot Agent mode | Restart VS Code; check **Output → MCP** panel for errors |
|
|
277
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"environmentUrl": "https://yourorg.crm.dynamics.com",
|
|
3
|
+
"authMode": "pac",
|
|
4
|
+
"pacProfileName": "default",
|
|
5
|
+
"tenantId": "",
|
|
6
|
+
"clientId": "",
|
|
7
|
+
"clientSecret": "",
|
|
8
|
+
"redirectUri": "http://localhost:3000/auth/callback",
|
|
9
|
+
"requestTimeoutMs": 30000,
|
|
10
|
+
"maxRetries": 3
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.factory.d.ts","sourceRoot":"","sources":["../../src/auth/auth-provider.factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAGjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAW/D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PacAuthProvider } from './pac-auth-provider.js';
|
|
2
|
+
import { MsalAuthProvider } from './msal-auth-provider.js';
|
|
3
|
+
export function createAuthProvider(config) {
|
|
4
|
+
switch (config.authMode) {
|
|
5
|
+
case 'pac':
|
|
6
|
+
return new PacAuthProvider(config.environmentUrl);
|
|
7
|
+
case 'msal':
|
|
8
|
+
return new MsalAuthProvider(config);
|
|
9
|
+
default: {
|
|
10
|
+
const _exhaustive = config.authMode;
|
|
11
|
+
throw new Error(`Unknown auth mode: ${_exhaustive}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=auth-provider.factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.factory.js","sourceRoot":"","sources":["../../src/auth/auth-provider.factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,QAAQ,MAAM,CAAC,QAAQ,EAAE,CAAC;QACxB,KAAK,KAAK;YACR,OAAO,IAAI,eAAe,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACpD,KAAK,MAAM;YACT,OAAO,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,WAAW,GAAU,MAAM,CAAC,QAAQ,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC,sBAAsB,WAAW,EAAE,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface AuthProvider {
|
|
2
|
+
/**
|
|
3
|
+
* Returns a valid Bearer token for the Dataverse environment.
|
|
4
|
+
* Implementations must handle token refresh silently.
|
|
5
|
+
*/
|
|
6
|
+
getToken(): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Invalidates any cached token, forcing a fresh acquisition on the next getToken() call.
|
|
9
|
+
* Must be called before retrying a request that received a 401 response.
|
|
10
|
+
*/
|
|
11
|
+
invalidateToken(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if the current auth session is valid.
|
|
14
|
+
*/
|
|
15
|
+
isAuthenticated(): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* The Dataverse environment URL this provider authenticates against.
|
|
18
|
+
*/
|
|
19
|
+
readonly environmentUrl: string;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=auth-provider.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.interface.d.ts","sourceRoot":"","sources":["../../src/auth/auth-provider.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,eAAe,IAAI,IAAI,CAAC;IAExB;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpC;;OAEG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-provider.interface.js","sourceRoot":"","sources":["../../src/auth/auth-provider.interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthProvider } from './auth-provider.interface.js';
|
|
2
|
+
import type { Config } from '../config/config.schema.js';
|
|
3
|
+
export declare class MsalAuthProvider implements AuthProvider {
|
|
4
|
+
readonly environmentUrl: string;
|
|
5
|
+
private readonly msalApp;
|
|
6
|
+
private readonly scope;
|
|
7
|
+
private cachedToken;
|
|
8
|
+
private tokenExpiresAt;
|
|
9
|
+
constructor(config: Config);
|
|
10
|
+
getToken(): Promise<string>;
|
|
11
|
+
invalidateToken(): void;
|
|
12
|
+
isAuthenticated(): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=msal-auth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msal-auth-provider.d.ts","sourceRoot":"","sources":["../../src/auth/msal-auth-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAEzD,qBAAa,gBAAiB,YAAW,YAAY;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAqB;gBAE/B,MAAM,EAAE,MAAM;IA0BpB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAuBjC,eAAe,IAAI,IAAI;IAKjB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;CAQ1C"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ConfidentialClientApplication } from '@azure/msal-node';
|
|
2
|
+
export class MsalAuthProvider {
|
|
3
|
+
environmentUrl;
|
|
4
|
+
msalApp;
|
|
5
|
+
scope;
|
|
6
|
+
cachedToken = null;
|
|
7
|
+
tokenExpiresAt = null;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.environmentUrl = config.environmentUrl;
|
|
10
|
+
if (!config.tenantId) {
|
|
11
|
+
throw new Error('MsalAuthProvider requires config.tenantId for client_credentials flow');
|
|
12
|
+
}
|
|
13
|
+
if (!config.clientId) {
|
|
14
|
+
throw new Error('MsalAuthProvider requires config.clientId for client_credentials flow');
|
|
15
|
+
}
|
|
16
|
+
if (!config.clientSecret) {
|
|
17
|
+
throw new Error('MsalAuthProvider requires config.clientSecret for client_credentials flow');
|
|
18
|
+
}
|
|
19
|
+
this.msalApp = new ConfidentialClientApplication({
|
|
20
|
+
auth: {
|
|
21
|
+
clientId: config.clientId,
|
|
22
|
+
clientSecret: config.clientSecret,
|
|
23
|
+
authority: `https://login.microsoftonline.com/${config.tenantId}`,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// Scope for Dataverse client_credentials: <environmentUrl>/.default
|
|
27
|
+
const baseUrl = config.environmentUrl.replace(/\/$/, '');
|
|
28
|
+
this.scope = `${baseUrl}/.default`;
|
|
29
|
+
}
|
|
30
|
+
async getToken() {
|
|
31
|
+
// Return cached token if still valid (60-second buffer before expiry)
|
|
32
|
+
if (this.cachedToken !== null && this.tokenExpiresAt !== null && new Date() < this.tokenExpiresAt) {
|
|
33
|
+
return this.cachedToken;
|
|
34
|
+
}
|
|
35
|
+
const result = await this.msalApp.acquireTokenByClientCredential({
|
|
36
|
+
scopes: [this.scope],
|
|
37
|
+
});
|
|
38
|
+
if (result === null || !result.accessToken) {
|
|
39
|
+
throw new Error('MSAL: acquireTokenByClientCredential returned no access token');
|
|
40
|
+
}
|
|
41
|
+
this.cachedToken = result.accessToken;
|
|
42
|
+
// Subtract 60 seconds from expiry to avoid using a token that is about to expire
|
|
43
|
+
this.tokenExpiresAt = result.expiresOn
|
|
44
|
+
? new Date(result.expiresOn.getTime() - 60_000)
|
|
45
|
+
: new Date(Date.now() + 3_540_000); // fallback: 59 minutes
|
|
46
|
+
return this.cachedToken;
|
|
47
|
+
}
|
|
48
|
+
invalidateToken() {
|
|
49
|
+
this.cachedToken = null;
|
|
50
|
+
this.tokenExpiresAt = null;
|
|
51
|
+
}
|
|
52
|
+
async isAuthenticated() {
|
|
53
|
+
try {
|
|
54
|
+
await this.getToken();
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=msal-auth-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msal-auth-provider.js","sourceRoot":"","sources":["../../src/auth/msal-auth-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAIjE,MAAM,OAAO,gBAAgB;IAClB,cAAc,CAAS;IACf,OAAO,CAAgC;IACvC,KAAK,CAAS;IACvB,WAAW,GAAkB,IAAI,CAAC;IAClC,cAAc,GAAgB,IAAI,CAAC;IAE3C,YAAY,MAAc;QACxB,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;QAE5C,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC3F,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;QAC/F,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,IAAI,6BAA6B,CAAC;YAC/C,IAAI,EAAE;gBACJ,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,SAAS,EAAE,qCAAqC,MAAM,CAAC,QAAQ,EAAE;aAClE;SACF,CAAC,CAAC;QAEH,oEAAoE;QACpE,MAAM,OAAO,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,KAAK,GAAG,GAAG,OAAO,WAAW,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,sEAAsE;QACtE,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI,IAAI,IAAI,CAAC,cAAc,KAAK,IAAI,IAAI,IAAI,IAAI,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YAClG,OAAO,IAAI,CAAC,WAAW,CAAC;QAC1B,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,8BAA8B,CAAC;YAC/D,MAAM,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;SACrB,CAAC,CAAC;QAEH,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;QACnF,CAAC;QAED,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QACtC,iFAAiF;QACjF,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,SAAS;YACpC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC;YAC/C,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,uBAAuB;QAE7D,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,eAAe;QACb,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;YACtB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AuthProvider } from './auth-provider.interface.js';
|
|
2
|
+
export declare class PacAuthProvider implements AuthProvider {
|
|
3
|
+
readonly environmentUrl: string;
|
|
4
|
+
private readonly pca;
|
|
5
|
+
private cachedToken;
|
|
6
|
+
private tokenExpiresAt;
|
|
7
|
+
constructor(environmentUrl: string);
|
|
8
|
+
getToken(): Promise<string>;
|
|
9
|
+
invalidateToken(): void;
|
|
10
|
+
isAuthenticated(): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Interactive device code flow — call once via `npm run auth:setup`.
|
|
13
|
+
* Writes to stderr so it doesn't disturb the stdio MCP transport.
|
|
14
|
+
*/
|
|
15
|
+
setupViaDeviceCode(): Promise<void>;
|
|
16
|
+
private refreshToken;
|
|
17
|
+
private cacheResult;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=pac-auth-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pac-auth-provider.d.ts","sourceRoot":"","sources":["../../src/auth/pac-auth-provider.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAwEjE,qBAAa,eAAgB,YAAW,YAAY;IAClD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0B;IAC9C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAa;gBAEvB,cAAc,EAAE,MAAM;IAY5B,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;IAQjC,eAAe,IAAI,IAAI;IAKjB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IASzC;;;OAGG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;YAY3B,YAAY;IAiC1B,OAAO,CAAC,WAAW;CAIpB"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PublicClientApplication, } from '@azure/msal-node';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
|
|
5
|
+
// Microsoft Power Platform CLI App ID — public client, no app registration needed
|
|
6
|
+
const PLATFORM_CLIENT_ID = '1950a258-227b-4e31-a9cf-717495945fc2';
|
|
7
|
+
const TOKEN_CACHE_FILE = join(process.cwd(), '.msal-cache.json');
|
|
8
|
+
/**
|
|
9
|
+
* Derives a machine+user-scoped encryption key.
|
|
10
|
+
* Not a substitute for OS-level credential storage (DPAPI/keytar), but prevents
|
|
11
|
+
* the cache file from being directly readable as plaintext by other processes.
|
|
12
|
+
*/
|
|
13
|
+
function getDerivedKey() {
|
|
14
|
+
const seed = [
|
|
15
|
+
process.env['COMPUTERNAME'] ?? process.env['HOSTNAME'] ?? '',
|
|
16
|
+
process.env['USERNAME'] ?? process.env['USER'] ?? '',
|
|
17
|
+
'mcp-dataverse-cache-v1',
|
|
18
|
+
].join('.');
|
|
19
|
+
return createHash('sha256').update(seed).digest();
|
|
20
|
+
}
|
|
21
|
+
function encryptForDisk(plaintext) {
|
|
22
|
+
const key = getDerivedKey();
|
|
23
|
+
const iv = randomBytes(16);
|
|
24
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
25
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
26
|
+
return JSON.stringify({
|
|
27
|
+
v: 1,
|
|
28
|
+
iv: iv.toString('hex'),
|
|
29
|
+
tag: cipher.getAuthTag().toString('hex'),
|
|
30
|
+
d: encrypted.toString('hex'),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function decryptFromDisk(raw) {
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (parsed['v'] !== 1)
|
|
36
|
+
throw new Error('Unknown cache format version');
|
|
37
|
+
const iv = Buffer.from(parsed['iv'], 'hex');
|
|
38
|
+
const tag = Buffer.from(parsed['tag'], 'hex');
|
|
39
|
+
const encrypted = Buffer.from(parsed['d'], 'hex');
|
|
40
|
+
const decipher = createDecipheriv('aes-256-gcm', getDerivedKey(), iv);
|
|
41
|
+
decipher.setAuthTag(tag);
|
|
42
|
+
return decipher.update(encrypted).toString('utf-8') + decipher.final('utf-8');
|
|
43
|
+
}
|
|
44
|
+
function createCachePlugin() {
|
|
45
|
+
return {
|
|
46
|
+
beforeCacheAccess: async (cacheContext) => {
|
|
47
|
+
if (existsSync(TOKEN_CACHE_FILE)) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = readFileSync(TOKEN_CACHE_FILE, 'utf-8');
|
|
50
|
+
// Support both encrypted (v1) and legacy plaintext caches
|
|
51
|
+
let serialized;
|
|
52
|
+
try {
|
|
53
|
+
serialized = decryptFromDisk(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Legacy plaintext — accept for migration, will be re-written encrypted
|
|
57
|
+
serialized = raw;
|
|
58
|
+
}
|
|
59
|
+
cacheContext.tokenCache.deserialize(serialized);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Corrupt cache — ignore, user will need to re-authenticate
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
afterCacheAccess: async (cacheContext) => {
|
|
67
|
+
if (cacheContext.cacheHasChanged) {
|
|
68
|
+
writeFileSync(TOKEN_CACHE_FILE, encryptForDisk(cacheContext.tokenCache.serialize()), { encoding: 'utf-8', mode: 0o600 });
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export class PacAuthProvider {
|
|
74
|
+
environmentUrl;
|
|
75
|
+
pca;
|
|
76
|
+
cachedToken = null;
|
|
77
|
+
tokenExpiresAt = 0;
|
|
78
|
+
constructor(environmentUrl) {
|
|
79
|
+
this.environmentUrl = environmentUrl.replace(/\/$/, '');
|
|
80
|
+
this.pca = new PublicClientApplication({
|
|
81
|
+
auth: {
|
|
82
|
+
clientId: PLATFORM_CLIENT_ID,
|
|
83
|
+
authority: 'https://login.microsoftonline.com/common',
|
|
84
|
+
},
|
|
85
|
+
cache: { cachePlugin: createCachePlugin() },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async getToken() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (this.cachedToken !== null && this.tokenExpiresAt > now + 60_000) {
|
|
91
|
+
return this.cachedToken;
|
|
92
|
+
}
|
|
93
|
+
return this.refreshToken();
|
|
94
|
+
}
|
|
95
|
+
invalidateToken() {
|
|
96
|
+
this.cachedToken = null;
|
|
97
|
+
this.tokenExpiresAt = 0;
|
|
98
|
+
}
|
|
99
|
+
async isAuthenticated() {
|
|
100
|
+
try {
|
|
101
|
+
await this.getToken();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Interactive device code flow — call once via `npm run auth:setup`.
|
|
110
|
+
* Writes to stderr so it doesn't disturb the stdio MCP transport.
|
|
111
|
+
*/
|
|
112
|
+
async setupViaDeviceCode() {
|
|
113
|
+
const result = await this.pca.acquireTokenByDeviceCode({
|
|
114
|
+
scopes: [`${this.environmentUrl}/.default`],
|
|
115
|
+
deviceCodeCallback: (response) => {
|
|
116
|
+
process.stderr.write(`\n${response.message}\n`);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
if (result) {
|
|
120
|
+
this.cacheResult(result);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async refreshToken() {
|
|
124
|
+
const accounts = await this.pca.getAllAccounts();
|
|
125
|
+
if (accounts.length === 0) {
|
|
126
|
+
throw new Error('No authenticated account found.\n' +
|
|
127
|
+
'Run once: npm run auth:setup\n' +
|
|
128
|
+
'Then restart the server.');
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const result = await this.pca.acquireTokenSilent({
|
|
132
|
+
scopes: [`${this.environmentUrl}/.default`],
|
|
133
|
+
account: accounts[0],
|
|
134
|
+
});
|
|
135
|
+
if (!result?.accessToken) {
|
|
136
|
+
throw new Error('Silent token acquisition returned empty token');
|
|
137
|
+
}
|
|
138
|
+
this.cacheResult(result);
|
|
139
|
+
return result.accessToken;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
this.cachedToken = null;
|
|
143
|
+
throw new Error('Token refresh failed. Re-authenticate:\n' +
|
|
144
|
+
'npm run auth:setup\n' +
|
|
145
|
+
'Then restart the server.');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
cacheResult(result) {
|
|
149
|
+
this.cachedToken = result.accessToken;
|
|
150
|
+
this.tokenExpiresAt = result.expiresOn?.getTime() ?? (Date.now() + 55 * 60 * 1000);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=pac-auth-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pac-auth-provider.js","sourceRoot":"","sources":["../../src/auth/pac-auth-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.loader.d.ts","sourceRoot":"","sources":["../../src/config/config.loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAI/D,wBAAgB,UAAU,IAAI,MAAM,CAqDnC"}
|