Skip to main content
Glama

RAG-MCP Server

Model Context Protocol (MCP) サーバー。FlaskLangGraphLlamaIndex を使用して RAG 用に構築されています。最小限で機能的、かつ拡張しやすいように設計されています。

概要

  • HTTP 上の MCP: JSON-RPC 2.0 による POST /mcp

  • LangGraph: 関数呼び出しでエージェントをオーケストレーションします。

  • LlamaIndex: ローカルドキュメントをインデックス化し、RAG で回答します。

  • Docker: docker compose up --build で完了です。


Related MCP server: DocAgent-MCP

アーキテクチャ

MCP Client  →  Flask /mcp  →  LangGraph Agent  →  Tools
                                                ├─ rag_search (LlamaIndex RAG)
                                                ├─ agora (UTC datetime)
                                                └─ calcular (arithmetic expression)
  • クライアントはツールを一覧表示し (tools/list)、呼び出します (tools/call)。

  • ask_agent ツールは LangGraph グラフをトリガーします。LLM が関数呼び出しを使用するタイミングを決定します。

  • 他のツールは MCP によって直接呼び出されます。


前提条件

  • DockerDocker Compose (またはローカルの Python 3.11+)。

  • OPENAI_API_KEY (LLM と埋め込み用)。


プロジェクト構造

.
├── app.py              # MCP server + LangGraph + LlamaIndex
├── data/               # RAG corpus (.md, .txt, .pdf, etc.)
│   └── kb.md
├── requirements.txt    # Python dependencies
├── Dockerfile
├── docker-compose.yml
├── .env                # OPENAI_API_KEY and variables
└── README.md

設定

1. プロジェクトのクローン / 作成

空のディレクトリを作成し、プロジェクトファイルを貼り付けます (「ファイル」セクションを参照)。

2. 環境変数

ルートに .env を作成します:

OPENAI_API_KEY=sk-...
LLM_MODEL=gpt-4o-mini

サポートされている変数:

変数

デフォルト

説明

OPENAI_API_KEY

(必須)

OpenAI API キー。

LLM_MODEL

gpt-4o-mini

LLM と埋め込み用のモデル。

DATA_DIR

/app/data

RAG 用のドキュメントが含まれるディレクトリ。

PORT

8080

サーバーポート。

3. RAG データ

ドキュメントを data/ に配置します (例: kb.mdpolicies.mdmanuals/)。サーバーは起動時にすべてをインデックス化します。

最小限の例 (data/kb.md):

# Acme Corp
Support SLA: 4 hours during business hours (UTC-3).
Pro Plan costs USD 49/month and includes 10k RAG queries/day.
P1 incidents must be opened in #sre channel.

実行

Docker (推奨)

docker compose up --build

サーバーは http://127.0.0.1:8080 で実行されます。

ローカル (Docker なし)

pip install -r requirements.txt
export OPENAI_API_KEY=sk-...
export DATA_DIR=./data
python app.py

エンドポイント

POST /mcp (JSON-RPC 2.0)

MCP は 3 つの主要なメソッドを使用します:

initialize

curl -s http://127.0.0.1:8080/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"initialize","params":{}
}'

応答:

{
  "jsonrpc":"2.0",
  "id":1,
  "result":{
    "protocolVersion":"2024-11-05",
    "capabilities":{"tools":{}},
    "serverInfo":{"name":"rag-mcp","version":"1.0.0"}
  }
}

tools/list

利用可能なすべてのツールを一覧表示します (ask_agent を含む):

curl -s http://127.0.0.1:8080/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":2,"method":"tools/list","params":{}
}'

tools/call

ツールを呼び出します:

curl -s http://127.0.0.1:8080/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":3,"method":"tools/call",
  "params":{
    "name":"ask_agent",
    "arguments":{"question":"What is the SLA and how much does Pro cost?"}
  }
}'

応答:

{
  "jsonrpc":"2.0",
  "id":3,
  "result":{
    "content":[{"type":"text","text":"The SLA is 4 hours during business hours (UTC-3). The Pro Plan costs USD 49/month..."}]
  }
}

GET /health

シンプルなヘルスチェック:

curl -s http://127.0.0.1:8080/health
# {"ok": true}

利用可能なツール

ツール

説明

InputSchema

ask_agent

RAG + 関数呼び出しを備えた LangGraph エージェント。

{"question": "string"}

rag_search

RAG (LlamaIndex) でベース内のファクトを検索します。

{"query": "string"}

agora

現在の UTC 日時 (ISO-8601) を返します。

{}

calcular

安全な算術式を評価します。

{"expressao": "string"}

直接使用の例

# RAG direct
curl -s http://127.0.0.1:8080/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":4,"method":"tools/call",
  "params":{"name":"rag_search","arguments":{"query":"support SLA"}}
}'

# Datetime
curl -s http://127.0.0.1:8080/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":5,"method":"tools/call",
  "params":{"name":"agora","arguments":{}}
}'

# Calculation
curl -s http://127.0.0.1:8080/mcp -H 'content-type: application/json' -d '{
  "jsonrpc":"2.0","id":6,"method":"tools/call",
  "params":{"name":"calcular","arguments":{"expressao":"(2+3)*4"}}
}'

Cursor / Zed / その他の MCP クライアントとの統合

エディタの設定に追加します (例: ~/.cursor/settings.json):

{
  "mcpServers": {
    "rag-mcp": {
      "url": "http://127.0.0.1:8080/mcp"
    }
  }
}

クライアントは次のことを行います:

  1. initialize を呼び出します。

  2. ツールを一覧表示します (tools/list)。

  3. 必要に応じて ask_agent を使用するか、ツールを直接呼び出します。


RAG の仕組み

  1. インデックス化: 起動時に SimpleDirectoryReaderdata/ を読み取り、VectorStoreIndextext-embedding-3-small で埋め込みを作成します。

  2. クエリ: as_query_engine が最も類似した 3 つのチャンクを取得し、LLM が回答を合成します。

  3. 更新: 再インデックス化するには、data/ 内のファイルを追加/削除してコンテナを再起動します。


エージェントの仕組み (LangGraph)

  • agent ノードは bind_tools(TOOLS) で LLM を呼び出します。

  • tools_condition が決定します: モデルが関数呼び出しを要求した場合は tools ノードに進み、それ以外の場合は終了します。

  • tools ノードはツールを実行して agent に戻り、agent が最終応答を生成します。

フロー:

START → agent → (tools?) → tools → agent → END

拡張

新しいツールの追加

app.py に追加します:

@tool
def my_tool(param1: str, param2: int = 0) -> str:
    """Clear description of what the tool does."""
    # logic
    return "result"

次に:

TOOLS.append(my_tool)

サーバーを再起動します。ツールは tools/list に自動的に表示されます。

モデルの変更

.envLLM_MODEL を変更します:

LLM_MODEL=gpt-4o

または、app.pyChatOpenAI と埋め込みを置き換えて、別のプロバイダー (例: Anthropic、Groq) を使用します。

ベクターストアの変更

VectorStoreIndex を永続ストア (Chroma、Pinecone、Weaviate など) に置き換えます:

from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb

client = chromadb.PersistentClient(path="./chroma")
collection = client.get_or_create_collection("rag")
vector_store = ChromaVectorStore(chroma_collection=collection)
_index = VectorStoreIndex.from_documents(docs, vector_store=vector_store)

セキュリティとベストプラクティス

  • 認証なしでサーバーをインターネットに直接公開しないでください。

  • MCP クライアントが同じホスト上にある場合は、内部ネットワーク (Docker) を使用します。

  • カスタムツールで入力を検証します (特に DB や外部 API にアクセスする場合)。

  • 本番環境では、以下を追加します:

    • レート制限。

    • 構造化ログ。

    • メトリクス (Prometheus、OpenTelemetry)。


トラブルシューティング

ModuleNotFoundError

  • requirements.txt をインストールしたことを確認します。

  • Docker では、docker compose build --no-cache を実行します。

無効な OPENAI_API_KEY

  • docker compose exec mcp env | grep OPENAI でキーを確認します。

  • ローカルでテスト: curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"

RAG がドキュメントを見つけられない

  • data/ に有効なファイル (.md.txt など) があることを確認します。

  • ログを確認: docker compose logs mcp

  • コンテナを再起動して再インデックス化します。

calcular のエラー

  • このツールは単純な算術式のみを受け付けます。

  • 変数、関数、複雑な Python 構文は避けてください。


ファイル

app.py

"""MCP Server (JSON-RPC) + LangGraph + LlamaIndex RAG."""
from __future__ import annotations

import ast
import operator as op
import os
from datetime import datetime, timezone
from typing import Annotated, TypedDict

from flask import Flask, jsonify, request
from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from llama_index.core import Settings, SimpleDirectoryReader, VectorStoreIndex
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI as LlamaLLM

DATA_DIR = os.getenv("DATA_DIR", "./data")
MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")

# --- RAG: index ./data once on process startup ---
Settings.llm = LlamaLLM(model=MODEL)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
_index = VectorStoreIndex.from_documents(SimpleDirectoryReader(DATA_DIR).load_data())
_qe = _index.as_query_engine(similarity_top_k=3)


# --- Function calling: each @tool becomes JSON schema for LLM and MCP ---
@tool
def rag_search(query: str) -> str:
    """Search facts in local base via RAG (LlamaIndex). Use for policies, products, and docs."""
    return str(_qe.query(query))


@tool
def agora() -> str:
    """Returns current UTC datetime (ISO-8601)."""
    return datetime.now(timezone.utc).isoformat()


