MCPを繋ぐだけで使えるCode Interpreter環境をAgentCore Runtimeで作ってみた

MCPを繋ぐだけで使えるCode Interpreter環境をAgentCore Runtimeで作ってみた

AWS AgentCore RuntimeとMCPプロトコルを組み合わせ、複数のLLMから共有できるCode Interpreterサーバーをゼロから構築する方法を紹介します。CSVの投入からpptx作成まで、実際の動作確認も交えて解説しました。
2026.08.12

はじめに

こんにちは、AI事業本部・生成AIインテグレーション部・西日本開発チームの政岡です。

複数の LLM プロバイダのモデルを切り替えて使える LibreChat のようなチャット環境で、「CSV を渡して集計してもらう」「グラフや資料を作ってもらう」といった作業もさせたくなりました。そのためにはコード実行環境が必要です。

LibreChat 自体にも Code Interpreter 機能はありますが、ホスト型APIは新規受付停止中で、無料で使うには実行基盤のセルフホストが必要です。

また、もともと使っていた Azure AI Foundry の Code Interpreter は、そもそも LibreChat が対応していません。仮に対応していたとしても Foundry のエージェント側からしか呼べないため、Bedrock の Claude で使うにはまた別の仕組みを用意することになります。モデルやプロバイダが変わるたびにコード実行環境を作り直すのは避けたいところです。

そこで、実行環境は1つに固定して、どのモデルからも同じインターフェースで呼べる形を試してみました。Amazon Bedrock AgentCore の Code Interpreter を MCP サーバーとして切り出し、MCP を繋げるクライアントならどれからでもコード実行とファイル入出力が使えます。

今回作るもの

構成図

MCPクライアントは、AWS公式のstdioプロキシであるmcp-proxy-for-awsを挟んでSigV4署名し、AgentCore Runtime上のMCPサーバーに接続します。
MCPサーバーは内部でCode Interpreterのセッションを1つ保持し、コード実行やファイル操作をツールとして公開します。

提供するツール

提供するツールは8つです。

ツール 役割
execute_python Pythonコードをサンドボックスで実行
execute_js JavaScriptコードをサンドボックスで実行
execute_ts TypeScriptコードをサンドボックスで実行
execute_command シェルコマンドを実行
write_file テキストをそのままサンドボックスに書き込む(小さいファイル向け)
get_upload_url アップロード用の署名付きURLを発行
load_file S3のファイルをサンドボックスに取り込む
save_file サンドボックスのファイルを取り出す(画像は画像のまま、それ以外はダウンロード用URL)

ファイルの受け渡し

アップロードは以下の流れです。

  1. get_upload_urlで署名付きURLを発行する
  2. クライアントがそのURLへHTTP PUTする
  3. load_fileでS3からサンドボックスに取り込む

ダウンロードは逆向きで、save_fileがサンドボックスからS3に書き出し、ダウンロード用の署名付きURLを返します。
ただし画像(png / jpg / gif / webp)だけは例外で、URLではなく画像そのものをMCPの画像コンテンツとして返します。対応クライアントならチャットにそのまま表示されるので、グラフを作らせたときにリンクを踏まずに結果が見えます。

ファイルの受け渡し

使用する技術について

AgentCore RuntimeのMCPサーバーホスティング

AgentCore RuntimeはMCPプロトコルをネイティブサポートしています。
コンテナが0.0.0.0:8000/mcpでstreamable-HTTPを受ければ、あとはRuntimeがセッション管理(Mcp-Session-Idがない場合の自動付与)とスケーリングをやってくれます。

サーバーはstatelessモード(stateless_http=True)で実装します。公式ドキュメントが基本のMCPサーバーに推奨しているモードで、statefulモードが必要になるのはelicitationやsamplingといったMCPの双方向機能を使う場合だけです。statelessにしてもやり取りのたびに状態が消えるわけではありません。同じMcp-Session-Idのリクエストは同じインスタンスに届くので、状態の維持はRuntime側のセッション管理に任せられます。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-mcp.html

インバウンド認証はIAM(SigV4)かOAuth 2.0(JWT)の2択で、今回はSigV4を使います。

AgentCore Code Interpreter

コードを安全に実行できるマネージドなサンドボックスです。以下のような特徴があります。

  • セッション単位で状態を保持する。タイムアウトはデフォルト15分(最大8時間)で、終了時にサンドボックス内のファイルは消える
  • executeCode(Python / JavaScript / TypeScript)、executeCommandwriteFilesなどのツール操作をinvokeできる
  • 今回使うのはAWSマネージドのデフォルト環境(識別子aws.codeinterpreter.v1)。外部インターネットに出られない閉じたネットワーク設定(networkMode: SANDBOX)で、S3などAWSサービスへのアクセスは通るが、PyPIやnpmレジストリはDNS解決から失敗するためpip installnpm installはできない

