Skip to main content
Glama

RSpace MCP Server

Official
by rspace-os
main.py48.5 kB
""" RSpace MCP Server This MCP server provides access to RSpace's Electronic Lab Notebook (ELN) and Inventory Management systems through a set of tools. Architecture: - Uses FastMCP framework for tool registration and server setup - Connects to RSpace via official Python client libraries (rspace_client) - Uses Pydantic models for type safety and validation Extension Guide: - ELN tools: Add new functions using @mcp.tool decorator with tags={"rspace"} - Inventory tools: Use tags={"rspace", "inventory", "<category>"} for organization - Follow existing patterns for error handling and return types - All tools should include comprehensive docstrings for Claude's understanding """ from typing import Annotated, Dict, List, Optional, Union, Literal from fastmcp import FastMCP from rspace_client.eln import eln as e # Electronic Lab Notebook client from rspace_client.inv import inv as i # Inventory Management client from rspace_client.eln.advanced_query_builder import AdvancedQueryBuilder import os from dotenv import load_dotenv from pydantic import BaseModel, Field # ============================================================================ # PYDANTIC MODELS - Data Structure Definitions # ============================================================================ # These models define the structure of data returned by RSpace APIs # Extend these when adding new data types or modify existing ones for new fields class Document(BaseModel): """ELN Document metadata - used for document listings""" name: str = Field("document's name") globalId: str = Field(description="Global identifier") created: str = Field(description="The document's creation date") class RSField(BaseModel): """Individual field content within an ELN document""" textContent: str = Field(description="text content of a field as HTML") class FullDocument(BaseModel): """Complete ELN document with all content concatenated""" content: str = Field(description="concatenated text content from all fields") class Sample(BaseModel): """Inventory sample metadata""" name: str = Field(description="Sample name") globalId: str = Field(description="Global identifier") created: str = Field(description="Creation date") tags: List[str] = Field(description="Sample tags") quantity: Optional[Dict] = Field(description="Sample quantity and units") class Container(BaseModel): """Inventory container metadata""" name: str = Field(description="Container name") globalId: str = Field(description="Global identifier") cType: str = Field(description="Container type (LIST, GRID, WORKBENCH, IMAGE)") capacity: Optional[int] = Field(description="Container capacity if applicable") class GridLocation(BaseModel): """Specific position within a grid container""" x: int = Field(description="Column position (1-based)") y: int = Field(description="Row position (1-based)") # ============================================================================ # SERVER INITIALIZATION AND CLIENT SETUP # ============================================================================ # This section handles MCP server setup and RSpace client authentication # Modify environment variable names here if your deployment uses different names mcp = FastMCP("RSpace MCP Server") load_dotenv() # Environment configuration - customize these variable names as needed api_key = os.getenv("RSPACE_API_KEY") api_url = os.getenv("RSPACE_URL") # Initialize RSpace clients eln_cli = e.ELNClient(api_url, api_key) # Electronic Lab Notebook operations inv_cli = i.InventoryClient(api_url, api_key) # Inventory Management operations # ============================================================================ # ELECTRONIC LAB NOTEBOOK (ELN) TOOLS # ============================================================================ # This section contains all tools related to documents, notebooks, forms, and # general ELN functionality. When adding new ELN features, add them here. # ==================== SYSTEM STATUS AND HEALTH ==================== @mcp.tool(tags={"rspace"}) def status() -> str: """ System health check - determines if RSpace server is accessible and running Usage: Call this first to verify connectivity before other operations Returns: Status message from RSpace server """ resp = eln_cli.get_status() return resp['message'] # ==================== DOCUMENT MANAGEMENT ==================== # Core document operations - reading, creating, updating documents @mcp.tool(tags={"rspace"}) def get_documents(page_size: int = 20) -> list[Document]: """ Retrieves recent RSpace documents with pagination Usage: Get overview of recent documents for browsing/selection Limit: Maximum 200 documents per call for performance Returns: List of document metadata (not full content) """ if page_size > 200 or page_size < 0: raise ValueError("page size must be less than 200") resp = eln_cli.get_documents(page_size=page_size) return resp['documents'] @mcp.tool(tags={"rspace"}, name="get_single_Rspace_document") def get_document(doc_id: int | str) -> FullDocument: """ Retrieves complete content of a single document Usage: Get full document text for reading/analysis Parameters: doc_id can be numeric ID or string globalId (e.g., "SD12345") Returns: Full document with concatenated field content """ resp = eln_cli.get_document(doc_id) # Concatenate all field content for easier processing resp['content'] = '' for fld in resp['fields']: resp['content'] = resp['content'] + fld['content'] return resp @mcp.tool(tags={"rspace"}) def update_document( document_id: int | str, name: str = None, tags: List[str] = None, form_id: int | str = None, fields: List[dict] = None ) -> dict: """ Updates existing RSpace document content and metadata Usage: Modify document name, tags, or field content Fields format: [{"id": field_id, "content": "new HTML content"}] Returns: Updated document information """ return eln_cli.update_document( document_id=document_id, name=name, tags=tags, form_id=form_id, fields=fields ) @mcp.tool(tags={"rspace", "search"}) def search_documents( query: str, search_type: Literal["simple", "advanced"] = "simple", query_types: List[Literal["global", "fullText", "tag", "name", "created", "lastModified", "form", "attachment"]] = None, operator: Literal["and", "or"] = "and", order_by: str = "lastModified desc", page_number: int = 0, page_size: int = 20, include_content: bool = False ) -> dict: """ Generic search tool for RSpace documents with flexible search options Usage: Search across all your RSpace documents using various criteria Parameters: - query: The search term(s) to look for - search_type: "simple" for basic search, "advanced" for multi-criteria search - query_types: List of search types to use (for advanced search): - "global": Search across all document content and metadata - "fullText": Search within document text content - "tag": Search by document tags - "name": Search by document names/titles - "created": Search by creation date (use ISO format like "2024-01-01") - "lastModified": Search by modification date - "form": Search by form type - "attachment": Search by attachments - operator: "and" (all criteria must match) or "or" (any criteria can match) - order_by: Sort results by field (e.g., "lastModified desc", "name asc") - page_number: Page number for pagination (0-based) - page_size: Number of results per page (max 200) - include_content: Whether to fetch full document content (slower but more complete) Returns: Dictionary with search results and metadata Examples: - Simple text search: search_documents("PCR protocol") - Search by tags: search_documents("experiment", search_type="advanced", query_types=["tag"]) - Multi-criteria search: search_documents("DNA", search_type="advanced", query_types=["fullText", "tag"], operator="or") """ if page_size > 200: raise ValueError("page_size must be 200 or less") if search_type == "simple": # Use simple search - works like RSpace's "All" search results = eln_cli.get_documents( query=query, order_by=order_by, page_number=page_number, page_size=page_size ) else: # Use advanced search with AdvancedQueryBuilder if query_types is None: query_types = ["global"] # Default to global search builder = AdvancedQueryBuilder(operator=operator) # Add search terms for each specified query type for query_type in query_types: if query_type == "global": builder.add_term(query, AdvancedQueryBuilder.QueryType.GLOBAL) elif query_type == "fullText": builder.add_term(query, AdvancedQueryBuilder.QueryType.FULL_TEXT) elif query_type == "tag": builder.add_term(query, AdvancedQueryBuilder.QueryType.TAG) elif query_type == "name": builder.add_term(query, AdvancedQueryBuilder.QueryType.NAME) elif query_type == "created": builder.add_term(query, AdvancedQueryBuilder.QueryType.CREATED) elif query_type == "lastModified": builder.add_term(query, AdvancedQueryBuilder.QueryType.LAST_MODIFIED) elif query_type == "form": builder.add_term(query, AdvancedQueryBuilder.QueryType.FORM) elif query_type == "attachment": builder.add_term(query, AdvancedQueryBuilder.QueryType.ATTACHMENT) advanced_query = builder.get_advanced_query() results = eln_cli.get_documents_advanced_query( advanced_query=advanced_query, order_by=order_by, page_number=page_number, page_size=page_size ) # Optionally fetch full content for each document if include_content and 'documents' in results: for doc in results['documents']: try: full_doc = eln_cli.get_document(doc['globalId']) # Add concatenated content to the document content = '' for field in full_doc.get('fields', []): content += field.get('content', '') doc['fullContent'] = content except Exception as e: doc['fullContent'] = f"Error fetching content: {str(e)}" return results @mcp.tool(tags={"rspace", "search"}) def search_by_tags( tags: List[str], operator: Literal["and", "or"] = "and", order_by: str = "lastModified desc", page_number: int = 0, page_size: int = 20 ) -> dict: """ Search documents by specific tags Usage: Find documents tagged with specific keywords Parameters: - tags: List of tags to search for - operator: "and" (document must have all tags) or "or" (document can have any tag) - order_by: Sort results by field - page_number: Page number for pagination - page_size: Number of results per page Returns: Dictionary with search results Example: search_by_tags(["PCR", "protocol"], operator="and") """ builder = AdvancedQueryBuilder(operator=operator) for tag in tags: builder.add_term(tag, AdvancedQueryBuilder.QueryType.TAG) advanced_query = builder.get_advanced_query() return eln_cli.get_documents_advanced_query( advanced_query=advanced_query, order_by=order_by, page_number=page_number, page_size=page_size ) @mcp.tool(tags={"rspace", "search"}) def search_recent_documents( days_back: int = 7, query: str = None, page_size: int = 20 ) -> dict: """ Search for recently modified documents Usage: Find documents modified within a specific timeframe Parameters: - days_back: Number of days to look back - query: Optional text search within recent documents - page_size: Number of results to return Returns: Dictionary with recent documents Example: search_recent_documents(7, "experiment") """ from datetime import datetime, timedelta # Calculate date range - RSpace expects "startDate;endDate" format for date ranges start_date = (datetime.now() - timedelta(days=days_back)).strftime("%Y-%m-%d") end_date = datetime.now().strftime("%Y-%m-%d") date_range = f"{start_date};{end_date}" builder = AdvancedQueryBuilder(operator="and") builder.add_term(date_range, AdvancedQueryBuilder.QueryType.LAST_MODIFIED) if query: builder.add_term(query, AdvancedQueryBuilder.QueryType.GLOBAL) advanced_query = builder.get_advanced_query() return eln_cli.get_documents_advanced_query( advanced_query=advanced_query, order_by="lastModified desc", page_number=0, page_size=page_size ) @mcp.tool(tags={"rspace", "search"}) def find_documents_by_content( content_terms: List[str], operator: Literal["and", "or"] = "and", exclude_terms: List[str] = None, order_by: str = "lastModified desc", page_size: int = 20 ) -> dict: """ Advanced content-based document search Usage: Find documents containing specific content terms Parameters: - content_terms: List of terms that should appear in document content - operator: "and" (all terms must appear) or "or" (any term can appear) - exclude_terms: Optional list of terms to exclude from results - order_by: Sort results by field - page_size: Number of results to return Returns: Dictionary with search results Example: find_documents_by_content(["DNA", "extraction"], operator="and") """ builder = AdvancedQueryBuilder(operator=operator) for term in content_terms: builder.add_term(term, AdvancedQueryBuilder.QueryType.FULL_TEXT) # Note: RSpace API doesn't directly support exclusion, but we can filter results advanced_query = builder.get_advanced_query() results = eln_cli.get_documents_advanced_query( advanced_query=advanced_query, order_by=order_by, page_number=0, page_size=page_size ) # Filter out documents containing excluded terms if specified if exclude_terms and 'documents' in results: filtered_docs = [] for doc in results['documents']: # Check if any exclude terms are in the document name or other available text doc_text = (doc.get('name', '') + ' ' + doc.get('tags', '')).lower() if not any(exclude_term.lower() in doc_text for exclude_term in exclude_terms): filtered_docs.append(doc) results['documents'] = filtered_docs results['totalHits'] = len(filtered_docs) return results # ==================== NOTEBOOK OPERATIONS ==================== # Specialized tools for notebook creation and entry management @mcp.tool(tags={"rspace"}, name="createNewNotebook") def create_notebook( name: Annotated[str, Field(description="The name of the notebook to create")], ) -> Dict[str, any]: """ Creates a new electronic lab notebook Usage: Organize related experiments/entries under a single notebook Returns: Created notebook information including ID for adding entries """ resp = eln_cli.create_folder(name, notebook=True) return resp @mcp.tool(tags={"rspace"}, name="createNotebookEntry") def create_notebook_entry( name: Annotated[str, Field(description="The name of the notebook entry")], text_content: Annotated[str, Field(description="html or plain text content ")], notebook_id: Annotated[int, Field(description="The id of the notebook to add the entry")], ) -> Dict[str, any]: """ Adds a new entry to an existing notebook Usage: Add experimental procedures, results, or observations to a notebook Content: Supports both HTML and plain text formatting Returns: Created entry information """ resp = eln_cli.create_document(name, parent_folder_id=notebook_id, fields=[{'content': text_content}]) return resp # ==================== DOCUMENT METADATA MANAGEMENT ==================== # Tools for organizing and categorizing documents @mcp.tool(tags={"rspace"}, name="tagDocumentOrNotebookEntry") def tag_document( doc_id: int | str, tags: Annotated[List[str], Field(description="One or more tags in a list")] ) -> Dict[str, any]: """ Adds tags to documents for organization and searchability Usage: Categorize documents by project, experiment type, etc. Tags: Use consistent naming for better organization Returns: Updated document with new tags """ resp = eln_cli.update_document(document_id=doc_id, tags=tags) return resp @mcp.tool(tags={"rspace"}, name="renameDocumentOrNotebookEntry") def rename_document( doc_id: int | str, name: str ) -> Dict[str, any]: """ Changes the name/title of a document or notebook entry Usage: Update document titles for better organization Returns: Updated document information """ resp = eln_cli.update_document(document_id=doc_id, name=name) return resp # ==================== FORM MANAGEMENT ==================== # Custom form creation and management for structured data entry @mcp.tool(tags={"rspace"}) def get_forms(query: str = None, order_by: str = "lastModified desc", page_number: int = 0, page_size: int = 20) -> dict: """ Lists available custom forms for structured document creation Usage: Browse available templates before creating structured documents Filtering: Use query parameter to search form names/descriptions Returns: Paginated list of form metadata """ return eln_cli.get_forms(query=query, order_by=order_by, page_number=page_number, page_size=page_size) @mcp.tool(tags={"rspace"}) def get_form(form_id: int | str) -> dict: """ Retrieves detailed information about a specific form template Usage: Examine form structure before creating documents or new forms Returns: Complete form definition including field specifications """ return eln_cli.get_form(form_id) @mcp.tool(tags={"rspace"}) def create_form( name: str, tags: List[str] = None, fields: List[dict] = None ) -> dict: """ Creates a new custom form template for structured data entry Usage: Define reusable templates for experiments, protocols, reports Fields structure: [ { "name": "Field Name", "type": "String|Text|Number|Radio|Date|Choice", "mandatory": True/False, "defaultValue": "optional default" } ] Returns: Created form information (form will be in NEW state) """ return eln_cli.create_form(name=name, tags=tags, fields=fields) @mcp.tool(tags={"rspace"}) def publish_form(form_id: int | str) -> dict: """ Makes a form available for creating documents Usage: Activate form after creation/modification Note: Forms must be published before they can be used for document creation Returns: Updated form status """ return eln_cli.publish_form(form_id) @mcp.tool(tags={"rspace"}) def unpublish_form(form_id: int | str) -> dict: """ Hides a form from document creation interface Usage: Temporarily disable forms without deletion Returns: Updated form status """ return eln_cli.unpublish_form(form_id) @mcp.tool(tags={"rspace"}) def share_form(form_id: int | str) -> dict: """ Shares form with user's groups for collaborative use Usage: Make custom forms available to team members Returns: Updated sharing status """ return eln_cli.share_form(form_id) @mcp.tool(tags={"rspace"}) def unshare_form(form_id: int | str) -> dict: """ Removes form sharing with groups Usage: Make form private again Returns: Updated sharing status """ return eln_cli.unshare_form(form_id) @mcp.tool(tags={"rspace"}) def delete_form(form_id: int | str) -> dict: """ Permanently deletes a form template Usage: Remove unused forms (only works for forms in NEW state) Warning: This operation cannot be undone Returns: Deletion confirmation """ return eln_cli.delete_form(form_id) @mcp.tool(tags={"rspace"}) def create_document_from_form( form_id: int | str, name: str = None, parent_folder_id: int | str = None, tags: List[str] = None, fields: List[dict] = None ) -> dict: """ Creates a structured document using a form template Usage: Generate documents with predefined structure and validation Fields: Pre-populate form fields with initial data Returns: Created document information """ return eln_cli.create_document( name=name, parent_folder_id=parent_folder_id, tags=tags, form_id=form_id, fields=fields ) # ==================== AUDIT AND ACTIVITY TRACKING ==================== # Tools for monitoring system usage and document history @mcp.tool(tags={"rspace"}, name="getAuditEvents") def activity( username: str = None, global_id: str = None, date_from: str = None, date_to: str = None ) -> Dict[str, any]: """ Retrieves audit trail of all actions performed in RSpace Usage: Monitor document access, modifications, and user activity Filtering options: - username: Filter by specific user actions - global_id: Filter by specific document - date_from/date_to: ISO8601 format date range Returns: Chronological list of system events """ resp = eln_cli.get_activity(users=[username], global_id=global_id, date_from=date_from, date_to=date_to) return resp # ==================== FILE MANAGEMENT ==================== # Tools for handling file attachments and downloads @mcp.tool(tags={"rspace"}, name="downloadFile") def download_file( file_id: int, file_path: str ) -> Dict[str, any]: """ Downloads file attachments from RSpace documents Usage: Retrieve images, data files, or other attachments Parameters: - file_id: Numeric ID of the file attachment - file_path: Local filesystem path where file should be saved Returns: Download status and file information """ resp = eln_cli.download_file(file_id=file_id, filename=file_path, chunk_size=1024) return resp @mcp.tool(tags={"rspace", "files"}) def uploadAndAttachFile( document_id: Union[int, str], file_path: str, caption: Optional[str] = None, description: Optional[str] = None ) -> dict: """ Uploads a file to RSpace and attaches it to a document as a proper file attachment Usage: One-step process to upload any file and attach it to an RSpace document File types: Supports all file types (images, PDFs, data files, protocols, etc.) Attachment: Creates proper RSpace file attachment, not just a link Parameters: - document_id: RSpace document ID (numeric or global ID like "SD12345") - file_path: Path to the file to upload (e.g., "data/results.pdf") - caption: Optional caption that appears with the attachment - description: Optional description for the uploaded file Returns: Upload confirmation and document update information """ try: # Step 1: Upload the file to RSpace with open(file_path, 'rb') as file: upload_result = eln_cli.upload_file(file, caption=description) file_id = upload_result.get('id') if not file_id: return {"error": "File upload failed - no file ID returned"} # Step 2: Get the current document document = eln_cli.get_document(document_id) if not document.get('fields'): return {"error": f"Document {document_id} has no fields to attach file to"} # Step 3: Create proper RSpace file attachment # This is the key fix - use RSpace's native attachment format attachment_html = f'<fileId={file_id}>' # Add caption as separate paragraph if provided if caption: attachment_html = f'<p><strong>{caption}</strong></p>\n{attachment_html}' # Step 4: Update the document with the file attachment first_field = document['fields'][0] current_content = first_field.get('content', '') updated_content = current_content + '\n' + attachment_html # Update the document update_result = eln_cli.update_document( document_id=document_id, fields=[{ 'id': first_field['id'], 'content': updated_content }] ) return { "success": True, "message": "File uploaded and attached successfully", "file_info": { "file_id": file_id, "name": upload_result.get('name'), "size": upload_result.get('size'), "globalId": upload_result.get('globalId'), "description": description }, "attachment_info": { "document_id": str(document_id), "caption": caption, "attachment_format": "rspace_native", "field_updated": first_field['id'] }, "updated_document": update_result } except FileNotFoundError: return {"error": f"File not found: {file_path}"} except Exception as e: return {"error": f"Failed to upload and attach file: {str(e)}"} # ============================================================================ # INVENTORY MANAGEMENT TOOLS # ============================================================================ # This section contains all tools related to sample management, container # organization, and inventory tracking. When adding new inventory features, # organize them by category (samples, containers, movement, templates, utility). # ==================== SAMPLE MANAGEMENT ==================== # Core sample creation, retrieval, and manipulation tools @mcp.tool(tags={"rspace", "inventory", "samples"}) def create_sample( name: str, tags: List[str] = None, description: str = None, subsample_count: int = 1, total_quantity_value: float = None, total_quantity_unit: str = "ml" ) -> dict: """ Creates a new sample in the inventory system Usage: Register new samples with metadata and quantity tracking Subsamples: Automatically creates specified number of subsample aliquots Quantity: Tracks total amount with specified units (ml, mg, μl, etc.) Returns: Created sample information including generated subsample IDs """ tag_objects = i.gen_tags(tags) if tags else [] quantity = None if total_quantity_value: from rspace_client.inv import quantity_unit as qu unit = qu.QuantityUnit.of(total_quantity_unit) quantity = i.Quantity(total_quantity_value, unit) return inv_cli.create_sample( name=name, tags=tag_objects, description=description, subsample_count=subsample_count, total_quantity=quantity ) @mcp.tool(tags={"rspace", "inventory", "samples"}) def get_sample(sample_id: Union[int, str]) -> dict: """ Retrieves complete information about a specific sample Usage: Get detailed sample metadata, location, and subsample information Parameters: sample_id can be numeric ID or global ID (e.g., "SA12345") Returns: Full sample details including all subsamples """ return inv_cli.get_sample_by_id(sample_id) @mcp.tool(tags={"rspace", "inventory", "samples"}) def list_samples(page_size: int = 20, order_by: str = "lastModified", sort_order: str = "desc") -> dict: """ Lists samples in the inventory with pagination and sorting Usage: Browse sample collection, find recent additions Sorting: Options include "lastModified", "name", "created" Returns: Paginated list of sample metadata """ pagination = i.Pagination(page_size=page_size, order_by=order_by, sort_order=sort_order) return inv_cli.list_samples(pagination) @mcp.tool(tags={"rspace", "inventory", "samples"}) def duplicate_sample(sample_id: Union[int, str], new_name: str = None) -> dict: """ Creates an exact copy of an existing sample Usage: Replicate samples for parallel experiments or backup Returns: New sample information with fresh ID and subsamples """ return inv_cli.duplicate(sample_id, new_name) @mcp.tool(tags={"rspace", "inventory", "samples"}) def split_subsample( subsample_id: Union[int, str], num_new_subsamples: int, quantity_per_subsample: float = None ) -> dict: """ Divides a subsample into multiple new subsamples Usage: Create aliquots for distribution or different experiments Quantity: If specified, each new subsample gets this amount Returns: Information about newly created subsamples """ result = inv_cli.split_subsample(subsample_id, num_new_subsamples, quantity_per_subsample) return result.data if hasattr(result, 'data') else result @mcp.tool(tags={"rspace", "inventory", "samples"}) def add_note_to_subsample(subsample_id: Union[int, str], note: str) -> dict: """ Adds annotations or observations to a specific subsample Usage: Record experimental notes, observations, or handling instructions Returns: Updated subsample information with new note """ return inv_cli.add_note_to_subsample(subsample_id, note) # ==================== SEARCH AND DISCOVERY ==================== # Tools for finding inventory items across the system @mcp.tool(tags={"rspace", "inventory", "samples"}) def search_inventory(query: str, result_type: str = None) -> dict: """ Searches across all inventory items using text query Usage: Find samples, containers, or templates by name, tags, or description Result types: 'SAMPLE', 'SUBSAMPLE', 'CONTAINER', 'TEMPLATE' (or None for all) Returns: Matching items with relevance scoring """ rt = None if result_type: rt = getattr(i.ResultType, result_type.upper(), None) return inv_cli.search(query, result_type=rt) # ==================== CONTAINER MANAGEMENT ==================== # Tools for creating and managing storage containers @mcp.tool(tags={"rspace", "inventory", "containers"}) def create_list_container( name: str, description: str = None, tags: List[str] = None, can_store_containers: bool = True, can_store_samples: bool = True, parent_container_id: Union[int, str] = None ) -> dict: """ Creates a simple list-based container for organizing inventory Usage: Create folders, boxes, or other containers without specific positioning Storage permissions: Configure what types of items can be stored Hierarchy: Optionally nest within another container Returns: Created container information with storage settings """ tag_objects = i.gen_tags(tags) if tags else [] location = i.TopLevelTargetLocation() if parent_container_id: location = i.ListContainerTargetLocation(parent_container_id) return inv_cli.create_list_container( name=name, description=description, tags=tag_objects, can_store_containers=can_store_containers, can_store_samples=can_store_samples, location=location ) @mcp.tool(tags={"rspace", "inventory", "containers"}) def create_grid_container( name: str, rows: int, columns: int, description: str = None, tags: List[str] = None, can_store_containers: bool = True, can_store_samples: bool = True, parent_container_id: Union[int, str] = None ) -> dict: """ Creates a grid-based container with specific positioning Usage: Create microplates, freezer boxes, or other position-specific storage Dimensions: Define exact grid size (e.g., 8x12 for 96-well plate) Positioning: Items placed at specific coordinates (row, column) Returns: Created container information with grid specifications """ tag_objects = i.gen_tags(tags) if tags else [] location = i.TopLevelTargetLocation() if parent_container_id: location = i.ListContainerTargetLocation(parent_container_id) return inv_cli.create_grid_container( name=name, row_count=rows, column_count=columns, description=description, tags=tag_objects, can_store_containers=can_store_containers, can_store_samples=can_store_samples, location=location ) @mcp.tool(tags={"rspace", "inventory", "containers"}) def get_container(container_id: Union[int, str], include_content: bool = False) -> dict: """ Retrieves container information with optional content listing Usage: Examine container properties and optionally see what's inside Performance: Set include_content=False for faster queries on large containers Returns: Container details and optionally contained items """ return inv_cli.get_container_by_id(container_id, include_content) @mcp.tool(tags={"rspace", "inventory", "containers"}) def list_containers(page_size: int = 20) -> dict: """ Lists top-level containers (not nested within other containers) Usage: Browse main container organization structure Returns: Paginated list of root-level containers """ pagination = i.Pagination(page_size=page_size) return inv_cli.list_top_level_containers(pagination) @mcp.tool(tags={"rspace", "inventory", "containers"}) def get_workbenches() -> List[dict]: """ Retrieves all available workbenches (virtual workspaces) Usage: Find available workspaces for organizing current work Workbenches: Special containers representing physical or logical workspaces Returns: List of all workbench containers """ return inv_cli.get_workbenches() # ==================== ITEM MOVEMENT AND ORGANIZATION ==================== # Tools for moving samples and containers between locations @mcp.tool(tags={"rspace", "inventory", "movement"}) def move_items_to_list_container( target_container_id: Union[int, str], item_ids: List[str] ) -> dict: """ Moves multiple items to a list-based container Usage: Organize items in simple containers without specific positioning Items: Can move both samples/subsamples and other containers Returns: Success status and results for each moved item """ result = inv_cli.add_items_to_list_container(target_container_id, *item_ids) return {"success": result.is_ok(), "results": result.data if hasattr(result, 'data') else str(result)} @mcp.tool(tags={"rspace", "inventory", "movement"}) def move_items_to_grid_container_by_row( target_container_id: Union[int, str], item_ids: List[str], start_column: int = 1, start_row: int = 1, total_columns: int = None, total_rows: int = None ) -> dict: """ Moves items to grid container, filling positions row by row Usage: Systematic filling of plates, boxes, or other gridded containers Auto-positioning: Automatically calculates next available positions Dimensions: Auto-detected from container if not provided Returns: Success status and final positions of moved items """ # Auto-detect container dimensions if not provided if total_columns is None or total_rows is None: container = inv_cli.get_container_by_id(target_container_id) container_obj = i.Container.of(container) if hasattr(container_obj, 'column_count'): total_columns = container_obj.column_count() total_rows = container_obj.row_count() else: raise ValueError("Container dimensions required for non-grid containers") placement = i.ByRow(start_column, start_row, total_columns, total_rows, *item_ids) result = inv_cli.add_items_to_grid_container(target_container_id, placement) return {"success": result.is_ok(), "results": result.data if hasattr(result, 'data') else str(result)} @mcp.tool(tags={"rspace", "inventory", "movement"}) def move_items_to_grid_container_by_column( target_container_id: Union[int, str], item_ids: List[str], start_column: int = 1, start_row: int = 1, total_columns: int = None, total_rows: int = None ) -> dict: """ Moves items to grid container, filling positions column by column Usage: Alternative filling pattern for specific experimental layouts Auto-positioning: Fills down columns before moving to next column Returns: Success status and final positions of moved items """ # Auto-detect container dimensions if not provided if total_columns is None or total_rows is None: container = inv_cli.get_container_by_id(target_container_id) container_obj = i.Container.of(container) if hasattr(container_obj, 'column_count'): total_columns = container_obj.column_count() total_rows = container_obj.row_count() else: raise ValueError("Container dimensions required for non-grid containers") placement = i.ByColumn(start_column, start_row, total_columns, total_rows, *item_ids) result = inv_cli.add_items_to_grid_container(target_container_id, placement) return {"success": result.is_ok(), "results": result.data if hasattr(result, 'data') else str(result)} @mcp.tool(tags={"rspace", "inventory", "movement"}) def move_items_to_specific_grid_locations( target_container_id: Union[int, str], item_ids: List[str], grid_locations: List[GridLocation] ) -> dict: """ Places items at specific coordinates within a grid container Usage: Precise positioning for experimental layouts or protocols Coordinates: Each item gets an exact (row, column) position Validation: Ensures equal number of items and positions Returns: Success status and confirmation of final positions """ if len(item_ids) != len(grid_locations): raise ValueError("Number of items must match number of grid locations") locations = [i.GridLocation(loc.x, loc.y) for loc in grid_locations] placement = i.ByLocation(locations, *item_ids) result = inv_cli.add_items_to_grid_container(target_container_id, placement) return {"success": result.is_ok(), "results": result.data if hasattr(result, 'data') else str(result)} # ==================== TEMPLATE MANAGEMENT ==================== # Tools for creating and using sample templates for standardization @mcp.tool(tags={"rspace", "inventory", "templates"}) def create_sample_template(template_data: dict) -> dict: """ Creates a reusable template for sample creation Usage: Standardize sample creation with predefined fields and validation Template data: Define field structure, default values, and constraints Returns: Created template information for future sample generation """ return inv_cli.create_sample_template(template_data) @mcp.tool(tags={"rspace", "inventory", "templates"}) def get_sample_template(template_id: Union[int, str]) -> dict: """ Retrieves detailed information about a sample template Usage: Examine template structure before using for sample creation Returns: Complete template definition including field specifications """ return inv_cli.get_sample_template_by_id(template_id) @mcp.tool(tags={"rspace", "inventory", "templates"}) def list_sample_templates(page_size: int = 20) -> dict: """ Lists available sample templates for reuse Usage: Browse existing templates before creating new samples Returns: Paginated list of template metadata """ pagination = i.Pagination(page_size=page_size) return inv_cli.list_sample_templates(pagination) # ==================== UTILITY AND HELPER FUNCTIONS ==================== # General-purpose tools for inventory management and optimization @mcp.tool(tags={"rspace", "inventory", "utility"}) def rename_inventory_item(item_id: Union[int, str], new_name: str) -> dict: """ Changes the name of any inventory item Usage: Rename samples, subsamples, containers, or templates Universal: Works with any inventory item type Returns: Updated item information with new name """ return inv_cli.rename(item_id, new_name) @mcp.tool(tags={"rspace", "inventory", "utility"}) def add_extra_fields_to_item(item_id: Union[int, str], field_data: List[dict]) -> dict: """ Adds custom metadata fields to inventory items Usage: Extend items with experiment-specific or project-specific data Field format: [{"name": "Field Name", "type": "text|number", "content": "value"}] Types: 'text' for strings, 'number' for numeric values Returns: Updated item with new custom fields """ extra_fields = [] for field in field_data: field_type = i.ExtraFieldType.TEXT if field.get('type', 'text').lower() == 'text' else i.ExtraFieldType.NUMBER ef = i.ExtraField(field['name'], field_type, field.get('content', '')) extra_fields.append(ef) return inv_cli.add_extra_fields(item_id, *extra_fields) @mcp.tool(tags={"rspace", "inventory", "utility"}) def generate_barcode(global_id: str, barcode_type: str = "BARCODE") -> bytes: """ Generates scannable barcodes for inventory items Usage: Create physical labels for sample tracking and identification Types: 'BARCODE' for standard linear barcodes, 'QR' for QR codes Returns: Binary barcode image data for printing or display """ bc_type = i.Barcode.BARCODE if barcode_type.upper() == "BARCODE" else i.Barcode.QR return inv_cli.barcode(global_id, barcode_type=bc_type) # ==================== PERFORMANCE-OPTIMIZED UTILITY FUNCTIONS ==================== # These functions are designed for high-performance operations with large datasets @mcp.tool(tags={"rspace", "inventory", "utility"}) def get_container_summary(container_id: int | str) -> dict: """ Retrieves container metadata without content for fast queries Usage: Quick container information lookup without performance impact Performance: Avoids loading large content lists for better response times Returns: Container metadata only (name, type, capacity, etc.) """ return inv_cli.get_container_by_id(container_id, include_content=False) @mcp.tool(tags={"rspace", "inventory", "utility"}) def get_container_contents_only(container_id: int | str) -> list: """ Retrieves only the items stored in a container Usage: Get container contents without metadata overhead Performance: Focused query for container content analysis Returns: List of contained items with minimal metadata """ container = inv_cli.get_container_by_id(container_id, include_content=True) return container.get('locations', []) @mcp.tool(tags={"rspace", "inventory", "utility"}) def bulk_create_samples(sample_definitions: List[dict]) -> dict: """ Creates multiple samples efficiently in a single operation Usage: High-performance sample creation for large datasets Performance: Much faster than individual create_sample calls Format: List of sample definition dictionaries Note: Implementation should use batch API endpoints when available Returns: Results for all created samples with error handling """ # TODO: Implement bulk creation logic # This would use batch endpoints or optimized iteration # depending on what the RSpace API supports pass @mcp.tool(tags={"rspace", "inventory", "utility"}) def get_recent_samples_summary(days_back: int = 7, page_size: int = 10) -> list: """ Retrieves recent samples with minimal data for dashboard views Usage: Quick overview of recent activity without full sample details Performance: Optimized for dashboard and summary displays Filtering: Configurable time window and result count Returns: Lightweight sample list with essential information only """ # TODO: Implement efficient recent samples query # This would use date filtering and minimal field selection # for optimal performance pass # ============================================================================ # SERVER EXECUTION # ============================================================================ # This section handles the actual MCP server startup # Modify this section only if changing server configuration or adding # initialization logic if __name__ == "__main__": """ Main entry point for the RSpace MCP Server Extension Guide: - Server runs on FastMCP framework with automatic tool discovery - All functions decorated with @mcp.tool are automatically registered - Tool tags are used for organization and filtering - Add new tools anywhere in the file with proper tagging Deployment: - Ensure RSPACE_API_KEY and RSPACE_URL environment variables are set - Server will automatically expose all registered tools to MCP clients - Use appropriate tags for tool categorization and discovery """ mcp.run() # ============================================================================ # EXTENSION GUIDELINES FOR CONTRIBUTORS # ============================================================================ """ ADDING NEW FUNCTIONALITY: 1. ELN (Electronic Lab Notebook) Extensions: - Add new functions in the ELN section with @mcp.tool(tags={"rspace"}) - Follow existing patterns for error handling and return types - Use the eln_cli client for all ELN operations - Add comprehensive docstrings explaining usage and parameters 2. Inventory Extensions: - Add functions in appropriate inventory subsection - Use tags like @mcp.tool(tags={"rspace", "inventory", "category"}) - Categories: "samples", "containers", "movement", "templates", "utility" - Use the inv_cli client for all inventory operations 3. Performance Considerations: - Add utility functions for common operations that need optimization - Consider bulk operations for high-volume tasks - Use appropriate pagination for large result sets - Implement content filtering to reduce data transfer 4. Error Handling: - Follow existing patterns for input validation - Provide meaningful error messages for common failure cases - Use appropriate exception types and handle API errors gracefully 5. Documentation: - Include comprehensive docstrings for all new functions - Explain parameters, return values, and usage examples - Document any special requirements or limitations - Update this guide when adding new categories or patterns 6. Testing: - Test new functions with various input scenarios - Verify error handling with invalid inputs - Test integration with existing tools and workflows - Document any dependencies or setup requirements COMMON PATTERNS: - ID Parameters: Accept both numeric IDs and string global IDs - Pagination: Use appropriate page sizes and provide pagination options - Tags: Use consistent tagging for organization and discoverability - Return Types: Return appropriate data structures (dict, list, custom models) - Optional Parameters: Provide sensible defaults for optional parameters - Client Usage: Use eln_cli for ELN operations, inv_cli for inventory ARCHITECTURE NOTES: - FastMCP Framework: Handles tool registration and server communication - RSpace Clients: Official Python clients provide API access - Pydantic Models: Used for type safety and validation - Environment Config: API credentials loaded from .env file - Modular Organization: Functions grouped by feature area for maintainability """

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/rspace-os/rspace-mcp'

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