Labsco
kyopark2014 logo

MCP RAG

โ˜… 1

from kyopark2014

A managed Retrieval-Augmented Generation (RAG) server using MCP, integrated with knowledge bases and OpenSearch.

๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅโœ“ VerifiedAccount requiredNeeds API keys

MCP RAG

MCP๋ฅผ ์ด์šฉํ•˜์—ฌ RAG๋ฅผ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ด์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ๋Š” ์™„์ „ ๊ด€๋ฆฌํ˜• RAG ์„œ๋ฒ„์Šค์ธ knowledge base์™€ ๊ด€๋ฆฌํ˜• RAG์ธ OpenSearch์—์„œ MCP๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ RAG์˜ ์„ฑ๋Šฅํ–ฅ์ƒ ๊ธฐ๋ฒ•์ธ advanced RAG๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒด์ ์ธ architecture๋Š” ์•„๋ž˜์™€ ๊ฐ™๊ณ  RAG๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” 4๊ฐ€์ง€ ํ˜•ํƒœ์˜ MCP๋ฅผ ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋ฌธ์„œ๋ฅผ Amazon S3๋กœ ์—…๋กœ๋“œํ•˜๋ฉด Knowledge Base์—์„œ๋Š” sync๋ฅผ ํ†ตํ•ด ๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์™€์„œ Amazon Opensearch Serverless๋กœ ๋ฌธ์„œ๋ฅผ ์ ์žฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋ฏธ๋ฆฌ ์ง€์ •ํ•œ embedding model์„ ์ด์šฉํ•˜๊ณ  multi modal์„ ํ†ตํ•ด ๋ถ„์„๋œ ์ •๋ณด๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ Amazon S3์— ๋ฌธ์„œ๊ฐ€ ์—…๋กœ๋“œ ๋  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” event๋ฅผ AWS Lambda (s3-event-manager)๊ฐ€ ๋ฐ›์•„์„œ SQS์— ์ „๋‹ฌํ•œ ๋‹ค์Œ์— ์ˆœ์ฐจ์ ์œผ๋กœ AWS Lambda (document-manager)๊ฐ€ embedding ๋ฐ multi modal ๋ถ„์„์„ ํ†ตํ•ด ์–ป์–ด์ง„ context๋ฅผ managed OpenSearch์— ์ ์žฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Amazon EC2์— ์žˆ๋Š” AI application์€ MCP client / server ๊ตฌ์กฐ๋ฅผ ์ด์šฉํ•˜์—ฌ MCP ์„œ๋ฒ„์˜ tool๋“ค์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ RAG๋ฅผ ํ™œ์šฉํ•  ๋•Œ์— ์•„๋ž˜ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์ด (1) AWS MCP (Knowledge Base) (2) MCP Lambda (Knowledge Base) (3) OpenSearch MCP (4) MCP Lambda (OpenSearch)์˜ 4๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ค‘์— ํ•œ๊ฐ€์ง€๋ฅผ ์„ ํƒํ•˜์—ฌ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. RAG๋ฅผ ์ด์šฉํ•ด ํ•„์š”ํ•œ OpenSearch, lambda, SQS ๋“ฑ์˜ ์ธํ”„๋ผ๋Š” AWS CDK๋ฅผ ์ด์šฉํ•˜์—ฌ ์‰ฝ๊ฒŒ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

image

๋ฌธ์„œ์˜ ๊ฐ ํŽ˜์ด์ง€๋“ค์„ ์บก์ถฐํ•˜์—ฌ ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜๋ฉด ๋ฌธ์„œ ์•ˆ์˜ ํ‘œ๋‚˜ ์ด๋ฏธ์ง€์— ๋Œ€ํ•œ ์ข€๋” ๋งŽ์€ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ๋กœ ๋ถ€ํ„ฐ ํŽ˜์ด์ง€ ์ด๋ฏธ์ง€๋ฅผ ์ถ”์ถœํ•˜์—ฌ ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•˜์—ฌ Agentic RAG ๊ตฌํ˜„ํ•˜๊ธฐ์™€ ๊ฐ™์ด event ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋งŒ๋“ค์–ด์„œ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ, ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋Š” lambda-s3-event-manager๋กœ ์ˆ˜ํ–‰ํ•˜๊ณ , ๋ฌธ์„œ์˜ ์ฒ˜๋ฆฌ๋Š” lambda-document-manager๋กœ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

