added mcp server
This commit is contained in:
parent
f097f5e6da
commit
a7c073909a
|
|
@ -56,10 +56,13 @@ COPY --from=builder /install /usr/local
|
|||
# Copy minified frontend from minifier stage
|
||||
COPY --from=minifier /build/frontend ./frontend
|
||||
|
||||
# Copy other application files
|
||||
# Copy application files
|
||||
COPY backend ./backend
|
||||
COPY mcp_server ./mcp_server
|
||||
COPY config.yaml .
|
||||
COPY VERSION .
|
||||
COPY run.py .
|
||||
COPY pyproject.toml .
|
||||
COPY plugins ./plugins
|
||||
COPY themes ./themes
|
||||
COPY locales ./locales
|
||||
|
|
|
|||
|
|
@ -356,6 +356,32 @@ NoteDiscovery can be installed as a standalone app on your device:
|
|||
|
||||
📄 **See [AUTHENTICATION.md](AUTHENTICATION.md)** for setup guide.
|
||||
|
||||
## 🤖 AI Integration (MCP)
|
||||
|
||||
Built-in **Model Context Protocol (MCP)** server for AI assistant integration:
|
||||
|
||||
- **Search notes** - AI can search through your knowledge base
|
||||
- **Read content** - AI can read and understand your notes
|
||||
- **Browse tags** - AI understands your organization
|
||||
- **Create notes** - AI can save summaries and insights
|
||||
- **Knowledge graph** - AI can explore note relationships
|
||||
- **Zero setup** - Works with Docker or Python, just add config to Cursor/Claude
|
||||
|
||||
### Quick Setup (Docker)
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"notediscovery": {
|
||||
"command": "docker",
|
||||
"args": ["run", "--rm", "-i", "ghcr.io/gamosoft/notediscovery:latest", "python", "-m", "mcp_server"],
|
||||
"env": { "NOTEDISCOVERY_URL": "http://host.docker.internal:8000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
📄 **See [MCP.md](MCP.md)** for complete setup guide.
|
||||
|
||||
## 🚀 Performance
|
||||
|
||||
- **Instant loading** - No lag, no loading spinners
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
# MCP Integration (AI Assistants)
|
||||
|
||||
NoteDiscovery includes a built-in **Model Context Protocol (MCP)** server that enables AI assistants like **Cursor**, **Claude Desktop**, and other MCP-compatible clients to interact with your notes.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
MCP (Model Context Protocol) is an open standard that allows AI assistants to securely access external tools and data sources. With the NoteDiscovery MCP server, your AI assistant can:
|
||||
|
||||
- 🔍 **Search** through your notes
|
||||
- 📖 **Read** note contents
|
||||
- 🏷️ **Browse** by tags
|
||||
- 📝 **Create** new notes
|
||||
- 🔗 **Explore** the knowledge graph
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### If You Use Docker
|
||||
|
||||
Add this to your `~/.cursor/mcp.json` (or Claude Desktop config):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"notediscovery": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run", "--rm", "-i",
|
||||
"-e", "NOTEDISCOVERY_URL",
|
||||
"-e", "NOTEDISCOVERY_API_KEY",
|
||||
"ghcr.io/gamosoft/notediscovery:latest",
|
||||
"python", "-m", "mcp_server"
|
||||
],
|
||||
"env": {
|
||||
"NOTEDISCOVERY_URL": "http://host.docker.internal:8000",
|
||||
"NOTEDISCOVERY_API_KEY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### If You Use Python
|
||||
|
||||
1. **Install NoteDiscovery** (if not already):
|
||||
```bash
|
||||
pip install notediscovery
|
||||
# or from source:
|
||||
pip install .
|
||||
```
|
||||
|
||||
2. **Add to your MCP config:**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"notediscovery": {
|
||||
"command": "notediscovery-mcp",
|
||||
"env": {
|
||||
"NOTEDISCOVERY_URL": "http://localhost:8000",
|
||||
"NOTEDISCOVERY_API_KEY": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running from Source (No Install)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"notediscovery": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server"],
|
||||
"cwd": "/path/to/NoteDiscovery",
|
||||
"env": {
|
||||
"PYTHONPATH": "/path/to/NoteDiscovery",
|
||||
"NOTEDISCOVERY_URL": "http://localhost:8000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** The `PYTHONPATH` is required so Python can find the `mcp_server` module. On Windows, use backslashes: `"PYTHONPATH": "C:\\path\\to\\NoteDiscovery"`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `NOTEDISCOVERY_URL` | Yes | `http://localhost:8000` | URL where NoteDiscovery is running |
|
||||
| `NOTEDISCOVERY_API_KEY` | If auth enabled | - | API key from `config.yaml` |
|
||||
| `NOTEDISCOVERY_TIMEOUT` | No | `30` | Request timeout in seconds |
|
||||
| `NOTEDISCOVERY_MAX_RETRIES` | No | `3` | Max retry attempts for failed requests |
|
||||
|
||||
### URL Configuration by Setup
|
||||
|
||||
| Your Setup | `NOTEDISCOVERY_URL` |
|
||||
|------------|---------------------|
|
||||
| Local Python (`run.py`) | `http://localhost:8000` |
|
||||
| Docker with `-p 8000:8000` | `http://host.docker.internal:8000` |
|
||||
| Docker with `-p 3000:8000` | `http://host.docker.internal:3000` |
|
||||
| Remote server | `https://notes.example.com` |
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server provides these tools to AI assistants:
|
||||
|
||||
### Search & Discovery
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `search_notes` | Full-text search across all notes |
|
||||
| `list_notes` | List all notes with metadata |
|
||||
| `get_note` | Read a specific note's content |
|
||||
|
||||
### Organization
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_tags` | List all tags with note counts |
|
||||
| `get_notes_by_tag` | Find notes with a specific tag |
|
||||
| `get_graph` | Get knowledge graph data |
|
||||
|
||||
### Note Management
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `create_note` | Create or update a note |
|
||||
| `delete_note` | Delete a note |
|
||||
| `create_folder` | Create a new folder |
|
||||
|
||||
### Templates
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_templates` | List available templates |
|
||||
| `get_template` | Get template content |
|
||||
|
||||
### System
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `health_check` | Verify server connectivity |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Once configured, you can interact with your notes naturally:
|
||||
|
||||
> **User:** "What did I write about Kubernetes?"
|
||||
>
|
||||
> **AI:** *Uses `search_notes` to find relevant notes, then `get_note` to read them*
|
||||
>
|
||||
> "I found 3 notes about Kubernetes. In your 'devops/k8s-setup.md' note from last week, you documented..."
|
||||
|
||||
> **User:** "Create a new note summarizing our conversation"
|
||||
>
|
||||
> **AI:** *Uses `create_note` to save the summary*
|
||||
>
|
||||
> "Done! I've created 'meetings/ai-discussion-2024-03-13.md' with the summary."
|
||||
|
||||
> **User:** "Show me all notes tagged with #project"
|
||||
>
|
||||
> **AI:** *Uses `get_notes_by_tag` to find them*
|
||||
>
|
||||
> "You have 7 notes with the #project tag..."
|
||||
|
||||
## Authentication
|
||||
|
||||
If you have authentication enabled in NoteDiscovery:
|
||||
|
||||
1. Generate an API key in `config.yaml`:
|
||||
```yaml
|
||||
authentication:
|
||||
enabled: true
|
||||
api_key: "your-secure-api-key-here"
|
||||
```
|
||||
|
||||
2. Add the key to your MCP config:
|
||||
```json
|
||||
"env": {
|
||||
"NOTEDISCOVERY_URL": "http://localhost:8000",
|
||||
"NOTEDISCOVERY_API_KEY": "your-secure-api-key-here"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" error
|
||||
|
||||
- Ensure NoteDiscovery is running
|
||||
- Check the `NOTEDISCOVERY_URL` is correct
|
||||
- For Docker: use `host.docker.internal` instead of `localhost`
|
||||
|
||||
### "Not authenticated" error
|
||||
|
||||
- Check that your API key is correct
|
||||
- Ensure the API key in MCP config matches `config.yaml`
|
||||
|
||||
### MCP server not starting
|
||||
|
||||
- Check Cursor/Claude Desktop logs for errors
|
||||
- Try running manually: `python -m mcp_server`
|
||||
- Verify Python 3.10+ is installed
|
||||
|
||||
### Verify connectivity manually
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export NOTEDISCOVERY_URL=http://localhost:8000
|
||||
export NOTEDISCOVERY_API_KEY=your-key
|
||||
|
||||
# Run the MCP server (Ctrl+C to stop)
|
||||
python -m mcp_server
|
||||
```
|
||||
|
||||
Then in another terminal:
|
||||
```bash
|
||||
# Test the health endpoint directly
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ stdio (JSON-RPC) ┌─────────────────┐
|
||||
│ AI Assistant │ ◄──────────────────────► │ MCP Server │
|
||||
│ (Cursor/Claude) │ │ (notediscovery- │
|
||||
└─────────────────┘ │ mcp) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
│ HTTP/REST
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ NoteDiscovery │
|
||||
│ Server │
|
||||
│ (port 8000) │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Your Notes │
|
||||
│ (./data/*.md) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
The MCP server is a **separate process** that:
|
||||
1. Communicates with AI assistants via stdio (stdin/stdout)
|
||||
2. Translates MCP requests into HTTP API calls
|
||||
3. Returns results back to the AI assistant
|
||||
|
||||
Your notes stay local. The MCP server just provides a bridge for AI access.
|
||||
|
||||
## Privacy & Security
|
||||
|
||||
- **Notes stay local**: The MCP server only accesses notes through NoteDiscovery's API
|
||||
- **No external calls**: No data is sent to external services
|
||||
- **API key protected**: Use authentication to control access
|
||||
- **Read what you allow**: AI can only access notes NoteDiscovery serves
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
NoteDiscovery/
|
||||
├── mcp_server/
|
||||
│ ├── __init__.py # Package entry point
|
||||
│ ├── __main__.py # Module runner
|
||||
│ ├── server.py # MCP protocol implementation
|
||||
│ ├── client.py # HTTP client for NoteDiscovery API
|
||||
│ ├── config.py # Configuration management
|
||||
│ └── tools.py # Tool definitions
|
||||
└── ...
|
||||
```
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
"""
|
||||
NoteDiscovery MCP Server
|
||||
|
||||
A Model Context Protocol (MCP) server that enables AI assistants
|
||||
to interact with NoteDiscovery notes.
|
||||
|
||||
Usage:
|
||||
# As module
|
||||
python -m mcp_server
|
||||
|
||||
# As installed CLI
|
||||
notediscovery-mcp
|
||||
|
||||
Environment Variables:
|
||||
NOTEDISCOVERY_URL: NoteDiscovery server URL (default: http://localhost:8000)
|
||||
NOTEDISCOVERY_API_KEY: API key for authentication (optional)
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "NoteDiscovery"
|
||||
|
||||
from .server import main
|
||||
|
||||
__all__ = ["main", "__version__"]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
Entry point for running the MCP server as a module.
|
||||
|
||||
Usage:
|
||||
python -m mcp_server
|
||||
"""
|
||||
|
||||
from .server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
"""
|
||||
HTTP client for NoteDiscovery API.
|
||||
|
||||
Provides a clean interface to all NoteDiscovery API endpoints
|
||||
with proper error handling, retries, and timeout management.
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
from typing import Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .config import MCPConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class APIResponse:
|
||||
"""Represents an API response."""
|
||||
success: bool
|
||||
data: Any
|
||||
error: Optional[str] = None
|
||||
status_code: int = 200
|
||||
|
||||
|
||||
class NoteDiscoveryClient:
|
||||
"""
|
||||
HTTP client for NoteDiscovery API.
|
||||
|
||||
Uses only stdlib (urllib) for minimal dependencies.
|
||||
Handles authentication, retries, and error formatting.
|
||||
"""
|
||||
|
||||
def __init__(self, config: MCPConfig) -> None:
|
||||
"""
|
||||
Initialize the client.
|
||||
|
||||
Args:
|
||||
config: MCP configuration object
|
||||
"""
|
||||
self.config = config
|
||||
self.base_url = config.base_url
|
||||
self.headers = config.headers
|
||||
self.timeout = config.timeout
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: Optional[dict[str, str]] = None,
|
||||
data: Optional[dict[str, Any]] = None,
|
||||
) -> APIResponse:
|
||||
"""
|
||||
Make an HTTP request to the API.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, DELETE, etc.)
|
||||
endpoint: API endpoint (e.g., "/api/notes")
|
||||
params: Query parameters
|
||||
data: JSON body data
|
||||
|
||||
Returns:
|
||||
APIResponse with success status and data/error
|
||||
"""
|
||||
# Build URL with query parameters
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
if params:
|
||||
query_string = urllib.parse.urlencode(params)
|
||||
url = f"{url}?{query_string}"
|
||||
|
||||
# Prepare request body
|
||||
body = None
|
||||
if data is not None:
|
||||
body = json.dumps(data).encode("utf-8")
|
||||
|
||||
# Create request
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers=self.headers,
|
||||
method=method,
|
||||
)
|
||||
|
||||
# Execute with retries
|
||||
last_error = None
|
||||
for attempt in range(self.config.max_retries):
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
||||
response_data = response.read().decode("utf-8")
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=json.loads(response_data) if response_data else None,
|
||||
status_code=response.status,
|
||||
)
|
||||
except urllib.error.HTTPError as e:
|
||||
# HTTP error (4xx, 5xx)
|
||||
error_body = ""
|
||||
try:
|
||||
error_body = e.read().decode("utf-8")
|
||||
error_detail = json.loads(error_body).get("detail", error_body)
|
||||
except Exception:
|
||||
error_detail = error_body or str(e)
|
||||
|
||||
return APIResponse(
|
||||
success=False,
|
||||
data=None,
|
||||
error=f"HTTP {e.code}: {error_detail}",
|
||||
status_code=e.code,
|
||||
)
|
||||
except urllib.error.URLError as e:
|
||||
# Network error - retry
|
||||
last_error = f"Connection error: {e.reason}"
|
||||
continue
|
||||
except TimeoutError:
|
||||
last_error = f"Request timed out after {self.timeout}s"
|
||||
continue
|
||||
except json.JSONDecodeError as e:
|
||||
return APIResponse(
|
||||
success=False,
|
||||
data=None,
|
||||
error=f"Invalid JSON response: {e}",
|
||||
)
|
||||
|
||||
# All retries exhausted
|
||||
return APIResponse(
|
||||
success=False,
|
||||
data=None,
|
||||
error=last_error or "Unknown error after retries",
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Notes API
|
||||
# =========================================================================
|
||||
|
||||
def list_notes(self) -> APIResponse:
|
||||
"""
|
||||
List all notes with metadata.
|
||||
|
||||
Returns:
|
||||
APIResponse with notes and folders data
|
||||
"""
|
||||
return self._request("GET", "/api/notes")
|
||||
|
||||
def get_note(self, path: str) -> APIResponse:
|
||||
"""
|
||||
Get a specific note's content.
|
||||
|
||||
Args:
|
||||
path: Note path (e.g., "folder/note.md")
|
||||
|
||||
Returns:
|
||||
APIResponse with note content and metadata
|
||||
"""
|
||||
# URL-encode the path
|
||||
encoded_path = urllib.parse.quote(path, safe="")
|
||||
return self._request("GET", f"/api/notes/{encoded_path}")
|
||||
|
||||
def create_note(self, path: str, content: str) -> APIResponse:
|
||||
"""
|
||||
Create or update a note.
|
||||
|
||||
Args:
|
||||
path: Note path
|
||||
content: Markdown content
|
||||
|
||||
Returns:
|
||||
APIResponse with creation result
|
||||
"""
|
||||
encoded_path = urllib.parse.quote(path, safe="")
|
||||
return self._request("POST", f"/api/notes/{encoded_path}", data={"content": content})
|
||||
|
||||
def delete_note(self, path: str) -> APIResponse:
|
||||
"""
|
||||
Delete a note.
|
||||
|
||||
Args:
|
||||
path: Note path
|
||||
|
||||
Returns:
|
||||
APIResponse with deletion result
|
||||
"""
|
||||
encoded_path = urllib.parse.quote(path, safe="")
|
||||
return self._request("DELETE", f"/api/notes/{encoded_path}")
|
||||
|
||||
# =========================================================================
|
||||
# Search API
|
||||
# =========================================================================
|
||||
|
||||
def search(self, query: str) -> APIResponse:
|
||||
"""
|
||||
Search notes by query.
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
|
||||
Returns:
|
||||
APIResponse with search results
|
||||
"""
|
||||
return self._request("GET", "/api/search", params={"q": query})
|
||||
|
||||
# =========================================================================
|
||||
# Tags API
|
||||
# =========================================================================
|
||||
|
||||
def list_tags(self) -> APIResponse:
|
||||
"""
|
||||
List all tags with note counts.
|
||||
|
||||
Returns:
|
||||
APIResponse with tags data
|
||||
"""
|
||||
return self._request("GET", "/api/tags")
|
||||
|
||||
def get_notes_by_tag(self, tag: str) -> APIResponse:
|
||||
"""
|
||||
Get notes with a specific tag.
|
||||
|
||||
Args:
|
||||
tag: Tag name
|
||||
|
||||
Returns:
|
||||
APIResponse with matching notes
|
||||
"""
|
||||
encoded_tag = urllib.parse.quote(tag, safe="")
|
||||
return self._request("GET", f"/api/tags/{encoded_tag}")
|
||||
|
||||
# =========================================================================
|
||||
# Folders API
|
||||
# =========================================================================
|
||||
|
||||
def create_folder(self, path: str) -> APIResponse:
|
||||
"""
|
||||
Create a new folder.
|
||||
|
||||
Args:
|
||||
path: Folder path
|
||||
|
||||
Returns:
|
||||
APIResponse with creation result
|
||||
"""
|
||||
return self._request("POST", "/api/folders", data={"path": path})
|
||||
|
||||
# =========================================================================
|
||||
# Graph API
|
||||
# =========================================================================
|
||||
|
||||
def get_graph(self) -> APIResponse:
|
||||
"""
|
||||
Get note relationship graph data.
|
||||
|
||||
Returns:
|
||||
APIResponse with graph nodes and links
|
||||
"""
|
||||
return self._request("GET", "/api/graph")
|
||||
|
||||
# =========================================================================
|
||||
# Templates API
|
||||
# =========================================================================
|
||||
|
||||
def list_templates(self) -> APIResponse:
|
||||
"""
|
||||
List available note templates.
|
||||
|
||||
Returns:
|
||||
APIResponse with templates list
|
||||
"""
|
||||
return self._request("GET", "/api/templates")
|
||||
|
||||
def get_template(self, name: str) -> APIResponse:
|
||||
"""
|
||||
Get a specific template's content.
|
||||
|
||||
Args:
|
||||
name: Template name
|
||||
|
||||
Returns:
|
||||
APIResponse with template content
|
||||
"""
|
||||
encoded_name = urllib.parse.quote(name, safe="")
|
||||
return self._request("GET", f"/api/templates/{encoded_name}")
|
||||
|
||||
# =========================================================================
|
||||
# System API
|
||||
# =========================================================================
|
||||
|
||||
def health_check(self) -> APIResponse:
|
||||
"""
|
||||
Check if NoteDiscovery server is healthy.
|
||||
|
||||
Returns:
|
||||
APIResponse with health status
|
||||
"""
|
||||
return self._request("GET", "/health")
|
||||
|
||||
def get_config(self) -> APIResponse:
|
||||
"""
|
||||
Get NoteDiscovery configuration.
|
||||
|
||||
Returns:
|
||||
APIResponse with config data
|
||||
"""
|
||||
return self._request("GET", "/api/config")
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
Configuration management for the MCP server.
|
||||
|
||||
Handles environment variables and provides validated configuration.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MCPConfig:
|
||||
"""Immutable configuration for the MCP server."""
|
||||
|
||||
base_url: str
|
||||
api_key: Optional[str]
|
||||
timeout: float
|
||||
max_retries: int
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate configuration after initialization."""
|
||||
# Validate URL format
|
||||
parsed = urlparse(self.base_url)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
raise ValueError(f"Invalid NOTEDISCOVERY_URL: {self.base_url}")
|
||||
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise ValueError(f"URL must use http or https: {self.base_url}")
|
||||
|
||||
@property
|
||||
def headers(self) -> dict[str, str]:
|
||||
"""Get HTTP headers for API requests."""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": "NoteDiscovery-MCP/1.0",
|
||||
}
|
||||
if self.api_key:
|
||||
headers["X-API-Key"] = self.api_key
|
||||
return headers
|
||||
|
||||
|
||||
def load_config() -> MCPConfig:
|
||||
"""
|
||||
Load configuration from environment variables.
|
||||
|
||||
Environment Variables:
|
||||
NOTEDISCOVERY_URL: Server URL (default: http://localhost:8000)
|
||||
NOTEDISCOVERY_API_KEY: API key for authentication (optional)
|
||||
NOTEDISCOVERY_TIMEOUT: Request timeout in seconds (default: 30)
|
||||
NOTEDISCOVERY_MAX_RETRIES: Max retry attempts (default: 3)
|
||||
|
||||
Returns:
|
||||
MCPConfig: Validated configuration object
|
||||
|
||||
Raises:
|
||||
ValueError: If configuration is invalid
|
||||
"""
|
||||
base_url = os.getenv("NOTEDISCOVERY_URL", "http://localhost:8000").rstrip("/")
|
||||
api_key = os.getenv("NOTEDISCOVERY_API_KEY", "").strip() or None
|
||||
|
||||
try:
|
||||
timeout = float(os.getenv("NOTEDISCOVERY_TIMEOUT", "30"))
|
||||
except ValueError:
|
||||
timeout = 30.0
|
||||
|
||||
try:
|
||||
max_retries = int(os.getenv("NOTEDISCOVERY_MAX_RETRIES", "3"))
|
||||
except ValueError:
|
||||
max_retries = 3
|
||||
|
||||
return MCPConfig(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
timeout=timeout,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
"""
|
||||
MCP Server implementation for NoteDiscovery.
|
||||
|
||||
Implements the Model Context Protocol (MCP) over stdio,
|
||||
enabling AI assistants to interact with NoteDiscovery notes.
|
||||
|
||||
This implementation uses only Python stdlib for minimal dependencies.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Any, Optional
|
||||
|
||||
from .config import load_config, MCPConfig
|
||||
from .client import NoteDiscoveryClient, APIResponse
|
||||
from .tools import TOOLS, get_tool_names
|
||||
|
||||
|
||||
# MCP Protocol version
|
||||
MCP_VERSION = "2024-11-05"
|
||||
|
||||
# Server info
|
||||
SERVER_INFO = {
|
||||
"name": "notediscovery-mcp",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
|
||||
class MCPServer:
|
||||
"""
|
||||
MCP Server that bridges AI assistants with NoteDiscovery.
|
||||
|
||||
Implements the MCP protocol over stdio (JSON-RPC 2.0).
|
||||
"""
|
||||
|
||||
def __init__(self, config: MCPConfig) -> None:
|
||||
"""
|
||||
Initialize the MCP server.
|
||||
|
||||
Args:
|
||||
config: MCP configuration
|
||||
"""
|
||||
self.config = config
|
||||
self.client = NoteDiscoveryClient(config)
|
||||
self._initialized = False
|
||||
|
||||
def _log(self, message: str) -> None:
|
||||
"""Log message to stderr (not stdout which is for MCP protocol)."""
|
||||
print(f"[notediscovery-mcp] {message}", file=sys.stderr)
|
||||
|
||||
def _send_response(self, id: Any, result: Any = None, error: Optional[dict] = None) -> None:
|
||||
"""
|
||||
Send a JSON-RPC response.
|
||||
|
||||
Args:
|
||||
id: Request ID
|
||||
result: Result data (for success)
|
||||
error: Error object (for failure)
|
||||
"""
|
||||
response: dict[str, Any] = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
}
|
||||
|
||||
if error is not None:
|
||||
response["error"] = error
|
||||
else:
|
||||
response["result"] = result
|
||||
|
||||
# Write to stdout with newline
|
||||
print(json.dumps(response), flush=True)
|
||||
|
||||
def _send_notification(self, method: str, params: Optional[dict] = None) -> None:
|
||||
"""
|
||||
Send a JSON-RPC notification (no response expected).
|
||||
|
||||
Args:
|
||||
method: Notification method
|
||||
params: Optional parameters
|
||||
"""
|
||||
notification: dict[str, Any] = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
}
|
||||
if params is not None:
|
||||
notification["params"] = params
|
||||
|
||||
print(json.dumps(notification), flush=True)
|
||||
|
||||
def _error(self, code: int, message: str, data: Any = None) -> dict:
|
||||
"""Create a JSON-RPC error object."""
|
||||
error = {"code": code, "message": message}
|
||||
if data is not None:
|
||||
error["data"] = data
|
||||
return error
|
||||
|
||||
# =========================================================================
|
||||
# MCP Protocol Handlers
|
||||
# =========================================================================
|
||||
|
||||
def handle_initialize(self, params: dict) -> dict:
|
||||
"""Handle initialize request."""
|
||||
self._initialized = True
|
||||
self._log(f"Initialized with client: {params.get('clientInfo', {}).get('name', 'unknown')}")
|
||||
|
||||
return {
|
||||
"protocolVersion": MCP_VERSION,
|
||||
"serverInfo": SERVER_INFO,
|
||||
"capabilities": {
|
||||
"tools": {}, # We support tools
|
||||
},
|
||||
}
|
||||
|
||||
def handle_initialized(self, params: dict) -> None:
|
||||
"""Handle initialized notification."""
|
||||
self._log(f"Connected to NoteDiscovery at {self.config.base_url}")
|
||||
|
||||
def handle_list_tools(self, params: dict) -> dict:
|
||||
"""Handle tools/list request."""
|
||||
return {"tools": TOOLS}
|
||||
|
||||
def handle_call_tool(self, params: dict) -> dict:
|
||||
"""
|
||||
Handle tools/call request.
|
||||
|
||||
Dispatches to the appropriate tool handler based on tool name.
|
||||
"""
|
||||
name = params.get("name", "")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
self._log(f"Calling tool: {name}")
|
||||
|
||||
# Dispatch to tool handler
|
||||
handler = getattr(self, f"_tool_{name}", None)
|
||||
if handler is None:
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Unknown tool: {name}. Available tools: {', '.join(get_tool_names())}",
|
||||
}],
|
||||
"isError": True,
|
||||
}
|
||||
|
||||
try:
|
||||
result = handler(arguments)
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": result,
|
||||
}],
|
||||
}
|
||||
except Exception as e:
|
||||
self._log(f"Tool error: {e}")
|
||||
return {
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": f"Error executing {name}: {str(e)}",
|
||||
}],
|
||||
"isError": True,
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# Tool Implementations
|
||||
# =========================================================================
|
||||
|
||||
def _format_response(self, response: APIResponse) -> str:
|
||||
"""Format API response as readable text."""
|
||||
if not response.success:
|
||||
return f"Error: {response.error}"
|
||||
|
||||
if response.data is None:
|
||||
return "Success (no data)"
|
||||
|
||||
# Pretty print JSON
|
||||
return json.dumps(response.data, indent=2, ensure_ascii=False)
|
||||
|
||||
def _tool_search_notes(self, args: dict) -> str:
|
||||
"""Search notes by query."""
|
||||
query = args.get("query", "")
|
||||
if not query:
|
||||
return "Error: query is required"
|
||||
|
||||
response = self.client.search(query)
|
||||
|
||||
if not response.success:
|
||||
return f"Search failed: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
results = data.get("results", [])
|
||||
|
||||
if not results:
|
||||
return f"No notes found matching '{query}'"
|
||||
|
||||
# Format search results
|
||||
output = [f"Found {len(results)} result(s) for '{query}':\n"]
|
||||
for i, result in enumerate(results[:20], 1): # Limit to 20 results
|
||||
path = result.get("path", "unknown")
|
||||
snippet = result.get("snippet", "")
|
||||
output.append(f"{i}. **{path}**")
|
||||
if snippet:
|
||||
output.append(f" {snippet[:200]}...")
|
||||
output.append("")
|
||||
|
||||
if len(results) > 20:
|
||||
output.append(f"... and {len(results) - 20} more results")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_list_notes(self, args: dict) -> str:
|
||||
"""List all notes."""
|
||||
response = self.client.list_notes()
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to list notes: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
notes = data.get("notes", [])
|
||||
folders = data.get("folders", [])
|
||||
|
||||
output = [f"Found {len(notes)} note(s) in {len(folders)} folder(s):\n"]
|
||||
|
||||
# Group notes by folder
|
||||
notes_by_folder: dict[str, list] = {}
|
||||
for note in notes:
|
||||
path = note.get("path", "")
|
||||
folder = "/".join(path.split("/")[:-1]) or "(root)"
|
||||
if folder not in notes_by_folder:
|
||||
notes_by_folder[folder] = []
|
||||
notes_by_folder[folder].append(note)
|
||||
|
||||
for folder in sorted(notes_by_folder.keys()):
|
||||
output.append(f"📁 {folder}/")
|
||||
for note in notes_by_folder[folder]:
|
||||
name = note.get("name", "unknown")
|
||||
modified = note.get("modified", "")
|
||||
output.append(f" 📝 {name} (modified: {modified})")
|
||||
output.append("")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_get_note(self, args: dict) -> str:
|
||||
"""Get note content."""
|
||||
path = args.get("path", "")
|
||||
if not path:
|
||||
return "Error: path is required"
|
||||
|
||||
response = self.client.get_note(path)
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to get note: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
content = data.get("content", "")
|
||||
metadata = data.get("metadata", {})
|
||||
|
||||
output = [f"# {path}\n"]
|
||||
if metadata:
|
||||
output.append(f"Modified: {metadata.get('modified', 'unknown')}")
|
||||
output.append(f"Size: {metadata.get('size', 0)} bytes\n")
|
||||
output.append("---\n")
|
||||
output.append(content)
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_list_tags(self, args: dict) -> str:
|
||||
"""List all tags."""
|
||||
response = self.client.list_tags()
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to list tags: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
tags = data.get("tags", {})
|
||||
|
||||
if not tags:
|
||||
return "No tags found in any notes."
|
||||
|
||||
output = [f"Found {len(tags)} tag(s):\n"]
|
||||
# tags is a dict: {"tag_name": count, ...}
|
||||
for name, count in sorted(tags.items(), key=lambda x: x[1], reverse=True):
|
||||
output.append(f" #{name} ({count} note{'s' if count != 1 else ''})")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_get_notes_by_tag(self, args: dict) -> str:
|
||||
"""Get notes with a specific tag."""
|
||||
tag = args.get("tag", "")
|
||||
if not tag:
|
||||
return "Error: tag is required"
|
||||
|
||||
response = self.client.get_notes_by_tag(tag)
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to get notes by tag: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
notes = data.get("notes", [])
|
||||
|
||||
if not notes:
|
||||
return f"No notes found with tag '#{tag}'"
|
||||
|
||||
output = [f"Notes with tag #{tag}:\n"]
|
||||
for note in notes:
|
||||
path = note.get("path", "unknown")
|
||||
output.append(f" 📝 {path}")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_get_graph(self, args: dict) -> str:
|
||||
"""Get knowledge graph data."""
|
||||
response = self.client.get_graph()
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to get graph: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
nodes = data.get("nodes", [])
|
||||
links = data.get("links", [])
|
||||
|
||||
output = [f"Knowledge Graph: {len(nodes)} nodes, {len(links)} connections\n"]
|
||||
|
||||
# Find most connected notes
|
||||
connection_count: dict[str, int] = {}
|
||||
for link in links:
|
||||
source = link.get("source", "")
|
||||
target = link.get("target", "")
|
||||
connection_count[source] = connection_count.get(source, 0) + 1
|
||||
connection_count[target] = connection_count.get(target, 0) + 1
|
||||
|
||||
if connection_count:
|
||||
output.append("Most connected notes:")
|
||||
sorted_notes = sorted(connection_count.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
for note, count in sorted_notes:
|
||||
output.append(f" {note}: {count} connections")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_create_note(self, args: dict) -> str:
|
||||
"""Create or update a note."""
|
||||
path = args.get("path", "")
|
||||
content = args.get("content", "")
|
||||
|
||||
if not path:
|
||||
return "Error: path is required"
|
||||
if not content:
|
||||
return "Error: content is required"
|
||||
|
||||
response = self.client.create_note(path, content)
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to create note: {response.error}"
|
||||
|
||||
return f"✅ Note created/updated: {path}"
|
||||
|
||||
def _tool_delete_note(self, args: dict) -> str:
|
||||
"""Delete a note."""
|
||||
path = args.get("path", "")
|
||||
if not path:
|
||||
return "Error: path is required"
|
||||
|
||||
response = self.client.delete_note(path)
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to delete note: {response.error}"
|
||||
|
||||
return f"🗑️ Note deleted: {path}"
|
||||
|
||||
def _tool_create_folder(self, args: dict) -> str:
|
||||
"""Create a folder."""
|
||||
path = args.get("path", "")
|
||||
if not path:
|
||||
return "Error: path is required"
|
||||
|
||||
response = self.client.create_folder(path)
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to create folder: {response.error}"
|
||||
|
||||
return f"📁 Folder created: {path}"
|
||||
|
||||
def _tool_list_templates(self, args: dict) -> str:
|
||||
"""List templates."""
|
||||
response = self.client.list_templates()
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to list templates: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
templates = data.get("templates", [])
|
||||
|
||||
if not templates:
|
||||
return "No templates available."
|
||||
|
||||
output = ["Available templates:\n"]
|
||||
for template in templates:
|
||||
name = template.get("name", "unknown")
|
||||
output.append(f" 📄 {name}")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def _tool_get_template(self, args: dict) -> str:
|
||||
"""Get template content."""
|
||||
name = args.get("name", "")
|
||||
if not name:
|
||||
return "Error: name is required"
|
||||
|
||||
response = self.client.get_template(name)
|
||||
|
||||
if not response.success:
|
||||
return f"Failed to get template: {response.error}"
|
||||
|
||||
data = response.data or {}
|
||||
content = data.get("content", "")
|
||||
|
||||
return f"Template: {name}\n---\n{content}"
|
||||
|
||||
def _tool_health_check(self, args: dict) -> str:
|
||||
"""Check server health."""
|
||||
response = self.client.health_check()
|
||||
|
||||
if not response.success:
|
||||
return f"❌ NoteDiscovery is not reachable: {response.error}"
|
||||
|
||||
return f"✅ NoteDiscovery is healthy at {self.config.base_url}"
|
||||
|
||||
# =========================================================================
|
||||
# Main Loop
|
||||
# =========================================================================
|
||||
|
||||
def handle_request(self, request: dict) -> None:
|
||||
"""
|
||||
Handle a single JSON-RPC request.
|
||||
|
||||
Args:
|
||||
request: Parsed JSON-RPC request
|
||||
"""
|
||||
request_id = request.get("id")
|
||||
method = request.get("method", "")
|
||||
params = request.get("params", {})
|
||||
|
||||
try:
|
||||
# Route to handler
|
||||
if method == "initialize":
|
||||
result = self.handle_initialize(params)
|
||||
self._send_response(request_id, result)
|
||||
|
||||
elif method == "notifications/initialized":
|
||||
self.handle_initialized(params)
|
||||
# Notifications don't get responses
|
||||
|
||||
elif method == "tools/list":
|
||||
result = self.handle_list_tools(params)
|
||||
self._send_response(request_id, result)
|
||||
|
||||
elif method == "tools/call":
|
||||
result = self.handle_call_tool(params)
|
||||
self._send_response(request_id, result)
|
||||
|
||||
elif method == "ping":
|
||||
self._send_response(request_id, {})
|
||||
|
||||
else:
|
||||
# Unknown method
|
||||
if request_id is not None:
|
||||
self._send_response(
|
||||
request_id,
|
||||
error=self._error(-32601, f"Method not found: {method}")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Error handling {method}: {e}")
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
if request_id is not None:
|
||||
self._send_response(
|
||||
request_id,
|
||||
error=self._error(-32603, f"Internal error: {str(e)}")
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Run the MCP server, reading requests from stdin.
|
||||
|
||||
This is the main event loop that processes JSON-RPC messages.
|
||||
"""
|
||||
self._log("Starting MCP server...")
|
||||
self._log(f"NoteDiscovery URL: {self.config.base_url}")
|
||||
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
self.handle_request(request)
|
||||
except json.JSONDecodeError as e:
|
||||
self._log(f"Invalid JSON: {e}")
|
||||
# Send parse error
|
||||
self._send_response(
|
||||
None,
|
||||
error=self._error(-32700, f"Parse error: {str(e)}")
|
||||
)
|
||||
|
||||
self._log("Server shutting down")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Main entry point for the MCP server.
|
||||
|
||||
Loads configuration from environment variables and starts the server.
|
||||
"""
|
||||
try:
|
||||
config = load_config()
|
||||
except ValueError as e:
|
||||
print(f"Configuration error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
server = MCPServer(config)
|
||||
|
||||
try:
|
||||
server.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[notediscovery-mcp] Interrupted", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"[notediscovery-mcp] Fatal error: {e}", file=sys.stderr)
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
"""
|
||||
MCP Tool definitions for NoteDiscovery.
|
||||
|
||||
Defines all available tools, their schemas, and descriptions.
|
||||
Following MCP specification for tool definitions.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
# Tool definitions following MCP schema specification
|
||||
TOOLS: list[dict[str, Any]] = [
|
||||
# =========================================================================
|
||||
# Search & Discovery
|
||||
# =========================================================================
|
||||
{
|
||||
"name": "search_notes",
|
||||
"description": "Search through all notes using full-text search. Returns matching notes with snippets showing where the match was found. Use this to find notes by content, keywords, or phrases.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query. Can be keywords, phrases, or natural language."
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_notes",
|
||||
"description": "List all notes in the knowledge base with their metadata (title, path, last modified date, size). Use this to get an overview of available notes or find notes by browsing.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_note",
|
||||
"description": "Read the full content of a specific note by its path. Returns the complete markdown content along with metadata. Use this after finding a note via search or list to read its contents.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the note (e.g., 'folder/note.md' or 'note.md')"
|
||||
}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
|
||||
# =========================================================================
|
||||
# Tags & Organization
|
||||
# =========================================================================
|
||||
{
|
||||
"name": "list_tags",
|
||||
"description": "List all tags used across notes with the count of notes for each tag. Use this to understand how notes are organized and find topics.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_notes_by_tag",
|
||||
"description": "Get all notes that have a specific tag. Use this to find related notes on a topic.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"description": "Tag name (without the # symbol)"
|
||||
}
|
||||
},
|
||||
"required": ["tag"]
|
||||
}
|
||||
},
|
||||
|
||||
# =========================================================================
|
||||
# Knowledge Graph
|
||||
# =========================================================================
|
||||
{
|
||||
"name": "get_graph",
|
||||
"description": "Get the knowledge graph showing relationships between notes. Returns nodes (notes) and edges (links between them). Use this to understand how notes connect to each other.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
|
||||
# =========================================================================
|
||||
# Note Management (Write Operations)
|
||||
# =========================================================================
|
||||
{
|
||||
"name": "create_note",
|
||||
"description": "Create a new note or update an existing one. The note will be saved as a markdown file. Use this to save new information or update existing notes.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path for the note (e.g., 'folder/new-note.md'). Include .md extension."
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Markdown content for the note"
|
||||
}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete_note",
|
||||
"description": "Delete a note permanently. Use with caution - this cannot be undone.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path to the note to delete"
|
||||
}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create_folder",
|
||||
"description": "Create a new folder for organizing notes.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path for the new folder (e.g., 'projects/2024')"
|
||||
}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
|
||||
# =========================================================================
|
||||
# Templates
|
||||
# =========================================================================
|
||||
{
|
||||
"name": "list_templates",
|
||||
"description": "List available note templates. Templates provide pre-formatted structures for common note types.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_template",
|
||||
"description": "Get the content of a specific template. Use this to see what a template contains before using it.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Template name"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
},
|
||||
|
||||
# =========================================================================
|
||||
# System
|
||||
# =========================================================================
|
||||
{
|
||||
"name": "health_check",
|
||||
"description": "Check if NoteDiscovery server is running and healthy. Use this to verify connectivity.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_tool_names() -> list[str]:
|
||||
"""Get list of all tool names."""
|
||||
return [tool["name"] for tool in TOOLS]
|
||||
|
||||
|
||||
def get_tool_by_name(name: str) -> dict[str, Any] | None:
|
||||
"""Get tool definition by name."""
|
||||
for tool in TOOLS:
|
||||
if tool["name"] == name:
|
||||
return tool
|
||||
return None
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "notediscovery"
|
||||
dynamic = ["version"]
|
||||
description = "A beautiful, fast markdown notes discovery and browsing tool"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "NoteDiscovery", email = "contact@notediscovery.com"}
|
||||
]
|
||||
keywords = [
|
||||
"notes",
|
||||
"markdown",
|
||||
"knowledge-base",
|
||||
"personal-wiki",
|
||||
"note-taking",
|
||||
"mcp",
|
||||
"ai",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: FastAPI",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business :: News/Diary",
|
||||
"Topic :: Text Processing :: Markup :: Markdown",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi>=0.104.0",
|
||||
"uvicorn[standard]>=0.24.0",
|
||||
"python-multipart>=0.0.6",
|
||||
"markdown>=3.5.0",
|
||||
"pyyaml>=6.0",
|
||||
"aiofiles>=23.2.0",
|
||||
"cryptography>=41.0.0",
|
||||
"bcrypt>=4.1.0",
|
||||
"itsdangerous>=2.1.0",
|
||||
"slowapi>=0.1.9",
|
||||
"colorama>=0.4.6",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"httpx>=0.25.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://notediscovery.com"
|
||||
Documentation = "https://github.com/gamosoft/NoteDiscovery#readme"
|
||||
Repository = "https://github.com/gamosoft/NoteDiscovery"
|
||||
Issues = "https://github.com/gamosoft/NoteDiscovery/issues"
|
||||
|
||||
[project.scripts]
|
||||
# Main application
|
||||
notediscovery = "run:main"
|
||||
# MCP server for AI assistants
|
||||
notediscovery-mcp = "mcp_server:main"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["backend", "mcp_server", "plugins"]
|
||||
include-package-data = true
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {file = "VERSION"}
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = [
|
||||
"*.yaml",
|
||||
"*.json",
|
||||
"*.css",
|
||||
"*.html",
|
||||
"*.js",
|
||||
"*.svg",
|
||||
"*.md",
|
||||
]
|
||||
Loading…
Reference in New Issue