Skip to content

Commit 28a842b

Browse files
committed
Improve AutoGen Studio: deprecate FunctionTool, harden MCP WebSocket endpoint
- Deprecate FunctionTool creation in the UI; show deprecation warning for existing configs - Skip FunctionTool instantiation during validation to avoid exec() on user code - Store MCP server params server-side instead of passing via WebSocket query string - Remove FunctionTool template and direct users to MCP Workbenches
1 parent 13e144e commit 28a842b

7 files changed

Lines changed: 103 additions & 488 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,6 @@ registry.json
203203
# files created by the gitty agent in python/samples/gitty
204204
.gitty/
205205
.aider*
206+
207+
# Claude Code
208+
.claude/

python/packages/autogen-studio/autogenstudio/validation/validation_service.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ def validate_instantiation(component: ComponentModel) -> Optional[ValidationErro
115115
"""Validate that the component can be instantiated"""
116116
try:
117117
model = component.model_copy(deep=True)
118+
119+
# SECURITY: Skip instantiation for FunctionTool to prevent arbitrary code execution.
120+
# FunctionTool._from_config() uses exec() on user-provided source_code, which is an RCE vector.
121+
# Schema validation is sufficient for FunctionTool - we validate the config structure without
122+
# actually executing the code. This blocks drive-by attacks via the /api/validate/ endpoint.
123+
if "FunctionTool" in model.provider:
124+
return None
125+
118126
# Attempt to load the component
119127
module_path, class_name = model.provider.rsplit(".", maxsplit=1)
120128
module = importlib.import_module(module_path)

python/packages/autogen-studio/autogenstudio/web/routes/mcp.py

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import base64
2-
import json
31
import uuid
42
from datetime import datetime, timezone
5-
from typing import Any, Dict
3+
from typing import Any, Dict, Union
64

75
from autogen_ext.tools.mcp._config import (
86
McpServerParams,
@@ -32,6 +30,11 @@
3230
# Global session tracking for status endpoint
3331
active_sessions: Dict[str, Dict[str, Any]] = {}
3432

33+
# Server-side storage for pending MCP session parameters.
34+
# Params are registered via POST /ws/connect and consumed (popped) when the WebSocket connects.
35+
# This prevents attackers from injecting arbitrary server_params via the WebSocket query string.
36+
pending_session_params: Dict[str, Union[StdioServerParams, SseServerParams, StreamableHttpServerParams]] = {}
37+
3538

3639
class CreateWebSocketConnectionRequest(BaseModel):
3740
server_params: McpServerParams
@@ -129,35 +132,19 @@ async def create_mcp_session(bridge: MCPWebSocketBridge, server_params: McpServe
129132

130133
@router.websocket("/ws/{session_id}")
131134
async def mcp_websocket(websocket: WebSocket, session_id: str):
132-
"""Main WebSocket endpoint - now a thin layer"""
135+
"""Main WebSocket endpoint - looks up server params from server-side storage"""
136+
# Look up pre-registered server params (one-time use)
137+
server_params = pending_session_params.pop(session_id, None)
138+
if server_params is None:
139+
await websocket.close(code=4004, reason="Unknown or expired session")
140+
return
141+
133142
await websocket.accept()
134143
logger.info(f"MCP WebSocket connection established for session {session_id}")
135144

136145
bridge = None
137146

138147
try:
139-
# Parse server parameters
140-
query_params = dict(websocket.query_params)
141-
server_params_encoded = query_params.get("server_params")
142-
143-
if not server_params_encoded:
144-
await websocket.close(code=4000, reason="Missing server_params")
145-
return
146-
147-
decoded_params = base64.b64decode(server_params_encoded).decode("utf-8")
148-
server_params_dict = json.loads(decoded_params)
149-
150-
# Create appropriate server params object
151-
if server_params_dict.get("type") == "StdioServerParams":
152-
server_params = StdioServerParams(**server_params_dict)
153-
elif server_params_dict.get("type") == "SseServerParams":
154-
server_params = SseServerParams(**server_params_dict)
155-
elif server_params_dict.get("type") == "StreamableHttpServerParams":
156-
server_params = StreamableHttpServerParams(**server_params_dict)
157-
else:
158-
await websocket.close(code=4000, reason="Invalid server parameters")
159-
return
160-
161148
# Create bridge and run MCP session
162149
bridge = MCPWebSocketBridge(websocket, session_id)
163150
await create_mcp_session(bridge, server_params, session_id)
@@ -197,18 +184,18 @@ async def mcp_websocket(websocket: WebSocket, session_id: str):
197184

198185
@router.post("/ws/connect")
199186
async def create_mcp_websocket_connection(request: CreateWebSocketConnectionRequest):
200-
"""Create WebSocket connection URL"""
187+
"""Register server params and return a WebSocket URL with session_id only"""
201188
try:
202189
session_id = str(uuid.uuid4())
203190

204-
server_params_json = json.dumps(serialize_for_json(request.server_params.model_dump()))
205-
server_params_encoded = base64.b64encode(server_params_json.encode("utf-8")).decode("utf-8")
191+
# Store params server-side — WebSocket handler will pop them on connect
192+
pending_session_params[session_id] = request.server_params
206193

207194
return {
208195
"status": True,
209196
"message": "WebSocket connection URL created",
210197
"session_id": session_id,
211-
"websocket_url": f"/api/mcp/ws/{session_id}?server_params={server_params_encoded}",
198+
"websocket_url": f"/api/mcp/ws/{session_id}",
212199
"timestamp": datetime.now(timezone.utc).isoformat(),
213200
}
214201

python/packages/autogen-studio/frontend/src/components/types/component-templates.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -229,35 +229,9 @@ export const MODEL_TEMPLATES: ComponentTemplate<ModelConfig>[] = [
229229
];
230230

231231
// Tool Templates
232+
// NOTE: FunctionTool has been removed due to security concerns (arbitrary code execution via exec()).
233+
// Use MCP Workbenches instead for custom tool functionality.
232234
export const TOOL_TEMPLATES: ComponentTemplate<ToolConfig>[] = [
233-
{
234-
id: "function-tool",
235-
label: "Function Tool",
236-
description: "A custom Python function that can be called by agents",
237-
provider: PROVIDERS.FUNCTION_TOOL,
238-
component_type: "tool",
239-
version: 1,
240-
component_version: 1,
241-
config: {
242-
name: "my_function",
243-
description: "A custom function that performs a specific task",
244-
source_code: `def my_function(input_text: str) -> str:
245-
"""
246-
A template function that processes input text.
247-
248-
Args:
249-
input_text: The text to process
250-
251-
Returns:
252-
Processed text result
253-
"""
254-
# Replace this with your custom function logic
255-
result = f"Processed: {input_text}"
256-
return result`,
257-
global_imports: [],
258-
has_cancellation_support: false,
259-
} as FunctionToolConfig,
260-
},
261235
{
262236
id: "code-execution-tool",
263237
label: "Code Execution Tool",

python/packages/autogen-studio/frontend/src/components/views/teambuilder/builder/component-editor/fields/agent-fields.tsx

Lines changed: 2 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -121,94 +121,8 @@ export const AgentFields: React.FC<AgentFieldsProps> = ({
121121
[component, handleComponentUpdate]
122122
);
123123

124-
const handleAddTool = useCallback(() => {
125-
if (!isAssistantAgent(component)) return;
126-
127-
const blankTool: Component<FunctionToolConfig> = {
128-
provider: "autogen_core.tools.FunctionTool",
129-
component_type: "tool",
130-
version: 1,
131-
component_version: 1,
132-
description: "Create custom tools by wrapping standard Python functions.",
133-
label: "New Tool",
134-
config: {
135-
source_code: "def new_function():\n pass",
136-
name: "new_function",
137-
description: "Description of the new function",
138-
global_imports: [],
139-
has_cancellation_support: false,
140-
},
141-
};
142-
143-
// Get or create workbenches array
144-
let workbenches = normalizeWorkbenches(component.config.workbench);
145-
146-
// Find existing StaticWorkbench or create one
147-
let workbenchIndex = workbenches.findIndex((wb) => isStaticWorkbench(wb));
148-
149-
let workbench: Component<StaticWorkbenchConfig>;
150-
if (workbenchIndex === -1) {
151-
// Create a new StaticWorkbench
152-
workbench = {
153-
provider: "autogen_core.tools.StaticWorkbench",
154-
component_type: "workbench",
155-
config: {
156-
tools: [],
157-
},
158-
label: "Static Workbench",
159-
} as Component<StaticWorkbenchConfig>;
160-
workbenches = [...workbenches, workbench];
161-
workbenchIndex = workbenches.length - 1;
162-
} else {
163-
workbench = workbenches[
164-
workbenchIndex
165-
] as Component<StaticWorkbenchConfig>;
166-
}
167-
168-
const staticConfig = workbench.config as StaticWorkbenchConfig;
169-
const currentTools = staticConfig.tools || [];
170-
const updatedTools = [...currentTools, blankTool];
171-
172-
// Update workbench config
173-
const updatedWorkbench = {
174-
...workbench,
175-
config: {
176-
...staticConfig,
177-
tools: updatedTools,
178-
},
179-
};
180-
181-
// Update the workbenches array
182-
const updatedWorkbenches = [...workbenches];
183-
updatedWorkbenches[workbenchIndex] = updatedWorkbench;
184-
185-
handleConfigUpdate("workbench", updatedWorkbenches);
186-
187-
// If working copy functionality is available, update that too
188-
if (
189-
workingCopy &&
190-
setWorkingCopy &&
191-
updateComponentAtPath &&
192-
getCurrentComponent &&
193-
editPath
194-
) {
195-
const updatedCopy = updateComponentAtPath(workingCopy, editPath, {
196-
config: {
197-
...getCurrentComponent(workingCopy)?.config,
198-
workbench: updatedWorkbenches,
199-
},
200-
});
201-
setWorkingCopy(updatedCopy);
202-
}
203-
}, [
204-
component,
205-
handleConfigUpdate,
206-
workingCopy,
207-
setWorkingCopy,
208-
updateComponentAtPath,
209-
getCurrentComponent,
210-
editPath,
211-
]);
124+
// NOTE: handleAddTool removed - FunctionTool creation is deprecated due to security concerns
125+
// (arbitrary code execution via exec()). Users should use MCP Workbenches instead.
212126

213127
// Helper functions to add different types of workbenches
214128
const addStaticWorkbench = useCallback(() => {

0 commit comments

Comments
 (0)