🛒 MCP 마켓플레이스 & 게이트웨이

등록 서버(Registry) + 프록시 게이트웨이(Gateway)
이 환경의 공개 베이스 URL: https://demo.makemyproject.net/marketplace
아래 예제의 이 주소를 그대로 복사해서 쓰면 됩니다.
🔑 공유 토큰은 관리 작업(서버 등록/삭제, 컨슈머 생성/삭제, 구독 변경)과 채팅에만 필요합니다. 이때 Authorization: Bearer <토큰> 헤더를 보냅니다. UI에서는 우상단 🔑 토큰에서 확인/복사.
게이트웨이(MCP) 접속·도구 호출·목록/검색에는 토큰이 필요 없습니다 — MCP 표준대로 그냥 붙으면 됩니다.

개념① 서버 등록(공급자) ② 게이트웨이로 사용(소비자)③ 상태/장애 에러 코드

개념 한 장 요약

마켓플레이스는 등록 서버(Registry)이자 프록시 게이트웨이(Gateway)입니다.

두 가지 게이트웨이 엔드포인트

형태주소도구 이름언제
통합https://demo.makemyproject.net/marketplace/mcp/consumers/<컨슈머id> 서버id__도구명여러 서버를 한 에이전트로 묶을 때 (보통 이걸 씀)
개별https://demo.makemyproject.net/marketplace/mcp/servers/<namespace>/<서버id>
(namespace 생략한 /mcp/servers/<서버id> 도 동작)
원래 이름 그대로특정 서버 하나만 쓸 때

① 내 MCP 서버를 등록하기 (공급자)

먼저 평범한 MCP 서버를 만듭니다. (공식 mcp SDK의 FastMCP 예시)

# my_server.py   —   pip install mcp   /   실행: python my_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My MCP", host="0.0.0.0", port=8101)

@mcp.tool()
def greet(name: str) -> str:
    """이름을 받아 인사한다."""
    return f"안녕하세요, {name}님!"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")   # → http://<내주소>:8101/mcp

그다음 마켓플레이스에 등록합니다. 🖥️ MCP 서버 페이지의 “새 MCP 서버 등록” 폼을 쓰거나, API로:

curl -X POST https://demo.makemyproject.net/marketplace/api/servers \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <공유토큰>" \
  -d '{
    "id": "myteam-greet",
    "name": "인사 서버",
    "owner": "4조",
    "namespace": "myteam",
    "endpoint": "http://my-public-host:8101/mcp",
    "description": "이름을 받아 인사"
  }'
endpoint는 게이트웨이(마켓플레이스 서버)가 접속할 수 있는 도달 가능한 주소입니다. 소비자에게 노출되는 주소가 아니라, 마켓플레이스에서 실제로 접속되는 MCP 주소를 적으세요. 보안상 사설/내부망·클라우드 메타데이터(169.254.x) 주소는 거부됩니다(SSRF 방지).

등록하면 마켓플레이스가 즉시 list_tools를 호출해 도구·스키마를 자동 수집합니다. 도구를 바꿨다면 UI의 “도구 새로고침” 또는 POST /api/servers/<id>/refresh.

② 게이트웨이로 사용하기 (소비자)

👤 컨슈머 페이지에서 컨슈머를 만들고, 쓰고 싶은 서버들을 구독 체크하세요. 그러면 아래 한 줄이면 끝입니다.

LangChain 에이전트 (그대로 복붙해서 실행되는 전체 코드)

# pip install mcp langchain langchain-openai langchain-mcp-adapters langgraph requests
# export OPENAI_API_KEY=sk-...                ← 본인 OpenAI 키
import asyncio, requests
from langchain_openai import ChatOpenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

MARKET   = "https://demo.makemyproject.net/marketplace"
CONSUMER = "my-agent"                          # 내 컨슈머 id (원하는 이름)
TOKEN    = "<공유토큰>"                       # 컨슈머 생성/구독(관리)에만 필요. 게이트웨이 접속엔 불필요.
H = {"Authorization": f"Bearer {TOKEN}"}

