feat(fable-mcp): implement FableClient, all tool modules, and server.py
- client.py: async httpx wrapper, FableAPIError, stream_get() SSE generator, singleton init_client()/get_client() and _reset_client() for tests - tools/: notes, tasks, projects, milestones, search, chat — thin async functions that accept FableClient and call Fable REST endpoints - server.py: FastMCP entry point with 20 tools registered via @mcp.tool(), each opening a fresh FableClient context per call; validates env vars at startup - tests: 34 tests covering client HTTP behaviour, error handling, singleton, SSE streaming, and all tool modules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"""MCP tools for Fable milestones."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fable_mcp.client import FableClient
|
||||
|
||||
|
||||
async def list_milestones(client: FableClient, *, project_id: int) -> dict[str, Any]:
|
||||
"""List milestones for a project."""
|
||||
return await client.get(f"/api/projects/{project_id}/milestones")
|
||||
|
||||
|
||||
async def create_milestone(
|
||||
client: FableClient,
|
||||
*,
|
||||
project_id: int,
|
||||
title: str,
|
||||
description: str = "",
|
||||
status: str = "active",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a milestone within a project."""
|
||||
payload: dict[str, Any] = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"status": status,
|
||||
}
|
||||
return await client.post(f"/api/projects/{project_id}/milestones", json=payload)
|
||||
|
||||
|
||||
async def update_milestone(
|
||||
client: FableClient,
|
||||
*,
|
||||
project_id: int,
|
||||
milestone_id: int,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
status: str | None = None,
|
||||
order_index: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Update an existing milestone."""
|
||||
payload: dict[str, Any] = {}
|
||||
if title is not None:
|
||||
payload["title"] = title
|
||||
if description is not None:
|
||||
payload["description"] = description
|
||||
if status is not None:
|
||||
payload["status"] = status
|
||||
if order_index is not None:
|
||||
payload["order_index"] = order_index
|
||||
return await client.patch(
|
||||
f"/api/projects/{project_id}/milestones/{milestone_id}", json=payload
|
||||
)
|
||||
Reference in New Issue
Block a user