image

Advanced RAG ๊ธฐ๋ฒ•

RAG์˜ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•œ advanced RAG ๊ธฐ๋ฒ•์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

OCR

๋ฌธ์„œ์˜ ๊ฐ ํŽ˜์ด์ง€๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•œ ํ›„์— multimodal์„ ํ†ตํ•ด ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋งฅ๋ฝ์— ๋งž๋Š” ์ด๋ฏธ์ง€ ๋ถ„์„์„ ์œ„ํ•ด contextual embedding์„ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ƒ์„ธํ•œ ์ฝ”๋“œ๋Š” lambda-document-manager์„ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค. Contextual embedding์„ ์œ„ํ•ด managed OpenSearch๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์™€ ๊ฐ™์ด os_client๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์˜ํ•˜๊ณ  OpenSearch index๋ฅผ ์ƒ์„ฑํ• ๋•Œ ์ด์šฉํ•ฉ๋‹ˆ๋‹ค.

session = boto3.Session(region_name=region)
credentials = session.get_credentials()

awsauth = AWS4Auth(
    credentials.access_key,
    credentials.secret_key,
    region,
    'es',  
    session_token=credentials.token
)

os_client = OpenSearch(
    hosts=[{
        'host': opensearch_url.replace("https://", ""), 
        'port': 443
    }],
    http_compress=True,
    http_auth=awsauth,
    use_ssl=True,
    verify_certs=True,
    ssl_assert_hostname=False,
    ssl_show_warn=False,
    connection_class=RequestsHttpConnection
)

์ด์ œ vectorstore ์ •์˜ํ•ด์„œ ๋ฌธ์„œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์‚ญ์ œํ• ๋•Œ์— ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

from langchain_community.vectorstores.opensearch_vector_search import OpenSearchVectorSearch
vectorstore = OpenSearchVectorSearch(
    index_name=index_name,  
    is_aoss = False,
    embedding_function=bedrock_embeddings,
    opensearch_url=opensearch_url,
    http_auth=awsauth,
    connection_class=RequestsHttpConnection
)

๊ฐ ํŽ˜์ด์ง€๊ฐ€ ์ „์ฒด ๋ฌธ์„œ์—์„œ ์–ด๋–ค ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€๋Š”์ง€ contextual_text๋ฅผ ์ถ”์ถœํ•˜์—ฌ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

def get_contextual_text(whole_text, splitted_text, llm): 
    contextual_template = (
        "<document>"
        "{WHOLE_DOCUMENT}"
        "</document>"
        "Here is the chunk we want to situate within the whole document."
        "<chunk>"
        "{CHUNK_CONTENT}"
        "</chunk>"
        "Please give a short succinct context to situate this chunk within the overall document for the purposes of improving search retrieval of the chunk."
        "Answer only with the succinct context and nothing else in English."
        "Put it in <result> tags."
    )              
    contextual_prompt = ChatPromptTemplate([
        ('human', contextual_template)
    ])

    contextual_text = ""        
    contexual_chain = contextual_prompt | llm            
    response = contexual_chain.invoke(
        {
            "WHOLE_DOCUMENT": whole_text,
            "CHUNK_CONTENT": splitted_text
        }
    )    
    output = response.content
    return output[output.find('<result>')+8:output.find('</result>')]

ํŽ˜์ด์ง€ ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜๊ธฐ ์œ„ํ•ด ์ด๋ฏธ์ง€ ์‚ฌ์ด์ฆˆ๋ฅผ ์กฐ์ •ํ•œ ํ›„์— contextual text์™€ ํ•จ๊ป˜ OpenSearch์— ๋“ฑ๋กํ•˜์—ฌ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

