mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
refactor(mcp): flatten to mcp_server package, drop src layout
Rename the import package surfsense_mcp -> mcp_server and remove the src/ layer so the project mirrors the backend's shape (project folder != package name, e.g. surfsense_backend/app). Kills the redundant surfsense_mcp/src/surfsense_mcp nesting. Distribution name and console command (surfsense-mcp) are unchanged; only python -m and internal import paths move to mcp_server. Dockerfile CMD updated; no PYTHONPATH added since the editable install already makes the package importable.
This commit is contained in:
parent
839618ef09
commit
116291a3b6
46 changed files with 31 additions and 31 deletions
100
surfsense_mcp/mcp_server/features/workspaces/__init__.py
Normal file
100
surfsense_mcp/mcp_server/features/workspaces/__init__.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""Search-space selector: discover workspaces and choose the active one.
|
||||
|
||||
A workspace (the product calls it a "search space") scopes every other tool.
|
||||
These two tools let a client list what's available and pick one by name, so the
|
||||
rest of the conversation needs no ids.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
from pydantic import Field
|
||||
|
||||
from ...core.rendering import ResponseFormatParam, to_json
|
||||
from ...core.workspace_context import Workspace, WorkspaceContext
|
||||
|
||||
_READ_ONLY = ToolAnnotations(
|
||||
readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=False
|
||||
)
|
||||
|
||||
|
||||
def register(mcp: FastMCP, context: WorkspaceContext) -> None:
|
||||
"""Register the workspace selector tools on the server."""
|
||||
|
||||
@mcp.tool(
|
||||
name="surfsense_list_workspaces",
|
||||
title="List workspaces",
|
||||
annotations=_READ_ONLY,
|
||||
structured_output=False,
|
||||
)
|
||||
async def list_workspaces(
|
||||
response_format: ResponseFormatParam = "markdown",
|
||||
) -> str:
|
||||
"""List the SurfSense workspaces (search spaces) the account can access.
|
||||
|
||||
Use this to discover which workspaces exist before selecting one, or
|
||||
when the user asks what search spaces they have. Returns each
|
||||
workspace's name, id, description, ownership, and member count, and
|
||||
marks the currently active one.
|
||||
"""
|
||||
workspaces = await context.fetch_all()
|
||||
if response_format == "json":
|
||||
return to_json([_as_dict(w) for w in workspaces])
|
||||
return _render_list(workspaces, active=context.active)
|
||||
|
||||
@mcp.tool(
|
||||
name="surfsense_select_workspace",
|
||||
title="Select active workspace",
|
||||
annotations=_READ_ONLY,
|
||||
structured_output=False,
|
||||
)
|
||||
async def select_workspace(
|
||||
workspace: Annotated[
|
||||
str,
|
||||
Field(
|
||||
description="Workspace name or numeric id; matching is "
|
||||
"case-insensitive and a unique partial name works. "
|
||||
"Example: 'Research'."
|
||||
),
|
||||
],
|
||||
) -> str:
|
||||
"""Set the active workspace (search space) that later tools default to.
|
||||
|
||||
Use this when the user says which search space to work in ("use my
|
||||
Research space"), or after surfsense_list_workspaces when several
|
||||
exist. Once set, workspace-scoped tools use it unless given a
|
||||
different 'workspace'. Do NOT call it before every tool — once per
|
||||
session is enough.
|
||||
"""
|
||||
selected = await context.resolve(workspace)
|
||||
return (
|
||||
f"Active workspace is now '{selected.name}' (id {selected.id}). "
|
||||
"Other tools will use it unless you pass a different 'workspace'."
|
||||
)
|
||||
|
||||
|
||||
def _render_list(workspaces: list[Workspace], *, active: Workspace | None) -> str:
|
||||
if not workspaces:
|
||||
return "No accessible workspaces."
|
||||
lines = ["# Workspaces", ""]
|
||||
for workspace in workspaces:
|
||||
marker = " — active" if active and active.id == workspace.id else ""
|
||||
role = "owner" if workspace.is_owner else "member"
|
||||
lines.append(f"- **{workspace.name}** (id {workspace.id}){marker}")
|
||||
if workspace.description:
|
||||
lines.append(f" - {workspace.description}")
|
||||
lines.append(f" - {role}, {workspace.member_count} member(s)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _as_dict(workspace: Workspace) -> dict:
|
||||
return {
|
||||
"id": workspace.id,
|
||||
"name": workspace.name,
|
||||
"description": workspace.description,
|
||||
"is_owner": workspace.is_owner,
|
||||
"member_count": workspace.member_count,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue