From 91d83b6a3723e62d30c93988e3beacc563a8ee4d Mon Sep 17 00:00:00 2001 From: Ulugbek <46451363+ulughbeck@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:38:08 +0500 Subject: [PATCH 1/9] chore: add gql to dependencies list, fix license, pyproject.toml (#11) --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1eab5f6..dcc9ea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,8 @@ authors = [ { name = "Razvan-Ion Radulescu" }, { name = "Andrei-Stefan Ghiurtu" }, { name = "Juan Viera Garcia" }, - { name = "Ali Raza" } + { name = "Ali Raza" }, + { name = "Ulugbek Isroilov" } ] description = "Universal Tool Calling Protocol (UTCP) client library for Python" readme = "README.md" @@ -22,6 +23,7 @@ dependencies = [ "aiohttp>=3.8", "mcp>=1.0", "pyyaml>=6.0", + "gql>=3.0", ] classifiers = [ "Development Status :: 4 - Beta", @@ -29,8 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -license = "MPL-2.0" -license-files = ["LICEN[CS]E*"] +license = {text = "MPL-2.0"} [project.optional-dependencies] dev = [ From 4dae4cdc0845c7b5063e1cf5241e16336ee9a90d Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:44:18 +0200 Subject: [PATCH 2/9] v0.1.4 --- pyproject.toml | 2 +- src/utcp/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dcc9ea7..d92fe34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "utcp" -version = "0.1.3" +version = "0.1.4" authors = [ { name = "Razvan-Ion Radulescu" }, { name = "Andrei-Stefan Ghiurtu" }, diff --git a/src/utcp/version.py b/src/utcp/version.py index 28f1838..f05d87b 100644 --- a/src/utcp/version.py +++ b/src/utcp/version.py @@ -2,7 +2,7 @@ import tomli from pathlib import Path -__version__ = "0.1.3" +__version__ = "0.1.4" try: __version__ = version("utcp") except PackageNotFoundError: From f31b53e1cf0fb6392fda09a4bebed30b08fc8a84 Mon Sep 17 00:00:00 2001 From: Ulugbek <46451363+ulughbeck@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:39:47 +0500 Subject: [PATCH 3/9] fix in memory repository variables (#15) * chore: add gql to dependencies list, fix license, pyproject.toml * fix: make tools an instanse variables --- .gitignore | 6 +++++- src/utcp/client/tool_repositories/in_mem_tool_repository.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4e1d022..31bc36d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# OS +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -193,4 +196,5 @@ cython_debug/ .cursorignore .cursorindexingignore -.DS_Store +# Kiro +.kiro \ No newline at end of file diff --git a/src/utcp/client/tool_repositories/in_mem_tool_repository.py b/src/utcp/client/tool_repositories/in_mem_tool_repository.py index 846a1dc..2ca043b 100644 --- a/src/utcp/client/tool_repositories/in_mem_tool_repository.py +++ b/src/utcp/client/tool_repositories/in_mem_tool_repository.py @@ -4,8 +4,9 @@ from utcp.client.tool_repository import ToolRepository class InMemToolRepository(ToolRepository): - tools: List[Tool] = [] - tool_per_provider: Dict[str, Tuple[Provider, List[Tool]]] = {} + def __init__(self): + self.tools: List[Tool] = [] + self.tool_per_provider: Dict[str, Tuple[Provider, List[Tool]]] = {} async def save_provider_with_tools(self, provider: Provider, tools: List[Tool]) -> None: self.tools.extend(tools) From 48cd65f919b97f5d51c90cacc9e8a5417f82312a Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:08:56 +0200 Subject: [PATCH 4/9] v0.1.5 --- pyproject.toml | 4 ++-- src/utcp/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d92fe34..7f85044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "utcp" -version = "0.1.4" +version = "0.1.5" authors = [ { name = "Razvan-Ion Radulescu" }, { name = "Andrei-Stefan Ghiurtu" }, @@ -31,7 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent", ] -license = {text = "MPL-2.0"} +license = "MPL-2.0" [project.optional-dependencies] dev = [ diff --git a/src/utcp/version.py b/src/utcp/version.py index f05d87b..edf885e 100644 --- a/src/utcp/version.py +++ b/src/utcp/version.py @@ -2,7 +2,7 @@ import tomli from pathlib import Path -__version__ = "0.1.4" +__version__ = "0.1.5" try: __version__ = version("utcp") except PackageNotFoundError: From ac5570c06c9dd35f2f4f5ae9e6896dca0746204b Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:11:05 +0200 Subject: [PATCH 5/9] v0.1.6 --- pyproject.toml | 2 +- src/utcp/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7f85044..8711c3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "utcp" -version = "0.1.5" +version = "0.1.6" authors = [ { name = "Razvan-Ion Radulescu" }, { name = "Andrei-Stefan Ghiurtu" }, diff --git a/src/utcp/version.py b/src/utcp/version.py index edf885e..75aaa95 100644 --- a/src/utcp/version.py +++ b/src/utcp/version.py @@ -2,7 +2,7 @@ import tomli from pathlib import Path -__version__ = "0.1.5" +__version__ = "0.1.6" try: __version__ = version("utcp") except PackageNotFoundError: From 3563bdb41285c309a35139b1fe72e64d2157b48a Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:42:46 +0200 Subject: [PATCH 6/9] v0.1.7 Fix issue of shared memory of UTCPClients --- src/utcp/client/utcp_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utcp/client/utcp_client.py b/src/utcp/client/utcp_client.py index 91c7a6d..a13f1a7 100644 --- a/src/utcp/client/utcp_client.py +++ b/src/utcp/client/utcp_client.py @@ -98,7 +98,7 @@ def __init__(self, config: UtcpClientConfig, tool_repository: ToolRepository, se self.config = config @classmethod - async def create(cls, config: Optional[Union[Dict[str, Any], UtcpClientConfig]] = None, tool_repository: ToolRepository = InMemToolRepository(), search_strategy: Optional[ToolSearchStrategy] = None) -> 'UtcpClient': + async def create(cls, config: Optional[Union[Dict[str, Any], UtcpClientConfig]] = None, tool_repository: Optional[ToolRepository] = None, search_strategy: Optional[ToolSearchStrategy] = None) -> 'UtcpClient': """ Create a new instance of UtcpClient. @@ -110,6 +110,8 @@ async def create(cls, config: Optional[Union[Dict[str, Any], UtcpClientConfig]] Returns: A new instance of UtcpClient. """ + if tool_repository is None: + tool_repository = InMemToolRepository() if search_strategy is None: search_strategy = TagSearchStrategy(tool_repository) if config is None: From 8e4e12661cc832b0213cc1b7f7f8788e856b57d8 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:43:25 +0200 Subject: [PATCH 7/9] Update version --- pyproject.toml | 2 +- src/utcp/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8711c3d..bd71bab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "utcp" -version = "0.1.6" +version = "0.1.7" authors = [ { name = "Razvan-Ion Radulescu" }, { name = "Andrei-Stefan Ghiurtu" }, diff --git a/src/utcp/version.py b/src/utcp/version.py index 75aaa95..2a776a7 100644 --- a/src/utcp/version.py +++ b/src/utcp/version.py @@ -2,7 +2,7 @@ import tomli from pathlib import Path -__version__ = "0.1.6" +__version__ = "0.1.7" try: __version__ = version("utcp") except PackageNotFoundError: From 99e7e1a541a326bbdca17f1c80e4fe97e0bf254c Mon Sep 17 00:00:00 2001 From: Andrei Ghiurtu Date: Sun, 27 Jul 2025 11:47:14 +0300 Subject: [PATCH 8/9] Add secret manager example (#21) * Add secret manager example * Remove load from env parser --- example/src/secret_manager_example/client.py | 46 +++++++++++++++++++ .../src/secret_manager_example/example.env | 1 + .../src/secret_manager_example/providers.json | 9 ++++ example/src/simple_example/client.py | 6 ++- src/utcp/client/utcp_client_config.py | 11 +---- 5 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 example/src/secret_manager_example/client.py create mode 100644 example/src/secret_manager_example/example.env create mode 100644 example/src/secret_manager_example/providers.json diff --git a/example/src/secret_manager_example/client.py b/example/src/secret_manager_example/client.py new file mode 100644 index 0000000..8b20421 --- /dev/null +++ b/example/src/secret_manager_example/client.py @@ -0,0 +1,46 @@ +import asyncio +from os import getcwd +from typing import Optional +from google.cloud import secretmanager +from utcp.client.utcp_client import UtcpClient +from utcp.client.utcp_client_config import UtcpClientConfig, UtcpVariablesConfig + + +class UtcpGcpSecretManager(UtcpVariablesConfig): + """Configuration for UTCP GCP Secret Manager client.""" + def __init__(self, project_id: str): + self.secrets_path = f"projects/{project_id}/secrets" + self.client = secretmanager.SecretManagerServiceClient() + + def get(self, key: str, version: str = "latest") -> Optional[str]: + """Get a specific secret by key.""" + request = {"name": f"{self.secrets_path}/{key}/versions/{version}"} + response = self.client.access_secret_version(request=request) + secret_value = response.payload.data.decode('UTF-8') + return secret_value + +async def main(): + client: UtcpClient = await UtcpClient.create( + config=UtcpClientConfig( + providers_file_path=str(getcwd() + "/providers.json"), + load_variables_from=[ + UtcpGcpSecretManager(project_id="your-gcp-project-id") + ] + ) + ) + + # List all available tools + print("Registered tools:") + for tool in await client.tool_repository.get_tools(): + print(f" - {tool.name}") + + # Call one of the tools + tool_to_call = (await client.tool_repository.get_tools())[0].name + args = {"body": {"value": "test"}} + + result = await client.call_tool(tool_to_call, args) + print(f"\nTool call result for '{tool_to_call}':") + print(result) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/example/src/secret_manager_example/example.env b/example/src/secret_manager_example/example.env new file mode 100644 index 0000000..ea925dd --- /dev/null +++ b/example/src/secret_manager_example/example.env @@ -0,0 +1 @@ +NEWS_API_KEY=INSERT_YOUR_NEWS_API_KEY_HERE \ No newline at end of file diff --git a/example/src/secret_manager_example/providers.json b/example/src/secret_manager_example/providers.json new file mode 100644 index 0000000..a5db2ba --- /dev/null +++ b/example/src/secret_manager_example/providers.json @@ -0,0 +1,9 @@ +[ + { + "name": "test_provider", + "provider_type": "http", + "http_method": "GET", + "type": "utcp", + "url": "http://localhost:8080/utcp" + } +] \ No newline at end of file diff --git a/example/src/simple_example/client.py b/example/src/simple_example/client.py index 58c38dc..120df2e 100644 --- a/example/src/simple_example/client.py +++ b/example/src/simple_example/client.py @@ -1,11 +1,15 @@ import asyncio from os import getcwd from utcp.client.utcp_client import UtcpClient +from utcp.client.utcp_client_config import UtcpClientConfig async def main(): client: UtcpClient = await UtcpClient.create( - config={"providers_file_path": "./providers.json"} + config=UtcpClientConfig( + providers_file_path=str(getcwd() + "/providers.json"), + load_variables_from=[] + ) ) # List all available tools diff --git a/src/utcp/client/utcp_client_config.py b/src/utcp/client/utcp_client_config.py index fd96552..34bb011 100644 --- a/src/utcp/client/utcp_client_config.py +++ b/src/utcp/client/utcp_client_config.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Literal, TypedDict +from typing import Optional, List, Dict, Literal from dotenv import dotenv_values class UtcpVariableNotFound(Exception): @@ -13,10 +13,6 @@ def __init__(self, variable_name: str): class UtcpVariablesConfig(BaseModel, ABC): type: Literal["dotenv"] = "dotenv" - @abstractmethod - def load(self) -> Dict[str, str]: - pass - @abstractmethod def get(self, key: str) -> Optional[str]: pass @@ -24,11 +20,8 @@ def get(self, key: str) -> Optional[str]: class UtcpDotEnv(UtcpVariablesConfig): env_file_path: str - def load(self) -> Dict[str, str]: - return dotenv_values(self.env_file_path) - def get(self, key: str) -> Optional[str]: - return self.load().get(key) + return dotenv_values(self.env_file_path).get(key) class UtcpClientConfig(BaseModel): variables: Optional[Dict[str, str]] = Field(default_factory=dict) From 86ea413e5c983a6f4b7cbef909e49014329cdc62 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Sun, 27 Jul 2025 10:58:43 +0200 Subject: [PATCH 9/9] Revert "Add secret manager example (#21)" (#30) This reverts commit 99e7e1a541a326bbdca17f1c80e4fe97e0bf254c. --- example/src/secret_manager_example/client.py | 46 ------------------- .../src/secret_manager_example/example.env | 1 - .../src/secret_manager_example/providers.json | 9 ---- example/src/simple_example/client.py | 6 +-- src/utcp/client/utcp_client_config.py | 11 ++++- 5 files changed, 10 insertions(+), 63 deletions(-) delete mode 100644 example/src/secret_manager_example/client.py delete mode 100644 example/src/secret_manager_example/example.env delete mode 100644 example/src/secret_manager_example/providers.json diff --git a/example/src/secret_manager_example/client.py b/example/src/secret_manager_example/client.py deleted file mode 100644 index 8b20421..0000000 --- a/example/src/secret_manager_example/client.py +++ /dev/null @@ -1,46 +0,0 @@ -import asyncio -from os import getcwd -from typing import Optional -from google.cloud import secretmanager -from utcp.client.utcp_client import UtcpClient -from utcp.client.utcp_client_config import UtcpClientConfig, UtcpVariablesConfig - - -class UtcpGcpSecretManager(UtcpVariablesConfig): - """Configuration for UTCP GCP Secret Manager client.""" - def __init__(self, project_id: str): - self.secrets_path = f"projects/{project_id}/secrets" - self.client = secretmanager.SecretManagerServiceClient() - - def get(self, key: str, version: str = "latest") -> Optional[str]: - """Get a specific secret by key.""" - request = {"name": f"{self.secrets_path}/{key}/versions/{version}"} - response = self.client.access_secret_version(request=request) - secret_value = response.payload.data.decode('UTF-8') - return secret_value - -async def main(): - client: UtcpClient = await UtcpClient.create( - config=UtcpClientConfig( - providers_file_path=str(getcwd() + "/providers.json"), - load_variables_from=[ - UtcpGcpSecretManager(project_id="your-gcp-project-id") - ] - ) - ) - - # List all available tools - print("Registered tools:") - for tool in await client.tool_repository.get_tools(): - print(f" - {tool.name}") - - # Call one of the tools - tool_to_call = (await client.tool_repository.get_tools())[0].name - args = {"body": {"value": "test"}} - - result = await client.call_tool(tool_to_call, args) - print(f"\nTool call result for '{tool_to_call}':") - print(result) - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/example/src/secret_manager_example/example.env b/example/src/secret_manager_example/example.env deleted file mode 100644 index ea925dd..0000000 --- a/example/src/secret_manager_example/example.env +++ /dev/null @@ -1 +0,0 @@ -NEWS_API_KEY=INSERT_YOUR_NEWS_API_KEY_HERE \ No newline at end of file diff --git a/example/src/secret_manager_example/providers.json b/example/src/secret_manager_example/providers.json deleted file mode 100644 index a5db2ba..0000000 --- a/example/src/secret_manager_example/providers.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "name": "test_provider", - "provider_type": "http", - "http_method": "GET", - "type": "utcp", - "url": "http://localhost:8080/utcp" - } -] \ No newline at end of file diff --git a/example/src/simple_example/client.py b/example/src/simple_example/client.py index 120df2e..58c38dc 100644 --- a/example/src/simple_example/client.py +++ b/example/src/simple_example/client.py @@ -1,15 +1,11 @@ import asyncio from os import getcwd from utcp.client.utcp_client import UtcpClient -from utcp.client.utcp_client_config import UtcpClientConfig async def main(): client: UtcpClient = await UtcpClient.create( - config=UtcpClientConfig( - providers_file_path=str(getcwd() + "/providers.json"), - load_variables_from=[] - ) + config={"providers_file_path": "./providers.json"} ) # List all available tools diff --git a/src/utcp/client/utcp_client_config.py b/src/utcp/client/utcp_client_config.py index 34bb011..fd96552 100644 --- a/src/utcp/client/utcp_client_config.py +++ b/src/utcp/client/utcp_client_config.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from pydantic import BaseModel, Field -from typing import Optional, List, Dict, Literal +from typing import Optional, List, Dict, Literal, TypedDict from dotenv import dotenv_values class UtcpVariableNotFound(Exception): @@ -13,6 +13,10 @@ def __init__(self, variable_name: str): class UtcpVariablesConfig(BaseModel, ABC): type: Literal["dotenv"] = "dotenv" + @abstractmethod + def load(self) -> Dict[str, str]: + pass + @abstractmethod def get(self, key: str) -> Optional[str]: pass @@ -20,8 +24,11 @@ def get(self, key: str) -> Optional[str]: class UtcpDotEnv(UtcpVariablesConfig): env_file_path: str + def load(self) -> Dict[str, str]: + return dotenv_values(self.env_file_path) + def get(self, key: str) -> Optional[str]: - return dotenv_values(self.env_file_path).get(key) + return self.load().get(key) class UtcpClientConfig(BaseModel): variables: Optional[Dict[str, str]] = Field(default_factory=dict)