Skip to main content
Glama

Dataverse MCP Server

by mwhesse
relationship-tools.ts18.5 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { DataverseClient } from "../dataverse-client.js"; import { OneToManyRelationshipMetadata, ManyToManyRelationshipMetadata, ODataResponse, LocalizedLabel } from "../types.js"; // Helper function to create localized labels function createLocalizedLabel(text: string, languageCode: number = 1033): LocalizedLabel { return { LocalizedLabels: [ { Label: text, LanguageCode: languageCode, IsManaged: false, MetadataId: "00000000-0000-0000-0000-000000000000" } ], UserLocalizedLabel: { Label: text, LanguageCode: languageCode, IsManaged: false, MetadataId: "00000000-0000-0000-0000-000000000000" } }; } export function createRelationshipTool(server: McpServer, client: DataverseClient) { server.registerTool( "create_dataverse_relationship", { title: "Create Dataverse Relationship", description: "Creates a relationship between two Dataverse tables. Supports One-to-Many relationships (parent-child with lookup field) and Many-to-Many relationships (junction table). Use this to establish data connections between tables, enable navigation, and maintain referential integrity.", inputSchema: { relationshipType: z.enum(["OneToMany", "ManyToMany"]).describe("Type of relationship to create"), schemaName: z.string().describe("Schema name for the relationship (e.g., 'new_account_contact')"), // One-to-Many specific fields referencedEntity: z.string().optional().describe("Referenced (parent) entity logical name for One-to-Many relationships"), referencingEntity: z.string().optional().describe("Referencing (child) entity logical name for One-to-Many relationships"), referencingAttributeLogicalName: z.string().optional().describe("Logical name for the lookup attribute to be created"), referencingAttributeDisplayName: z.string().optional().describe("Display name for the lookup attribute"), // Many-to-Many specific fields entity1LogicalName: z.string().optional().describe("First entity logical name for Many-to-Many relationships"), entity2LogicalName: z.string().optional().describe("Second entity logical name for Many-to-Many relationships"), intersectEntityName: z.string().optional().describe("Name for the intersect entity (auto-generated if not provided)"), // Cascade configuration for One-to-Many cascadeAssign: z.enum(["NoCascade", "Cascade", "Active", "UserOwned", "RemoveLink", "Restrict"]).default("NoCascade").describe("Cascade behavior for assign operations"), cascadeDelete: z.enum(["NoCascade", "Cascade", "Active", "UserOwned", "RemoveLink", "Restrict"]).default("RemoveLink").describe("Cascade behavior for delete operations"), cascadeMerge: z.enum(["NoCascade", "Cascade", "Active", "UserOwned", "RemoveLink", "Restrict"]).default("NoCascade").describe("Cascade behavior for merge operations"), cascadeReparent: z.enum(["NoCascade", "Cascade", "Active", "UserOwned", "RemoveLink", "Restrict"]).default("NoCascade").describe("Cascade behavior for reparent operations"), cascadeShare: z.enum(["NoCascade", "Cascade", "Active", "UserOwned", "RemoveLink", "Restrict"]).default("NoCascade").describe("Cascade behavior for share operations"), cascadeUnshare: z.enum(["NoCascade", "Cascade", "Active", "UserOwned", "RemoveLink", "Restrict"]).default("NoCascade").describe("Cascade behavior for unshare operations"), // Associated menu configuration menuBehavior: z.enum(["UseCollectionName", "UseLabel", "DoNotDisplay"]).default("UseCollectionName").describe("How the relationship appears in associated menus"), menuGroup: z.enum(["Details", "Sales", "Service", "Marketing"]).default("Details").describe("Menu group for the relationship"), menuLabel: z.string().optional().describe("Custom label for the menu (required if menuBehavior is UseLabel)"), menuOrder: z.number().optional().describe("Order in the menu"), isValidForAdvancedFind: z.boolean().default(true).describe("Whether the relationship is valid for Advanced Find"), isHierarchical: z.boolean().default(false).describe("Whether this is a hierarchical relationship (One-to-Many only)") } }, async (params) => { try { if (params.relationshipType === "OneToMany") { if (!params.referencedEntity || !params.referencingEntity || !params.referencingAttributeLogicalName || !params.referencingAttributeDisplayName) { throw new Error("For One-to-Many relationships, referencedEntity, referencingEntity, referencingAttributeLogicalName, and referencingAttributeDisplayName are required"); } const cascadeConfig = { Assign: getCascadeValue(params.cascadeAssign), Delete: getCascadeValue(params.cascadeDelete), Merge: getCascadeValue(params.cascadeMerge), Reparent: getCascadeValue(params.cascadeReparent), Share: getCascadeValue(params.cascadeShare), Unshare: getCascadeValue(params.cascadeUnshare), RollupView: "NoCascade" }; const menuConfig = { Behavior: getMenuBehaviorValue(params.menuBehavior), Group: getMenuGroupValue(params.menuGroup), Label: params.menuLabel ? createLocalizedLabel(params.menuLabel) : undefined, Order: params.menuOrder }; const relationshipDefinition = { "@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata", SchemaName: params.schemaName, ReferencedEntity: params.referencedEntity, ReferencingEntity: params.referencingEntity, CascadeConfiguration: cascadeConfig, AssociatedMenuConfiguration: menuConfig, IsValidForAdvancedFind: params.isValidForAdvancedFind, IsHierarchical: params.isHierarchical, IsCustomRelationship: true, Lookup: { "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata", LogicalName: params.referencingAttributeLogicalName, SchemaName: params.referencingAttributeLogicalName.charAt(0).toUpperCase() + params.referencingAttributeLogicalName.slice(1), DisplayName: createLocalizedLabel(params.referencingAttributeDisplayName), RequiredLevel: { Value: "None", CanBeChanged: true, ManagedPropertyLogicalName: "canmodifyrequirementlevelsettings" }, Targets: [params.referencedEntity], IsCustomAttribute: true } }; const result = await client.postMetadata("RelationshipDefinitions", relationshipDefinition); return { content: [ { type: "text", text: `Successfully created One-to-Many relationship '${params.schemaName}' between '${params.referencedEntity}' and '${params.referencingEntity}'.\n\nResponse: ${JSON.stringify(result, null, 2)}` } ] }; } else { // ManyToMany if (!params.entity1LogicalName || !params.entity2LogicalName) { throw new Error("For Many-to-Many relationships, entity1LogicalName and entity2LogicalName are required"); } const intersectName = params.intersectEntityName || `${params.entity1LogicalName}_${params.entity2LogicalName}`; const menuConfig1 = { Behavior: getMenuBehaviorValue(params.menuBehavior), Group: getMenuGroupValue(params.menuGroup), Label: params.menuLabel ? createLocalizedLabel(params.menuLabel) : undefined, Order: params.menuOrder }; const menuConfig2 = { Behavior: getMenuBehaviorValue(params.menuBehavior), Group: getMenuGroupValue(params.menuGroup), Label: params.menuLabel ? createLocalizedLabel(params.menuLabel) : undefined, Order: params.menuOrder }; const relationshipDefinition = { "@odata.type": "Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata", SchemaName: params.schemaName, Entity1LogicalName: params.entity1LogicalName, Entity1AssociatedMenuConfiguration: menuConfig1, Entity2LogicalName: params.entity2LogicalName, Entity2AssociatedMenuConfiguration: menuConfig2, IntersectEntityName: intersectName, IsValidForAdvancedFind: params.isValidForAdvancedFind, IsCustomRelationship: true }; const result = await client.postMetadata("RelationshipDefinitions", relationshipDefinition); return { content: [ { type: "text", text: `Successfully created Many-to-Many relationship '${params.schemaName}' between '${params.entity1LogicalName}' and '${params.entity2LogicalName}'.\n\nResponse: ${JSON.stringify(result, null, 2)}` } ] }; } } catch (error) { return { content: [ { type: "text", text: `Error creating relationship: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); } function getCascadeValue(cascade: string): string { // Return the string value directly instead of numeric return cascade; } function getMenuBehaviorValue(behavior: string): string { // Return the string value directly instead of numeric return behavior; } function getMenuGroupValue(group: string): string { // Return the string value directly instead of numeric return group; } export function getRelationshipTool(server: McpServer, client: DataverseClient) { server.registerTool( "get_dataverse_relationship", { title: "Get Dataverse Relationship", description: "Retrieves detailed information about a specific relationship between Dataverse tables, including its configuration, cascade settings, and menu behavior. Use this to inspect relationship definitions and understand table connections.", inputSchema: { schemaName: z.string().describe("Schema name of the relationship to retrieve") } }, async (params) => { try { const result = await client.getMetadata( `RelationshipDefinitions(SchemaName='${params.schemaName}')` ); return { content: [ { type: "text", text: `Relationship information for '${params.schemaName}':\n\n${JSON.stringify(result, null, 2)}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving relationship: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); } export function deleteRelationshipTool(server: McpServer, client: DataverseClient) { server.registerTool( "delete_dataverse_relationship", { title: "Delete Dataverse Relationship", description: "Permanently deletes a relationship between Dataverse tables. WARNING: This action cannot be undone and will remove the connection between tables, including any lookup fields for One-to-Many relationships. Use with extreme caution.", inputSchema: { schemaName: z.string().describe("Schema name of the relationship to delete") } }, async (params) => { try { await client.deleteMetadata(`RelationshipDefinitions(SchemaName='${params.schemaName}')`); return { content: [ { type: "text", text: `Successfully deleted relationship '${params.schemaName}'.` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error deleting relationship: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); } export function listRelationshipsTool(server: McpServer, client: DataverseClient) { server.registerTool( "list_dataverse_relationships", { title: "List Dataverse Relationships", description: "Retrieves a list of relationships in the Dataverse environment with filtering options. Use this to discover table connections, find custom relationships, or get an overview of the data model relationships. Supports filtering by entity, relationship type, and managed/unmanaged status.", inputSchema: { entityLogicalName: z.string().optional().describe("Filter relationships for a specific entity"), relationshipType: z.enum(["OneToMany", "ManyToMany", "All"]).default("All").describe("Type of relationships to list"), customOnly: z.boolean().default(false).describe("Whether to list only custom relationships"), includeManaged: z.boolean().default(false).describe("Whether to include managed relationships"), filter: z.string().optional().describe("OData filter expression") } }, async (params) => { try { let allRelationships: (OneToManyRelationshipMetadata | ManyToManyRelationshipMetadata)[] = []; // Build base filters const baseFilters = []; if (params.customOnly) { baseFilters.push("IsCustomRelationship eq true"); } if (!params.includeManaged) { baseFilters.push("IsManaged eq false"); } if (params.filter) { baseFilters.push(params.filter); } // Handle different relationship type scenarios using the correct cast syntax if (params.relationshipType === "OneToMany" || params.relationshipType === "All") { // Query OneToMany relationships using cast syntax const oneToManyFilters = [...baseFilters]; if (params.entityLogicalName) { oneToManyFilters.push(`(ReferencedEntity eq '${params.entityLogicalName}' or ReferencingEntity eq '${params.entityLogicalName}')`); } const oneToManyParams: Record<string, any> = { $select: "SchemaName,RelationshipType,IsCustomRelationship,IsManaged,IsValidForAdvancedFind,ReferencedEntity,ReferencingEntity,ReferencingAttribute,IsHierarchical" }; if (oneToManyFilters.length > 0) { oneToManyParams.$filter = oneToManyFilters.join(" and "); } const oneToManyResult = await client.getMetadata<ODataResponse<OneToManyRelationshipMetadata>>( "RelationshipDefinitions/Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata", oneToManyParams ); allRelationships.push(...oneToManyResult.value); } if (params.relationshipType === "ManyToMany" || params.relationshipType === "All") { // Query ManyToMany relationships using cast syntax const manyToManyFilters = [...baseFilters]; if (params.entityLogicalName) { manyToManyFilters.push(`(Entity1LogicalName eq '${params.entityLogicalName}' or Entity2LogicalName eq '${params.entityLogicalName}')`); } const manyToManyParams: Record<string, any> = { $select: "SchemaName,RelationshipType,IsCustomRelationship,IsManaged,IsValidForAdvancedFind,Entity1LogicalName,Entity2LogicalName,IntersectEntityName" }; if (manyToManyFilters.length > 0) { manyToManyParams.$filter = manyToManyFilters.join(" and "); } const manyToManyResult = await client.getMetadata<ODataResponse<ManyToManyRelationshipMetadata>>( "RelationshipDefinitions/Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata", manyToManyParams ); allRelationships.push(...manyToManyResult.value); } // Note: $top parameter is not supported by Dataverse metadata endpoints const relationshipList = allRelationships.map(relationship => { // Determine relationship type based on the presence of specific properties // rather than the RelationshipType enum value const isOneToMany = 'ReferencedEntity' in relationship && 'ReferencingEntity' in relationship; const relationshipType = isOneToMany ? "OneToMany" : "ManyToMany"; const baseInfo = { schemaName: relationship.SchemaName, relationshipType: relationshipType, isCustom: relationship.IsCustomRelationship, isManaged: relationship.IsManaged, isValidForAdvancedFind: relationship.IsValidForAdvancedFind }; if (isOneToMany) { // OneToMany const oneToMany = relationship as OneToManyRelationshipMetadata; return { ...baseInfo, referencedEntity: oneToMany.ReferencedEntity, referencingEntity: oneToMany.ReferencingEntity, referencingAttribute: oneToMany.ReferencingAttribute, isHierarchical: oneToMany.IsHierarchical }; } else { // ManyToMany const manyToMany = relationship as ManyToManyRelationshipMetadata; return { ...baseInfo, entity1LogicalName: manyToMany.Entity1LogicalName, entity2LogicalName: manyToMany.Entity2LogicalName, intersectEntityName: manyToMany.IntersectEntityName }; } }); return { content: [ { type: "text", text: `Found ${relationshipList.length} relationships${params.entityLogicalName ? ` for entity '${params.entityLogicalName}'` : ''}:\n\n${JSON.stringify(relationshipList, null, 2)}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error listing relationships: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mwhesse/dataverse-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server