Model Context Protocol (MCP) support with new use case (#42)

* initial mcp

* food ordering with mcp

* prompt eng

* splitting out goals and updating docs

* a diff so I can get tests from codex

* a diff so I can get tests from codex

* oops, missing files

* tests, file formatting

* readme and setup updates

* setup.md link fixes

* readme change

* readme change

* readme change

* stripe food setup script

* single agent mode default

* prompt engineering for better multi agent performance

* performance should be greatly improved

* Update goals/finance.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update activities/tool_activities.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* co-pilot PR suggested this change, and now fixed it

* stronger wording around json format response

* formatting

* moved docs to dir

* moved image assets under docs

* cleanup env example, stripe guidance

* cleanup

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Steve Androulakis
2025-06-09 16:39:57 -07:00
committed by GitHub
parent 1811e4cf59
commit 5d55a9fe80
49 changed files with 3268 additions and 279 deletions

View File

@@ -20,10 +20,11 @@ from workflows.workflow_helpers import (
)
with workflow.unsafe.imports_passed_through():
from activities.tool_activities import ToolActivities
from activities.tool_activities import ToolActivities, mcp_list_tools
from goals import goal_list
from models.data_types import CombinedInput, ToolPromptInput
from prompts.agent_prompt_generators import generate_genai_prompt
from tools.goal_registry import goal_list
from tools.tool_registry import create_mcp_tool_definitions
# Constants
MAX_TURNS_BEFORE_CONTINUE = 250
@@ -59,6 +60,7 @@ class AgentGoalWorkflow:
self.multi_goal_mode: bool = (
False # set from env file in activity lookup_wf_env_settings
)
self.mcp_tools_info: Optional[dict] = None # stores complete MCP tools result
# see ../api/main.py#temporal_client.start_workflow() for how the input parameters are set
@workflow.run
@@ -70,6 +72,10 @@ class AgentGoalWorkflow:
await self.lookup_wf_env_settings(combined_input)
# If the goal has an MCP server definition, dynamically load MCP tools
if self.goal.mcp_server_definition:
await self.load_mcp_tools()
# add message from sample conversation provided in tools/goal_registry.py, if it exists
if params and params.conversation_summary:
self.add_message("conversation_summary", params.conversation_summary)
@@ -146,6 +152,7 @@ class AgentGoalWorkflow:
conversation_history=self.conversation_history,
multi_goal_mode=self.multi_goal_mode,
raw_json=self.tool_data,
mcp_tools_info=self.mcp_tools_info,
)
prompt_input = ToolPromptInput(
@@ -368,6 +375,7 @@ class AgentGoalWorkflow:
self.tool_results,
self.add_message,
self.prompt_queue,
self.goal,
)
# set new goal if we should
@@ -398,3 +406,43 @@ class AgentGoalWorkflow:
else:
print("no tool data initialized yet")
print(f"self.confirmed: {self.confirmed}")
async def load_mcp_tools(self) -> None:
"""Load MCP tools dynamically from the server definition"""
if not self.goal.mcp_server_definition:
return
workflow.logger.info(
f"Loading MCP tools from server: {self.goal.mcp_server_definition.name}"
)
# Get the list of tools to include (if specified)
include_tools = self.goal.mcp_server_definition.included_tools
# Call the MCP list tools activity
mcp_tools_result = await workflow.execute_activity(
mcp_list_tools,
args=[self.goal.mcp_server_definition, include_tools],
start_to_close_timeout=LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT,
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=5), backoff_coefficient=1
),
summary=f"{self.goal.mcp_server_definition.name}",
)
if mcp_tools_result.get("success", False):
tools_info = mcp_tools_result.get("tools", {})
workflow.logger.info(f"Successfully loaded {len(tools_info)} MCP tools")
# Store complete MCP tools result for use in prompt generation
self.mcp_tools_info = mcp_tools_result
# Convert MCP tools to ToolDefinition objects and add to goal
mcp_tool_definitions = create_mcp_tool_definitions(tools_info)
self.goal.tools.extend(mcp_tool_definitions)
workflow.logger.info(f"Added {len(mcp_tool_definitions)} MCP tools to goal")
else:
error_msg = mcp_tools_result.get("error", "Unknown error")
workflow.logger.error(f"Failed to load MCP tools: {error_msg}")
# Continue execution without MCP tools

