mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-08 08:01:16 +08:00
Compare commits
3 Commits
codex/add-
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dca14390ca | ||
|
|
39875acd35 | ||
|
|
62edc75735 |
@@ -31,9 +31,11 @@ By default the server listens on port `8085` and serves MCP over Streamable HTTP
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `search` | Perform a global search against the OpenIsle backend. |
|
||||
| `reply_to_post` | Create a new comment on a post using a JWT token. |
|
||||
| `reply_to_comment` | Reply to an existing comment using a JWT token. |
|
||||
| `recent_posts` | Retrieve posts created within the last *N* minutes. |
|
||||
|
||||
The tools return structured data mirroring the backend DTOs, including highlighted snippets for
|
||||
search results, the full comment payload for replies, and detailed metadata for recent posts.
|
||||
search results, the full comment payload for post replies and comment replies, and detailed
|
||||
metadata for recent posts.
|
||||
|
||||
|
||||
@@ -177,6 +177,12 @@ class CommentReplyResult(BaseModel):
|
||||
comment: CommentData = Field(description="Reply comment returned by the backend.")
|
||||
|
||||
|
||||
class CommentCreateResult(BaseModel):
|
||||
"""Structured response returned when creating a comment on a post."""
|
||||
|
||||
comment: CommentData = Field(description="Comment returned by the backend.")
|
||||
|
||||
|
||||
class PostSummary(BaseModel):
|
||||
"""Summary information for a post."""
|
||||
|
||||
|
||||
@@ -66,6 +66,35 @@ class SearchClient:
|
||||
response.raise_for_status()
|
||||
return self._ensure_dict(response.json())
|
||||
|
||||
async def reply_to_post(
|
||||
self,
|
||||
post_id: int,
|
||||
token: str,
|
||||
content: str,
|
||||
captcha: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a comment on a post and return the backend payload."""
|
||||
|
||||
client = self._get_client()
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
}
|
||||
payload: dict[str, Any] = {"content": content}
|
||||
if captcha is not None:
|
||||
stripped_captcha = captcha.strip()
|
||||
if stripped_captcha:
|
||||
payload["captcha"] = stripped_captcha
|
||||
|
||||
response = await client.post(
|
||||
f"/api/posts/{post_id}/comments",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return self._ensure_dict(response.json())
|
||||
|
||||
async def recent_posts(self, minutes: int) -> list[dict[str, Any]]:
|
||||
"""Return posts created within the given timeframe."""
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from pydantic import Field as PydanticField
|
||||
|
||||
from .config import get_settings
|
||||
from .schemas import (
|
||||
CommentCreateResult,
|
||||
CommentData,
|
||||
CommentReplyResult,
|
||||
PostDetail,
|
||||
@@ -41,9 +42,9 @@ async def lifespan(_: FastMCP):
|
||||
app = FastMCP(
|
||||
name="openisle-mcp",
|
||||
instructions=(
|
||||
"Use this server to search OpenIsle content, reply to comments with an authentication "
|
||||
"token, retrieve details for a specific post, and list posts created within a recent time "
|
||||
"window."
|
||||
"Use this server to search OpenIsle content, reply to posts and comments with an "
|
||||
"authentication token, retrieve details for a specific post, and list posts created "
|
||||
"within a recent time window."
|
||||
),
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
@@ -96,6 +97,100 @@ async def search(
|
||||
return SearchResponse(keyword=sanitized, total=len(results), results=results)
|
||||
|
||||
|
||||
@app.tool(
|
||||
name="reply_to_post",
|
||||
description="Create a comment on a post using an authentication token.",
|
||||
structured_output=True,
|
||||
)
|
||||
async def reply_to_post(
|
||||
post_id: Annotated[
|
||||
int,
|
||||
PydanticField(ge=1, description="Identifier of the post being replied to."),
|
||||
],
|
||||
token: Annotated[
|
||||
str,
|
||||
PydanticField(description="JWT bearer token for the user performing the reply."),
|
||||
],
|
||||
content: Annotated[
|
||||
str,
|
||||
PydanticField(description="Markdown content of the reply."),
|
||||
],
|
||||
captcha: Annotated[
|
||||
str | None,
|
||||
PydanticField(
|
||||
default=None,
|
||||
description="Optional captcha solution if the backend requires it.",
|
||||
),
|
||||
] = None,
|
||||
ctx: Context | None = None,
|
||||
) -> CommentCreateResult:
|
||||
"""Create a comment on a post and return the backend payload."""
|
||||
|
||||
sanitized_content = content.strip()
|
||||
if not sanitized_content:
|
||||
raise ValueError("Reply content must not be empty.")
|
||||
|
||||
sanitized_token = token.strip()
|
||||
if not sanitized_token:
|
||||
raise ValueError("Authentication token must not be empty.")
|
||||
|
||||
sanitized_captcha = captcha.strip() if isinstance(captcha, str) else None
|
||||
|
||||
try:
|
||||
raw_comment = await search_client.reply_to_post(
|
||||
post_id,
|
||||
sanitized_token,
|
||||
sanitized_content,
|
||||
sanitized_captcha,
|
||||
)
|
||||
except httpx.HTTPStatusError as exc: # pragma: no cover - network errors
|
||||
status_code = exc.response.status_code
|
||||
if status_code == 401:
|
||||
message = (
|
||||
"Authentication failed while replying to post "
|
||||
f"{post_id}. Please verify the token."
|
||||
)
|
||||
elif status_code == 403:
|
||||
message = (
|
||||
"The provided token is not authorized to reply to post "
|
||||
f"{post_id}."
|
||||
)
|
||||
elif status_code == 404:
|
||||
message = f"Post {post_id} was not found."
|
||||
else:
|
||||
message = (
|
||||
"OpenIsle backend returned HTTP "
|
||||
f"{status_code} while replying to 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 = (
|
||||
"Unable to reach OpenIsle backend comment service: "
|
||||
f"{exc}."
|
||||
)
|
||||
if ctx is not None:
|
||||
await ctx.error(message)
|
||||
raise ValueError(message) from exc
|
||||
|
||||
try:
|
||||
comment = CommentData.model_validate(raw_comment)
|
||||
except ValidationError as exc:
|
||||
message = "Received malformed data from the post comment endpoint."
|
||||
if ctx is not None:
|
||||
await ctx.error(message)
|
||||
raise ValueError(message) from exc
|
||||
|
||||
if ctx is not None:
|
||||
await ctx.info(
|
||||
"Reply created successfully for post "
|
||||
f"{post_id}."
|
||||
)
|
||||
|
||||
return CommentCreateResult(comment=comment)
|
||||
|
||||
|
||||
@app.tool(
|
||||
name="reply_to_comment",
|
||||
description="Reply to an existing comment using an authentication token.",
|
||||
|
||||
Reference in New Issue
Block a user