From 0483ce502e540480d964d1f86c2539e9d7931116 Mon Sep 17 00:00:00 2001 From: Emanuele De Rossi Date: Mon, 1 Dec 2025 13:19:54 +0100 Subject: [PATCH 1/4] refactor(mcp): move prompts to template files --- sdk/src/rhesis/sdk/services/mcp/agent.py | 72 +++++++------------ .../mcp/prompt_templates/iteration_prompt.j2 | 22 ++++++ .../mcp/prompt_templates/system_prompt.j2 | 17 +++++ 3 files changed, 63 insertions(+), 48 deletions(-) create mode 100644 sdk/src/rhesis/sdk/services/mcp/prompt_templates/iteration_prompt.j2 create mode 100644 sdk/src/rhesis/sdk/services/mcp/prompt_templates/system_prompt.j2 diff --git a/sdk/src/rhesis/sdk/services/mcp/agent.py b/sdk/src/rhesis/sdk/services/mcp/agent.py index 12d084532..0bc27153c 100644 --- a/sdk/src/rhesis/sdk/services/mcp/agent.py +++ b/sdk/src/rhesis/sdk/services/mcp/agent.py @@ -3,8 +3,11 @@ import asyncio import json import logging +from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union +import jinja2 + from rhesis.sdk.models.base import BaseLLM from rhesis.sdk.models.factory import get_model from rhesis.sdk.services.mcp.client import MCPClient @@ -28,25 +31,6 @@ class MCPAgent: and accomplish tasks. Clients can customize behavior via system prompts. """ - DEFAULT_SYSTEM_PROMPT = """You are an autonomous agent that can use MCP tools \ -to accomplish tasks. - -You operate in a ReAct loop: Reason → Act → Observe → Repeat - -For each iteration: -- Reason: Think step-by-step about what information you need and how to get it -- Act: Either call tools to gather information, or finish with your final answer -- Observe: Examine tool results and plan next steps - -Guidelines: -- Break complex tasks into simple tool calls -- Use tool results to inform next actions -- When you have sufficient information, use action="finish" with your final_answer -- Be efficient: minimize unnecessary tool calls -- You can call multiple tools in a single iteration if they don't depend on each other - -Remember: You must explicitly use action="finish" when done.""" - def __init__( self, model: Optional[Union[str, BaseLLM]] = None, @@ -69,10 +53,19 @@ def __init__( if not mcp_client: raise ValueError("mcp_client is required") + # Initialize template environment + templates_dir = Path(__file__).parent / "prompt_templates" + self._jinja_env = jinja2.Environment( + loader=jinja2.FileSystemLoader(str(templates_dir)), + autoescape=False, + trim_blocks=True, + lstrip_blocks=True, + ) + # Convert model to BaseLLM instance if needed self.model = self._set_model(model) self.mcp_client = mcp_client - self.system_prompt = system_prompt or self.DEFAULT_SYSTEM_PROMPT + self.system_prompt = system_prompt or self._load_default_system_prompt() self.max_iterations = max_iterations self.verbose = verbose self.executor = ToolExecutor(mcp_client) @@ -83,6 +76,11 @@ def _set_model(self, model: Optional[Union[str, BaseLLM]]) -> BaseLLM: return model return get_model(model) + def _load_default_system_prompt(self) -> str: + """Load the default system prompt from template.""" + template = self._jinja_env.get_template("system_prompt.j2") + return template.render() + async def run_async(self, user_query: str) -> AgentResult: """ Execute the agent's ReAct loop asynchronously. @@ -383,34 +381,12 @@ def _build_prompt( tools_text = self._format_tools(available_tools) history_text = self._format_history(history) - prompt = f"""User Query: {user_query} - -Available MCP Tools: -{tools_text} - -""" - if history_text: - prompt += f"""Execution History: -{history_text} - -Based on the query, available tools, and execution history above, decide what to do next. - -""" - else: - prompt += """This is the first iteration. Analyze the query and decide \ -what tools to call. - -""" - - prompt += """Your response should follow this structure: -- reasoning: Your step-by-step thinking about what to do -- action: Either "call_tool" (to execute tools) or "finish" (when you have the answer) -- tool_calls: List of tools to call if action="call_tool" (can be multiple) -- final_answer: Your complete answer if action="finish" - -Think carefully about what information you need and how to get it efficiently.""" - - return prompt + template = self._jinja_env.get_template("iteration_prompt.j2") + return template.render( + user_query=user_query, + tools_text=tools_text, + history_text=history_text, + ) def _format_tools(self, tools: List[Dict[str, Any]]) -> str: """Format tool list into human-readable text with names, descriptions, \ diff --git a/sdk/src/rhesis/sdk/services/mcp/prompt_templates/iteration_prompt.j2 b/sdk/src/rhesis/sdk/services/mcp/prompt_templates/iteration_prompt.j2 new file mode 100644 index 000000000..6544c8d41 --- /dev/null +++ b/sdk/src/rhesis/sdk/services/mcp/prompt_templates/iteration_prompt.j2 @@ -0,0 +1,22 @@ +User Query: {{ user_query }} + +Available MCP Tools: +{{ tools_text }} + +{% if history_text %} +Execution History: +{{ history_text }} + +Based on the query, available tools, and execution history above, decide what to do next. + +{% else %} +This is the first iteration. Analyze the query and decide what tools to call. + +{% endif %} +Your response should follow this structure: +- reasoning: Your step-by-step thinking about what to do +- action: Either "call_tool" (to execute tools) or "finish" (when you have the answer) +- tool_calls: List of tools to call if action="call_tool" (can be multiple) +- final_answer: Your complete answer if action="finish" + +Think carefully about what information you need and how to get it efficiently. diff --git a/sdk/src/rhesis/sdk/services/mcp/prompt_templates/system_prompt.j2 b/sdk/src/rhesis/sdk/services/mcp/prompt_templates/system_prompt.j2 new file mode 100644 index 000000000..25fdebbdf --- /dev/null +++ b/sdk/src/rhesis/sdk/services/mcp/prompt_templates/system_prompt.j2 @@ -0,0 +1,17 @@ +You are an autonomous agent that can use MCP tools to accomplish tasks. + +You operate in a ReAct loop: Reason → Act → Observe → Repeat + +For each iteration: +- Reason: Think step-by-step about what information you need and how to get it +- Act: Either call tools to gather information, or finish with your final answer +- Observe: Examine tool results and plan next steps + +Guidelines: +- Break complex tasks into simple tool calls +- Use tool results to inform next actions +- When you have sufficient information, use action="finish" with your final_answer +- Be efficient: minimize unnecessary tool calls +- You can call multiple tools in a single iteration if they don't depend on each other + +Remember: You must explicitly use action="finish" when done. From 1fa8b7c25ad3111f94e3f4383612ccceb9b8462f Mon Sep 17 00:00:00 2001 From: Emanuele De Rossi Date: Mon, 1 Dec 2025 13:44:26 +0100 Subject: [PATCH 2/4] docs: clarify tool creation docstring for MCP providers --- .../src/rhesis/backend/app/routers/tools.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/backend/src/rhesis/backend/app/routers/tools.py b/apps/backend/src/rhesis/backend/app/routers/tools.py index d52d658e9..ebf7c0590 100644 --- a/apps/backend/src/rhesis/backend/app/routers/tools.py +++ b/apps/backend/src/rhesis/backend/app/routers/tools.py @@ -33,19 +33,32 @@ def create_tool( current_user: User = Depends(require_current_user_or_token), ): """ - Create a new tool integration. + Create a new tool. - The credentials (JSON dict) will be encrypted in the database. - Examples: {"NOTION_TOKEN": "ntn_abc..."} or {"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_abc..."} + A tool allows the system to connect to an external service or API. Examples of tools are: - For custom providers (provider_type="custom"), you must provide the MCP server configuration - in tool_metadata with credential placeholders. Placeholders MUST use simple format like - {{ TOKEN }} (not {{ TOKEN | tojson }}) because the JSON must be valid before Jinja2 rendering. + - MCPs + - APIs + + Currently, we support the following MCP tool providers: + + 1. **Notion** + - Store the Notion token in the credentials dictionary with the key `"NOTION_TOKEN"`. + - Example: + ```json + {"NOTION_TOKEN": "ntn_abc..."} + ``` + + 2. **Custom MCP provider** + - You must provide the MCP server configuration JSON in `tool_metadata`. + - The custom provider should use **npx** to run the MCP server. + - Any environment variables required by the MCP server should be included in the `env` object. - Example tool_metadata for custom provider: + Example `tool_metadata` for a custom provider: + ```json { - "command": "bunx", - "args": ["--bun", "@custom/mcp-server"], + "command": "npx", + "args": ["@custom/mcp-server"], "env": { "NOTION_TOKEN": "{{ NOTION_TOKEN }}" } From 18e9d0d799fe9883da39eab6beab4e1fface35c3 Mon Sep 17 00:00:00 2001 From: Emanuele De Rossi Date: Tue, 2 Dec 2025 16:52:25 +0100 Subject: [PATCH 3/4] feat(mcp): add npx compatibility for mcp server commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add npx → bunx symlink alias in Dockerfiles for automatic conversion - Update provider templates (notion, atlassian) to use npx instead of bunx - Update frontend example JSON to show npx usage - Update code documentation to reference npx in examples Users can now write npx in MCP configurations. In Docker, the alias automatically converts npx to bunx. During local development, users can use npx directly with Node.js installed. --- apps/backend/Dockerfile | 4 +++- apps/backend/Dockerfile.dev | 2 ++ .../app/(protected)/mcp/components/MCPConnectionDialog.tsx | 2 +- sdk/src/rhesis/sdk/services/mcp/client.py | 4 ++-- .../sdk/services/mcp/provider_templates/atlassian.json.j2 | 2 +- .../rhesis/sdk/services/mcp/provider_templates/notion.json.j2 | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile index fcf7e07df..25a8aba05 100644 --- a/apps/backend/Dockerfile +++ b/apps/backend/Dockerfile @@ -40,7 +40,7 @@ ENV UV_COMPILE_BYTECODE=1 ENV UV_NO_MANAGED_PYTHON=1 # Use copy mode for linking dependencies (required by the cache) -ENV UV_LINK_MODE=copy +ENV UV_LINK_MODE=copy # Skip the installation of the dev dependencies ENV UV_NO_DEV=1 @@ -68,6 +68,8 @@ FROM mirror.gcr.io/library/python:3.10.17-slim AS runtime # Copy Bun from official image (lighter than Node.js, required for bunx to run MCP servers) COPY --from=oven/bun:latest /usr/local/bin/bun /usr/local/bin/bun COPY --from=oven/bun:latest /usr/local/bin/bunx /usr/local/bin/bunx +# Create npx alias to bunx for compatibility (users can write npx, Docker will use bunx) +RUN ln -s /usr/local/bin/bunx /usr/local/bin/npx WORKDIR /app diff --git a/apps/backend/Dockerfile.dev b/apps/backend/Dockerfile.dev index 4bc5f4e2d..542c7c06d 100644 --- a/apps/backend/Dockerfile.dev +++ b/apps/backend/Dockerfile.dev @@ -51,6 +51,8 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ # Copy Bun from official image (lighter than Node.js, required for bunx to run MCP servers) COPY --from=oven/bun:latest /usr/local/bin/bun /usr/local/bin/bun COPY --from=oven/bun:latest /usr/local/bin/bunx /usr/local/bin/bunx +# Create npx alias to bunx for compatibility (users can write npx, Docker will use bunx) +RUN ln -s /usr/local/bin/bunx /usr/local/bin/npx WORKDIR /app diff --git a/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx b/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx index fac5a2aa7..16ff210e7 100644 --- a/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx +++ b/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx @@ -519,7 +519,7 @@ export function MCPConnectionDialog({ }} > {`{ - "command": "bunx", + "command": "npx", "args": ["--bun", "@notionhq/notion-mcp-server"], "env": { "NOTION_TOKEN": "{{ TOKEN }}" diff --git a/sdk/src/rhesis/sdk/services/mcp/client.py b/sdk/src/rhesis/sdk/services/mcp/client.py index d8c2cc519..c415248cb 100644 --- a/sdk/src/rhesis/sdk/services/mcp/client.py +++ b/sdk/src/rhesis/sdk/services/mcp/client.py @@ -30,7 +30,7 @@ def __init__( Args: server_name: Friendly name for the server (e.g., "notionApi") - command: Command to launch the server (e.g., "bunx", "python") + command: Command to launch the server (e.g., "npx", "python") args: Command arguments (e.g., ["--bun", "@notionhq/notion-mcp-server"]) env: Environment variables to pass to the server process """ @@ -274,7 +274,7 @@ def from_tool_config(cls, tool_name: str, tool_config: Dict, credentials: Dict[s Example: tool_config = { - "command": "bunx", + "command": "npx", "args": ["--bun", "@notionhq/notion-mcp-server"], "env": { "NOTION_TOKEN": "{{ NOTION_TOKEN }}" diff --git a/sdk/src/rhesis/sdk/services/mcp/provider_templates/atlassian.json.j2 b/sdk/src/rhesis/sdk/services/mcp/provider_templates/atlassian.json.j2 index 16026e987..305683d17 100644 --- a/sdk/src/rhesis/sdk/services/mcp/provider_templates/atlassian.json.j2 +++ b/sdk/src/rhesis/sdk/services/mcp/provider_templates/atlassian.json.j2 @@ -1,4 +1,4 @@ { - "command": "bunx", + "command": "npx", "args": ["--bun", "mcp-remote", "https://mcp.atlassian.com/v1/sse"] } diff --git a/sdk/src/rhesis/sdk/services/mcp/provider_templates/notion.json.j2 b/sdk/src/rhesis/sdk/services/mcp/provider_templates/notion.json.j2 index 661097a68..a365f8fa4 100644 --- a/sdk/src/rhesis/sdk/services/mcp/provider_templates/notion.json.j2 +++ b/sdk/src/rhesis/sdk/services/mcp/provider_templates/notion.json.j2 @@ -1,5 +1,5 @@ { - "command": "bunx", + "command": "npx", "args": ["--bun", "@notionhq/notion-mcp-server"], "env": { "NOTION_TOKEN": {{ NOTION_TOKEN | tojson }} From 669a4d449bccec41fea7426d154b85c37d740c1c Mon Sep 17 00:00:00 2001 From: Emanuele De Rossi Date: Wed, 3 Dec 2025 14:25:41 +0100 Subject: [PATCH 4/4] Use provider-agnostic example metadata for custom MCP servers - Replace provider-specific examples with generic command/env placeholders - Make explicit the usage of TOKEN in frontend --- .../src/rhesis/backend/app/routers/tools.py | 14 ++++++++++---- .../mcp/components/MCPConnectionDialog.tsx | 12 ++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/backend/src/rhesis/backend/app/routers/tools.py b/apps/backend/src/rhesis/backend/app/routers/tools.py index ebf7c0590..32095de2c 100644 --- a/apps/backend/src/rhesis/backend/app/routers/tools.py +++ b/apps/backend/src/rhesis/backend/app/routers/tools.py @@ -50,19 +50,25 @@ def create_tool( ``` 2. **Custom MCP provider** - - You must provide the MCP server configuration JSON in `tool_metadata`. + - The MCP server configuration JSON should be provided in `tool_metadata` + - The API token should be stored in the credentials dictionary - The custom provider should use **npx** to run the MCP server. - - Any environment variables required by the MCP server should be included in the `env` object. Example `tool_metadata` for a custom provider: ```json { "command": "npx", - "args": ["@custom/mcp-server"], + "args": ["@example/mcp-server"], "env": { - "NOTION_TOKEN": "{{ NOTION_TOKEN }}" + "API_TOKEN": "{{ TOKEN }}" } } + ``` + + Where the credentials dictionary is: + ```json + {"TOKEN": "your_api_token_123"} + ``` """ organization_id, user_id = tenant_context return crud.create_tool(db=db, tool=tool, organization_id=organization_id, user_id=user_id) diff --git a/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx b/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx index 16ff210e7..a35550706 100644 --- a/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx +++ b/apps/frontend/src/app/(protected)/mcp/components/MCPConnectionDialog.tsx @@ -409,7 +409,7 @@ export function MCPConnectionDialog({ - Configure your custom MCP server. Use credential - placeholders with {'{{'} and{' '} - {'}}'} format. + Provide your API token above, then paste your MCP server config below + using {"{{ TOKEN }}"} as a placeholder + wherever the token is required. {`{ "command": "npx", - "args": ["--bun", "@notionhq/notion-mcp-server"], + "args": ["@example/mcp-server"], "env": { - "NOTION_TOKEN": "{{ TOKEN }}" + "API_TOKEN": "{{ TOKEN }}" } }`}