---
title: Herramientas personalizadas en sandboxes autoalojados
url: https://platform.claude.com/docs/es/managed-agents/self-hosted-sandboxes-custom-tools
description: Sirve herramientas personalizadas desde un worker de sandbox autoalojado y envuelve un servidor MCP dentro de tu red como herramientas personalizadas sin ejecutar un túnel.
featureMetadata:
  topic:
    title: Managed Agents
    url: https://platform.claude.com/docs/en/managed-agents/overview
  status: beta
  betaHeader: managed-agents-2026-04-01
---

Las [herramientas personalizadas](https://platform.claude.com/docs/es/managed-agents/tools#custom-tools) son herramientas que ejecuta tu propio código: el agente emite un evento `agent.custom_tool_use` y espera un `user.custom_tool_result` correspondiente. Tu worker puede ser ese código. Como se ejecuta dentro de tu sandbox, la herramienta alcanza los servicios internos, las credenciales y la salida de red que configuraste para el sandbox, y nada más.

La clave del entorno autoriza la publicación de resultados de herramientas personalizadas, por lo que tu clave de API de Claude se mantiene fuera del host del worker.

<Note>
  Servir herramientas personalizadas requiere el worker del SDK. El worker de la CLI `ant` no tiene forma de registrar una implementación de herramienta personalizada. En el patrón de un sandbox por sesión, [ejecuta el worker del SDK dentro del sandbox](https://platform.claude.com/docs/es/managed-agents/self-hosted-sandboxes-workers#run-the-sdk-worker-inside-the-sandbox).
</Note>

## Servir una herramienta personalizada

<Steps>
  <Step title="Declara la herramienta en el agente">
    Agrega una entrada `custom` a las `tools` del agente cuyo `name` coincida con la herramienta que registra tu worker. Consulta [Herramientas personalizadas](https://platform.claude.com/docs/es/managed-agents/tools#custom-tools) para ver la forma completa de la declaración.

    ```json
    {
      "type": "custom",
      "name": "get_order_status",
      "description": "Look up an order in the internal fulfillment system by order ID.",
      "input_schema": {
        "type": "object",
        "properties": {
          "order_id": { "type": "string", "description": "The order ID" }
        },
        "required": ["order_id"]
      }
    }
    ```
  </Step>

  <Step title="Registra la implementación en el worker">
    Pasa la herramienta a través de la fábrica `tools` (go: `ToolsFunc`) del worker (consulta [`EnvironmentWorker`](https://platform.claude.com/docs/es/managed-agents/self-hosted-sandboxes-reference#environment-worker)), junto con el conjunto de herramientas integrado:

    <CodeGroup exclude="shell, csharp, java, php, ruby">
      ```python Python
      import asyncio
      import os
      from anthropic import AsyncAnthropic, beta_async_tool
      from anthropic.lib.environments import EnvironmentWorker
      from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401


      @beta_async_tool
      async def get_order_status(order_id: str) -> str:
          """Look up an order in the internal fulfillment system by order ID."""
          # Se ejecuta en el host del worker: llama a cualquier cosa que el sandbox pueda alcanzar.
          return f"Order {order_id}: shipped"


      async def main() -> None:
          environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
          environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
          async with AsyncAnthropic(auth_token=environment_key) as client:
              await EnvironmentWorker(
                  client,
                  environment_id=environment_id,
                  environment_key=environment_key,
                  workdir="/workspace",
                  tools=lambda env: [*beta_agent_toolset_20260401(env), get_order_status],
              ).run()


      asyncio.run(main())
      ```

      ```typescript TypeScript
      import Anthropic from "@anthropic-ai/sdk";
      import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
      import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema";
      import { betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node";

      const getOrderStatus = betaTool({
        name: "get_order_status",
        description: "Look up an order in the internal fulfillment system by order ID.",
        inputSchema: {
          type: "object",
          properties: { order_id: { type: "string", description: "The order ID" } },
          required: ["order_id"]
        },
        // Se ejecuta en el host del worker: llama a cualquier cosa que el sandbox pueda alcanzar.
        run: async ({ order_id }) => `Order ${order_id}: shipped`
      });

      const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
      const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
      const client = new Anthropic({ authToken: environmentKey });
      const controller = new AbortController();
      process.once("SIGTERM", () => controller.abort());

      await new EnvironmentWorker({
        client,
        environmentId,
        environmentKey,
        workdir: "/workspace",
        signal: controller.signal,
        tools: (ctx) => [...betaAgentToolset20260401(ctx), getOrderStatus]
      }).run();
      ```

      <CodeGroupItem language="C#">
        El SDK de C# no tiene `EnvironmentWorker`. Para responder a las llamadas a herramientas personalizadas desde tu propio código, envía cada resultado en el flujo de eventos de la sesión, como en [Devolver un resultado de herramienta personalizada](https://platform.claude.com/docs/es/managed-agents/events-and-streaming#return-a-custom-tool-result).
      </CodeGroupItem>

      ```go Go
      package main

      import (
      	"context"
      	"log"
      	"os"
      	"os/signal"
      	"syscall"

      	"github.com/anthropics/anthropic-sdk-go"
      	"github.com/anthropics/anthropic-sdk-go/lib/environments"
      	"github.com/anthropics/anthropic-sdk-go/option"
      	"github.com/anthropics/anthropic-sdk-go/toolrunner"
      	"github.com/anthropics/anthropic-sdk-go/tools/agenttoolset"
      )

      type orderStatusInput struct {
      	OrderID string `json:"order_id"`
      }

      func main() {
      	environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")
      	environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")

      	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
      	defer stop()

      	getOrderStatus := toolrunner.NewBetaTool(
      		"get_order_status",
      		"Look up an order in the internal fulfillment system by order ID.",
      		anthropic.BetaToolInputSchemaParam{
      			Properties: map[string]any{
      				"order_id": map[string]any{"type": "string", "description": "The order ID"},
      			},
      			Required: []string{"order_id"},
      		},
      		// Se ejecuta en el host del worker: llama a cualquier cosa que el sandbox pueda alcanzar.
      		func(ctx context.Context, input orderStatusInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
      			return anthropic.BetaToolResultBlockParamContentUnion{
      				OfText: &anthropic.BetaTextBlockParam{Text: "Order " + input.OrderID + ": shipped"},
      			}, nil
      		},
      	)

      	client := anthropic.NewClient(option.WithAuthToken(environmentKey))

      	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
      		EnvironmentID:  environmentID,
      		EnvironmentKey: environmentKey,
      		Workdir:        "/workspace",
      		ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool {
      			return append(agenttoolset.BetaAgentToolset20260401(env), getOrderStatus)
      		},
      	})
      	if err := worker.Run(ctx); err != nil {
      		log.Fatalf("worker: %v", err)
      	}
      }
      ```

      <CodeGroupItem language="Java">
        El SDK de Java no tiene `EnvironmentWorker`. Para responder a las llamadas a herramientas personalizadas desde tu propio código, envía cada resultado en el flujo de eventos de la sesión, como en [Devolver un resultado de herramienta personalizada](https://platform.claude.com/docs/es/managed-agents/events-and-streaming#return-a-custom-tool-result).
      </CodeGroupItem>

      <CodeGroupItem language="PHP">
        El SDK de PHP no tiene `EnvironmentWorker`. Para responder a las llamadas a herramientas personalizadas desde tu propio código, envía cada resultado en el flujo de eventos de la sesión, como en [Devolver un resultado de herramienta personalizada](https://platform.claude.com/docs/es/managed-agents/events-and-streaming#return-a-custom-tool-result).
      </CodeGroupItem>

      <CodeGroupItem language="Ruby">
        El SDK de Ruby no tiene `EnvironmentWorker`. Para responder a las llamadas a herramientas personalizadas desde tu propio código, envía cada resultado en el flujo de eventos de la sesión, como en [Devolver un resultado de herramienta personalizada](https://platform.claude.com/docs/es/managed-agents/events-and-streaming#return-a-custom-tool-result).
      </CodeGroupItem>
    </CodeGroup>
  </Step>
</Steps>

El worker responde solo a las herramientas registradas en él. Si una herramienta está declarada en el agente pero ningún worker o cliente la sirve, la sesión se pausa con un motivo de detención `requires_action`. Permanece en pausa hasta que algo publique el resultado. Consulta [Responder llamadas a herramientas que pausan la sesión](https://platform.claude.com/docs/es/managed-agents/events-and-streaming#answer-tool-calls-that-pause-the-session) para ver el flujo de eventos.

## Envolver un servidor MCP como herramientas personalizadas

El [conector MCP](https://platform.claude.com/docs/es/managed-agents/mcp-connector) se conecta a servidores MCP desde el lado de Anthropic. Por lo tanto, un servidor debe exponer un endpoint HTTP al que Anthropic pueda llegar, directamente o a través de un [túnel MCP](https://platform.claude.com/docs/es/agents-and-tools/mcp-tunnels/overview).

Para usar un servidor al que solo tu red puede llegar, haz que el worker sea el cliente MCP y declara las herramientas del servidor como herramientas personalizadas. El servidor MCP no necesita conectividad entrante desde fuera de tu red. Anthropic recibe las definiciones de herramientas que declaras en el agente, la entrada de cada llamada y el resultado que tu worker publica de vuelta.

En tiempo de ejecución, el modelo llama a una herramienta envuelta como a cualquier otra herramienta personalizada:

1. El agente emite un evento `agent.custom_tool_use`.
2. El worker, dentro de tu sandbox, reenvía la llamada a través de su sesión MCP abierta al servidor en tu red.
3. El worker publica la respuesta del servidor como el `user.custom_tool_result`.

### Instalar un SDK de MCP

Los [helpers de MCP del lado del cliente](https://platform.claude.com/docs/es/agents-and-tools/mcp-connector#client-side-mcp-helpers) del SDK convierten las herramientas del servidor en las herramientas ejecutables que acepta el worker. Instala un SDK de MCP junto con el SDK de Anthropic: `pip install "anthropic[mcp]" "mcp>=1.24"` (typescript: `npm install @modelcontextprotocol/sdk`; go: `go get github.com/modelcontextprotocol/go-sdk`; csharp: `dotnet add package ModelContextProtocol`; java: `com.anthropic:anthropic-java-mcp`; php: `composer require mcp/sdk`; ruby: `bundle add mcp faraday event_stream_parser`).

Los ejemplos se conectan sin autenticación. Para enviar credenciales, configura el `http_client` (typescript: `requestInit`; go: `HTTPClient`; csharp: `AdditionalHeaders`; java: `customizeRequest`; php, ruby: `headers`) que le pasas al transporte MCP.

### Declarar y servir las herramientas

<Steps>
  <Step title="Declara las herramientas del servidor en el agente">
    Lista las herramientas del servidor MCP y declara cada una como una herramienta `custom`. Los campos `name`, `description` e `inputSchema` de MCP se asignan uno a uno a los campos de la herramienta personalizada. Si el servidor pagina su lista de herramientas, declara todas las páginas; el worker debe listar las mismas páginas.

    <CodeGroup exclude="shell">
      ```python Python
      import asyncio
      from typing import Any, cast
      from anthropic import AsyncAnthropic
      from anthropic.types.beta import BetaManagedAgentsCustomToolParams
      from mcp import ClientSession, types

      # Requiere mcp >= 1.24, que renombró streamablehttp_client a streamable_http_client.
      from mcp.client.streamable_http import streamable_http_client

      MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"


      def to_custom_tool(tool: types.Tool) -> BetaManagedAgentsCustomToolParams:
          # Los campos MCP se corresponden uno a uno con una declaración de herramienta personalizada. El cast
          # entrega el diccionario del esquema al parámetro tipado del SDK sin cambios.
          return {
              "type": "custom",
              "name": tool.name,
              "description": tool.description or tool.name,
              "input_schema": cast(Any, tool.inputSchema),
          }


      async def main() -> None:
          # Ejecuta esto donde crees los agentes, no en el host worker:
          # se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
          async with (
              streamable_http_client(MCP_SERVER_URL) as (read, write, _),
              ClientSession(read, write) as mcp_session,
              AsyncAnthropic() as client,
          ):
              await mcp_session.initialize()
              listed = await mcp_session.list_tools()
              agent = await client.beta.agents.create(
                  name="Internal tools agent",
                  model="claude-opus-5-5",
                  tools=[
                      {"type": "agent_toolset_20260401"},
                      *[to_custom_tool(tool) for tool in listed.tools],
                  ],
              )
              print(agent.id)


      asyncio.run(main())
      ```

      ```typescript TypeScript
      import Anthropic from "@anthropic-ai/sdk";
      import { Client } from "@modelcontextprotocol/sdk/client/index.js";
      import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

      const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";

      // Ejecuta esto donde crees los agentes, no en el host worker:
      // se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
      const client = new Anthropic();

      const mcpClient = new Client({ name: "declare-agent-tools", version: "1.0.0" });
      await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
      const { tools } = await mcpClient.listTools();

      const agent = await client.beta.agents.create({
        name: "Internal tools agent",
        model: "claude-opus-5-5",
        tools: [
          { type: "agent_toolset_20260401" },
          // Los campos MCP se corresponden uno a uno con una declaración de herramienta personalizada.
          ...tools.map((tool) => ({
            type: "custom" as const,
            name: tool.name,
            description: tool.description || tool.name,
            input_schema: tool.inputSchema
          }))
        ]
      });
      console.log(agent.id);

      await mcpClient.close();
      ```

      ```csharp C#
      using System.Text.Json;
      using Anthropic;
      using Anthropic.Models.Beta.Agents;
      using ModelContextProtocol.Client;

      const string McpServerUrl = "http://mcp.internal.example.com:8000/mcp";

      // Ejecuta esto donde crees agentes, no en el host del worker:
      // se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
      var client = new AnthropicClient();

      await using var mcpClient = await McpClient.CreateAsync(
          new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri(McpServerUrl) })
      );
      var mcpTools = await mcpClient.ListToolsAsync();

      List<Tool> tools =
      [
          new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401" },
      ];
      foreach (var tool in mcpTools)
      {
          var description = tool.Description;
          if (string.IsNullOrEmpty(description))
          {
              description = tool.Name;
          }

          // Los campos de MCP se corresponden uno a uno con una declaración de herramienta personalizada.
          // Las palabras clave del esquema pasan al SDK sin cambios.
          tools.Add(
              new BetaManagedAgentsCustomToolParams
              {
                  Type = "custom",
                  Name = tool.Name,
                  Description = description,
                  InputSchema = new(
                      tool.ProtocolTool.InputSchema.Deserialize<Dictionary<string, JsonElement>>()!
                  ),
              }
          );
      }

      var agent = await client.Beta.Agents.Create(new()
      {
          Name = "Internal tools agent",
          Model = BetaManagedAgentsModel.ClaudeOpus5_5,
          Tools = tools,
      });
      Console.WriteLine(agent.ID);
      ```

      ```go Go
      package main

      import (
      	"context"
      	"encoding/json"
      	"fmt"
      	"log"

      	"github.com/anthropics/anthropic-sdk-go"
      	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
      )

      const mcpServerURL = "http://mcp.internal.example.com:8000/mcp"

      // toCustomTool mapea una definición de herramienta MCP a una declaración de herramienta personalizada.
      // Los campos se mapean uno a uno: el parámetro tipado lleva `properties` y
      // `required`, y cualquier otra palabra clave de JSON Schema que emita el servidor viaja en
      // ExtraFields para que el esquema declarado coincida con el esquema del servidor.
      func toCustomTool(tool *mcpsdk.Tool) (anthropic.BetaAgentNewParamsToolUnion, error) {
      	raw, err := json.Marshal(tool.InputSchema)
      	if err != nil {
      		return anthropic.BetaAgentNewParamsToolUnion{}, err
      	}
      	var schema map[string]any
      	if err := json.Unmarshal(raw, &schema); err != nil {
      		return anthropic.BetaAgentNewParamsToolUnion{}, err
      	}

      	inputSchema := anthropic.BetaManagedAgentsCustomToolInputSchemaParam{ExtraFields: map[string]any{}}
      	for keyword, value := range schema {
      		switch keyword {
      		case "type":
      			// El tipo del parámetro siempre serializa "type": "object".
      		case "properties":
      			properties, _ := value.(map[string]any)
      			inputSchema.Properties = properties
      		case "required":
      			entries, _ := value.([]any)
      			for _, entry := range entries {
      				if name, isString := entry.(string); isString {
      					inputSchema.Required = append(inputSchema.Required, name)
      				}
      			}
      		default:
      			inputSchema.ExtraFields[keyword] = value
      		}
      	}

      	description := tool.Description
      	if description == "" {
      		description = tool.Name
      	}
      	return anthropic.BetaAgentNewParamsToolUnion{
      		OfCustom: &anthropic.BetaManagedAgentsCustomToolParams{
      			Type:        anthropic.BetaManagedAgentsCustomToolParamsTypeCustom,
      			Name:        tool.Name,
      			Description: description,
      			InputSchema: inputSchema,
      		},
      	}, nil
      }

      func main() {
      	ctx := context.Background()

      	// Ejecuta esto donde sea que crees agentes, no en el host worker:
      	// se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
      	client := anthropic.NewClient()

      	mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "declare-agent-tools", Version: "1.0.0"}, nil)
      	session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil)
      	if err != nil {
      		log.Fatalf("connect to MCP server: %v", err)
      	}
      	defer session.Close()

      	listed, err := session.ListTools(ctx, nil)
      	if err != nil {
      		log.Fatalf("list MCP tools: %v", err)
      	}

      	tools := []anthropic.BetaAgentNewParamsToolUnion{
      		{OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{
      			Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
      		}},
      	}
      	for _, tool := range listed.Tools {
      		custom, err := toCustomTool(tool)
      		if err != nil {
      			log.Fatalf("convert MCP tool %s: %v", tool.Name, err)
      		}
      		tools = append(tools, custom)
      	}

      	agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
      		Name:  "Internal tools agent",
      		Model: anthropic.BetaManagedAgentsModelConfigParams{ID: anthropic.BetaManagedAgentsModelClaudeOpus5_5},
      		Tools: tools,
      	})
      	if err != nil {
      		log.Fatalf("create agent: %v", err)
      	}
      	fmt.Println(agent.ID)
      }
      ```

      ```java Java
      import com.anthropic.client.okhttp.AnthropicOkHttpClient;
      import com.anthropic.core.JsonValue;
      import com.anthropic.models.beta.agents.AgentCreateParams;
      import com.anthropic.models.beta.agents.BetaManagedAgentsAgentToolset20260401Params;
      import com.anthropic.models.beta.agents.BetaManagedAgentsCustomToolInputSchema;
      import com.anthropic.models.beta.agents.BetaManagedAgentsCustomToolParams;
      import com.anthropic.models.beta.agents.BetaManagedAgentsModel;
      import io.modelcontextprotocol.client.McpClient;
      import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
      import io.modelcontextprotocol.spec.McpSchema;

      // Los campos de MCP se asignan uno a uno a una declaración de herramienta personalizada. El SDK de Java
      // de MCP conserva solo estas palabras clave de nivel superior del esquema de entrada de una herramienta: type,
      // properties, required, additionalProperties, $defs y definitions.
      BetaManagedAgentsCustomToolParams toCustomTool(McpSchema.Tool tool) {
          var description = tool.description();
          if (description == null || description.isEmpty()) {
              description = tool.name();
          }

          return BetaManagedAgentsCustomToolParams.builder()
              .type(BetaManagedAgentsCustomToolParams.Type.CUSTOM)
              .name(tool.name())
              .description(description)
              .inputSchema(JsonValue.from(tool.inputSchema())
                  .convert(BetaManagedAgentsCustomToolInputSchema.class))
              .build();
      }

      void main() {
          // Ejecuta esto donde crees agentes, no en el host del worker:
          // se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
          var client = AnthropicOkHttpClient.fromEnv();

          // La dirección del servidor MCP y luego la ruta de su endpoint MCP.
          var transport = HttpClientStreamableHttpTransport.builder("http://mcp.internal.example.com:8000")
              .endpoint("/mcp")
              .build();
          try (var mcpClient = McpClient.sync(transport)
                  .clientInfo(new McpSchema.Implementation("declare-agent-tools", "1.0.0"))
                  .build()) {
              mcpClient.initialize();

              var params = AgentCreateParams.builder()
                  .name("Internal tools agent")
                  .model(BetaManagedAgentsModel.CLAUDE_OPUS_5_5)
                  .addTool(BetaManagedAgentsAgentToolset20260401Params.builder()
                      .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401)
                      .build());
              for (var tool : mcpClient.listTools().tools()) {
                  params.addTool(toCustomTool(tool));
              }

              var agent = client.beta().agents().create(params.build());
              IO.println(agent.id());
          }
      }
      ```

      ```php PHP
      use Anthropic\Beta\Agents\BetaManagedAgentsModel;
      use Anthropic\Client as Anthropic;
      use Mcp\Client as McpClient;
      use Mcp\Client\Transport\HttpTransport;

      $mcpServerUrl = 'http://mcp.internal.example.com:8000/mcp';

      // Ejecuta esto donde crees agentes, no en el host del worker:
      // se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
      $client = new Anthropic();

      $mcpClient = McpClient::builder()->build();
      $mcpClient->connect(new HttpTransport($mcpServerUrl));

      $tools = [['type' => 'agent_toolset_20260401']];
      foreach ($mcpClient->listTools()->tools as $tool) {
          // Los campos de MCP se corresponden uno a uno con una declaración de herramienta personalizada.
          // Las palabras clave del esquema pasan al SDK sin cambios.
          $tools[] = [
              'type' => 'custom',
              'name' => $tool->name,
              'description' => $tool->description ?: $tool->name,
              'inputSchema' => $tool->inputSchema,
          ];
      }

      $agent = $client->beta->agents->create(
          name: 'Internal tools agent',
          model: BetaManagedAgentsModel::CLAUDE_OPUS_5_5,
          tools: $tools,
      );
      echo $agent->id, "\n";

      $mcpClient->disconnect();
      ```

      ```ruby Ruby
      require "anthropic"
      require "mcp"

      MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"

      # Ejecuta esto donde crees agentes, no en el host del worker:
      # se autentica con tu clave de API de Claude (ANTHROPIC_API_KEY).
      client = Anthropic::Client.new

      transport = MCP::Client::HTTP.new(url: MCP_SERVER_URL)
      mcp_client = MCP::Client.new(transport: transport)
      mcp_client.connect

      custom_tools = mcp_client.tools.map do |tool|
        description = tool.description
        description = tool.name if description.nil? || description.empty?

        # Los campos de MCP se corresponden uno a uno con una declaración de herramienta personalizada.
        # Las palabras clave del esquema pasan al SDK sin cambios.
        {
          type: :custom,
          name: tool.name,
          description: description,
          input_schema: tool.input_schema
        }
      end

      agent = client.beta.agents.create(
        name: "Internal tools agent",
        model: Anthropic::Beta::BetaManagedAgentsModel::CLAUDE_OPUS_5_5,
        tools: [{type: :agent_toolset_20260401}, *custom_tools]
      )
      puts agent.id

      transport.close
      ```
    </CodeGroup>
  </Step>

  <Step title="Sirve las herramientas desde el worker">
    Conéctate al mismo servidor MCP al iniciar, convierte sus herramientas con `async_mcp_tool` (typescript: `mcpTools`; go: `mcp.NewBetaTools`) y regístralas junto con `beta_agent_toolset_20260401` (typescript: `betaAgentToolset20260401`; go: `agenttoolset.BetaAgentToolset20260401`). Mantén una sesión MCP abierta durante toda la vida del worker.

    <CodeGroup exclude="shell, csharp, java, php, ruby">
      ```python Python
      import asyncio
      import os
      from datetime import timedelta
      from anthropic import AsyncAnthropic
      from anthropic.lib.environments import EnvironmentWorker
      from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401
      from anthropic.lib.tools.mcp import async_mcp_tool
      from mcp import ClientSession

      # Requiere mcp >= 1.24, que renombró streamablehttp_client a streamable_http_client.
      from mcp.client.streamable_http import streamable_http_client

      MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"


      async def main() -> None:
          environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
          environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
          # Conéctate al servidor MCP una vez al inicio y mantén la sesión abierta durante
          # la vida del worker. El timeout convierte una llamada a herramienta colgada en un
          # resultado de error en lugar de una llamada estancada.
          async with (
              streamable_http_client(MCP_SERVER_URL) as (read, write, _),
              ClientSession(
                  read, write, read_timeout_seconds=timedelta(seconds=60)
              ) as mcp_session,
              AsyncAnthropic(auth_token=environment_key) as client,
          ):
              await mcp_session.initialize()
              listed = await mcp_session.list_tools()
              mcp_tools = [async_mcp_tool(tool, mcp_session) for tool in listed.tools]
              await EnvironmentWorker(
                  client,
                  environment_id=environment_id,
                  environment_key=environment_key,
                  workdir="/workspace",
                  tools=lambda env: [*beta_agent_toolset_20260401(env), *mcp_tools],
              ).run()


      asyncio.run(main())
      ```

      ```typescript TypeScript
      import Anthropic from "@anthropic-ai/sdk";
      import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
      import {
        mcpTools,
        type MCPCallToolResultLike,
        type MCPClientLike
      } from "@anthropic-ai/sdk/helpers/beta/mcp";
      import { betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node";
      import { Client } from "@modelcontextprotocol/sdk/client/index.js";
      import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

      const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";

      const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
      const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
      const client = new Anthropic({ authToken: environmentKey });
      const controller = new AbortController();
      process.once("SIGTERM", () => controller.abort());

      // Conéctate al servidor MCP una vez al inicio y mantén la conexión abierta durante
      // toda la vida del worker.
      const mcpClient = new Client({ name: "sandbox-worker", version: "1.0.0" });
      await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
      const { tools } = await mcpClient.listTools();

      // El tipo de retorno de callTool del SDK de MCP aún incluye una forma de resultado heredada que
      // mcpTools no acepta; acótala. Elimina esto cuando MCPClientLike se amplíe.
      const mcpClientForTools: MCPClientLike = {
        callTool: (params) => mcpClient.callTool(params) as Promise<MCPCallToolResultLike>
      };

      await new EnvironmentWorker({
        client,
        environmentId,
        environmentKey,
        workdir: "/workspace",
        signal: controller.signal,
        tools: (ctx) => [...betaAgentToolset20260401(ctx), ...mcpTools(tools, mcpClientForTools)]
      }).run();
      ```

      <CodeGroupItem language="C#">
        El SDK de C# no tiene `EnvironmentWorker`.
      </CodeGroupItem>

      ```go Go
      package main

      import (
      	"context"
      	"log"
      	"os"
      	"os/signal"
      	"syscall"

      	"github.com/anthropics/anthropic-sdk-go"
      	"github.com/anthropics/anthropic-sdk-go/lib/environments"
      	"github.com/anthropics/anthropic-sdk-go/mcp"
      	"github.com/anthropics/anthropic-sdk-go/option"
      	"github.com/anthropics/anthropic-sdk-go/tools/agenttoolset"
      	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
      )

      const mcpServerURL = "http://mcp.internal.example.com:8000/mcp"

      func main() {
      	environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")
      	environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")

      	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
      	defer stop()

      	client := anthropic.NewClient(option.WithAuthToken(environmentKey))

      	// Conéctate al servidor MCP una vez al inicio y mantén la sesión abierta durante
      	// toda la vida del worker.
      	mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "sandbox-worker", Version: "1.0.0"}, nil)
      	session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil)
      	if err != nil {
      		log.Fatalf("connect to MCP server: %v", err)
      	}
      	defer session.Close()

      	listed, err := session.ListTools(ctx, nil)
      	if err != nil {
      		log.Fatalf("list MCP tools: %v", err)
      	}
      	mcpTools, err := mcp.NewBetaTools(listed.Tools, session)
      	if err != nil {
      		log.Fatalf("convert MCP tools: %v", err)
      	}

      	worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
      		EnvironmentID:  environmentID,
      		EnvironmentKey: environmentKey,
      		Workdir:        "/workspace",
      		ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool {
      			return append(agenttoolset.BetaAgentToolset20260401(env), mcpTools...)
      		},
      	})
      	if err := worker.Run(ctx); err != nil {
      		log.Fatalf("worker: %v", err)
      	}
      }
      ```

      <CodeGroupItem language="Java">
        El SDK de Java no tiene `EnvironmentWorker`.
      </CodeGroupItem>

      <CodeGroupItem language="PHP">
        El SDK de PHP no tiene `EnvironmentWorker`.
      </CodeGroupItem>

      <CodeGroupItem language="Ruby">
        El SDK de Ruby no tiene `EnvironmentWorker`.
      </CodeGroupItem>
    </CodeGroup>
  </Step>
</Steps>

## Límites y comportamiento

### Las herramientas se declaran, no se descubren en tiempo de ejecución

El worker lista las herramientas del servidor MCP una vez al iniciar y no puede agregar herramientas a una sesión en ejecución. Cuando cambien las herramientas del servidor:

1. Vuelve a declararlas, en el agente o en una sesión inactiva mediante [Actualizar la configuración del agente](https://platform.claude.com/docs/es/managed-agents/session-operations#updating-the-agent-configuration).
2. Reinicia el worker.

### Las declaraciones deben ajustarse a la API de Managed Agents

Los helpers de MCP conservan los nombres y descripciones del servidor, y la mayoría de los esquemas pasan sin cambios. Cambia el nombre, recorta o inserta en línea cuando una declaración infrinja una de estas reglas:

| Campo                         | Regla                                                                                                                                                                                                                                                                                                                                                 |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`                        | Único por agente. Letras, dígitos, guiones bajos y guiones, de 1 a 128 caracteres. No puede coincidir con una herramienta integrada del agente como `bash` o `read`, ni usar el prefijo reservado `mcp__` o el prefijo `ant__`.                                                                                                                       |
| `description`                 | Obligatorio y no vacío.                                                                                                                                                                                                                                                                                                                               |
| `input_schema`                | Acepta las palabras clave de JSON Schema que los servidores MCP suelen emitir, como `additionalProperties` y `title`. Rechaza palabras clave de referencia como `$ref` en cualquier lugar, y `oneOf`, `anyOf` y `allOf` en el nivel superior. Los nombres de propiedades usan letras, dígitos, guiones bajos, puntos y guiones, de 1 a 64 caracteres. |
| El arreglo `tools` del agente | Como máximo 256 entradas. Cada herramienta envuelta es una entrada, y el conjunto de herramientas integrado es una más.                                                                                                                                                                                                                               |

Dos casos requieren trabajo adicional:

* **Dos servidores exponen el mismo nombre de herramienta:** Define tú mismo el envoltorio con un nombre con prefijo y haz que llame al nombre original de la herramienta del servidor.
* **Un generador como pydantic factoriza los esquemas en `$defs`:** Inserta esos esquemas en línea antes de declarar la herramienta.

### Los fallos de herramientas aparecen como resultados de herramienta con error

Cuando el servidor MCP informa un error de herramienta, el worker publica un resultado de herramienta con error al que el modelo puede reaccionar. El contenido MCP sin equivalente en resultados de herramienta, como bloques de audio y enlaces a recursos, también aparece como error.

Establece un tiempo de espera en el cliente MCP para obtener un fallo más rápido y claro, como hace el ejemplo del worker de Python con `read_timeout_seconds`. Consulta [Una llamada a herramienta MCP envuelta se bloquea](https://platform.claude.com/docs/es/managed-agents/self-hosted-sandboxes-operations#a-wrapped-mcp-tool-call-hangs) para saber qué ocurre sin uno.

### Envuelve solo servidores que operas o en los que confías

El nombre, la descripción y los resultados de una herramienta envuelta entran en el contexto del modelo como los de cualquier otra herramienta. Son entradas no confiables que pueden influir en lo que el agente hace con sus otras herramientas, incluido `bash` en el host del worker. Declara solo las herramientas que quieres que use el agente.

### Las políticas de permisos no se aplican

Las [políticas de permisos](https://platform.claude.com/docs/es/managed-agents/permission-policies#custom-tools) rigen los conjuntos de herramientas integrado y de MCP. El worker ejecuta cada llamada a herramienta envuelta que hace el modelo, así que coloca cualquier paso de aprobación en tu propio código de herramienta.
