Compare commits

...

2 Commits

3 changed files with 89 additions and 1 deletions

View File

@@ -260,3 +260,14 @@ class RecentPostsResponse(BaseModel):
CommentData.model_rebuild()
class PostDetail(PostSummary):
"""Detailed information for a single post, including comments."""
comments: list[CommentData] = Field(
default_factory=list,
description="Comments that belong to the post.",
)
model_config = ConfigDict(populate_by_name=True, extra="allow")

View File

@@ -84,6 +84,18 @@ class SearchClient:
)
return [self._ensure_dict(entry) for entry in payload]
async def get_post(self, post_id: int, token: str | None = None) -> dict[str, Any]:
"""Retrieve the detailed payload for a single post."""
client = self._get_client()
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
response = await client.get(f"/api/posts/{post_id}", headers=headers)
response.raise_for_status()
return self._ensure_dict(response.json())
async def aclose(self) -> None:
"""Dispose of the underlying HTTP client."""

View File

@@ -14,6 +14,7 @@ from .config import get_settings
from .schemas import (
CommentData,
CommentReplyResult,
PostDetail,
PostSummary,
RecentPostsResponse,
SearchResponse,
@@ -41,7 +42,8 @@ app = FastMCP(
name="openisle-mcp",
instructions=(
"Use this server to search OpenIsle content, reply to comments with an authentication "
"token, and list posts created within a recent time window."
"token, retrieve details for a specific post, and list posts created within a recent time "
"window."
),
host=settings.host,
port=settings.port,
@@ -229,6 +231,69 @@ async def recent_posts(
return RecentPostsResponse(minutes=minutes, total=len(posts), posts=posts)
@app.tool(
name="get_post",
description="Retrieve detailed information for a single post.",
structured_output=True,
)
async def get_post(
post_id: Annotated[
int,
PydanticField(ge=1, description="Identifier of the post to retrieve."),
],
token: Annotated[
str | None,
PydanticField(
default=None,
description="Optional JWT bearer token to view the post as an authenticated user.",
),
] = None,
ctx: Context | None = None,
) -> PostDetail:
"""Fetch post details from the backend and validate the response."""
sanitized_token = token.strip() if isinstance(token, str) else None
if sanitized_token == "":
sanitized_token = None
try:
raw_post = await search_client.get_post(post_id, sanitized_token)
except httpx.HTTPStatusError as exc: # pragma: no cover - network errors
status_code = exc.response.status_code
if status_code == 404:
message = f"Post {post_id} was not found."
elif status_code == 401:
message = "Authentication failed while retrieving the post."
elif status_code == 403:
message = "The provided token is not authorized to view this post."
else:
message = (
"OpenIsle backend returned HTTP "
f"{status_code} while retrieving post {post_id}."
)
if ctx is not None:
await ctx.error(message)
raise ValueError(message) from exc
except httpx.RequestError as exc: # pragma: no cover - network errors
message = f"Unable to reach OpenIsle backend post service: {exc}."
if ctx is not None:
await ctx.error(message)
raise ValueError(message) from exc
try:
post = PostDetail.model_validate(raw_post)
except ValidationError as exc:
message = "Received malformed data from the post detail endpoint."
if ctx is not None:
await ctx.error(message)
raise ValueError(message) from exc
if ctx is not None:
await ctx.info(f"Retrieved post {post_id} successfully.")
return post
def main() -> None:
"""Run the MCP server using the configured transport."""