diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index 602d2ba91ed..1851a2aa09a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -290,7 +290,7 @@ public class OpenMetadataApplication extends Application void setOwners(T entity, Map params) { + + List owners = getTeamsOrUsers(params.get("owners")); + + if (!owners.isEmpty()) { + entity.setOwners(owners); + } + } + + public static List getTeamsOrUsers(Object teamsOrUsersParam) { + UserRepository userRepository = Entity.getUserRepository(); + TeamRepository teamRepository = (TeamRepository) Entity.getEntityRepository(Entity.TEAM); + List teamsOrUsers = new java.util.ArrayList<>(); + + for (String owner : JsonUtils.readOrConvertValues(teamsOrUsersParam, String.class)) { + try { + User user = userRepository.findByNameOrNull(owner, Include.NON_DELETED); + if (user == null) { + // If the owner is not a user, check if it's a team + Team team = teamRepository.findByNameOrNull(owner, Include.NON_DELETED); + if (team != null) { + teamsOrUsers.add(team.getEntityReference()); + } + } else { + // If the owner is a user, add their reference + teamsOrUsers.add(user.getEntityReference()); + } + } catch (Exception e) { + LOG.error(String.format("Could not add teams or users '%s' due to", e.getMessage()), e); + } + } + return teamsOrUsers; + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTermTool.java b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTermTool.java index 8841fbfb392..4676e37826f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTermTool.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTermTool.java @@ -1,25 +1,23 @@ package org.openmetadata.service.mcp.tools; import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; -import static org.openmetadata.service.Entity.ADMIN_USER_NAME; import java.util.HashMap; -import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.entity.data.Glossary; import org.openmetadata.schema.entity.data.GlossaryTerm; -import org.openmetadata.schema.entity.teams.User; -import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; +import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.service.Entity; -import org.openmetadata.service.exception.EntityNotFoundException; import org.openmetadata.service.jdbi3.GlossaryRepository; import org.openmetadata.service.jdbi3.GlossaryTermRepository; -import org.openmetadata.service.jdbi3.UserRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.glossary.GlossaryTermMapper; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.auth.CatalogSecurityContext; +import org.openmetadata.service.security.policyevaluator.CreateResourceContext; +import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.util.JsonUtils; import org.openmetadata.service.util.RestUtil; @@ -30,47 +28,51 @@ public class GlossaryTermTool implements McpTool { @Override public Map execute( Authorizer authorizer, CatalogSecurityContext securityContext, Map params) { - // TODO:Use the securityContext to validate permissions + throw new UnsupportedOperationException("GlossaryTermTool requires limit validation."); + } + + @Override + public Map execute( + Authorizer authorizer, + Limits limits, + CatalogSecurityContext securityContext, + Map params) { org.openmetadata.schema.api.data.CreateGlossaryTerm createGlossaryTerm = new org.openmetadata.schema.api.data.CreateGlossaryTerm(); - createGlossaryTerm.setGlossary((String) params.get("glossary")); createGlossaryTerm.setName((String) params.get("name")); + createGlossaryTerm.setGlossary((String) params.get("glossary")); + createGlossaryTerm.setParent((String) params.get("parentTerm")); createGlossaryTerm.setDescription((String) params.get("description")); - UserRepository userRepository = Entity.getUserRepository(); - List owners = new java.util.ArrayList<>(); - // TODO: Deal with Teams vs Users if (params.containsKey("owners")) { - for (String owner : JsonUtils.readOrConvertValues(params.get("owners"), String.class)) { - try { - User user = userRepository.findByName(owner, Include.NON_DELETED); - owners.add(user.getEntityReference()); - } catch (EntityNotFoundException e) { - LOG.error(String.format("User '%s' not found", owner)); - Map error = new HashMap<>(); - error.put("error", e.getMessage()); - return error; - } - } + CommonUtils.setOwners(createGlossaryTerm, params); } - if (!owners.isEmpty()) { - createGlossaryTerm.setOwners(owners); - } + GlossaryTerm glossaryTerm = + glossaryTermMapper.createToEntity( + createGlossaryTerm, securityContext.getUserPrincipal().getName()); + + // Validate If the User Can Perform the Create Operation + OperationContext operationContext = + new OperationContext(Entity.GLOSSARY_TERM, MetadataOperation.CREATE); + CreateResourceContext createResourceContext = + new CreateResourceContext<>(Entity.GLOSSARY_TERM, glossaryTerm); + limits.enforceLimits(securityContext, createResourceContext, operationContext); + authorizer.authorize(securityContext, operationContext, createResourceContext); try { GlossaryRepository glossaryRepository = (GlossaryRepository) Entity.getEntityRepository(Entity.GLOSSARY); Glossary glossary = glossaryRepository.findByNameOrNull(createGlossaryTerm.getGlossary(), Include.ALL); - GlossaryTerm glossaryTerm = - glossaryTermMapper.createToEntity(createGlossaryTerm, ADMIN_USER_NAME); + GlossaryTermRepository glossaryTermRepository = (GlossaryTermRepository) Entity.getEntityRepository(Entity.GLOSSARY_TERM); // TODO: Get the updatedBy from the tool request. glossaryTermRepository.prepare(glossaryTerm, nullOrEmpty(glossary)); glossaryTermRepository.setFullyQualifiedName(glossaryTerm); RestUtil.PutResponse response = - glossaryTermRepository.createOrUpdate(null, glossaryTerm, "admin"); + glossaryTermRepository.createOrUpdate( + null, glossaryTerm, securityContext.getUserPrincipal().getName()); return JsonUtils.convertValue(response.getEntity(), Map.class); } catch (Exception e) { Map error = new HashMap<>(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTool.java b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTool.java new file mode 100644 index 00000000000..bce61770d02 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/GlossaryTool.java @@ -0,0 +1,82 @@ +package org.openmetadata.service.mcp.tools; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.openmetadata.schema.api.data.CreateGlossary; +import org.openmetadata.schema.entity.data.Glossary; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.MetadataOperation; +import org.openmetadata.service.Entity; +import org.openmetadata.service.jdbi3.GlossaryRepository; +import org.openmetadata.service.limits.Limits; +import org.openmetadata.service.resources.glossary.GlossaryMapper; +import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.security.auth.CatalogSecurityContext; +import org.openmetadata.service.security.policyevaluator.CreateResourceContext; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.util.JsonUtils; +import org.openmetadata.service.util.RestUtil; + +@Slf4j +public class GlossaryTool implements McpTool { + private static GlossaryMapper glossaryMapper = new GlossaryMapper(); + + @Override + public Map execute( + Authorizer authorizer, CatalogSecurityContext securityContext, Map params) { + throw new UnsupportedOperationException("GlossaryTermTool requires limit validation."); + } + + @Override + public Map execute( + Authorizer authorizer, + Limits limits, + CatalogSecurityContext securityContext, + Map params) { + CreateGlossary createGlossary = new CreateGlossary(); + createGlossary.setName((String) params.get("name")); + createGlossary.setDescription((String) params.get("description")); + if (params.containsKey("owners")) { + CommonUtils.setOwners(createGlossary, params); + } + if (params.containsKey("reviewers")) { + setReviewers(createGlossary, params); + } + + Glossary glossary = + glossaryMapper.createToEntity(createGlossary, securityContext.getUserPrincipal().getName()); + + // Validate If the User Can Perform the Create Operation + OperationContext operationContext = + new OperationContext(Entity.GLOSSARY, MetadataOperation.CREATE); + CreateResourceContext createResourceContext = + new CreateResourceContext<>(Entity.GLOSSARY, glossary); + limits.enforceLimits(securityContext, createResourceContext, operationContext); + authorizer.authorize(securityContext, operationContext, createResourceContext); + + try { + GlossaryRepository glossaryRepository = + (GlossaryRepository) Entity.getEntityRepository(Entity.GLOSSARY); + + glossaryRepository.prepare(glossary, true); + glossaryRepository.setFullyQualifiedName(glossary); + RestUtil.PutResponse response = + glossaryRepository.createOrUpdate( + null, glossary, securityContext.getUserPrincipal().getName()); + return JsonUtils.convertValue(response.getEntity(), Map.class); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return error; + } + } + + public static void setReviewers(CreateGlossary entity, Map params) { + List reviewers = CommonUtils.getTeamsOrUsers(params.get("reviewers")); + if (!reviewers.isEmpty()) { + entity.setReviewers(reviewers); + } + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/McpTool.java b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/McpTool.java index 5592f7fa6f4..7fdb82ad890 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/McpTool.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/McpTool.java @@ -1,10 +1,17 @@ package org.openmetadata.service.mcp.tools; import java.util.Map; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.auth.CatalogSecurityContext; public interface McpTool { Map execute( Authorizer authorizer, CatalogSecurityContext securityContext, Map params); + + Map execute( + Authorizer authorizer, + Limits limits, + CatalogSecurityContext securityContext, + Map params); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/PatchEntityTool.java b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/PatchEntityTool.java index d9af2016c17..3ee91adc894 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/PatchEntityTool.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/mcp/tools/PatchEntityTool.java @@ -7,6 +7,7 @@ import org.openmetadata.schema.EntityInterface; import org.openmetadata.schema.type.change.ChangeSource; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.EntityRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.auth.CatalogSecurityContext; import org.openmetadata.service.security.policyevaluator.OperationContext; @@ -30,7 +31,21 @@ public class PatchEntityTool implements McpTool { EntityRepository repository = Entity.getEntityRepository(entityType); RestUtil.PatchResponse response = - repository.patch(null, entityFqn, "admin", patch, ChangeSource.MANUAL); + repository.patch( + null, + entityFqn, + securityContext.getUserPrincipal().getName(), + patch, + ChangeSource.MANUAL); return JsonUtils.convertValue(response, Map.class); } + + @Override + public Map execute( + Authorizer authorizer, + Limits limits, + CatalogSecurityContext securityContext, + Map params) { + throw new UnsupportedOperationException("PatchEntityTool does not support limits enforcement."); + } } diff --git a/openmetadata-service/src/main/resources/json/data/mcp/tools.json b/openmetadata-service/src/main/resources/json/data/mcp/tools.json index 336229faf35..80135739855 100644 --- a/openmetadata-service/src/main/resources/json/data/mcp/tools.json +++ b/openmetadata-service/src/main/resources/json/data/mcp/tools.json @@ -49,7 +49,7 @@ }, { "name": "create_glossary_term", - "description": "Creates a new Glossary Term", + "description": "Creates a new Glossary Term. Note that a glossary term must be part of a Glossary, so the glossary must be specified in the parameters. If you can't find the right glossary to use, respond back to the user to create a new Glossary first. Note that you can help the user to create the Glossary as well. If you don't find any Glossary that could be related, please list to the user the available Glossaries so users can choose if they want to create or reuse something. Also, note that glossary terms can be hierarchical: for example, a glossary term 'Accounts' can have a child term 'Credit Account', 'Savings Account', etc. So if you find any terms that can be related, it might make sense to create a new term as a child of an existing term.", "parameters": { "type": "object", "properties": { @@ -57,6 +57,10 @@ "type": "string", "description": "Glossary in which the term belongs. This should be its fully qualified name." }, + "parentTerm": { + "type": "string", + "description": "Optional parent term for the new term. This should be its fully qualified name defined as .. If the Glossary Term has other parents, the Fully Qualified Name will be ..... If not provided, the term will be created at the root level of the glossary." + }, "name": { "type": "string", "description": "Glossary Term name." @@ -67,7 +71,7 @@ }, "owners": { "type": "array", - "description": "Glossary Term owner. This should be an OpenMetadata User", + "description": "Glossary Term owner. This could be an OpenMetadata User or Team. If you don't know who the owner is, you can leave this empty, but let the user know that they can add owners later.", "items": { "type": "string" } @@ -80,6 +84,47 @@ ] } }, + { + "name": "create_glossary", + "description": "Creates a new Glossary. A Glossary is a collection of terms that are used to define the business vocabulary of an organization. Typically, similar terms are grouped together in a Glossary. For example, a Glossary names 'Marketing' could contain terms like 'Campaign', 'Lead', 'Opportunity', etc.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Glossary Term name." + }, + "description": { + "type": "string", + "description": "Glossary Term description." + }, + "owners": { + "type": "array", + "description": "Glossary Term owner. This could be an OpenMetadata User or Team. If you don't know who the owner is, you can leave this empty, but let the user know that they can add owners later.", + "items": { + "type": "string" + } + }, + "reviewers": { + "type": "array", + "description": "Glossary Term owner. This could be an OpenMetadata User or Team. If you don't know who the owner is, you can leave this empty, but let the user know that they can add owners later.", + "items": { + "type": "string" + } + }, + "mutuallyExclusive": { + "type": "boolean", + "description": "Glossary terms that are direct children in this glossary are mutually exclusive. When mutually exclusive is `true` only one term can be used to tag an entity. When mutually exclusive is `false`, multiple terms from this group can be used to tag an entity. This is an important setting. If you are not sure, ask the user to clarify. If the user doesn't know, set it to `false`.", + "default": false + } + }, + "required": [ + "name", + "description", + "mutuallyExclusive" + ] + } + }, { "name": "patch_entity", "description": "Patches an Entity based on a JSONPatch. Beforehand the Entity should be validated by finding it and creating a proper patch.", diff --git a/openmetadata-spec/src/main/resources/json/schema/api/mcp/mcpTools.json b/openmetadata-spec/src/main/resources/json/schema/api/mcp/mcpTools.json deleted file mode 100644 index d732a2db88d..00000000000 --- a/openmetadata-spec/src/main/resources/json/schema/api/mcp/mcpTools.json +++ /dev/null @@ -1,352 +0,0 @@ -{ - "$id": "https://open-metadata.org/schema/api/mcp/mcpTools.json", - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MCP Tools", - "description": "Central definition of all tools available in the Model Context Protocol", - "type": "object", - "javaType": "org.openmetadata.schema.api.mcp.MCPTools", - "definitions": { - "searchMetadata": { - "description": "Search for metadata entities in OpenMetadata", - "type": "object", - "properties": { - "tool": { - "type": "object", - "properties": { - "name": { - "type": "string", - "default": "search_metadata" - }, - "description": { - "type": "string", - "default": "Search for metadata entities in OpenMetadata based on keywords or phrases" - }, - "parameters": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "object" - }, - "properties": { - "type": "object", - "properties": { - "query": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "The search query or keywords to find relevant metadata" - } - } - }, - "entity_type": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Optional entity type to filter results (e.g., 'table', 'dashboard', 'topic')" - } - } - }, - "limit": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "integer" - }, - "description": { - "type": "string", - "default": "Maximum number of results to return (default: 10)" - } - } - } - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - }, - "default": ["query"] - } - } - } - } - } - } - }, - "getEntityDetails": { - "description": "Get details about a specific entity", - "type": "object", - "properties": { - "tool": { - "type": "object", - "properties": { - "name": { - "type": "string", - "default": "get_entity_details" - }, - "description": { - "type": "string", - "default": "Get detailed information about a specific entity when you know its fully qualified name (FQN)" - }, - "parameters": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "object" - }, - "properties": { - "type": "object", - "properties": { - "entity_type": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "The type of entity (e.g., 'table', 'dashboard', 'topic')" - } - } - }, - "fqn": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "The fully qualified name of the entity" - } - } - } - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - }, - "default": ["entity_type", "fqn"] - } - } - } - } - } - } - }, - "nlqSearch": { - "description": "Search with natural language", - "type": "object", - "properties": { - "tool": { - "type": "object", - "properties": { - "name": { - "type": "string", - "default": "nlq_search" - }, - "description": { - "type": "string", - "default": "Search OpenMetadata using natural language queries" - }, - "parameters": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "object" - }, - "properties": { - "type": "object", - "properties": { - "query": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Natural language query" - } - } - }, - "entity_type": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Entity type to search in (default: table)" - } - } - }, - "limit": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "integer" - }, - "description": { - "type": "string", - "default": "Maximum number of results to return (default: 10)" - } - } - } - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - }, - "default": ["query"] - } - } - } - } - } - } - }, - "advancedSearch": { - "description": "Advanced search with filters", - "type": "object", - "properties": { - "tool": { - "type": "object", - "properties": { - "name": { - "type": "string", - "default": "advanced_search" - }, - "description": { - "type": "string", - "default": "Perform advanced search with multiple filters and conditions" - }, - "parameters": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "object" - }, - "properties": { - "type": "object", - "properties": { - "query": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Base search query" - } - } - }, - "entity_type": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Entity type to search in" - } - } - }, - "filters": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "object" - }, - "description": { - "type": "string", - "default": "Additional filters to apply (key-value pairs)" - } - } - }, - "sort_field": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Field to sort results by" - } - } - }, - "sort_order": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "string" - }, - "description": { - "type": "string", - "default": "Sort order (asc or desc)" - } - } - }, - "limit": { - "type": "object", - "properties": { - "type": { - "type": "string", - "default": "integer" - }, - "description": { - "type": "string", - "default": "Maximum number of results to return (default: 10)" - } - } - } - } - }, - "required": { - "type": "array", - "items": { - "type": "string" - }, - "default": ["query"] - } - } - } - } - } - } - } - } -} \ No newline at end of file