どうしてもpip installが必要な場合は、create-code-interpreter APIでカスタムのCode Interpreterを作り、networkMode: PUBLICを指定すれば外部インターネットに出られます。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-interpreter-create.html

mcp-proxy-for-aws

SigV4認証では、リクエストごとに本文や時刻から署名を計算してヘッダーに載せる必要があります。一方、Claude CodeやLibreChatといったMCPクライアントの設定でリモートMCPサーバーに付けられるのは、Authorization: Bearer xxxのような固定値のヘッダーだけです。リクエストごとに署名を計算する仕組みがないのでそのままでは繋げません。

そこでAWS公式のmcp-proxy-for-awsを使います。クライアントのローカルでstdioのMCPサーバーとして起動し、受け取ったリクエストをAWS認証情報でSigV4署名してRuntimeに転送してくれます。

https://github.com/aws/mcp-proxy-for-aws

やってみる

前提

検証時のバージョンです。

項目 バージョン
リージョン ap-northeast-1
Python 3.14
uv 0.9.7
AgentCore CLI(@aws/agentcore) 0.24.0
mcp 2.0.0
bedrock-agentcore 1.21.0
boto3 1.43.67
mcp-proxy-for-aws 1.6.4
LibreChat 0.8.7

プロジェクトを作成する

AgentCore CLIでプロジェクトを作ります。

pnpm add -g @aws/agentcore
agentcore create --project-name cimcpserver --name code_interpreter_mcp \
  --protocol MCP --language Python --build CodeZip

--build CodeZipは、コードと依存パッケージをzipにまとめてデプロイする方式で、Dockerが要りません。

実行するとCDKベースのプロジェクトが生成されます。MCPサーバー本体はapp/code_interpreter_mcp/main.pyに書いていくので、先に依存を追加しておきます。

cd cimcpserver/app/code_interpreter_mcp
uv add "mcp>=2.0.0,<3" "bedrock-agentcore>=1.21.0,<2" boto3

バージョンに上限を付けているのは、CodeZipビルドがuv.lockを見ずに依存を解決し直すためです。
上限がないと、デプロイのたびに最新のメジャーバージョンが入ってきます。

MCPサーバーを実装する

公式ドキュメントのMCPサーバー例を参考に実装していきます。Runtime向けのポイントはstateless_http=Truehost="0.0.0.0"だけです。

公式ドキュメントのコードはMCP Python SDK v1系のFastMCPで書かれていますが、ここではv2系で書きます。2026年7月28日に出たv2でFastMCPMCPServerにリネームされ、インポート元もmcp.server.fastmcpからmcp.server.mcpserverに変わりました。v1系はメンテナンスモードに入っています。

https://py.sdk.modelcontextprotocol.io/migration/

骨格を作る

まず、設定値の読み込みとクライアントの初期化までを書きます。
この後のヘルパー関数とツールは、code_interpreterの定義とif __name__ == "__main__":のあいだに足していきます。
なお、環境変数FILE_BUCKETSIGNING_SECRET_IDが指すS3バケットとシークレットはこの後の手順で作成し、値はデプロイ設定から渡します。

main.py(骨格)
import json
import os
from pathlib import Path

import boto3
from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter
from botocore.config import Config
from mcp.server.mcpserver import Image, MCPServer

REGION = os.environ.get("AWS_REGION", "ap-northeast-1")
FILE_BUCKET = os.environ["FILE_BUCKET"]  # ファイル受け渡し用の S3 バケット名
SIGNING_SECRET_ID = os.environ.get("SIGNING_SECRET_ID")  # 署名専用アクセスキーのシークレット ID
URL_EXPIRES_SECONDS = 900  # 署名付き URL の有効期限
SESSION_TIMEOUT_SECONDS = 1800  # Code Interpreter セッションのアイドルタイムアウト

mcp = MCPServer("code-interpreter")

S3_CONFIG = Config(signature_version="s3v4", s3={"addressing_style": "virtual"})

s3 = boto3.client("s3", region_name=REGION, config=S3_CONFIG)

code_interpreter = CodeInterpreter(REGION)

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, stateless_http=True)

署名付きURLをSigV4で発行させる

s3クライアントを作るときにConfig(signature_version="s3v4")を明示しているのは、これがないと発行した署名付きURLへのPUTが307 Temporary Redirect403 SignatureDoesNotMatchで落ちるからです。
明示しない場合、以下の流れで失敗します。

  1. boto3が署名付きURLを旧方式のSigV2で生成することがある
  2. SigV2はリージョン情報を署名に含まないため、URLはグローバルエンドポイント(s3.amazonaws.com)形式になる
  3. グローバルエンドポイントの実体は米国東部にあるので、東京のバケットへのPUTには「リージョナルエンドポイントへ行け」という307リダイレクトが返り、そこで失敗する

s3v4を明示しておけば、発行されるURLは<バケット名>.s3.ap-northeast-1.amazonaws.comという東京リージョンのホスト名になり、PUTもGETもリダイレクトなしで通ります。

