arbentia-dataverse-mcp 1.0.2

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 ADDED
@@ -0,0 +1,73 @@
1
+ # Dataverse MCP Server
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for Microsoft Dataverse.
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).
6
+
7
+ ## Features
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.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g dataverse-mcp
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Prerequisites
24
+
25
+ - A Microsoft Dataverse environment.
26
+ - A user account with permissions to read metadata.
27
+ - An MCP Client.
28
+
29
+ ### Running the Server
30
+
31
+ You can run the server directly using `npx` or by installing it globally.
32
+
33
+ **Using npx:**
34
+
35
+ ```bash
36
+ npx dataverse-mcp --url "https://your-org.crm.dynamics.com"
37
+ ```
38
+
39
+ **Configuration in Claude Desktop:**
40
+
41
+ Add the following to your `claude_desktop_config.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "dataverse": {
47
+ "command": "npx",
48
+ "args": [
49
+ "-y",
50
+ "dataverse-mcp",
51
+ "--url",
52
+ "https://your-org.crm.dynamics.com"
53
+ ]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Authentication
60
+
61
+ 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.
62
+
63
+ ## Tools
64
+
65
+ - `refresh_metadata`: Redownloads the metadata cache.
66
+ - `list_tables_by_name`: Lists tables matching a regex pattern.
67
+ - `get_tables_details`: Detailed schema for given tables. Supports filtering by `Fields`, `Relationships`, or `Keys`.
68
+ - `get_global_optionset_details`: Details for Global OptionSets.
69
+ - `get_local_optionset_details`: Details for Local OptionSets (StringMaps) for specific entities.
70
+
71
+ ## License
72
+
73
+ MIT
package/auth.js ADDED
@@ -0,0 +1,38 @@
1
+ import { InteractiveBrowserCredential } from "@azure/identity";
2
+
3
+ // Constants
4
+ const AZURE_CLI_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
5
+ const COMMON_TENANT_ID = "common";
6
+
7
+ /**
8
+ * Gets an access token for the specified Dataverse URL.
9
+ * @param {string} dataverseUrl The URL of the Dataverse environment (e.g., https://org.crm.dynamics.com).
10
+ * @returns {Promise<string>} The access token.
11
+ */
12
+ export async function getAccessToken(dataverseUrl) {
13
+ console.error(`[Auth] Authenticating against ${dataverseUrl}...`); // Stderr for logs so stdout is clean for MCP
14
+
15
+ // Ensure URL is clean for scope
16
+ const url = new URL(dataverseUrl);
17
+ const scope = `${url.origin}/user_impersonation`; // Or .default
18
+
19
+ try {
20
+ const credential = new InteractiveBrowserCredential({
21
+ clientId: AZURE_CLI_CLIENT_ID,
22
+ tenantId: COMMON_TENANT_ID,
23
+ redirectUri: "http://localhost:8400", // Azure CLI often uses this or similar
24
+ });
25
+
26
+ const tokenResponse = await credential.getToken([scope]);
27
+
28
+ if (!tokenResponse) {
29
+ throw new Error("Failed to acquire token.");
30
+ }
31
+
32
+ console.error(`[Auth] Successfully authenticated.`);
33
+ return tokenResponse.token;
34
+ } catch (error) {
35
+ console.error(`[Auth] Error: ${error.message}`);
36
+ throw error;
37
+ }
38
+ }
package/bin/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../index.js';
package/index.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListResourcesRequestSchema,
7
+ ListToolsRequestSchema,
8
+ ReadResourceRequestSchema,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import yargs from 'yargs/yargs';
11
+ import { hideBin } from 'yargs/helpers';
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ import { getAccessToken } from './auth.js';
15
+ import { ensureMetadata } from './metadata.js';
16
+
17
+ // Tool Imports
18
+ import * as refreshMetadata from './tools/refreshMetadata.js';
19
+ import * as listTablesByName from './tools/listTablesByName.js';
20
+ import * as getTablesDetails from './tools/getTablesDetails.js';
21
+ import * as getGlobalOptionSetDetails from './tools/getGlobalOptionSetDetails.js';
22
+ import * as getLocalOptionSetDetails from './tools/getLocalOptionSetDetails.js';
23
+
24
+ // Parse arguments
25
+ const argv = yargs(hideBin(process.argv))
26
+ .option('url', {
27
+ alias: 'u',
28
+ type: 'string',
29
+ description: 'Dataverse Environment URL',
30
+ demandOption: true
31
+ })
32
+ .help()
33
+ .argv;
34
+
35
+ const dataverseUrl = argv.url;
36
+
37
+ // Initialize Server
38
+ const server = new Server(
39
+ {
40
+ name: "dataverse-mcp",
41
+ version: "1.0.0",
42
+ },
43
+ {
44
+ capabilities: {
45
+ resources: {},
46
+ tools: {},
47
+ },
48
+ }
49
+ );
50
+
51
+ // Resource for metadata
52
+ const METADATA_RESOURCE_URI = "dataverse://metadata";
53
+
54
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
55
+ return {
56
+ resources: [
57
+ {
58
+ uri: METADATA_RESOURCE_URI,
59
+ name: "Dataverse Metadata",
60
+ mimeType: "text/xml",
61
+ description: "The full OData $metadata XML definition for the Dataverse environment",
62
+ },
63
+ ],
64
+ };
65
+ });
66
+
67
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
68
+ if (request.params.uri === METADATA_RESOURCE_URI) {
69
+ const filePath = path.resolve(process.cwd(), '.dataversemetadata', 'metadata.xml');
70
+
71
+ if (!await fs.pathExists(filePath)) {
72
+ throw new Error("Metadata file not found. It might be downloading or failed.");
73
+ }
74
+
75
+ const content = await fs.readFile(filePath, 'utf-8');
76
+ return {
77
+ contents: [
78
+ {
79
+ uri: METADATA_RESOURCE_URI,
80
+ mimeType: "text/xml",
81
+ text: content,
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ throw new Error("Resource not found");
87
+ });
88
+
89
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
90
+ return {
91
+ tools: [
92
+ refreshMetadata.toolDefinition,
93
+ listTablesByName.toolDefinition,
94
+ getTablesDetails.toolDefinition,
95
+ getGlobalOptionSetDetails.toolDefinition,
96
+ getLocalOptionSetDetails.toolDefinition,
97
+ ],
98
+ };
99
+ });
100
+
101
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
102
+ const { name, arguments: args } = request.params;
103
+
104
+ try {
105
+ if (name === refreshMetadata.toolDefinition.name) {
106
+ return await refreshMetadata.handleRefreshMetadata(args, dataverseUrl);
107
+ }
108
+ if (name === listTablesByName.toolDefinition.name) {
109
+ return await listTablesByName.handleListTablesByName(args);
110
+ }
111
+ if (name === getTablesDetails.toolDefinition.name) {
112
+ return await getTablesDetails.handleGetTablesDetails(args);
113
+ }
114
+ if (name === getGlobalOptionSetDetails.toolDefinition.name) {
115
+ return await getGlobalOptionSetDetails.handleGetGlobalOptionSetDetails(args);
116
+ }
117
+ if (name === getLocalOptionSetDetails.toolDefinition.name) {
118
+ return await getLocalOptionSetDetails.handleGetLocalOptionSetDetails(args);
119
+ }
120
+ } catch (err) {
121
+ return {
122
+ content: [{ type: "text", text: `Error executing tool ${name}: ${err.message}` }],
123
+ isError: true,
124
+ };
125
+ }
126
+
127
+ throw new Error("Tool not found");
128
+ });
129
+
130
+ // Startup logic
131
+ async function main() {
132
+ const transport = new StdioServerTransport();
133
+ await server.connect(transport);
134
+ console.error("MCP Server running on stdio");
135
+
136
+ // Trigger initial download (or ensure existence) in background
137
+ console.error("Server started. Metadata will be loaded on demand.");
138
+ }
139
+
140
+ main().catch((error) => {
141
+ console.error("Fatal error:", error);
142
+ process.exit(1);
143
+ });
package/metadata.js ADDED
@@ -0,0 +1,277 @@
1
+ import axios from 'axios';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { XMLParser } from 'fast-xml-parser';
5
+
6
+ let cachedParsedData = null;
7
+
8
+ // Helper to get parser
9
+ function getParser() {
10
+ return new XMLParser({
11
+ ignoreAttributes: false,
12
+ attributeNamePrefix: "@_"
13
+ });
14
+ }
15
+
16
+ /**
17
+ * Ensures metadata exists. If not, downloads it.
18
+ * @param {string} dataverseUrl
19
+ * @param {string} token
20
+ */
21
+ export async function ensureMetadata(dataverseUrl, token) {
22
+ const cacheDir = path.resolve(process.cwd(), '.dataversemetadata');
23
+ const filePath = path.join(cacheDir, 'metadata.xml');
24
+
25
+ if (await fs.pathExists(filePath)) {
26
+ console.error(`[Metadata] Metadata already exists at ${filePath}. Skipping download.`);
27
+ return filePath;
28
+ }
29
+
30
+ return await downloadMetadata(dataverseUrl, token);
31
+ }
32
+
33
+ /**
34
+ * Downloads the Dataverse $metadata file and saves it locally.
35
+ * @param {string} dataverseUrl The Dataverse environment URL.
36
+ * @param {string} token The access token.
37
+ */
38
+ export async function downloadMetadata(dataverseUrl, token) {
39
+ const url = new URL(dataverseUrl);
40
+ const baseUrl = url.origin;
41
+ const metadataUrl = `${baseUrl}/api/data/v9.2/$metadata`;
42
+ const cacheDir = path.resolve(process.cwd(), '.dataversemetadata');
43
+ const filePath = path.join(cacheDir, 'metadata.xml');
44
+
45
+ console.error(`[Metadata] Downloading from ${metadataUrl}...`);
46
+
47
+ try {
48
+ const response = await axios.get(metadataUrl, {
49
+ headers: {
50
+ 'Authorization': `Bearer ${token}`,
51
+ 'Accept': 'application/xml',
52
+ },
53
+ responseType: 'text'
54
+ });
55
+
56
+ await fs.ensureDir(cacheDir);
57
+ await fs.writeFile(filePath, response.data);
58
+ console.error(`[Metadata] Saved to ${filePath}`);
59
+
60
+ await downloadStringMaps(baseUrl, token);
61
+
62
+ // Invalidate cache on new download
63
+ cachedParsedData = null;
64
+
65
+ return filePath;
66
+ } catch (error) {
67
+ console.error(`[Metadata] Download failed: ${error.message}`);
68
+ if (error.response) {
69
+ console.error(`[Metadata] Server responded with: ${error.response.status} ${error.response.statusText}`);
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ async function downloadStringMaps(baseUrl, token) {
76
+ const records = [];
77
+ let nextLink = `${baseUrl}/api/data/v9.2/stringmaps?$select=objecttypecode,attributename,attributevalue,value,displayorder`;
78
+
79
+ console.error(`[Metadata] Downloading StringMaps...`);
80
+
81
+ while (nextLink) {
82
+ try {
83
+ const response = await axios.get(nextLink, {
84
+ headers: {
85
+ 'Authorization': `Bearer ${token}`,
86
+ 'Accept': 'application/json',
87
+ 'Prefer': 'odata.include-annotations="*"'
88
+ }
89
+ });
90
+
91
+ if (response.data.value) {
92
+ records.push(...response.data.value);
93
+ }
94
+
95
+ nextLink = response.data['@odata.nextLink'];
96
+ if (nextLink) console.error(`[Metadata] Fetching next page of StringMaps...`);
97
+
98
+ } catch (error) {
99
+ console.error(`[Metadata] Failed to download StringMaps page: ${error.message}`);
100
+ // Decide if we should fail the whole process or just log warning.
101
+ // For now, let's log and break to save what we have.
102
+ break;
103
+ }
104
+ }
105
+
106
+ const cacheDir = path.resolve(process.cwd(), '.dataversemetadata');
107
+ const filePath = path.join(cacheDir, 'stringmaps.json');
108
+ await fs.writeJson(filePath, records);
109
+ console.error(`[Metadata] Saved ${records.length} StringMaps to ${filePath}`);
110
+ }
111
+
112
+ async function getParsedMetadata() {
113
+ if (cachedParsedData) return cachedParsedData;
114
+
115
+ const cacheDir = path.resolve(process.cwd(), '.dataversemetadata');
116
+ const filePath = path.join(cacheDir, 'metadata.xml');
117
+
118
+ if (!await fs.pathExists(filePath)) {
119
+ throw new Error("Metadata file not found. Please run the 'refresh_metadata' tool to download it.");
120
+ }
121
+
122
+ console.error(`[Metadata] Parsing metadata from ${filePath}...`);
123
+ const xmlData = await fs.readFile(filePath, 'utf-8');
124
+ const parser = getParser();
125
+ cachedParsedData = parser.parse(xmlData);
126
+ return cachedParsedData;
127
+ }
128
+
129
+ /**
130
+ * Returns a list of EntityType names.
131
+ * @returns {Promise<string[]>} List of entity names.
132
+ */
133
+ export async function getEntities() {
134
+ const parsed = await getParsedMetadata();
135
+ const entities = [];
136
+
137
+ const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
138
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
139
+
140
+ for (const schema of schemaList) {
141
+ if (schema.EntityType) {
142
+ const entityTypes = Array.isArray(schema.EntityType) ? schema.EntityType : [schema.EntityType];
143
+ for (const et of entityTypes) {
144
+ if (et['@_Name']) {
145
+ entities.push(et['@_Name']);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return entities;
151
+ }
152
+
153
+ /**
154
+ * Returns details for valid entities.
155
+ * @param {string[]} tableNames
156
+ * @param {'Fields'|'Relationships'|'Keys'|'All'} detailType
157
+ */
158
+ export async function getEntityDetails(tableNames, detailType = 'All') {
159
+ const parsed = await getParsedMetadata();
160
+ const result = {};
161
+ const lowerNames = tableNames.map(n => n.toLowerCase());
162
+
163
+ const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
164
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
165
+
166
+ for (const schema of schemaList) {
167
+ if (schema.EntityType) {
168
+ const entityTypes = Array.isArray(schema.EntityType) ? schema.EntityType : [schema.EntityType];
169
+ for (const et of entityTypes) {
170
+ if (et['@_Name'] && lowerNames.includes(et['@_Name'].toLowerCase())) {
171
+ const cleanEt = { ...et };
172
+ delete cleanEt['Action'];
173
+ delete cleanEt['Function'];
174
+
175
+ if (detailType !== 'All') {
176
+ const keepProps = [];
177
+ if (detailType === 'Fields') keepProps.push('Property');
178
+ if (detailType === 'Relationships') keepProps.push('NavigationProperty');
179
+ if (detailType === 'Keys') keepProps.push('Key');
180
+
181
+ if (!keepProps.includes('Property')) delete cleanEt['Property'];
182
+ if (!keepProps.includes('NavigationProperty')) delete cleanEt['NavigationProperty'];
183
+ if (!keepProps.includes('Key')) delete cleanEt['Key'];
184
+ }
185
+ result[et['@_Name']] = cleanEt;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return result;
191
+ }
192
+
193
+ /**
194
+ * Returns details for valid Global OptionSets (EnumTypes).
195
+ * @param {string[]} optionSetNames
196
+ */
197
+ export async function getGlobalOptionSetDetails(optionSetNames) {
198
+ const parsed = await getParsedMetadata();
199
+ const result = {};
200
+ const lowerNames = optionSetNames.map(n => n.toLowerCase());
201
+
202
+ const schemas = parsed['edmx:Edmx']['edmx:DataServices']['Schema'];
203
+ const schemaList = Array.isArray(schemas) ? schemas : [schemas];
204
+
205
+ for (const schema of schemaList) {
206
+ if (schema.EnumType) {
207
+ const enumTypes = Array.isArray(schema.EnumType) ? schema.EnumType : [schema.EnumType];
208
+ for (const et of enumTypes) {
209
+ if (!et['@_Name']) continue;
210
+
211
+ const name = et['@_Name'].toLowerCase();
212
+ const namespace = schema['@_Namespace'] ? schema['@_Namespace'].toLowerCase() : "";
213
+ const fullName = namespace ? `${namespace}.${name}` : name;
214
+
215
+ const matchedRequest = lowerNames.find(req => req === name || req === fullName);
216
+
217
+ if (matchedRequest) {
218
+ const key = schema['@_Namespace'] ? `${schema['@_Namespace']}.${et['@_Name']}` : et['@_Name'];
219
+ result[key] = et;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ return result;
225
+ }
226
+
227
+ /**
228
+ * Returns details for Local OptionSets from stringmaps.
229
+ * @param {Object.<string, string[]>} requestMap Map of entity logical name to array of attribute names.
230
+ */
231
+ export async function getLocalOptionSetDetails(requestMap) {
232
+ const cacheDir = path.resolve(process.cwd(), '.dataversemetadata');
233
+ const stringMapsPath = path.join(cacheDir, 'stringmaps.json');
234
+ const result = {};
235
+
236
+ if (!await fs.pathExists(stringMapsPath)) {
237
+ console.warn("[Metadata] stringmaps.json not found.");
238
+ return result;
239
+ }
240
+
241
+ try {
242
+ const stringMaps = await fs.readJson(stringMapsPath);
243
+
244
+ // requestMap structure: { "account": ["statuscode", "industrycode"], "contact": ["..."] }
245
+ // Normalize request keys to lowercase
246
+ const normalizedRequest = {};
247
+ for (const [ent, attrs] of Object.entries(requestMap)) {
248
+ normalizedRequest[ent.toLowerCase()] = attrs.map(a => a.toLowerCase());
249
+ }
250
+
251
+ // Iterate stringmaps
252
+ for (const sm of stringMaps) {
253
+ // "objecttypecode" is expected to be the entity logical name (string) based on user input.
254
+ // If it's not, we might fail here, but we rely on user's instruction.
255
+ const entityName = (sm['objecttypecode'] || '').toLowerCase();
256
+ const attrName = (sm['attributename'] || '').toLowerCase();
257
+
258
+ if (normalizedRequest[entityName] && normalizedRequest[entityName].includes(attrName)) {
259
+ if (!result[entityName]) result[entityName] = {};
260
+ if (!result[entityName][attrName]) result[entityName][attrName] = [];
261
+
262
+ result[entityName][attrName].push({
263
+ Display: sm['value'],
264
+ OptionSetValue: sm['attributevalue']
265
+ });
266
+ }
267
+ }
268
+
269
+ // Sort options by something? Maybe displayorder if available, but user didn't ask.
270
+ // Let's ensure integer parsing for OptionSetValue if it's a string?
271
+ // stringmaps json usually has numbers for attributevalue.
272
+ } catch (e) {
273
+ console.error(`[Metadata] Error reading/processing stringmaps: ${e}`);
274
+ }
275
+
276
+ return result;
277
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "arbentia-dataverse-mcp",
3
+ "version": "1.0.2",
4
+ "description": "Model Context Protocol (MCP) server for Microsoft Dataverse Metadata",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "arbentia-dataverse-mcp": "./bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin/cli.js",
12
+ "index.js",
13
+ "auth.js",
14
+ "metadata.js",
15
+ "tools/*.js"
16
+ ],
17
+ "scripts": {
18
+ "start": "node index.js"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "dataverse",
23
+ "dynamics-365",
24
+ "metadata",
25
+ "model-context-protocol"
26
+ ],
27
+ "author": "Arbentia",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/arbentia/dataverse-mcp.git"
32
+ },
33
+ "dependencies": {
34
+ "@azure/identity": "^4.0.0",
35
+ "@modelcontextprotocol/sdk": "^1.25.0",
36
+ "axios": "^1.6.0",
37
+ "fast-xml-parser": "^5.3.3",
38
+ "fs-extra": "^11.2.0",
39
+ "yargs": "^17.7.2"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ }
44
+ }
@@ -0,0 +1,29 @@
1
+ import { getGlobalOptionSetDetails } from '../metadata.js';
2
+
3
+ export const toolDefinition = {
4
+ name: "get_global_optionset_details",
5
+ description: "Get schema details for a list of Dataverse Global 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 (e.g. ['prv_budgettypes', 'account_statuscode'])",
13
+ },
14
+ },
15
+ required: ["optionset_names"],
16
+ },
17
+ };
18
+
19
+ export async function handleGetGlobalOptionSetDetails(args) {
20
+ const { optionset_names } = args;
21
+ const details = await getGlobalOptionSetDetails(optionset_names);
22
+
23
+ return {
24
+ content: [{
25
+ type: "text",
26
+ text: JSON.stringify(details, null, 2)
27
+ }],
28
+ };
29
+ }
@@ -0,0 +1,32 @@
1
+ import { getLocalOptionSetDetails } from '../metadata.js';
2
+
3
+ export const toolDefinition = {
4
+ name: "get_local_optionset_details",
5
+ description: "Get details for local OptionSets (StringMaps) for specified entities and attributes.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ request_map: {
10
+ type: "object",
11
+ additionalProperties: {
12
+ type: "array",
13
+ items: { type: "string" }
14
+ },
15
+ description: "Map of entity logical names to lists of attribute names. Example: { 'account': ['statuscode', 'industrycode'] }",
16
+ },
17
+ },
18
+ required: ["request_map"],
19
+ },
20
+ };
21
+
22
+ export async function handleGetLocalOptionSetDetails(args) {
23
+ const { request_map } = args;
24
+ const details = await getLocalOptionSetDetails(request_map);
25
+
26
+ return {
27
+ content: [{
28
+ type: "text",
29
+ text: JSON.stringify(details, null, 2)
30
+ }],
31
+ };
32
+ }
@@ -0,0 +1,34 @@
1
+ import { getEntityDetails } from '../metadata.js';
2
+
3
+ export const toolDefinition = {
4
+ name: "get_tables_details",
5
+ description: "Get full schema details for a list of Dataverse tables (entities).",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ table_names: {
10
+ type: "array",
11
+ items: { type: "string" },
12
+ description: "List of logical table names (e.g. ['account', 'contact'])",
13
+ },
14
+ detail_type: {
15
+ type: "string",
16
+ enum: ["Fields", "Relationships", "Keys", "All"],
17
+ description: "Optional filter for details. Defaults to 'All'.",
18
+ },
19
+ },
20
+ required: ["table_names"],
21
+ },
22
+ };
23
+
24
+ export async function handleGetTablesDetails(args) {
25
+ const { table_names, detail_type } = args;
26
+ const details = await getEntityDetails(table_names, detail_type);
27
+
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: JSON.stringify(details, null, 2)
32
+ }],
33
+ };
34
+ }
@@ -0,0 +1,48 @@
1
+ import { getEntities } from '../metadata.js';
2
+
3
+ export const toolDefinition = {
4
+ name: "list_tables_by_name",
5
+ description: "Search for Dataverse tables (entities) by name using a regex pattern. Returns a paginated list of matching entity names.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ name_pattern: {
10
+ type: "string",
11
+ description: "Regex pattern to match entity names (case insensitive)",
12
+ },
13
+ page: {
14
+ type: "number",
15
+ description: "Page number (1-based, default: 1)",
16
+ },
17
+ size: {
18
+ type: "number",
19
+ description: "Page size (default: 50)",
20
+ },
21
+ },
22
+ required: ["name_pattern"],
23
+ },
24
+ };
25
+
26
+ export async function handleListTablesByName(args) {
27
+ const { name_pattern, page = 1, size = 50 } = args;
28
+ const entities = await getEntities();
29
+
30
+ const regex = new RegExp(name_pattern, 'i');
31
+ const matches = entities.filter(e => regex.test(e));
32
+
33
+ // Pagination
34
+ const startIndex = (page - 1) * size;
35
+ const pagedResults = matches.slice(startIndex, startIndex + size);
36
+
37
+ return {
38
+ content: [{
39
+ type: "text",
40
+ text: JSON.stringify({
41
+ page,
42
+ total_matches: matches.length,
43
+ total_pages: Math.ceil(matches.length / size),
44
+ results: pagedResults
45
+ }, null, 2)
46
+ }],
47
+ };
48
+ }
@@ -0,0 +1,27 @@
1
+ import { downloadMetadata } from '../metadata.js';
2
+ import { getAccessToken } from '../auth.js';
3
+
4
+ export const toolDefinition = {
5
+ name: "refresh_metadata",
6
+ description: "Force refresh/redownload of the Dataverse metadata",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {},
10
+ },
11
+ };
12
+
13
+ export async function handleRefreshMetadata(args, dataverseUrl) {
14
+ // Note: dataverseUrl needs to be passed in from main server context
15
+ try {
16
+ const token = await getAccessToken(dataverseUrl);
17
+ await downloadMetadata(dataverseUrl, token);
18
+ return {
19
+ content: [{ type: "text", text: "Metadata refreshed successfully." }],
20
+ };
21
+ } catch (err) {
22
+ return {
23
+ content: [{ type: "text", text: `Failed to refresh metadata: ${err.message}` }],
24
+ isError: true,
25
+ };
26
+ }
27
+ }