UNPKG

powerplatform-mcp

Version:

PowerPlatform Model Context Protocol server

444 lines (443 loc) 20.2 kB
import { z } from "zod"; const privilegeAssignmentSchema = z.object({ privilegeId: z.string().describe("Privilege ID (GUID) — use list-privileges to discover values"), depth: z.enum(["Basic", "Local", "Deep", "Global"]).describe("Privilege depth: Basic=User, Local=BU, Deep=BU+child, Global=Org"), businessUnitId: z.string().optional().describe("Optional business unit ID for the privilege assignment"), }); /** * Register security role tools with the MCP server. */ export function registerSecurityRoleTools(server, registry) { // Get Security Roles server.registerTool("get-security-roles", { title: "Get Security Roles", description: "Get security roles in the PowerPlatform environment, filtered to unmanaged or customizable roles. Supports solution scoping and optional privilege details.", inputSchema: { solutionUniqueName: z.string().optional().describe("Filter to roles in a specific solution"), excludeSystemRoles: z.boolean().optional().describe("Exclude system roles like System Administrator (default: true)"), maxRecords: z.number().optional().describe("Maximum number of records to return (default: 100)"), includePrivileges: z.boolean().optional().describe("Include privilege details for each role (default: false)"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roles: z.any(), }), }, async ({ solutionUniqueName, excludeSystemRoles, maxRecords, includePrivileges, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); const result = await service.getSecurityRoles({ solutionUniqueName, excludeSystemRoles, maxRecords, includePrivileges }); return { structuredContent: { roles: result.value }, content: [ { type: "text", text: `Found ${result.value.length} security roles:\n\n${JSON.stringify(result.value, null, 2)}`, }, ], }; } catch (error) { console.error("Error getting security roles:", error); return { content: [ { type: "text", text: `Failed to get security roles: ${error.message}`, }, ], }; } }); // Get Security Role Privileges server.registerTool("get-security-role-privileges", { title: "Get Security Role Privileges", description: "Get privileges assigned to a specific security role, optionally filtered by entity or access right", inputSchema: { roleId: z.string().describe("The security role ID (GUID)"), entityFilter: z.string().optional().describe("Filter privileges by entity name (contains match)"), accessRightFilter: z.string().optional().describe("Filter privileges by access right (e.g., Read, Write, Create, Delete)"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), privileges: z.any(), }), }, async ({ roleId, entityFilter, accessRightFilter, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); const result = await service.getSecurityRolePrivileges(roleId, { entityFilter, accessRightFilter }); return { structuredContent: { roleId, privileges: result.value }, content: [ { type: "text", text: `Found ${result.value.length} privileges for role ${roleId}:\n\n${JSON.stringify(result.value, null, 2)}`, }, ], }; } catch (error) { console.error("Error getting security role privileges:", error); return { content: [ { type: "text", text: `Failed to get security role privileges: ${error.message}`, }, ], }; } }); // List Privileges (catalog) server.registerTool("list-privileges", { title: "List Privileges", description: "List the system privilege catalog. Use to discover privilegeId GUIDs and supported depths before assigning privileges to a role.", inputSchema: { entityFilter: z.string().optional().describe("Filter by privilege name contains (Dataverse privileges are named prv{Action}{Entity}, e.g. prvReadAccount)"), accessRightFilter: z.string().optional().describe("Filter by access right (Read, Write, Create, Delete, Append, AppendTo, Assign, Share)"), maxRecords: z.number().optional().describe("Maximum records to return (default: 100)"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ privileges: z.any(), }), }, async ({ entityFilter, accessRightFilter, maxRecords, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); const result = await service.listPrivileges({ entityFilter, accessRightFilter, maxRecords }); return { structuredContent: { privileges: result.value }, content: [ { type: "text", text: `Found ${result.value.length} privileges:\n\n${JSON.stringify(result.value, null, 2)}`, }, ], }; } catch (error) { console.error("Error listing privileges:", error); return { content: [ { type: "text", text: `Failed to list privileges: ${error.message}`, }, ], }; } }); // Create Security Role server.registerTool("create-security-role", { title: "Create Security Role", description: "Create a new security role. If businessUnitId is omitted, the role is created in the root business unit. Pass solutionUniqueName to add it to a specific unmanaged solution in one step.", inputSchema: { name: z.string().describe("Role display name"), businessUnitId: z.string().optional().describe("Business unit ID (GUID). Defaults to the organization's root BU when omitted."), description: z.string().optional().describe("Optional role description"), solutionUniqueName: z.string().optional().describe("Solution unique name to land the role in (uses MSCRM.SolutionUniqueName header)"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), name: z.string(), }), }, async ({ name, businessUnitId, description, solutionUniqueName, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); const result = await service.createSecurityRole({ name, businessUnitId, description, solutionUniqueName }); return { structuredContent: { roleId: result.roleId, name }, content: [ { type: "text", text: `Created security role '${name}' (ID: ${result.roleId})${solutionUniqueName ? ` in solution '${solutionUniqueName}'` : ''}`, }, ], }; } catch (error) { console.error("Error creating security role:", error); return { content: [ { type: "text", text: `Failed to create security role: ${error.message}`, }, ], }; } }); // Clone Security Role server.registerTool("clone-security-role", { title: "Clone Security Role", description: "Clone an existing security role into a new role, copying its privileges. Uses CloneAsRole with a fallback to create-then-copy-privileges if the action is unavailable.", inputSchema: { sourceRoleId: z.string().describe("ID (GUID) of the role to clone from"), newName: z.string().optional().describe("Optional name for the new role (defaults to '<source> (copy)' on fallback path)"), targetBusinessUnitId: z.string().optional().describe("Business unit ID for the new role. Defaults to root BU."), solutionUniqueName: z.string().optional().describe("Solution unique name to land the new role in"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), sourceRoleId: z.string(), }), }, async ({ sourceRoleId, newName, targetBusinessUnitId, solutionUniqueName, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); const result = await service.cloneSecurityRole(sourceRoleId, { newName, targetBusinessUnitId, solutionUniqueName }); return { structuredContent: { roleId: result.roleId, sourceRoleId }, content: [ { type: "text", text: `Cloned role ${sourceRoleId} → new role ${result.roleId}${newName ? ` ('${newName}')` : ''}`, }, ], }; } catch (error) { console.error("Error cloning security role:", error); return { content: [ { type: "text", text: `Failed to clone security role: ${error.message}`, }, ], }; } }); // Update Security Role server.registerTool("update-security-role", { title: "Update Security Role", description: "Update properties of a security role (name, description, business unit). At least one field must be provided.", inputSchema: { roleId: z.string().describe("ID (GUID) of the role to update"), name: z.string().optional().describe("New role name"), description: z.string().optional().describe("New role description"), businessUnitId: z.string().optional().describe("New business unit ID to move the role to"), solutionUniqueName: z.string().optional().describe("Solution unique name (uses MSCRM.SolutionUniqueName header)"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), }), }, async ({ roleId, name, description, businessUnitId, solutionUniqueName, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); await service.updateSecurityRole(roleId, { name, description, businessUnitId, solutionUniqueName }); return { structuredContent: { roleId }, content: [ { type: "text", text: `Updated security role ${roleId}`, }, ], }; } catch (error) { console.error("Error updating security role:", error); return { content: [ { type: "text", text: `Failed to update security role: ${error.message}`, }, ], }; } }); // Delete Security Role server.registerTool("delete-security-role", { title: "Delete Security Role", description: "Delete a security role. Destructive — requires confirm: true to proceed.", inputSchema: { roleId: z.string().describe("ID (GUID) of the role to delete"), confirm: z.boolean().describe("Must be true to actually delete the role"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), deleted: z.boolean(), }), }, async ({ roleId, confirm, environment }) => { try { if (confirm !== true) { return { structuredContent: { roleId, deleted: false }, content: [ { type: "text", text: `Refused to delete role ${roleId}: confirm must be set to true.`, }, ], }; } const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); await service.deleteSecurityRole(roleId); return { structuredContent: { roleId, deleted: true }, content: [ { type: "text", text: `Deleted security role ${roleId}`, }, ], }; } catch (error) { console.error("Error deleting security role:", error); return { content: [ { type: "text", text: `Failed to delete security role: ${error.message}`, }, ], }; } }); // Add Security Role Privileges server.registerTool("add-security-role-privileges", { title: "Add Security Role Privileges", description: "Append privileges to a security role using AddPrivilegesRole. Existing privileges are preserved.", inputSchema: { roleId: z.string().describe("ID (GUID) of the role"), privileges: z.array(privilegeAssignmentSchema).min(1).describe("Privileges to add"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), added: z.number(), }), }, async ({ roleId, privileges, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); await service.addRolePrivileges(roleId, privileges); return { structuredContent: { roleId, added: privileges.length }, content: [ { type: "text", text: `Added ${privileges.length} privilege(s) to role ${roleId}`, }, ], }; } catch (error) { console.error("Error adding role privileges:", error); return { content: [ { type: "text", text: `Failed to add role privileges: ${error.message}`, }, ], }; } }); // Remove Security Role Privileges server.registerTool("remove-security-role-privileges", { title: "Remove Security Role Privileges", description: "Remove privileges from a security role. Loops RemovePrivilegeRole — each privilege is removed in its own call.", inputSchema: { roleId: z.string().describe("ID (GUID) of the role"), privilegeIds: z.array(z.string()).min(1).describe("Privilege IDs (GUIDs) to remove"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), removed: z.number(), }), }, async ({ roleId, privilegeIds, environment }) => { try { const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); await service.removeRolePrivileges(roleId, privilegeIds); return { structuredContent: { roleId, removed: privilegeIds.length }, content: [ { type: "text", text: `Removed ${privilegeIds.length} privilege(s) from role ${roleId}`, }, ], }; } catch (error) { console.error("Error removing role privileges:", error); return { content: [ { type: "text", text: `Failed to remove role privileges: ${error.message}`, }, ], }; } }); // Replace Security Role Privileges server.registerTool("replace-security-role-privileges", { title: "Replace Security Role Privileges", description: "Replace the entire set of privileges on a role with the supplied list (ReplacePrivilegesRole). Destructive — wipes existing privileges.", inputSchema: { roleId: z.string().describe("ID (GUID) of the role"), privileges: z.array(privilegeAssignmentSchema).describe("Full set of privileges to apply (existing ones are wiped)"), confirm: z.boolean().describe("Must be true to apply the destructive replace"), environment: z.string().optional().describe("Environment name (e.g. DEV, UAT). Uses default if omitted."), }, outputSchema: z.object({ roleId: z.string(), applied: z.number(), replaced: z.boolean(), }), }, async ({ roleId, privileges, confirm, environment }) => { try { if (confirm !== true) { return { structuredContent: { roleId, applied: 0, replaced: false }, content: [ { type: "text", text: `Refused to replace privileges on role ${roleId}: confirm must be set to true.`, }, ], }; } const ctx = registry.getContext(environment); const service = ctx.getSecurityRoleService(); await service.replaceRolePrivileges(roleId, privileges); return { structuredContent: { roleId, applied: privileges.length, replaced: true }, content: [ { type: "text", text: `Replaced privileges on role ${roleId} (${privileges.length} now applied)`, }, ], }; } catch (error) { console.error("Error replacing role privileges:", error); return { content: [ { type: "text", text: `Failed to replace role privileges: ${error.message}`, }, ], }; } }); }