ヘルパー関数を実装する

ヘルパーは3つ用意しました。

main.py(ヘルパー)
def get_session_id() -> str:
    """Code Interpreter セッションの ID を返す。未開始なら開始する。"""
    if not code_interpreter.session_id:
        code_interpreter.start(session_timeout_seconds=SESSION_TIMEOUT_SECONDS)
    return code_interpreter.session_id

def call_code_interpreter(tool_name: str, arguments: dict) -> str:
    """Code Interpreter のツールを1回呼び出し、結果のテキストを返す。"""
    get_session_id()
    response = code_interpreter.invoke(tool_name, arguments)
    for event in response["stream"]:
        result = event["result"]
        texts = [c["text"] for c in result.get("content", []) if c.get("type") == "text"]
        output = "\n".join(texts)
        if result.get("isError"):
            raise RuntimeError(f"Code Interpreter でエラーが発生しました: {output}")
        return output
    return ""

def build_s3_key(filename: str) -> str:
    """セッション ID をプレフィックスにした S3 キーを組み立てる。

    セッションごとにプレフィックスを分けることで、別セッションのファイルと混ざらない。
    """
    return f"{get_session_id()}/{filename}"

コード実行ツールを実装する

ここからツール本体です。まずコード実行系の4つです。

main.py(execute_python)
@mcp.tool()
def execute_python(code: str) -> str:
    """Python コードをサンドボックスで実行し、実行結果を返す。

    pandas、matplotlib、python-pptx などがプリインストールされている。
    同じセッション内では変数やファイルが実行をまたいで保持される。
    """
    return call_code_interpreter("executeCode", {"language": "python", "code": code})
main.py(execute_js)
@mcp.tool()
def execute_js(code: str) -> str:
    """JavaScript コードをサンドボックスで実行し、実行結果を返す。

    ESM 構文のみ対応(require は使えない)。組み込みモジュールは `import fs from "node:fs"`
    のように node: プレフィックスで読み込む。外部ネットワークに出られないため
    npm パッケージの取得はできない。Python と同じサンドボックスを共有するため、
    ファイルは言語をまたいで読み書きできる。
    """
    return call_code_interpreter("executeCode", {"language": "javascript", "code": code})
main.py(execute_ts)
@mcp.tool()
def execute_ts(code: str) -> str:
    """TypeScript コードをサンドボックスで実行し、実行結果を返す。

    型注釈付きのコードをトランスパイルなしでそのまま実行できる。
    モジュールの制約は execute_js と同じ(ESM のみ・npm パッケージの取得は不可)。
    """
    return call_code_interpreter("executeCode", {"language": "typescript", "code": code})
main.py(execute_command)
@mcp.tool()
def execute_command(command: str) -> str:
    """サンドボックスでシェルコマンドを実行し、実行結果を返す。

    外部ネットワークには出られない(pip install / npm install は不可)。
    """
    return call_code_interpreter("executeCommand", {"command": command})

MCP Python SDKは、関数名をツール名、docstringをツールのdescriptionとして登録します。
AIはこのdescriptionを見てツールを選びます。

https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/servers/tools.md

実際、最初はexecute_jsの説明に「npm installできる」と書いてしまい、AIが律儀にnpm installを試みてタイムアウトすることがありました。

署名付きURLヘルパーを実装する

署名付きURLの発行を3つのヘルパーにまとめます。

main.py(署名付き URL ヘルパー)
_signer = None  # 署名専用 S3 クライアントのキャッシュ

def get_signer():
    """署名付き URL の生成にだけ使う S3 クライアントを返す。"""
    global _signer
    if _signer is None:
        if SIGNING_SECRET_ID:
            secrets = boto3.client("secretsmanager", region_name=REGION)
            creds = json.loads(secrets.get_secret_value(SecretId=SIGNING_SECRET_ID)["SecretString"])
            _signer = boto3.client(
                "s3",
                region_name=REGION,
                aws_access_key_id=creds["AccessKeyId"],
                aws_secret_access_key=creds["SecretAccessKey"],
                config=S3_CONFIG,
            )
        else:
            _signer = s3
    return _signer

def presign_upload(filename: str) -> str:
    """S3 へのアップロード用(HTTP PUT)署名付き URL を発行する。"""
    return get_signer().generate_presigned_url(
        "put_object",
        Params={"Bucket": FILE_BUCKET, "Key": build_s3_key(filename)},
        ExpiresIn=URL_EXPIRES_SECONDS,
    )

def presign_download(filename: str) -> str:
    """S3 からのダウンロード用(HTTP GET)署名付き URL を発行する。"""
    return get_signer().generate_presigned_url(
        "get_object",
        Params={"Bucket": FILE_BUCKET, "Key": build_s3_key(filename)},
        ExpiresIn=URL_EXPIRES_SECONDS,
    )

