Skip to main content
Glama
roshano3o3

mcp-toolserver

by roshano3o3

mcp-toolserver

문서 검색, SQL, 산술, 코퍼스 int 루어 등 4개의 실제 도구를 제공하는 MCP 서버와, 그 서버에 연결하여 런타임에 도구를 발견하고 클로드를 연쇄 호출해 단일 도구로는 답할 수 없는 질문에 답하는 에이전트 클라이언트를 저장소입니다.

이 프로젝트가 보여주는 것

  • Model Context Protocol — AI 응용 프로그램이 외부 도구 및 데이터에 연결하는 방식을 표준화하는 개방형 프로토콜(Anthropic, 2024년 11월)입니다. 이 프로토콜이 없다면 모든 AI 앱은 모든 도구에 대해 맞춤 통합을, 모든 도구는 모든 AI 앱에 대한 맞춤 통합을 구축해야 하는 N×M 문제가 발생합니다. MCP는 이 문제를 N+M으로 해결합니다. 도구 제공자는 MCP 서버 하나만 만들면 MCP를 지원하는 모든 클라이언트가 별도의 통합 코드 없이 사용할 수 있습니다. 이 저장소는 그 형태의 작은 실제 예시입니다. 서버와 클라이언트는 서로의 내부 구현을 모르며, 오직 조약된 프로토콜만 알고 있습니다.

  • 동적 도구 발견 — 에이전트 클라이언트는 도구 목록을 하드코딩하지 않습니다. 연결 시점에 list_tools()( 를) 호출해 서버가 현재 제공하는 도구를 모두 Anthropic의 도구 사용 형식으로 변환합니다. 서버에서 도구를 추가 또는 제거하면 클라이언트는 자동으로 해당 변경을 수신하며, 클라이언트 코드는 변경할 필요가 없습니다.

  • 멀티 스텝 도구 체이닝 — 하나의 질문에 두 가지 도구를 순서대로 사용해야 하는 경우가 있습니다(재원을 조회한 후 그것으로 계산). 에이전트 루프가 이 과정을 스스로 처리합니다. Claude는 첫 번째 도구의 결과를 사용해 두 번째 도구를 호출하도록 결정합니다. 그렇게 하라는 지시 없이도 말입니다.

Related MCP server: Nexus

네 가지 도구

도구

시그니처

기능

search_documents

(query: str, top_k: int = 5) -> list[dict]

docmind의 처리된 PDF 코퍼스에 대한 시맨틱 검색(밀집 임베딩 + Chroma). 청크당 {text, source, page, score}를 반환합니다.

query_database

(sql: str) -> list[dict]

소규모 시드된 데모 회사 데이터베이스(employees, departments)에 대한 읽기 전용 SQL입니다. SELECT만 가능합니다 — 아래 안전 참고.

calculate

(expression: str) -> float

산술식 처리(+ - * / ** %, 소괄호)를 실행하며 코드 실행 경로가 없습니다 — 아래 안전 참고.

list_documents

() -> list[dict]

처리된 코퍼스의 목록을 문헌별로 제공합니다: {source, pages, chunks}.

각 도구의 docstring이 곧 해당 도구의 MCP 설명이 됩니다. LLM은 실제로 이 설명을 읽고 호출 시점을 결정하며, 소스를 훑어보는 사람이 아니라 LLM을 위해 작성됩니다.

라이브 데모 실행

아래 세 데모는 모두 실제 Claude API와 실제로 생성된 MCP 서버 하위 프로세스에 대한 실제 실행 결과입니다. 가짜 시뮬레이션이 아닙니다. 가장 흥미로운 케이스인 두 도구를 실제로 이어 쓰는 예시를 먼저 보여드립니다.

1. 멀티 스텝: query: 표준부터 시작 → calculate

"Engineering 부서의 평균 월급은 얼마이고, 12% 인사 시 총 비용은 얼마인가?"

Answer:
Here's the breakdown for the Engineering department:

| Metric | Value |
|---|---|
| Average Salary | $141,600 |
| Total Current Payroll | $708,000 |
| Cost of 12% Raise | $84,960 |
| New Total Payroll | $792,960 |

A 12% raise across all Engineering employees would cost an additional $84,960,
bringing the department's total payroll from $708,000 to $792,960.

