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
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()