---
title: 자체 호스팅 샌드박스의 사용자 정의 도구
url: https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-custom-tools
description: 자체 호스팅 샌드박스 워커에서 사용자 정의 도구를 제공하고, 터널을 실행하지 않고도 네트워크 내부의 MCP 서버를 사용자 정의 도구로 래핑합니다.
featureMetadata:
  topic:
    title: Managed Agents
    url: https://platform.claude.com/docs/en/managed-agents/overview
  status: beta
  betaHeader: managed-agents-2026-04-01
---

[사용자 정의 도구](https://platform.claude.com/docs/ko/managed-agents/tools#custom-tools)는 사용자의 코드가 직접 실행하는 도구입니다. 에이전트는 `agent.custom_tool_use` 이벤트를 내보내고 일치하는 `user.custom_tool_result`를 기다립니다. 사용자의 워커가 바로 그 코드가 될 수 있습니다. 워커는 샌드박스 내부에서 실행되므로, 도구는 샌드박스에 구성한 내부 서비스, 자격 증명, 네트워크 이그레스에만 접근하며 그 이상에는 접근하지 않습니다.

환경 키가 사용자 정의 도구 결과 게시를 승인하므로, Claude API 키는 워커 호스트에 두지 않아도 됩니다.

<Note>
  사용자 정의 도구를 제공하려면 SDK 워커가 필요합니다. `ant` CLI 워커에는 사용자 정의 도구 구현을 등록할 방법이 없습니다. 세션별 샌드박스 패턴에서는 [샌드박스 내부에서 SDK 워커를 실행하세요](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-workers#run-the-sdk-worker-inside-the-sandbox).
</Note>

## 사용자 정의 도구 제공하기

<Steps>
  <Step title="에이전트에 도구 선언하기">
    에이전트의 `tools`에 워커가 등록하는 도구와 `name`이 일치하는 `custom` 항목을 추가하세요. 전체 선언 형식은 [사용자 정의 도구](https://platform.claude.com/docs/ko/managed-agents/tools#custom-tools)를 참조하세요.

    ```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="워커에 구현 등록하기">
    내장 도구 세트와 함께 워커의 `tools` (go: `ToolsFunc`) 팩토리([`EnvironmentWorker`](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-reference#environment-worker) 참조)를 통해 도구를 전달하세요:

    <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."""
          # 워커 호스트에서 실행됩니다: 샌드박스가 접근할 수 있는 모든 것을 호출할 수 있습니다.
          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"]
        },
        // 워커 호스트에서 실행됩니다: 샌드박스가 접근할 수 있는 모든 것을 호출할 수 있습니다.
        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#">
        C# SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 사용자 정의 도구 호출에 응답하려면 [커스텀 도구 결과 반환하기](https://platform.claude.com/docs/ko/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"},
      		},
      		// 워커 호스트에서 실행됩니다: 샌드박스가 접근할 수 있는 모든 것을 호출할 수 있습니다.
      		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">
        Java SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 사용자 정의 도구 호출에 응답하려면 [커스텀 도구 결과 반환하기](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming#return-a-custom-tool-result)에서처럼 각 결과를 세션의 이벤트 스트림으로 전송하세요.
      </CodeGroupItem>

      <CodeGroupItem language="PHP">
        PHP SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 사용자 정의 도구 호출에 응답하려면 [커스텀 도구 결과 반환하기](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming#return-a-custom-tool-result)에서처럼 각 결과를 세션의 이벤트 스트림으로 전송하세요.
      </CodeGroupItem>

      <CodeGroupItem language="Ruby">
        Ruby SDK에는 `EnvironmentWorker`가 없습니다. 자체 코드에서 사용자 정의 도구 호출에 응답하려면 [커스텀 도구 결과 반환하기](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming#return-a-custom-tool-result)에서처럼 각 결과를 세션의 이벤트 스트림으로 전송하세요.
      </CodeGroupItem>
    </CodeGroup>
  </Step>
</Steps>

워커는 자신에게 등록된 도구에만 응답합니다. 도구가 에이전트에 선언되어 있지만 이를 제공하는 워커나 클라이언트가 없으면, 세션은 `requires_action` 중지 사유와 함께 일시 중지됩니다. 무언가가 결과를 게시할 때까지 일시 중지 상태로 유지됩니다. 이벤트 흐름은 [세션을 일시 중지시키는 도구 호출에 응답하기](https://platform.claude.com/docs/ko/managed-agents/events-and-streaming#answer-tool-calls-that-pause-the-session)를 참조하세요.

## MCP 서버를 사용자 정의 도구로 래핑하기

[MCP 커넥터](https://platform.claude.com/docs/ko/managed-agents/mcp-connector)는 Anthropic 측에서 MCP 서버에 연결합니다. 따라서 서버는 Anthropic이 직접 또는 [MCP 터널](https://platform.claude.com/docs/ko/agents-and-tools/mcp-tunnels/overview)을 통해 접근할 수 있는 HTTP 엔드포인트를 노출해야 합니다.

사용자의 네트워크에서만 접근할 수 있는 서버를 사용하려면, 대신 워커를 MCP 클라이언트로 만들고 서버의 도구를 사용자 정의 도구로 선언하세요. MCP 서버는 네트워크 외부로부터의 인바운드 연결이 필요하지 않습니다. Anthropic은 에이전트에 선언한 도구 정의, 각 호출의 입력, 그리고 워커가 다시 게시하는 결과를 받습니다.

런타임에 모델은 래핑된 도구를 다른 사용자 정의 도구와 마찬가지로 호출합니다:

1. 에이전트가 `agent.custom_tool_use` 이벤트를 내보냅니다.
2. 샌드박스 내부의 워커가 열려 있는 MCP 세션을 통해 네트워크상의 서버로 호출을 전달합니다.
3. 워커가 서버의 응답을 `user.custom_tool_result`로 게시합니다.

### MCP SDK 설치하기

SDK의 [클라이언트 측 MCP 헬퍼](https://platform.claude.com/docs/ko/agents-and-tools/mcp-connector#client-side-mcp-helpers)는 서버의 도구를 워커가 받아들이는 실행 가능한 도구로 변환합니다. Anthropic SDK와 함께 MCP SDK를 설치하세요: `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`).

예제는 인증 없이 연결합니다. 자격 증명을 보내려면 MCP 전송에 전달하는 `http_client` (typescript: `requestInit`; go: `HTTPClient`; csharp: `AdditionalHeaders`; java: `customizeRequest`; php, ruby: `headers`)를 구성하세요.

### 도구 선언 및 제공하기

<Steps>
  <Step title="에이전트에 서버의 도구 선언하기">
    MCP 서버의 도구를 나열하고 각각을 `custom` 도구로 선언하세요. MCP의 `name`, `description`, `inputSchema`는 사용자 정의 도구의 필드에 일대일로 대응됩니다. 서버가 도구 목록을 페이지로 나누어 제공하는 경우 모든 페이지를 선언하세요. 워커도 동일한 페이지를 나열해야 합니다.

    <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

      # mcp >= 1.24가 필요합니다. 이 버전에서 streamablehttp_client가 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:
          # MCP 필드는 커스텀 도구 선언에 일대일로 매핑됩니다. 캐스트는
          # 스키마 딕셔너리를 SDK의 타입 지정 매개변수에 그대로 전달합니다.
          return {
              "type": "custom",
              "name": tool.name,
              "description": tool.description or tool.name,
              "input_schema": cast(Any, tool.inputSchema),
          }


      async def main() -> None:
          # 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요.
          # Claude API 키(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";

      // 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요.
      // 이 코드는 Claude API 키(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" },
          // MCP 필드는 커스텀 도구 선언에 일대일로 매핑됩니다.
          ...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";

      // 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요:
      // Claude API 키(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;
          }

          // MCP 필드는 사용자 지정 도구 선언에 일대일로 대응합니다.
          // 스키마의 키워드는 변경 없이 SDK에 전달됩니다.
          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은 하나의 MCP 도구 정의를 커스텀 도구 선언으로 매핑합니다.
      // 필드는 일대일로 매핑됩니다: 타입이 지정된 매개변수는 `properties`와
      // `required`를 담고, 서버가 내보내는 다른 모든 JSON Schema 키워드는
      // ExtraFields로 전달되어 선언된 스키마가 서버의 스키마와 일치하게 됩니다.
      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":
      			// 매개변수 타입은 항상 "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()

      	// 이 코드는 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요:
      	// Claude API 키(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;

      // MCP 필드는 custom tool 선언에 일대일로 대응합니다. MCP Java
      // SDK는 도구 입력 스키마의 다음 최상위 키워드만 유지합니다: type,
      // properties, required, additionalProperties, $defs, 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() {
          // 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요:
          // Claude API 키(ANTHROPIC_API_KEY)로 인증하기 때문입니다.
          var client = AnthropicOkHttpClient.fromEnv();

          // MCP 서버의 주소와 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';

      // 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요:
      // Claude API 키(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) {
          // MCP 필드는 사용자 지정 도구 선언에 일대일로 대응합니다.
          // 스키마의 키워드는 변경 없이 SDK에 전달됩니다.
          $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"

      # 이 코드는 워커 호스트가 아니라 에이전트를 생성하는 곳에서 실행하세요:
      # Claude API 키(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?

        # MCP 필드는 사용자 지정 도구 선언에 일대일로 대응합니다.
        # 스키마의 키워드는 변경 없이 SDK에 전달됩니다.
        {
          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="워커에서 도구 제공하기">
    시작 시 동일한 MCP 서버에 연결하고, `async_mcp_tool` (typescript: `mcpTools`; go: `mcp.NewBetaTools`)로 해당 도구를 변환한 다음, `beta_agent_toolset_20260401` (typescript: `betaAgentToolset20260401`; go: `agenttoolset.BetaAgentToolset20260401`)와 함께 등록하세요. 워커가 실행되는 동안 하나의 MCP 세션을 계속 열어 두세요.

    <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

      # mcp >= 1.24가 필요합니다. 이 버전에서 streamablehttp_client가 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"]
          # 시작 시 MCP 서버에 한 번 연결하고 워커가 실행되는 동안
          # 세션을 열어 둡니다. 타임아웃은 멈춘 도구 호출을 중단된 호출이 아닌
          # 오류 결과로 바꿉니다.
          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());

      // 시작 시 MCP 서버에 한 번 연결하고 워커가 살아 있는 동안
      // 연결을 유지합니다.
      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();

      // MCP SDK의 callTool 반환 타입에는 mcpTools가 허용하지 않는 레거시 결과 형태가
      // 아직 포함되어 있으므로 타입을 좁힙니다. MCPClientLike가 확장되면 제거하세요.
      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#">
        C# SDK에는 `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))

      	// 시작 시 MCP 서버에 한 번 연결하고 워커가 실행되는 동안
      	// 세션을 계속 열어 둡니다.
      	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">
        Java SDK에는 `EnvironmentWorker`가 없습니다.
      </CodeGroupItem>

      <CodeGroupItem language="PHP">
        PHP SDK에는 `EnvironmentWorker`가 없습니다.
      </CodeGroupItem>

      <CodeGroupItem language="Ruby">
        Ruby SDK에는 `EnvironmentWorker`가 없습니다.
      </CodeGroupItem>
    </CodeGroup>
  </Step>
</Steps>

## 제한 사항 및 동작

### 도구는 런타임에 검색되지 않고 선언됩니다

워커는 시작 시 MCP 서버의 도구를 한 번 나열하며, 실행 중인 세션에 도구를 추가할 수 없습니다. 서버의 도구가 변경되면:

1. 에이전트에서, 또는 [에이전트 구성 업데이트](https://platform.claude.com/docs/ko/managed-agents/session-operations#updating-the-agent-configuration)를 통해 유휴 세션에서 도구를 다시 선언하세요.
2. 워커를 다시 시작하세요.

### 선언은 Managed Agents API에 맞아야 합니다

MCP 헬퍼는 서버의 이름과 설명을 유지하며, 대부분의 스키마는 변경 없이 그대로 전달됩니다. 선언이 다음 규칙 중 하나를 위반하는 경우 이름을 바꾸거나, 줄이거나, 인라인으로 처리하세요:

| 필드               | 규칙                                                                                                                                                                                                |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`           | 에이전트별로 고유해야 합니다. 문자, 숫자, 밑줄, 하이픈으로 구성된 1–128자입니다. `bash`나 `read` 같은 내장 에이전트 도구와 일치하거나, 예약된 `mcp__` 접두사 또는 `ant__` 접두사를 사용할 수 없습니다.                                                                |
| `description`    | 필수이며 비어 있으면 안 됩니다.                                                                                                                                                                                |
| `input_schema`   | `additionalProperties`, `title` 등 MCP 서버가 일반적으로 내보내는 JSON Schema 키워드를 허용합니다. `$ref` 같은 참조 키워드는 위치에 관계없이 거부되며, 최상위 수준의 `oneOf`, `anyOf`, `allOf`도 거부됩니다. 속성 이름은 문자, 숫자, 밑줄, 점, 하이픈으로 구성된 1–64자입니다. |
| 에이전트의 `tools` 배열 | 최대 256개 항목입니다. 래핑된 각 도구가 하나의 항목이며, 내장 도구 세트가 하나의 항목을 더 차지합니다.                                                                                                                                     |

다음 두 가지 경우에는 추가 작업이 필요합니다:

* **두 서버가 동일한 도구 이름을 노출하는 경우:** 접두사가 붙은 이름으로 래퍼를 직접 정의하고, 해당 래퍼가 서버의 원래 도구 이름을 호출하도록 하세요.
* **pydantic과 같은 생성기가 스키마를 `$defs`로 분리하는 경우:** 도구를 선언하기 전에 해당 스키마를 인라인으로 처리하세요.

### 도구 실패는 오류 도구 결과로 나타납니다

MCP 서버가 도구 오류를 보고하면, 워커는 모델이 대응할 수 있는 오류 도구 결과를 게시합니다. 오디오 블록이나 리소스 링크처럼 도구 결과에 대응하는 형식이 없는 MCP 콘텐츠도 오류로 나타납니다.

Python 워커 예제에서 `read_timeout_seconds`로 하는 것처럼, 더 빠르고 명확하게 실패하도록 MCP 클라이언트에 타임아웃을 설정하세요. 타임아웃이 없을 때 어떤 일이 발생하는지는 [래핑된 MCP 도구 호출이 멈춤](https://platform.claude.com/docs/ko/managed-agents/self-hosted-sandboxes-operations#a-wrapped-mcp-tool-call-hangs)을 참조하세요.

### 직접 운영하거나 신뢰하는 서버만 래핑하세요

래핑된 도구의 이름, 설명, 결과는 다른 도구와 마찬가지로 모델의 컨텍스트에 들어갑니다. 이는 에이전트가 워커 호스트의 `bash`를 포함한 다른 도구로 수행하는 작업에 영향을 줄 수 있는 신뢰할 수 없는 입력입니다. 에이전트가 사용하도록 의도한 도구만 선언하세요.

### 권한 정책은 적용되지 않습니다

[권한 정책](https://platform.claude.com/docs/ko/managed-agents/permission-policies#custom-tools)은 내장 도구 세트와 MCP 도구 세트를 관리합니다. 워커는 모델이 수행하는 모든 래핑된 도구 호출을 실행하므로, 승인 단계가 필요하다면 직접 작성한 도구 코드에 넣으세요.
