Skip to main content
Glama

Core Content Services MCP Server

Official
by ibm-ecm
folders.py20.8 kB
# Licensed Materials - Property of IBM (c) Copyright IBM Corp. 2025 All Rights Reserved. # US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with # IBM Corp. # DISCLAIMER OF WARRANTIES : # Permission is granted to copy and modify this Sample code, and to distribute modified versions provided that both the # copyright notice, and this permission notice and warranty disclaimer appear in all copies and modified versions. # THIS SAMPLE CODE IS LICENSED TO YOU AS-IS. IBM AND ITS SUPPLIERS AND LICENSORS DISCLAIM ALL WARRANTIES, EITHER # EXPRESS OR IMPLIED, IN SUCH SAMPLE CODE, INCLUDING THE WARRANTY OF NON-INFRINGEMENT AND THE IMPLIED WARRANTIES OF # MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT WILL IBM OR ITS LICENSORS OR SUPPLIERS BE LIABLE FOR # ANY DAMAGES ARISING OUT OF THE USE OF OR INABILITY TO USE THE SAMPLE CODE, DISTRIBUTION OF THE SAMPLE CODE, OR # COMBINATION OF THE SAMPLE CODE WITH ANY OTHER CODE. IN NO EVENT SHALL IBM OR ITS LICENSORS AND SUPPLIERS BE LIABLE # FOR ANY LOST REVENUE, LOST PROFITS OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE # DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, EVEN IF IBM OR ITS LICENSORS OR SUPPLIERS HAVE # BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. import logging import re import traceback import uuid from typing import Optional, Union from mcp.server.fastmcp import FastMCP from cs_mcp_server.client import GraphQLClient from cs_mcp_server.utils import ToolError from cs_mcp_server.utils.model.core import Document, Folder from cs_mcp_server.utils.model.coreInput import FolderPropertiesInput from cs_mcp_server.utils.constants import ( DEFAULT_FOLDER_CLASS, TRACEBACK_LIMIT, ) # Logger for this module logger = logging.getLogger(__name__) def register_folder_tools(mcp: FastMCP, graphql_client: GraphQLClient) -> None: @mcp.tool( name="create_folder", ) def create_folder( name: str, parent_folder: str, class_identifier: Optional[str] = None, id: Optional[str] = None, ) -> Union[Folder, ToolError]: """ Creates a folder in the content repository with specified properties. This tool interfaces with the GraphQL API to create a new folder object with the provided parameters. :param name string Yes The name of the folder to be created. :param parent_folder string Yes The identifier of the parent folder where this folder will be created. :param class_identifier string No The class identifier for the folder. If not provided, defaults to "Folder". :param id string No The unique identifier for the folder. If not provided, a new UUID with curly braces will be generated (format: {uuid}). :returns: If successful, return a folder object with the following properties: id: The identifier of the created folder name: The name of the folder parent_folder: The identifier of the parent folder creator: The user who created the folder class_identifier: The class identifier of the folder Else, return a ToolError instance that describes the error. """ method_name = "create_folder" try: # default class and id if not class_identifier: class_identifier = DEFAULT_FOLDER_CLASS if not id: id = "{" + str(uuid.uuid4()) + "}" mutation = """ mutation createFolder($repo:String!, $name:String!, $id:ID! $className:String, $parent:String!) { createFolder(repositoryIdentifier: $repo, classIdentifier:$className, id: $id folderProperties: { name:$name, parent: { identifier:$parent } } ) { id properties { id value } } } """ var = { "repo": graphql_client.object_store, "parent": parent_folder, "name": name, "id": id, "className": class_identifier, } response = graphql_client.execute(query=mutation, variables=var) # handling exception, for example duplicate folder name if "errors" in response: return ToolError( message=f"create_folder failed: got err {response}.", ) # return response["data"]["createFolder"] return Folder.create_an_instance( graphQL_changed_object_dict=response["data"]["createFolder"], class_identifier=class_identifier, ) except Exception as e: error_traceback = traceback.format_exc(limit=TRACEBACK_LIMIT) logger.error( f"{method_name} failed: {e.__class__.__name__} - {str(e)}\n{error_traceback}" ) return ToolError( message=f"{method_name} failed: got err {e}. Trace available in server logs.", ) @mcp.tool( name="delete_folder", ) def delete_folder(id_or_path: str) -> Union[str, ToolError]: """ Deletes a folder in the content repository. This tool interfaces with the GraphQL API to delete a folder object with the provided id. :param id_or_path string Yes The unique identifier or path for the folder. If not provided, an error will be returned. :returns: If successful, return the folder id. Else, return a ToolError instance that describes the error. """ method_name = "delete_folder" try: # dcheck id or path if not id_or_path: return ToolError( message=f"delete_folder failed: id is a required input.", ) mutation = """ mutation deleteFolder( $id_or_path:String! $repo: String!) { deleteFolder(repositoryIdentifier: $repo, identifier: $id_or_path ) { id } } """ var = { "repo": graphql_client.object_store, "id_or_path": id_or_path, } response = graphql_client.execute(query=mutation, variables=var) # handling exception, for example duplicate folder name if "errors" in response: return ToolError( message=f"delete_folder failed: got err {response}.", ) return_id = response["data"]["deleteFolder"]["id"] return return_id except Exception as e: error_traceback = traceback.format_exc(limit=TRACEBACK_LIMIT) logger.error( f"{method_name} failed: {e.__class__.__name__} - {str(e)}\n{error_traceback}" ) return ToolError( message=f"{method_name} failed: got err {e}. Trace available in server logs.", ) @mcp.tool( name="unfile_document", ) async def unfile_document( folder_id_or_path: str, document_id: str ) -> Union[str, ToolError]: """ Unfile a document from a folder in the content repository. This tool interfaces with the GraphQL API to unfile document from folder with the provided ids. :param folder_id_or_path string Yes The unique identifier or path for the folder. If not provided, an error will be returned. :param document_id string Yes The unique identifier for the document. If not provided, an error will be returned. :returns: If successful, return the folder id. Else, return a ToolError instance that describes the error. """ method_name = "unfile_document" try: # check folder id or path and documetn id if not folder_id_or_path: return ToolError( message=f"unfile_document failed: folder id or path is a required input.", ) if not document_id: return ToolError( message=f"unfile_document failed: document id is a required input.", ) mutation = """ query rcr($repo:String!,$where_clause: String!) { repositoryObjects(repositoryIdentifier:$repo from: "ReferentialContainmentRelationship" where : $where_clause) { independentObjects { ... on ReferentialContainmentRelationship { id tail { id } head { id } } } } } """ formatted_folder_value = "" if is_guid_with_braces(folder_id_or_path): formatted_folder_value = f"({folder_id_or_path})" else: formatted_folder_value = lookup_folder_id( folder_name=folder_id_or_path, graphql_client=graphql_client ) if type(formatted_folder_value) is ToolError: return formatted_folder_value formatted_document_value = f"({document_id})" condition_string = ( f"tail = {formatted_folder_value} and head = {formatted_document_value}" ) var = { "repo": graphql_client.object_store, "where_clause": condition_string, } response = graphql_client.execute(query=mutation, variables=var) # handling exception if "errors" in response: return ToolError( message=f"unfile_document failed: got err {response}.", ) return_rcr = response["data"]["repositoryObjects"]["independentObjects"] return_id = "" if len(return_rcr) > 0: return_id = return_rcr[0]["id"] else: return ToolError( message=f"unfile_document failed: no such document in the folder.", ) mutation = """ mutation deleteRcr($repo:String!, $id:String!) { deleteReferentialContainmentRelationship(repositoryIdentifier: $repo, identifier:$id ) { id } } """ var = {"repo": graphql_client.object_store, "id": return_id} response = await graphql_client.execute_async(query=mutation, variables=var) if "errors" in response: return ToolError( message=f"unfile_document failed: got err {response}.", ) return response["data"]["deleteReferentialContainmentRelationship"]["id"] except Exception as e: error_traceback = traceback.format_exc(limit=TRACEBACK_LIMIT) logger.error( f"{method_name} failed: {e.__class__.__name__} - {str(e)}\n{error_traceback}" ) return ToolError( message=f"{method_name} failed: got err {e}. Trace available in server logs.", ) def lookup_folder_id( folder_name: str, graphql_client: GraphQLClient ) -> Union[str, ToolError]: """ Retrieves the folder id for the given folder name. """ query = """ query folder($repo:String!, $folder_name: String!) { folder(repositoryIdentifier:$repo identifier:$folder_name) { id } } """ vars = {"repo": graphql_client.object_store, "folder_name": folder_name} response = graphql_client.execute( query, vars ) # Changed from execute_graphql to execute if "errors" in response: return ToolError( message=f"lookup_folder_id failed: got err {response}.", ) else: return response["data"]["folder"]["id"] def is_guid_with_braces(input_string): """ Check if a string is a valid GUID/UUID with curly braces. Args: input_string (str): The string to check Returns: bool: True if the string is a valid GUID with curly braces, False otherwise """ # Check if string starts with '{' and ends with '}' if not (input_string.startswith("{") and input_string.endswith("}")): return False # Remove the curly braces guid_string = input_string[1:-1] # Pattern for UUID: 8-4-4-4-12 hexadecimal digits pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" # Case-insensitive match if re.match(pattern, guid_string, re.IGNORECASE): return True # Alternative validation using uuid module try: uuid_obj = uuid.UUID(guid_string) return str(uuid_obj) == guid_string.lower() except ValueError: return False @mcp.tool( name="update_folder", ) async def update_folder( identifier: str, class_identifier: Optional[str] = None, folder_properties: Optional[FolderPropertiesInput] = None, ) -> Union[Folder, ToolError]: """ **PREREQUISITES IN ORDER**: To use this tool, you MUST call two other tools first in a specific sequence. 1. determine_class tool to get the class_identifier. 2. get_class_property_descriptions to get a list of valid properties for the given class_identifier Description: Updates an existing folder in the content repository with specified properties. :param identifier: The folder identifier or path (required). This can be either the folder's ID (GUID) or its path in the repository (e.g., "/Folder1/folder123"). :param class_identifier: The class identifier for the folder. If provided, allows changing the folder's class. :param folder_properties: Properties to update for the folder including name, etc :returns: If successful, returns a Folder object with its updated properties. If unsuccessful, returns a ToolError with details about the failure. """ method_name = "update_folder" try: # Prepare the mutation mutation = """ mutation ($object_store_name: String!, $identifier: String!, $class_identifier: String, $folder_properties: FolderPropertiesInput) { updateFolder( repositoryIdentifier: $object_store_name identifier: $identifier classIdentifier: $class_identifier folderProperties: $folder_properties ) { id className properties { id value } } } """ # Prepare variables for the GraphQL query variables = { "object_store_name": graphql_client.object_store, # Always use the default object store "identifier": identifier, "class_identifier": class_identifier if class_identifier else None, "folder_properties": None, } # Process folder properties if provided if folder_properties: try: transformed_props = folder_properties.transform_properties_dict( exclude_none=True ) variables["folder_properties"] = transformed_props except Exception as e: logger.error("Error transforming folder properties: %s", str(e)) logger.error(traceback.format_exc()) return ToolError( message=f"{method_name} failed: {str(e)}. Trace available in server logs." ) # Execute the GraphQL mutation logger.info("Executing folder update") response = await graphql_client.execute_async( query=mutation, variables=variables ) # Handle errors if "errors" in response: logger.error("GraphQL error: %s", response["errors"]) return ToolError(message=f"{method_name} failed: {response['errors']}") # Create and return a folder instance from the response return Folder.create_an_instance( graphQL_changed_object_dict=response["data"]["updateFolder"], class_identifier=( class_identifier if class_identifier else DEFAULT_FOLDER_CLASS ), ) except Exception as e: logger.error("%s failed: %s", method_name, str(e)) logger.error(traceback.format_exc()) return ToolError( message=f"{method_name} failed: {str(e)}. Trace available in server logs." ) @mcp.tool( name="get_folder_documents", ) async def get_folder_documents( folder_id_or_path: str, ) -> Union[list[Document], ToolError]: """ Retrieves a folder's contained documents. :param folder_id_or_path: The folder id or path. :returns: A list contains documents in the folder """ method_name = "get_folder_documents" logger.info("%s started", method_name) try: query = """ query getContainedDocuments($object_store_name: String!, $folder_id_or_path: String!){ folder( repositoryIdentifier: $object_store_name identifier: $folder_id_or_path ) { containedDocuments { documents { id name className properties { id value } } } } } """ variables = { "folder_id_or_path": folder_id_or_path, "object_store_name": graphql_client.object_store, } # return await graphql_client.execute_async(query=query, variables=variables) docs = await graphql_client.execute_async(query=query, variables=variables) if "errors" in docs: return ToolError( message=f"get_folder_documents failed: got err {docs}.", ) docslist = docs["data"]["folder"]["containedDocuments"]["documents"] if len(docslist) == 0: return [] else: contained_docs = [] for doc in docslist: onedoc = Document.create_an_instance( graphQL_changed_object_dict=doc, class_identifier=doc["className"], ) contained_docs.append(onedoc) return contained_docs except Exception as ex: error_traceback = traceback.format_exc(limit=TRACEBACK_LIMIT) logger.error( f"{method_name} failed: {ex.__class__.__name__} - {str(ex)}\n{error_traceback}" ) return ToolError( message=f"{method_name} failed: got err {ex}. Trace available in server logs.", )

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/ibm-ecm/cs-mcp-server'

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