...
This commit is contained in:
parent
2207542bb5
commit
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")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user