From 4008912401c903071612c8fdd073cd55271bc0fc Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Mon, 13 Jul 2026 20:20:19 -0500 Subject: [PATCH] Working, but only SSE at /sse --- docker-compose.mcphttp.yaml | 20 +++ mcp_server_http/Dockerfile | 22 ++++ mcp_server_http/docker-compose.yml | 18 +++ mcp_server_http/requirements.txt | 2 + mcp_server_http/server.py | 198 +++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+) create mode 100644 docker-compose.mcphttp.yaml create mode 100644 mcp_server_http/Dockerfile create mode 100644 mcp_server_http/docker-compose.yml create mode 100644 mcp_server_http/requirements.txt create mode 100644 mcp_server_http/server.py diff --git a/docker-compose.mcphttp.yaml b/docker-compose.mcphttp.yaml new file mode 100644 index 0000000..8057669 --- /dev/null +++ b/docker-compose.mcphttp.yaml @@ -0,0 +1,20 @@ +services: + mcp-server-http: + build: + context: . + dockerfile: mcp_server_http/Dockerfile + container_name: mcp-server-http + ports: + - "9000:8001" + restart: unless-stopped + environment: + - TZ=CDT + - PORT=8001 + - HOST=0.0.0.0 + - NOTEDISCOVERY_URL=http://192.168.1.99:3440 + healthcheck: + test: ["CMD", "python", "-c", "import socket; s = socket.socket(); s.connect(('127.0.0.1', 8001)); s.close()"] + interval: 60s + timeout: 3s + retries: 3 + start_period: 5s diff --git a/mcp_server_http/Dockerfile b/mcp_server_http/Dockerfile new file mode 100644 index 0000000..7ba43bc --- /dev/null +++ b/mcp_server_http/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Copy requirements from the mcp_server_http directory +COPY mcp_server_http/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the original mcp_server to get the client implementation +COPY mcp_server/ ./mcp_server/ + +# Copy the HTTP server +COPY mcp_server_http/ ./mcp_server_http/ + +ENV PORT=8001 +ENV HOST=0.0.0.0 +# Point this to the NoteDiscovery API backend +ENV NOTEDISCOVERY_URL=http://backend:8000 + +EXPOSE 8001 + +CMD ["python", "-m", "mcp_server_http.server"] diff --git a/mcp_server_http/docker-compose.yml b/mcp_server_http/docker-compose.yml new file mode 100644 index 0000000..3e1785d --- /dev/null +++ b/mcp_server_http/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.8" + +services: + mcp-server-http: + build: + context: .. + dockerfile: mcp_server_http/Dockerfile + ports: + - "8001:8001" + environment: + - PORT=8001 + - HOST=0.0.0.0 + # URL to reach NoteDiscovery. Change this to the actual host/port + # or docker service name if part of a larger compose network. + - NOTEDISCOVERY_URL=http://host.docker.internal:8000 + # extra_hosts is used so it can reach the host network if NoteDiscovery runs locally + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/mcp_server_http/requirements.txt b/mcp_server_http/requirements.txt new file mode 100644 index 0000000..cb991ba --- /dev/null +++ b/mcp_server_http/requirements.txt @@ -0,0 +1,2 @@ +mcp[cli]>=1.0.0 +pydantic>=2.0.0 diff --git a/mcp_server_http/server.py b/mcp_server_http/server.py new file mode 100644 index 0000000..3494713 --- /dev/null +++ b/mcp_server_http/server.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +HTTP Streamable MCP Server for NoteDiscovery. + +This server exposes NoteDiscovery tools over SSE using FastMCP. +""" + +import sys +import os +import json +from typing import Optional + +# Add parent directory to path so we can import from mcp_server +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel, Field + +from mcp_server.config import load_config +from mcp_server.client import NoteDiscoveryClient + +# Initialize FastMCP server +mcp = FastMCP("notediscovery_mcp_http") + +# Load config and client +try: + config = load_config() + client = NoteDiscoveryClient(config) +except ValueError as e: + print(f"Configuration error: {e}", file=sys.stderr) + sys.exit(1) + +# Helper for responses +def format_response(response) -> str: + if not response.success: + return f"Error: {response.error}" + if response.data is None: + return "Success (no data)" + return json.dumps(response.data, indent=2, ensure_ascii=False) + +# Pydantic models for tools +class SearchNotesInput(BaseModel): + query: str = Field(..., description="Search query") + limit: Optional[int] = Field(None, description="Maximum results to return") + offset: int = Field(0, description="Pagination offset") + +class ListNotesInput(BaseModel): + limit: Optional[int] = Field(None, description="Maximum results to return") + offset: int = Field(0, description="Pagination offset") + +class PathInput(BaseModel): + path: str = Field(..., description="Note or folder path") + +class TagInput(BaseModel): + tag: str = Field(..., description="Tag name") + limit: Optional[int] = Field(None, description="Maximum results to return") + offset: int = Field(0, description="Pagination offset") + +class CreateNoteInput(BaseModel): + path: str = Field(..., description="Note path") + content: str = Field(..., description="Markdown content") + +class AppendNoteInput(BaseModel): + path: str = Field(..., description="Note path") + content: str = Field(..., description="Content to append") + add_timestamp: bool = Field(False, description="Whether to add timestamp") + +class MoveNoteInput(BaseModel): + old_path: str = Field(..., description="Current path") + new_path: str = Field(..., description="New path") + +class CreateFromTemplateInput(BaseModel): + template_name: str = Field(..., description="Template name") + note_path: str = Field(..., description="Note path") + +class GetTemplateInput(BaseModel): + name: str = Field(..., description="Template name") + +# Register Tools +@mcp.tool() +def search_notes(params: SearchNotesInput) -> str: + """Search notes by query.""" + resp = client.search(params.query, limit=params.limit, offset=params.offset) + return format_response(resp) + +@mcp.tool() +def list_notes(params: ListNotesInput) -> str: + """List all notes.""" + resp = client.list_notes(limit=params.limit, offset=params.offset) + return format_response(resp) + +@mcp.tool() +def get_note(params: PathInput) -> str: + """Get note content.""" + resp = client.get_note(params.path) + return format_response(resp) + +@mcp.tool() +def list_tags() -> str: + """List all tags.""" + resp = client.list_tags() + return format_response(resp) + +@mcp.tool() +def get_notes_by_tag(params: TagInput) -> str: + """Get notes with a specific tag.""" + resp = client.get_notes_by_tag(params.tag, limit=params.limit, offset=params.offset) + return format_response(resp) + +@mcp.tool() +def get_graph() -> str: + """Get knowledge graph data.""" + resp = client.get_graph() + return format_response(resp) + +@mcp.tool() +def get_backlinks(params: PathInput) -> str: + """Get backlinks for a note.""" + resp = client.get_backlinks(params.path) + return format_response(resp) + +@mcp.tool() +def create_note(params: CreateNoteInput) -> str: + """Create or update a note.""" + resp = client.create_note(params.path, params.content) + return format_response(resp) + +@mcp.tool() +def delete_note(params: PathInput) -> str: + """Delete a note.""" + resp = client.delete_note(params.path) + return format_response(resp) + +@mcp.tool() +def create_folder(params: PathInput) -> str: + """Create a folder.""" + resp = client.create_folder(params.path) + return format_response(resp) + +@mcp.tool() +def list_templates() -> str: + """List templates.""" + resp = client.list_templates() + return format_response(resp) + +@mcp.tool() +def get_template(params: GetTemplateInput) -> str: + """Get template content.""" + resp = client.get_template(params.name) + return format_response(resp) + +@mcp.tool() +def append_to_note(params: AppendNoteInput) -> str: + """Append content to an existing note.""" + resp = client.append_to_note(params.path, params.content, params.add_timestamp) + return format_response(resp) + +@mcp.tool() +def move_note(params: MoveNoteInput) -> str: + """Move or rename a note.""" + resp = client.move_note(params.old_path, params.new_path) + return format_response(resp) + +@mcp.tool() +def create_note_from_template(params: CreateFromTemplateInput) -> str: + """Create a note from a template.""" + resp = client.create_note_from_template(params.template_name, params.note_path) + return format_response(resp) + +@mcp.tool() +def health_check() -> str: + """Check server health.""" + resp = client.health_check() + return format_response(resp) + +@mcp.tool() +def get_config() -> str: + """Get NoteDiscovery server configuration.""" + resp = client.get_config() + return format_response(resp) + +if __name__ == "__main__": + port = int(os.environ.get("PORT", "8001")) + host = os.environ.get("HOST", "0.0.0.0") + print(f"Starting HTTP streamable MCP server on {host}:{port}", file=sys.stderr) + + app = mcp.sse_app() + from starlette.middleware.cors import CORSMiddleware + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + import uvicorn + uvicorn.run(app, host=host, port=port)