arbentia-dataverse-mcp 1.0.7 → 1.0.9

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/README.md CHANGED
@@ -1,43 +1,50 @@
1
1
  # Dataverse MCP Server
2
2
 
3
- A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for Microsoft Dataverse.
3
+ Un servidor [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) para Microsoft Dataverse.
4
4
 
5
- This server provides tools to explore Dataverse table definitions (metadata), columns (fields), relationships, keys, and OptionSets (Choice columns) using natural language through an MCP Client (like Claude Desktop or other AI agents).
5
+ Este servidor proporciona herramientas para explorar definiciones de tablas (metadatos), columnas (campos), relaciones, claves y OptionSets (columnas de elección) de Dataverse, además de crear entidades y gestionar campos mediante lenguaje natural usando un cliente MCP (como Claude Desktop u otros agentes de IA).
6
6
 
7
- ## Features
7
+ ## Características
8
8
 
9
- - **Refresh Metadata**: Downloads the full `$metadata` definition and `StringMaps` (Local OptionSets) from your Dataverse environment.
10
- - **List Tables**: Search for tables by name pattern.
11
- - **Get Table Details**: Get detailed schema information for specific tables, including Fields, Relationships, and Keys.
12
- - **Get Global OptionSets**: Retrieve details for global choices (Global OptionSets).
13
- - **Get Local OptionSets**: Retrieve values for local choices (StringMaps) specific to an entity and attribute.
9
+ - **Actualizar metadatos**: Descarga la definición completa de `$metadata` y `StringMaps` (OptionSets locales) de tu entorno de Dataverse.
10
+ - **Listar tablas**: Busca tablas por patrón de nombre.
11
+ - **Obtener detalles de tabla**: Obtiene información detallada del esquema para tablas específicas, incluyendo Campos, Relaciones y Claves.
12
+ - **Obtener OptionSets globales**: Recupera detalles para opciones globales (Global OptionSets).
13
+ - **Obtener OptionSets locales**: Recupera valores para opciones locales (StringMaps) específicas de una entidad y atributo.
14
+ - **Crear entidad**: Crea nuevas entidades en una solución específica de Dataverse.
15
+ - **Gestionar campos**: Crea o elimina campos (atributos) de forma masiva en una entidad.
14
16
 
15
- ## Installation
17
+ > **Nota importante**: Las herramientas de manipulación de datos (`create_entity` y `manage_fields`) requieren que se inicie el servidor con el parámetro `--solution` indicando el nombre único de la solución donde se realizarán los cambios.
18
+
19
+ ## Instalación
16
20
 
17
21
  ```bash
18
22
  npm install -g dataverse-mcp
19
23
  ```
20
24
 
21
- ## Usage
25
+ ## Uso
22
26
 
23
- ### Prerequisites
27
+ ### Prerrequisitos
24
28
 
25
- - A Microsoft Dataverse environment.
26
- - A user account with permissions to read metadata.
27
- - An MCP Client.
29
+ - Un entorno de Microsoft Dataverse.
30
+ - Una cuenta de usuario con permisos para leer metadatos y personalizar el sistema (si se usan herramientas de creación).
31
+ - Un cliente MCP.
28
32
 
29
- ### Running the Server
33
+ ### Ejecución del servidor
30
34
 
31
- You can run the server directly using `npx` or by installing it globally.
35
+ Puedes ejecutar el servidor directamente usando `npx` o instalándolo globalmente.
32
36
 
33
- **Using npx:**
37
+ **Usando npx:**
34
38
 
35
39
  ```bash
36
- npx dataverse-mcp --url "https://your-org.crm.dynamics.com"
40
+ npx dataverse-mcp --url "https://your-org.crm.dynamics.com" --solution "NombreSolucion"
37
41
  ```
38
- **Configuration in VsCode:**
39
42
 
40
- Add the following to your `mcp.json` in folder `.vscode`
43
+ *El parámetro `--solution` es opcional para lectura, pero obligatorio para crear entidades o campos.*
44
+
45
+ **Configuración en VsCode:**
46
+
47
+ Agrega lo siguiente a tu `mcp.json` en la carpeta `.vscode`:
41
48
 
42
49
  ```json
43
50
  {
@@ -48,16 +55,18 @@ Add the following to your `mcp.json` in folder `.vscode`
48
55
  "-y",
49
56
  "arbentia-dataverse-mcp",
50
57
  "--url",
51
- "https://your-org.crm.dynamics.com"
58
+ "https://your-org.crm.dynamics.com",
59
+ "--solution",
60
+ "NombreSolucion"
52
61
  ]
53
62
  }
54
63
  }
55
64
  }
56
65
  ```
57
66
 
58
- **Configuration in Claude Desktop:**
67
+ **Configuración en Claude Desktop:**
59
68
 
60
- Add the following to your `claude_desktop_config.json`:
69
+ Agrega lo siguiente a tu `claude_desktop_config.json`:
61
70
 
62
71
  ```json
63
72
  {
@@ -68,25 +77,29 @@ Add the following to your `claude_desktop_config.json`:
68
77
  "-y",
69
78
  "arbentia-dataverse-mcp",
70
79
  "--url",
71
- "https://your-org.crm.dynamics.com"
80
+ "https://your-org.crm.dynamics.com",
81
+ "--solution",
82
+ "NombreSolucion"
72
83
  ]
73
84
  }
74
85
  }
75
86
  }
76
87
  ```
77
88
 
78
- ### Authentication
89
+ ### Autenticación
79
90
 
80
- This tool uses `@azure/identity`'s `InteractiveBrowserCredential`. When you first use a tool that requires access, a browser window will open asking you to log in to your Microsoft account.
91
+ Esta herramienta utiliza `InteractiveBrowserCredential` de `@azure/identity`. Cuando uses por primera vez una herramienta que requiera acceso, se abrirá una ventana del navegador pidiéndote que inicies sesión en tu cuenta de Microsoft.
81
92
 
82
- ## Tools
93
+ ## Herramientas
83
94
 
