create_dataverse_relationship
Create relationships between Dataverse tables to establish data connections, enable navigation, and maintain referential integrity. Supports One-to-Many and Many-to-Many relationship types.
Instructions
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.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| cascadeAssign | No | Cascade behavior for assign operations | NoCascade |
| cascadeDelete | No | Cascade behavior for delete operations | RemoveLink |
| cascadeMerge | No | Cascade behavior for merge operations | NoCascade |
| cascadeReparent | No | Cascade behavior for reparent operations | NoCascade |
| cascadeShare | No | Cascade behavior for share operations | NoCascade |
| cascadeUnshare | No | Cascade behavior for unshare operations | NoCascade |
| entity1LogicalName | No | First entity logical name for Many-to-Many relationships | |
| entity2LogicalName | No | Second entity logical name for Many-to-Many relationships | |
| intersectEntityName | No | Name for the intersect entity (auto-generated if not provided) | |
| isHierarchical | No | Whether this is a hierarchical relationship (One-to-Many only) | |
| isValidForAdvancedFind | No | Whether the relationship is valid for Advanced Find | |
| menuBehavior | No | How the relationship appears in associated menus | UseCollectionName |
| menuGroup | No | Menu group for the relationship | Details |
| menuLabel | No | Custom label for the menu (required if menuBehavior is UseLabel) | |
| menuOrder | No | Order in the menu | |
| referencedEntity | No | Referenced (parent) entity logical name for One-to-Many relationships | |
| referencingAttributeDisplayName | No | Display name for the lookup attribute | |
| referencingAttributeLogicalName | No | Logical name for the lookup attribute to be created | |
| referencingEntity | No | Referencing (child) entity logical name for One-to-Many relationships | |
| relationshipType | Yes | Type of relationship to create | |
| schemaName | Yes | Schema name for the relationship (e.g., 'new_account_contact') |
Implementation Reference
- src/tools/relationship-tools.ts:65-180 (handler)The async handler function implementing the core logic for creating One-to-Many or Many-to-Many relationships in Dataverse by posting metadata definitions via the DataverseClient.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 }; } }
- Zod schema defining the input parameters for the tool, including relationship type, entity names, attributes, cascade behaviors, menu settings, and other configurations.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)") }
- src/tools/relationship-tools.ts:27-181 (registration)The server.registerTool call within createRelationshipTool that registers the 'create_dataverse_relationship' tool with the MCP server, including title, description, schema, and handler.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 }; } } );
- src/index.ts:25-153 (registration)Import of createRelationshipTool and its invocation in the main server initialization, which triggers the tool registration.createRelationshipTool, getRelationshipTool, deleteRelationshipTool, listRelationshipsTool } from "./tools/relationship-tools.js"; import { createOptionSetTool, getOptionSetTool, updateOptionSetTool, deleteOptionSetTool, listOptionSetsTool, getOptionSetOptionsTool } from "./tools/optionset-tools.js"; import { createPublisherTool, createSolutionTool, getSolutionTool, getPublisherTool, listSolutionsTool, listPublishersTool, setSolutionContextTool, getSolutionContextTool, clearSolutionContextTool } from "./tools/solution-tools.js"; import { createRoleTool, getRoleTool, updateRoleTool, deleteRoleTool, listRolesTool, addPrivilegesToRoleTool, removePrivilegeFromRoleTool, replaceRolePrivilegesTool, getRolePrivilegesTool, assignRoleToUserTool, removeRoleFromUserTool, assignRoleToTeamTool, removeRoleFromTeamTool } from "./tools/role-tools.js"; import { createTeamTool, getTeamTool, updateTeamTool, deleteTeamTool, listTeamsTool, addMembersToTeamTool, removeMembersFromTeamTool, getTeamMembersTool, convertOwnerTeamToAccessTeamTool } from "./tools/team-tools.js"; import { createBusinessUnitTool, getBusinessUnitTool, updateBusinessUnitTool, deleteBusinessUnitTool, listBusinessUnitsTool, getBusinessUnitHierarchyTool, setBusinessUnitParentTool, getBusinessUnitUsersTool, getBusinessUnitTeamsTool } from "./tools/businessunit-tools.js"; import { exportSolutionSchemaTool, generateMermaidDiagramTool } from "./tools/schema-tools.js"; import { generateWebAPICallTool } from "./tools/webapi-tools.js"; import { generatePowerPagesWebAPICallTool } from "./tools/powerpages-webapi-tools.js"; import { managePowerPagesWebAPIConfigTool } from "./tools/powerpages-config-tools.js"; import { createAutoNumberColumnTool, updateAutoNumberFormatTool, setAutoNumberSeedTool, getAutoNumberColumnTool, listAutoNumberColumnsTool, convertToAutoNumberTool } from "./tools/autonumber-tools.js"; import { registerWebAPIResources } from "./resources/webapi-resources.js"; import { registerPowerPagesResources } from "./resources/powerpages-resources.js"; // Environment variables for Dataverse authentication const DATAVERSE_URL = process.env.DATAVERSE_URL; const CLIENT_ID = process.env.DATAVERSE_CLIENT_ID; const CLIENT_SECRET = process.env.DATAVERSE_CLIENT_SECRET; const TENANT_ID = process.env.DATAVERSE_TENANT_ID; if (!DATAVERSE_URL || !CLIENT_ID || !CLIENT_SECRET || !TENANT_ID) { throw new Error('Missing required environment variables: DATAVERSE_URL, DATAVERSE_CLIENT_ID, DATAVERSE_CLIENT_SECRET, DATAVERSE_TENANT_ID'); } // Create MCP server const server = new McpServer({ name: "dataverse-mcp", version: "0.2.2" }); // Initialize Dataverse client const dataverseClient = new DataverseClient({ dataverseUrl: DATAVERSE_URL, clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, tenantId: TENANT_ID }); // Register table tools createTableTool(server, dataverseClient); getTableTool(server, dataverseClient); updateTableTool(server, dataverseClient); deleteTableTool(server, dataverseClient); listTablesTool(server, dataverseClient); // Register column tools createColumnTool(server, dataverseClient); getColumnTool(server, dataverseClient); updateColumnTool(server, dataverseClient); deleteColumnTool(server, dataverseClient); listColumnsTool(server, dataverseClient); // Register relationship tools createRelationshipTool(server, dataverseClient);
- src/tools/relationship-tools.ts:7-23 (helper)Helper function to create standardized LocalizedLabel objects used in relationship metadata definitions for display names and 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" } };