def store_image_for_opensearch(key):
    image_obj = s3_client.get_object(Bucket=s3_bucket, Key=key)
    image_content = image_obj['Body'].read()
    img = Image.open(BytesIO(image_content))
                        
    width, height = img.size 
    print(f"width: {width}, height: {height}, size: {width*height}")
            
    isResized = False
    while(width*height > 5242880):
        width = int(width/2)
        height = int(height/2)
        isResized = True
        print(f"width: {width}, height: {height}, size: {width*height}")
           
    buffer = BytesIO()
    img.save(buffer, format="PNG")
    img_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
                                                            
    llm = get_model()
    text = extract_text(llm, img_base64)
    extracted_text = text[text.find('<result>')+8:text.find('</result>')] 
    
    contextual_text = object_meta["contextual_text"]
    summary = summary_image(llm, img_base64, contextual_text)
    image_summary = summary[summary.find('<result>')+8:summary.find('</result>')]    
    contents = f"[์ด๋ฏธ์ง€ ์š”์•ฝ]\n{image_summary}\n\n[์ถ”์ถœ๋œ ํ…์ŠคํŠธ]\n{extracted_text}"
    
    page = object_meta["page"]
    docs = []
    docs.append(
        Document(
            page_content=contents,
            metadata={
                'name': key,
                'page': page,
                'url': path+parse.quote(key)
            }
        )
    )
    return add_to_opensearch(docs)                                                                                                      

์—ฌ๊ธฐ์„œ multimodal์„ ์ด์šฉํ•ด ์ด๋ฏธ์ง€์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•˜๋Š” ํ•จ์ˆ˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

def summary_image(llm, img_base64, contextual_text):  
    query = "์ด๋ฏธ์ง€๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๋‚ด์šฉ์„ ํ’€์–ด์„œ ์ž์„ธํžˆ ์•Œ๋ ค์ฃผ์„ธ์š”. markdown ํฌ๋งท์œผ๋กœ ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค."
    if contextual_text:
        query += "\n์•„๋ž˜ <reference>๋Š” ์ด๋ฏธ์ง€์™€ ๊ด€๋ จ๋œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€ ๋ถ„์„์‹œ ์ฐธ๊ณ ํ•˜์„ธ์š”. \n<reference>\n"+contextual_text+"\n</reference>"    
    messages = [
        HumanMessage(
            content=[
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{img_base64}", 
                    },
                },
                {
                    "type": "text", "text": query
                },
            ]
        )
    ]    
    result = llm.invoke(messages)
    extracted_text = result.content        
    return extracted_text

Parent Child Chunking

๋ฌธ์„œ ๊ฒ€์ƒ‰์˜ ์ •ํ™•๋„๋ฅผ ๋†’์ด๋ฉด์„œ Context๋ฅผ ์ถฉ๋ถ„ํžˆ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” Parent Child Chunking์„ ์ ์šฉํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์ด RecursiveCharacterTextSplitter๋กœ parent์™€ child๋ฅผ ๊ฐ๊ฐ ๋‚˜๋ˆ•๋‹ˆ๋‹ค.

parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=2000,
    chunk_overlap=100,
    separators=["\n\n", "\n", ".", " ", ""],
    length_function = len,
)
child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400,
    chunk_overlap=50,
    length_function = len,
)

๋จผ์ € parent chunk๋“ค์„ OpenSearch์— ๋„ฃ๊ณ  id๋“ค์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. child chunk์˜ meta์— parent chunk์˜ id๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ๊ฒ€์ƒ‰์‹œ child chunk๋ฅผ ํ•˜๊ณ , ์‹ค์ œ context๋Š” parent์˜ id๋กœ ์กฐํšŒํ•œ parent์˜ text๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. Contextual text๋Š” ์‹ค์ œ ์‚ฌ์šฉํ•˜๊ฒŒ ๋  parent chunk์˜ ํŠน์ง•์„ ์„ค๋ช…ํ•˜์—ฌ์•ผ ํ•˜๋ฏ€๋กœ, parent chunk๋กœ ์–ป์€ contextual text๋ฅผ child chunk์— ์ถ”๊ฐ€ํ•˜์—ฌ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ child chunk๋“ค๋„ OpenSearch์— ๋“ฑ๋กํ•˜์—ฌ id๋“ค์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. parent/child์˜ id๋“ค์„ ํŒŒ์ผ meta์— ์ €์žฅํ•˜์˜€๋‹ค๊ฐ€ ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ/์‚ญ์ œ์‹œ์— ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

splitted_docs = parent_splitter.split_documents(docs)
parent_docs, contexualized_chunks = get_contextual_docs_using_parallel_processing(docs[-1], splitted_docs)

for i, doc in enumerate(parent_docs):
    doc.metadata["doc_level"] = "parent"
        
parent_doc_ids = vectorstore.add_documents(parent_docs, bulk_size = 10000)
ids = parent_doc_ids

