Merge pull request #242 from gamosoft/fix/mcp-server-cleanup
Fix/mcp server cleanup
This commit is contained in:
commit
58e4ef0eb5
|
|
@ -203,27 +203,28 @@ The MCP server provides these tools to AI assistants:
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `list_templates` | List available templates |
|
| `list_templates` | List available templates |
|
||||||
| `get_template` | Get template content |
|
| `get_template` | Get template content |
|
||||||
| `create_note_from_template` | Create a note from a template with variable substitution |
|
| `create_note_from_template` | Create a note from a template (built-in placeholders only) |
|
||||||
|
|
||||||
### System
|
### System
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `health_check` | Verify server connectivity |
|
| `health_check` | Verify server connectivity |
|
||||||
|
| `get_config` | Get server config (name, version, enabled features, autosave delay) |
|
||||||
|
|
||||||
## Tool Details
|
## Tool Details
|
||||||
|
|
||||||
### Pagination with `max_results` and `offset`
|
### Pagination with `limit` and `offset`
|
||||||
|
|
||||||
Some tools support optional pagination parameters for large vaults:
|
Some tools support optional pagination parameters for large vaults:
|
||||||
|
|
||||||
| Tool | Parameters | Description |
|
| Tool | Parameters | Description |
|
||||||
|------|------------|-------------|
|
|------|------------|-------------|
|
||||||
| `search_notes` | `max_results`, `offset` | Paginate search results |
|
| `search_notes` | `limit`, `offset` | Paginate search results |
|
||||||
| `list_notes` | `max_results`, `offset` | Paginate notes list |
|
| `list_notes` | `limit`, `offset` | Paginate notes list |
|
||||||
| `get_notes_by_tag` | `max_results`, `offset` | Paginate notes by tag |
|
| `get_notes_by_tag` | `limit`, `offset` | Paginate notes by tag |
|
||||||
|
|
||||||
- `max_results` - Maximum items to return (omit for all)
|
- `limit` - Maximum items to return (omit for all)
|
||||||
- `offset` - Number of items to skip (for pagination)
|
- `offset` - Number of items to skip (for pagination)
|
||||||
|
|
||||||
**Example prompts:**
|
**Example prompts:**
|
||||||
|
|
@ -291,15 +292,18 @@ Get recently modified notes. Useful for context about what you've been working o
|
||||||
|
|
||||||
### `create_note_from_template`
|
### `create_note_from_template`
|
||||||
|
|
||||||
Create a new note from a template with variable substitution.
|
Create a new note from a template. Built-in placeholders are substituted
|
||||||
|
server-side. Need to inject custom content? Call `update_note` after creation.
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| `template_name` | string | Yes | Template name (e.g., "meeting-notes") |
|
| `template_name` | string | Yes | Template name (e.g., "meeting-notes") |
|
||||||
| `note_path` | string | Yes | Path for the new note |
|
| `note_path` | string | Yes | Path for the new note |
|
||||||
| `variables` | object | No | Variables to substitute (e.g., `{"project": "Alpha"}`) |
|
|
||||||
|
|
||||||
**Built-in placeholders:** `{{date}}`, `{{time}}`, `{{datetime}}`, `{{title}}`, `{{folder}}`
|
**Built-in placeholders:** `{{title}}`, `{{date}}`, `{{time}}`, `{{datetime}}`,
|
||||||
|
`{{timestamp}}`, `{{year}}`, `{{month}}`, `{{day}}`, `{{folder}}`, plus the
|
||||||
|
`strftime` escape hatch — `{{date:FMT}}`, `{{time:FMT}}`, `{{datetime:FMT}}`
|
||||||
|
where `FMT` is a Python `strftime()` format string. See [TEMPLATES.md](TEMPLATES.md).
|
||||||
|
|
||||||
**Example prompt:** "Create a new meeting note for Project Alpha using the meeting-notes template"
|
**Example prompt:** "Create a new meeting note for Project Alpha using the meeting-notes template"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,33 @@ Environment Variables:
|
||||||
NOTEDISCOVERY_API_KEY: API key for authentication (optional)
|
NOTEDISCOVERY_API_KEY: API key for authentication (optional)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _read_version() -> str:
|
||||||
|
"""
|
||||||
|
Resolve the package version.
|
||||||
|
|
||||||
|
Order:
|
||||||
|
1. importlib.metadata — works when installed via pip/uvx/setuptools
|
||||||
|
2. VERSION file at repo root — works when running from source / Docker
|
||||||
|
3. fallback string — so the server still starts cleanly in odd setups
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
return version("notediscovery")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
version_file = Path(__file__).resolve().parent.parent / "VERSION"
|
||||||
|
if version_file.is_file():
|
||||||
|
return version_file.read_text(encoding="utf-8").strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "0.0.0+dev"
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = _read_version()
|
||||||
__author__ = "NoteDiscovery"
|
__author__ = "NoteDiscovery"
|
||||||
|
|
||||||
from .server import main
|
from .server import main
|
||||||
|
|
|
||||||
|
|
@ -163,16 +163,23 @@ class NoteDiscoveryClient:
|
||||||
def get_note(self, path: str) -> APIResponse:
|
def get_note(self, path: str) -> APIResponse:
|
||||||
"""
|
"""
|
||||||
Get a specific note's content.
|
Get a specific note's content.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Note path (e.g., "folder/note.md")
|
path: Note path (e.g., "folder/note.md")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
APIResponse with note content and metadata
|
APIResponse with note content and metadata
|
||||||
"""
|
"""
|
||||||
# URL-encode the path
|
# /api/notes/{path} defaults to include_backlinks=True server-side,
|
||||||
|
# which scans every note in the vault. The MCP get_note tool only
|
||||||
|
# surfaces content + metadata, so we skip the backlinks computation.
|
||||||
|
# Backlinks are still available via the dedicated get_backlinks method.
|
||||||
encoded_path = urllib.parse.quote(path, safe="")
|
encoded_path = urllib.parse.quote(path, safe="")
|
||||||
return self._request("GET", f"/api/notes/{encoded_path}")
|
return self._request(
|
||||||
|
"GET",
|
||||||
|
f"/api/notes/{encoded_path}",
|
||||||
|
params={"include_backlinks": "false"},
|
||||||
|
)
|
||||||
|
|
||||||
def create_note(self, path: str, content: str) -> APIResponse:
|
def create_note(self, path: str, content: str) -> APIResponse:
|
||||||
"""
|
"""
|
||||||
|
|
@ -241,16 +248,19 @@ class NoteDiscoveryClient:
|
||||||
self,
|
self,
|
||||||
template_name: str,
|
template_name: str,
|
||||||
note_path: str,
|
note_path: str,
|
||||||
variables: dict | None = None
|
|
||||||
) -> APIResponse:
|
) -> APIResponse:
|
||||||
"""
|
"""
|
||||||
Create a note from a template.
|
Create a note from a template.
|
||||||
|
|
||||||
|
Built-in placeholders ({{title}}, {{date}}, {{datetime}},
|
||||||
|
{{folder}}, {{date:FMT}}, etc.) are substituted server-side
|
||||||
|
by apply_template_placeholders. There is no per-call variable
|
||||||
|
injection; use update_note after creation for custom content.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
template_name: Name of the template
|
template_name: Name of the template
|
||||||
note_path: Path for the new note
|
note_path: Path for the new note
|
||||||
variables: Variables to substitute in the template
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
APIResponse with creation result
|
APIResponse with creation result
|
||||||
"""
|
"""
|
||||||
|
|
@ -259,9 +269,6 @@ class NoteDiscoveryClient:
|
||||||
"templateName": template_name,
|
"templateName": template_name,
|
||||||
"notePath": note_path,
|
"notePath": note_path,
|
||||||
}
|
}
|
||||||
if variables:
|
|
||||||
data["variables"] = variables
|
|
||||||
|
|
||||||
return self._request("POST", "/api/templates/create-note", data=data)
|
return self._request("POST", "/api/templates/create-note", data=data)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from .config import load_config, MCPConfig
|
from .config import load_config, MCPConfig
|
||||||
from .client import NoteDiscoveryClient, APIResponse
|
from .client import NoteDiscoveryClient, APIResponse
|
||||||
from .tools import TOOLS, get_tool_names
|
from .tools import TOOLS, get_tool_names
|
||||||
|
|
@ -21,10 +22,11 @@ from .tools import TOOLS, get_tool_names
|
||||||
# MCP Protocol version
|
# MCP Protocol version
|
||||||
MCP_VERSION = "2024-11-05"
|
MCP_VERSION = "2024-11-05"
|
||||||
|
|
||||||
# Server info
|
# Server info: version comes from the package (resolved against the VERSION
|
||||||
|
# file at repo root, or importlib.metadata when pip-installed).
|
||||||
SERVER_INFO = {
|
SERVER_INFO = {
|
||||||
"name": "notediscovery-mcp",
|
"name": "notediscovery-mcp",
|
||||||
"version": "1.0.0",
|
"version": __version__,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -208,14 +210,14 @@ class MCPServer:
|
||||||
def _tool_search_notes(self, args: dict) -> str:
|
def _tool_search_notes(self, args: dict) -> str:
|
||||||
"""Search notes by query."""
|
"""Search notes by query."""
|
||||||
query = args.get("query", "").strip()
|
query = args.get("query", "").strip()
|
||||||
max_results = args.get("max_results")
|
limit = args.get("limit")
|
||||||
offset = args.get("offset", 0)
|
offset = args.get("offset", 0)
|
||||||
|
|
||||||
if not query:
|
if not query:
|
||||||
return "No search term provided. Please specify a query."
|
return "No search term provided. Please specify a query."
|
||||||
|
|
||||||
# Use server-side pagination if max_results is specified
|
# Use server-side pagination if limit is specified
|
||||||
response = self.client.search(query, limit=max_results, offset=offset)
|
response = self.client.search(query, limit=limit, offset=offset)
|
||||||
|
|
||||||
if not response.success:
|
if not response.success:
|
||||||
return f"Search failed: {response.error}"
|
return f"Search failed: {response.error}"
|
||||||
|
|
@ -252,17 +254,17 @@ class MCPServer:
|
||||||
|
|
||||||
# Add pagination hint if there are more results
|
# Add pagination hint if there are more results
|
||||||
if pagination and pagination.get("has_more"):
|
if pagination and pagination.get("has_more"):
|
||||||
output.append(f"... more results available (use max_results and offset to paginate)")
|
output.append(f"... more results available (use limit and offset to paginate)")
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
def _tool_list_notes(self, args: dict) -> str:
|
def _tool_list_notes(self, args: dict) -> str:
|
||||||
"""List all notes."""
|
"""List all notes."""
|
||||||
max_results = args.get("max_results")
|
limit = args.get("limit")
|
||||||
offset = args.get("offset", 0)
|
offset = args.get("offset", 0)
|
||||||
|
|
||||||
# Use server-side pagination if max_results is specified
|
# Use server-side pagination if limit is specified
|
||||||
response = self.client.list_notes(limit=max_results, offset=offset)
|
response = self.client.list_notes(limit=limit, offset=offset)
|
||||||
|
|
||||||
if not response.success:
|
if not response.success:
|
||||||
return f"Failed to list notes: {response.error}"
|
return f"Failed to list notes: {response.error}"
|
||||||
|
|
@ -298,10 +300,10 @@ class MCPServer:
|
||||||
|
|
||||||
# Add pagination hint if there are more results
|
# Add pagination hint if there are more results
|
||||||
if pagination and pagination.get("has_more"):
|
if pagination and pagination.get("has_more"):
|
||||||
output.append(f"... more notes available (use max_results to limit)")
|
output.append(f"... more notes available (use limit and offset to paginate)")
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
def _tool_get_note(self, args: dict) -> str:
|
def _tool_get_note(self, args: dict) -> str:
|
||||||
"""Get note content."""
|
"""Get note content."""
|
||||||
path = args.get("path", "")
|
path = args.get("path", "")
|
||||||
|
|
@ -350,14 +352,14 @@ class MCPServer:
|
||||||
def _tool_get_notes_by_tag(self, args: dict) -> str:
|
def _tool_get_notes_by_tag(self, args: dict) -> str:
|
||||||
"""Get notes with a specific tag."""
|
"""Get notes with a specific tag."""
|
||||||
tag = args.get("tag", "")
|
tag = args.get("tag", "")
|
||||||
max_results = args.get("max_results")
|
limit = args.get("limit")
|
||||||
offset = args.get("offset", 0)
|
offset = args.get("offset", 0)
|
||||||
|
|
||||||
if not tag:
|
if not tag:
|
||||||
return "Error: tag is required"
|
return "Error: tag is required"
|
||||||
|
|
||||||
# Use server-side pagination if max_results is specified
|
# Use server-side pagination if limit is specified
|
||||||
response = self.client.get_notes_by_tag(tag, limit=max_results, offset=offset)
|
response = self.client.get_notes_by_tag(tag, limit=limit, offset=offset)
|
||||||
|
|
||||||
if not response.success:
|
if not response.success:
|
||||||
return f"Failed to get notes by tag: {response.error}"
|
return f"Failed to get notes by tag: {response.error}"
|
||||||
|
|
@ -382,7 +384,7 @@ class MCPServer:
|
||||||
|
|
||||||
# Add pagination hint if there are more results
|
# Add pagination hint if there are more results
|
||||||
if pagination and pagination.get("has_more"):
|
if pagination and pagination.get("has_more"):
|
||||||
output.append(f"\n... more notes available (use max_results to limit)")
|
output.append(f"\n... more notes available (use limit and offset to paginate)")
|
||||||
|
|
||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
|
|
@ -625,20 +627,19 @@ class MCPServer:
|
||||||
"""Create a note from a template."""
|
"""Create a note from a template."""
|
||||||
template_name = args.get("template_name", "")
|
template_name = args.get("template_name", "")
|
||||||
note_path = args.get("note_path", "")
|
note_path = args.get("note_path", "")
|
||||||
variables = args.get("variables", {})
|
|
||||||
|
|
||||||
if not template_name:
|
if not template_name:
|
||||||
return "Error: template_name is required"
|
return "Error: template_name is required"
|
||||||
|
|
||||||
is_valid, error = self._validate_path(note_path)
|
is_valid, error = self._validate_path(note_path)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
return f"Error: note_path - {error}"
|
return f"Error: note_path - {error}"
|
||||||
|
|
||||||
response = self.client.create_note_from_template(template_name, note_path, variables)
|
response = self.client.create_note_from_template(template_name, note_path)
|
||||||
|
|
||||||
if not response.success:
|
if not response.success:
|
||||||
return f"Failed to create note from template: {response.error}"
|
return f"Failed to create note from template: {response.error}"
|
||||||
|
|
||||||
return f"✅ Note created from template '{template_name}': {note_path}"
|
return f"✅ Note created from template '{template_name}': {note_path}"
|
||||||
|
|
||||||
def _tool_health_check(self, args: dict) -> str:
|
def _tool_health_check(self, args: dict) -> str:
|
||||||
|
|
@ -649,6 +650,26 @@ class MCPServer:
|
||||||
return f"❌ NoteDiscovery is not reachable: {response.error}"
|
return f"❌ NoteDiscovery is not reachable: {response.error}"
|
||||||
|
|
||||||
return f"✅ NoteDiscovery is healthy at {self.config.base_url}"
|
return f"✅ NoteDiscovery is healthy at {self.config.base_url}"
|
||||||
|
|
||||||
|
def _tool_get_config(self, args: dict) -> str:
|
||||||
|
"""Get NoteDiscovery server configuration."""
|
||||||
|
response = self.client.get_config()
|
||||||
|
|
||||||
|
if not response.success:
|
||||||
|
return f"Failed to get config: {response.error}"
|
||||||
|
|
||||||
|
data = response.data or {}
|
||||||
|
if not data:
|
||||||
|
return "Server returned empty configuration."
|
||||||
|
|
||||||
|
output = ["NoteDiscovery Server Configuration:\n"]
|
||||||
|
for key in sorted(data.keys()):
|
||||||
|
value = data[key]
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
value = json.dumps(value, ensure_ascii=False)
|
||||||
|
output.append(f" {key}: {value}")
|
||||||
|
|
||||||
|
return "\n".join(output)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Main Loop
|
# Main Loop
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,13 @@ TOOLS: list[dict[str, Any]] = [
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Search query. Can be keywords, phrases, or natural language."
|
"description": "Search query. Can be keywords, phrases, or natural language."
|
||||||
},
|
},
|
||||||
"max_results": {
|
"limit": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Maximum number of results to return. Useful for large vaults. If not specified, returns all matches."
|
"description": "Maximum number of results to return. Useful for large vaults. If not specified, returns all matches."
|
||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of results to skip. Use with max_results for pagination."
|
"description": "Number of results to skip. Use with limit for pagination."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["query"]
|
"required": ["query"]
|
||||||
|
|
@ -40,13 +40,13 @@ TOOLS: list[dict[str, Any]] = [
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"max_results": {
|
"limit": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Maximum number of notes to return. Useful for large vaults. If not specified, returns all notes."
|
"description": "Maximum number of notes to return. Useful for large vaults. If not specified, returns all notes."
|
||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of notes to skip. Use with max_results for pagination."
|
"description": "Number of notes to skip. Use with limit for pagination."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
|
@ -89,13 +89,13 @@ TOOLS: list[dict[str, Any]] = [
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Tag name (without the # symbol)"
|
"description": "Tag name (without the # symbol)"
|
||||||
},
|
},
|
||||||
"max_results": {
|
"limit": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Maximum number of notes to return. Useful for large vaults. If not specified, returns all matches."
|
"description": "Maximum number of notes to return. Useful for large vaults. If not specified, returns all matches."
|
||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Number of notes to skip. Use with max_results for pagination."
|
"description": "Number of notes to skip. Use with limit for pagination."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["tag"]
|
"required": ["tag"]
|
||||||
|
|
@ -238,7 +238,12 @@ TOOLS: list[dict[str, Any]] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "create_note_from_template",
|
"name": "create_note_from_template",
|
||||||
"description": "Create a new note from a template with variable substitution. Variables in the template like {{variable_name}} will be replaced.",
|
"description": (
|
||||||
|
"Create a new note from a template. Built-in placeholders like "
|
||||||
|
"{{title}}, {{date}}, {{datetime}}, {{folder}}, {{date:FMT}} are "
|
||||||
|
"substituted automatically (FMT is a Python strftime string). "
|
||||||
|
"Use update_note afterwards if you need to inject custom content."
|
||||||
|
),
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -249,10 +254,6 @@ TOOLS: list[dict[str, Any]] = [
|
||||||
"note_path": {
|
"note_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path for the new note (e.g., 'meetings/2024-03-13.md')"
|
"description": "Path for the new note (e.g., 'meetings/2024-03-13.md')"
|
||||||
},
|
|
||||||
"variables": {
|
|
||||||
"type": "object",
|
|
||||||
"description": "Variables to substitute in the template (e.g., {\"project\": \"Alpha\", \"date\": \"2024-03-13\"})"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["template_name", "note_path"]
|
"required": ["template_name", "note_path"]
|
||||||
|
|
@ -298,6 +299,20 @@ TOOLS: list[dict[str, Any]] = [
|
||||||
"required": []
|
"required": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "get_config",
|
||||||
|
"description": (
|
||||||
|
"Get NoteDiscovery server configuration: app name, version, "
|
||||||
|
"whether search is enabled, authentication mode, autosave delay. "
|
||||||
|
"Useful for confirming what server you're connected to and what "
|
||||||
|
"features are available before calling other tools."
|
||||||
|
),
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue