mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-20 14:00:56 +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 |
|
| Tool | Description |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `search` | Perform a global search against the OpenIsle backend. |
|
| `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. |
|
| `reply_to_comment` | Reply to an existing comment using a JWT token. |
|
||||||
| `recent_posts` | Retrieve posts created within the last *N* minutes. |
|
| `recent_posts` | Retrieve posts created within the last *N* minutes. |
|
||||||
|
|
||||||
The tools return structured data mirroring the backend DTOs, including highlighted snippets for
|
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.")
|
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):
|
class PostSummary(BaseModel):
|
||||||
"""Summary information for a post."""
|
"""Summary information for a post."""
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,35 @@ class SearchClient:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return self._ensure_dict(response.json())
|
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]]:
|
async def recent_posts(self, minutes: int) -> list[dict[str, Any]]:
|
||||||
"""Return posts created within the given timeframe."""
|
"""Return posts created within the given timeframe."""
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from pydantic import Field as PydanticField
|
|||||||
|
|
||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
|
CommentCreateResult,
|
||||||
CommentData,
|
CommentData,
|
||||||
CommentReplyResult,
|
CommentReplyResult,
|
||||||
PostDetail,
|
PostDetail,
|
||||||
@@ -41,9 +42,9 @@ async def lifespan(_: FastMCP):
|
|||||||
app = FastMCP(
|
app = FastMCP(
|
||||||
name="openisle-mcp",
|
name="openisle-mcp",
|
||||||
instructions=(
|
instructions=(
|
||||||
"Use this server to search OpenIsle content, reply to comments with an authentication "
|
"Use this server to search OpenIsle content, reply to posts and comments with an "
|
||||||
"token, retrieve details for a specific post, and list posts created within a recent time "
|
"authentication token, retrieve details for a specific post, and list posts created "
|
||||||
"window."
|
"within a recent time window."
|
||||||
),
|
),
|
||||||
host=settings.host,
|
host=settings.host,
|
||||||
port=settings.port,
|
port=settings.port,
|
||||||
@@ -96,6 +97,100 @@ async def search(
|
|||||||
return SearchResponse(keyword=sanitized, total=len(results), results=results)
|
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(
|
@app.tool(
|
||||||
name="reply_to_comment",
|
name="reply_to_comment",
|
||||||
description="Reply to an existing comment using an authentication token.",
|
description="Reply to an existing comment using an authentication token.",
|
||||||
|
|||||||
Reference in New Issue
Block a user