This commit is contained in:
despiegk 2025-08-04 10:43:14 +02:00
parent 2207542bb5
commit 5e54d48a98
10 changed files with 52 additions and 164 deletions

3
env.sh
View File

@ -29,3 +29,6 @@ fi
echo "🔄 Activating virtual environment..." echo "🔄 Activating virtual environment..."
source .venv/bin/activate source .venv/bin/activate
# Add src to PYTHONPATH
export PYTHONPATH="$SCRIPT_DIR/src:$PYTHONPATH"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,31 +0,0 @@
class RpcError(Exception):
"""Base class for custom RPC errors."""
code = -32000
message = "Server error"
def __init__(self, message: str = None):
super().__init__(message or self.message)
class NotFoundError(RpcError):
"""Presentation not found."""
code = -32001
message = "Not found"
class InvalidArgumentError(RpcError):
"""Invalid arguments provided."""
code = -32002
message = "Invalid argument"
class CopyUnsupportedError(RpcError):
"""Content cannot be copied safely."""
code = -32003
message = "Copy unsupported"
class InternalOpError(RpcError):
"""Unexpected python-pptx/OPC failure."""
code = -32004
message = "Internal operation error"

View File

@ -1,78 +0,0 @@
import argparse
import json
import asyncio
from mcp.client.http import StreamableHttpClient
def print_json(data):
"""Prints a JSON object with indentation."""
print(json.dumps(data, indent=2))
async def main():
parser = argparse.ArgumentParser(description="A client for the Docsorter MCP server.")
subparsers = parser.add_subparsers(dest="command", required=True)
# find command
find_parser = subparsers.add_parser("find", help="Find presentations.")
find_parser.add_argument("--start", dest="start_dir", required=True, help="The directory to start searching from.")
find_parser.add_argument("--pattern", help="A pattern to filter results.")
# list-slides command
list_slides_parser = subparsers.add_parser("list-slides", help="List slides in a presentation.")
list_slides_parser.add_argument("--path", required=True, help="The path to the presentation.")
# notes command
notes_parser = subparsers.add_parser("notes", help="Get notes from a presentation.")
notes_parser.add_argument("--path", required=True, help="The path to the presentation.")
notes_parser.add_argument("--slides", nargs='+', type=int, help="A list of slide numbers.")
# copy command
copy_parser = subparsers.add_parser("copy", help="Copy slides between presentations.")
copy_parser.add_argument("--src", dest="src_path", required=True, help="The source presentation path.")
copy_parser.add_argument("--dst", dest="dst_path", required=True, help="The destination presentation path.")
copy_parser.add_argument("--slides", nargs='+', type=int, required=True, help="A list of slide numbers to copy.")
copy_parser.add_argument("--insert", dest="insert_position", type=int, help="The position to insert the copied slides.")
# delete command
delete_parser = subparsers.add_parser("delete", help="Delete slides from a presentation.")
delete_parser.add_argument("--path", required=True, help="The path to the presentation.")
delete_parser.add_argument("--slides", nargs='+', type=int, required=True, help="A list of slide numbers to delete.")
args = parser.parse_args()
client = StreamableHttpClient(url="http://localhost:8000")
try:
await client.start()
if args.command == "find":
result = await client.call("presentations/find", {"start_dir": args.start_dir, "pattern": args.pattern})
print_json(result)
elif args.command == "list-slides":
result = await client.call("slides/list", {"path": args.path})
print_json(result)
elif args.command == "notes":
result = await client.call("slides/notes", {"path": args.path, "slides": args.slides})
print_json(result)
elif args.command == "copy":
params = {
"src_path": args.src_path,
"dst_path": args.dst_path,
"slides": args.slides,
}
if args.insert_position:
params["insert_position"] = args.insert_position
result = await client.call("slides/copy", params)
print_json(result)
elif args.command == "delete":
result = await client.call("slides/delete", {"path": args.path, "slides": args.slides})
print_json(result)
finally:
await client.stop()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -1,83 +1,77 @@
import sys import sys
import logging import logging
from mcp.server import Server import socket
from mcp.common.rpc import JsonRpcError from fastmcp import FastMCP
from mcp.server.stdio import stdio_server
from .selection import find_presentations, resolve_presentations from typing import Optional, List
from .pptx_ops import list_slide_titles, list_slide_notes, copy_slides, delete_slides from docsorter.selection import find_presentations, resolve_presentations
from .errors import RpcError from docsorter.pptx_ops import list_slide_titles, list_slide_notes, copy_slides, delete_slides
from .logging_utils import setup_logging, notify_log from docsorter.logging_utils import setup_logging, notify_log
def map_exceptions_to_jsonrpc(exc: Exception) -> JsonRpcError:
"""Maps custom exceptions to JSON-RPC errors."""
if isinstance(exc, RpcError):
return JsonRpcError(code=exc.code, message=str(exc))
# Default error for unhandled exceptions
return JsonRpcError(code=-32000, message=f"Internal server error: {exc}")
def main() -> None: def main() -> None:
logger = setup_logging(stream=sys.stderr) logger = setup_logging(stream=sys.stderr)
server = Server("docsorter") mcp = FastMCP("docsorter")
@server.method("presentations/find") @mcp.tool()
async def m_find(params: dict) -> dict: async def m_find(start_dir: str, pattern: Optional[str] = None, max_results: int = 50) -> dict:
notify_log(server, "Searching presentations…") notify_log(mcp, "Searching presentations…")
logger.info(f"Finding presentations with params: {params}") logger.info(f"Finding presentations with params: start_dir={start_dir}, pattern={pattern}, max_results={max_results}")
pres = find_presentations( pres = find_presentations(
params["start_dir"], params.get("pattern"), params.get("max_results", 50) start_dir, pattern, max_results
) )
return {"presentations": pres} return {"presentations": pres}
@server.method("presentations/resolve") @mcp.tool()
async def m_resolve(params: dict) -> dict: async def m_resolve(start_dir: str, name_or_pattern: str, limit: int = 2) -> dict:
notify_log(server, f"Resolving presentation: {params.get('name_or_pattern')}") notify_log(mcp, f"Resolving presentation: {name_or_pattern}")
logger.info(f"Resolving presentations with params: {params}") logger.info(f"Resolving presentations with params: start_dir={start_dir}, name_or_pattern={name_or_pattern}, limit={limit}")
pres = resolve_presentations( pres = resolve_presentations(
params["start_dir"], params["name_or_pattern"], params.get("limit", 2) start_dir, name_or_pattern, limit
) )
return {"presentations": pres} return {"presentations": pres}
@server.method("slides/list") @mcp.tool()
async def m_list(params: dict) -> dict: async def m_list(path: str) -> dict:
notify_log(server, f"Listing slides for: {params.get('path')}") notify_log(mcp, f"Listing slides for: {path}")
logger.info(f"Listing slides for presentation: {params.get('path')}") logger.info(f"Listing slides for presentation: {path}")
slides = list_slide_titles(params["path"]) slides = list_slide_titles(path)
return {"slides": slides} return {"slides": slides}
@server.method("slides/notes") @mcp.tool()
async def m_notes(params: dict) -> dict: async def m_notes(path: str, slides: Optional[List[int]] = None) -> dict:
notify_log(server, f"Fetching notes for: {params.get('path')}") notify_log(mcp, f"Fetching notes for: {path}")
logger.info(f"Getting notes for presentation: {params.get('path')}") logger.info(f"Getting notes for presentation: {path}")
notes = list_slide_notes(params["path"], params.get("slides")) notes = list_slide_notes(path, slides)
return {"notes": notes} return {"notes": notes}
@server.method("slides/copy") @mcp.tool()
async def m_copy(params: dict) -> dict: async def m_copy(src_path: str, dst_path: str, slides: List[int], insert_position: Optional[int] = None) -> dict:
notify_log( notify_log(
server, mcp,
f"Copying {len(params.get('slides', []))} slides from " f"Copying {len(slides)} slides from "
f"{params.get('src_path')} to {params.get('dst_path')}" f"{src_path} to {dst_path}"
) )
logger.info(f"Copying slides with params: {params}") logger.info(f"Copying slides with params: src_path={src_path}, dst_path={dst_path}, slides={slides}, insert_position={insert_position}")
report = copy_slides( report = copy_slides(
params["src_path"], src_path,
params["dst_path"], dst_path,
params["slides"], slides,
params.get("insert_position") insert_position
) )
return {"report": report} return {"report": report}
@server.method("slides/delete") @mcp.tool()
async def m_delete(params: dict) -> dict: async def m_delete(path: str, slides: List[int]) -> dict:
notify_log(server, f"Deleting slides from: {params.get('path')}") notify_log(mcp, f"Deleting slides from: {path}")
logger.info(f"Deleting slides from presentation: {params.get('path')}") logger.info(f"Deleting slides from presentation: {path}")
report = delete_slides(params["path"], params["slides"]) report = delete_slides(path, slides)
return {"report": report} return {"report": report}
# Centralized error mapping to JSON-RPC codes # Centralized error mapping to JSON-RPC codes
server.set_exception_handler(map_exceptions_to_jsonrpc)
# Run stdio JSON-RPC # Run SSE JSON-RPC
logger.info("Docsorter MCP server started.") port = 59001
stdio_server(server).run_forever() host = "0.0.0.0"
logger.info(f"Docsorter MCP server started at http://{socket.gethostname()}:{port}/sse/")
mcp.run(transport="sse", host=host, port=port)

View File

@ -1,7 +1,7 @@
from fastmcp import FastMCP from fastmcp import FastMCP
from fastmcp.utilities.http import find_available_port # from fastmcp.utilities.http import find_available_port
import socket import socket
import uvicorn # import uvicorn
mcp = FastMCP("MyAgent") mcp = FastMCP("MyAgent")