for i, doc in enumerate(splitted_docs):
    _id = parent_doc_ids[i]
    child_docs = child_splitter.split_documents([doc])
    for _doc in child_docs:
        _doc.metadata["parent_doc_id"] = _id
        _doc.metadata["doc_level"] = "child"

    contexualized_child_docs = []
    for _doc in child_docs:
        contexualized_child_docs.append(
            Document(
                page_content=contexualized_chunks[i]+"\n\n"+_doc.page_content,
                metadata=_doc.metadata
            )
        )
    child_docs = contexualized_child_docs

    child_doc_ids = vectorstore.add_documents(child_docs, bulk_size = 10000)        
    ids += child_doc_ids           

Knowledge Base ํ™œ์šฉ

์™„์ „ ๊ด€๋ฆฌํ˜• RAG ์„œ๋น„์Šค์ธ knowledge base๋Š” S3์™€ ๊ฐ™์€ storage์— ๋Œ€ํ•ด์„œ sync ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฏ€๋กœ์จ ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ OCR์ด๋‚˜ contextual embedding์„ ์ด์šฉํ•  ๊ฒฝ์šฐ์—๋Š” custom์œผ๋กœ lambda๋“ฑ์„ ํ™œ์šฉํ•˜์—ฌ ์ง์ ‘ ํŒŒ์‹ฑํ›„ ๋„ฃ์–ด์ฃผ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

MCP๋กœ RAG ํ™œ์šฉํ•˜๊ธฐ

AWS MCP (Knowledge Base)

