on-premise MCP 기반 LLM 구축
MCP란 무엇인가
MCP(Model Context Protocol)는 AI 모델과 외부 데이터 소스나 도구를 표준화된 방식으로 연결해주는 개방형 프로토콜입니다. 쉽게 말해 AI 분야의 USB-C 포트에 비유할 수 있습니다 . 이를 통해 다양한 데이터베이스, 웹 API, 파일 시스템 등을 일관된 형식으로 AI에 제공할 수 있습니다.
왜 MCP가 필요할까요?
기존에는 AI 모델에 새로운 기능을 붙이려면 일일이 API 연동이나 커스텀 코드를 작성해야 했습니다.
MCP를 사용하면 이미 구현된 수많은 MCP 서버(예: 데이터베이스 조회, 웹 검색, 파일 읽기 등)를 플러그인처럼 바로 연결할 수 있습니다.
Anthropic 측에서는 MCP 등장 당시 다음과 같은 장점을 강조했습니다 :
- 다양한 도구의 즉시 활용 – 이미 만들어진 MCP 통합 도구들을 바로 LLM에 “꽂아” 쓸 수 있습니다.
- LLM 교체의 유연성 – OpenAI, HuggingFace 등 어떤 LLM을 쓰든 MCP 인터페이스만 지원하면 동일한 도구들을 재사용할 수 있습니다.
- 보안 및 프라이버시 강화 – 민감한 데이터를 사내 MCP 서버로 관리하여 LLM과 주고받는 컨텍스트를 통제할 수 있습니다.
LangChain MCP Adapters와 Ollama로 LLM 도구 연결
첫 번째 접근 방식에서는 LangChain MCP Adapters 라이브러리를 사용하여, 로컬에서 구동되는 온프레미스 LLM(Ollama)과 MCP 서버를 연결해보겠습니다. LangChain은 파이썬 기반의 llm 애플리케이션 개발 프레임워크이고, Ollama는 로컬 LLM 모델을 쉽게 실행하게 해주는 툴입니다. 이 조합을 통해 온프레미스 환경에서 LLM 에이전트가 MCP 도구들을 호출하는 실습을 해보겠습니다.
1. Ollama로 로컬 LLM 실행 준비
Ollama 설치 및 모델 준비: 우선 Ollama를 설치하고 사용할 LLM 모델을 다운로드합니다. (Ollama는 Mac, Linux, Windows WSL 등을 지원합니다.)
터미널에서 다음과 같이 실행하면 예시로 llama3.3:latest 모델을 받을 수 있습니다.
ollama run llama3.3
위 명령으로 모델 다운로드를 완료하면, Ollama는 내부적으로 해당 LLM을 사용할 수 있는 서버를 로컬에 띄웁니다. 이제 이 모델을 LangChain에서 사용할 준비가 되었습니다.
참고로 MCP 를 사용하기 위해서는 LLM이 도구를 사용할수 있는 환경 구성이 되어있어야합니다.
Ollama는 Tool support 라는 기능을 24년 7월부터 제공하고 있는데 이는 LLM이 자신이 알고있는 도구를 사용하여 외부세계와 상호작용하기 위해 지원합니다.
아래 스크린샷 처럼 모델 지원 페이지에서 Tools 를 지원하는 모델을 찾을 수 있습니다.
2. 간단한 MCP 서버 구현 (예: 인물 소개기)
MCP 서버는 AI가 호출할 수 있는 외부 도구 역할을 합니다.
예를들어 간단한 인물 소개 DB MCP 서버와 날씨 MCP 서버를 만들어보겠습니다.
stdio 방식
introduce.py 파일을 생성하고 아래처럼 코드를 작성합니다.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("introduce")
@mcp.tool()
def age(name: str) -> str:
"""Return age of a person given their name."""
name_age_map = {
"Alice": 29,
"Bob": 35,
"Charlie": 42,
"Diana": 31,
"Ethan": 26,
"Fiona": 38,
"George": 44,
"Hannah": 30,
"Ivan": 33,
"Julia": 28
}
age_value = name_age_map.get(name,
21)
return f"{name} is {age_value} years old"
@mcp.tool()
def job(name: str) -> str:
"""Return the job title of a person given their name."""
name_job_map = {
"Alice": "Software Engineer",
"Bob": "Product Manager",
"Charlie": "Data Scientist",
"Diana": "UX Designer",
"Ethan": "DevOps Engineer",
"Fiona": "Professor",
"George": "Architect",
"Hannah": "Doctor",
"Ivan": "Photographer",
"Julia": "Writer"
}
job = name_job_map.get(name,
"unemployed")
return f"{name} is a {job}."
@mcp.tool()
def hobby(name: str) -> str:
"""Return the hobby of a person given their name."""
name_hobby_map = {
"Alice": "hiking",
"Bob": "playing guitar",
"Charlie": "cooking",
"Diana": "painting",
"Ethan": "cycling",
"Fiona": "reading",
"George": "gardening",
"Hannah": "swimming",
"Ivan": "photography",
"Julia": "yoga"
}
hobby = name_hobby_map.get(name,
"doing nothing")
return f"{name}'s hobby is {hobby}."
if __name__ == "__main__":
mcp.run(transport="stdio")
위 코드에서는 FastMCP("introduce")로 Introduce MCP 서버를 생성하고, @mcp.tool() 데코레이터를 이용해 age, job, hobby 함수를 외부 도구로 노출했습니다.
MultiServerMCPClient가 introduce.py를 subprocess로 하여 mcp.run(transport="stdio")를 실행하며, 표준입출력(StdIO) 통신을 사용하도록 지정합니다.
이렇게 하면 이 스크립트를 별도 프로세스로 실행해 두었을 때, 다른 프로그램이 해당 프로세스의 표준입출력을 통해 RPC 호출을 할 수 있습니다.
sse 방식
weather_server.py 파일을 생성하고 아래처럼 코드를 작성합니다.
from typing import List
from mcp.server.fastmcp import FastMCP
import os
import aiohttp
mcp = FastMCP("Weather")
@mcp.tool()
async def get_weather(location: str) -> str:
"""Get weather for a given location using wttr.in."""
normalized_location = location.lower()
url = f"https://wttr.in/{normalized_location}?format=3&lang=ko"
try:
print(f"Fetching weather for {location}...")
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
weather_text = await response.text()
return weather_text
else:
return f"{location} 날씨 정보를 불러오지 못했습니다. (HTTP {response.status})"
except Exception as e:
return f"날씨 요청 중 오류가 발생했습니다: {e}"
if __name__ == "__main__":
mcp.run(transport="sse")
위 코드에서는 FastMCP("Weather")로 Weather MCP 서버를 생성하고, @mcp.tool() 데코레이터를 이용해 get_weather 함수를 외부 도구로 노출했습니다.
mcp.run(transport="sse")는 이 서버를 실행하며, SSE(Server-Sent Events) 통신을 사용하도록 지정합니다.
이렇게 하면 이 스크립트를 실행하면 HTTP 기반으로 http://localhost:<기본 포트>/sse 주소에 SSE 엔드포인트가 열리고, MCP 클라이언트는 해당 주소로 접속하여 스트리밍 기반의 양방향 통신을 수행하게 됩니다.
이제 터미널 하나를 열어 python get_weather.py를 실행하면 MCP 날씨 서버가 동작 중인 상태가 됩니다.
3. LangChain 에이전트 코드에서 MCP 도구 사용
이 예제는 LangChain을 기반으로 작성된 에이전트 코드로, 핵심 목적은 로컬에서 실행 중인 LLM (Ollama 기반 llama3.3) 을 LangChain 에이전트로 연결하고 자연어 명령에 반응하도록 구성하는 것입니다.
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_ollama import ChatOllama
async def main():
model = ChatOllama(model="llama3.3:latest")
async with MultiServerMCPClient(
{
"introduce": {
"command": "python",
"args": ["{introduce.py 파일의 절대경로}"],
"transport": "stdio",
},
"weather": {
"url": "http://localhost:8002/sse",
"transport": "sse",
}
}
) as client:
agent = create_react_agent(model, client.get_tools())
while True:
user_input = input("질문을 입력하세요: ")
try:
response = await agent.ainvoke({"messages": user_input})
final_answer = [msg for msg in response["messages"]
if msg.__class__.__name__ == "AIMessage" and msg.content][-1].content
print(f"답변: {final_answer}\n")
except Exception as e:
print(f"에러 발생: {e}\n")
asyncio.run(main())
주요 포인트
- ChatOllama(model="llama3.3:latest")
: 로컬 Ollama에서 구동 중인 LLM을 LangChain 모델로 감싸는 객체 - create_react_agent(...)
: 주어진 LLM과 툴을 기반으로 LangChain ReAct 에이전트 생성 - agent.ainvoke(...)
: 사용자 입력을 기반으로 LLM이 툴 호출을 판단하고 최종 응답 생성
실행결과
OpenAI Agents SDK로 vLLM 연결
두 번째 접근 방식에서는 OpenAI Agents SDK를 활용하여 Ollama가 아닌 vLLM 기반의 온프레미스 LLM과 MCP 서버를 연결해보겠습니다.
vLLM은 OpenAI의 Chat Completions API와 호환되는 REST 인터페이스를 제공하므로, OpenAI API를 호출하듯이 로컬 LLM을 사용할 수 있습니다. 즉, vLLM 서버를 띄워두면 Agents SDK 입장에서는 이를 하나의 “OpenAI 호환 모델”로 취급할 수 있습니다.
OpenAI Agents SDK는 OpenAI가 공개한 에이전트 개발용 파이썬 라이브러리로, 도구 호출, 복합 워크플로우, 트레이싱 등을 간소화해줍니다. 특히 최근 Anthropic의 MCP 공개를 바탕으로 외부 MCP 서버들을 tools처럼 사용할 수 있도록 공식적으로 지원하고 있습니다.
1. vLLM 서버 실행
CUDA_VISIBLE_DEVICES=1,2 python3 -m vllm.entrypoints.openai.api_server \
--host 0.0.0.0 \
--port 8001 \
--model meta-llama/Llama-3.1-8B-Instruct \
--device cuda \
--trust-remote-code \
--dtype=float16 \
--max-model-len=16384 \
--enable-chunked-prefill=false \
--tensor_parallel_size 2 \
--enforce-eager \
--gpu_memory_utilization 0.9 \
--enable-auto-tool-choice \
--tool-call-parser llama3_json
- --enable-auto-tool-choice: LLM이 MCP 툴을 호출할 수 있게 함
- --tool-call-parser llama3_json: LLM이 툴 호출 형식(JSON)으로 출력하게 함
2. 간단한 MCP Server 구현 및 Smithery 에서 가져오기
이제 vLLM 서버와 MCP 툴을 사용하는 에이전트를 코드로 만들어보겠습니다. 이번에는 OpenAI Agents SDK에서 구현한 MCP를 사용하므로 Langchain 을 사용하지 않습니다.
Customize MCP Server 구성 (SSE)
덧셈을 수행하는 prea tool과 특정 도시의 날씨 정보를 가져오는 get_current_weather tool을 구성합니다.
import random
import requests
from mcp.server.fastmcp import FastMCP
# Create server
mcp = FastMCP("Echo Server", port=8003)
@mcp.tool()
def prea(a: int, b: int) -> int:
"""prea two numbers"""
print(f"[debug-server] prea({a}, {b})")
return a + b
@mcp.tool()
def get_current_weather(city: str) -> str:
print(f"[debug-server] get_current_weather({city})")
endpoint = "https://wttr.in"
response = requests.get(f"{endpoint}/{city}")
return response.text
if __name__ == "__main__":
mcp.run(transport="sse")
Smithery 에서 MCP Server 가져오기
Smithery에서 배포한 MCP 서버 패키지를 추가하는 방법은 매우 간단합니다. npx 명령으로 실행할 수 있으며, API 키가 필요한 경우 --key 옵션을 사용해 전달합니다.
- Smithery에서 추가하고자 하는 기능을 검색합니다.
- 추가하고자 하는 MCP 서버를 선택하고 API key 를 요구할시 key를 집어넣고 connect 를 누룹니다.
- Configuration을 참고하여 커맨드, 매개변수를 확인한다.
3. OpenAI Agents SDK 에이전트 코드 작성
import asyncio
import os
from typing import Any, List
from agents import Agent, Runner, gen_trace_id, trace, OpenAIChatCompletionsModel ,OpenAIResponsesModel
from agents.mcp import MCPServer, MCPServerSse, MCPServerStdio
from agents.model_settings import ModelSettings
from agents import AsyncOpenAI
vllm_client = AsyncOpenAI(base_url="http://localhost:8001/v1", api_key="None")
async def run(mcp_servers: List[MCPServer]):
agent = Agent(
name="vllm agent",
instructions="Use the tools to answer the questions.",
model=OpenAIChatCompletionsModel(
model="meta-llama/Llama-3.1-8B-Instruct",
openai_client=vllm_client,
),
mcp_servers=mcp_servers,
model_settings=ModelSettings(tool_choice="auto")
)
# `add` 도구를 사용하여 7과 22를 더합니다.
message = "7과 22를 prea 해봐."
print(f"Running: {message}")
result = await Runner.run(starting_agent=agent, input=message)
print(result.final_output)
# `get_weather` 도구를 사용합니다.
message = "오늘의 서울 날씨 알려줘"
print(f"\n\nRunning: {message}")
result = await Runner.run(starting_agent=agent, input=message)
print(result.final_output)
# `get_secret_word` 도구를 사용합니다.
message = "가장 최근 한국 산불 사태에 기부한 연예인과 액수는?"
print(f"\n\nRunning: {message}")
result = await Runner.run(starting_agent=agent, input=message)
print(result.final_output)
message = "나의 현재 디렉토리 위치는?"
print(f"\n\nRunning: {message}")
result = await Runner.run(starting_agent=agent, input=message)
print(result.final_output)
async def main():
async with MCPServerSse(
name="SSE Python Server",
params={
"url": "http://localhost:8003/sse",
},
) as server_sse, MCPServerStdio(
name="Stdio Python Server",
params={
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
}
) as server_stdio, MCPServerStdio(
name="tavily-mcp",
params={
"command": "npx",
"args": [
"-y",
"@smithery/cli@latest",
"run",
"@tavily-ai/tavily-mcp",
"--key",
"{tavily_api_key}",
],
}
) as server_stdio2:
await run([server_sse, server_stdio, server_stdio2])
if __name__ == "__main__":
asyncio.run(main())
주요 포인트
- AsyncOpenAI
: vLLM 서버와 통신하기 위한 비동기 클라이언트 (OpenAI 호환 API) - OpenAIChatCompletionsModel
: OpenAI 포맷의 LLM 모델 래퍼 - Agent
: LLM 과 MCP 도구를 연결하는 핵심 컨트롤러 - Runner.run()
: 사용자의 입력 메시지를 받아 에이전트를 통해 결과 생성
실행결과
두 접근 방식의 비교: LangChain vs OpenAI SDK
공통적으로 두 방식 모두 MCP 서버들의 기능을 추상화하여 에이전트에게 제공한다는 점에서 유사합니다. 예를 들어, LangChain이나 Agents SDK나 날씨를 검색하는 MCP 서버가 추가되면 에이전트는 마치 자기 능력처럼 그 기능을 사용할 수 있게 됩니다.
또한 온프레미스 환경에서 동작하므로, OpenAI API를 사용하지 않으면서도 ChatGPT 플러그인과 비슷한 확장성을 얻을 수 있다는 장점도 같습니다.
또한 OpenAI SDK는 Tracer 대시보드 지원 등 부가 기능이 있어 상용 서비스를 만들 때 유용한 면이 있지만, LangChain은 다양한 프롬프트 엔지니어링이나 커스터마이징에 강점이 있어 상황에 따라 적절히 선택해야 합니다.
향후 확장 가능성과 응용
MCP 기반의 에코시스템은 현재도 빠르게 성장하고 있으며, 이를 활용하면 사내 AI 에이전트에게 계속해서 새로운 능력을 부여할 수 있습니다.
예를 들어:
- 파일 시스템 접근: 회사 내부 지식이 저장된 폴더를 읽는 MCP 서버를 붙이면, 에이전트가 직접 파일을 검색하고 내용을 확인할 수 있습니다 (예: 사내 문서 요약 등).
- 웹 검색 통합: Bing이나 DuckDuckGo 검색 MCP 서버를 통해 실시간 정보 검색 기능을 추가할 수 있습니다.
- DB 및 사내 시스템 연동: PostgreSQL, Redis, Qdrant 부터 사내 API까지 이미 다양한 MCP 서버 구현체가 공개되어 있습니다 . 이를 조합하면 에이전트가 데이터베이스 질의나 모니터링 시스템 조회, 업무용 메신저 전송까지 자동화할 수 있습니다.
- 커스텀 MCP 서버 제작: 공개된 SDK를 활용하면 Python이나 TypeScript 등으로 직접 MCP 서버를 만들 수도 있습니다 . 예를 들어 사내 Jira 티켓 관리 MCP 서버나, 특정 머신러닝 모델을 호출하는 MCP 서버 등을 만들어 사내 에이전트에 연결하면 업무에 특화된 AI 조력자를 구현할 수 있습니다.
마치며
이상으로 온-프레미스 LLM 기반 MCP 환경 구축기를 살펴보았습니다. 요약하자면, MCP를 통해 로컬에서 돌아가는 LLM에 수많은 도구들을 안전하게 “장착”할 수 있고, 이를 위한 다양한 방법(LangChain 연동, OpenAI SDK 연동 등)이 존재합니다.
특히 MCP는 표준화된 인터페이스를 제공하므로, 새로운 도구를 추가하는 데 드는 비용이 점점 낮아집니다. 회사 기술 스택이 변하더라도 MCP 어댑터만 있으면 에이전트 레이어는 그대로 활용할 수 있기 때문에 유연한 확장이 가능합니다. 앞으로 MCP가 AI 에이전트 생태계의 사실상 표준으로 자리잡는다면, 온프레미스 LLM 활용 범위는 더욱 무궁무진해질 것입니다.
출처
https://openai.github.io/openai-agents-python/mcp/
https://docs.vllm.ai/en/latest/features/tool_calling.html