def setup():
    """컨슈머가 없으면 만들고, 구독이 비어 있으면 등록된 서버 전체를 구독한다(관리=토큰 필요)."""
    s = requests.get(f"{MARKET}/api/consumers/{CONSUMER}/subscriptions")
    if s.status_code == 404:
        requests.post(f"{MARKET}/api/consumers", json={"id": CONSUMER, "name": CONSUMER}, headers=H)
        s = requests.get(f"{MARKET}/api/consumers/{CONSUMER}/subscriptions")
    if not s.json():
        ids = [x["id"] for x in requests.get(f"{MARKET}/api/servers").json()]
        requests.put(f"{MARKET}/api/consumers/{CONSUMER}/subscriptions",
                     json={"server_ids": ids}, headers=H)

async def main():
    setup()
    # 게이트웨이는 토큰 불필요 — 그냥 붙는다
    client = MultiServerMCPClient({"market": {
        "url": f"{MARKET}/mcp/consumers/{CONSUMER}", "transport": "streamable_http"}})
    tools = await client.get_tools()           # 구독한 모든 서버의 도구 (서버id__도구명)
    print("쓸 수 있는 도구:", [t.name for t in tools])
    agent = create_agent(ChatOpenAI(model="gpt-4o-mini"), tools)
    res = await agent.ainvoke({"messages": [("user", "도쿄 여행 상품 검색해줘")]})
    print(res["messages"][-1].content)

asyncio.run(main())

구독 레시피 — 상황별 간단 예제

구독은 서버 단위입니다(도구 단위 선택은 ④처럼 클라이언트에서 거릅니다). 아래 네 토막은 모두 같은 변수(MARKET·CONSUMER·H)를 앞에 둔다고 가정합니다.

# 공통 — 아래 네 예제 앞에 한 번만 둔다.  pip install requests
import requests
MARKET   = "https://demo.makemyproject.net/marketplace"
CONSUMER = "my-agent"                           # 내 컨슈머 id (원하는 이름)
TOKEN    = "<공유토큰>"                       # 등록/구독(관리)에만 필요. 게이트웨이 접속엔 불필요.
H = {"Authorization": f"Bearer {TOKEN}"}
SUBS = f"{MARKET}/api/consumers/{CONSUMER}/subscriptions"

① 내 앱(서버)을 등록 — 켜둔 MCP 서버 주소를 마켓에 알린다. 등록 즉시 도구가 자동 수집된다.

requests.post(f"{MARKET}/api/servers", headers=H, json={
    "id": "myteam-greet", "name": "인사 서버", "owner": "4조",
    "namespace": "myteam", "endpoint": "http://my-public-host:8101/mcp"})
# 도구를 바꾼 뒤 다시 수집: POST {MARKET}/api/servers/myteam-greet/refresh

② 모두 구독 — 등록된 서버 전부를 구독한다.

requests.post(f"{MARKET}/api/consumers", json={"id": CONSUMER, "name": CONSUMER}, headers=H)  # 없으면 생성(있어도 안전)
ids = [s["id"] for s in requests.get(f"{MARKET}/api/servers").json()]
requests.put(SUBS, json={"server_ids": ids}, headers=H)

③ 원하는 서버만 구독 — id 목록으로 구독을 통째 교체한다.

requests.post(f"{MARKET}/api/consumers", json={"id": CONSUMER, "name": CONSUMER}, headers=H)
requests.put(SUBS, json={"server_ids": ["weather", "travel"]}, headers=H)   # ← 원하는 id 만

④ 원하는 서버 안에서 원하는 도구만 — 구독은 서버 단위라, 도구는 클라이언트에서 거른다. 게이트웨이는 도구 이름을 서버id__도구명 으로 준다.

# ③으로 weather·travel 을 구독해 둔 상태라고 하자. (async 함수 안에서)
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({"market": {
    "url": f"{MARKET}/mcp/consumers/{CONSUMER}", "transport": "streamable_http"}})
all_tools = await client.get_tools()                       # 구독한 서버의 모든 도구
WANT = {"weather__get_weather", "travel__search_trips"}    # 실제로 쓸 도구만
tools = [t for t in all_tools if t.name in WANT]
# 이 tools 만 에이전트에 넘기면 나머지 도구는 LLM 에게 보이지 않는다.
구독 조회(GET …/subscriptions) 응답은 서버 객체 목록이라 id 만 뽑아 씁니다([s["id"] for s in …]). 구독을 추가만 하려면 현재 id 에 합쳐 보내고 (cur = requests.get(SUBS).json() → server_ids = list({s["id"] for s in cur} | {"shopping"})), server_ids: [] 는 전체 해지입니다. ④의 도구 단위 선택은 서버에 저장되지 않고 매번 클라이언트가 거릅니다.