84
- - `refresh_metadata`: Redownloads the metadata cache.
85
- - `list_tables_by_name`: Lists tables matching a regex pattern.
86
- - `get_tables_details`: Detailed schema for given tables. Supports filtering by `Fields`, `Relationships`, or `Keys`.
87
- - `get_global_optionset_details`: Details for Global OptionSets.
88
- - `get_local_optionset_details`: Details for Local OptionSets (StringMaps) for specific entities.
95
+ - `refresh_metadata`: Vuelve a descargar la caché de metadatos.
96
+ - `list_tables_by_name`: Lista tablas que coinciden con un patrón regex.
97
+ - `get_tables_details`: Esquema detallado para tablas dadas. Soporta filtrado por `Fields` (Campos), `Relationships` (Relaciones) o `Keys` (Claves).
98
+ - `get_global_optionset_details`: Detalles para OptionSets globales.
99
+ - `get_local_optionset_details`: Detalles para OptionSets locales (StringMaps) para entidades específicas.
100
+ - `create_entity`: Crea una nueva entidad en la solución especificada. Requiere el parámetro `solution` al inicio.
101
+ - `manage_fields`: Creación o eliminación por lotes de campos en una entidad. Requiere el parámetro `solution` al inicio.
89
102
 
90
- ## License
103
+ ## Licencia
91
104
 
92
105
  MIT
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as https from 'https';
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import * as crypto from 'crypto';
4
5
  import {
5
6
  CallToolRequestSchema,
6
7
  ListResourcesRequestSchema,
@@ -20,6 +21,8 @@ import * as listTablesByName from './tools/listTablesByName.js';
20
21
  import * as getTablesDetails from './tools/getTablesDetails.js';
21
22
  import * as getGlobalOptionSetDetails from './tools/getGlobalOptionSetDetails.js';
22
23
  import * as getLocalOptionSetDetails from './tools/getLocalOptionSetDetails.js';
24
+ import * as createEntity from './tools/createEntity.js';
25
+ import * as manageFields from './tools/manageFields.js';
23
26
 
24
27
  // Parse arguments
25
28
  const argv = yargs(hideBin(process.argv))
@@ -29,10 +32,16 @@ const argv = yargs(hideBin(process.argv))
29
32
  description: 'Dataverse Environment URL',
30
33
  demandOption: true
31
34
  })
35
+ .option('solution', {
36
+ alias: 's',
37
+ type: 'string',
38
+ description: 'Optional Dataverse solution name'
39
+ })
32
40
  .help()
33
41
  .argv;
34
42
 
35
43
  const dataverseUrl = argv.url;
44
+ const solutionName = argv.solution;
36
45
 
37
46
  // Initialize Server
38
47
  const server = new Server(
@@ -95,6 +104,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
95
104
  getTablesDetails.toolDefinition,
96
105
  getGlobalOptionSetDetails.toolDefinition,
97
106
  getLocalOptionSetDetails.toolDefinition,
107
+ createEntity.toolDefinition,
108
+ manageFields.toolDefinition,
98
109
  ],
99
110
  };
100
111
  });
@@ -104,7 +115,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
104
115
 