Iterations: 3
Tool calls:
  1. query_database({'sql': "SELECT AVG(salary) as avg_salary, SUM(salary) as total_salary FROM employees WHERE department_id = (SELECT id FROM departments WHERE name = 'Engineering')"})
     -> [{'avg_salary': 141600.0, 'total_salary': 708000}]
  2. calculate({'expression': '708000 * 0.12'})
     -> 84960.0

claude는 SQL을 직접 작성하고, 결과를 읽고, 실제 산술식을 직접 작성해 실행했습니다 — 위의 도구 호출 입력은 Claude가 만든 것입니다. (직접 검증완료: 시드 데이터에 Engineering 직원 5명 급여의 합이 708,000달러, 5로 나누면 평균 141,600달러, 0.12를 곱하면 84,960달러.)

2. 단일 도구: query\is a database

"공학 부서 직원 수는 몇 명인가요?"

Answer:
There are 5 employees in the Engineering department.

Iterations: 2
Tool calls:
  1. query_database({'sql': "SELECT COUNT(*) as employee_count FROM employees e JOIN departments d ON e.department_id = d.id WHERE d.name = 'Engineering'"})
     -> [{'employee_count': 5}]

3. 단일 도구: search\is a documents

"corrective RAG 란 무엇인가?"

Answer:
## Corrective RAG (CRAG)

Corrective RAG (CRAG) is an enhanced version of standard Retrieval-Augmented
Generation (RAG) that adds a self-correction step after the initial retrieval
phase. [...] Standard (vanilla) RAG simply takes the top-k retrieved documents
and passes them directly to the language model generator -- regardless of
whether those documents actually answer the question. CRAG improves on this by
checking retrieval quality before generation.

[... full answer continues with the retrieve -> grade -> (generate | rewrite &
retry) flow and the latency/LLM-call tradeoff, condensed here for length ...]

Iterations: 2
Tool calls:
  1. search_documents({'query': 'corrective RAG'})
     -> [5 chunks from langgraph_agents.pdf and llm_evaluation.pdf, scores 0.44-0.58]

이 데모의 답변은 모델의 일반 지식이 아니라 실제 검색된 텍스트(docmind의 langgraph_agents.pdf)에 근거합니다. CRAG에 대한 일반 지식은 Claude도 있지만, 이 경우 사용하라고 요청하지 않았습니다.

안전성

query_database — 단일 방어가 아닌 계층형 방어:

  1. 애플리케이션 수준의 키워드/형상 검사 — SQLite로 전달되기 전에 단일 SELECT(또는 WITH ... SELECT) 문이 아닌 것을 거부합니다. INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, ATTACH, DETACH, PRAGMA, VACUUM, REINDEX를 차단하며 ; 로 연결된 여러 문장은 즉시 거부합니다.

  2. SQLite 고유의 읽기 전용 모드 — 연결은 URI에 ?mode=ro 를 지정해 실제로 읽기 전용으로 열립니다. 이는 애플리케이션 코드가 아닌 SQLite엔진의 강제이므로, 1단계에 틈이 있어도 실제로 쓰기를 실행할 수 없는 최종 안전판입니다.

  3. 행 상한 설정 — 모든 쿼리는 SELECT * FROM (<query>) LIMIT 500 로 감싸이므로, 어떤 쿼리라도 쿼리 내용과 무관하게 500행 이상 반환할 수 없습니다.

  4. 벽시계시간 초과 — sqlite3 진행 처리기가 소요 시간을 확인하고 너무 오래 실행되면 문장을 중단합니다.

calculate — 핵심 화이트리스트(AWG) AST, eval() 아님: 식을 구문 분석(ast.parse(..., mode="eval") 단, 수동 우주 탐사) 후 허용된 노드 유형은 Constant(숫자), BinOp(+ - * / ** %), UnaryOp(+/-) 뿐입니다. 그 밖의 명사 조회, Call, Attribute는 탐색 어에 해당 브랜치가 없으므로 구조적으로 ValueError를 올립니다. 그렇기 때문에 calculate("__import__('os').system('...')") 조회 결과 실패하는 것입니다. 위험한 호출 목록인 블랙리스트와의 일치 검사하지 않고, 아예 Call 노드를 실행할 코드 경로가 없습니다.

