diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts index cf23a9b32c..770bcaca27 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool.ts @@ -212,20 +212,20 @@ export class CodeInterpreterTool implements Tool { ), ); - return { + const output = { success: result.exitCode === 0, message: result.exitCode === 0 ? 'Code executed successfully' : 'Code execution failed', - result: { - stdout: result.stdout, - stderr: result.stderr, - exitCode: result.exitCode, - files: allOutputFileUrls, - }, + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + files: allOutputFileUrls, error: result.error, }; + + return output; } catch (error) { this.logger.error('Code interpreter execution failed', error); diff --git a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const.ts b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const.ts index d8f78de370..ced7db2e37 100644 --- a/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const.ts +++ b/packages/twenty-server/src/engine/core-modules/tool/tools/code-interpreter-tool/twenty-mcp-helper.const.ts @@ -10,7 +10,29 @@ except ImportError: _REQUESTS_AVAILABLE = False class TwentyMCP: - """Helper class to call Twenty tools via MCP protocol""" + """Helper for calling Twenty tools from sandboxed code. + + Two categories of tools exist behind /mcp: + + - MCP-native: execute_tool, learn_tools, load_skills, get_tool_catalog, + search_help_center. These are the 5 surfaces exposed directly. + + - Workspace catalog: 250+ CRUD / view / workflow / dashboard tools + like find_companies, create_person, update_opportunity. These are + reached through execute_tool as a dispatcher. + + call_tool(name, args) accepts both — catalog tools are routed via + execute_tool transparently and the envelope is unwrapped, so callers + see the inner tool's result directly. + """ + + _MCP_NATIVE_TOOLS = frozenset({ + 'execute_tool', + 'learn_tools', + 'load_skills', + 'get_tool_catalog', + 'search_help_center', + }) def __init__(self): self.url = os.environ.get('TWENTY_SERVER_URL', '') @@ -24,21 +46,77 @@ class TwentyMCP: def call_tool(self, name: str, arguments: dict = None): """ - Call a Twenty tool via MCP protocol. + Call any Twenty tool by name. + + Catalog tools (find_companies, create_person, …) are routed + through execute_tool. MCP-native tools are called directly. + The execute_tool envelope { success, message, result } is + unwrapped so you always get the inner tool's result back. Args: - name: Tool name (e.g., 'find_person_records', 'create_company_record') + name: Tool name (catalog or MCP-native) arguments: Tool arguments as a dictionary Returns: Tool result as parsed JSON Example: - people = twenty.call_tool('find_person_records', {'limit': 10}) + companies = twenty.call_tool('find_companies', {'limit': 5}) + # companies == {'records': [...], 'count': '5'} """ if not self._available: raise RuntimeError('Twenty MCP bridge not available. Missing requests library or credentials.') + if name in self._MCP_NATIVE_TOOLS: + return self._raw_mcp_call(name, arguments) + + wrapped = self._raw_mcp_call('execute_tool', { + 'toolName': name, + 'arguments': arguments or {}, + }) + # execute_tool returns one of: + # success: { success: True, message, result: {...} } + # failure: { success: False, message, error } + # Raise on failure, unwrap on success, pass through unknown shapes. + if isinstance(wrapped, dict): + if wrapped.get('success') is False: + raise Exception(wrapped.get('error') or wrapped.get('message') or + f"execute_tool failed for {name}") + if 'result' in wrapped: + return wrapped['result'] + return wrapped + + def list_tools(self): + """ + List all workspace catalog tools (250+), not the 5 MCP meta-tools. + + Use call_tool(name, args) to invoke any of them — routing is + handled for you. + + Returns: + Flat list of tool entries, each with name, description, and + category. get_tool_catalog groups by category internally; we + flatten for ergonomics. + """ + catalog = self.call_tool('get_tool_catalog', {}) + # get_tool_catalog returns { 'catalog': { '': [tools...] } } + # Flatten to a single list; preserve the category as a per-tool field + # so the docstring's promise holds and consumers don't have to re-call + # the catalog. Leave untouched if the shape is unexpected. + if isinstance(catalog, dict): + grouped = catalog.get('catalog', catalog) + if isinstance(grouped, dict): + return [ + ({**tool, 'category': category} + if isinstance(tool, dict) and 'category' not in tool + else tool) + for category, tools in grouped.items() + for tool in tools + ] + return catalog + + def _raw_mcp_call(self, name: str, arguments: dict = None): + """Low-level: issue a tools/call against the MCP surface verbatim.""" response = requests.post( f"{self.url}/mcp", headers={"Authorization": f"Bearer {self.token}"}, @@ -61,31 +139,11 @@ class TwentyMCP: return json.loads(content[0]["text"]) return result.get("result") - def list_tools(self): - """ - List all available Twenty tools. - - Returns: - List of tool definitions with name, description, and inputSchema - """ - if not self._available: - raise RuntimeError('Twenty MCP bridge not available.') - - response = requests.post( - f"{self.url}/mcp", - headers={"Authorization": f"Bearer {self.token}"}, - json={ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/list", - "params": {} - }, - timeout=30 - ) - response.raise_for_status() - result = response.json() - return result.get("result", {}).get("tools", []) - -# Pre-instantiated helper - use 'twenty' in your code +# -------------------------------------------------------------------------- +# \`twenty\` is a pre-built instance of the TwentyMCP class above. It is +# already bound in this module scope — DO NOT \`import twenty\`. There is +# no Python package by that name. Just use it directly, e.g.: +# companies = twenty.call_tool('find_companies', {'limit': 10}) +# -------------------------------------------------------------------------- twenty = TwentyMCP() `; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts index ed7c19e686..001a61b8c7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts @@ -631,32 +631,50 @@ print('Analysis complete!') ## Calling Twenty Tools from Python (MCP Bridge) -A \`twenty\` helper is automatically available in your code. Use it to call any Twenty tool directly from Python: +**A \`twenty\` variable is already bound in your code's scope.** Do NOT write +\`import twenty\` — there is no Python package by that name. The helper is an +instance of a class that has been pre-instantiated for you; just call methods +on it directly. + +Real catalog tools follow the pattern \`find_\` / \`find_one_\` / +\`create_\` / \`update_\` / \`delete_\` / +\`group_by_\` — e.g. \`find_companies\`, \`find_people\`, \`create_person\`. +Call \`twenty.list_tools()\` to discover exact names. Catalog tools are routed +through \`execute_tool\` automatically, and the helper raises an Exception on +server-side failures with the error message. \`\`\`python -# Find records -people = twenty.call_tool('find_person_records', {'limit': 10}) -print(f"Found {len(people['edges'])} people") +# List catalog tools (flat list, not grouped) +tools = twenty.list_tools() +print(f"{len(tools)} catalog tools available") +for tool in tools[:5]: + print(f"- {tool['name']}") -# Create a record -result = twenty.call_tool('create_company_record', { - 'data': {'name': 'Acme Corp', 'domainName': {'primaryLinkUrl': 'acme.com'}} +# Find records — returns { 'records': [...], 'count': '5' } +companies = twenty.call_tool('find_companies', {'limit': 5, 'offset': 0}) +for c in companies['records']: + print(c['name'], c.get('employees')) + +# Create a record — arguments match the tool's inputSchema directly, +# no nested 'data' wrapper. Use twenty.call_tool('learn_tools', ...) to +# inspect a schema if unsure. +result = twenty.call_tool('create_company', { + 'name': 'Acme Corp', + 'domainName': {'primaryLinkUrl': 'https://acme.com'}, + 'position': 'first', }) -print(f"Created company: {result['id']}") +print(f"Created company id={result['id']}") # Update a record -twenty.call_tool('update_person_record', { - 'id': 'person-uuid', - 'data': {'jobTitle': 'CEO'} +twenty.call_tool('update_person', { + 'id': 'person-uuid-here', + 'jobTitle': 'CEO', }) - -# List available tools -tools = twenty.list_tools() -for tool in tools: - print(f"- {tool['name']}: {tool['description']}") \`\`\` -This allows you to orchestrate complex multi-step operations in a single code execution, which is more efficient than multiple tool calls.`, +This lets you orchestrate multi-step data workflows in a single sandbox +execution — faster than an equivalent chain of individual tool calls from +the agent, and the computation stays server-side.`, isCustom: false, }, }),