105
116
  try {
106
117
  if (name === refreshMetadata.toolDefinition.name) {
107
- return await refreshMetadata.handleRefreshMetadata(args, dataverseUrl);
118
+ return await refreshMetadata.handleRefreshMetadata(args, dataverseUrl, solutionName);
108
119
  }
109
120
  if (name === listTablesByName.toolDefinition.name) {
110
121
  return await listTablesByName.handleListTablesByName(args, dataverseUrl);
@@ -118,6 +129,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
118
129
  if (name === getLocalOptionSetDetails.toolDefinition.name) {
119
130
  return await getLocalOptionSetDetails.handleGetLocalOptionSetDetails(args, dataverseUrl);
120
131
  }
132
+ if (name === createEntity.toolDefinition.name) {
133
+ return await createEntity.handleCreateEntity(args, dataverseUrl, solutionName);
134
+ }
135
+ if (name === manageFields.toolDefinition.name) {
136
+ return await manageFields.handleManageFields(args, dataverseUrl, solutionName);
137
+ }
121
138
  } catch (err) {
122
139
  return {
123
140
  content: [{ type: "text", text: `Error executing tool ${name}: ${err.message}` }],
@@ -134,10 +151,11 @@ async function main() {
134
151
  await server.connect(transport);
135
152
  console.error("MCP Server running on stdio");
136
153
 
137
- // Trigger initial download (or ensure existence) in background
154
+
138
155
  console.error("Server started. Metadata will be loaded on demand.");
139
- ARBDownloadInstructions();
140
156
 
157
+
158
+ ARBDownloadInstructions();
141
159
  }
142
160
  async function ARBDownloadInstructions() {
143
161
  try {
package/metadata.js CHANGED
@@ -34,25 +34,31 @@ export function getMetadataDir(dataverseUrl) {
34
34
  * Ensures metadata exists. If not, downloads it.
35
35
  * @param {string} dataverseUrl
36
36
  * @param {string} token
37
+ * @param {string} [solutionName]
37
38
  */
38
- export async function ensureMetadata(dataverseUrl, token) {
39
+ export async function ensureMetadata(dataverseUrl, token, solutionName) {
39
40
  const cacheDir = getMetadataDir(dataverseUrl);
40
41
  const filePath = path.join(cacheDir, 'metadata.xml');
42
+ const solutionFilePath = solutionName ? path.join(cacheDir, `solution_${solutionName}.json`) : null;
41
43
 
42
- if (await fs.pathExists(filePath)) {
44
+ const metadataExists = await fs.pathExists(filePath);
45
+ const solutionExists = solutionFilePath ? await fs.pathExists(solutionFilePath) : true;
46
+
47
+ if (metadataExists && solutionExists) {
43
48
  console.error(`[Metadata] Metadata already exists at ${filePath}. Skipping download.`);
44
- return filePath;
49
+ return { filePath, solutionFound: true };
45
50
  }
46
51
 
47
- return await downloadMetadata(dataverseUrl, token);
52
+ return await downloadMetadata(dataverseUrl, token, solutionName);
48
53
  }
49
54
 
50
55
  /**
51
56
  * Downloads the Dataverse $metadata file and saves it locally.
52
57
  * @param {string} dataverseUrl The Dataverse environment URL.
53
58
  * @param {string} token The access token.
59
+ * @param {string} [solutionName] The optional solution name.
54
60
  */
55
- export async function downloadMetadata(dataverseUrl, token) {
61
+ export async function downloadMetadata(dataverseUrl, token, solutionName) {
56
62
  const url = new URL(dataverseUrl);
57
63
  const baseUrl = url.origin;
58
64
  const metadataUrl = `${baseUrl}/api/data/v9.2/$metadata`;
@@ -76,11 +82,16 @@ export async function downloadMetadata(dataverseUrl, token) {
76
82
 
77
83
  await downloadStringMaps(baseUrl, token, dataverseUrl);
78
84
 
85
+ let solutionFound = true;
86
+ if (solutionName) {
87
+ solutionFound = await downloadSolutionDetails(baseUrl, token, dataverseUrl, solutionName);
88
+ }
89
+
79
90
  // Invalidate cache on new download or if URL changed
80
91
  cachedParsedData = null;
81
92
  cachedDataverseUrl = null;
82
93
 
83
- return filePath;
94
+ return { filePath, solutionFound };
84
95
  } catch (error) {
85
96
  console.error(`[Metadata] Download failed: ${error.message}`);
86
97
  if (error.response) {
@@ -127,7 +138,37 @@ async function downloadStringMaps(baseUrl, token, dataverseUrl) {
127
138
  console.error(`[Metadata] Saved ${records.length} StringMaps to ${filePath}`);
128
139
  }
129
140
 
130
- async function getParsedMetadata(dataverseUrl) {
141
+ async function downloadSolutionDetails(baseUrl, token, dataverseUrl, solutionName) {
142
+ console.error(`[Metadata] Downloading details for solution: ${solutionName}...`);
143
+ const solutionUrl = `${baseUrl}/api/data/v9.2/solutions?$filter=uniquename eq '${solutionName}'&$expand=solution_solutioncomponent,publisherid`;
144
+
145
+ try {
146
+ const response = await axios.get(solutionUrl, {
147
+ headers: {
148
+ 'Authorization': `Bearer ${token}`,
149
+ 'Accept': 'application/json',
150
+ 'Prefer': 'odata.include-annotations="*"'
151
+ }
152
+ });
153
+
154
+ if (response.data.value && response.data.value.length > 0) {
155
+ const cacheDir = getMetadataDir(dataverseUrl);
156
+ const filePath = path.join(cacheDir, `solution_${solutionName}.json`);
157
+ await fs.writeJson(filePath, response.data.value[0]);
158
+ console.error(`[Metadata] Saved solution details to ${filePath}`);
159
+ return true;
160
+ } else {
161
+ console.warn(`[Metadata] Solution '${solutionName}' not found.`);
162
+ return false;
163
+ }
164
+ } catch (error) {
165
+ console.error(`[Metadata] Failed to download solution details: ${error.message}`);
166
+ // We might want to throw or return false. For now, let's treat error as not found/failed.
167
+ return false;
168
+ }
169
+ }
170
+
171
+ export async function getParsedMetadata(dataverseUrl) {
131
172
  if (cachedParsedData && cachedDataverseUrl === dataverseUrl) return cachedParsedData;
132
173
 
133
174
  const cacheDir = getMetadataDir(dataverseUrl);
@@ -145,155 +186,3 @@ async function getParsedMetadata(dataverseUrl) {
145
186
  return cachedParsedData;
146
187
  }
147
188
 
148
- /**
149
- * Returns a list of EntityType names.
150
- * @param {string} dataverseUrl
151
- * @returns {Promise<string[]>} List of entity names.
152
- */
153
- export async function getEntities(dataverseUrl) {
154
- const parsed = await getParsedMetadata(dataverseUrl);
155
- const entities = [];
156
-
157
- const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
158
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
159
-
160
- for (const schema of schemaList) {
161
- if (schema.EntityType) {
162
- const entityTypes = Array.isArray(schema.EntityType) ? schema.EntityType : [schema.EntityType];
163
- for (const et of entityTypes) {
164
- if (et['@_Name']) {
165
- entities.push(et['@_Name']);
166
- }
167
- }
168
- }
169
- }
170
- return entities;
171
- }
172
-
173
- /**
174
- * Returns details for valid entities.
175
- * @param {string[]} tableNames
176
- * @param {'Fields'|'Relationships'|'Keys'|'All'} detailType
177
- * @param {string} dataverseUrl
178
- */
179
- export async function getEntityDetails(tableNames, detailType = 'All', dataverseUrl) {
180
- const parsed = await getParsedMetadata(dataverseUrl);
181
- const result = {};
182
- const lowerNames = tableNames.map(n => n.toLowerCase());
183
-
184
- const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
185
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
186
-
187
- for (const schema of schemaList) {
188
- if (schema.EntityType) {
189
- const entityTypes = Array.isArray(schema.EntityType) ? schema.EntityType : [schema.EntityType];
190
- for (const et of entityTypes) {
191
- if (et['@_Name'] && lowerNames.includes(et['@_Name'].toLowerCase())) {
192
- const cleanEt = { ...et };
193
- delete cleanEt['Action'];
194
- delete cleanEt['Function'];
195
-
196
- if (detailType !== 'All') {
197
- const keepProps = [];
198
- if (detailType === 'Fields') keepProps.push('Property');
199
- if (detailType === 'Relationships') keepProps.push('NavigationProperty');
200
- if (detailType === 'Keys') keepProps.push('Key');
201
-
202
- if (!keepProps.includes('Property')) delete cleanEt['Property'];
203
- if (!keepProps.includes('NavigationProperty')) delete cleanEt['NavigationProperty'];
204
- if (!keepProps.includes('Key')) delete cleanEt['Key'];
205
- }
206
- result[et['@_Name']] = cleanEt;
207
- }
208
- }
209
- }
210
- }
211
- return result;
212
- }
213
-
214
- /**
215
- * Returns details for valid Global OptionSets (EnumTypes).
216
- * @param {string[]} optionSetNames
217
- * @param {string} dataverseUrl
218
- */
219
- export async function getGlobalOptionSetDetails(optionSetNames, dataverseUrl) {
220
- const parsed = await getParsedMetadata(dataverseUrl);
221
- const result = {};
222
- const lowerNames = optionSetNames.map(n => n.toLowerCase());
223
-
224
- const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
225
- const schemaList = Array.isArray(schemas) ? schemas : [schemas];
226
-
227
- for (const schema of schemaList) {
228
- if (schema.EnumType) {
229
- const enumTypes = Array.isArray(schema.EnumType) ? schema.EnumType : [schema.EnumType];
230
- for (const et of enumTypes) {
231
- if (!et['@_Name']) continue;
232
-
233
- const name = et['@_Name'].toLowerCase();
234
- const namespace = schema['@_Namespace'] ? schema['@_Namespace'].toLowerCase() : "";
235
- const fullName = namespace ? `${namespace}.${name}` : name;
236
-
237
- const matchedRequest = lowerNames.find(req => req === name || req === fullName);
238
-
239
- if (matchedRequest) {
240
- const key = schema['@_Namespace'] ? `${schema['@_Namespace']}.${et['@_Name']}` : et['@_Name'];
241
- result[key] = et;
242
- }
243
- }
244
- }
245
- }
246
- return result;
247
- }
248
-
249
- /**
250
- * Returns details for Local OptionSets from stringmaps.
251
- * @param {Object.<string, string[]>} requestMap Map of entity logical name to array of attribute names.
252
- * @param {string} dataverseUrl
253
- */
254
- export async function getLocalOptionSetDetails(requestMap, dataverseUrl) {
255
- const cacheDir = getMetadataDir(dataverseUrl);
256
- const stringMapsPath = path.join(cacheDir, 'stringmaps.json');
257
- const result = {};
258
-
259
- if (!await fs.pathExists(stringMapsPath)) {
260
- throw new Error("Metadata not found. Please run the 'refresh_metadata' tool to download it.");
261
- }
262
-
263
- try {
264
- const stringMaps = await fs.readJson(stringMapsPath);
265
-
266
- // requestMap structure: { "account": ["statuscode", "industrycode"], "contact": ["..."] }
267
- // Normalize request keys to lowercase
268
- const normalizedRequest = {};
269
- for (const [ent, attrs] of Object.entries(requestMap)) {
270
- normalizedRequest[ent.toLowerCase()] = attrs.map(a => a.toLowerCase());
271
- }
272
-
273
- // Iterate stringmaps
274
- for (const sm of stringMaps) {
275
- // "objecttypecode" is expected to be the entity logical name (string) based on user input.
276
- // If it's not, we might fail here, but we rely on user's instruction.
277
- const entityName = (sm['objecttypecode'] || '').toLowerCase();
278
- const attrName = (sm['attributename'] || '').toLowerCase();
279
-
280
- if (normalizedRequest[entityName] && normalizedRequest[entityName].includes(attrName)) {
281
- if (!result[entityName]) result[entityName] = {};
282
- if (!result[entityName][attrName]) result[entityName][attrName] = [];
283
-
284
- result[entityName][attrName].push({
285
- Display: sm['value'],
286
- OptionSetValue: sm['attributevalue']
287
- });
288
- }
289
- }
290
-
291
- // Sort options by something? Maybe displayorder if available, but user didn't ask.
292
- // Let's ensure integer parsing for OptionSetValue if it's a string?
293
- // stringmaps json usually has numbers for attributevalue.
294
- } catch (e) {
295
- console.error(`[Metadata] Error reading/processing stringmaps: ${e}`);
296
- }
297
-
298
- return result;
299
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arbentia-dataverse-mcp",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Model Context Protocol (MCP) server for Microsoft Dataverse Metadata",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,131 @@
1
+
2
+ import { requestConfirmation } from '../utils/userInteraction.js';
3
+ import { getAccessToken } from '../auth.js';
4
+ import { getMetadataDir, downloadMetadata } from '../metadata.js';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import axios from 'axios';
8
+
9
+ export const toolDefinition = {
10
+ name: "create_entity",
11
+ description: "Creates a new Entity in the Dataverse solution. Requires 'solution' parameter to be set on MCP server. Validates schema prefix against solution publisher.",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ displayName: { type: "string", description: "Display name of the entity (Spanish)" },
16
+ pluralName: { type: "string", description: "Plural display name (Spanish)" },
17
+ schemaName: { type: "string", description: "Schema name (English, must start with solution prefix, e.g. 'prefix_TableName')" },
18
+ description: { type: "string", description: "Description of the entity (Spanish)" },
19
+ primaryAttributeName: { type: "string", description: "Schema name of the primary attribute (default: prefix_Name)" },
20
+ primaryAttributeDisplayName: { type: "string", description: "Display name of the primary attribute (default: Nombre)" },
21
+ ownershipType: { type: "string", enum: ["UserOwned", "OrganizationOwned"], default: "UserOwned" },
22
+ isActivity: { type: "boolean", default: false }
23
+ },
24
+ required: ["displayName", "pluralName", "schemaName"]
25
+ }
26
+ };
27
+
28
+ export async function handleCreateEntity(args, dataverseUrl, solutionName) {
29
+ if (!solutionName) {
30
+ return { isError: true, content: [{ type: "text", text: "Error: No solution defined. Please restart MCP with --solution argument." }] };
31
+ }
32
+
33
+ let { displayName, pluralName, schemaName, description, ownershipType, isActivity, primaryAttributeName, primaryAttributeDisplayName } = args;
34
+ const cacheDir = getMetadataDir(dataverseUrl);
35
+ const solutionInfoPath = path.join(cacheDir, `solution_${solutionName}.json`);
36
+
37
+ // Check metadata
38
+ if (!await fs.pathExists(solutionInfoPath)) {
39
+ return { isError: true, content: [{ type: "text", text: `Error: Metadata for solution '${solutionName}' not found. Please run 'refresh_metadata'.` }] };
40
+ }
41
+
42
+ const solutionData = await fs.readJson(solutionInfoPath);
43
+ const prefix = solutionData.publisherid?.customizationprefix;
44
+
45
+ if (prefix && !schemaName.startsWith(prefix + '_')) {
46
+ return { isError: true, content: [{ type: "text", text: `Error: Schema name '${schemaName}' must start with solution prefix '${prefix}_'.` }] };
47
+ }
48
+
49
+ // Default Primary Attribute
50
+ if (!primaryAttributeName) {
51
+ primaryAttributeName = `${prefix ? prefix + '_' : ''}Name`;
52
+ }
53
+ if (!primaryAttributeDisplayName) {
54
+ primaryAttributeDisplayName = "Nombre";
55
+ }
56
+
57
+ // Validation for primary attribute prefix
58
+ if (prefix && !primaryAttributeName.startsWith(prefix + '_')) {
59
+ return { isError: true, content: [{ type: "text", text: `Error: Primary Attribute Schema name '${primaryAttributeName}' must start with solution prefix '${prefix}_'.` }] };
60
+ }
61
+
62
+
63
+ // Confirmation
64
+ const detailsHtml = `
65
+ <p><strong>Solución:</strong> ${solutionName}</p>
66
+ <p><strong>Nombre Mostrado (ES):</strong> ${displayName}</p>
67
+ <p><strong>Nombre Plural (ES):</strong> ${pluralName}</p>
68
+ <p><strong>Nombre de Esquema:</strong> ${schemaName}</p>
69
+ <p><strong>Atributo Principal:</strong> ${primaryAttributeName} (${primaryAttributeDisplayName})</p>
70
+ <p><strong>Descripción:</strong> ${description || 'N/A'}</p>
71
+ <p><strong>Tipo de Propiedad:</strong> ${ownershipType}</p>
72
+ <p><strong>Es Actividad:</strong> ${isActivity}</p>
73
+ `;
74
+
75
+ const confirmed = await requestConfirmation('Crear Nueva Entidad', detailsHtml);
76
+ if (!confirmed) {
77
+ return { content: [{ type: "text", text: "Operation cancelled or timed out." }] };
78
+ }
79
+
80
+ // Execution
81
+ try {
82
+ const token = await getAccessToken(dataverseUrl);
83
+ const url = new URL(dataverseUrl);
84
+ const apiBase = `${url.origin}/api/data/v9.2`;
85
+
86
+ const entityDefinition = {
87
+ "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata",
88
+ SchemaName: schemaName,
89
+ DisplayName: { LocalizedLabels: [{ Label: displayName, LanguageCode: 3082 }] }, // ES
90
+ DisplayCollectionName: { LocalizedLabels: [{ Label: pluralName, LanguageCode: 3082 }] },
91
+ Description: { LocalizedLabels: [{ Label: description || "", LanguageCode: 3082 }] },
92
+ OwnershipType: ownershipType || "UserOwned",
93
+ IsActivity: !!isActivity,
94
+ HasActivities: false,
95
+ HasNotes: false,
96
+ Attributes: [
97
+ {
98
+ "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
99
+ SchemaName: primaryAttributeName,
100
+ RequiredLevel: { Value: "None", CanBeChanged: true },
101
+ MaxLength: 100,
102
+ DisplayName: { LocalizedLabels: [{ Label: primaryAttributeDisplayName, LanguageCode: 3082 }] },
103
+ IsPrimaryName: true,
104
+ AttributeType: "String",
105
+ FormatName: { Value: "Text" }
106
+ }
107
+ ]
108
+ };
109
+
110
+ console.error(`[CreateEntity] Creating entity ${schemaName}...`);
111
+
112
+ await axios.post(`${apiBase}/EntityDefinitions`, entityDefinition, {
113
+ headers: {
114
+ 'Authorization': `Bearer ${token}`,
115
+ 'Content-Type': 'application/json',
116
+ 'MSCRM.SolutionUniqueName': solutionName
117
+ }
118
+ });
119
+
120
+ console.error(`[CreateEntity] Refreshing metadata...`);
121
+ await downloadMetadata(dataverseUrl, token, solutionName);
122
+
123
+ return {
124
+ content: [{ type: "text", text: `Successfully created entity '${schemaName}' in solution '${solutionName}'. Metadata refreshed.` }]
125
+ };
126
+
127
+ } catch (error) {
128
+ const errorMsg = error.response ? JSON.stringify(error.response.data, null, 2) : error.message;
129
+ return { isError: true, content: [{ type: "text", text: `Failed to create entity: ${errorMsg}` }] };
130
+ }
131
+ }
@@ -1,4 +1,4 @@
1
- import { getGlobalOptionSetDetails } from '../metadata.js';
1
+ import { getParsedMetadata } from '../metadata.js';
2
2
 
3
3
  export const toolDefinition = {
4
4
  name: "get_global_optionset_details",
@@ -9,7 +9,7 @@ export const toolDefinition = {
9
9
  optionset_names: {
10
10
  type: "array",
11
11
  items: { type: "string" },
12
- description: "List of OptionSet names (e.g. ['prv_budgettypes', 'account_statuscode'])",
12
+ description: "List of Global OptionSet names",
13
13
  },
14
14
  },
15
15
  required: ["optionset_names"],
@@ -18,12 +18,38 @@ export const toolDefinition = {
18
18
 
19
19
  export async function handleGetGlobalOptionSetDetails(args, dataverseUrl) {
20
20
  const { optionset_names } = args;
21
- const details = await getGlobalOptionSetDetails(optionset_names, dataverseUrl);
21
+
22
+ const parsed = await getParsedMetadata(dataverseUrl);
23
+ const result = {};
24
+ const lowerNames = optionset_names.map(n => n.toLowerCase());
25
+
26
+ const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
27
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
28
+
29
+ for (const schema of schemaList) {
30
+ if (schema.EnumType) {
31
+ const enumTypes = Array.isArray(schema.EnumType) ? schema.EnumType : [schema.EnumType];
32
+ for (const et of enumTypes) {
33
+ if (!et['@_Name']) continue;
34
+
35
+ const name = et['@_Name'].toLowerCase();
36
+ const namespace = schema['@_Namespace'] ? schema['@_Namespace'].toLowerCase() : "";
37
+ const fullName = namespace ? `${namespace}.${name}` : name;
38
+
39
+ const matchedRequest = lowerNames.find(req => req === name || req === fullName);
40
+
41
+ if (matchedRequest) {
42
+ const key = schema['@_Namespace'] ? `${schema['@_Namespace']}.${et['@_Name']}` : et['@_Name'];
43
+ result[key] = et;
44
+ }
45
+ }
46
+ }
47
+ }
22
48
 
23
49
  return {
24
50
  content: [{
25
51
  type: "text",
26
- text: JSON.stringify(details, null, 2)
52
+ text: JSON.stringify(result, null, 2)
27
53
  }],
28
54
  };
29
55
  }
@@ -1,18 +1,20 @@
1
- import { getLocalOptionSetDetails } from '../metadata.js';
1
+ import { getMetadataDir } from '../metadata.js';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
2
4
 
3
5
  export const toolDefinition = {
4
6
  name: "get_local_optionset_details",
5
- description: "Get details for local OptionSets (StringMaps) for specified entities and attributes.",
7
+ description: "Get schema details for local Dataverse OptionSets (statuscode, statecode, etc.) from stringmaps.",
6
8
  inputSchema: {
7
9
  type: "object",
8
10
  properties: {
9
11
  request_map: {
10
12
  type: "object",
13
+ description: "Map of entity logical names to arrays of attribute names, e.g., { 'account': ['statuscode', 'industrycode'] }",
11
14
  additionalProperties: {
12
15
  type: "array",
13
16
  items: { type: "string" }
14
- },
15
- description: "Map of entity logical names to lists of attribute names. Example: { 'account': ['statuscode', 'industrycode'] }",
17
+ }
16
18
  },
17
19
  },
18
20
  required: ["request_map"],
@@ -21,12 +23,47 @@ export const toolDefinition = {
21
23
 
22
24
  export async function handleGetLocalOptionSetDetails(args, dataverseUrl) {
23
25
  const { request_map } = args;
24
- const details = await getLocalOptionSetDetails(request_map, dataverseUrl);
26
+
27
+ const cacheDir = getMetadataDir(dataverseUrl);
28
+ const stringMapsPath = path.join(cacheDir, 'stringmaps.json');
29
+ const result = {};
30
+
31
+ if (!await fs.pathExists(stringMapsPath)) {
32
+ throw new Error("Metadata not found. Please run the 'refresh_metadata' tool to download it.");
33
+ }
34
+
35
+ try {
36
+ const stringMaps = await fs.readJson(stringMapsPath);
37
+
38
+ // Normalize request keys to lowercase
39
+ const normalizedRequest = {};
40
+ for (const [ent, attrs] of Object.entries(request_map)) {
41
+ normalizedRequest[ent.toLowerCase()] = attrs.map(a => a.toLowerCase());
42
+ }
43
+
44
+ // Iterate stringmaps
45
+ for (const sm of stringMaps) {
46
+ const entityName = (sm['objecttypecode'] || '').toLowerCase();
47
+ const attrName = (sm['attributename'] || '').toLowerCase();
48
+
49
+ if (normalizedRequest[entityName] && normalizedRequest[entityName].includes(attrName)) {
50
+ if (!result[entityName]) result[entityName] = {};
51
+ if (!result[entityName][attrName]) result[entityName][attrName] = [];
52
+
53
+ result[entityName][attrName].push({
54
+ Display: sm['value'],
55
+ OptionSetValue: sm['attributevalue']
56
+ });
57
+ }
58
+ }
59
+ } catch (e) {
60
+ console.error(`[Metadata] Error reading/processing stringmaps: ${e}`);
61
+ }
25
62
 
26
63
  return {
27
64
  content: [{
28
65
  type: "text",
29
- text: JSON.stringify(details, null, 2)
66
+ text: JSON.stringify(result, null, 2)
30
67
  }],
31
68
  };
32
69
  }
@@ -1,4 +1,4 @@
1
- import { getEntityDetails } from '../metadata.js';
1
+ import { getParsedMetadata } from '../metadata.js';
2
2
 
3
3
  export const toolDefinition = {
4
4
  name: "get_tables_details",
@@ -22,13 +22,44 @@ export const toolDefinition = {
22
22
  };
23
23
 
24
24
  export async function handleGetTablesDetails(args, dataverseUrl) {
25
- const { table_names, detail_type } = args;
26
- const details = await getEntityDetails(table_names, detail_type, dataverseUrl);
25
+ const { table_names, detail_type = 'All' } = args;
26
+
27
+ const parsed = await getParsedMetadata(dataverseUrl);
28
+ const result = {};
29
+ const lowerNames = table_names.map(n => n.toLowerCase());
30
+
31
+ const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
32
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
33
+
34
+ for (const schema of schemaList) {
35
+ if (schema.EntityType) {
36
+ const entityTypes = Array.isArray(schema.EntityType) ? schema.EntityType : [schema.EntityType];
37
+ for (const et of entityTypes) {
38
+ if (et['@_Name'] && lowerNames.includes(et['@_Name'].toLowerCase())) {
39
+ const cleanEt = { ...et };
40
+ delete cleanEt['Action'];
41
+ delete cleanEt['Function'];
42
+
43
+ if (detail_type !== 'All') {
44
+ const keepProps = [];
45
+ if (detail_type === 'Fields') keepProps.push('Property');
46
+ if (detail_type === 'Relationships') keepProps.push('NavigationProperty');
47
+ if (detail_type === 'Keys') keepProps.push('Key');
48
+
49
+ if (!keepProps.includes('Property')) delete cleanEt['Property'];
50
+ if (!keepProps.includes('NavigationProperty')) delete cleanEt['NavigationProperty'];
51
+ if (!keepProps.includes('Key')) delete cleanEt['Key'];
52
+ }
53
+ result[et['@_Name']] = cleanEt;
54
+ }
55
+ }
56
+ }
57
+ }
27
58
 
28
59
  return {
29
60
  content: [{
30
61
  type: "text",
31
- text: JSON.stringify(details, null, 2)
62
+ text: JSON.stringify(result, null, 2)
32
63
  }],
33
64
  };
34
65
  }
@@ -1,4 +1,4 @@
1
- import { getEntities } from '../metadata.js';
1
+ import { getParsedMetadata } from '../metadata.js';
2
2
 
3
3
  export const toolDefinition = {
4
4
  name: "list_tables_by_name",
@@ -25,7 +25,23 @@ export const toolDefinition = {
25
25
 
26
26
  export async function handleListTablesByName(args, dataverseUrl) {
27
27
  const { name_pattern, page = 1, size = 50 } = args;
28
- const entities = await getEntities(dataverseUrl);
28
+
29
+ const parsed = await getParsedMetadata(dataverseUrl);
30
+ const entities = [];
31
+
32
+ const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
33
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
34
+
35
+ for (const schema of schemaList) {
36
+ if (schema.EntityType) {
37
+ const entityTypes = Array.isArray(schema.EntityType) ? schema.EntityType : [schema.EntityType];
38
+ for (const et of entityTypes) {
39
+ if (et['@_Name']) {
40
+ entities.push(et['@_Name']);
41
+ }
42
+ }
43
+ }
44
+ }
29
45
 
30
46
  const regex = new RegExp(name_pattern, 'i');
31
47
  const matches = entities.filter(e => regex.test(e));
@@ -0,0 +1,246 @@
1
+
2
+ import { requestConfirmation } from '../utils/userInteraction.js';
3
+ import { getAccessToken } from '../auth.js';
4
+ import { getMetadataDir, downloadMetadata } from '../metadata.js';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import axios from 'axios';
8
+
9
+ export const toolDefinition = {
10
+ name: "manage_fields",
11
+ description: "Batched creation or deletion of fields (attributes) in an entity. Requires 'solution' parameter.",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ entityName: { type: "string", description: "Logical/Schema name of the entity" },
16
+ operations: {
17
+ type: "array",
18
+ description: "List of operations to perform",
19
+ items: {
20
+ type: "object",
21
+ properties: {
22
+ action: { type: "string", enum: ["create", "delete"], description: "Action to perform" },
23
+ // For Create
24
+ displayName: { type: "string", description: "Display name (Spanish)" },
25
+ schemaName: { type: "string", description: "Schema name (English, with prefix)" },
26
+ type: { type: "string", enum: ["String", "Integer", "Boolean", "DateTime", "Memo", "Money", "Double", "Decimal", "Picklist", "Lookup"], description: "Data type" },
27
+ description: { type: "string", description: "Description (Spanish)" },
28
+ requiredLevel: { type: "string", enum: ["None", "SystemRequired", "ApplicationRequired", "Recommended"], default: "None" },
29
+
30
+
31
+ maxLength: { type: "integer", description: "Max length for String/Memo fields (optional)" },
32
+
33
+ // Specific properties
34
+ targetEntity: { type: "string", description: "Target entity logical name for Lookup fields (e.g. 'account')" },
35
+ options: {
36
+ type: "array",
37
+ description: "Options for Picklist fields",
38
+ items: {
39
+ type: "object",
40
+ properties: {
41
+ label: { type: "string", description: "Option label" },
42
+ value: { type: "integer", description: "Option integer value" }
43
+ },
44
+ required: ["label", "value"]
45
+ }
46
+ }
47
+ // For Delete
48
+ // schemaName is used.
49
+ },
50
+ required: ["action", "schemaName"]
51
+ }
52
+ }
53
+ },
54
+ required: ["entityName", "operations"]
55
+ }
56
+ };
57
+
58
+ const TYPE_MAPPING = {
59
+ "String": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
60
+ "Integer": "Microsoft.Dynamics.CRM.IntegerAttributeMetadata",
61
+ "Boolean": "Microsoft.Dynamics.CRM.BooleanAttributeMetadata",
62
+ "DateTime": "Microsoft.Dynamics.CRM.DateTimeAttributeMetadata",
63
+ "Memo": "Microsoft.Dynamics.CRM.MemoAttributeMetadata",
64
+ "Money": "Microsoft.Dynamics.CRM.MoneyAttributeMetadata",
65
+ "Double": "Microsoft.Dynamics.CRM.DoubleAttributeMetadata",
66
+ "Decimal": "Microsoft.Dynamics.CRM.DecimalAttributeMetadata",
67
+ "Picklist": "Microsoft.Dynamics.CRM.PicklistAttributeMetadata",
68
+ "Lookup": "Microsoft.Dynamics.CRM.LookupAttributeMetadata"
69
+ };
70
+
71
+ export async function handleManageFields(args, dataverseUrl, solutionName) {
72
+ if (!solutionName) {
73
+ return { isError: true, content: [{ type: "text", text: "Error: No solution defined." }] };
74
+ }
75
+
76
+ const { entityName, operations } = args;
77
+ const cacheDir = getMetadataDir(dataverseUrl);
78
+ const solutionInfoPath = path.join(cacheDir, `solution_${solutionName}.json`);
79
+
80
+ if (!await fs.pathExists(solutionInfoPath)) {
81
+ return { isError: true, content: [{ type: "text", text: `Error: Metadata for solution '${solutionName}' not found.` }] };
82
+ }
83
+
84
+ const solutionData = await fs.readJson(solutionInfoPath);
85
+ const prefix = solutionData.publisherid?.customizationprefix;
86
+
87
+ // Validate
88
+ const invalidOps = operations.filter(op => op.action === 'create' && prefix && !op.schemaName.startsWith(prefix + '_'));
89
+ if (invalidOps.length > 0) {
90
+ return { isError: true, content: [{ type: "text", text: `Error: Fields must start with prefix '${prefix}_'. Invalid: ${invalidOps.map(o => o.schemaName).join(', ')}` }] };
91
+ }
92
+
93
+ // Build HTML
94
+ let rows = operations.map(op => {
95
+ const color = op.action === 'delete' ? '#ffebee' : '#e8f5e9';
96
+ const actionLabel = op.action === 'delete' ? 'Eliminar' : 'Crear';
97
+ let details = op.type || '-';
98
+
99
+ if (op.type === 'Lookup' && op.targetEntity) {
100
+ details += ` -> ${op.targetEntity}`;
101
+ } else if (op.type === 'Picklist' && op.options) {
102
+ details += ` (${op.options.length} opciones)`;
103
+ } else if ((op.type === 'String' || op.type === 'Memo') && op.maxLength) {
104
+ details += ` (Max: ${op.maxLength})`;
105
+ }
106
+
107
+ return `<tr style="background-color: ${color}">
108
+ <td>${actionLabel}</td>
109
+ <td>${op.schemaName}</td>
110
+ <td>${op.displayName || '-'}</td>
111
+ <td>${details}</td>
112
+ </tr>`;
113
+ }).join('');
114
+
115
+ const html = `
116
+ <p><strong>Entidad:</strong> ${entityName}</p>
117
+ <p><strong>Solución:</strong> ${solutionName}</p>
118
+ <table style="width:100%; border-collapse: collapse;">
119
+ <thead>
120
+ <tr style="text-align:left; background: #eee;">
121
+ <th style="padding: 8px;">Acción</th>
122
+ <th style="padding: 8px;">Nombre Esquema</th>
123
+ <th style="padding: 8px;">Nombre Mostrar</th>
124
+ <th style="padding: 8px;">Tipo / Detalles</th>
125
+ </tr>
126
+ </thead>
127
+ <tbody>
128
+ ${rows}
129
+ </tbody>
130
+ </table>
131
+ `;
132
+
133
+ const confirmed = await requestConfirmation('Gestión de Campos', html);
134
+ if (!confirmed) return { content: [{ type: "text", text: "Operation cancelled." }] };
135
+
136
+ // Execute
137
+ const token = await getAccessToken(dataverseUrl);
138
+ const url = new URL(dataverseUrl);
139
+ const apiBase = `${url.origin}/api/data/v9.2`;
140
+ let report = [];
141
+
142
+ for (const op of operations) {
143
+ try {
144
+ if (op.action === 'create') {
145
+ if (op.type === "Lookup") {
146
+ // Create Lookup via RelationshipDefinitions
147
+ if (!op.targetEntity) throw new Error("targetEntity is required for Lookup fields.");
148
+
149
+ const relName = `${op.schemaName}_Rel`;
150
+ const body = {
151
+ "@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata",
152
+ SchemaName: relName,
153
+ ReferencedEntity: op.targetEntity,
154
+ ReferencingEntity: entityName,
155
+ Lookup: {
156
+ SchemaName: op.schemaName,
157
+ DisplayName: { LocalizedLabels: [{ Label: op.displayName, LanguageCode: 3082 }] },
158
+ Description: { LocalizedLabels: [{ Label: op.description || "", LanguageCode: 3082 }] },
159
+ RequiredLevel: { Value: op.requiredLevel || "None", CanBeChanged: true }
160
+ },
161
+ CascadeConfiguration: {
162
+ Assign: "NoCascade",
163
+ Delete: "RemoveLink",
164
+ Merge: "NoCascade",
165
+ Reparent: "NoCascade",
166
+ Share: "NoCascade",
167
+ Unshare: "NoCascade"
168
+ }
169
+ };
170
+
171
+ console.error(`[ManageFields] Creating Lookup Relationship ${relName}...`);
172
+ await axios.post(`${apiBase}/RelationshipDefinitions`, body, {
173
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'MSCRM.SolutionUniqueName': solutionName }
174
+ });
175
+ report.push(`✅ Created Lookup ${op.schemaName} (via Relationship ${relName})`);
176
+
177
+ } else {
178
+ // Standard Attribute Creation
179
+ const odataType = TYPE_MAPPING[op.type] || "Microsoft.Dynamics.CRM.StringAttributeMetadata";
180
+ const body = {
181
+ "@odata.type": odataType,
182
+ SchemaName: op.schemaName,
183
+ DisplayName: { LocalizedLabels: [{ Label: op.displayName, LanguageCode: 3082 }] },
184
+ Description: { LocalizedLabels: [{ Label: op.description || "", LanguageCode: 3082 }] },
185
+ RequiredLevel: { Value: op.requiredLevel || "None", CanBeChanged: true }
186
+ };
187
+
188
+ // Specific defaults
189
+ if (op.type === "String") body.MaxLength = op.maxLength || 100;
190
+ if (op.type === "Memo") body.MaxLength = op.maxLength || 2000;
191
+ if (op.type === "Boolean") {
192
+ body.OptionSet = {
193
+ TrueOption: {
194
+ Value: 1,
195
+ Label: { LocalizedLabels: [{ Label: "Sí", LanguageCode: 3082 }] }
196
+ },
197
+ FalseOption: {
198
+ Value: 0,
199
+ Label: { LocalizedLabels: [{ Label: "No", LanguageCode: 3082 }] }
200
+ }
201
+ };
202
+ }
203
+ if (op.type === "DateTime") {
204
+ body.Format = "DateAndTime";
205
+ }
206
+ if (op.type === "Picklist") {
207
+ if (!op.options || !Array.isArray(op.options)) throw new Error("options array is required for Picklist fields.");
208
+ body.OptionSet = {
209
+ IsGlobal: false,
210
+ OptionSetType: "Picklist",
211
+ Options: op.options.map(opt => ({
212
+ Value: opt.value,
213
+ Label: { LocalizedLabels: [{ Label: opt.label, LanguageCode: 3082 }] }
214
+ }))
215
+ };
216
+ }
217
+
218
+ console.error(`[ManageFields] Creating ${op.schemaName}...`);
219
+ await axios.post(`${apiBase}/EntityDefinitions(LogicalName='${entityName.toLowerCase()}')/Attributes`, body, {
220
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', 'MSCRM.SolutionUniqueName': solutionName }
221
+ });
222
+ report.push(`✅ Created ${op.schemaName}`);
223
+ }
224
+
225
+ } else if (op.action === 'delete') {
226
+ console.error(`[ManageFields] Deleting ${op.schemaName}...`);
227
+ await axios.delete(`${apiBase}/EntityDefinitions(LogicalName='${entityName.toLowerCase()}')/Attributes(LogicalName='${op.schemaName.toLowerCase()}')`, {
228
+ headers: { 'Authorization': `Bearer ${token}`, 'MSCRM.SolutionUniqueName': solutionName }
229
+ });
230
+ report.push(`🗑️ Deleted ${op.schemaName}`);
231
+ }
232
+ } catch (error) {
233
+ const msg = error.response ? JSON.stringify(error.response.data) : error.message;
234
+ report.push(`❌ Failed ${op.action} ${op.schemaName}: ${msg}`);
235
+ }
236
+ }
237
+
238
+
239
+
240
+ console.error(`[ManageFields] Refreshing metadata...`);
241
+ await downloadMetadata(dataverseUrl, token, solutionName);
242
+
243
+ report.push(`ℹ️ Metadata refreshed.`);
244
+
245
+ return { content: [{ type: "text", text: report.join('\n') }] };
246
+ }
@@ -6,17 +6,34 @@ export const toolDefinition = {
6
6
  description: "Force refresh/redownload of the Dataverse metadata",
7
7
  inputSchema: {
8
8
  type: "object",
9
- properties: {},
9
+ properties: {
10
+ solution_name: {
11
+ type: "string",
12
+ description: "Optional solution name to download details for",
13
+ },
14
+ },
10
15
  },
11
16
  };
12
17
 
13
- export async function handleRefreshMetadata(args, dataverseUrl) {
14
- // Note: dataverseUrl needs to be passed in from main server context
18
+ export async function handleRefreshMetadata(args, dataverseUrl, globalSolutionName) {
19
+ const { solution_name } = args;
20
+ const effectiveSolutionName = solution_name || globalSolutionName;
21
+
15
22
  try {
16
23
  const token = await getAccessToken(dataverseUrl);
17
- await downloadMetadata(dataverseUrl, token);
24
+ const { filePath, solutionFound } = await downloadMetadata(dataverseUrl, token, effectiveSolutionName);
25
+
26
+ let message = `Metadata refreshed successfully.`;
27
+ if (effectiveSolutionName) {
28
+ if (solutionFound) {
29
+ message += ` Solution '${effectiveSolutionName}' details downloaded.`;
30
+ } else {
31
+ message += ` WARNING: Solution '${effectiveSolutionName}' was specified but could not be downloaded (not found).`;
32
+ }
33
+ }
34
+
18
35
  return {
19
- content: [{ type: "text", text: "Metadata refreshed successfully." }],
36
+ content: [{ type: "text", text: message }],
20
37
  };
21
38
  } catch (err) {
22
39
  return {
@@ -1,29 +0,0 @@
1
- import { getOptionSetDetails } from '../metadata.js';
2
-
3
- export const toolDefinition = {
4
- name: "get_optionset_details",
5
- description: "Get schema details for a list of Dataverse OptionSets (EnumTypes)",
6
- inputSchema: {
7
- type: "object",
8
- properties: {
9
- optionset_names: {
10
- type: "array",
11
- items: { type: "string" },
12
- description: "List of OptionSet names",
13
- },
14
- },
15
- required: ["optionset_names"],
16
- },
17
- };
18
-
19
- export async function handleGetOptionSetDetails(args) {
20
- const { optionset_names } = args;
21
- const details = await getOptionSetDetails(optionset_names);
22
-
23
- return {
24
- content: [{
25
- type: "text",
26
- text: JSON.stringify(details, null, 2)
27
- }],
28
- };
29
- }