Compare commits
	
		
			2 Commits
		
	
	
		
			76c30241dd
			...
			015fc38101
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 015fc38101 | |||
| 5e54d48a98 | 
							
								
								
									
										3
									
								
								env.sh
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								env.sh
									
									
									
									
									
								
							| @@ -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" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/__init__.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/errors.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/errors.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/models.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/models.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/pptx_ops.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/pptx_ops.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/selection.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/docsorter/__pycache__/selection.cpython-312.pyc
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -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" |  | ||||||
| @@ -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()) |  | ||||||
| @@ -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) | ||||||
| @@ -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") | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user