Amazon Bedrock Knowledge Base Retrieval MCP Server์™€ ๊ฐ™์ด AWS์—์„œ ์ œ๊ณตํ•˜๋Š” MCP๋ฅผ ์ด์šฉํ•˜์—ฌ Amazon Knowledge Base์˜ ๋ฌธ์„œ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ ๋ณ„๋„๋กœ ์กฐํšŒํ•˜๋Š” ์ธํ”„๋ผ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์‰ฝ๊ฒŒ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ์˜ MCP ์„ค์ •์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{
  "mcpServers": {
    "awslabs.bedrock-kb-retrieval-mcp-server": {
      "command": "uvx",
      "args": ["awslabs.bedrock-kb-retrieval-mcp-server@latest"],
      "env": {
        "AWS_PROFILE": "your-profile-name",
        "AWS_REGION": "us-east-1",
        "FASTMCP_LOG_LEVEL": "ERROR",
        "KB_INCLUSION_TAG_KEY": "optional-tag-key-to-filter-kbs",
        "BEDROCK_KB_RERANKING_ENABLED": "false"
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}

AWS์˜ knowledge base MCP๋Š” knowledge base๋ฅผ ์กฐํšŒํ•˜์—ฌ ํŠน์ • tag(๊ธฐ๋ณธ์€ mcp-tag)๋ฅผ knowledge base๋ฅผ ์ฐพ์€ ๋‹ค์Œ์— query๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ knowledge base๋ฅผ ์ƒ์„ฑํ•  ๋•Œ์— ์•„๋ž˜์™€ ๊ฐ™์ด tag๋ฅผ ์„ค์ •ํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

image

Amazon Bedrock Knowledge Base Retrieval MCP Server์—์„œ๋Š” GetKnowledgeBases๋ฅผ resource๋กœ ํ˜ธ์ถœํ•˜๋Š”๋ฐ, LangGraph์—์„œ ์ฐธ์กฐ๊ฐ€ ์•ˆ๋˜๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ, ์—ฌ๊ธฐ์—์„œ๋Š” ์†Œ์Šค๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ mcp_server_knowledge_base.py์™€ ๊ฐ™์ด tool๋กœ ์ˆ˜์ •ํ•˜์—ฌ์„œ ํ™œ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ์˜ MCP ์„ค์ •์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{
    "mcpServers": {
        "knowledge_base_custom": {
            "command": "python",
            "args": [
                "application/mcp_server_knowledge_base.py"
            ],
            "env": {
                "KB_INCLUSION_TAG_KEY": "mcp-rag"
            }
        }
    }
}

MCP Lambda (Knowledge Base)

MCP๋กœ knowledge base๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด์„œ, lambda๋ฅผ ์ด์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž์˜ ๋ชฉ์ ์— ๋งž๋Š” RAG ๋™์ž‘์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์—์„œ๋Š” Lambda๋ฅผ ์ด์šฉํ•œ custom MCP ์„œ๋ฒ„๋ฅผ ์ •์˜ํ•˜๋Š”๊ฒƒ์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. Lambda๋ฅผ ์ด์šฉํ•ด knowledge base๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์€ lambda-knowledge-base์— ๊ด€๋ จ๋œ ์ฝ”๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด knowledge_base_search๋ฅผ tool๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

@mcp.tool()
def knowledge_base_search(keyword: str) -> list:
    """
    Search the knowledge base with the given keyword.
    keyword: the keyword to search
    return: the result of search
    """

    return rag.retrieve_knowledge_base(keyword)

knowledge_base_search๋Š” mcp_knowledge_base.py์— ์ •์˜๋œ retrieve_knowledge_base์™€ ๊ฐ™์ด lambda๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ knowledge base์˜ ๋ฌธ์„œ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

def retrieve_knowledge_base(query):
    lambda_client = boto3.client(
        service_name='lambda',
        region_name=bedrock_region
    )
    functionName = f"knowledge-base-for-{projectName}"

    mcp_env = utils.load_mcp_env()
    grading_mode = mcp_env['grading_mode']
    multi_region = mcp_env['multi_region']

    payload = {
        'function': 'search_rag',
        'knowledge_base_name': knowledge_base_name,
        'keyword': query,
        'top_k': numberOfDocs,
        'grading': grading_mode,
        'model_name': model_name,
        'multi_region': multi_region
    }

    output = lambda_client.invoke(
        FunctionName=functionName,
        Payload=json.dumps(payload),
    )
    payload = json.load(output['Payload'])        
    return payload['response']

Lambda๋กœ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ์ถ”๊ฐ€์ ์ธ ์ธํ”„๋ผ๊ฐ€ ํ•„์š”ํ•˜์ง€๋งŒ, grading์„ ํ†ตํ•ด ๊ด€๋ จ๋„๊ฐ€ ๋‚ฎ์€ ๋ฌธ์„œ๋ฅผ ์ œ์™ธํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ custom ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ณ , knowledge base ๋ฆฌ์ŠคํŠธ๋ฅผ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ query๋ฅผ ํ•˜๋ฏ€๋กœ ๋” ๋น ๋ฅธ ์‘๋‹ต์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

OpenSearch MCP

OpenSearch MCP๋ฅผ ์ด์šฉํ•˜๋ฉด ์ถ”๊ฐ€์ ์ธ ๋ฆฌ์†Œ์Šค ์—†์ด ๋ฐ”๋กœ OpenSearch๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ 2025๋…„ 6์›” ํ˜„์žฌ๋Š” ํ…์ŠคํŠธ ๊ฒ€์ƒ‰๋งŒ์„ ์ œ๊ณตํ•˜์—ฌ ์„ฑ๋Šฅ์ƒ ์ œํ•œ์ด ์žˆ์Šต๋‹ˆ๋‹ค. OpenSearch MCP๋ฅผ ์ด์šฉํ•  ๋•Œ์—๋Š” ์•„๋ž˜ config๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค.

{
    "mcpServers": {
        "opensearch-mcp-server": {
            "command": "uvx",
            "args": [
                "opensearch-mcp-server-py"
            ],
            "env": {
                "OPENSEARCH_URL": managed_opensearch_url,
                "AWS_REGION": aws_region,
                "AWS_ACCESS_KEY_ID": credentials.access_key,
                "AWS_SECRET_ACCESS_KEY": credentials.secret_key
            }
        }
    }
}

MCP Lambda (OpenSearch)

OCR, contextual embedding๊ณผ ๊ฐ™์€ customํ•œ RAG๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ์—๋Š” ์ง์ ‘ OpenSearch์— ๋„ฃ๊ณ  ์กฐํšŒํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์กฐํšŒํ•  ๋•Œ์—๋Š” lambda๋กœ custom MCP ์„œ๋ฒ„๋ฅผ ์ •์˜ํ•˜๊ฑฐ๋‚˜ OpenSearch MCP์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Lambda๋กœ OpenSearch๋ฅผ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์€ lambda-opensearch์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ custom MCP ์„œ๋ฒ„๋กœ ๊ตฌํ˜„ํ•  ๋•Œ์—๋Š” mcp_server_lambda_opensearch.py์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

@mcp.tool()
def opensearch_search(keyword: str) -> list:
    """
    Search the knowledge base with the given keyword.
    keyword: the keyword to search
    return: the result of search
    """

    return rag.retrieve_opensearch(keyword)

์—ฌ๊ธฐ์„œ mcp_opensearch.py์™€ ๊ฐ™์ด lambda-opensearch๋กœ ์ง์ ‘ ์š”์ฒญํ•˜์—ฌ Lambda๊ฐ€ ๊ฐ€์ ธ์˜จ OpenSearch ๋ฌธ์„œ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Lambda์—์„œ๋Š” OpenSearch๋กœ ๋ฌธ์„œ ์กฐํšŒ๋ฟ ์•„๋‹ˆ๋ผ, ๋ฌธ์„œ์˜ ๊ด€๋ จ๋„๋ฅผ ๊ธฐ์ค€์œผ๋กœ grading์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

def retrieve_opensearch(query):
    lambda_client = boto3.client(
        service_name='lambda',
        region_name=bedrock_region
    )
    functionName = f"opensearch-for-{projectName}"

    mcp_env = utils.load_mcp_env()
    grading_mode = mcp_env['grading_mode']
    multi_region = mcp_env['multi_region']

    payload = {
        'function': 'search_rag',
        'keyword': query,
        'top_k': numberOfDocs,
        'grading': grading_mode,
        'model_name': model_name,
        'multi_region': multi_region
    }
    output = lambda_client.invoke(
        FunctionName=functionName,
        Payload=json.dumps(payload),
    )
    payload = json.load(output['Payload'])        
    return payload['response']

AgentCore Gateway

AgentCore Gateway๋Š” Lambda๋กœ MCP์„œ๋ฒ„๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. config.json.sample์„ ๋ณต์‚ฌํ•˜์—ฌ config.json ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

cd gateway/opensearch-retriever && cp config.json.sample config.json

config.json์—์„œ ์•„๋ž˜์™€ ๊ฐ™์ด project name๊ณผ opensearch url ๋ฐ CloudFront์˜ domain์ธ sharing_url์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ •๋ณด๋“ค์€ cdk ๋ฐฐํฌ์‹œ์— ์ƒ์„ฑ๋œ output์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

{
  "projectName": "mcp-rag",
  "opensearch_url": "https://search-mcp-rag-mxtkul2z3qv5iiqprb7q3jx4wy.us-west-2.es.amazonaws.com",
  "sharing_url": "https://d20lfnyi6fvd87.cloudfront.net"
}

์•„๋ž˜ ๋ฐฉ์‹์œผ๋กœ AgentCore์— "mcp-rag"๋ผ๋Š” gateway๋ฅผ ์œ„ํ•œ role์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

python create_gateway_role.py

์ด๋•Œ์˜ ๊ฒฐ๊ณผ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

image

์•„๋ž˜์™€ ๊ฐ™์ด AgentCore Gateway๋ฅผ ์„ค์น˜ํ•˜๊ณ  target์œผ๋กœ "opensearch-retriever"๋ฅผ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ OpenSearch๋ฅผ ์กฐํšŒํ•˜๋Š” RAG์šฉ Lambda๊ฐ€ ์—†๋‹ค๋ฉด ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

python create_gateway_tool.py

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์œผ๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๋™์ž‘์„ ํ…Œ์ŠคํŠธ ํ•ฉ๋‹ˆ๋‹ค.

test_mcp_remote.py

์ด๋•Œ์˜ ๊ฒฐ๊ณผ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. secret์˜ bearer token์ด expire๋˜๋ฉด ๊ฐฑ์‹ ํ›„ ์ ‘์†์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ list_tools๋กœ available tools์— ๋Œ€ํ•ด ํ™•์ธํ›„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

image

์„ค์น˜ํ•˜๊ธฐ

Repository๋ฅผ clone ํ•ฉ๋‹ˆ๋‹ค.

git clone https://github.com/kyopark2014/mcp-rag/

ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

cd mcp-rag && pip install -r requirements.txt

CDK๋กœ ๊ตฌ๋™์ด ํ•„์š”ํ•œ ์ธํ”„๋ผ์ธ CloudFront, S3, OpenSearch, Knowledge base, tavily, weather๋“ฑ์˜ secret์„ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ cdk boootstraping์ด ์•ˆ๋˜์–ด ์žˆ๋‹ค๋ฉด ์„ค์น˜ํ›„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

cd cdk-mcp-rag/ && cdk deploy --all

์„ค์น˜๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด "CdkMcpRagStack.environmentformcprag"๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ application/config.json ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

image

config.json์€ agent์˜ ๋™์ž‘์— ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , .gitignore์— ์˜ํ•ด git์œผ๋กœ ๊ณต์œ  ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ƒ์„ฑ๋œ config.json์˜ ์…ˆํ”Œ์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

{
    "projectName":"mcp-rag",
    "accountId":"862926741992",
    "region":"us-west-2",
    "roleKnowledgeBase":"arn:aws:iam::862926741992:role/role-knowledge-base-for-mcp-rag-us-west-2",
    "collectionArn":"arn:aws:aoss:us-west-2:862926741992:collection/8krsnuq4it9gpl70i3u6",
    "serverless_opensearch_url":"https://8krsnuq4it9gpl70i3u6.us-west-2.aoss.amazonaws.com",
    "managed_opensearch_url":"https://search-mcp-rag-mxtkul3z3qv5iiqprb7q3jx4wy.us-west-2.es.amazonaws.com",
    "knowledge_base_role":"arn:aws:iam::862926741992:role/role-knowledge-base-for-mcp-rag-us-west-2",
    "s3_bucket":"storage-for-mcp-rag-862926741992-us-west-2",
    "s3_arn":"arn:aws:s3:::storage-for-mcp-rag-862926741992-us-west-2",
    "sharing_url":"https://d3mo4kqj5cjiuy.cloudfront.net"
 }

์ดํ›„ Secret Manager์— ์ ‘์†ํ•˜์—ฌ ์•„๋ž˜์™€ ๊ฐ™์€ credential์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค.

image

๋งŒ์•ฝ streamlit์ด ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด streamlit์„ ์ฐธ์กฐํ•˜์—ฌ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. ์ดํ›„ ์•„๋ž˜์™€ ๊ฐ™์ด ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

streamlit run application/app.py

์‹คํ–‰ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ํ™”๋ฉด์ด ๋ณด์—ฌ์ง‘๋‹ˆ๋‹ค. Agent๋ฅผ ์„ ํƒํ•˜๋ฉด ์‹คํ–‰ํ•˜๊ณ  ๋™์ž‘์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹คํ–‰ ๊ฒฐ๊ณผ

์—ฌ๊ธฐ์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ MCP ์„œ๋ฒ„๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

image

"AWS MCP (Knowledge Base)"์„ ์„ ํƒํ•˜๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด GetKnowledgeBases์œผ๋กœ mcp-rag๋ผ๋Š” tag๋ฅผ ๊ฐ€์ง„ knowledge base๋ฅผ ๊ฒ€์ƒ‰ํ•œ ํ›„์— QueryKnowledgeBases๋กœ ๊ฒ€์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

"MCP Lambda (Knowledge Base)"๋ฅผ ์„ ํƒํ•˜๊ณ  ๊ฒ€์ƒ‰ํ•˜์—ฌ ์•„๋ž˜์™€ ๊ฐ™์ด knowledge_base_search๋ฅผ ์ด์šฉํ•ด ๋ฌธ์„œ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

"OpenSearch MCP"๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด OpenSearch MCP๋ฅผ ์ด์šฉํ•ด ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. Text ๊ฒ€์ƒ‰์ด๋ฏ€๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฒฐ๊ณผ๊ฐ€ ์•ˆ๋‚˜์˜ฌ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

"OpenSearch MCP"์—์„œ "๋ณด์ผ๋Ÿฌ ์ฝ”๋“œ?"์™€ ๊ฐ™์ด ๊ฒ€์ƒ‰ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฒ€์ƒ‰์— ์„ฑ๊ณตํ•ฉ๋‹ˆ๋‹ค.

"MCP Lambda (OpenSearch)"๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด opensearch_search๋ฅผ ์ด์šฉํ•˜์—ฌ OpenSearch๋ฅผ ๊ฒ€์ƒ‰ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Reference

๊ธฐ์—…์šฉ RAG๋Š” ์™œ ์‹คํŒจํ•˜๋Š”๊ฐ€โ€ฆ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ™˜๊ฒฝ์—์„œ RAG๋ฅผ ํ™•์žฅํ•˜๋Š” ๋ฒ•