MCPで署名付きURLを使ってファイルをアップロードする
MCPサーバーを開発していて、あれ、ファイルのアップロードってどうやるんだっけって思った方は多いと思います。
現状考えられる方法はいくつかあります。それぞれの内容はこちらの記事が詳しいですが、かんたんに説明します。
Base64エンコードしてMCPサーバーにアップロードする
すべての送信データがトークンとして消費されてしまいます。しかもそれなりのサイズのデータを送るとLLMのサイズ制限に引っかかってしまうため、数キロバイトくらいまでのデータしか送ることができません。よほど限られた要件でない限り、使い物になりません。
ファイル名をMCPサーバーに渡してアップロードする
ローカルMCPサーバー限定になりますが、MCPサーバーにアップローダーを組み込んでしまえば、ファイル名をMCPサーバーに渡してアップロードできるだろうと考えるかもしれません。一部のエージェントでは動作するかもしれませんが、Claude Coworkではセキュリティ対策のため仮想的なファイルシステムを使っており、MCPサーバーに実際のパスではないファイル名を渡してしまうため、うまく動きません(チャットウインドウには仮想ディレクトリのパスがローカルのパスに変換されて表示されるので、なぜ動かないのか分かりにくいのですが、一瞬だけ変換前のパスが出てたの見逃さなかったぞ!)。
MCPの仕様がバイナリ対応されるのを待つ
いくつかissueが立ち上がって議論されているようですが、これといった決め手はなく、具体的なアクションには至っていません。まあアップロードしたいという要望は普通に多いでしょうから、そのうち何らかの対応はされるのだろうと思います。
署名付きURLを使う
現在、唯一実現可能な方法です。この記事では署名付きURLで実装するための現実的なアプローチについて説明します。
署名付きURLとは?
説明するまでもないかもしれませんが、S3やCloudflareなどのオンラインストレージには、安全にファイルをアップロード、ダウンロードするための署名付きURLというしくみがあります。MCPサーバー側で、認証したユーザーに対してアップロード可能なURLを生成し、AIエージェントにCurlやfetchなどを使って直接アップロードしてもらいます。今回はS3にアップロードする想定で説明します。

署名付きURL方式の注意点
S3で普通に署名付きURLを発行すると、https://my-bucket.s3.ap-northeast-1.amazonaws.com/image.jpg?X-Amz-Credential=xxx みたいなURLになると思います。Claude Coworkでは、外部にデータを送信できるサイトが限定されているため、ホワイトリストにドメインを追加する必要があります。

Claudeの設定画面で追加するわけですが、自社で使うシステムならともかく、ここにmy-bucket.s3.ap-northeast-1.amazonaws.comみたいなドメインを追加してもらうのって、AWSのリージョンやS3バケット名が丸わかりで嫌じゃないですか。かといって *.amazonaws.com で追加してもらうと、AWSのどんなサーバーにも送信できることになり、セキュリティ上問題があります。
なのでできれば、CloudFrontの署名付きURLを使うのをおすすめします。CloudFront経由であれば、所有するドメインが使えるので、安全に許可リストに加えてもらうことができます。CloudFrontで署名付きURLを使う方法についてはいろいろ情報があると思いますので、調べるかAIに聞いてください。
MCPサーバーを実装する
こんな感じで、Toolのdescriptionにて、AIにファイルをアップロードするための手順について指示を行います。
tools['upload-file'] = {
description: `Get a presigned URL to upload an attachment file.
This tool does NOT upload the file itself. It returns an \`upload_url\` that you must use to upload the file contents via HTTP PUT (e.g., \`curl -X PUT --upload-file <path> "<upload_url>"\` or \`fetch(upload_url, { method: 'PUT', body: <content> })\`). Set the \`Content-Type\` header to match the \`content_type\` if provided.
After uploading, pass the returned \`uid\` to appropriate tool.
# Example:
...
`,
inputSchema: {
type: 'object',
properties: {
filename: {
type: 'string',
description: 'Name of the file to upload (e.g., "document.pdf")'
},
content_type: {
type: 'string',
description:
'Content type of the file (e.g., "application/pdf"). Optional.'
}
},
required: ['filename']
},
annotations: {
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
readOnlyHint: false,
title: 'Upload Run State File'
},
handler: async ({
filename,
content_type
}: {
filename: string;
content_type?: string;
}) => {
// 署名付きURLを取得
const response = await fetch('http://<FETCH_URL>', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename, content_type })
});
return {
content: [
{
type: 'text',
text: JSON.stringify(response, null, 2)
}
]
};
}
};署名付きURLを生成するたびに、それに紐づいたContentTypeやファイル名などの一時データをデータベースに登録するしくみにしています。確実にアップロードされるわけではないので、一時ファイルと一時レコードは一定期間後に削除します。このしくみはS3のライフサイクルルールや、DynamoDBのTTLなどを使うとよいでしょう。
実行例
こんな感じで、Claude Coworkからアップロードすることができました。