get_signerは、Secrets Managerに置いた静的アクセスキーでS3クライアントを作り、署名付きURLの発行にだけ使います。
実行ロールの一時認証情報で署名すると、URLに約900文字のX-Amz-Security-Tokenが付き、モデルがURLを書き写すときに文字を落とすためです。
静的キーで署名したURLは認証情報の失効では無効にならないので、有効期限(URL_EXPIRES_SECONDS)を15分にして、漏れたときの影響を抑えています。

ファイル操作ツールを実装する

ファイル系ツールは4つです。

main.py(get_upload_url)
@mcp.tool()
def get_upload_url(filename: str) -> str:
    """ファイルをサンドボックスに渡すためのアップロード用 URL を発行する。

    返された URL に HTTP PUT でファイルをアップロードしたあと、
    load_file を呼ぶとサンドボックスに取り込まれる。
    """
    return presign_upload(filename)
main.py(load_file)
@mcp.tool()
def load_file(filename: str) -> str:
    """アップロード済みのファイルをサンドボックスの作業ディレクトリに取り込む。"""
    url = presign_download(filename)
    call_code_interpreter("executeCommand", {"command": f'curl -sSf -o "{filename}" "{url}"'})
    return f"{filename} をサンドボックスに配置しました"

load_fileの中身は、サンドボックス内でcurlを叩いてS3から取ってくるだけです。MCPサーバーのプロセスがファイルを中継しないので、Code Interpreterが対応する5GBまでのファイルをそのまま扱えます。

https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/code-interpreter-tool.html

main.py(save_file)
INLINE_IMAGE_FORMATS = {
    ".png": "png",
    ".jpg": "jpeg",
    ".jpeg": "jpeg",
    ".gif": "gif",
    ".webp": "webp",
}
MAX_INLINE_IMAGE_BYTES = 5 * 1024 * 1024

@mcp.tool()
def save_file(filename: str):
    """サンドボックス内のファイルを手元に取り出す。

    画像(png/jpg/gif/webp)は画像そのものを返すので、対応クライアントではチャットに
    そのまま表示される。それ以外のファイルはダウンロード用 URL を返す。
    URL は絶対に書き換えず、受け取った文字列のまま提示すること。
    """
    upload_url = presign_upload(filename)
    call_code_interpreter("executeCommand", {"command": f'curl -sSf -X PUT -T "{filename}" "{upload_url}"'})

    image_format = INLINE_IMAGE_FORMATS.get(Path(filename).suffix.lower())
    if image_format:
        body = s3.get_object(Bucket=FILE_BUCKET, Key=build_s3_key(filename))["Body"].read()
        if len(body) <= MAX_INLINE_IMAGE_BYTES:
            return Image(data=body, format=image_format)

    return presign_download(filename)

ImageはMCP Python SDKのクラスで、これを返すと画像がそのままクライアントに渡ります。
画像の実体がbase64でLLMのコンテキストに載るため、5MB以下の画像だけこの経路にし、それ以外はダウンロード用URLを返します。
また、返り値を-> str | Imageと注釈するとMCP Python SDKがスキーマを作れずインポート時点で落ちるため、save_fileだけ型注釈を省いています。

main.py(write_file)
@mcp.tool()
def write_file(filename: str, content: str) -> str:
    """テキスト内容をそのままサンドボックスにファイルとして書き込む。

    S3 を経由しない(MCP の通信だけで完結する)ため、小さいテキストの受け渡しに向く。
    大きいファイルやバイナリは get_upload_url / load_file を使う。
    """
    call_code_interpreter("writeFiles", {"content": [{"path": filename, "text": content}]})
    return f"{filename} をサンドボックスに書き込みました"

write_fileload_fileはどちらも「ファイルをサンドボックスに置く」ツールですが、経路が違います。

write_file get_upload_url + load_file
ファイルが流れる経路 MCPのツール引数(LLMのコンテキストを通る) S3(HTTPで直行)
サイズ 数十KBのテキストが実用上の限界 5GBまで
バイナリ 不可
クライアント側の要件 MCPが繋がればよい S3エンドポイントへのHTTPSアクセスが必要

普段使いはload_file側で、write_fileは設定ファイルや小さなスクリプトを1枚置くときに用いる使い分けです。

ここまで分けて載せてきたコードをつなげた全文を置いておきます。

main.py 全文
main.py
import json
import os
from pathlib import Path

import boto3
from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter
from botocore.config import Config
from mcp.server.mcpserver import Image, MCPServer

REGION = os.environ.get("AWS_REGION", "ap-northeast-1")
FILE_BUCKET = os.environ["FILE_BUCKET"]  # ファイル受け渡し用の S3 バケット名
SIGNING_SECRET_ID = os.environ.get("SIGNING_SECRET_ID")  # 署名専用アクセスキーのシークレット ID
URL_EXPIRES_SECONDS = 900  # 署名付き URL の有効期限
SESSION_TIMEOUT_SECONDS = 1800  # Code Interpreter セッションのアイドルタイムアウト

mcp = MCPServer("code-interpreter")

S3_CONFIG = Config(signature_version="s3v4", s3={"addressing_style": "virtual"})

s3 = boto3.client("s3", region_name=REGION, config=S3_CONFIG)

code_interpreter = CodeInterpreter(REGION)

def get_session_id() -> str:
    """Code Interpreter セッションの ID を返す。未開始なら開始する。"""
    if not code_interpreter.session_id:
        code_interpreter.start(session_timeout_seconds=SESSION_TIMEOUT_SECONDS)
    return code_interpreter.session_id

def call_code_interpreter(tool_name: str, arguments: dict) -> str:
    """Code Interpreter のツールを1回呼び出し、結果のテキストを返す。"""
    get_session_id()
    response = code_interpreter.invoke(tool_name, arguments)
    for event in response["stream"]:
        result = event["result"]
        texts = [c["text"] for c in result.get("content", []) if c.get("type") == "text"]
        output = "\n".join(texts)
        if result.get("isError"):
            raise RuntimeError(f"Code Interpreter でエラーが発生しました: {output}")
        return output
    return ""

def build_s3_key(filename: str) -> str:
    """セッション ID をプレフィックスにした S3 キーを組み立てる。

    セッションごとにプレフィックスを分けることで、別セッションのファイルと混ざらない。
    """
    return f"{get_session_id()}/{filename}"

@mcp.tool()
def execute_python(code: str) -> str:
    """Python コードをサンドボックスで実行し、実行結果を返す。

    pandas、matplotlib、python-pptx などがプリインストールされている。
    同じセッション内では変数やファイルが実行をまたいで保持される。
    """
    return call_code_interpreter("executeCode", {"language": "python", "code": code})

@mcp.tool()
def execute_js(code: str) -> str:
    """JavaScript コードをサンドボックスで実行し、実行結果を返す。

    ESM 構文のみ対応(require は使えない)。組み込みモジュールは `import fs from "node:fs"`
    のように node: プレフィックスで読み込む。外部ネットワークに出られないため
    npm パッケージの取得はできない。Python と同じサンドボックスを共有するため、
    ファイルは言語をまたいで読み書きできる。
    """
    return call_code_interpreter("executeCode", {"language": "javascript", "code": code})

@mcp.tool()
def execute_ts(code: str) -> str:
    """TypeScript コードをサンドボックスで実行し、実行結果を返す。

    型注釈付きのコードをトランスパイルなしでそのまま実行できる。
    モジュールの制約は execute_js と同じ(ESM のみ・npm パッケージの取得は不可)。
    """
    return call_code_interpreter("executeCode", {"language": "typescript", "code": code})

@mcp.tool()
def execute_command(command: str) -> str:
    """サンドボックスでシェルコマンドを実行し、実行結果を返す。

    外部ネットワークには出られない(pip install / npm install は不可)。
    """
    return call_code_interpreter("executeCommand", {"command": command})

_signer = None  # 署名専用 S3 クライアントのキャッシュ

def get_signer():
    """署名付き URL の生成にだけ使う S3 クライアントを返す。"""
    global _signer
    if _signer is None:
        if SIGNING_SECRET_ID:
            secrets = boto3.client("secretsmanager", region_name=REGION)
            creds = json.loads(secrets.get_secret_value(SecretId=SIGNING_SECRET_ID)["SecretString"])
            _signer = boto3.client(
                "s3",
                region_name=REGION,
                aws_access_key_id=creds["AccessKeyId"],
                aws_secret_access_key=creds["SecretAccessKey"],
                config=S3_CONFIG,
            )
        else:
            _signer = s3
    return _signer

def presign_upload(filename: str) -> str:
    """S3 へのアップロード用(HTTP PUT)署名付き URL を発行する。"""
    return get_signer().generate_presigned_url(
        "put_object",
        Params={"Bucket": FILE_BUCKET, "Key": build_s3_key(filename)},
        ExpiresIn=URL_EXPIRES_SECONDS,
    )

def presign_download(filename: str) -> str:
    """S3 からのダウンロード用(HTTP GET)署名付き URL を発行する。"""
    return get_signer().generate_presigned_url(
        "get_object",
        Params={"Bucket": FILE_BUCKET, "Key": build_s3_key(filename)},
        ExpiresIn=URL_EXPIRES_SECONDS,
    )

@mcp.tool()
def get_upload_url(filename: str) -> str:
    """ファイルをサンドボックスに渡すためのアップロード用 URL を発行する。

    返された URL に HTTP PUT でファイルをアップロードしたあと、
    load_file を呼ぶとサンドボックスに取り込まれる。
    """
    return presign_upload(filename)

