はじめに
前回の記事 では、「MCP を使えばローカルや外部のデータソースを自然言語で扱える」という可能性を、自然言語 → SQL のデモを通して探りました。
今回はその "次のステップ" として、自作チャットアプリから MCP を呼び出し、最小のエージェントとして動かすことをテーマにしています。
そのために、まずドキュメント(PDF)を扱える Streamable HTTP MCP サーバー を TypeScript で実装し、それを Claude Desktop と OpenAI Agents SDK の両方から利用できるかを検証しました。
最終的には、「自作チャットアプリ → MCP(Streamable HTTP) → PDF 検索/読取 → 回答」という一連の流れが成立し、期待通り "最小エージェント" として動作したので、その内容をまとめます。
この記事でやること(先に概要)
- Streamable HTTP 対応の MCP サーバーを TypeScript で実装
- PDF を読み取るための list / read / search の最小ツールを作成
- セッション管理を実装
- MCP Inspector で動作確認
- Claude Desktop で動作確認
- 自作チャットアプリ(OpenAI Agents SDK)から MCP を呼び出し、最小エージェントとして動かしてみる
🧪 成果
- Claude Desktop から「花王の最新の決算サマリを200字以内にまとめてください」と話しかけると、Streamable HTTP 経由の MCP サーバーが PDF を読み取り、正しく応答した
- OpenAI Agents SDK を使った自作チャットアプリでも MCP ツールを呼び出せ、最小構成のエージェントとして動作した
背景とねらい
- MCP をより汎用的に使えるよう、ローカル stdio ではなく HTTP で動作する Streamable HTTP を試す
- MCP の題材として、業務でもよく扱う PDF や社内資料などドキュメント系を選択
- 今回は検索精度の追求よりも、最小構成の MCP サーバーが正常に動くかを重視
- Claude Desktop や自作チャットアプリ(OpenAI Agents SDK)が同じ MCP サーバーをどう扱うかを確認したかった
特に「自作チャットアプリから MCP を叩けるか?」を確かめ、 "アプリにエージェント的な性質を簡易に付与できるのか" がゴールでした。
全体アーキテクチャ
Claude Desktop・自作チャットアプリ(OpenAI Agents SDK) ──(MCP Streamable HTTP)──> Node.js MCP Server ──> PDF (ローカル)
▲
│
PDF の読取や検索を自然言語で指示
自作チャットアプリからも、Claude Desktop と同じ MCP サーバーに対して、自然言語で PDF 一覧 → 読取 → 検索 といった操作を行います。
実装ハイライト
Streamable HTTP MCP サーバーを作成
実装の詳細は省略しますが、最小構成は下記のようになります。
前回の stdio とは異なり HTTP 通信を行うため、express を利用して createServer 内に Tool 定義・実行処理を記述しています。
// 必要なライブラリのインポート
import express, { Request, Response } from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamablehttp.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const app = express();
const PORT = 8888; // ポート番号を指定
app.use(express.json());
// MCP サーバーを生成
function createServer(): Server {
const mcp = new Server(
{ name: "pdf-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Tool 定義
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
// 後述
}));
// Tool 実行
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
// 後述
});
return mcp;
}
// 本記事では最小限のPOSTのみ
app.post("/mcp", async (req: Request, res: Response) => {
try {
// リクエストごとに新しいServerとTransportを生成
const server = createServer();
const transport = new StreamableHTTPServerTransport({
// セッションIDは毎回新規生成(この処理では上手くいかないので、本記事のセッション管理で言及
sessionIdGenerator: () => "temp-session",
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (err) {
console.error("Error:", err);
res.status(500).json({ error: "Internal error" });
}
});
// 起動
app.listen(PORT, () => {
console.log(`MCP Streamable HTTP Server running at http://localhost:${PORT}/mcp`);
});
Tool は list / read / search の3つだけ
pdfs フォルダに PDF を置き、MCP サーバー側で以下の3つの Tool を公開します。
- list_pdfs:PDF の一覧取得
- read_pdf:指定 PDF の全文取得
- search_pdf:指定 PDF 内からキーワード検索
いずれも最小限の構成で、MCP 経由で必要な情報が取得できることを確認する目的です。
Tool の定義は下記の通りとします。
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{ name: "list_pdfs", description: "PDFの一覧を取得して返却します。", inputSchema: { type: "object" } },
{ name: "read_pdf", description: "指定されたPDFファイルの中身を返却します。", inputSchema: { type: "object", required: ["name"], properties: { name: { type: "string" } }}},
{ name: "search_pdf", description: "指定されたPDFファイルに対して、指定したキーワードで検索します。", inputSchema: { type: "object", required: ["name", "query"], properties: { name: { type: "string" }, query: { type: "string" }}}},
],
}));
Tool の具体的な処理は本記事で割愛させていただきますが、定義は以下のようになります。
// Tool 実行
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
const name = req.params.name;
const args = req.params.arguments as any ?? {};
if (name === "list_pdfs") {
// 中略
}
if (name === "read_pdf") {
// 中略
}
if (name === "search_pdf") {
// 中略
}
throw new Error("Unknown tool");
});
セッション管理
今回もっともハマったのが "セッション管理" です。
app.post 内で毎回セッション ID を生成してしまうと、ステートが保持できません。そのため以下を実装しました。
-
mcp-session-idヘッダを参照 - ヘッダがなければ新規発行しレスポンスに返却
- sessionId ごとに
ServerとTransportを保持
この仕組みにより、Streamable HTTP でも意図通りセッションを維持できます。
コードとしては、以下の処理を追加します。
// セッション管理(最小構成)
const sessions = new Map<
string,
{ server: Server; transport: StreamableHTTPServerTransport }
>();
// セッション取得 or 作成
async function getSession(sessionId: string) {
let session = sessions.get(sessionId);
if (!session) {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => sessionId,
});
await server.connect(transport);
session = { server, transport };
sessions.set(sessionId, session);
}
return session;
}
// セッションID取得
function getSessionId(req: Request): string {
return (req.headers["mcp-session-id"] as string) || randomUUID();
}
上記の変数や関数を基に app.post を以下のように修正します。
app.post("/mcp", async (req: Request, res: Response) => {
try {
const sessionId = getSessionId(req);
const session = await getSession(sessionId);
res.setHeader("mcp-session-id", sessionId);
await session.transport.handleRequest(req, res, req.body);
} catch (err) {
res.status(500).json({ error: "Internal error" });
}
});
MCP Inspector で動作確認
以下コマンドで MCP Inspector を起動し、
npx @modelcontextprotocol/inspector tsx src/index.ts
Transport Type を Streamable HTTP 、 URL を http://localhost:8888/mcp に設定して接続します。
Connect → List Tools をクリックすると、サーバーが公開しているツール一覧を確認できます。
例えば list_pdfs を選んで実行すると、保管されているドキュメント一覧を取得できます(本記事では花王の24年度、25年度の決算短信を準備)。
read_pdf を選び kao-results-fy2025q3-jp.pdf を指定して実行すると、以下のような結果を得ることができます。
Claude Desktop で動作確認
MCP 設定
ここまで動作確認ができたら、Claude Desktop に MCP 設定を追加します。
{
"mcpServers": {
"qiita-document-mcp": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:8888/mcp",
"--allow-http"
]
}
}
}
ℹ️ 補足
Claude Desktop はデフォルトでは stdio MCP サーバーのみ対応しています。
Streamable HTTP の MCP サーバーに接続するには、npm パッケージの mcp-remote を経由して「HTTP の MCP サーバーである」ことを明示する必要があります。
再起動すると、「設定 → コネクタ」に登録した MCP サーバーが表示されます。
Claude に話しかける
Claude に「花王の最新の決算サマリを200字以内にまとめてください。」と話しかけると、MCP サーバーが PDF を検索 → 読み取り → 回答生成、まで一連の処理を実行します。
OpenAI Agents SDK も試してみた
今回のタイトルにもありますように、自作チャットアプリに OpenAI Agents SDK を組み込み MCP を指定し、同じ MCP サーバーがどのように利用されるか試してみました。
実装内容
実装自体はシンプルで、MCP サーバーを定義して Agent の mcpServers に追加するだけです。こちらの検証では GPT 5.1 を使用しました。
// Agent などのライブラリをインポート
const { Agent, run, MCPServerStreamableHttp } = require('@openai/agents');
// MCP Server の初期化
const mcpServer = new MCPServerStreamableHttp({
url: process.env.MCP_SERVER_URL || 'http://localhost:8888/mcp',
name: process.env.MCP_SERVER_NAME || 'qiita-document-mcp',
});
// OpenAI Agent を MCP サーバーを指定して初期化
const agent = new Agent({
name: 'Chat Assistant',
model: 'gpt-5.1',
instructions: `
あなたはMCPツールを通じてPDFドキュメントにアクセスできるアシスタントです。
ユーザーがPDFドキュメントについて質問した場合、以下のツールを適切に使い分けてください:
- list_pdfs: 利用可能なすべてのPDFファイルの一覧を取得します
- read_pdf: 特定のPDFファイルの全文テキストを読み込んで内容を確認します
- search_pdf: PDFファイル内で特定のキーワードを検索し、該当箇所を特定します
回答する際は、以下を心がけてください:
- 明確で分かりやすい説明を提供する
- 正確な情報を基に回答する
- 親切で丁寧な対応を心がける
`,
mcpServers: [mcpServer],
});
// MCP 通信の初期化
async function initializeMCP() {
if (process.env.MCP_SERVER_URL) {
try {
await mcpServer.connect();
} catch (error) {
// エラー処理
}
} else {
// エラー処理
}
}
OpenAI Agents SDK 経由で GPT 5.1 に話しかける
簡易的なチャットアプリの UI を作成した上で、いざ試してみます。
まずは PDF の一覧を取得できるかを聞いてみました。
次に、 Claude Desktop と同じように「花王の最新の決算サマリを200字以内にまとめてください。」と聞いてみました。
ここまでやってみて(所感)
Streamable HTTP を利用した MCP サーバーを実装し、Claude Desktop・OpenAI Agents SDK の両方から利用できることを確認できました。
特に、OpenAI Agents SDK のシンプルなコードで Claude Desktop と同等の結果を再現できたことに可能性を感じました。
一方で、プロンプトの関係なのか期待している Tool を使ってくれないケースもあり、 MCP や Tool が増えた際の工夫は必要になってくると考えています。
また実際に提供する段階では、サーバーのデプロイや認証・認可の実装も不可欠になるため、このあたりは今後調査したいと思います。
おわりに
この記事では、 Streamable HTTP MCP を自作し、Claude Desktop と自作チャットアプリの両方から最小エージェントとして動かすところまで検証しました。
MCP × 自作アプリ × Agents SDK の組み合わせは、それほど事例を見かけませんが、アプリへのエージェント機能の付与を非常にシンプルにしてくれると感じています。
今後も応用範囲が広がりそうなので、改善点やアドバイスがあれば、ぜひコメントいただけると嬉しいです。