Source code for agentstr.nostr_mcp_client

import json
import time
from typing import Any

from pynostr.utils import get_public_key

from agentstr.logger import get_logger
from agentstr.nostr_client import NostrClient

logger = get_logger(__name__)


[docs] class NostrMCPClient: """Client for interacting with Model Context Protocol (MCP) servers on Nostr. Discovers and calls tools from MCP servers, handling payments via NWC when needed. """
[docs] def __init__(self, mcp_pubkey: str, nostr_client: NostrClient | None = None, relays: list[str] | None = None, private_key: str | None = None, nwc_str: str | None = None): """Initialize the MCP client. Args: mcp_pubkey: Public key of the MCP server to interact with. nostr_client: Existing NostrClient instance (optional). relays: List of Nostr relay URLs (if no client provided). private_key: Nostr private key (if no client provided). nwc_str: Nostr Wallet Connect string for payments (optional). """ self.client = nostr_client or NostrClient(relays=relays, private_key=private_key, nwc_str=nwc_str) self.mcp_pubkey = get_public_key(mcp_pubkey).hex() self.tool_to_sats_map = {} # Maps tool names to their satoshi costs
[docs] async def list_tools(self) -> dict[str, Any] | None: """Retrieve the list of available tools from the MCP server. Returns: Dictionary of tools with their metadata, or None if not found. """ metadata = await self.client.get_metadata_for_pubkey(self.mcp_pubkey) tools = json.loads(metadata.about) for tool in tools["tools"]: self.tool_to_sats_map[tool["name"]] = tool["satoshis"] return tools
[docs] async def call_tool(self, name: str, arguments: dict[str, Any], timeout: int = 60) -> dict[str, Any] | None: """Call a tool on the MCP server with provided arguments. Args: name: Name of the tool to call. arguments: Dictionary of arguments for the tool. timeout: Timeout in seconds for receiving a response. Returns: Response dictionary from the server, or None if no response. """ response = await self.client.send_direct_message_and_receive_response(self.mcp_pubkey, json.dumps({ "action": "call_tool", "tool_name": name, "arguments": arguments, }), timeout=timeout) if response is None: logger.warning("Tool call returned None") return None message = response.message timestamp = int(time.time()) + 1 logger.debug(f"MCP Client received message: {message}") if isinstance(message, str) and message.startswith("lnbc"): invoice = message.strip() logger.info(f"Paying invoice: {invoice}") await self.client.nwc_relay.try_pay_invoice(invoice=invoice, amount=self.tool_to_sats_map[name]) response = await self.client.receive_direct_message(self.mcp_pubkey, timestamp=timestamp, timeout=timeout) if response: logger.debug(f"MCP Client received response.message: {response.message}") return json.loads(response.message) return None