Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ MCP 서버를 만들고, 원클릭 배포. 시크릿 불필요.

- **MCP SDK** — `mcp` (FastMCP) + stdio 전송
- **Python 3.11+** — 타입 힌트, async/await, hatchling 빌드
- **MCP 3대 프리미티브** — Tools, Resources, Prompts 예제 전부 포함
- **Safety Annotations** — 모든 도구에 readOnly/destructive/idempotent 힌트
- **검증된 Prompt** — pydantic `@validate_call`로 핸들러 실행 전 인자 검증
- **응답 헬퍼** — `ok()`, `err()`로 일관된 응답
- **CI** — gitleaks, ruff, 라이선스 검증, pytest (3.11/3.12/3.13)
- **CD** — OIDC trusted publishing으로 PyPI 배포 (시크릿 불필요)
Expand Down Expand Up @@ -58,6 +60,60 @@ async def your_tool(input: str) -> str:
return f"처리 완료: {input}"
```

## Resource 추가

Resource는 고정된 URI로 클라이언트에 데이터를 노출합니다 (동작을 수행하는 Tool과 대비).

예시: `src/my_mcp_server/resources/server_info.py`

```python
from mcp.server.fastmcp import FastMCP


def register(mcp: FastMCP) -> None:
@mcp.resource(
"info://your/resource",
name="your-resource",
description="리소스가 노출하는 데이터.",
mime_type="application/json",
)
async def your_resource() -> str:
return "..." # str, bytes, 또는 JSON 직렬화 가능한 객체
```

`server.py`에서:

```python
from my_mcp_server.resources.your_resource import register as register_your_resource
register_your_resource(mcp)
```

## Prompt 추가

Prompt는 파라미터화된 재사용 가능한 메시지 템플릿입니다. pydantic으로 인자를 검증합니다.

예시: `src/my_mcp_server/prompts/code_review.py`

```python
from typing import Annotated, Literal

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts.base import UserMessage
from pydantic import Field, validate_call


@validate_call
def your_prompt(
mode: Literal["short", "long"],
topic: Annotated[str, Field(min_length=1)],
) -> list[UserMessage]:
return [UserMessage(content=f"{topic}에 대한 {mode} 노트를 작성해주세요.")]


def register(mcp: FastMCP) -> None:
mcp.prompt(name="your-prompt", title="Your Prompt")(your_prompt)
```

## CI/CD

### CI (push/PR마다 실행)
Expand Down
70 changes: 67 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ Build your MCP server. One-click publish. Zero secrets needed.

- **MCP SDK** — `mcp` (FastMCP) with stdio transport
- **Python 3.11+** — Type hints, async/await, hatchling build
- **All three MCP primitives** — Tools, Resources, and Prompts with working examples
- **Safety Annotations** — readOnly/destructive/idempotent hints on every tool
- **Validated Prompts** — pydantic `@validate_call` rejects bad args before the handler runs
- **Response Helpers** — `ok()` and `err()` for consistent tool responses
- **Config** — Environment variable parsing pattern
- **CI** — gitleaks, ruff, license compliance, pytest (3.11/3.12/3.13)
Expand Down Expand Up @@ -99,6 +101,60 @@ from my_mcp_server.tools.your_tool import register
register(mcp)
```

## Adding Resources

Resources expose read-only data to the client at a stable URI (contrast with Tools, which perform actions).

See `src/my_mcp_server/resources/server_info.py` for the example. Pattern:

```python
from mcp.server.fastmcp import FastMCP


def register(mcp: FastMCP) -> None:
@mcp.resource(
"info://your/resource",
name="your-resource",
description="What this resource exposes.",
mime_type="application/json",
)
async def your_resource() -> str:
return "..." # str, bytes, or JSON-serializable object
```

Then in `server.py`:

```python
from my_mcp_server.resources.your_resource import register as register_your_resource
register_your_resource(mcp)
```

## Adding Prompts

Prompts are reusable, parameterized message templates. Arguments are validated via pydantic before the handler runs.

See `src/my_mcp_server/prompts/code_review.py` for the example. Pattern:

```python
from typing import Annotated, Literal

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts.base import UserMessage
from pydantic import Field, validate_call


@validate_call
def your_prompt(
mode: Literal["short", "long"],
topic: Annotated[str, Field(min_length=1)],
) -> list[UserMessage]:
return [UserMessage(content=f"Write a {mode} note about {topic}.")]


def register(mcp: FastMCP) -> None:
mcp.prompt(name="your-prompt", title="Your Prompt")(your_prompt)
```

## Configuration

