
SSE는 서버-센트 이벤트(Server-Sent Events)의 약자로, 웹 브라우저(클라이언트)가 서버로부터 자동으로 업데이트를 푸시(push)받을 수 있도록 하는 웹 기술입니다.
서버에서 클라이언트로의 단방향 통신을 위해 설계되었으며, HTTP 프로토콜 위에서 동작합니다. 클라이언트가 한번 연결을 요청하면, 서버는 그 연결을 계속 열어두고 있다가 새로운 데이터나 이벤트가 발생할 때마다 클라이언트로 데이터를 보낼 수 있습니다.
주요 특징
단방향 통신 (서버 → 클라이언트): 서버에서 클라이언트로만 데이터를 전송합니다. 클라이언트가 서버에 데이터를 보낼 필요 없이, 업데이트만 받으면 되는 경우에 이상적입니다.
자동 재연결: 네트워크 문제 등으로 연결이 끊어지면 브라우저가 자동으로 재연결을 시도하는 표준 기능이 내장되어 있습니다.
간단한 구현: WebSocket에 비해 구현이 훨씬 간단합니다. 클라이언트 측에서는 자바스크립트의 EventSource API를 사용해 쉽게 구현할 수 있습니다.
용도: 실시간 뉴스 피드, 스포츠 경기 스코어 업데이트, 주식 시세 알림, 채팅 알림 등 서버로부터 지속적인 업데이트가 필요한 기능에 주로 사용됩니다.
stdio는 표준 입출력(Standard Input/Output)의 약자로, C 언어와 같은 시스템 프로그래밍 언어에서 사용되는 핵심 라이브러리입니다.
주요 특징
로컬 입출력: 네트워크 통신이 아닌, 프로그램이 실행되는 로컬 시스템의 자원(키보드, 화면, 파일 등)과의 데이터 교환을 다룹니다.
스트림(Stream): 데이터의 흐름을 ‘스트림’이라는 개념으로 추상화하여 다룹니다. 대표적인 표준 스트림으로는 다음 세 가지가 있습니다.
stdin (Standard Input): 표준 입력 (기본값: 키보드)
stdout (Standard Output): 표준 출력 (기본값: 화면)
stderr (Standard Error): 표준 오류 출력 (기본값: 화면)
파일 처리: fopen, fread, fwrite, fclose 등의 함수를 통해 파일 시스템의 파일을 읽고 쓰는 기능을 제공합니다.
용도: 명령줄 인터페이스(CLI) 애플리케이션, 시스템 유틸리티, 파일 처리 프로그램 등 거의 모든 종류의 로컬 애플리케이션 개발에 기본적으로 사용됩니다.
| 구분 | SSE (Server-Sent Events) | stdio (Standard Input/Output) |
|---|---|---|
| 분야 | 웹 기술 (Web Technology) | 프로그래밍 언어 라이브러리 (C Library) |
| 목적 | 서버 → 클라이언트 간의 실시간 데이터 푸시 | 프로그램과 로컬 자원(키보드, 화면, 파일) 간 데이터 입출력 |
| 통신 방식 | 네트워크 통신 (HTTP 기반, 단방향) | 로컬 I/O (시스템 내부 처리) |
| 사용 환경 | 웹 브라우저와 웹 서버 간 | 운영체제 내에서 실행되는 응용 프로그램 |
| 주요 구성 요소 | EventSource API, HTTP 프로토콜 | stdin, stdout, stderr, FILE 포인터 |
| 대표적 사용 사례 | 실시간 알림, 뉴스 피드, 라이브 스코어 | 명령줄 도구(CLI), 텍스트 편집기, 파일 변환기 |
| 장점 | - 웹 기반 환경에서 사용 가능 - 네트워크를 통한 원격 접근 가능 - 실시간 스트리밍 지원 | - 구현이 매우 간단함 - 로컬 환경에서 빠른 통신 속도 - 설정이 최소화됨 |
| 단점 | - 구현이 상대적으로 복잡함 - HTTP 서버 설정 필요 - 네트워크 오버헤드 존재 | - 네트워크 통신 불가 - 웹 환경에서 사용이 제한적임 |
현재 진행 중인 MCP(Model Context Protocol) 프로젝트 관점에서 이 두 방식을 선택하는 기준은 다음과 같습니다.
stdio 방식 선택 시
Claude Desktop과 같은 로컬 앱에 직접 MCP 서버를 연결할 때 사용합니다.
가장 빠르고 설정이 간편하며, 보안상 외부 네트워크 노출이 필요 없을 때 최적입니다.
SSE 방식 선택 시
멀리 떨어진 서버에 에이전트 도구를 배포하거나, 여러 클라이언트가 동시에 MCP 서버에 접속해야 할 때 사용합니다.
웹 기반 UI(예: 현재 프로젝트의 React 프론트엔드)에서 실시간으로 도구 실행 로그를 받아올 때 유리합니다.
MCP 서버는 AI 모델이 외부 리소스나 도구에 접근할 수 있도록 해주는 서버입니다.
사전 준비
pip install mcp
stdio 방식은 표준 입출력(Standard I/O)을 통해 통신하는 가장 간단한 방식입니다. 주로 Local 시스템 내에 연동에 사용됩니다.
MCP Client가 설치된 Local System 내에 MCP Server가 존재할 때, 활용할 수 있습니다.
# stdio_server.py
import asyncio
import sys
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
# 서버 인스턴스 생성
server = Server("my-mcp-server")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
"""사용 가능한 도구 목록을 반환합니다."""
return [
types.Tool(
name="calculator",
description="간단한 수학 계산을 수행합니다",
inputSchema={
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "계산할 수학 표현식 (예: 2+2)"
}
},
"required": ["expression"]
}
),
types.Tool(
name="echo",
description="입력된 메시지를 그대로 반환합니다",
inputSchema={
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "반환할 메시지"
}
},
"required": ["message"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
"""도구 호출을 처리합니다."""
if name == "calculator":
try:
expression = arguments["expression"]
# 보안을 위해 간단한 검증
if any(char in expression for char in ['import', 'exec', 'eval', '__']):
raise ValueError("허용되지 않는 표현식입니다")
result = eval(expression)
return [types.TextContent(
type="text",
text=f"계산 결과: {expression} = {result}"
)]
except Exception as e:
return [types.TextContent(
type="text",
text=f"계산 오류: {str(e)}"
)]
elif name == "echo":
message = arguments["message"]
return [types.TextContent(
type="text",
text=f"Echo: {message}"
)]
else:
raise ValueError(f"알 수 없는 도구: {name}")
async def main():
"""메인 실행 함수"""
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
FastMCP의 장점
- 코드량 감소: list_tools와 call_tool 내부의 if-else 분기 로직이 사라졌습니다. 데코레이터가 이를 자동화합니다.
- 타입 힌트 활용: FastMCP는 Python의 타입 힌트(str, int 등)를 읽어서 자동으로 MCP용 inputSchema를 생성합니다. JSON Schema를 직접 작성할 필요가 없습니다.
- Docstring 활용: 함수 아래의 독스트링("""...""")이 자동으로 도구의 설명(Description)으로 변환되어 에이전트에게 전달됩니다.
- 일관된 인터페이스: 비즈니스 로직(함수)은 그대로 두고, 마지막 mcp.run()의 transport 값만 바꾸는 것으로 로컬 전용(stdio)과 웹 서비스용(SSE)을 자유롭게 오갈 수 있습니다.
# fastmcp_stdio.py
from mcp.server.fastmcp import FastMCP
# 서버 인스턴스 생성
mcp = FastMCP("My FastMCP Stdio Server")
@mcp.tool()
def calculator(expression: str) -> str:
"""간단한 수학 계산을 수행합니다. (예: 2+2)"""
try:
# 보안을 위한 기초 검증
if any(char in expression for char in ['import', 'exec', 'eval', '__']):
return "오류: 허용되지 않는 표현식입니다."
result = eval(expression)
return f"계산 결과: {expression} = {result}"
except Exception as e:
return f"계산 오류: {str(e)}"
@mcp.tool()
def echo(message: str) -> str:
"""입력된 메시지를 그대로 반환합니다."""
return f"Echo: {message}"
if __name__ == "__main__":
# transport를 stdio로 설정 (기본값)
mcp.run(transport="stdio")
python stdio_server.py
SSE 방식은 HTTP를 통해 실시간 스트리밍 통신을 하는 방식입니다. 웹 기반 환경에서 유용합니다.
SSE 방식을 위한 추가 의존성 설치
pip install starlette uvicorn
# sse_server.py
import asyncio
import json
from mcp.server import Server
from mcp.server.sse import SseServerTransport
from mcp import types
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Route
import uvicorn
# 서버 인스턴스 생성
server = Server("my-sse-mcp-server")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
"""사용 가능한 도구 목록을 반환합니다."""
return [
types.Tool(
name="weather",
description="날씨 정보를 가져옵니다 (모의 데이터)",
inputSchema={
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "도시 이름"
}
},
"required": ["city"]
}
),
types.Tool(
name="timestamp",
description="현재 시간을 반환합니다",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
"""도구 호출을 처리합니다."""
if name == "weather":
city = arguments["city"]
# 실제로는 날씨 API를 호출하겠지만, 여기서는 모의 데이터 사용
weather_data = {
"서울": "맑음, 22°C",
"부산": "흐림, 25°C",
"대구": "비, 18°C"
}
weather = weather_data.get(city, f"{city}의 날씨 정보를 찾을 수 없습니다")
return [types.TextContent(
type="text",
text=f"{city} 날씨: {weather}"
)]
elif name == "timestamp":
from datetime import datetime
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return [types.TextContent(
type="text",
text=f"현재 시간: {now}"
)]
else:
raise ValueError(f"알 수 없는 도구: {name}")
# SSE 엔드포인트 설정
async def handle_sse(request):
"""SSE 연결을 처리합니다."""
transport = SseServerTransport("/message")
async def message_handler():
async for message in request.stream():
data = await message
# 메시지 처리 로직
yield f"data: {json.dumps({'response': 'processed'})}\n\n"
return Response(
message_handler(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
}
)
# Starlette 앱 설정
app = Starlette(routes=[
Route("/sse", handle_sse, methods=["POST"]),
])
# 서버 실행을 위한 메인 함수
async def run_sse_server():
"""SSE 서버를 실행합니다."""
config = uvicorn.Config(
app,
host="0.0.0.0",
port=8000,
log_level="info"
)
server_instance = uvicorn.Server(config)
await server_instance.serve()
if __name__ == "__main__":
print("SSE MCP 서버를 시작합니다...")
print("서버 주소: http://localhost:8000")
asyncio.run(run_sse_server())
# fastmcp_sse.py
from mcp.server.fastmcp import FastMCP
from datetime import datetime
# 서버 인스턴스 생성
mcp = FastMCP("My FastMCP SSE Server")
@mcp.tool()
def weather(city: str) -> str:
"""도시 이름을 입력하면 현재 날씨를 알려줍니다."""
weather_data = {
"서울": "맑음, 22°C",
"부산": "흐림, 25°C",
"대구": "비, 18°C"
}
result = weather_data.get(city, f"{city}의 날씨 정보를 찾을 수 없습니다.")
return f"{city} 날씨: {result}"
@mcp.tool()
def timestamp() -> str:
"""현재 시간을 반환합니다."""
return f"현재 시간: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
if __name__ == "__main__":
# transport를 sse로 설정하고 호스트/포트 지정
# 내부적으로 uvicorn을 사용하여 서버를 구동합니다.
mcp.run(transport="sse", host="0.0.0.0", port=8000)
python sse_server.py
#리소스 제공 기능
@server.list_resources()
async def list_resources() -> list[types.Resource]:
"""사용 가능한 리소스 목록을 반환합니다."""
return [
types.Resource(
uri="file//config.json",
name="설정 파일"
description="애플리케이션 설정 파일"
mimeType="application/json"
)
]
@server.read_resource()
async def read_resources(uri: str) -> str:
"""리소스를 읽어서 반환합니다."""
if uri == "file://config.json":
return json.dumps({
"version": "1.0",
"debug": True,
"max_connections": 100
}, indents=2)
else:
raise ValueError(f"알 수 없는 리소스: {uri}")
]
#프롬프트 기능
@server.list_prompts()
async def list_prompts() -> list[types.Prompt]:
"""사용 가능한 프롬프트 목록을 반환합니다."""
return [
types.Prompt(
name="code_review"
description="코드 리뷰를 위한 프롬프트"
arguments=[
types.PromptArgument(
name="code",
description="리뷰할 코드",
required=True
)
]
)
]
@server.get_prompt()
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
"""프롬프트를 반환합니다."""
if name == "code_review":
code = arguments["code"]
return types.GetPromptResult(
description="코드 리뷰 프롬프트"
message=[
types.PromptMessage(
role="user"
content=types.TextContent(
type="text",
text=f"다음 코드를 리뷰해주세요:\n\n```\n{code}\n```"
)
)
]
)
else:
raise ValueError(f"알 수 없는 프롬프트: {name}")