Skip to main content
Glama

Story SDK MCP Server

by piplabs
server.py52.6 kB
from mcp.server.fastmcp import FastMCP from services.story_service import StoryService import os from dotenv import load_dotenv from typing import Union, Optional import json import sys from pathlib import Path import logging logger = logging.getLogger(__name__) # Add the parent directory to the Python path so we can import utils sys.path.append(str(Path(__file__).parent.parent)) # Load environment variables load_dotenv(override=True) print(f"RPC URL from env: {os.getenv('RPC_PROVIDER_URL')}") # Get environment variables private_key = os.getenv("WALLET_PRIVATE_KEY") rpc_url = os.getenv("RPC_PROVIDER_URL") if not private_key or not rpc_url: raise ValueError( "WALLET_PRIVATE_KEY and RPC_PROVIDER_URL environment variables are required" ) # Initialize Story service story_service = StoryService(rpc_url=rpc_url, private_key=private_key) # Initialize MCP mcp = FastMCP("Story Protocol Server") # Only register IPFS-related tools if IPFS is enabled if story_service.ipfs_enabled: @mcp.tool() def upload_image_to_ipfs(image_data: Union[bytes, str]) -> str: """ Upload an image to IPFS using Pinata API. Args: image_data: Either bytes of image data or URL to image Returns: str: IPFS URI of the uploaded image """ try: ipfs_uri = story_service.upload_image_to_ipfs(image_data) return f"Successfully uploaded image to IPFS: {ipfs_uri}" except Exception as e: return f"Error uploading image to IPFS: {str(e)}" @mcp.tool() def create_ip_metadata( image_uri: str, name: str, description: str, attributes: Optional[list] = None ) -> str: """ Create and upload both NFT and IP metadata to IPFS. Args: image_uri: IPFS URI of the uploaded image name: Name of the NFT/IP description: Description of the NFT/IP attributes: Optional list of attribute dictionaries Returns: str: Result message with metadata details and IPFS URIs """ try: result = story_service.create_ip_metadata( image_uri=image_uri, name=name, description=description, attributes=attributes, ) return ( f"Successfully created and uploaded metadata! Here's what happened:\n\n" f"Your Request:\n" f" • Image URI: {image_uri}\n" f" • Name: {name}\n" f" • Description: {description}\n" f" • Attributes: {len(attributes) if attributes else 0} attributes\n\n" f"Generated Metadata:\n" f" • NFT Metadata URI: {result['nft_metadata_uri']}\n" f" • IP Metadata URI: {result['ip_metadata_uri']}\n\n" f"Registration metadata for minting:\n" f"```json\n" f"{json.dumps(result['registration_metadata'], indent=2)}\n" f"```\n" ) except Exception as e: return f"Error creating metadata: {str(e)}" @mcp.tool() def get_license_terms(license_terms_id: int) -> str: """Get the license terms for a specific ID. Args: license_terms_id: The ID of the license terms Returns: str: Information about the license terms """ try: terms = story_service.get_license_terms(license_terms_id) return ( f"Successfully retrieved license terms! Here are the complete details:\n\n" f"Your Request:\n" f" • License Terms ID: {license_terms_id}\n\n" f"License Terms Details:\n" f" • Transferable: {terms.get('transferable', 'N/A')}\n" f" • Royalty Policy: {terms.get('royaltyPolicy', 'N/A')}\n" f" • Default Minting Fee: {terms.get('defaultMintingFee', 'N/A')} wei \n" f" • Commercial Use: {terms.get('commercialUse', 'N/A')}\n" f" • Commercial Attribution: {terms.get('commercialAttribution', 'N/A')}\n" f" • Commercial Revenue Share: {terms.get('commercialRevShare', 'N/A')}\n" f" • Derivatives Allowed: {terms.get('derivativesAllowed', 'N/A')}\n" f" • Derivatives Attribution: {terms.get('derivativesAttribution', 'N/A')}\n" f" • Derivatives Reciprocal: {terms.get('derivativesReciprocal', 'N/A')}\n" f" • Currency: {terms.get('currency', 'N/A')}" ) except Exception as e: return f"❌ Error retrieving license terms for ID {license_terms_id}: {str(e)}" @mcp.tool() def get_license_minting_fee(license_terms_id: int) -> str: """ Get the minting fee for a specific license terms ID. Args: license_terms_id: The ID of the license terms Returns: str: Information about the minting fee """ try: minting_fee = story_service.get_license_minting_fee(license_terms_id) fee_in_ether = story_service.web3.from_wei(minting_fee, 'ether') return ( f"Successfully retrieved minting fee information for License Terms ID {license_terms_id}:\n\n" f"Your Request:\n" f" • License Terms ID: {license_terms_id}\n\n" f"Minting Fee Details:\n" f" • Fee Amount: {minting_fee} wei ({fee_in_ether} IP)\n" f" • This is the cost to mint each license token from this license terms" ) except Exception as e: return f"❌ Error retrieving license minting fee for License Terms ID {license_terms_id}: {str(e)}" @mcp.tool() def get_license_revenue_share(license_terms_id: int) -> str: """ Get the commercial revenue share percentage for a specific license terms ID. Args: license_terms_id: The ID of the license terms Returns: str: Information about the revenue share percentage """ try: revenue_share = story_service.get_license_revenue_share(license_terms_id) return ( f"Successfully retrieved revenue share information for License Terms ID {license_terms_id}:\n\n" f"Your Request:\n" f" • License Terms ID: {license_terms_id}\n\n" f"Revenue Share Details:\n" f" • Commercial Revenue Share: {revenue_share}%\n" f" • This is the percentage of commercial revenue that must be shared" ) except Exception as e: return f"❌ Error retrieving license revenue share for License Terms ID {license_terms_id}: {str(e)}" @mcp.tool() def mint_license_tokens( licensor_ip_id: str, license_terms_id: int, receiver: Optional[str] = None, amount: int = 1, max_minting_fee: Optional[int] = None, max_revenue_share: Optional[int] = None, license_template: Optional[str] = None ) -> str: """ Mint license tokens for a given IP and license terms. 💰 AUTO-APPROVE: This method automatically approves the exact amount of WIP tokens needed for minting. The system will approve only the required fee amount to ensure the transaction succeeds. Args: licensor_ip_id: The ID of the licensor's intellectual property license_terms_id: The ID of the license terms receiver: [Optional] the recipient's address for the tokens (ask user if not provided) amount: [Optional] number of license tokens to mint (ask user, defaults to 1) max_minting_fee: [HIDDEN] DO NOT ask user - automatically set from get_license_minting_fee() max_revenue_share: [HIDDEN] DO NOT ask user - automatically set from get_license_revenue_share() license_template: [HIDDEN] DO NOT ask user - uses default template Returns: str: Success message with transaction hash and token IDs """ try: response = story_service.mint_license_tokens( licensor_ip_id=licensor_ip_id, license_terms_id=license_terms_id, receiver=receiver, amount=amount, max_minting_fee=max_minting_fee, max_revenue_share=max_revenue_share, license_template=license_template ) return ( f"Successfully minted license tokens! Here's what happened:\n\n" f"Your Request:\n" f" • Licensor IP ID: {licensor_ip_id}\n" f" • License Terms ID: {license_terms_id}\n" f" • Number of tokens minted: {amount}\n" f" • Recipient: {receiver if receiver else 'Your wallet (default)'}\n" f"Result Summary:\n" f" • Transaction Hash: {response['tx_hash']}\n" f" • License Token IDs: {response['license_token_ids']}\n" f" • Your license tokens are now ready to use" ) except ValueError as e: return f"Validation error: {str(e)}" except Exception as e: return f"Error minting license tokens: {str(e)}" # @mcp.tool() # def send_ip(to_address: str, amount: float) -> str: # """ # Send IP tokens to another address. # :param to_address: The recipient's wallet address # :param amount: Amount of IP tokens to send (1 IP = 1 Ether) # :return: Transaction result message # """ # try: # response = story_service.send_ip(to_address, amount) # return f"Successfully sent {amount} IP to {to_address}. Transaction hash: {response['txHash']}" # except Exception as e: # return f"Error sending IP: {str(e)}" @mcp.tool() def mint_and_register_ip_with_terms( commercial_rev_share: int, derivatives_allowed: bool, registration_metadata: dict, commercial_use: bool = True, minting_fee: int = 0, recipient: Optional[str] = None, spg_nft_contract: Optional[str] = None, # Make this optional spg_nft_contract_max_minting_fee: Optional[int] = None, spg_nft_contract_mint_fee_token: Optional[str] = None ) -> str: """ Mint an NFT, register it as an IP Asset, and attach PIL terms. 💰 AUTO-APPROVE: This method automatically approves the exact amount of tokens needed for minting. The system will approve only the required fee amount to ensure the transaction succeeds. Args: commercial_rev_share: Percentage of revenue share (0-100) (ask user) derivatives_allowed: Whether derivatives are allowed (ask user) registration_metadata: Dict containing metadata URIs and hashes from create_ip_metadata (ask user) commercial_use: [Optional] Whether this is a commercial license (ask user, defaults to True) minting_fee: [Optional] Fee required to mint license tokens in wei (ask user, defaults to 0) recipient: [Optional] recipient address (ask user if not provided, defaults to sender) spg_nft_contract: [Optional] SPG NFT contract address (ask user, defaults to network-specific default) spg_nft_contract_max_minting_fee: [HIDDEN] DO NOT ask user - automatically set from get_spg_nft_minting_token() spg_nft_contract_mint_fee_token: [HIDDEN] DO NOT ask user - automatically set from get_spg_nft_minting_token() Returns: str: Result message with transaction details """ try: response = story_service.mint_and_register_ip_with_terms( commercial_rev_share=commercial_rev_share, derivatives_allowed=derivatives_allowed, registration_metadata=registration_metadata, commercial_use=commercial_use, minting_fee=minting_fee, recipient=recipient, spg_nft_contract=spg_nft_contract, spg_nft_contract_max_minting_fee=spg_nft_contract_max_minting_fee, spg_nft_contract_mint_fee_token=spg_nft_contract_mint_fee_token ) # Determine which explorer URL to use based on network explorer_url = ( "https://explorer.story.foundation" if story_service.network == "mainnet" else "https://aeneid.explorer.story.foundation" ) return ( f"Successfully minted NFT and registered as IP Asset with license terms! Here's the complete summary:\n\n" f"Your Configuration:\n" f" • Commercial Revenue Share: {commercial_rev_share}%\n" f" • Derivatives Allowed: {'Yes' if derivatives_allowed else 'No'}\n" f" • Commercial Use: {'Enabled' if commercial_use else 'Disabled'}\n" f" • Minting Fee: {minting_fee} WIP in wei\n" f" • Recipient: {recipient if recipient else 'Your wallet (default)'}\n" f" • SPG NFT Contract: {spg_nft_contract if spg_nft_contract else 'Default network contract'}\n\n" f"Created Assets:\n" f" • IP Asset ID: {response['ip_id']}\n" f" • NFT Token ID: {response['token_id']}\n" f" • License Terms IDs: {response['license_terms_ids']}\n" f" • Transaction Hash: {response.get('tx_hash')}\n" f" • View your IP Asset: {explorer_url}/ipa/{response['ip_id']}" ) except Exception as e: return f"Error minting and registering IP with terms: {str(e)}" @mcp.tool() def create_spg_nft_collection( name: str, symbol: str, is_public_minting: bool = True, mint_open: bool = True, mint_fee_recipient: Optional[str] = None, contract_uri: str = "", base_uri: str = "", max_supply: Optional[int] = None, mint_fee: Optional[int] = None, mint_fee_token: Optional[str] = None, owner: Optional[str] = None, ) -> str: """ Create a new SPG NFT collection that can be used for minting and registering IP assets. Args: name: Name of the NFT collection symbol: Symbol for the NFT collection is_public_minting: [OPTIONAL] Whether anyone can mint NFTs from this collection (defaults to True) mint_open: [OPTIONAL] Whether minting is currently enabled (defaults to True) mint_fee_recipient: [OPTIONAL] Address to receive minting fees (defaults to sender) contract_uri: [OPTIONAL] URI for the collection metadata (ERC-7572 standard) base_uri: [OPTIONAL] Base URI for the collection. If not empty, tokenURI will be either baseURI + token ID or baseURI + nftMetadataURI max_supply: [OPTIONAL] Maximum supply of the collection (defaults to unlimited) mint_fee: [OPTIONAL] Cost to mint a token in wei (defaults to 0) mint_fee_token: [OPTIONAL] Token address used for minting fees (defaults to WIP) owner: [OPTIONAL] Owner address of the collection (defaults to sender) Returns: str: Information about the created collection """ try: response = story_service.create_spg_nft_collection( name=name, symbol=symbol, is_public_minting=is_public_minting, mint_open=mint_open, mint_fee_recipient=mint_fee_recipient, contract_uri=contract_uri, base_uri=base_uri, max_supply=max_supply, mint_fee=mint_fee, mint_fee_token=mint_fee_token, owner=owner, ) return ( f"Successfully created your SPG NFT collection! Here's what was set up:\n\n" f"Your Collection Configuration:\n" f" • Collection Name: {name}\n" f" • Symbol: {symbol}\n" f" • Public Minting: {'Enabled (anyone can mint)' if is_public_minting else 'Restricted (only authorized minters)'}\n" f" • Minting Status: {'Open (minting allowed)' if mint_open else 'Closed (minting paused)'}\n" f" • Base URI: {base_uri if base_uri else 'Not set (tokens will use individual metadata URIs)'}\n" f" • Max Supply: {max_supply if max_supply is not None else 'Unlimited'}\n" f" • Mint Fee: {mint_fee if mint_fee is not None else '0'} wei\n" f" • Fee Token: {mint_fee_token if mint_fee_token else 'WIP (default)'}\n" f" • Fee Recipient: {mint_fee_recipient if mint_fee_recipient else 'Your wallet (default)'}\n" f" • Collection Owner: {owner if owner else 'Your wallet (default)'}\n\n" f"Result Summary:\n" f" • Transaction Hash: {response['tx_hash']}\n" f" • SPG NFT Contract Address: {response['spg_nft_contract']}" ) except Exception as e: return f"Error creating SPG NFT collection: {str(e)}" @mcp.tool() def get_spg_nft_minting_token(spg_nft_contract: str) -> str: """ Get the minting fee required by an SPG NFT contract. Args: spg_nft_contract: The address of the SPG NFT contract Returns: str: Information about the minting fee """ try: fee_info = story_service.get_spg_nft_minting_token(spg_nft_contract) fee_amount = fee_info['mint_fee'] fee_token = fee_info['mint_fee_token'] # Format the fee amount nicely if fee_amount == 0: fee_display = "FREE (0)" else: # Convert from wei to a more readable format fee_in_ether = story_service.web3.from_wei(fee_amount, 'ether') fee_display = f"{fee_amount} wei ({fee_in_ether} IP)" token_display = f"Token at {fee_token}" return ( f"Successfully retrieved SPG NFT contract fee information:\n\n" f"Your Request:\n" f" • SPG Contract Address: {spg_nft_contract}\n\n" f"Minting Fee Details:\n" f" • Fee Amount: {fee_display}\n" f" • Payment Token: {token_display}" ) except Exception as e: return f"Error getting SPG minting fee: {str(e)}" # @mcp.tool() # def mint_nft( # nft_contract: str, # to_address: str, # metadata_uri: str, # metadata_hash: str, # allow_duplicates: bool = False, # ) -> str: # """ # Mint an NFT from an existing SPG collection using the Story Protocol SDK. # Uses the IPAsset.mint() method from the Story Protocol Python SDK to mint NFTs from SPG contracts. # Args: # nft_contract: The address of the SPG NFT contract to mint from # to_address: The recipient address for the minted NFT # metadata_uri: The metadata URI for the NFT # metadata_hash: The metadata hash as a hex string (will be converted to bytes) # allow_duplicates: Whether to allow minting NFTs with duplicate metadata (default: False) # Returns: # str: Result message with transaction details # """ # try: # # Convert hex string to bytes for metadata_hash # if metadata_hash.startswith('0x'): # metadata_hash_bytes = bytes.fromhex(metadata_hash[2:]) # else: # metadata_hash_bytes = bytes.fromhex(metadata_hash) # result = story_service.mint_nft( # nft_contract=nft_contract, # to_address=to_address, # metadata_uri=metadata_uri, # metadata_hash=metadata_hash_bytes, # allow_duplicates=allow_duplicates # ) # return ( # f"Successfully minted NFT:\n" # f"Transaction Hash: {result['txHash']}\n" # f"NFT Contract: {result['nftContract']}\n" # f"Token ID: {result['tokenId']}\n" # f"Recipient: {result['recipient']}\n" # f"Metadata URI: {result['metadataUri']}\n" # f"Allow Duplicates: {allow_duplicates}\n" # f"Gas Used: {result['gasUsed']}\n\n" # f"You can now use this NFT with the register function to create an IP without license terms." # ) # except Exception as e: # return f"Error minting NFT: {str(e)}" # new added but haven't tested # @mcp.tool() # def mint_and_register_ip_asset( # spg_nft_contract: str, # recipient: Optional[str] = None, # ip_metadata: Optional[dict] = None, # allow_duplicates: bool = True, # spg_nft_contract_max_minting_fee: Optional[int] = None, # approve_amount: Optional[int] = None # ) -> str: # """ # Mint an NFT and register it as an IP asset in one transaction (without license terms). # 💰 AUTO-APPROVE: This method automatically approves the required WIP token spending before minting. # The approve_amount parameter controls how much WIP to approve for the spender. # Args: # spg_nft_contract: The address of the SPG NFT contract to mint from # recipient: Optional recipient address (defaults to sender) # ip_metadata: Optional metadata for the IP # ip_metadata_uri: Optional metadata URI for the IP # ip_metadata_hash: Optional metadata hash for the IP # nft_metadata_uri: Optional metadata URI for the NFT # nft_metadata_hash: Optional metadata hash for the NFT # spg_nft_contract_max_minting_fee: Optional maximum minting fee user is willing to pay (in wei). # If not specified, will accept whatever the contract requires. # If specified, will reject if contract requires more than this amount. # approve_amount: Optional; amount to approve for spending WIP tokens (Default is exact amount needed for the transaction). # Returns: # str: Result message with transaction details # """ # try: # response = story_service.mint_and_register_ip_asset( # spg_nft_contract=spg_nft_contract, # recipient=recipient, # ip_metadata=ip_metadata, # allow_duplicates=allow_duplicates, # spg_nft_contract_max_minting_fee=spg_nft_contract_max_minting_fee, # approve_amount=approve_amount, # ) # # Determine which explorer URL to use based on network # explorer_url = ( # "https://explorer.story.foundation" # if story_service.network == "mainnet" # else "https://aeneid.explorer.story.foundation" # ) # # Format fee information for display # fee_info = "" # if response.get('actual_minting_fee') is not None: # actual_fee = response['actual_minting_fee'] # if actual_fee == 0: # fee_info = f"SPG NFT Mint Fee: FREE (0 wei)\n" # else: # fee_in_ether = story_service.web3.from_wei(actual_fee, 'ether') # fee_info = f"SPG NFT Mint Fee: {actual_fee} wei ({fee_in_ether} IP)\n" # return ( # f"Successfully minted and registered IP asset with terms:\n" # f"Transaction Hash: {response.get('tx_hash')}\n" # f"IP ID: {response['ip_id']}\n" # f"Token ID: {response['token_id']}\n" # f"{fee_info}" # f"View the IPA here: {explorer_url}/ipa/{response['ip_id']}" # ) # except Exception as e: # return f"Error minting and registering IP with terms: {str(e)}" @mcp.tool() def register( nft_contract: str, token_id: int, ip_metadata: Optional[dict] = None ) -> str: """ Register an NFT as IP, creating a corresponding IP record. Args: nft_contract: The address of the NFT contract token_id: The token identifier of the NFT ip_metadata: Optional metadata for the IP ip_metadata_uri: Optional metadata URI for the IP ip_metadata_hash: Optional metadata hash for the IP nft_metadata_uri: Optional metadata URI for the NFT nft_metadata_hash: Optional metadata hash for the NFT Returns: str: Result message with transaction hash and IP ID """ try: result = story_service.register( nft_contract=nft_contract, token_id=token_id, ip_metadata=ip_metadata ) if result.get('tx_hash'): return ( f"Successfully registered NFT as IP Asset! Here's your registration summary:\n\n" f"Your Registration:\n" f" • NFT Contract: {nft_contract}\n" f" • Token ID: {token_id}\n" f" • IP Metadata: {'Provided' if ip_metadata else 'Not provided (using defaults)'}\n\n" f"Result Summary:\n" f" • Transaction Hash: {result['tx_hash']}\n" f" • New IP Asset ID: {result['ip_id']}\n\n" ) else: return ( f"NFT was already registered as an IP Asset:\n\n" f"Your Request:\n" f" • NFT Contract: {nft_contract}\n" f" • Token ID: {token_id}\n\n" f"Registration Status:\n" f" • Existing IP Asset ID: {result['ip_id']}\n" f" • Status: Already registered (no transaction needed)" ) except Exception as e: return f"Error registering NFT as IP: {str(e)}" @mcp.tool() def attach_license_terms(ip_id: str, license_terms_id: int, license_template: Optional[str] = None) -> str: """ Attaches license terms to an IP. Args: ip_id: The address of the IP to which the license terms are attached license_terms_id: The ID of the license terms license_template: Optional address of the license template (defaults to the default template) Returns: str: Result message with transaction hash """ try: result = story_service.attach_license_terms( ip_id=ip_id, license_terms_id=license_terms_id, license_template=license_template ) return ( f"Successfully attached license terms {license_terms_id} to IP {ip_id}\n\n" f"License Template: {license_template if license_template else 'Default template'}\n" f"Result Summary:\n" f" • Transaction Hash: {result['tx_hash']}" ) except Exception as e: return f"Error attaching license terms: {str(e)}" # bug in sdk, will update after next sdk release # @mcp.tool() # def register_derivative( # child_ip_id: str, # parent_ip_ids: list, # license_terms_ids: list, # max_minting_fee: int = 0, # max_rts: int = 0, # max_revenue_share: int = 0, # license_template: Optional[str] = None, # approve_amount: Optional[int] = None # ) -> str: # """ # Registers a derivative directly with parent IP's license terms, without needing license tokens. # ⚠️ IMPORTANT: This method makes blockchain transactions. # Please double-check all parameters with the user and get their confirmation before proceeding. # Args: # child_ip_id: The derivative IP ID # parent_ip_ids: The parent IP IDs (list of IP IDs) # license_terms_ids: The IDs of the license terms that the parent IP supports (list of term IDs) # max_minting_fee: The maximum minting fee that the caller is willing to pay in wei (default: 0 = no limit) # max_rts: The maximum number of royalty tokens that can be distributed (max: 100,000,000) # max_revenue_share: The maximum revenue share percentage allowed 0-100 # license_template: [Optional] address of the license template (defaults to the default template) # approve_amount: [Optional] amount to approve for spending WIP tokens in wei (Default is exact amount needed for the transaction). # Returns: # str: Result message with transaction hash # """ # try: # result = story_service.register_derivative( # child_ip_id=child_ip_id, # parent_ip_ids=parent_ip_ids, # license_terms_ids=license_terms_ids, # max_minting_fee=max_minting_fee, # max_rts=max_rts, # max_revenue_share=max_revenue_share, # license_template=license_template, # approve_amount=approve_amount # ) # return f"Successfully registered derivative. Transaction hash: {result['tx_hash']}" # except Exception as e: # return f"Error registering derivative: {str(e)}" @mcp.tool() def pay_royalty_on_behalf( receiver_ip_id: str, payer_ip_id: str, token: str, amount: int ) -> str: """ Allows the function caller to pay royalties to the receiver IP asset on behalf of the payer IP asset. 💰 AUTO-APPROVE: This method automatically approves the exact amount of tokens needed for paying royalties. The system will approve only the required amount to ensure the transaction succeeds. Args: receiver_ip_id: The IP ID that receives the royalties payer_ip_id: The ID of the IP asset that pays the royalties token: The token address to use to pay the royalties amount: The amount to pay in wei Returns: str: Success message with transaction hash """ try: response = story_service.pay_royalty_on_behalf( receiver_ip_id=receiver_ip_id, payer_ip_id=payer_ip_id, token=token, amount=amount ) return ( f"Successfully paid royalty on behalf! Here's what happened:\n\n" f"Your Payment Details:\n" f" • Receiver IP ID: {receiver_ip_id}\n" f" • Payer IP ID: {payer_ip_id}\n" f" • Payment Token: {token}\n" f" • Amount Paid: {amount} wei\n" f" • Transaction Hash: {response['tx_hash']}\n" f" • You paid royalties to {receiver_ip_id} on behalf of {payer_ip_id}" ) except Exception as e: return f"Error paying royalty on behalf: {str(e)}" @mcp.tool() def claim_all_revenue( ancestor_ip_id: str, child_ip_ids: list, license_ids: list, auto_transfer: bool = True, claimer: Optional[str] = None ) -> str: """ Claims all revenue from the child IPs of an ancestor IP, then optionally transfers tokens to the claimer. Args: ancestor_ip_id: The ancestor IP ID child_ip_ids: The list of child IP IDs (must be in same order as license_ids) license_ids: The list of license terms IDs auto_transfer: Whether to automatically transfer the claimed tokens to the claimer claimer: Optional claimer address (defaults to current account) Returns: str: User-friendly summary of the revenue claim process and results """ try: response = story_service.claim_all_revenue( ancestor_ip_id=ancestor_ip_id, child_ip_ids=child_ip_ids, license_ids=license_ids, auto_transfer=auto_transfer, claimer=claimer ) # Return user-friendly formatted string return ( f"Successfully claimed all revenue! Here's your revenue claim summary:\n\n" f"Your Request:\n" f" • Ancestor IP ID: {ancestor_ip_id}\n" f" • Child IP IDs: {child_ip_ids}\n" f" • License IDs: {license_ids}\n" f" • Auto Transfer: {'Enabled' if auto_transfer else 'Disabled'}\n" f" • Claimer: {claimer if claimer else 'Your wallet (default)'}\n\n" f"Result Summary:\n" f" • Transaction Hash: {response.get('tx_hash', 'N/A')}\n" ) except Exception as e: return ( f"❌ Error claiming revenue: {str(e)}\n\n" f"📋 Your Request Details:\n" f" • Ancestor IP ID: {ancestor_ip_id}\n" f" • Child IP IDs: {child_ip_ids}\n" f" • License IDs: {license_ids}\n" f" • Auto Transfer: {'Enabled' if auto_transfer else 'Disabled'}\n" f" • Claimer: {claimer if claimer else 'Your wallet (default)'}\n\n" f"💡 Please check your inputs and try again, or contact support if the issue persists." ) @mcp.tool() def raise_dispute( target_ip_id: str, target_tag: str, cid: str, bond_amount: int, liveness: int = 30 ) -> str: """ Raises a dispute against an IP asset using the Story Protocol SDK. 💰 AUTO-APPROVE: This method automatically approves the exact amount of WIP tokens needed for the dispute bond. The system will approve only the required bond amount to ensure the transaction succeeds. Args: target_ip_id: The IP ID to dispute (must be a valid hex address starting with 0x) target_tag: The dispute tag name. Must be EXACTLY one of these: • "IMPROPER_REGISTRATION" - IP was registered improperly • "IMPROPER_USAGE" - IP is being used improperly • "IMPROPER_PAYMENT" - Payment issues with the IP • "CONTENT_STANDARDS_VIOLATION" - IP violates content standards • "IN_DISPUTE" - General dispute status cid: The Content Identifier (CID) for the dispute evidence, obtained from IPFS (e.g., "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR") bond_amount: The amount of the bond to post for the dispute, as an integer in wei (e.g., 100000000000000000 for 0.1 IP) liveness: The liveness of the dispute in days, must be between 30 and 365 days (defaults to 30 days) Returns: str: Result message with transaction hash and dispute ID 💡 Bond Amount Format: - Use wei (1 IP = 1,000,000,000,000,000,000 wei) - Example: 100000000000000000 wei = 0.1 IP ⚠️ IMPORTANT: Tags must be whitelisted by protocol governance. Use EXACT tag strings above. """ try: result = story_service.raise_dispute( target_ip_id=target_ip_id, target_tag=target_tag, cid=cid, bond_amount=bond_amount, liveness=liveness ) if 'error' in result: return f"Error raising dispute: {result['error']}" dispute_id = result.get('dispute_id', 'Unknown') dispute_tag = result.get('dispute_tag', 'Unknown') liveness_days = result.get('liveness_days', 'Unknown') liveness_seconds = result.get('liveness_seconds', 'Unknown') bond_amount_ip = result.get('bond_amount_ip', 'Unknown') return ( f"⚖️ Successfully raised dispute! Here's your dispute summary:\n\n" f"📋 Your Dispute Details:\n" f" • Target IP ID: {target_ip_id}\n" f" • Dispute Tag: {target_tag}\n" f" • Evidence CID: {cid}\n" f" • Bond Amount: {bond_amount} wei ({bond_amount_ip} IP)\n" f" • Liveness Period: {liveness_days} days ({liveness_seconds} seconds)\n\n" f"🔗 Dispute Registration:\n" f" • Transaction Hash: {result['tx_hash']}\n" f" • Dispute ID: {dispute_id}\n\n" f"💰 Auto-Approval Applied:\n" f" • The system automatically approved {bond_amount} wei of WIP tokens for the dispute bond\n" f" • Your bond has been locked and will be returned if the dispute is successful\n\n" f"🚀 What Happened:\n" f" • Filed a formal dispute against IP {target_ip_id} with tag '{dispute_tag}'\n" f" • Posted your dispute bond to the Story Protocol dispute system\n" f" • Uploaded evidence to IPFS with identifier: {cid}\n" f" • Set dispute resolution period to {liveness_days} days\n\n" f"⏰ What's Next:\n" f" • The dispute is now active and under review\n" f" • Community and validators can examine your evidence\n" f" • Resolution will occur within {liveness_days} days\n" f" • You'll receive your bond back if the dispute is upheld\n\n" f"⚠️ Important Notes:\n" f" • Monitor the dispute status using Dispute ID: {dispute_id}\n" f" • Ensure your evidence at {cid} remains accessible\n" f" • False disputes may result in bond forfeiture" ) except Exception as e: return f"Error raising dispute: {str(e)}" # @mcp.tool() # def pay_royalty_on_behalf_approve(amount: int) -> dict: # """ # Approve a spender to use the wallet's WIP balance for royalty payments. # :param amount int: The amount of WIP to approve for royalty. # :return dict: A dictionary containing the transaction hash. # """ # try: # response = story_service.pay_royalty_on_behalf_approve(amount=amount) # return { # 'tx_hash': response.get('tx_hash') # } # except Exception as e: # return f"Error approving royalty: {str(e)}" # @mcp.tool() # def mint_and_register_ip_approve(amount: int) -> dict: # """ # Approve a spender to use the wallet's WIP balance for minting and registering IP. # :param amount int: The amount of WIP to approve for minting and registering IP. # :return dict: A dictionary containing the transaction hash. # """ # try: # response = story_service.mint_and_register_ip_approve(amount=amount) # return { # 'tx_hash': response.get('tx_hash') # } # except Exception as e: # return f"Error approving mint and register IP: {str(e)}" # @mcp.tool() # def raise_dispute_bond_approve(amount: int) -> dict: # """ # Approve a spender to use the wallet's WIP balance for raise dispute bond payments. # :param amount int: The amount of WIP to approve for raise dispute bond. # :return dict: A dictionary containing the transaction hash. # """ # try: # response = story_service.raise_dispute_bond_approve(amount=amount) # return { # 'tx_hash': response.get('tx_hash') # } # except Exception as e: # return f"Error approving raise dispute bond: {str(e)}" # @mcp.tool() # def mint_license_tokens_approve(amount: int) -> dict: # """ # Approve a spender to use the wallet's WIP balance for license token minting. # :param amount int: The amount of WIP to approve for license token minting. # :return dict: A dictionary containing the transaction hash. # """ # try: # response = story_service.mint_license_tokens_approve(amount=amount) # return { # 'tx_hash': response.get('tx_hash') # } # except Exception as e: # return f"Error approving mint license tokens: {str(e)}" @mcp.tool() def deposit_wip(amount: int) -> str: """ Wraps the selected amount of IP to WIP and deposits to the wallet. Args: amount int: The amount of IP to wrap in wei. Returns: str: User-friendly summary of the wrapping process and results. """ try: response = story_service.deposit_wip(amount=amount) amount_in_ip = story_service.web3.from_wei(amount, 'ether') return ( f"Successfully wrapped {amount_in_ip} IP tokens to WIP!" f"Transaction Hash: {response.get('tx_hash')}" ) except Exception as e: return ( f"❌ Error wrapping IP to WIP: {str(e)}\n\n" f"Your Request Details:\n" f" • Amount to wrap: {amount} wei ({story_service.web3.from_wei(amount, 'ether')} IP)\n" f" • Action: Convert IP tokens to WIP (Wrapped IP) tokens\n\n" f"Please check your IP balance and try again, or contact support if the issue persists." ) @mcp.tool() def get_erc20_token_balance(token_address: str, account_address: Optional[str] = None) -> str: """ Get the balance of any ERC20 token for an account. Args: token_address: The address of the ERC20 token contract (e.g., MERC20: 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E) account_address: [Optional] The address to check balance for (defaults to your wallet address) Returns: str: Balance information including amount in both wei and decimal format Example: # Check MERC20 balance get_erc20_token_balance("0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E") """ try: balance_info = story_service.get_token_balance( token_address=token_address, account_address=account_address ) return ( f"✅ Successfully retrieved token balance information:\n\n" f"📋 Your Request:\n" f" • Token Contract: {token_address}\n" f" • Account: {account_address if account_address else 'Your wallet (default)'}\n\n" f"💰 Balance Details:\n" f" • Token: {balance_info['symbol']} ({balance_info['token_address']})\n" f" • Account Address: {balance_info['account_address']}\n" f" • Balance: {balance_info['balance']} {balance_info['symbol']}\n" f" • Balance (wei): {balance_info['balance_wei']} wei\n" f" • Token Decimals: {balance_info['decimals']}\n\n" f"💡 Understanding Your Balance:\n" f" • The balance shows how many {balance_info['symbol']} tokens you own\n" f" • Wei is the smallest unit (like cents for dollars)\n" f" • You can use these tokens for transactions if the contract supports them\n\n" f"🎉 What You Can Do:\n" f" • Transfer tokens to other addresses\n" f" • Use tokens in Story Protocol if they're supported (like WIP, MERC20)\n" f" • Check transaction history for this token" ) except Exception as e: return f"Error getting token balance: {str(e)}" @mcp.tool() def mint_test_erc20_tokens( token_address: str, amount: int, recipient: Optional[str] = None ) -> str: """ Attempt to mint test ERC20 tokens if the contract has a public mint/faucet function. This is common for testnet tokens like MERC20. Args: token_address: The address of the ERC20 token contract (e.g., MERC20: 0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E) amount: The amount to mint in wei (e.g., 1000000000000000000 for 1 token with 18 decimals) recipient: [Optional] The recipient address (defaults to your wallet) Returns: str: Result message with transaction details Example: # Mint 100 MERC20 tokens (with 18 decimals) mint_test_erc20_tokens("0xF2104833d386a2734a4eB3B8ad6FC6812F29E38E", 100000000000000000000) """ try: result = story_service.mint_test_token( token_address=token_address, amount=amount, recipient=recipient ) function_used = result.get('function_used', 'unknown') amount_display = result.get('amount', 'unknown') if amount_display != 'faucet default': # Try to get decimals and convert for display try: balance_info = story_service.get_token_balance(token_address) decimals = balance_info['decimals'] symbol = balance_info['symbol'] amount_decimal = amount / (10 ** decimals) amount_display = f"{amount_decimal} {symbol}" except: amount_display = f"{amount} wei" return ( f"✅ Successfully minted test tokens! Here's what happened:\n\n" f"📋 Your Request:\n" f" • Token Contract: {token_address}\n" f" • Amount Requested: {amount} wei\n" f" • Recipient: {result['recipient']}\n\n" f"🔗 Transaction Details:\n" f" • Transaction Hash: {result['tx_hash']}\n" f" • Function Used: {function_used}\n" f" • Amount Minted: {amount_display}\n\n" f"🚀 What Happened:\n" f" • Found a public mint function ({function_used}) on the token contract\n" f" • Successfully called the mint function to create new tokens\n" f" • Tokens have been added to the recipient's balance\n\n" f"💡 Next Steps:\n" f" • Check your token balance to confirm the mint was successful\n" f" • You can now use these tokens for testing Story Protocol features\n" f" • Transaction may take a moment to confirm on the blockchain\n\n" f"⚠️ Note: These are test tokens for development/testing purposes only" ) except Exception as e: error_msg = str(e) if "No public mint function found" in error_msg: return ( f"Error: MERC20 at {token_address} doesn't have a public mint function.\n\n" f"Alternative options to get MERC20:\n" f"1. Ask someone with MERC20 to send you some\n" f"2. Check if there's a specific MERC20 faucet website\n" f"3. Contact the Story Protocol team on Discord/Telegram for test tokens\n" f"4. Use a different test token that has a public mint function" ) else: return f"Error minting test tokens: {error_msg}" @mcp.tool() def transfer_wip(to: str, amount: int) -> str: """ Transfers `amount` of WIP to a recipient `to`. Args: to str: The address of the recipient. amount int: The amount of WIP to transfer in wei. Returns: str: User-friendly summary of the transfer process and results. """ try: response = story_service.transfer_wip(to=to, amount=amount) amount_in_ip = story_service.web3.from_wei(amount, 'ether') return ( f"✅ Successfully transferred WIP tokens! Here's what happened:\n\n" f"📋 Your Transfer Details:\n" f" • Recipient: {to}\n" f" • Amount: {amount} wei ({amount_in_ip} WIP)\n" f" • Token Type: WIP (Wrapped IP)\n\n" f"🔗 Transaction Details:\n" f" • Transaction Hash: {response.get('tx_hash')}\n\n" f"💸 Transfer Process:\n" f" • {amount_in_ip} WIP tokens have been sent from your wallet\n" f" • The recipient will receive the tokens once the transaction confirms\n" f" • Your WIP balance has been reduced by {amount_in_ip} WIP\n\n" f"🚀 What Happened:\n" f" • Initiated a WIP token transfer on the Story Protocol network\n" f" • Used the ERC-20 transfer function for secure token movement\n" f" • Transaction is now being processed by the blockchain\n\n" f"💡 Next Steps:\n" f" • Monitor the transaction hash for confirmation status\n" f" • The recipient can check their WIP balance after confirmation\n" f" • You can verify your updated balance in your wallet\n\n" f"🎉 Transfer initiated successfully!" ) except Exception as e: return ( f"❌ Error transferring WIP tokens: {str(e)}\n\n" f"📋 Your Transfer Details:\n" f" • Recipient: {to}\n" f" • Amount: {amount} wei ({story_service.web3.from_wei(amount, 'ether')} WIP)\n" f" • Token Type: WIP (Wrapped IP)\n\n" f"💡 Please check your WIP balance and recipient address, then try again." ) # @mcp.tool() # def register_ip_asset(nft_contract: str, token_id: int, metadata: dict) -> str: # """ # Register an NFT as an IP Asset. # :param nft_contract: NFT contract address # :param token_id: Token ID of the NFT # :param metadata: IP Asset metadata following Story Protocol standard # :return: Registration result message # """ # try: # response = story_service.register_ip_asset(nft_contract, token_id, metadata) # return f"Successfully registered IP asset. IP ID: {response.get('ipId')}" # except Exception as e: # return f"Error registering IP asset: {str(e)}" # @mcp.tool() # def mint_and_register_nft(to_address: str, metadata_uri: str, ip_metadata: dict) -> str: # """ # Mint an NFT and register it as IP in one transaction. # :param to_address: Recipient's wallet address # :param metadata_uri: URI for the NFT metadata # :param ip_metadata: IP Asset metadata following Story Protocol standard # :return: Minting result message # """ # try: # response = story_service.mint_and_register_nft(to_address, metadata_uri, ip_metadata) # return f"Successfully minted and registered NFT to {to_address}. Transaction details: {response}" # except Exception as e: # return f"Error minting and registering NFT: {str(e)}" # @mcp.tool() # def mint_generated_image( # image_data: Union[bytes, str], # name: str, # description: str, # recipient_address: str, # attributes: list = None, # ip_metadata: dict = None # ) -> str: # """ # Upload a generated image, mint it as an NFT, and register it as IP. # :param image_data: Either bytes of image data or URL to image # :param name: Name for the NFT # :param description: Description for the NFT # :param recipient_address: Address to receive the NFT # :param attributes: Optional list of NFT attributes # :param ip_metadata: Optional IP Asset metadata # :return: Result message with URIs and transaction details # """ # try: # response = story_service.mint_generated_image( # image_data=image_data, # name=name, # description=description, # recipient_address=recipient_address, # attributes=attributes, # ip_metadata=ip_metadata # ) # return ( # f"Successfully processed generated image:\n" # f"Image URI: {response['image_uri']}\n" # f"Metadata URI: {response['metadata_uri']}\n" # f"Transaction Details: {response['transaction_details']}" # ) # except Exception as e: # return f"Error processing generated image: {str(e)}" # @mcp.tool() # def register_non_commercial_social_remixing_pil() -> str: # """Register a non-commercial social remixing PIL license.""" # try: # response = story_service.register_non_commercial_social_remixing_pil() # return f"Non-commercial social remixing PIL registered: {response}" # except Exception as e: # return f"Error registering non-commercial PIL: {str(e)}" @mcp.tool() def predict_minting_license_fee( licensor_ip_id: str, license_terms_id: int, amount: int, license_template: Optional[str] = None, receiver: Optional[str] = None, tx_options: Optional[dict] = None, ) -> dict: """ Pre-compute the minting license fee for the given IP, license terms and amount. Args: licensor_ip_id str: The IP ID of the licensor. license_terms_id int: The ID of the license terms. amount int: The amount of license tokens to mint. license_template str: [Optional] The address of the license template, default is Programmable IP License. receiver str: [Optional] The address of the receiver, default is your wallet address. tx_options dict: [Optional] Transaction options. Returns: dict: A dictionary containing the currency token and token amount. """ try: response = story_service.predict_minting_license_fee( licensor_ip_id=licensor_ip_id, license_terms_id=license_terms_id, amount=amount, license_template=license_template, receiver=receiver, tx_options=tx_options ) return ( f"currency_token: {response.get('currency')}\n" f"token_amount: {response.get('amount')}" ) except Exception as e: return f"Error predicting minting license fee: {str(e)}" if __name__ == "__main__": mcp.run()

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/piplabs/story-mcp-hub'

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