설계 결정

  • 명시적 에이전트 루프를 사용했으며 “감독 SDK 베타 Tool Runner” 아님. 이 SD k에는 anthropic.lib.tools.mcp MCP 브리지로 MCP 각 도구를 Tool Runner에 직접 연결하는 기능이 있습니다. 본 프로젝트에서는 의도적으로 사용하지 않았습니다. 돌아오는 명시적인 검사 가능 반환 구조 — {answer, tool_calls: [{tool, input, output}], iterations} — 갖기 위해서는 각 회차의 수동 원장기록이 필요했습니다. Tool Runner는 정확히 이 프로젝트가 보여주고자 하는 메커니즘(루프 제어, 호출별 추적)을 감추게 됩니다.

  • streamable-http이 아닌 stdio 운송. 클라이언트는 서버를 하위 프로세스로 그때그때 생성하며, 같은 실행 경계 내에 있고 네트워크 홉이 없므로 stdio의 단순성(포트 통신 없음, 인증 필요 없음)솔이 적합합니다. streamable-http는 지원하지만(--transport streamable-http / MCP_TRANSPORT 환경 변수) 서버와 클라이언트가 실제로는 프로세스별 또는 컴퓨터별로 분리된 경우를 위한 것으로, 보안경화 되어 있지 않습니다(Limitations 참조).

  • 반복 횟수 상한 (8회). 최악의 비용/지연율에 상한을 설정합니다 — 이 문서의 재작성 상한과 같은 취지입니다. 위 3개 데모는 모두 2–3회 반복에서 완료되었으며, 8회는 정상 동작에서 초과되지 않도록 여유있는 상한이며, 실제로는 잘못 설계된 요청이나 불안정한 모델 동작을 잡기 위한 의도입니다.

docmind와의 관계

search_documents와 list_documents는 docmind가 이미 구축한 Chroma 컬렉션에서 바로 읽기(DOCMIND_CHROMA_PATH, 기본적으로 같은 저장소의 docmind 프로젝트 data/chroma를 가리킴)를 수행하되, 쿼리 임베딩은 docmind가 ingest 시 사용한 것과 동일한 all-MiniTM-L6-v2 모델을 사용합니다. 접속 자체가 코드 수준에서 docmind 자체와 관련되어 있는 것은 아니며, 지정된 경로에 존재하는 색인 컬렉션에 Chroma가 어떻게 접속하는지 예시 일 뿐입니다. 그렇기에 이 저장소는 docmind의 코퍼스를 그대로 사용하는 진짜 두 번째 소비자이며, 복사본이 아닙니다. 즉 docmind의 리트리버 레벨이 FastAPI 백엔드 내부에 결합되어 있는 것이 아니라, 컬렉션이 위치한 경로를 아는 MCP 클라이언트라면 누구나 접근할 수 있음을 보여주는 작은 증거입니다.

알려진 제한 사항

  • 데모용 SQL은 작고 인공적입니다(직원 12명, 부서·4개) — 실제 규모 또는 공격적인 데이터베이스에 테스트되지 않았습니다.

  • SQL 키워드 블랙리스트는 실제 SQL 파서가 아닌 쿼리 텍스트에 대한 정규식입니다. 따라서 과도하게 많이 차단할 수도(실제로는 유효한 pragma_table_info()의 테이블 반환 함수 참조 방지하는 경우) 있고, 원리적으로 아무도 테스트하지 않은 구조는 빗금칠 수 있습니다만, 실제 후퇴는 블록리스트 완결성에 의존하지 않는 읽기 전용 연결 모드입니다.

  • calculate는 숫자 리터럴과 명시된 여섯 연산자만 지원하며, 함수(sqrt, sin, ...)와 변수를 지원하지 않습니다. 일반 표현식 엔진이라기보다 의도적으로 최소로 동작합니다.

  • MCP 서버는 인증이 없습니다. stdio(프로세스-로컬, 신뢰 경계 분리)에는 문제가 없지만, 현재 구현 내용 streamable-http 로 실행한다면 포트에 access 할 수 있는 누구라도 query_database를 포함한 모든 도구를 호출할 수 있습니다.

  • 8회 반복 상한은 하드 스톱보다는 문제로 인한 중저이며, 4번이 이상의 도구 왕복이 필요한 요청의 경우 실제 답가 아닌 오히려 "8회 후 중지" 메시지가 리턴됩니다.

  • CLI 호줄 전반에 대화 메모리가 없습니다. python -m toolserver.client.agent "..." 로 호출할 때마다 이전 문맥 없이 새 대화를 시작합니다.

  • 스트리밍을 하지 않습니다. 각 반복은 블로킹 messages.create 호출 때문에느린 도구나 긴 세대 응답이 그 턴 전체를 블록시킵니다.

  • 테스트는 AnthRopic 클라이언트와 MCP Client를 전혀 모의(mock) 합니다(의도적으로, 테스트에서 실제 API를 사용하지 않음). 이는 후자 어느 SDK의 스키마가 통합되지 않아도 pytest 만으로는 잡히지 않는다는 뜻입니다. 위 라이브 데모 실행이 실제 API를 사용한 유일한 검증이며, CI가 아닌 수동입니다.

