From 4fff03bed52242eae735b1c4a8d43d273e5a66e4 Mon Sep 17 00:00:00 2001 From: eyalz Date: Wed, 27 Aug 2025 14:46:26 +0300 Subject: [PATCH] Add supoprt in binary tool responses --- README.md | 14 +++ engine/globals.py | 21 +++- engine/template_engine.py | 4 +- mcp_client/__init__.py | 3 + mcp_client/binary_data_handler.py | 76 ++++++++++++++ prompt/prompt_helper.py | 24 +++-- tests/files/image1.avif | Bin 0 -> 31645 bytes tests/mcp_test_server.py | 10 +- tests/templates/spec10.md | 7 ++ tests/templates/spec9.md | 2 +- tests/test_binary_data_handler.py | 160 ++++++++++++++++++++++++++++++ 11 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 mcp_client/binary_data_handler.py create mode 100644 tests/files/image1.avif create mode 100644 tests/templates/spec10.md create mode 100644 tests/test_binary_data_handler.py diff --git a/README.md b/README.md index 7dbee3f..a898227 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,20 @@ You can also filter resources to mask sensitive information: {{ support_ticket_info | regex_replace(r'\b[\w.+-]+@[\w.-]+\.\w+\b', '[EMAIL_REDACTED]') }} ``` +### Interactively selecting MCP resources and tools + +Template variables can be given values either directly or by selecting an MCP tool to call. For example: + +``` +# Spec Template (spec.md) + +## Task description +{{ task }} +``` + +Running create-spec will then prompt you to either provide a direct value for `task` or select an MCP server, tool and args to call to fetch the task description. + + ### Initialize a project ``` cxk init diff --git a/engine/globals.py b/engine/globals.py index cf4ce30..b6934a4 100644 --- a/engine/globals.py +++ b/engine/globals.py @@ -5,12 +5,13 @@ from mcp.shared.metadata_utils import get_display_name from pydantic import AnyUrl -from mcp_client import get_client_session_by_server +from mcp_client import get_client_session_by_server, handle_binary_content from prompt import PromptHelper +from state import State from util.parse import parse_input_string -def create_mcp_tool_function(prompt_helper: PromptHelper): +def create_mcp_tool_function(prompt_helper: PromptHelper, state: State): """Create a call_mcp_tool function with state bound.""" async def call_mcp_tool(server: str, tool_name: str, args: dict) -> str | dict[str, Any]: @@ -26,6 +27,13 @@ async def call_mcp_tool(server: str, tool_name: str, args: dict) -> str | dict[s result_unstructured = result.content[0] if isinstance(result_unstructured, types.TextContent): return parse_input_string(result_unstructured.text) + elif isinstance(result_unstructured, types.ImageContent | types.AudioContent): + if state.config_dir: + file_path = handle_binary_content(state.config_dir, result_unstructured) + return file_path if file_path else "" + else: + logging.error("Config directory not available for binary data storage") + return "" else: return "" except Exception as e: @@ -38,7 +46,7 @@ async def call_mcp_tool(server: str, tool_name: str, args: dict) -> str | dict[s return call_mcp_tool -def create_mcp_resource_function(): +def create_mcp_resource_function(state: State): """Create a get_mcp_resource function with state bound.""" async def get_mcp_resource(server: str, resource_uri: str) -> str | dict[str, Any]: @@ -50,6 +58,13 @@ async def get_mcp_resource(server: str, resource_uri: str) -> str | dict[str, An result_unstructured = resource.contents[0] if isinstance(result_unstructured, types.TextResourceContents): return parse_input_string(result_unstructured.text) + elif isinstance(result_unstructured, types.BlobResourceContents): + if state.config_dir: + file_path = handle_binary_content(state.config_dir, result_unstructured) + return file_path if file_path else "" + else: + logging.error("Config directory not available for binary data storage") + return "" else: logging.debug(f"Resource {resource_uri} returned non-text content") return "" diff --git a/engine/template_engine.py b/engine/template_engine.py index f5d2e4e..966f50b 100644 --- a/engine/template_engine.py +++ b/engine/template_engine.py @@ -31,8 +31,8 @@ def __init__( self._prompt_helper = prompt_helper # Add global functions to env to support MCP tools and resources - self.env.globals["call_tool"] = create_mcp_tool_function(self._prompt_helper) - self.env.globals["get_resource"] = create_mcp_resource_function() + self.env.globals["call_tool"] = create_mcp_tool_function(self._prompt_helper, self._state) + self.env.globals["get_resource"] = create_mcp_resource_function(self._state) @classmethod def from_file(cls, path: str | Path, state: State, prompt_helper: PromptHelper) -> "TemplateEngine": diff --git a/mcp_client/__init__.py b/mcp_client/__init__.py index d5b51e5..b9e04a2 100644 --- a/mcp_client/__init__.py +++ b/mcp_client/__init__.py @@ -1,5 +1,6 @@ """MCP (Model Context Protocol) client implementation.""" +from .binary_data_handler import handle_binary_content, save_binary_data_to_file from .client_session_provider import get_client_session_by_server from .config import HTTPServerConfig, MCPServersConfig, SSEServerConfig, StdioServerConfig from .session_manager import MCPSessionManager, get_session_manager @@ -14,4 +15,6 @@ "KeychainTokenStorageWithFallback", "get_session_manager", "get_client_session_by_server", + "handle_binary_content", + "save_binary_data_to_file", ] diff --git a/mcp_client/binary_data_handler.py b/mcp_client/binary_data_handler.py new file mode 100644 index 0000000..1e18cef --- /dev/null +++ b/mcp_client/binary_data_handler.py @@ -0,0 +1,76 @@ +import base64 +import logging +import mimetypes +from pathlib import Path +from uuid import uuid4 + +from mcp import types + + +def save_binary_data_to_file(config_dir: Path, data: str, mime_type: str, file_prefix: str = "binary") -> str: + """ + Save binary data (base64-encoded) to a file in the config directory. + + Args: + config_dir: The .cxk config directory path + data: Base64-encoded binary data + mime_type: MIME type of the data (e.g., 'image/png', 'audio/wav') + file_prefix: Prefix for the generated filename + + Returns: + Relative path to the saved file from the project root + + Raises: + Exception: If file save fails (errors are logged but not re-raised) + """ + try: + # Create files directory if it doesn't exist + files_dir = config_dir / "files" + files_dir.mkdir(exist_ok=True) + + # Generate unique filename with appropriate extension + extension = mimetypes.guess_extension(mime_type) or "" + filename = f"{file_prefix}_{uuid4().hex[:8]}{extension}" + file_path = files_dir / filename + + # Decode base64 data and write to file + binary_data = base64.b64decode(data) + with open(file_path, "wb") as f: + f.write(binary_data) + + # Return relative path from project root + relative_path = file_path.relative_to(config_dir.parent) + logging.info(f"Saved binary data to {relative_path}") + return str(relative_path) + + except Exception as e: + error_msg = f"Failed to save binary data: {e}" + logging.error(error_msg) + raise Exception(error_msg) from e + + +def handle_binary_content(config_dir: Path, content) -> str: + """ + Handle binary content from MCP responses, saving to file and returning path. + + Args: + config_dir: The .cxk config directory path + content: MCP content object (ImageContent, AudioContent, or BlobResourceContents) + + Returns: + Relative path to the saved file or empty string if handling fails + """ + try: + if isinstance(content, types.ImageContent): + return save_binary_data_to_file(config_dir, content.data, content.mimeType, "image") + elif isinstance(content, types.AudioContent): + return save_binary_data_to_file(config_dir, content.data, content.mimeType, "audio") + elif isinstance(content, types.BlobResourceContents): + # For blob resources, we don't have mime type info, so we'll save as generic file + return save_binary_data_to_file(config_dir, content.blob, "application/octet-stream", "blob") + else: + logging.warning(f"Unsupported binary content type: {type(content)}") + return "" + except Exception as e: + logging.error(f"Error handling binary content: {e}") + return "" diff --git a/prompt/prompt_helper.py b/prompt/prompt_helper.py index 4a210e0..82bd3da 100644 --- a/prompt/prompt_helper.py +++ b/prompt/prompt_helper.py @@ -1,8 +1,9 @@ import logging import questionary +from mcp import types -from mcp_client import get_session_manager +from mcp_client import get_session_manager, handle_binary_content from state import State @@ -221,11 +222,20 @@ async def _collect_var_value_from_mcp(self, var_name: str) -> str: # Handle list of content items content_parts = [] for item in result.content: - # Try to get text content first (safely) - text_content = getattr(item, "text", None) - if text_content: - content_parts.append(str(text_content)) - # For non-text content, convert to string + # Handle binary content types + if isinstance(item, types.ImageContent | types.AudioContent): + if self._state.config_dir: + file_path = handle_binary_content(self._state.config_dir, item) + if file_path: + content_parts.append(file_path) + else: + logging.error(f"Failed to save binary content for variable '{var_name}'") + else: + logging.error("Config directory not available for binary data storage") + # Handle text content + elif isinstance(item, types.TextContent): + content_parts.append(str(item.text)) + # For other content types, convert to string else: content_parts.append(str(item)) content = "\n".join(content_parts) @@ -279,6 +289,6 @@ async def _select_mcp_tool(self, tools) -> str | None: choice_title = f"{tool.name} - {description}" choices.append(questionary.Choice(choice_title, tool.name)) - tool_name = await questionary.select("Select a tool:", choices=choices).ask_async() + tool_name = await questionary.select("Select a tool:", choices=choices, use_search_filter=True).ask_async() return tool_name diff --git a/tests/files/image1.avif b/tests/files/image1.avif new file mode 100644 index 0000000000000000000000000000000000000000..d330869d8db3f2ae34553d4ff4198ab3bef6444c GIT binary patch literal 31645 zcmXuIQ;;ys&NVu=ZQHgz&)BwY+qP}nwr$(Cect_@`qNce$y%u-SKSv4002N>>g-`} z;A&wC@Spt;Z7fU~Z7d8-Wds?8{-YjkOq>n=r~PLX=El~J|9=PoU~ggM{Qu(rp|yqK z|7!zhZ{ck7e;VL_9?!zs&gg%c2mk=!f9HQ501yiRz%%H-n8L!s_Wx%8pMv#YVgmHP z;{R$4Tp1XJ?X2zoZ=|J#y`%kqyp)B#k==iYV&Pfq zzz`4+{{f7FE2D4#2+aQ|WFtFkM_VIn_y0P>0)YAdN1jB4b90Xp�ff5ysanL%91gmM&@4u$aAS8 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 literal 0 HcmV?d00001 diff --git a/tests/mcp_test_server.py b/tests/mcp_test_server.py index 987be3e..3527b0f 100644 --- a/tests/mcp_test_server.py +++ b/tests/mcp_test_server.py @@ -4,7 +4,7 @@ from typing import Any -from mcp.server.fastmcp import FastMCP +from mcp.server.fastmcp import FastMCP, Image # Create an MCP server mcp = FastMCP("Test-Server", "1.0.0") @@ -32,3 +32,11 @@ def jsonTest(cloudId: str, ticketId: str, optional_other: str | None = None) -> def get_greeting(name: str) -> str: """Get a personalized greeting""" return f"Hello, {name}!" + + +# Add a binary tool (tests/files/image1.avif) +@mcp.tool() +def get_blob() -> Image: + """Get a binary blob test tool""" + with open("tests/files/image1.avif", "rb") as f: + return Image(data=f.read(), format="avif") diff --git a/tests/templates/spec10.md b/tests/templates/spec10.md new file mode 100644 index 0000000..6b41474 --- /dev/null +++ b/tests/templates/spec10.md @@ -0,0 +1,7 @@ +# Task Template + +## Image links + +{% set image = call_tool('test-mcp', 'get_blob', {}) %} + +{{ image }} \ No newline at end of file diff --git a/tests/templates/spec9.md b/tests/templates/spec9.md index 7adae2f..d361e03 100644 --- a/tests/templates/spec9.md +++ b/tests/templates/spec9.md @@ -1,6 +1,6 @@ # Task Template -## Issue description +## Image links {% set issue = call_tool('github', 'get_issue', {'issue_number': 17, 'owner': 'eyalzh', 'repo': 'browser-control-mcp'}) %} diff --git a/tests/test_binary_data_handler.py b/tests/test_binary_data_handler.py new file mode 100644 index 0000000..95776da --- /dev/null +++ b/tests/test_binary_data_handler.py @@ -0,0 +1,160 @@ +"""Tests for binary data handling functionality.""" + +import base64 +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock + +import pytest +from mcp import types +from pydantic import AnyUrl + +from mcp_client.binary_data_handler import handle_binary_content, save_binary_data_to_file + + +def test_save_binary_data_to_file(): + """Test saving binary data to file.""" + # Create a temporary directory + with TemporaryDirectory() as temp_dir: + config_dir = Path(temp_dir) / ".cxk" + config_dir.mkdir() + + # Create some test binary data + test_data = b"test binary data" + encoded_data = base64.b64encode(test_data).decode("utf-8") + + # Save the data + file_path = save_binary_data_to_file(config_dir, encoded_data, "image/png", "test") + + # Verify the file was created and contains correct data + full_path = Path(temp_dir) / file_path + assert full_path.exists() + + with open(full_path, "rb") as f: + saved_data = f.read() + + assert saved_data == test_data + assert file_path.startswith(".cxk/files/test_") + assert file_path.endswith(".png") + + +def test_handle_image_content(): + """Test handling ImageContent.""" + with TemporaryDirectory() as temp_dir: + config_dir = Path(temp_dir) / ".cxk" + config_dir.mkdir() + + # Create mock ImageContent + test_data = b"fake image data" + encoded_data = base64.b64encode(test_data).decode("utf-8") + + image_content = types.ImageContent( + type="image", + data=encoded_data, + mimeType="image/jpeg" + ) + + # Handle the content + file_path = handle_binary_content(config_dir, image_content) + + # Verify result + assert file_path + assert file_path.startswith(".cxk/files/image_") + assert file_path.endswith(".jpg") # mimetypes maps image/jpeg to .jpg + + # Verify file contents + full_path = Path(temp_dir) / file_path + with open(full_path, "rb") as f: + saved_data = f.read() + assert saved_data == test_data + + +def test_handle_audio_content(): + """Test handling AudioContent.""" + with TemporaryDirectory() as temp_dir: + config_dir = Path(temp_dir) / ".cxk" + config_dir.mkdir() + + # Create mock AudioContent + test_data = b"fake audio data" + encoded_data = base64.b64encode(test_data).decode("utf-8") + + audio_content = types.AudioContent( + type="audio", + data=encoded_data, + mimeType="audio/mpeg" # This should map to .mp3 + ) + + # Handle the content + file_path = handle_binary_content(config_dir, audio_content) + + # Verify result + assert file_path + assert file_path.startswith(".cxk/files/audio_") + # mimetypes may not always find an extension, so just check prefix + assert "audio_" in file_path + + # Verify file contents + full_path = Path(temp_dir) / file_path + with open(full_path, "rb") as f: + saved_data = f.read() + assert saved_data == test_data + + +def test_handle_blob_resource_contents(): + """Test handling BlobResourceContents.""" + with TemporaryDirectory() as temp_dir: + config_dir = Path(temp_dir) / ".cxk" + config_dir.mkdir() + + # Create mock BlobResourceContents + test_data = b"fake blob data" + encoded_data = base64.b64encode(test_data).decode("utf-8") + + blob_content = types.BlobResourceContents( + blob=encoded_data, + uri=AnyUrl("test://blob"), + mimeType="application/octet-stream" + ) + + # Handle the content + file_path = handle_binary_content(config_dir, blob_content) + + # Verify result + assert file_path + assert file_path.startswith(".cxk/files/blob_") + + # Verify file contents + full_path = Path(temp_dir) / file_path + with open(full_path, "rb") as f: + saved_data = f.read() + assert saved_data == test_data + + +def test_handle_unsupported_content(): + """Test handling unsupported content types.""" + with TemporaryDirectory() as temp_dir: + config_dir = Path(temp_dir) / ".cxk" + config_dir.mkdir() + + # Create mock unsupported content + unsupported_content = Mock() + + # Handle the content + file_path = handle_binary_content(config_dir, unsupported_content) + + # Should return empty string for unsupported types + assert file_path == "" + + + +def test_save_binary_data_error_handling(): + """Test error handling when save fails.""" + # Use a non-existent directory to trigger an error + config_dir = Path("/nonexistent/path") + + # This should raise an exception + with pytest.raises(Exception) as exc_info: + save_binary_data_to_file(config_dir, "dGVzdA==", "image/png") + + assert "Failed to save binary data" in str(exc_info.value)