diff --git a/env.sh b/env.sh index 49532c5..21a842a 100755 --- a/env.sh +++ b/env.sh @@ -29,3 +29,6 @@ fi echo "🔄 Activating virtual environment..." source .venv/bin/activate +# Add src to PYTHONPATH +export PYTHONPATH="$SCRIPT_DIR/src:$PYTHONPATH" + diff --git a/src/docsorter/__pycache__/__init__.cpython-312.pyc b/src/docsorter/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..9b90431 Binary files /dev/null and b/src/docsorter/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/docsorter/__pycache__/errors.cpython-312.pyc b/src/docsorter/__pycache__/errors.cpython-312.pyc new file mode 100644 index 0000000..4877e5b Binary files /dev/null and b/src/docsorter/__pycache__/errors.cpython-312.pyc differ diff --git a/src/docsorter/__pycache__/models.cpython-312.pyc b/src/docsorter/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..c90a182 Binary files /dev/null and b/src/docsorter/__pycache__/models.cpython-312.pyc differ diff --git a/src/docsorter/__pycache__/pptx_ops.cpython-312.pyc b/src/docsorter/__pycache__/pptx_ops.cpython-312.pyc new file mode 100644 index 0000000..97774c5 Binary files /dev/null and b/src/docsorter/__pycache__/pptx_ops.cpython-312.pyc differ diff --git a/src/docsorter/__pycache__/selection.cpython-312.pyc b/src/docsorter/__pycache__/selection.cpython-312.pyc new file mode 100644 index 0000000..9b154d7 Binary files /dev/null and b/src/docsorter/__pycache__/selection.cpython-312.pyc differ diff --git a/src/docsorter/errors.py b/src/docsorter/errors.py deleted file mode 100644 index 7920373..0000000 --- a/src/docsorter/errors.py +++ /dev/null @@ -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" \ No newline at end of file diff --git a/src/docsorter/mcp_client.py b/src/docsorter/mcp_client.py deleted file mode 100644 index 04822ff..0000000 --- a/src/docsorter/mcp_client.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/src/docsorter/mcp_server.py b/src/docsorter/mcp_server.py index 10297f9..2715232 100755 --- a/src/docsorter/mcp_server.py +++ b/src/docsorter/mcp_server.py @@ -1,83 +1,77 @@ import sys import logging -from mcp.server import Server -from mcp.common.rpc import JsonRpcError -from mcp.server.stdio import stdio_server +import socket +from fastmcp import FastMCP -from .selection import find_presentations, resolve_presentations -from .pptx_ops import list_slide_titles, list_slide_notes, copy_slides, delete_slides -from .errors import RpcError -from .logging_utils import setup_logging, notify_log +from typing import Optional, List +from docsorter.selection import find_presentations, resolve_presentations +from docsorter.pptx_ops import list_slide_titles, list_slide_notes, copy_slides, delete_slides +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: logger = setup_logging(stream=sys.stderr) - server = Server("docsorter") + mcp = FastMCP("docsorter") - @server.method("presentations/find") - async def m_find(params: dict) -> dict: - notify_log(server, "Searching presentations…") - logger.info(f"Finding presentations with params: {params}") + @mcp.tool() + async def m_find(start_dir: str, pattern: Optional[str] = None, max_results: int = 50) -> dict: + notify_log(mcp, "Searching presentations…") + logger.info(f"Finding presentations with params: start_dir={start_dir}, pattern={pattern}, max_results={max_results}") pres = find_presentations( - params["start_dir"], params.get("pattern"), params.get("max_results", 50) + start_dir, pattern, max_results ) return {"presentations": pres} - @server.method("presentations/resolve") - async def m_resolve(params: dict) -> dict: - notify_log(server, f"Resolving presentation: {params.get('name_or_pattern')}") - logger.info(f"Resolving presentations with params: {params}") + @mcp.tool() + async def m_resolve(start_dir: str, name_or_pattern: str, limit: int = 2) -> dict: + notify_log(mcp, f"Resolving presentation: {name_or_pattern}") + logger.info(f"Resolving presentations with params: start_dir={start_dir}, name_or_pattern={name_or_pattern}, limit={limit}") pres = resolve_presentations( - params["start_dir"], params["name_or_pattern"], params.get("limit", 2) + start_dir, name_or_pattern, limit ) return {"presentations": pres} - @server.method("slides/list") - async def m_list(params: dict) -> dict: - notify_log(server, f"Listing slides for: {params.get('path')}") - logger.info(f"Listing slides for presentation: {params.get('path')}") - slides = list_slide_titles(params["path"]) + @mcp.tool() + async def m_list(path: str) -> dict: + notify_log(mcp, f"Listing slides for: {path}") + logger.info(f"Listing slides for presentation: {path}") + slides = list_slide_titles(path) return {"slides": slides} - @server.method("slides/notes") - async def m_notes(params: dict) -> dict: - notify_log(server, f"Fetching notes for: {params.get('path')}") - logger.info(f"Getting notes for presentation: {params.get('path')}") - notes = list_slide_notes(params["path"], params.get("slides")) + @mcp.tool() + async def m_notes(path: str, slides: Optional[List[int]] = None) -> dict: + notify_log(mcp, f"Fetching notes for: {path}") + logger.info(f"Getting notes for presentation: {path}") + notes = list_slide_notes(path, slides) return {"notes": notes} - @server.method("slides/copy") - async def m_copy(params: dict) -> dict: + @mcp.tool() + async def m_copy(src_path: str, dst_path: str, slides: List[int], insert_position: Optional[int] = None) -> dict: notify_log( - server, - f"Copying {len(params.get('slides', []))} slides from " - f"{params.get('src_path')} to {params.get('dst_path')}" + mcp, + f"Copying {len(slides)} slides from " + 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( - params["src_path"], - params["dst_path"], - params["slides"], - params.get("insert_position") + src_path, + dst_path, + slides, + insert_position ) return {"report": report} - @server.method("slides/delete") - async def m_delete(params: dict) -> dict: - notify_log(server, f"Deleting slides from: {params.get('path')}") - logger.info(f"Deleting slides from presentation: {params.get('path')}") - report = delete_slides(params["path"], params["slides"]) + @mcp.tool() + async def m_delete(path: str, slides: List[int]) -> dict: + notify_log(mcp, f"Deleting slides from: {path}") + logger.info(f"Deleting slides from presentation: {path}") + report = delete_slides(path, slides) return {"report": report} # Centralized error mapping to JSON-RPC codes - server.set_exception_handler(map_exceptions_to_jsonrpc) - # Run stdio JSON-RPC - logger.info("Docsorter MCP server started.") - stdio_server(server).run_forever() \ No newline at end of file + # Run SSE JSON-RPC + port = 59001 + 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) \ No newline at end of file diff --git a/src/mcptest/mcptest.py b/src/mcptest/mcptest.py index f62f9f6..1f8d9e0 100644 --- a/src/mcptest/mcptest.py +++ b/src/mcptest/mcptest.py @@ -1,7 +1,7 @@ from fastmcp import FastMCP -from fastmcp.utilities.http import find_available_port +# from fastmcp.utilities.http import find_available_port import socket -import uvicorn +# import uvicorn mcp = FastMCP("MyAgent")