@tool
def calcular(expressao: str) -> str:
    """Evaluates safe arithmetic. Examples: (2+3)*4, 10/2, 2**8."""
    ops = {
        ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv,
        ast.Mod: op.mod, ast.Pow: op.pow, ast.USub: op.neg,
    }

    def _eval(n):
        if isinstance(n, ast.Expression):
            return _eval(n.body)
        if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)):
            return n.value
        if isinstance(n, ast.BinOp) and type(n.op) in ops:
            return ops[type(n.op)](_eval(n.left), _eval(n.right))
        if isinstance(n, ast.UnaryOp) and type(n.op) in ops:
            return ops[type(n.op)](_eval(n.operand))
        raise ValueError("invalid expression")

    return str(_eval(ast.parse(expressao, mode="eval")))


TOOLS = [rag_search, agora, calcular]


# --- LangGraph: agent ↔ tools until model stops requesting function calls ---
class State(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]


llm = ChatOpenAI(model=MODEL, temperature=0).bind_tools(TOOLS)


def agent_node(state: State) -> dict:
    sys = SystemMessage(content="MCP assistant. Use tools when needed. Respond in English.")
    return {"messages": [llm.invoke([sys, *state["messages"]])]}


_g = StateGraph(State)
_g.add_node("agent", agent_node)
_g.add_node("tools", ToolNode(TOOLS))
_g.add_edge(START, "agent")
_g.add_conditional_edges("agent", tools_condition)  # tools or END
_g.add_edge("tools", "agent")
GRAPH = _g.compile()


def _schema(t) -> dict:
    """Converts LangChain tool to MCP inputSchema."""
    s = t.args_schema.model_json_schema() if t.args_schema else {"type": "object"}
    s.pop("title", None)
    return s


MCP_TOOLS = [
    {"name": t.name, "description": t.description, "inputSchema": _schema(t)}
    for t in TOOLS
] + [{
    "name": "ask_agent",
    "description": "LangGraph agent with RAG + function calling. Pass the user question.",
    "inputSchema": {
        "type": "object",
        "properties": {"question": {"type": "string"}},
        "required": ["question"],
    },
}]


def _run(name: str, args: dict) -> str:
    if name == "ask_agent":
        out = GRAPH.invoke({"messages": [HumanMessage(content=args.get("question", ""))]})
        return str(out["messages"][-1].content)
    fn = {t.name: t for t in TOOLS}.get(name)
    if not fn:
        raise ValueError(f"unknown tool: {name}")
    return str(fn.invoke(args or {}))


# --- Flask: HTTP transport for MCP (JSON-RPC 2.0) ---
app = Flask(__name__)


@app.post("/mcp")
def mcp():
    body = request.get_json(force=True) or {}
    method, rid, params = body.get("method"), body.get("id"), body.get("params") or {}

    if method == "initialize":
        return jsonify({"jsonrpc": "2.0", "id": rid, "result": {
            "protocolVersion": "2024-11-05",
            "capabilities": {"tools": {}},
            "serverInfo": {"name": "rag-mcp", "version": "1.0.0"},
        }})
    if method == "tools/list":
        return jsonify({"jsonrpc": "2.0", "id": rid, "result": {"tools": MCP_TOOLS}})
    if method == "tools/call":
        try:
            text = _run(params.get("name"), params.get("arguments") or {})
            result = {"content": [{"type": "text", "text": text}]}
        except Exception as e:
            result = {"content": [{"type": "text", "text": str(e)}], "isError": True}
        return jsonify({"jsonrpc": "2.0", "id": rid, "result": result})
    if method == "notifications/initialized" or rid is None:
        return ("", 204)
    return jsonify({"jsonrpc": "2.0", "id": rid, "error": {"code": -32601, "message": method}}), 400


@app.get("/health")
def health():
    return {"ok": True}


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.getenv("PORT", 8080)))

requirements.txt

flask>=3.0
langgraph>=0.2
langchain-core>=0.3
langchain-openai>=0.2
llama-index>=0.12
llama-index-llms-openai
llama-index-embeddings-openai

Dockerfile

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY data ./data
ENV PORT=8080 DATA_DIR=/app/data
EXPOSE 8080
CMD ["python", "app.py"]

docker-compose.yml

services:
  mcp:
    build: .
    ports: ["8080:8080"]
    env_file: .env
    environment:
      PORT: "8080"
      DATA_DIR: /app/data
      LLM_MODEL: gpt-4o-mini
    volumes:
      - ./data:/app/data:ro
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"]
      interval: 15s
      retries: 5

data/kb.md

# Acme Corp
Support SLA: 4 hours during business hours (UTC-3).
Pro Plan costs USD 49/month and includes 10k RAG queries/day.
P1 incidents must be opened in #sre channel.

.env

OPENAI_API_KEY=sk-...
LLM_MODEL=gpt-4o-mini

ライセンス

MIT。

Related MCP Connectors

Related MCP Servers