View File

@@ -6,6 +6,7 @@ from temporalio.common import RetryPolicy
from temporalio.exceptions import ActivityError
from models.data_types import ConversationHistory, ToolPromptInput
from models.tool_definitions import AgentGoal
from prompts.agent_prompt_generators import (
generate_missing_args_prompt,
generate_tool_completion_prompt,
@@ -19,35 +20,118 @@ LLM_ACTIVITY_START_TO_CLOSE_TIMEOUT = timedelta(seconds=20)
LLM_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT = timedelta(minutes=30)
def is_mcp_tool(tool_name: str, goal: AgentGoal) -> bool:
"""Check if a tool is an MCP tool based on the goal's MCP server definition"""
if not goal.mcp_server_definition:
return False
# Check if the tool name matches any MCP tools that were loaded
# We can identify MCP tools by checking if they're not in the original static tools
from tools.tool_registry import (
book_pto_tool,
book_trains_tool,
change_goal_tool,
create_invoice_tool,
current_pto_tool,
ecomm_get_order,
ecomm_list_orders,
ecomm_track_package,
financial_check_account_is_valid,
financial_get_account_balances,
financial_move_money,
financial_submit_loan_approval,
find_events_tool,
food_add_to_cart_tool,
future_pto_calc_tool,
give_hint_tool,
guess_location_tool,
list_agents_tool,
paycheck_bank_integration_status_check,
search_fixtures_tool,
search_flights_tool,
search_trains_tool,
)
static_tool_names = {
list_agents_tool.name,
change_goal_tool.name,
give_hint_tool.name,
guess_location_tool.name,
search_flights_tool.name,
search_trains_tool.name,
book_trains_tool.name,
create_invoice_tool.name,
search_fixtures_tool.name,
find_events_tool.name,
current_pto_tool.name,
future_pto_calc_tool.name,
book_pto_tool.name,
paycheck_bank_integration_status_check.name,
financial_check_account_is_valid.name,
financial_get_account_balances.name,
financial_move_money.name,
financial_submit_loan_approval.name,
ecomm_list_orders.name,
ecomm_get_order.name,
ecomm_track_package.name,
food_add_to_cart_tool.name,
}
return tool_name not in static_tool_names
async def handle_tool_execution(
current_tool: str,
tool_data: Dict[str, Any],
tool_results: list,
add_message_callback: callable,
prompt_queue: Deque[str],
goal: AgentGoal = None,
) -> None:
"""Execute a tool after confirmation and handle its result."""
workflow.logger.info(f"Confirmed. Proceeding with tool: {current_tool}")
task_queue = (
TEMPORAL_LEGACY_TASK_QUEUE
if current_tool in ["SearchTrains", "BookTrains"]
else None
)
try:
dynamic_result = await workflow.execute_activity(
current_tool,
tool_data["args"],
task_queue=task_queue,
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=5), backoff_coefficient=1
),
)
# Check if this is an MCP tool
if goal and is_mcp_tool(current_tool, goal):
workflow.logger.info(f"Executing MCP tool: {current_tool}")
# Add server definition to args for MCP tools
mcp_args = tool_data["args"].copy()
mcp_args["server_definition"] = goal.mcp_server_definition
dynamic_result = await workflow.execute_activity(
current_tool,
mcp_args,
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=5), backoff_coefficient=1
),
summary=f"{goal.mcp_server_definition.name} (MCP Tool)",
)
else:
# Handle regular tools
task_queue = (
TEMPORAL_LEGACY_TASK_QUEUE
if current_tool in ["SearchTrains", "BookTrains"]
else None
)
dynamic_result = await workflow.execute_activity(
current_tool,
tool_data["args"],
task_queue=task_queue,
schedule_to_close_timeout=TOOL_ACTIVITY_SCHEDULE_TO_CLOSE_TIMEOUT,
start_to_close_timeout=TOOL_ACTIVITY_START_TO_CLOSE_TIMEOUT,
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=5), backoff_coefficient=1
),
)
dynamic_result["tool"] = current_tool
tool_results.append(dynamic_result)
except ActivityError as e:
workflow.logger.error(f"Tool execution failed: {str(e)}")
dynamic_result = {"error": str(e), "tool": current_tool}