설치 및 실행

Python 3.12, ANTHROPIC_API_KEY가 있어야 하며(가능하면 search_documents/list_documents 사용 시) docmind 저장소가 있어 ingest 처리된 코퍼스가 필요합니다.

git clone https://github.com/roshano3o3/mcp-toolserver.git
cd mcp-toolserver
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e .
cp .env.example .env              # edit .env and set ANTHROPIC_API_KEY

기본적으로 .env.example 안의 DOCMIND_CHROMA_PATH는 이웃한 docmind 저장소의 data/chroma를 가리킵니다. 실제 doc의 무단 corpus가 있는 경로가 바른곳로 설정하거나 무시하면 됩니다 — query_database, calculate 그리고 list_documents 의 오류 경로가 각각 모두 docmind 없이 동작하기 때문입니다.

에이전트를 직접 실행합니다(자체적으로 MCP 서버를 서브프로세스로 스냅하게 되므로 서버 프로세스가 별도로 있을 필요가 없음):

python -m toolserver.client.agent "How many employees are in the Engineering department?"

MCP 서버를 standalone으로 실행하는 경우, 예를 들어 다른 MCP 클라이언트가 접속할 수 있게 하려면:

python -m toolserver.server                      # stdio (default)
python -m toolserver.server --transport streamable-http   # http://127.0.0.1:8765/mcp by default

테스트 실행 방법:

pytest
ruff check .

실행된 SDK로 확인됨 (메모리 기반 작성이 아님)

mcp==2.0.0는 이전 mcp.server.fastmcp.FastMCP API와 비교해 상당히 달라졌습니다. 이 버전에는 해당 모듈이 존재하지 않습니다. 다음 내용은 모두 설치된 패키지 소스를 직접 읽고, 실존하는 smoke 테스트(인프로세스 및 실제 stdio 서브프로세스)를 실행해 확인했으며 훈련 데이터 기억으로부터 적지 않았습니다.

  • Server: from mcp.server.mcpserver import MCPServer — MCPServer("name"), @server.tool() 괄호 필요(없이는 함께 오류). server.run(transport="stdio" | "sse" | "streamable-http").

  • Client: from mcp.client import Client — 새클라이언트 통합 클라이언트, 대부분의 케이스에서 직접 ClientSession 사용를 대체. in-process Server/MCPServer 또는 URL 문자열 또는 Transport(예: stdio_client(StdioServerParameters(...)))를 받습니다.

  • Discovery: await client.list_tools() → ListToolsResult, 각 도구는 name, description, input_schema 필드를 가짐 — 이는 도구 사용 형식에 맞는 필드 이름이 같으므로 클라이언트 쪽 변환은 거의 직결 매핑이며 스키마 변환기가 아닙니다.

  • Tool results: CallToolResult은 .content (MCP 콘텐트 블록 목록, 항시 채워짐)과 .structured_content (타입 부여 {"result": ...} dict, 도구 함수에 반환 유형 annotation이 있는 경우 채워짐 — 여기서 네 도구 모두 만족)을 가집니다.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables MCP-compatible clients to perform retrieval-grounded Q&A over FordA dataset notes and safe arithmetic calculations through callable tools.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables models to query enterprise systems—structured data, live GitHub REST API, and unstructured document embeddings—through a single, per-caller scoped MCP tool surface with task-shaped tools and recovery-aware errors.
    90 npm
    ISC
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables Claude Code and Codex assistants to research and read/write a team's knowledge base, manage projects and files, and use integrations through MCP tools.
    Apache 2.0