1. はじめに
NTT西日本の鈴木*1です。
生成AIの活用が進む中、多くのエンジニアが次に注目しているのが「AIエージェント」です。
この記事では、機密性の高い会議情報を外部に出さず、オンプレミス環境内で完結して動作する「会議メモ要約&フォルダ格納AIエージェント」をMCP(Model Context Protocol)を用いて試作しましたので、その背景と技術的な仕組みについてご紹介します。
目次
- 1. はじめに
- 2. 対象読者
- 3. 背景:なぜ今、オンプレミスのAIエージェントなのか
- 4. 開発したエージェントの概要
- 5. 環境構築
- 6. テスト
- 7. まとめ
- 参考資料
- 執筆者
- 商標
- 免責事項
2. 対象読者
本記事が想定する対象読者は以下の通りです。
- オンプレミスの生成AIに興味がある方
- MCPの実装に興味がある方
- Difyを用いたAIエージェント開発に興味がある方
- 生成AIによるDXに興味がある方
3. 背景:なぜ今、オンプレミスのAIエージェントなのか
3.1 2025年:「対話」から「自律動作」へ
2023年から2024年にかけては、ChatGPTをはじめとする大規模言語モデル(LLM)に対し、人間が質問を投げかける「チャットボット」や、RAG(Retrieval-Augmented Generation)を用いた「社内ナレッジ検索」が市場に広く浸透しました。
そして、2025年は「AIエージェント元年」と呼ばれており、従来の生成AIが「聞かれたことに答える受動的な存在」だったのに対し、AIエージェントは「与えられたゴールに向かって、LLMが状況に応じてツールを使いながら自律的に動き、タスクを完遂する能動的な存在」へと進化しています。
この変化を支えているのが、以下の技術的成熟です。
推論能力の向上:
長文処理や論理推論などの面でLLMの能力が向上し、複雑な手順を分解して実行計画を立てる能力が飛躍的に向上したこと。ツール呼び出し方法の標準化:
生成AIが社内システムやAPI、ファイルサーバー等を操作するためのインターフェース(MCP)の環境が整備され始めたこと。
これにより、LLMが単なる「相談相手」ではなく、「手を動かす部下」として活用できることが現実的になってきました。
3.2 エージェント活用のカギとなる標準規格「MCP」
AIのエージェント化の流れを技術的に決定づけたのが、MCP (Model Context Protocol) の登場と普及です。MCPは、2024年11月にAnthropic社によって公開されたプロトコルで、大規模モデル(LLM)と外部システムやデータソースを接続するためのルールが設計されています。
公開からわずか1年足らずで、AI開発の現場において事実上の標準規格(デファクトスタンダード)となっています。その爆発的な普及の背景には、明確な理由があります。
従来の課題:「M×Nの接続問題」
MCPが登場する以前のAI開発者は、「M×Nの接続問題」に苦しんでいました。
N個のAIサービスとM個のデータソースを繋ぐために、それぞれの組み合わせで独自のAPI連携コードを書く必要があったのです。これがAI開発のコストを肥大化させ、開発したツールの再利用を妨げていました。MCPはこの問題の解決策として登場し、一気に普及しました。解決策:「USB-C」のような共通規格化
MCPはこの壁を取り払う、まさに「AI時代のUSB-C」として登場しました。
一度MCPサーバーとしてツールを作ることで、DifyやClaude Desktopなど、あらゆるMCP対応のクライアントから即座に利用することが可能です。結果:エコシステムの急拡大
この汎用性により、公開直後からGitHub等で多種多様な「MCPサーバー」が有志によって開発・公開されました。さらに、MCPサーバーをサービスとして提供する企業の登場も後押しとなり、エコシステムは爆発的に拡大しています。
以前ならAPI仕様書と格闘しながら自作していた連携機能が、MCPを使うことで、即座に自身の環境へ組み込めるようになったのです。
3.3 オンプレミスへの回帰と「社内システム連携」の必然性
ビジネスにおいてエージェントが実務を行うためには、社内システムとの連携が必要です。しかし、会議の議事録や顧客データなどの情報は機密性が高く、パブリッククラウド上のエージェントやサービスに接続することは大きなセキュリティ上の懸念となり得ます。
オンプレミスでも動作する小規模言語モデル(SLM)が実用的に動作するようになった今、「セキュアな環境で、社内システムを直接操作できるオンプレミス型エージェント」が企業のDXのカギとなります。
4. 開発したエージェントの概要
オンプレミス環境内で完結して動作するAIエージェントをMCPを用いて試作してみました。
具体的には、「会議メモを与えるだけで、要約したファイルを作成し、ファイルサーバー上の適切な案件フォルダに格納するエージェント」を作成しました。

処理フロー
作成したエージェントの処理フローは大別すると以下の通りです。
①テキスト入力:
ユーザーが会議の文字起こしテキストと会議開催日、案件名を入力。
②テキスト要約:
LLMが文字起こし内容を解析し、事前設定済みフォーマット(案件名や日時、議題、決定事項、宿題など)に要約。
③フォルダ格納:
案件名と打ち合わせ日時から格納先のファイルパスを決定し、決定された格納先に要約文をファイルとして保存。その後ユーザーに完了を通知。案件フォルダがない場合は作成するなど、状況に応じて行動。
4.1 開発環境
本エージェントは、以下の環境で開発しています。
| ホストサーバー | 仕様 |
|---|---|
| OS | Ubuntu 22.04.5 LTS |
| GPU | H100 NVL (94GB) |
| GPUドライバー | 580.95.05 |
| CUDA | 13.0 |
| Docker | 28.0.1 |
| Docker Compose | v2.33.1 |
| オーケストレータ | Dify v1.9.2(セルフホスト版) |
| SLM/LLM | gpt-oss-120b など |
| モデルサービングエンジン | vLLM |
| MCP環境 | Node.js v20-alpine |
| クライアントPC | 仕様 |
|---|---|
| OS | Windows 11 |
| ブラウザ | Firefox |
4.2 設計