@mcp.tool()
def load_file(filename: str) -> str:
    """アップロード済みのファイルをサンドボックスの作業ディレクトリに取り込む。"""
    url = presign_download(filename)
    call_code_interpreter("executeCommand", {"command": f'curl -sSf -o "{filename}" "{url}"'})
    return f"{filename} をサンドボックスに配置しました"

INLINE_IMAGE_FORMATS = {
    ".png": "png",
    ".jpg": "jpeg",
    ".jpeg": "jpeg",
    ".gif": "gif",
    ".webp": "webp",
}
MAX_INLINE_IMAGE_BYTES = 5 * 1024 * 1024

@mcp.tool()
def save_file(filename: str):
    """サンドボックス内のファイルを手元に取り出す。

    画像(png/jpg/gif/webp)は画像そのものを返すので、対応クライアントではチャットに
    そのまま表示される。それ以外のファイルはダウンロード用 URL を返す。
    URL は絶対に書き換えず、受け取った文字列のまま提示すること。
    """
    upload_url = presign_upload(filename)
    call_code_interpreter("executeCommand", {"command": f'curl -sSf -X PUT -T "{filename}" "{upload_url}"'})

    image_format = INLINE_IMAGE_FORMATS.get(Path(filename).suffix.lower())
    if image_format:
        body = s3.get_object(Bucket=FILE_BUCKET, Key=build_s3_key(filename))["Body"].read()
        if len(body) <= MAX_INLINE_IMAGE_BYTES:
            return Image(data=body, format=image_format)

    return presign_download(filename)

@mcp.tool()
def write_file(filename: str, content: str) -> str:
    """テキスト内容をそのままサンドボックスにファイルとして書き込む。

    S3 を経由しない(MCP の通信だけで完結する)ため、小さいテキストの受け渡しに向く。
    大きいファイルやバイナリは get_upload_url / load_file を使う。
    """
    call_code_interpreter("writeFiles", {"content": [{"path": filename, "text": content}]})
    return f"{filename} をサンドボックスに書き込みました"

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000, stateless_http=True)

受け渡し用のS3バケットを作成する

ファイル受け渡し用のS3バケットを作ります。パブリックアクセスをすべてブロックし、置いたファイルが1日で消えるライフサイクルルールを付けておきます。

aws s3api create-bucket \
  --bucket code-interpreter-mcp-files-123456789012 \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1

aws s3api put-public-access-block \
  --bucket code-interpreter-mcp-files-123456789012 \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

aws s3api put-bucket-lifecycle-configuration \
  --bucket code-interpreter-mcp-files-123456789012 \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "expire-transfer-files",
      "Status": "Enabled",
      "Filter": {},
      "Expiration": {"Days": 1},
      "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1}
    }]
  }'

署名用のIAMユーザーを作成する

get_signerが使う静的アクセスキーを用意します。このキーでできることは、作成したバケットのオブジェクトを読み書きすることだけです。発行したキーは画面に出さず、変数に入れたままSecrets Managerに渡します。

aws iam create-user --user-name code-interpreter-mcp-signer