순수 MCP 클라이언트 (LLM 없이 — 그대로 복붙 실행)

# pip install mcp requests
import asyncio, requests
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

MARKET   = "https://demo.makemyproject.net/marketplace"
CONSUMER = "my-agent"
TOKEN    = "<공유토큰>"                       # 컨슈머 생성/구독(관리)에만 필요 (🔑 토큰에서 복사)

def setup():
    """컨슈머가 없으면 만들고, 구독이 비었으면 등록된 서버 전체를 구독한다."""
    H = {"Authorization": f"Bearer {TOKEN}"}
    s = requests.get(f"{MARKET}/api/consumers/{CONSUMER}/subscriptions")
    if s.status_code == 404:
        requests.post(f"{MARKET}/api/consumers", json={"id": CONSUMER, "name": CONSUMER}, headers=H)
        s = requests.get(f"{MARKET}/api/consumers/{CONSUMER}/subscriptions")
    if not s.json():
        ids = [x["id"] for x in requests.get(f"{MARKET}/api/servers").json()]
        requests.put(f"{MARKET}/api/consumers/{CONSUMER}/subscriptions",
                     json={"server_ids": ids}, headers=H)

async def main():
    setup()                                       # ← 이게 없으면 게이트웨이가 404(=Session terminated)
    async with streamablehttp_client(f"{MARKET}/mcp/consumers/{CONSUMER}") as (r, w, _):
        async with ClientSession(r, w) as s:      # 게이트웨이 접속 — 토큰 불필요
            await s.initialize()
            tools = (await s.list_tools()).tools
            print("도구:", [t.name for t in tools])
            out = await s.call_tool("weather__get_weather", {"city": "서울"})   # 데모 도구 예시
            print(out.content[0].text)

asyncio.run(main())
개별 서버만 쓸 땐 https://demo.makemyproject.net/marketplace/mcp/servers/<서버id>에 붙고, 도구 이름은 네임스페이스 없이 greet 처럼 원래 이름 그대로입니다.

③ 서버 상태와 장애 처리

마켓플레이스는 등록된 서버를 주기적으로 폴링합니다. 서버가 죽어도 등록 정보는 지우지 않고 상태만 바꿉니다.

상태의미
ONLINE최근 응답함 — 정상 중계
UNHEALTHY잠시 응답이 늦음 — 아직 중계 시도
OFFLINE오래 응답 없음 — 호출 시 즉시 SERVER_OFFLINE 반환(타임아웃 X)
ARCHIVED아주 오래 죽어 있음 — 목록에서 흐리게 표시, 메타는 보존

폴링과 별개로, 서버가 직접 생존 신고를 할 수도 있습니다(선택):

curl -X POST https://demo.makemyproject.net/marketplace/heartbeat \
  -H "Content-Type: application/json" -H "Authorization: Bearer <공유토큰>" \
  -d '{"server_id": "myteam-greet"}'

게이트웨이 에러 코드

호출이 실패하면 게이트웨이는 도구 결과 텍스트로 JSON 에러를 돌려줍니다.

error의미
NOT_SUBSCRIBED그 컨슈머가 해당 서버를 구독하지 않음 → UI에서 구독 체크
SERVER_OFFLINE대상 서버가 OFFLINE/ARCHIVED 상태
UPSTREAM_ERROR서버는 살아있다고 보였으나 실제 호출이 실패
UNKNOWN_SERVER / BAD_TOOL_NAME없는 서버 / 잘못된 도구 이름 형식

그 외 HTTP 레벨: 401 UNAUTHORIZED(토큰 없음/오류), 400 ENDPOINT_REJECTED(SSRF 차단된 endpoint).

팁: 단발 점검은 🧪 도구 테스트 페이지에서 인자를 넣어 바로 호출해 보고, 🖥️ MCP 서버에서 도구를 검색할 수 있습니다.

🔑 공유 토큰

정말 삭제할까요?