Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ pydantic~=2.11.2
PyJWT~=2.9.0
cryptography~=45.0.6
setuptools~=80.9.0
langchain-core~=0.3.74
mcp~=1.12.4
40 changes: 38 additions & 2 deletions scalekit/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
Modifier, ModifierType, ToolNames,
apply_pre_modifiers, apply_post_modifiers
)
from scalekit.actions.frameworks.langchain import LangChain
from scalekit.common.exceptions import ScalekitNotFoundException


Expand Down Expand Up @@ -39,9 +38,46 @@ def __init__(self,tools_client, connected_accounts_client, mcp_client=None):
self.connected_accounts = connected_accounts_client
self.mcp = mcp_client
self._modifiers: List[Modifier] = []
self._google = None
self._langchain = None

# Initialize LangChain with tools client and execute callback
self.langchain = LangChain(tools_client, execute_callback=self.execute_tool)


# Initialize Google ADK with tools client and execute callback
#self.google = GoogleADK(tools_client, execute_callback=self.execute_tool)

@property
def langchain(self):
"""Get LangChain framework instance"""
if self._langchain is None:
try:
from scalekit.actions.frameworks.langchain import LangChain
self._langchain = LangChain(self.tools, execute_callback=self.execute_tool)
except ImportError as e:
raise ImportError(
"LangChain not found. To use LangChain integration, please install:\n"
"pip install langchain\n\n"
"For more information, see: https://python.langchain.com/docs/\n"
)
return self._langchain


@property
def google(self):
"""Get Google ADK framework instance"""
if self._google is None:
try:
from scalekit.actions.frameworks.google_adk import GoogleADK
self._google = GoogleADK(self.tools, execute_callback=self.execute_tool)
except ImportError as e:
raise ImportError(
"Google ADK not found. To use Google ADK integration, please install:\n"
"pip install google-adk\n\n"
"For more information, see: https://google.github.io/adk-docs/\n"
)

return self._google