aws iam put-user-policy --user-name code-interpreter-mcp-signer \
  --policy-name s3-presign-objects \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::code-interpreter-mcp-files-123456789012/*"
    }]
  }'

KEY_JSON=$(aws iam create-access-key --user-name code-interpreter-mcp-signer \
  --query 'AccessKey.{AccessKeyId:AccessKeyId,SecretAccessKey:SecretAccessKey}' \
  --output json)

aws secretsmanager create-secret \
  --name code-interpreter-mcp/signing-key \
  --secret-string "$KEY_JSON"

できあがったシークレットのARNは...:secret:code-interpreter-mcp/signing-key-XXXXXXのように、指定した名前の末尾に6文字が足された形になります。このサフィックスはSecrets Managerが自動で付けるので、この後のIAMポリシーではワイルドカードで書きます。

AgentCore Runtimeにデプロイする

agentcore/agentcore.jsonのruntime定義に2つ足します。envVarsでバケット名とシークレットIDを渡し、connectionsでCode InterpreterへのIAM権限を自動生成させます。

agentcore/agentcore.json(抜粋)
{
  "runtimes": [
    {
      "name": "code_interpreter_mcp",
      "build": "CodeZip",
      "entrypoint": "main.py",
      "codeLocation": "app/code_interpreter_mcp/",
      "runtimeVersion": "PYTHON_3_14",
      "networkMode": "PUBLIC",
      "protocol": "MCP",
      "envVars": [
        { "name": "FILE_BUCKET", "value": "code-interpreter-mcp-files-123456789012" },
        { "name": "SIGNING_SECRET_ID", "value": "code-interpreter-mcp/signing-key" }
      ],
      "connections": [
        { "to": { "type": "codeInterpreter" }, "description": "AWS マネージドの Code Interpreter を利用" }
      ]
    }
  ]
}

networkModeはプロジェクト生成時のままです。これはRuntimeコンテナ側のネットワーク設定で、Code InterpreterのnetworkMode(SANDBOX)とは別リソースの設定です。ここがPUBLICでもサンドボックスは閉じたままです。

S3とSecrets Managerへの権限はスキーマにフィールドがないので、生成されたCDKスタック(agentcore/cdk/lib/cdk-stack.ts)に足します。場所はconstructor内、this.application = new AgentCoreApplication(...)の直後です。

agentcore/cdk/lib/cdk-stack.ts(追記)
for (const env of this.application.environments.values()) {
  // ファイル受け渡し用 S3 バケットへのアクセス
  env.runtime.role.addToPrincipalPolicy(
    new iam.PolicyStatement({
      actions: ['s3:GetObject', 's3:PutObject'],
      resources: ['arn:aws:s3:::code-interpreter-mcp-files-123456789012/*'],
    })
  );

  // 署名専用アクセスキーの取得(末尾の -* は Secrets Manager が付けるサフィックス)
  env.runtime.role.addToPrincipalPolicy(
    new iam.PolicyStatement({
      actions: ['secretsmanager:GetSecretValue'],
      resources: [
        'arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:code-interpreter-mcp/signing-key-*',
      ],
    })
  );
}

デプロイします。

agentcore deploy --yes

数分で完了して、agentcore statusでエンドポイントURLが取れます。

Agents
  code_interpreter_mcp: Deployed - Runtime: READY (arn:aws:bedrock-agentcore:ap-northeast-1:123456789012:runtime/cimcpserver_code_interpreter_mcp-xxxxxxxxxx)
  URL: https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/arn%3Aaws%3A...%2Fcimcpserver_code_interpreter_mcp-xxxxxxxxxx/invocations

MCPクライアントからはこのURLに?qualifier=DEFAULTを付けて接続します。

動作確認

接続用のIAMユーザーを発行する

クライアントに必要な権限は、このRuntimeへのbedrock-agentcore:InvokeAgentRuntimeだけです。専用のIAMユーザーを発行します。

aws iam create-user --user-name code-interpreter-mcp-client

aws iam put-user-policy --user-name code-interpreter-mcp-client \
  --policy-name invoke-ci-mcp-runtime \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": "bedrock-agentcore:InvokeAgentRuntime",
      "Resource": [
        "arn:aws:bedrock-agentcore:ap-northeast-1:123456789012:runtime/cimcpserver_code_interpreter_mcp-xxxxxxxxxx",
        "arn:aws:bedrock-agentcore:ap-northeast-1:123456789012:runtime/cimcpserver_code_interpreter_mcp-xxxxxxxxxx/*"
      ]
    }]
  }'

aws iam create-access-key --user-name code-interpreter-mcp-client

最後のcreate-access-keyが出力するAccessKeyIdSecretAccessKeyを、次のLibreChatの設定で使います。

LibreChatから使う

複数のプロバイダを用意したLibreChatから、作成したMCPサーバーにつなぎます。なお、公式ドキュメントによると、LibreChatのMCP接続はユーザーごとに独立して確立される(User-Specific Connections)ため、全社提供しても他のユーザーとサンドボックスを共有することはないようです。

https://www.librechat.ai/docs/features/mcp

まず、発行したアクセスキーをLibreChatの.envに追記します。LibreChat本体のAWS設定と混ざらないよう、専用の変数名にしています。

.env(追記)
CI_MCP_AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
CI_MCP_AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

次に、librechat.yamlに3か所足します。

librechat.yaml
interface:
  agents:
    use: true

endpoints:
  agents:
    capabilities:
      - 'tools'

mcpServers:
  code-interpreter:
    type: stdio
    command: uvx
    args:
      - 'mcp-proxy-for-aws@latest'
      - '<エンドポイントURL>?qualifier=DEFAULT'
      - '--region'
      - 'ap-northeast-1'
    env:
      AWS_ACCESS_KEY_ID: '${CI_MCP_AWS_ACCESS_KEY_ID}'
      AWS_SECRET_ACCESS_KEY: '${CI_MCP_AWS_SECRET_ACCESS_KEY}'
      AWS_REGION: 'ap-northeast-1'
      UV_CACHE_DIR: '/tmp/uv-cache'
    initTimeout: 120000

mcpServersenv:は、子プロセスとして起動するプロキシに渡す環境変数です。
${CI_MCP_AWS_ACCESS_KEY_ID}のように書くと、さきほど.envに追記した値が展開されます。
UV_CACHE_DIRは、Docker版でuvxが既定のキャッシュ先を作れないことへの対策です。
initTimeoutを伸ばしているのは、uvxの初回パッケージ取得に19秒かかったためです。

interfaceendpointsも必要です。
とくにcapabilitiestoolsが抜けていると、UIにはツールが表示されるのにモデルには1つも渡らず、警告も出ません。

Azure OpenAI GPT-5.4で一連の流れを試す

モデルにAzure OpenAIのGPT-5.4を選び、1つのスレッドでデータ投入からpptx作成までを順に指示していきます。

まず、CSVをサンドボックスに置いて集計させます。

次の売上データをsales.csvとしてサンドボックスに保存して。month,salesの2列で、2026-01から2026-06まで順に120, 135, 158, 142, 171, 189
そのあと、pandasで合計・平均・前月比を計算して、表で見せてほしい

1つ目の指示でwrite_fileが呼ばれ、CSVがサンドボックスに書き込まれます。このサイズのテキストなら、S3を経由せずツール1つで渡せます。
2つ目はexecute_pythonで、プリインストールされたpandasが集計した表が返ってきます。

gpt-5-4-csv

次に、グラフを作らせます。

月次推移の棒グラフをsales.pngに保存して描画してほしい

execute_python(matplotlib)で描画したあとsave_fileが呼ばれ、pngなのでダウンロードURLではなく画像そのものがチャットに表示されます。

gpt-5-4-graph

続けて、資料も作らせます。

そのグラフを貼った1枚のスライドをsales.pptxにしてほしい

python-pptxもプリインストールされているので、execute_pythonだけでpptxが作れます。今度は画像ではないので、save_fileはダウンロード用の署名付きURLを返します。URLを開くとpptxが手元に落ちてきます。

gpt-5-4-pptx

作成されたパワポはこんな感じでした。

gpt-5-4-created-pptx

最後に、同じスレッドで続けて聞いてみます。

さっき作ったファイル、まだ残ってる?確認してほしい

execute_command(pwd && ls -l sales.csv sales.png sales.pptx) が呼ばれ、sales.csv、sales.png、sales.pptxが残っていることが返ってきます。同じMCPセッションのあいだは、ファイルも変数も保持されたままです。

gpt-5-4-check

Amazon Bedrock Claude Sonnet 4.6からも呼び出す

モデルをBedrockのClaude Sonnet 4.6に切り替えます。
MCP接続はユーザー単位なので、モデルを替えても見ているサンドボックスは同じで、GPT-5.4で作ったファイルもそのまま残っています。
別スレッドでここまでの成果をまとめてみましょう。

サンドボックスにあるsales.csvとsales.pngを使って、集計結果とグラフを1枚にまとめたスライドをsummary.pptxにしてほしい

こちらでもexecute_pythonsave_fileが呼ばれ、pptxのダウンロードまで通りました。

claude-thread-1
claude-thread-2

作成されたsummary.pptxの出来はあまり良くないです。(カードが画像とかぶさっています)

summary-pptx

これで「LLMプロバイダを問わず、MCPを繋いだクライアントならCode Interpreterが使える」が確認できました。

クリーンアップ

# Runtime(CloudFormation スタックごと削除)
aws cloudformation delete-stack --stack-name AgentCore-cimcpserver-default

# S3 バケット
aws s3 rm s3://code-interpreter-mcp-files-123456789012 --recursive
aws s3api delete-bucket --bucket code-interpreter-mcp-files-123456789012

# 署名専用アクセスキーのシークレット
aws secretsmanager delete-secret --secret-id code-interpreter-mcp/signing-key \
  --force-delete-without-recovery

# 接続用 IAM ユーザー
aws iam delete-user-policy --user-name code-interpreter-mcp-client --policy-name invoke-ci-mcp-runtime
aws iam list-access-keys --user-name code-interpreter-mcp-client
aws iam delete-access-key --user-name code-interpreter-mcp-client --access-key-id AKIAXXXXXXXXXXXXXXXX
aws iam delete-user --user-name code-interpreter-mcp-client

# 署名用 IAM ユーザー
aws iam delete-user-policy --user-name code-interpreter-mcp-signer --policy-name s3-presign-objects
aws iam list-access-keys --user-name code-interpreter-mcp-signer
aws iam delete-access-key --user-name code-interpreter-mcp-signer --access-key-id AKIAXXXXXXXXXXXXXXXX
aws iam delete-user --user-name code-interpreter-mcp-signer

# グローバルに入れた AgentCore CLI
pnpm remove -g @aws/agentcore

まとめ

今回は、Code Interpreterを内蔵したMCPサーバーをAgentCore Runtimeにデプロイし、LibreChatからAzure OpenAIのGPT-5.4とBedrockのClaude Sonnet 4.6で接続できることを検証しました。CSVの投入からpandasでの集計、グラフ画像のチャット表示、pptxのダウンロードまで、チャットの指示だけで一通り流せました。

Strands Agentsに組み込んでいたときはCode Interpreterは「そのエージェントの部品」でしたが、MCPサーバーにすることで、どのクライアントからでも繋げる共有インフラになりました。
エージェントごとにサンドボックス統合を作り込むより、この形で1つ置いておくほうが使い回しは効くと思います。

この記事をシェアする

関連記事