本エージェントでは、ホストサーバー上にインストールされたDocker環境内に構成し、実行に必要なすべての機能をオンプレミス環境に閉じて利用できるようにします。
構成のために、大きく3つのコンテナをホストサーバー内に構築します。
①Dify(オーケストレータ)
②vLLM(推論エンジン)
③MCPサーバー(ツール実行)
また、ホストサーバのファイルシステムと連携させることでファイル操作可能なエージェントを構成しました。
各コンテナの役割と処理フローは以下の通りです。
システム構成要素
Dify コンテナ (連携):
- ユーザーとの対話インターフェースおよびエージェント利用に必要な要素のオーケストレーションを担う。
- vLLMとはOpenAI互換APIで通信し、プロンプトの組み立てやコンテキスト管理を行う。
- MCPサーバーとはSSE(Server-Sent Events)で常時接続し、ツール定義の読み込みや実行要求を行う。
vLLM コンテナ (頭脳):
- LLMのモデル(gpt-oss-120b)をGPU上で稼働させる。
- Difyから送られる「プロンプト」と「ツール定義」に基づき、次に実行すべきアクション(Tool Call)を推論する。
MCPサーバー コンテナ (手足):
- Node.js上で動作するMCPサーバーの実行環境。
- LLMの推論結果に基づいて、Dockerのボリュームマウント機能を介してホストOSのファイルシステムにアクセスし、ファイルの読み書きやディレクトリ操作を実際に実行する。
5. 環境構築
本記事では、以下の状況を前提として想定して執筆しています。
- Linuxサーバー上にGPUドライバーやCUDAが既にインストールされていること
- Linuxサーバー上に既にDocker EngineやDocker Composeがインストールされコンテナ上でGPUを認識できること
- サーバーとクライアントPC間でネットワーク通信ができること
5.1 モデルデータのダウンロード
AIエージェント用に使うLLMをHugging Faceからダウンロードします。
ここでは、openai/gpt-oss-120bを作業用ディレクトリ内にダウンロードします。
別途任意のモデルをダウンロードして、ご利用いただいてもかまいません。
※別モデルを利用する際は、vLLMの起動オプションや設定値に注意してツール呼び出しができるようにしてください。
5.1項完了時のディレクトリ・ファイル構成例
ここでは、workspaces/ai-agent/models/以下を構成する。
workspaces/ai-agent
└── models/
└── gpt-oss-120b/ # ダウンロード済みモデル(vLLMがマウント)
├── config.json
├── model-00000-of-... #モデルの重み
├── chat_template.jinja #チャットテンプレート
└── tokenizer.json など
手順:Hugging Faceからモデルデータをダウンロードして保存する。
cd workspaces/ai-agent mkdir -p ./models/gpt-oss-120b #Hugging Faceからgpt-oss-120bをダウンロードする。 hf download openai/gpt-oss-120b --local-dir ./models/gpt-oss-120b
5.2 推論用APIサーバー(vLLM)の構築
ダウンロードしたモデルをサービングするvLLM環境を構築します。 ここでは、ホストサーバーの環境に影響を与えないように、Docker Composeを利用してDockerコンテナとしてvLLM環境を構成し、ダウンロード済みのモデルをデプロイします。
5.2項完了時のディレクトリ・ファイル構成例
ここでは、workspaces/ai-agent/vllm_gpt-oss-120b/ 以下を構成する。
workspaces/ai-agent ├── models/ │ └── gpt-oss-120b/ │ ├── config.json │ ├── model-00000-of-... │ ├── chat_template.jinja │ └── tokenizer.json など └── vllm_gpt-oss-120b/ └── docker-compose.yml # vLLM起動用のcomposeファイル
手順1:Docker Compose用ファイル(docker-compose.yml)の作成
以下を実行し、viエディタでdocker-compose.ymlを作成する。
cd workspaces/ai-agent mkdir ./vllm_gpt-oss-120b cd workspaces/ai-agent/vllm_gpt-oss-120b vi docker-compose.yml
次に、viエディタ内で以下のスクリプトをdocker-compose.yml内に記入して保存する。
services: vllm-gptoss120b: # Tool Calling等の最新機能やバグ修正を取り込むため nightly イメージを採用 image: vllm/vllm-openai:nightly container_name: vllm-server-gpt-oss-120b shm_size: '256g' deploy: resources: reservations: devices: - driver: nvidia capabilities: [gpu] # 複数基のGPUのうち特定の1基(Index 0)を割当て(ご自身の環境に合わせてください。) device_ids: ['0'] ports: - "8001:8001" volumes: # ホスト側のモデルディレクトリをコンテナへマウントして永続化 - workspaces/ai-agent/models/gpt-oss-120b/:/models command: # --- 基本設定 --- - --model - "/models/gpt-oss-120b/" - --served-model-name gpt-oss-120b # --- パフォーマンス・安定性設定 --- - --dtype - "auto" - --kv-cache-dtype - "auto" # H100環境用の設定 - --compilation-config - '{"cudagraph_mode":"PIECEWISE"}' # 推論スループット向上のための非同期スケジューリング有効化 - --async-scheduling # --- エージェント機能(Tool Calling)設定 --- # Dify等からOpenAI互換形式でツールを呼び出せるようにパーサーを指定 - --tool-call-parser - "openai" # GPT-OSSモデル特有の推論/思考プロセスを処理するための専用パーサー - --reasoning-parser - "openai_gptoss" # プロンプトテンプレートに基づき、利用可能なツールを自動選択する機能を有効化 - --enable-auto-tool-choice # --- メモリ・コンテキスト設定 --- # GPUメモリを最大限活用(95%) - --gpu-memory-utilization - "0.95" # メモリ不足回避のため、同時並列リクエスト数を8並列に限定する - --max-num-seqs - "8" # コンテキスト長を128kに設定 - --max-model-len - "131072" - --host - "0.0.0.0" - --port - "8001"
手順2:vLLM用コンテナのビルド~起動確認
Docker Composeを実行し、vLLMのサーバーをコンテナとして立ち上げる。
#コンテナ起動 docker-compose.yml #ログ確認(モデルロードの進捗が見れます) docker compose logs -f
以下のようなログが吐き出されたら正常にvLLMが起動しています。
INFO: Started server process [1] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
5.3 Difyサーバーの構築(セルフホスト版)
GitHub上にOSSとして公開されているDifyのセルフホスト版をDockerコンテナとして構築します。
5.3完了時のディレクトリ・ファイル構成例
ここでは、workspaces/ai-agent/dify/ 以下を構成する。
workspaces/ai-agent
├── models/
│ └── gpt-oss-120b/
│ ├── config.json
│ ├── model-00000-of-...
│ ├── chat_template.jinja
│ └── tokenizer.json など
├── vllm_gpt-oss-120b/
│ └── docker-compose.yml
└── dify/ # Difyセルフホスト版
├── docker/ # Docker関連設定
├── web/ # Webフロントエンド
├── api/ # サーバーAPI
├── .env # 環境変数
└── README.mdなど
手順 1: リポジトリのクローン
cd workspaces/ai-agent #Difyのリポジトリをクローン git clone https://github.com/langgenius/dify.git #Dockerディレクトリへ移動 cd dify/docker
手順 2: Dify環境変数の設定
ここでは割愛しますが、Difyにアップロードできるファイル数やサイズの上限値、ワークフローのタイムアウト設定、アクセス時のポート番号などを環境変数として設定・変更可能です。その際は.envファイルを編集することで調整可能です。
cp .env.example .env #vi .env
手順 3: Docker Compose で起動
Difyに必要なミドルウェア群(PostgreSQL, Redis, Weaviate, Nginx等)を一括で立ち上げます。
docker compose up -d
起動確認:
docker compose ps
全てのコンテナ(docker-api-1, docker-web-1, docker-worker-1 など)が Up 状態になっていれば成功です。
手順 4: Difyの初期セットアップ
ブラウザを開き、Difyのインストール画面にアクセスします。
- URL: http://<サーバーのIPアドレス>/install 管理者アカウントの作成画面が表示されるので、メールアドレスとパスワードを設定してログインしてください。
手順 5: オンプレミス vLLM との接続設定
ここが「vLLMでデプロイしたgpt-oss-120bモデル」をDifyから使うための設定です。
- Dify画面右上のアイコン → [設定] → [モデルプロバイダー] をクリックする。
- [Difyマーケットプレイス]から[vllm]のカードを探してインストールする。
- インストールされた[vllm] のカードを探して[モデルを追加]をクリックする。
- 以下の通り設定する。
| 項目 | 値 |
|---|---|
| Model Name | gpt-oss-120b |
| Model Type | LLM |
| API endpoint URL | http://(サーバーのIPアドレス):8001/v1 |
| Completion mode | Chat |
| Model context size | 131072 |
| Upper bound for max tokens | 131072 |
| Agent Thought | Not Support |
| Function calling | Tool Call |
| Stream function calling | Not Support |
5.4 MCPサーバーの構築(FileSystem MCP)
MCPではいくつかのトランスポート方式が使われおり、代表的なものではSTDIO、SSE、Streamable HTTPがあります。
今回利用したいファイルシステム用のMCPサーバーとしては、GitHubに公開されている公式のFileSystem MCP Server(@modelcontextprotocol/server-filesystem)がありますが、こちらではSTDIO方式が採用されています。一方で、DifyがMCPクライアントとして利用可能な方式はSSEおよびStreamable HTTP方式です。
両者でサポートされているトランスポート方式が異なるため、そのままでは連携ができません。そこで、今回はMCP公式SDK(@modelcontextprotocol/sdk)を使用して、ファイル操作するためのMCPサーバーをSSE対応として自作します。
5.4完了時のディレクトリ・ファイル構成例
ここでは、workspaces/ai-agent/mcp-server/ 以下を構成する。
workspaces/ai-agent
├── models/
│ └── gpt-oss-120b/
│ ├── config.json
│ ├── model-00000-of-...
│ ├── chat_template.jinja
│ └── tokenizer.json など
├── vllm_gpt-oss-120b/
│ └── docker-compose.yml
├── dify/
│ ├── docker/
│ ├── web/
│ ├── api/
│ └── README.mdなど
└── mcp-server/
├── Dockerfile # MCPサーバー(Node.js)の環境定義
├── docker-compose.yml # 起動構成
├── index.js # SSE対応ファイル操作MCPスクリプト
└── package.json # 依存関係定義
手順 1: 作業ディレクトリとファイルの準備
まずディレクトリを作成します。
mkdir -p workspaces/ai-agent/mcp-server cd workspaces/ai-agent/mcp-server
viエディタで以下の4つのファイルをそれぞれ作成します。
vi ファイル名
手順2:package.json の作成
MCPサーバーの動作に必要なライブラリを定義します。
viエディタを用いてpackage.jsonに以下の内容を記述して保存する。
package.jsonのスクリプト
{ "name": "mcp-filesystem-sse", "version": "1.0.0", "main": "index.js", "type": "module", "scripts": { "start": "node index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.0.1", "express": "^4.21.1", "cors": "^2.8.5", "uuid": "^10.0.0" } }
手順3:index.js の作成
DifyからのHTTPリクエストを受け取り、MCPサーバーとして処理結果をレスポンスします。
viエディタを用いてindex.jsに以下の内容を記述して保存する。
index.jsのスクリプトはこちら
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
import cors from "cors";
import { v4 as uuidv4 } from "uuid";
// 操作対象のルートディレクトリ
const TARGET_DIR = "/data";
const PORT = 3000;
// ヘルパー関数: パスが安全か確認(ディレクトリトラバーサル防止)
function getSafePath(inputPath) {
const safePath = path.join(TARGET_DIR, inputPath);
if (!safePath.startsWith(TARGET_DIR)) {
throw new Error("Access denied: Path is outside the target directory");
}
return safePath;
}
// ヘルパー関数: 再帰的ファイル検索
async function searchFilesRecursively(dir, pattern) {
let results = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results = results.concat(await searchFilesRecursively(fullPath, pattern));
} else {
try {
const content = await fs.readFile(fullPath, "utf-8");
if (content.includes(pattern)) {
// TARGET_DIR からの相対パスを返す
results.push(path.relative(TARGET_DIR, fullPath));
}
} catch (err) {
// バイナリファイルなどで読めない場合はスキップ
}
}
}
return results;
}
// ツール登録関数
function registerTools(server) {
// 1. 一覧取得 (ls)
server.tool("list_directory", "List files and directories",
{ path: z.string().optional().describe("Subdirectory path (default is root)") },
async ({ path: subPath = "" }) => {
try {
const safePath = getSafePath(subPath);
const files = await fs.readdir(safePath);
return { content: [{ type: "text", text: files.join("\n") }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 2. 読み込み (cat)
server.tool("read_file", "Read file content",
{ path: z.string() },
async ({ path: filePath }) => {
try {
const safePath = getSafePath(filePath);
const content = await fs.readFile(safePath, "utf-8");
return { content: [{ type: "text", text: content }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 3. 書き込み (write)
server.tool("write_file", "Create or overwrite a file",
{ path: z.string(), content: z.string() },
async ({ path: filePath, content }) => {
try {
const safePath = getSafePath(filePath);
await fs.writeFile(safePath, content, "utf-8");
return { content: [{ type: "text", text: `Successfully wrote to ${filePath}` }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 4. フォルダ作成 (mkdir -p)
server.tool("create_directory", "Create a new directory",
{ path: z.string() },
async ({ path: dirPath }) => {
try {
const safePath = getSafePath(dirPath);
await fs.mkdir(safePath, { recursive: true });
return { content: [{ type: "text", text: `Created directory ${dirPath}` }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 5. 移動・リネーム (mv)
server.tool("move_file", "Move or rename a file/directory",
{ source: z.string(), destination: z.string() },
async ({ source, destination }) => {
try {
const safeSource = getSafePath(source);
const safeDest = getSafePath(destination);
await fs.mkdir(path.dirname(safeDest), { recursive: true });
await fs.rename(safeSource, safeDest);
return { content: [{ type: "text", text: `Moved ${source} to ${destination}` }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 6. 削除 (rm)
server.tool("delete_file", "Delete a file or directory",
{ path: z.string() },
async ({ path: targetPath }) => {
try {
const safePath = getSafePath(targetPath);
await fs.rm(safePath, { recursive: true, force: true });
return { content: [{ type: "text", text: `Deleted ${targetPath}` }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 7. ファイル情報取得 (stat)
server.tool("get_file_info", "Get file metadata (size, created time, etc)",
{ path: z.string() },
async ({ path: targetPath }) => {
try {
const safePath = getSafePath(targetPath);
const stats = await fs.stat(safePath);
const info = {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile()
};
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
// 8. 検索 (grep)
server.tool("search_files", "Search for a string pattern in files recursively",
{ pattern: z.string() },
async ({ pattern }) => {
try {
// ルートから再帰的に検索
const results = await searchFilesRecursively(TARGET_DIR, pattern);
if (results.length === 0) {
return { content: [{ type: "text", text: "No matches found." }] };
}
return { content: [{ type: "text", text: `Found matches in:\n${results.join("\n")}` }] };
} catch (err) {
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
}
});
}
// --- Server Setup ---
const app = express();
app.use(cors());
const transports = new Map();
app.get("/sse", async (req, res) => {
console.log("-> New MCP connection");
res.setHeader("X-Accel-Buffering", "no");
const sessionId = uuidv4();
const server = new McpServer({ name: "filesystem-full", version: "1.0.0" });
registerTools(server);
const transport = new SSEServerTransport(`/messages/${sessionId}`, res);
transports.set(sessionId, transport);
transport.onclose = () => {
console.log(`<- Closed: ${sessionId}`);
transports.delete(sessionId);
};
await server.connect(transport);
});
app.post("/messages/:sessionId", async (req, res) => {
const transport = transports.get(req.params.sessionId);
if (!transport) return res.status(404).send("Session not found");
await transport.handlePostMessage(req, res);
});
app.listen(PORT, () => console.log(`Full Filesystem MCP Server running on ${PORT}`));
手順4:Dockerfile の作成
GitHubからソースコードを取得し、コンテナをビルドして起動するまでの手順をまとめます。
viエディタを用いてDockerfileに以下の内容を記述して保存する。 Dockerfileのスクリプト
FROM node:20-alpine WORKDIR /app COPY package.json ./ RUN npm install COPY index.js ./ #MCPサーバーがリッスンするポート EXPOSE 3000 CMD ["npm", "start"]
手順5:docker-compose.yml(MCPサーバー用))の作成
コンテナを立ち上げる設定です。
今回は、ホスト側のworkspaces/ai-agent/filesディレクトリをコンテナにマウントし、コンテナ内からファイルを操作できるようにします。
viエディタを用いてdocker-compose.ymlに以下の内容を記述して保存する。
docker-compose.ymlのスクリプト
services: mcp-fs: build: . container_name: mcp-filesystem-sse restart: always ports: - "3000:3000" # ホストからもテストできるように公開(任意) volumes: # ホスト側の操作したいディレクトリをコンテナ内の /data にマウント - workspaces/ai-agent/files:/data
手順6:MCP用コンテナのビルド~起動確認
Docker Composeを用いてコンテナを起動します。
#マウント用のディレクトリを作っておく mkdir -p workspaces/ai-agent/files #ビルドして起動 docker compose up -d --build
#起動ログを確認 docker compose logs -f
ログを確認し、Full Filesystem MCP Server running on 3000 と表示されれば成功です。 これで、http://<ホストIP>:3000/sse がMCPのエンドポイントとして機能します。
手順7:Dify側での設定
- DifyのGUIを開き、上部の[ツール] タブへ移動。
- [MCP]タブを選択し、MCPサーバーの接続設定[MCPサーバー(HTTP)を追加]を探す。
- 以下を入力する。
| 項目 | 値 |
|---|---|
| サーバーURL | http://(サーバーのIPアドレス):3000/sse |
| サーバー識別子 | MCP-Filesystem(任意) |
| タイムアウト | 300(任意) |
| SSE読み取りタイムアウト | 300(任意) |
これで、Difyから「ファイルを読み込む」「書き込む」「リストする」といった機能をMCPサーバーから読み込んで利用できるようになります。また、ここで作ったMCPサーバーは別のクライアントからも再利用できるエコシステムとなっています。
5.5 エージェントワークフローの作成

ここまででAIエージェントの動作環境が整ったので、DifyのGUIを使ってエージェントを組み立てていきます。 プログラミングは不要で、自然言語で指示を書くだけでMCPツールが連動します。
Dify上でのエージェントアプリの新規作成
- 画面上部の[スタジオ]タブを選択し、アプリを作成するの下部にある[最初から作成] をクリックする。
- アプリタイプから[初心者向けの基本的なアプリタイプ]を選択し、エージェントを選択する。
- 任意のアプリアイコンや名前を設定し、[作成する]を選択する。
- 「システムプロンプト」や「変数」、「ツールを選択」し、アプリを作成する。
今回は簡略化のために作成したエージェントアプリをDSLとして公開しています。 必要に応じてymlファイルとして保存し、Difyにインポートしてください。
エージェントアプリ(DSL)
app: description: '' icon: 🤖 icon_background: '#FFEAD5' mode: agent-chat name: 議事録要約&格納エージェント use_icon_as_answer_icon: false dependencies: - current_identifier: null type: marketplace value: marketplace_plugin_unique_identifier: yangyaofei/vllm:0.1.5@3eecf807d9767f40eb757ff70720291aa6055e9e1803893b4c93b61a5d4d4319 version: null kind: app model_config: agent_mode: enabled: true max_iteration: 10 prompt: null strategy: function_call tools: - enabled: true isDeleted: false notAuthor: false provider_id: 019b10d5-fbc3-7c1b-b580-b6858fe49f96 provider_name: テキスト要約フロー provider_type: workflow tool_label: テキスト要約フロー tool_name: Summarize_in_minutes_format tool_parameters: input: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: list_directory tool_name: list_directory tool_parameters: path: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: read_file tool_name: read_file tool_parameters: path: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: write_file tool_name: write_file tool_parameters: content: '' path: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: create_directory tool_name: create_directory tool_parameters: path: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: move_file tool_name: move_file tool_parameters: destination: '' source: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: delete_file tool_name: delete_file tool_parameters: path: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: get_file_info tool_name: get_file_info tool_parameters: path: '' - enabled: true isDeleted: false notAuthor: false provider_id: mcp-filesystem provider_name: mcp-filesystem provider_type: mcp tool_label: search_files tool_name: search_files tool_parameters: pattern: '' - enabled: true isDeleted: false notAuthor: false provider_id: time provider_name: time provider_type: builtin tool_label: Current Time tool_name: current_time tool_parameters: format: '' timezone: '' annotation_reply: enabled: false chat_prompt_config: {} completion_prompt_config: {} dataset_configs: datasets: datasets: [] retrieval_model: multiple top_k: 4 dataset_query_variable: '' external_data_tools: [] file_upload: allowed_file_extensions: - .JPG - .JPEG - .PNG - .GIF - .WEBP - .SVG - .MP4 - .MOV - .MPEG - .WEBM allowed_file_types: [] allowed_file_upload_methods: - remote_url - local_file enabled: false image: detail: high enabled: false number_limits: 3 transfer_methods: - remote_url - local_file number_limits: 3 model: completion_params: stop: [] mode: chat name: /models/gpt-oss-120b/ provider: yangyaofei/vllm/vllm more_like_this: enabled: false opening_statement: "こんにちは!音声認識の文字起こしファイルをもとに、ご指定の要約方法で内容を要約し、案件フォルダに保存するお手伝いをします。\ \ \nまず、要約するテキストと要約方法、案件名、打合せ日を教えてください。 \nファイルが既に存在する場合は、上書きしてもよろしいか確認いたしますので、安心してお任せください。" pre_prompt: '```xml <instruction> 1. 指定された音声認識の文字起こしファイル({{TRANSCRIPT_FILE}})を読み込み、内容を分析してください。 2. ユーザが指定した要約方法に基づいて、文字起こしファイルの内容を要約してください。要約は簡潔で重要な情報のみを含むようにしてください。 3. 案件名({{PROJECT_NAME}})と打合せ日({{MEETING_DATE}})を用いて、保存するファイル名を「{{PROJECT_NAME}}_{{MEETING_DATE}}.txt」として構築してください。 4. 現在のディレクトリ(./)下に、{{PROJECT_NAME}}という名前のフォルダが存在するか確認してください。存在しない場合は、新規に作成してください。 5. 指定されたファイル名({{PROJECT_NAME}}_{{MEETING_DATE}}.txt)が既に存在する場合のみ、ユーザに「このファイルを上書きしてもよろしいですか?」と確認してください。ユーザが「はい」と応答した場合のみ上書きし、「いいえ」と応答した場合は処理を中止してください。 6. 要約したテキストを、作成したまたは既存の./{{PROJECT_NAME}}/ディレクトリ内に、{{PROJECT_NAME}}_{{MEETING_DATE}}.txt として保存してください。 7. 出力には、XMLタグやその他の記号を一切含まないで、純粋なテキストのみを出力してください。 </instruction> <input> {{TRANSCRIPT_FILE}}: 音声認識による文字起こしテキストの内容 {{PROJECT_NAME}}: 案件の名前(例:プロジェクトA) {{MEETING_DATE}}: 打合せの日付(例:20240615) </input> <output> 要約されたテキスト内容(純粋なテキスト、XMLタグを含まない) </output> <example> {{TRANSCRIPT_FILE}}: "今日はプロジェクトAの進捗報告を行いました。開発チームは仕様の修正を完了し、来週からテスト段階に入ります。クライアントはUIの変更を希望しています。" {{PROJECT_NAME}}: プロジェクトA {{MEETING_DATE}}: 20240615 出力: 仕様修正完了。来週からテスト開始。クライアントはUI変更を希望。 </example> <input> TRANSCRIPT_FILE:{{TRANSCRIPT_FILE}} PROJECT_NAME: {{PROJECT_NAME}} MEETING_DATE: {{MEETING_DATE}} </input> ```' prompt_type: simple retriever_resource: enabled: true sensitive_word_avoidance: configs: [] enabled: false type: '' speech_to_text: enabled: false suggested_questions: [] suggested_questions_after_answer: enabled: false text_to_speech: enabled: false language: '' voice: '' user_input_form: - paragraph: default: '' label: TRANSCRIPT_FILE max_length: 50000 required: true variable: TRANSCRIPT_FILE - text-input: default: '' label: PROJECT_NAME required: true variable: PROJECT_NAME - text-input: default: '' label: MEETING_DATE required: true variable: MEETING_DATE version: 0.4.0
テキスト要約ワークフロー(DSL) ※エージェント内でツールとして利用
app: description: ツール連携用 icon: 🤖 icon_background: '#FFEAD5' mode: workflow name: テキスト要約フロー use_icon_as_answer_icon: false dependencies: - current_identifier: null type: marketplace value: marketplace_plugin_unique_identifier: yangyaofei/vllm:0.1.5@3eecf807d9767f40eb757ff70720291aa6055e9e1803893b4c93b61a5d4d4319 version: null kind: app version: 0.4.0 workflow: conversation_variables: [] environment_variables: [] features: file_upload: allowed_file_extensions: - .JPG - .JPEG - .PNG - .GIF - .WEBP - .SVG allowed_file_types: - image allowed_file_upload_methods: - local_file - remote_url enabled: false fileUploadConfig: audio_file_size_limit: 50 batch_count_limit: 10 file_size_limit: 100 image_file_size_limit: 10 video_file_size_limit: 100 workflow_file_upload_limit: 10 image: enabled: false number_limits: 3 transfer_methods: - local_file - remote_url number_limits: 3 opening_statement: '' retriever_resource: enabled: true sensitive_word_avoidance: enabled: false speech_to_text: enabled: false suggested_questions: [] suggested_questions_after_answer: enabled: false text_to_speech: enabled: false language: '' voice: '' graph: edges: - data: isInIteration: false isInLoop: false sourceType: start targetType: llm id: 1765513569997-source-1765513576891-target source: '1765513569997' sourceHandle: source target: '1765513576891' targetHandle: target type: custom zIndex: 0 - data: isInIteration: false isInLoop: false sourceType: llm targetType: end id: 1765513576891-source-1765513883340-target source: '1765513576891' sourceHandle: source target: '1765513883340' targetHandle: target type: custom zIndex: 0 nodes: - data: selected: false title: 開始 type: start variables: - default: '' hint: '' label: input max_length: 40000 options: [] placeholder: '' required: true type: paragraph variable: input height: 88 id: '1765513569997' position: x: 80 y: 282 positionAbsolute: x: 80 y: 282 selected: false sourcePosition: right targetPosition: left type: custom width: 242 - data: context: enabled: false variable_selector: [] model: completion_params: temperature: 0.7 mode: chat name: /root/.cache/huggingface/Qwen3-Next-80B-A3B-Instruct-FP8 provider: yangyaofei/vllm/vllm prompt_config: jinja2_variables: [] prompt_template: - edition_type: basic id: c0648a1f-4579-41fc-82e4-75a1c0861e75 role: system text: '```xml <instruction> あなたはユーザーから与えられたテキストを議事録フォーマットで要約するAIです。以下のステップに従ってタスクを完了してください。 1. 入力として与えられる「<input>」を注意深く読み込み、内容を完全に理解してください。 2. 「<output_format>」セクションで定義されている議事録フォーマットの各項目(会議名、開催日時、開催場所、出席者、議題、決定事項、特記事項/要点、次回アクション/宿題、次回開催日時)に該当する情報を「{{ INPUT_TEXT }}」から抽出してください。 3. 議事録フォーマットの各項目について、対応する情報が「<input>」内に明確に存在しない場合は、その項目に「不明」と記載してください。「特記事項/要点」については、特記事項や要点がテキスト内に見当たらない場合は「なし」と記載してください。 4. 「<output_format>」セクションで指定されたフォーマットを厳守し、抽出した情報または「不明」/「なし」を適切に埋め込んで最終的な議事録を作成してください。 5. 生成される出力には、このプロンプトテンプレートに含まれるいかなるXMLタグ(例: <instruction>, <example>, <input>, <output_format>など)も一切含めないでください。純粋に議事録フォーマットのテキストのみを出力してください。 </instruction> <input> {{#1765513569997.input#}} </input> <output_format> 会議名: [会議名、不明な場合は「不明」] 開催日時: [開催日時、不明な場合は「不明」] 開催場所: [開催場所、不明な場合は「不明」] 出席者: [出席者、不明な場合は「不明」] 議題: [議論された議題の要点、不明な場合は「不明」] 決定事項: [会議で決定された事項、不明な場合は「不明」] 特記事項/要点: [その他重要な情報や注意点、特になければ「なし」] 次回アクション/宿題: [次回の会議までに実行すべきタスクや担当者、不明な場合は「不明」] 次回開催日時: [次回会議の開催日時、不明な場合は「不明」] </output_format> ```' reasoning_format: separated selected: true title: LLM type: llm vision: enabled: false height: 88 id: '1765513576891' position: x: 384 y: 282 positionAbsolute: x: 384 y: 282 selected: true sourcePosition: right targetPosition: left type: custom width: 242 - data: outputs: - value_selector: - '1765513576891' - text value_type: string variable: text selected: false title: 終了 type: end height: 89 id: '1765513883340' position: x: 686 y: 282 positionAbsolute: x: 686 y: 282 selected: false sourcePosition: right targetPosition: left type: custom width: 242 viewport: x: 37 y: 160.5 zoom: 1 rag_pipeline_variables: []
6. テスト

作成したエージェントアプリをテスト実行し、正しく動作するか確認します。
今回は、オンプレミスで動作するモデルの性能を確認するために、クラウド型モデルを含めてモデルのみを差し替えて、動作結果を比較しました。
6-1. 比較条件
動作結果の比較は、以下の条件下で実施しました。
比較対象モデル:
- gpt-oss-120b (ここまでの手順で作成したモデル)
- gpt-oss-20b (OpenAI社製の軽量モデル)
- Phi-4 14B (Microsoft社製の軽量モデル)
- GPT-4o (Azure OpenAI Service / ベンチマーク用)
検証シナリオ:
- モデル以外の情報の統一:
比較観点であるモデルの精度の違いを明らかにするために、モデル以外の情報は統一。入力情報(会議メモ、案件名、打合せ実施日)やシステムプロンプト、与えるツールリストを全てのモデルで統一した。 - 要約文の標準化:
エージェントの中でツールとして与えている「テキスト要約ワークフロー」については、品質のばらつきを排除するため gpt-oss-120b で固定し、生成された要約文をエージェントに渡すようにした。 - ファイルシステムの初期化:
実行ごとに書き出し先のディレクトリ(/data 配下)を初期状態にリセットし、モデルに与えられるディレクトリ構造のコンテキスト情報を統一した。
評価方法:

- ツールの実行順序やプロセスは問わず、最終的な結果として ユーザの指示通りに正しいパスにファイルが格納されたか」 のみを成功の定義とした。
- 各モデルは3回ずつ実行した。
入力値・変数情報:
- TRANSCRIPT_FILE(文字起こしテキスト):
はい、それじゃあ時間になりましたので、定例の進捗会議を始めます。お疲れ様です。えーと、今日の主な議題は、来週のリリースに向けた最終確認と、現在残っている課題の洗い出しですね。まず、開発の状況について鈴木さんからお願いします。はい、開発チームの鈴木です。えー、進捗としては概ね予定通りなんですが、1点だけ、昨日発覚した不具合がありまして。ログイン画面で、特定のAndroid端末を使った場合にレイアウトが崩れるという現象が報告されています。機能自体、ログインができるかできないかで言うとできるんですが、見た目がちょっと厳しい状態です。なるほど。その「特定の端末」っていうのは、ユーザー数でいうとどれくらいの割合になりそうですか?そうですね、ログ解析した限りだと全体の2%未満だとは思うんですが、古い機種じゃなくて比較的新しいPixel系の一部で起きているので、放置はできないかなと。修正自体は明日いっぱいあれば完了する見込みです。ただ、その分、最終テストの開始が1日後ろ倒しになる可能性があります。うーん、テスト期間が削られるのはちょっと怖いですね。わかりました。一旦、修正優先で進めてください。テストチームには私から共有しておきます。次に佐藤さん、広報周りはどうですか?はい、広報の佐藤です。プレスリリースの原稿はほぼ完成していて、あとは実際のアプリ画面のスクリーンショットを差し込むだけの状態です。ただ、今鈴木さんからお話があったレイアウト崩れの件、もし修正でUIが変わるようであれば、スクショの撮影も待ったほうがいいですかね? 今週金曜中には入稿したいんですけど。あ、その点は大丈夫です。崩れているのはログイン画面の一部だけで、メインのダッシュボード画面とか設定画面には影響ないので、先にそっちの撮影を進めてもらえれば問題ないです。ログイン画面の修正版は、明日の夕方にはテスト環境に上げられると思います。あ、そうなんですね。安心しました。じゃあ、メイン機能の画面撮影は今日中に終わらせて、金曜の午前中にログイン画面だけ差し替えて最終版を作ります。それなら入稿には間に合います。よかった。じゃあ広報スケジュールは変更なしでいきましょう。あと、リリース当日の体制ですが、以前決めた通り、開発チームは朝9時から待機、広報は10時にリリース配信、という流れで大丈夫ですか?はい、開発側はシフト組んでますので問題ないです。あ、そうだ。サーバーの増強については、前日の夜、えーと来週の水曜日の夜に実施することになりましたので、そこだけ共有しておきます。承知しました。じゃあタスクとしては、鈴木さんはAndroidのレイアウト修正を明日までに完了させること。佐藤さんは予定通り金曜入稿を目指して素材作成を進めること。私はテストチームへのスケジュール調整を行う、という形ですね。他になにかありますか?大丈夫です。私も特にないです。はい、では今日のミーティングは以上で終わります。お疲れ様でした。
- PROJECT_NAME:Project-A
- MEETING_DATE:20241111
- INPUT(ユーザ入力):要約して案件フォルダに保存して
6-2. 検証結果
各モデルの実行結果は以下の通りです。
| モデル | パラメータ数 | 実行環境 | 成功率 (3回中) | 評価コメント |
|---|---|---|---|---|
| GPT-4o | 非公開 | Azure | 3/3 | 【基準】 安定して成功。ディレクトリ内のファイルリスト確認等の手順も手堅く踏んでいた。 |
| gpt-oss-120b | 120B | オンプレミス | 3/3 | 【優秀】 GPT-4oと同様、全ての試行で目的を達成。GPT-4oと比較すると処理完了までのツール実行数がわずかに多かったが、指示に無関係な処理はなく、ファイルパスの推論ミスやJSON形式エラーによる中断もなく、非常に安定していた。 |
| gpt-oss-20b | 20B | オンプレミス | 1/3 | 【厳しい】 タスク完了に必要なツールを呼び出さずに処理完了するケースが散見された。例えば、テキストの要約処理や格納処理を実施せずに応答完了するなど不安定な結果となった。 |
| Phi-4 14B | 14B | オンプレミス | 0/3 | 【厳しい】一度も成功することができなかった。Phi-4はコンテキスト長が16Kのため、これを超える処理が求められたため、タスク実行の途中で処理が中断されてしまった。 |
うまく動作している例

うまく動作しなかった例

6-3. 課題と考察
- ①オンプレミスモデルの実用性
gpt-oss-120bでは、今回のタスクにおいてはGPT-4oと同じく全実行回において正しくタスク実行することができた。一般に、クラウド型の超巨大モデルの方がより高性能なタスクを実行できるが、今回のタスクにおいては十分に実用的に用いることができると考えられる。
ただし、今回のタスクにおいては入力した文字起こしテキストが比較的短く、格納先のディレクトリについても既存ファイルが存在しないなど、実環境よりもシンプルな状況であったことに注意したい。
- ②モデルの長文処理能力の重要性
今回、複数のオンプレミスモデルを含めて比較したが、扱えるコンテキスト長が16KのPhi-4 14Bにおいては、タスク完了に必要なコンテキスト長が不足してしまい処理の途中で完了してしまった。(成功していたGPT4o等のモデルでは3万~4万トークンほどを消費していた。)
また、より長いコンテキスト長を扱うことのできるgpt-oss-20bにおいては、タスク完了に必要な処理を正しく計画・実行できずに正しく完了できなかった。
この結果より、AIエージェントの正確な動作には、長いコンテキストサイズを入力でき、かつ正しく処理できる能力を併せ持つLLMの選定が必要であると考えられる。
- ③オンプレミス型エージェントの実用化に向けて
2025年12月現在、クラウド型モデルでは、Gemini 3.0 Proなどの1Mを超える長いコンテキストを扱うことができ、かつ高い長文処理能力をもつモデルが登場している。一方で、オンプレミスでも動作できるSLMにおいては、一部のモデルを除いて128K~256Kほどのコンテキスト長が一般的である。
そのため、複雑なAIエージェントをオンプレミスで構成する場合は、モデル選定に加えて利用するトークン長を抑える仕組みも有効であると考えられる。
例えば、今回は要約したいテキストや要約後の内容など、一連の内容をすべて1つのエージェントが保持する形で実装したが、この構成では要約対象のテキストが大きくなった場合に消費トークン量が肥大化してしまう。
そこで、一連の処理を一度に実行するのではなく、要約するエージェント、ファイルを一時フォルダに書出しするエージェント、ファイルを移動するエージェントなど小さなプロセスごとにエージェントを分割し、タスクに含まれる各プロセスの中でAIに与える情報を最小限にすることで、1つのエージェントが保持するトークン長を抑える手法などが有効であると考えられる。
7. まとめ
オンプレミス型AIエージェントをMCPで作ってみたらちゃんと動いた
本記事では、オンプレミス環境に閉じて動作できるAIエージェントをMCPサーバーを用いて試作し、クラウドモデルと比較・評価しました。
オンプレミスで動作できるSLMを用いた場合でも精度よく実行できることが分かった反面、扱えるコンテキスト長と長文処理能力を踏まえてエージェントを設計することが重要であることが分かりました。
より実用的なエージェントを構成するために
SLMの性能はこの一年間でも目まぐるしく向上していますが、より実用性を高めるには、エージェントに任せたいタスクを細分化し、タスク用に設計された複数のAIエージェントを連携させて複雑なワークフローやプロセスを自動化することが有効と考えられます。
このような概念は「マルチエージェント・システム」と呼ばれており、ガートナージャパン社による見解では、2028年までの間にAIアプリケーションの70%が採用すると予測されています。
自社独自の業務においてDXを進めるためには、業務課題に基づく適切なアプローチの選択やシステムの設計が重要な第一歩であると考えています。
ビジネスにおけるMCPの展望
今回、社内システムを想定したファイルサーバとの連携によって、ユーザの指示をもとにファイルを作成し、適切なパスに格納するというエージェントアプリを実装しました。
MCPサーバーにはファイルシステムの操作だけでなく、データベース連携できるものなどもGitHub上に公開されています。また、今回作成したように自作することでより柔軟性をもったMCPサーバーを構成可能です。
これらが実用化されることで、蓄積された社内システム上の案件フォルダを横断した案件進捗の確認や、DBに蓄積されたデータに基づく分析や示唆出し、社内スケジュールシステムでの日程調整などが、より人との対話に近い形で実現される世の中がすぐにやってくるのではないかと感じています。
参考資料
本記事の執筆に当たり、以下のサイトを参考にしました。
- gpt-oss vLLM Usage Guide
- Gartner、2030年までに「ガーディアン・エージェント」がエージェント型AI市場の10〜15%を占めるようになるとの見解を発表
- MCP(Model Context Protocol) をやってみた (Windows版)
- fukabori.fm - 130. MCP(Model Context Protocol) w/ hudebakonosoto
執筆者
鈴木 優*1
データ・AI利活用を中心に、お客様のDX推進の支援業務に従事。コンサルティングによる課題抽出から、ソリューション提案、実務における伴走支援までを担う。
JDLA Deep Learning for ENGINEER 2023#2、Project Management Professional(PMP)、DXビジネス検定「DXビジネスプロフェッショナル レベル」の資格を保有。
商標
- Difyは、LangGenius, Inc.の商標または登録商標です。
- Dockerは、Docker, Inc. の商標または登録商標です。
- GPT-4o、gpt-oss-20b、gpt-oss-120bは、OpenAI, Inc.の商標または登録商標です。
- GitHubは、GitHub, Inc. の登録商標または商標です。
- Microsoft Azureは、Microsoft Corporation の商標または登録商標です。
- Next.jsは、Vercel Inc.の商標または登録商標です。
- Phi-4は、Microsoft Corporation の商標または登録商標です。
免責事項
- 本記事の内容は執筆時点(2025年12月)の情報に基づいており、サービスの仕様変更等により内容が変更される可能性があります。
- 本記事で紹介する設定手順や使用方法は、特定の環境での動作確認に基づくものであり、すべての環境での動作を保証するものではありません。
- 本記事では生成AIを活用した内容が含まれており、AIによる情報の生成過程でハルシネーション(事実に基づかない情報の生成)が発生する可能性があります。記載内容の正確性については、必ず公式ドキュメントや信頼できる情報源で検証してください。
*1 : 執筆時はNTT西日本ビジネス営業本部に所属。掲載時はNTT人間情報研究所に所属。