def execute_tool(
self,
Expand Down
81 changes: 81 additions & 0 deletions scalekit/actions/frameworks/google_adk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Optional, Any, Dict, List, Callable
from scalekit.tools import ToolsClient
from scalekit.v1.tools.tools_pb2 import Filter, ScopedToolFilter
from scalekit.actions.frameworks.types.google_adk_tool import (
ScalekitGoogleAdkTool,
)
from scalekit.actions.frameworks.util import build_mcp_tool_from_spec, struct_to_dict
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra space before 'build_mcp_tool_from_spec'. Should have single space after 'import'.

Copilot uses AI. Check for mistakes.



class GoogleADK:
def __init__(self, tools_client: ToolsClient, execute_callback: Callable):
if not execute_callback:
raise ValueError("execute_callback is required. GoogleADK must be initialized with ActionClient's execute_tool method.")

self.tools = tools_client
self.execute_callback = execute_callback


def get_tools(
self,
identifier: str,
providers: Optional[List[str]] = None,
tool_names: Optional[List[str]] = None,
connection_names: Optional[List[str]] = None,
page_size: Optional[int] = None,
page_token: Optional[str] = None
) -> List[ScalekitGoogleAdkTool]:
"""
Get scoped tools from Scalekit and convert them to Google ADK compatible tools
:param identifier: Identifier to scope the tools list
:param providers: List of provider names to filter by
:param tool_names: List of tool names to filter by
:param connection_names: List of connection names to filter by
:param page_size: Maximum number of tools to return per page
:param page_token: Token from a previous response for pagination
:returns: List of Google ADK compatible tools
:raises ImportError: If Google ADK dependencies are not installed
"""
if identifier is None or identifier == "":
raise ValueError("Identifier must be provided to get tools")


# Create ScopedToolFilter if any filter parameters are provided
scoped_filter = None
if providers or tool_names or connection_names:
scoped_filter = ScopedToolFilter(
providers=providers or [],
tool_names=tool_names or [],
connection_names=connection_names or []
)

# Call list_scoped_tools which returns (response, metadata) tuple
result_tuple = self.tools.list_scoped_tools(identifier, scoped_filter, page_size, page_token)

# Extract the response[0] (the actual ListScopedToolsResponse proto object)
response = result_tuple[0]

google_adk_tools = []
for scoped_tool in response.tools:
google_adk_tool = self._convert_tool_to_google_adk_tool(
scoped_tool.tool,
scoped_tool.connected_account_id
)
google_adk_tools.append(google_adk_tool)

return google_adk_tools

def _convert_tool_to_google_adk_tool(self, tool, connected_account_id: str):
"""Convert a Scalekit Tool to Google ADK compatible tool"""


spec = struct_to_dict(tool)
mcp_tool = build_mcp_tool_from_spec(spec)

return ScalekitGoogleAdkTool(
mcp_tool=mcp_tool,
connected_account_id=connected_account_id,
execute_callback=self.execute_callback
)

19 changes: 3 additions & 16 deletions scalekit/actions/frameworks/langchain.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import Optional, Any, Dict, List, Callable
from langchain_core.tools import StructuredTool
from scalekit.tools import ToolsClient
from scalekit.v1.tools.tools_pb2 import Filter, ScopedToolFilter
from scalekit.v1.tools.tools_pb2 import ScopedToolFilter
from scalekit.actions.frameworks.util import extract_tool_metadata


class LangChain:
Expand Down Expand Up @@ -63,23 +64,13 @@ def get_tools(
def _convert_tool_to_structured_tool(self, tool, connected_account_id: str) -> StructuredTool:
"""Convert a Scalekit Tool to LangChain StructuredTool"""


definition_dict = self._struct_to_dict(tool.definition) if hasattr(tool, 'definition') and tool.definition else {}


tool_name = definition_dict.get('name', getattr(tool, 'provider', 'unknown') + '_tool')
tool_description = definition_dict.get('description', 'Scalekit tool')

tool_name, tool_description, definition_dict = extract_tool_metadata(tool)

args_schema = definition_dict.get("input_schema", {})


def _call(**arguments: Dict[str, Any]) -> str:
try:
# Import here to avoid circular imports
from scalekit.actions.types import ToolInput


# Call connect.execute_tool via callback (includes modifiers and enhanced handling)
response = self.execute_callback(
tool_input=arguments,
Expand Down Expand Up @@ -119,7 +110,3 @@ async def call_tool_async(**arguments: Dict[str, Any]) -> str:
coroutine=call_tool_async,
)

def _struct_to_dict(self, struct) -> Dict[str, Any]:
"""Convert protobuf Struct to Python dict"""
from google.protobuf.json_format import MessageToDict
return MessageToDict(struct)
7 changes: 7 additions & 0 deletions scalekit/actions/frameworks/types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .google_adk_tool import (
ScalekitGoogleAdkTool,
)

__all__ = [
'ScalekitGoogleAdkTool',
]
77 changes: 77 additions & 0 deletions scalekit/actions/frameworks/types/google_adk_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from typing import Callable
from mcp.types import Tool as McpBaseTool

# Dynamic imports with helpful error messages
def _import_google_adk():
"""Import Google ADK with helpful error message if not available"""
try:
from google.adk.tools.mcp_tool.mcp_tool import McpTool
from google.adk.tools.tool_context import ToolContext
from google.adk.auth.auth_credential import AuthCredential
return McpTool, AuthCredential, ToolContext
except ImportError as e:
raise ImportError(
"Google ADK not found. To use Google ADK integration, please install:\n"
"pip install google-adk\n\n"
"For more information, see: https://google.github.io/adk-docs/\n"
f"Original error: {e}"
)

McpTool,AuthCredential,ToolContext = _import_google_adk()
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add spaces after commas in variable assignment. Should be 'McpTool, AuthCredential, ToolContext = _import_google_adk()'.

Copilot uses AI. Check for mistakes.



class ScalekitGoogleAdkTool(McpTool):
"""Google ADK Tool wrapper for Scalekit tools inheriting from Google BaseTool"""

def __init__(
self,
mcp_tool: McpBaseTool,
connected_account_id: str,
execute_callback: Callable
):
"""
Initialize ScalekitGoogleAdkTool
:param connected_account_id: Connected account ID for execution
:param execute_callback: Callback function for tool execution (ActionClient.execute_tool)
"""

super().__init__(
mcp_tool=mcp_tool,
mcp_session_manager=None,
auth_scheme=None,
auth_credential=None,
)

self.connected_account_id = connected_account_id
self.execute_callback = execute_callback
self.name = mcp_tool.name
self.description = mcp_tool.description






Comment on lines +51 to +54
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove excessive blank lines. Multiple consecutive empty lines reduce code readability.

Suggested change

Copilot uses AI. Check for mistakes.

async def _run_async_impl(self, *, args, **kwargs) -> str:

try:
# Call connect.execute_tool via callback (includes modifiers and enhanced handling)
response = self.execute_callback(
tool_input=args,
tool_name=self.name,
connected_account_id=self.connected_account_id
)

result_data = response.data if hasattr(response, 'data') else {}

execution_id = response.execution_id if hasattr(response, 'execution_id') else None

# Format the response
result_dict = dict(result_data) if result_data else {}
if execution_id:
result_dict['execution_id'] = execution_id

return str(result_dict) if result_dict else f"Tool {self.name} executed successfully"

except Exception as e:
return f"Error executing tool {self.name}: {str(e)}"
73 changes: 73 additions & 0 deletions scalekit/actions/frameworks/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Dict, Any
from mcp.types import Tool as McpBaseTool, ToolAnnotations


def struct_to_dict(struct) -> Dict[str, Any]:
"""
Convert protobuf Struct to Python dict

:param struct: Protobuf Struct object
:returns: Python dictionary representation
"""
from google.protobuf.json_format import MessageToDict
return MessageToDict(struct)


def extract_tool_metadata(tool, default_provider: str = 'unknown'):
"""
Extract common tool metadata from Scalekit tool definition

:param tool: Scalekit tool object
:param default_provider: Default provider name if not found
:returns: Tuple of (tool_name, tool_description, definition_dict)
"""
definition_dict = struct_to_dict(tool.definition) if hasattr(tool, 'definition') and tool.definition else {}

tool_name = definition_dict.get('name', getattr(tool, 'provider', default_provider) + '_tool')
tool_description = definition_dict.get('description', 'Scalekit tool')

return tool_name, tool_description, definition_dict


def convert_to_mcp_input_schema(definition_dict: Dict[str, Any]) -> Dict[str, Any]:
"""
Convert Scalekit tool definition to MCP input schema format

:param definition_dict: Scalekit tool definition dictionary
:returns: MCP-compatible input schema
"""
input_schema = definition_dict.get("input_schema", {})

return {
"type": "object",
"properties": input_schema.get("properties", {}),
"required": input_schema.get("required", [])
}

def build_mcp_tool_from_spec(spec: Dict[str, Any]) -> McpBaseTool:
"""Converts the raw spec dict into an MCP Tool instance.

Mapping performed:
definition.input_schema -> inputSchema
definition.annotations.* snake_case -> ToolAnnotations camelCase
"""
definition = spec["definition"]
ann_raw = definition.get("annotations", {})
ann_map = {
"title": ann_raw.get("title"),
"readOnlyHint": ann_raw.get("read_only_hint"),
"destructiveHint": ann_raw.get("destructive_hint"),
"idempotentHint": ann_raw.get("idempotent_hint"),
"openWorldHint": ann_raw.get("open_world_hint"),
}
# Filter out None values
ann_clean = {k: v for k, v in ann_map.items() if v is not None}
annotations = ToolAnnotations(**ann_clean) if ann_clean else None

mcp_tool = McpBaseTool(
name=definition["name"],
description=definition.get("description"),
inputSchema=definition["input_schema"],
annotations=annotations,
)
return mcp_tool
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"deprecation>=2.1.0",
"python-dotenv>=1.1.0",
"Faker~=25.8.0",
"pydantic~=2.11.2",
"langchain-core>=0.3.36,<0.4",
"pydantic>=2.10.6",
"mcp>= 1.15.0",
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra space before version constraint. Should be 'mcp>=1.15.0'.

Copilot uses AI. Check for mistakes.

],
url="https://github.com/scalekit-inc/scalekit-sdk-python",
license="MIT",
Expand Down
Loading