diff --git a/documentation/MCP.md b/documentation/MCP.md index a164557..654c205 100644 --- a/documentation/MCP.md +++ b/documentation/MCP.md @@ -203,27 +203,28 @@ The MCP server provides these tools to AI assistants: |------|-------------| | `list_templates` | List available templates | | `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 | Tool | Description | |------|-------------| | `health_check` | Verify server connectivity | +| `get_config` | Get server config (name, version, enabled features, autosave delay) | ## Tool Details -### Pagination with `max_results` and `offset` +### Pagination with `limit` and `offset` Some tools support optional pagination parameters for large vaults: | Tool | Parameters | Description | |------|------------|-------------| -| `search_notes` | `max_results`, `offset` | Paginate search results | -| `list_notes` | `max_results`, `offset` | Paginate notes list | -| `get_notes_by_tag` | `max_results`, `offset` | Paginate notes by tag | +| `search_notes` | `limit`, `offset` | Paginate search results | +| `list_notes` | `limit`, `offset` | Paginate notes list | +| `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) **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 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 | |-----------|------|----------|-------------| | `template_name` | string | Yes | Template name (e.g., "meeting-notes") | | `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" diff --git a/mcp_server/__init__.py b/mcp_server/__init__.py index 3f1bcbb..ec9ce36 100644 --- a/mcp_server/__init__.py +++ b/mcp_server/__init__.py @@ -16,7 +16,33 @@ Environment Variables: 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" from .server import main diff --git a/mcp_server/client.py b/mcp_server/client.py index 69012e3..6f52a23 100644 --- a/mcp_server/client.py +++ b/mcp_server/client.py @@ -163,16 +163,23 @@ class NoteDiscoveryClient: 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 + # /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="") - 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: """ @@ -241,16 +248,19 @@ class NoteDiscoveryClient: self, template_name: str, note_path: str, - variables: dict | None = None ) -> APIResponse: """ 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: template_name: Name of the template note_path: Path for the new note - variables: Variables to substitute in the template - + Returns: APIResponse with creation result """ @@ -259,9 +269,6 @@ class NoteDiscoveryClient: "templateName": template_name, "notePath": note_path, } - if variables: - data["variables"] = variables - return self._request("POST", "/api/templates/create-note", data=data) # ========================================================================= diff --git a/mcp_server/server.py b/mcp_server/server.py index e5a1e25..89adb04 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -13,6 +13,7 @@ import sys import traceback from typing import Any, Optional +from . import __version__ from .config import load_config, MCPConfig from .client import NoteDiscoveryClient, APIResponse from .tools import TOOLS, get_tool_names @@ -21,10 +22,11 @@ from .tools import TOOLS, get_tool_names # MCP Protocol version 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 = { "name": "notediscovery-mcp", - "version": "1.0.0", + "version": __version__, } @@ -208,14 +210,14 @@ class MCPServer: def _tool_search_notes(self, args: dict) -> str: """Search notes by query.""" query = args.get("query", "").strip() - max_results = args.get("max_results") + limit = args.get("limit") offset = args.get("offset", 0) - + if not query: return "No search term provided. Please specify a query." - - # Use server-side pagination if max_results is specified - response = self.client.search(query, limit=max_results, offset=offset) + + # Use server-side pagination if limit is specified + response = self.client.search(query, limit=limit, offset=offset) if not response.success: return f"Search failed: {response.error}" @@ -252,17 +254,17 @@ class MCPServer: # Add pagination hint if there are more results 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) - + def _tool_list_notes(self, args: dict) -> str: """List all notes.""" - max_results = args.get("max_results") + limit = args.get("limit") offset = args.get("offset", 0) - - # Use server-side pagination if max_results is specified - response = self.client.list_notes(limit=max_results, offset=offset) + + # Use server-side pagination if limit is specified + response = self.client.list_notes(limit=limit, offset=offset) if not response.success: return f"Failed to list notes: {response.error}" @@ -298,10 +300,10 @@ class MCPServer: # Add pagination hint if there are more results 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) - + def _tool_get_note(self, args: dict) -> str: """Get note content.""" path = args.get("path", "") @@ -350,14 +352,14 @@ class MCPServer: def _tool_get_notes_by_tag(self, args: dict) -> str: """Get notes with a specific tag.""" tag = args.get("tag", "") - max_results = args.get("max_results") + limit = args.get("limit") offset = args.get("offset", 0) - + if not tag: return "Error: tag is required" - # Use server-side pagination if max_results is specified - response = self.client.get_notes_by_tag(tag, limit=max_results, offset=offset) + # Use server-side pagination if limit is specified + response = self.client.get_notes_by_tag(tag, limit=limit, offset=offset) if not response.success: return f"Failed to get notes by tag: {response.error}" @@ -382,7 +384,7 @@ class MCPServer: # Add pagination hint if there are more results 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) @@ -625,20 +627,19 @@ class MCPServer: """Create a note from a template.""" template_name = args.get("template_name", "") note_path = args.get("note_path", "") - variables = args.get("variables", {}) - + if not template_name: return "Error: template_name is required" - + is_valid, error = self._validate_path(note_path) if not is_valid: 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: return f"Failed to create note from template: {response.error}" - + return f"✅ Note created from template '{template_name}': {note_path}" 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 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 diff --git a/mcp_server/tools.py b/mcp_server/tools.py index 360b580..5dad9d3 100644 --- a/mcp_server/tools.py +++ b/mcp_server/tools.py @@ -22,13 +22,13 @@ TOOLS: list[dict[str, Any]] = [ "type": "string", "description": "Search query. Can be keywords, phrases, or natural language." }, - "max_results": { + "limit": { "type": "integer", "description": "Maximum number of results to return. Useful for large vaults. If not specified, returns all matches." }, "offset": { "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"] @@ -40,13 +40,13 @@ TOOLS: list[dict[str, Any]] = [ "inputSchema": { "type": "object", "properties": { - "max_results": { + "limit": { "type": "integer", "description": "Maximum number of notes to return. Useful for large vaults. If not specified, returns all notes." }, "offset": { "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": [] @@ -89,13 +89,13 @@ TOOLS: list[dict[str, Any]] = [ "type": "string", "description": "Tag name (without the # symbol)" }, - "max_results": { + "limit": { "type": "integer", "description": "Maximum number of notes to return. Useful for large vaults. If not specified, returns all matches." }, "offset": { "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"] @@ -238,7 +238,12 @@ TOOLS: list[dict[str, Any]] = [ }, { "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": { "type": "object", "properties": { @@ -249,10 +254,6 @@ TOOLS: list[dict[str, Any]] = [ "note_path": { "type": "string", "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"] @@ -298,6 +299,20 @@ TOOLS: list[dict[str, Any]] = [ "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": [] + } + }, ]