arbentia-dataverse-mcp 1.0.8 → 1.0.10
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 +53 -33
- package/index.js +20 -3
- package/metadata.js +48 -159
- package/package.json +1 -1
- package/tools/createEntity.js +131 -0
- package/tools/getGlobalOptionSetDetails.js +30 -4
- package/tools/getLocalOptionSetDetails.js +43 -6
- package/tools/getTablesDetails.js +35 -4
- package/tools/listTablesByName.js +18 -2
- package/tools/manageFields.js +246 -0
- package/tools/refreshMetadata.js +22 -5
- package/tools/getOptionSetDetails.js +0 -29
package/README.md
CHANGED
|
@@ -1,43 +1,50 @@
|
|
|
1
1
|
# Dataverse MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Un servidor [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) para Microsoft Dataverse.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## Características
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
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
|
-
|
|
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
|
-
##
|
|
25
|
+
## Uso
|
|
22
26
|
|
|
23
|
-
###
|
|
27
|
+
### Prerrequisitos
|
|
24
28
|
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
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
|
-
###
|
|
33
|
+
### Ejecución del servidor
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
Puedes ejecutar el servidor directamente usando `npx` o instalándolo globalmente.
|
|
32
36
|
|
|
33
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
67
|
+
**Configuración en Claude Desktop:**
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
Agrega lo siguiente a tu `claude_desktop_config.json`:
|
|
61
70
|
|
|
62
71
|
```json
|
|
63
72
|
{
|
|
@@ -68,25 +77,36 @@ 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
|
-
###
|
|
89
|
+
### Autenticación
|
|
90
|
+
|
|
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.
|
|
92
|
+
|
|
93
|
+
## Herramientas
|
|
94
|
+
|
|
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.
|
|
79
102
|
|
|
80
|
-
|
|
103
|
+
### Aviso sobre Operaciones de Escritura (DDL)
|
|
81
104
|
|
|
82
|
-
|
|
105
|
+
Las herramientas que modifican la estructura de la base de datos (`create_entity` y `manage_fields`) incluyen medidas de seguridad adicionales:
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
-
|
|
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.
|
|
107
|
+
1. **Confirmación Visual**: Antes de aplicar cualquier cambio, el servidor abrirá automáticamente una página en tu navegador predeterminado mostrando el detalle de la operación (tablas a crear, campos a añadir/borrar). Deberás aprobar la operación en esa página para que continúe.
|
|
108
|
+
2. **Re-autenticación**: Para prevenir cambios accidentales o no supervisados, es posible que el sistema solicite autenticación interactiva (abrir ventana de login) para cada operación de escritura.
|
|
89
109
|
|
|
90
|
-
##
|
|
110
|
+
## Licencia
|
|
91
111
|
|
|
92
112
|
MIT
|
package/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import * as listTablesByName from './tools/listTablesByName.js';
|
|
|
21
21
|
import * as getTablesDetails from './tools/getTablesDetails.js';
|
|
22
22
|
import * as getGlobalOptionSetDetails from './tools/getGlobalOptionSetDetails.js';
|
|
23
23
|
import * as getLocalOptionSetDetails from './tools/getLocalOptionSetDetails.js';
|
|
24
|
+
import * as createEntity from './tools/createEntity.js';
|
|
25
|
+
import * as manageFields from './tools/manageFields.js';
|
|
24
26
|
|
|
25
27
|
// Parse arguments
|
|
26
28
|
const argv = yargs(hideBin(process.argv))
|
|
@@ -30,10 +32,16 @@ const argv = yargs(hideBin(process.argv))
|
|
|
30
32
|
description: 'Dataverse Environment URL',
|
|
31
33
|
demandOption: true
|
|
32
34
|
})
|
|
35
|
+
.option('solution', {
|
|
36
|
+
alias: 's',
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Optional Dataverse solution name'
|
|
39
|
+
})
|
|
33
40
|
.help()
|
|
34
41
|
.argv;
|
|
35
42
|
|
|
36
43
|
const dataverseUrl = argv.url;
|
|
44
|
+
const solutionName = argv.solution;
|
|
37
45
|
|
|
38
46
|
// Initialize Server
|
|
39
47
|
const server = new Server(
|
|
@@ -96,6 +104,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
96
104
|
getTablesDetails.toolDefinition,
|
|
97
105
|
getGlobalOptionSetDetails.toolDefinition,
|
|
98
106
|
getLocalOptionSetDetails.toolDefinition,
|
|
107
|
+
createEntity.toolDefinition,
|
|
108
|
+
manageFields.toolDefinition,
|
|
99
109
|
],
|
|
100
110
|
};
|
|
101
111
|
});
|
|
@@ -105,7 +115,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
105
115
|
|
|
106
116
|
try {
|
|
107
117
|
if (name === refreshMetadata.toolDefinition.name) {
|
|
108
|
-
return await refreshMetadata.handleRefreshMetadata(args, dataverseUrl);
|
|
118
|
+
return await refreshMetadata.handleRefreshMetadata(args, dataverseUrl, solutionName);
|
|
109
119
|
}
|
|
110
120
|
if (name === listTablesByName.toolDefinition.name) {
|
|
111
121
|
return await listTablesByName.handleListTablesByName(args, dataverseUrl);
|
|
@@ -119,6 +129,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
119
129
|
if (name === getLocalOptionSetDetails.toolDefinition.name) {
|
|
120
130
|
return await getLocalOptionSetDetails.handleGetLocalOptionSetDetails(args, dataverseUrl);
|
|
121
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
|
+
}
|
|
122
138
|
} catch (err) {
|
|
123
139
|
return {
|
|
124
140
|
content: [{ type: "text", text: `Error executing tool ${name}: ${err.message}` }],
|
|
@@ -135,10 +151,11 @@ async function main() {
|
|
|
135
151
|
await server.connect(transport);
|
|
136
152
|
console.error("MCP Server running on stdio");
|
|
137
153
|
|
|
138
|
-
|
|
154
|
+
|
|
139
155
|
console.error("Server started. Metadata will be loaded on demand.");
|
|
140
|
-
ARBDownloadInstructions();
|
|
141
156
|
|
|
157
|
+
|
|
158
|
+
ARBDownloadInstructions();
|
|
142
159
|
}
|
|
143
160
|
async function ARBDownloadInstructions() {
|
|
144
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
|
-
|
|
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
|
|
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
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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(
|
|
52
|
+
text: JSON.stringify(result, null, 2)
|
|
27
53
|
}],
|
|
28
54
|
};
|
|
29
55
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import {
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
66
|
+
text: JSON.stringify(result, null, 2)
|
|
30
67
|
}],
|
|
31
68
|
};
|
|
32
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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(
|
|
62
|
+
text: JSON.stringify(result, null, 2)
|
|
32
63
|
}],
|
|
33
64
|
};
|
|
34
65
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
+
}
|
package/tools/refreshMetadata.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
}
|