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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Add soft-delete timestamp to loop items.

Revision ID: b7c1d2e3f4a5
Revises: a6d94c3e5217
"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

revision: str = "b7c1d2e3f4a5"
down_revision: Union[str, None] = "a6d94c3e5217"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column("loop_items", sa.Column("deleted_at", sa.DateTime(), nullable=True))
op.create_index("ix_loop_items_deleted_at", "loop_items", ["deleted_at"])


def downgrade() -> None:
op.drop_index("ix_loop_items_deleted_at", table_name="loop_items")
op.drop_column("loop_items", "deleted_at")
17 changes: 17 additions & 0 deletions backend/app/api/endpoints/deliveries.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
LoopItemCollaboratorResponse,
LoopItemCreate,
LoopItemListResponse,
LoopItemReorder,
LoopItemResponse,
LoopItemTaskBind,
LoopItemTaskBindingResponse,
Expand Down Expand Up @@ -194,6 +195,22 @@ def create_loop_item(
return LoopItemResponse.model_validate(item)


@router.post(
"/cloud-projects/{project_id}/loop-items/reorder",
response_model=LoopItemListResponse,
)
def reorder_loop_items(
project_id: int,
values: LoopItemReorder,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> LoopItemListResponse:
items = loop_item_service.reorder(db, project_id, current_user.id, values)
return LoopItemListResponse(
items=[LoopItemResponse.model_validate(item) for item in items]
)


@router.get("/loop-items/{item_id}", response_model=LoopItemResponse)
def get_loop_item(
item_id: str,
Expand Down
77 changes: 73 additions & 4 deletions backend/app/mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"""

import contextvars
import json
import logging
from contextlib import asynccontextmanager
from dataclasses import dataclass, replace
Expand All @@ -47,6 +48,7 @@
)
from app.mcp_server.context import (
MCPRequestContext,
get_token_info_from_context,
reset_mcp_context,
set_mcp_context,
)
Expand All @@ -69,6 +71,16 @@
SUBSCRIPTION_MCP_TRANSPORT_PATH = "/sse"
DELIVERY_MCP_MOUNT_PATH = "/mcp/delivery"
DELIVERY_MCP_TRANSPORT_PATH = "/sse"
PROJECT_SPACE_PROTOCOL = "wegent.project-space"
PROJECT_SPACE_PROTOCOL_VERSION = 1
PROJECT_SPACE_CAPABILITIES = {
"projects.read": True,
"projects.create": True,
"todos.read": True,
"todos.write": True,
"files.read": True,
"deliveries.read": True,
}


@dataclass(frozen=True)
Expand Down Expand Up @@ -529,7 +541,7 @@ def ensure_subscription_tools_registered() -> None:
# ============== Delivery MCP Server ==============

delivery_mcp_server = FastMCP(
"wegent-delivery-mcp",
"wegent_delivery",
stateless_http=True,
json_response=True,
streamable_http_path="/",
Expand All @@ -542,18 +554,66 @@ def ensure_subscription_tools_registered() -> None:


def ensure_delivery_tools_registered() -> None:
"""Register AI-facing tools for authorized delivery snapshots."""
"""Register the project-space tools and addressable cloud resources."""
global _delivery_tools_registered
if _delivery_tools_registered:
return
from app.mcp_server.tool_registry import register_tools_to_server
from app.mcp_server.tools import delivery # noqa: F401

count = register_tools_to_server(delivery_mcp_server, "delivery")
delivery_mcp_server.resource(
"cloud://projects",
name="Wegent project spaces",
description="Every project space accessible to the authenticated user.",
mime_type="application/json",
)(_read_cloud_projects_resource)
delivery_mcp_server.resource(
"cloud://projects/{project_id}",
name="Wegent project space",
description="A project space with its shared workspace and board items.",
mime_type="application/json",
)(_read_cloud_project_resource)
delivery_mcp_server.resource(
"cloud://projects/{project_id}/{resource_type}/{resource_id}",
name="Wegent project-space object",
description="A task, file, or delivery in a project space.",
mime_type="application/json",
)(_read_cloud_object_resource)
logger.info("[MCP:Delivery] Registered %s tools", count)
_delivery_tools_registered = True


def _delivery_resource_token() -> MCPAuthInfo:
token_info = get_token_info_from_context()
if token_info is None:
raise PermissionError("Authentication required")
return token_info


def _serialize_delivery_resource(reference: str) -> str:
from app.mcp_server.tools.delivery import resolve_cloud_reference

result = resolve_cloud_reference(reference, _delivery_resource_token())
return json.dumps(result, ensure_ascii=False, default=str)


def _read_cloud_projects_resource() -> str:
return _serialize_delivery_resource("cloud://projects")


def _read_cloud_project_resource(project_id: str) -> str:
return _serialize_delivery_resource(f"cloud://projects/{project_id}")


def _read_cloud_object_resource(
project_id: str, resource_type: str, resource_id: str
) -> str:
return _serialize_delivery_resource(
f"cloud://projects/{project_id}/{resource_type}/{resource_id}"
)


# ============== Starlette App Factory ==============

_SYSTEM_MCP_SPEC = McpAppSpec(
Expand Down Expand Up @@ -613,7 +673,7 @@ def ensure_delivery_tools_registered() -> None:

_DELIVERY_MCP_SPEC = McpAppSpec(
name="delivery",
service_name="wegent-delivery-mcp",
service_name="wegent_delivery",
mount_path=DELIVERY_MCP_MOUNT_PATH,
transport_path=DELIVERY_MCP_TRANSPORT_PATH,
server=delivery_mcp_server,
Expand Down Expand Up @@ -644,14 +704,23 @@ def ensure_delivery_tools_registered() -> None:


def _build_root_metadata(spec: McpAppSpec) -> Dict[str, Any]:
return {
metadata = {
"service": spec.service_name,
"transport": "streamable-http",
"endpoints": {
"mcp": f"{spec.mount_path}{spec.transport_path}",
"health": f"{spec.mount_path}/health",
},
}
if spec.name == "delivery":
metadata.update(
{
"protocol": PROJECT_SPACE_PROTOCOL,
"protocolVersion": PROJECT_SPACE_PROTOCOL_VERSION,
"capabilities": PROJECT_SPACE_CAPABILITIES,
}
)
return metadata


def _build_mcp_app(spec: McpAppSpec) -> Starlette:
Expand Down
13 changes: 12 additions & 1 deletion backend/app/mcp_server/tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,18 @@ async def tool_wrapper(**kwargs: Any) -> str:
f"[MCP:{server_name}] Tool {tool_name} failed: {e}",
exc_info=True,
)
return json.dumps({"error": str(e)})
return json.dumps(
{
"error": {
"code": "MCP_TOOL_EXECUTION_FAILED",
"message": str(e),
"server": server_name,
"tool": tool_name,
"retryable": False,
}
},
ensure_ascii=False,
)

# Set function metadata for FastMCP
tool_wrapper.__name__ = tool_name
Expand Down
Loading
Loading