Environment variables:
Expand Down Expand Up @@ -150,11 +206,19 @@ src/my_mcp_server/
├── __init__.py # Version
├── __main__.py # python -m entry point
├── server.py # FastMCP server + inline tools + helpers
└── tools/
├── tools/
│ ├── __init__.py
│ └── greet.py # Example modular tool
├── resources/
│ ├── __init__.py
│ └── server_info.py # Example resource (info://server/status)
└── prompts/
├── __init__.py
└── greet.py # Example modular tool
└── code_review.py # Example prompt (validated args)
tests/
└── test_tools.py # Tool tests
├── test_tools.py # Tool tests
├── test_server_info.py # Resource tests
└── test_code_review.py # Prompt tests
.github/
├── workflows/
│ ├── ci.yml # Lint, test, security
Expand Down
1 change: 1 addition & 0 deletions src/my_mcp_server/prompts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Prompts are reusable, parameterized message templates for MCP clients."""
56 changes: 56 additions & 0 deletions src/my_mcp_server/prompts/code_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Example MCP Prompt — a templated code review prompt.

Prompts are reusable, parameterized message templates the client can surface to
the user or feed to the model. Arguments are validated via pydantic so empty
code or unsupported languages are rejected before the handler runs.
"""

from __future__ import annotations

from typing import Annotated, Literal

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts.base import UserMessage
from pydantic import Field, validate_call

NAME = "code-review"
TITLE = "Code Review"
DESCRIPTION = "Ask the model to review a snippet of code in the given language."

Language = Literal["py", "js", "ts", "go"]

_LANGUAGE_LABEL: dict[str, str] = {
"py": "Python",
"js": "JavaScript",
"ts": "TypeScript",
"go": "Go",
}


@validate_call
def code_review(
language: Language,
code: Annotated[str, Field(min_length=1, description="Source code to review.")],
) -> list[UserMessage]:
"""Render a code-review prompt as a single-user-message template.

Args:
language: Programming language of the snippet (one of py/js/ts/go).
code: Source code to review. Must be non-empty.

Returns:
A list containing one user message with a language-labeled fenced
code block.
"""
label = _LANGUAGE_LABEL[language]
text = (
f"Review this {label} code for bugs, readability, and idiomatic style. "
"Be specific and actionable.\n\n"
f"```{language}\n{code}\n```"
)
return [UserMessage(content=text)]


def register(mcp: FastMCP) -> None:
"""Register the code-review prompt on the server."""
mcp.prompt(name=NAME, title=TITLE, description=DESCRIPTION)(code_review)
1 change: 1 addition & 0 deletions src/my_mcp_server/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Resources expose data to the MCP client at a stable URI."""
82 changes: 82 additions & 0 deletions src/my_mcp_server/resources/server_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Example MCP Resource — exposes server metadata (name, version, runtime) at a
fixed URI.

Resources are how you expose data to the client (in contrast to Tools which
perform actions). Replace with your own resource.
"""

from __future__ import annotations

import json
import platform
import sys
import tomllib
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

from mcp.server.fastmcp import FastMCP

NAME = "server-info"
URI = "info://server/status"
TITLE = "Server Info"
DESCRIPTION = "Server metadata: name, version, Python runtime, and platform."
MIME_TYPE = "application/json"


def _read_pyproject() -> dict[str, str] | None:
"""Walk up from this file to locate pyproject.toml and parse it.

Returns the ``[project]`` table as a dict, or None if not found
(e.g. when installed from a wheel without the source tree).
"""
here = Path(__file__).resolve()
for parent in here.parents:
candidate = parent / "pyproject.toml"
if candidate.is_file():
with candidate.open("rb") as fh:
data = tomllib.load(fh)
project = data.get("project")
if isinstance(project, dict):
return project
return None
return None


def _server_metadata() -> dict[str, object]:
project = _read_pyproject()
if project is not None:
pkg_name = str(project.get("name", "my-mcp-server"))
pkg_version = str(project.get("version", "0.0.0"))
else:
# Fallback for wheel installs where pyproject.toml isn't shipped.
pkg_name = "my-mcp-server"
try:
pkg_version = version(pkg_name)
except PackageNotFoundError:
pkg_version = "0.0.0"

return {
"name": pkg_name,
"version": pkg_version,
"runtime": {
"python": sys.version.split()[0],
"platform": platform.system().lower(),
"arch": platform.machine(),
},
}


async def server_info() -> str:
"""Return server metadata as a JSON string."""
return json.dumps(_server_metadata(), indent=2)


def register(mcp: FastMCP) -> None:
"""Register the server-info resource on the server."""
mcp.resource(
URI,
name=NAME,
title=TITLE,
description=DESCRIPTION,
mime_type=MIME_TYPE,
)(server_info)
17 changes: 17 additions & 0 deletions src/my_mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations

from my_mcp_server.prompts.code_review import register as register_code_review
from my_mcp_server.resources.server_info import register as register_server_info

logger = logging.getLogger("my_mcp_server")

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -78,6 +81,20 @@ async def greet(name: str) -> str:
# See tools/greet.py for the modular pattern.


# ---------------------------------------------------------------------------
# Resources — expose data to the client at a stable URI
# ---------------------------------------------------------------------------

register_server_info(mcp)


# ---------------------------------------------------------------------------
# Prompts — reusable, parameterized message templates
# ---------------------------------------------------------------------------

register_code_review(mcp)


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading