feat(mcp): improve remote ingest guidance
Make upload-ticket auth the default remote-agent path by returning complete upload URLs and updating service output/docs. Add validate_upload and preview_upload_metadata for lightweight file checks and exact sha256 duplicate warnings before ingest. Expose version, capability schema, file-index freshness, queue state, and known limitations through get_mcp_capabilities. Validation: python3 -m unittest discover -v; python3 -m compileall obsidian_wiki tests scripts; git diff --check.
This commit is contained in:
@@ -16,6 +16,7 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Sequence
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
@@ -31,6 +32,7 @@ DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_PORT = 8765
|
||||
DEFAULT_PATH = "/mcp"
|
||||
DEFAULT_INGEST_UPLOAD_PATH = "/mcp-ingest/uploads"
|
||||
PUBLIC_BASE_URL_ENV = "OBSIDIAN_MCP_PUBLIC_BASE_URL"
|
||||
DEFAULT_TOKEN_BYTES = 32
|
||||
DEFAULT_UV_CACHE_DIR = "/tmp/uv-cache"
|
||||
DEFAULT_OPENCODE_PATH = "opencode"
|
||||
@@ -263,6 +265,7 @@ def write_token_env_file(
|
||||
host: str = DEFAULT_HOST,
|
||||
port: int = DEFAULT_PORT,
|
||||
path: str = DEFAULT_PATH,
|
||||
public_base_url: str | None = None,
|
||||
enabled_tools: Sequence[str] | None = None,
|
||||
enable_ingest: bool | None = None,
|
||||
enable_query: bool | None = None,
|
||||
@@ -281,6 +284,8 @@ def write_token_env_file(
|
||||
env_file.write(f"OBSIDIAN_MCP_HOST={host}\n")
|
||||
env_file.write(f"OBSIDIAN_MCP_PORT={port}\n")
|
||||
env_file.write(f"OBSIDIAN_MCP_PATH={path}\n")
|
||||
if public_base_url:
|
||||
env_file.write(f"{PUBLIC_BASE_URL_ENV}={public_base_url.rstrip('/')}\n")
|
||||
env_file.write(f"OBSIDIAN_MCP_ENABLED_TOOLS={format_enabled_tools(tools)}\n")
|
||||
env_file.write(f"OBSIDIAN_MCP_ENABLE_INGEST={1 if TOOL_INGEST in tools else 0}\n")
|
||||
env_file.write(f"OBSIDIAN_MCP_INGEST_OPENCODE_MODEL={models.ingest_model}\n")
|
||||
@@ -325,7 +330,7 @@ def model_settings_from_config(values: dict[str, str]) -> OpenCodeModelSettings:
|
||||
)
|
||||
|
||||
|
||||
def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[str, ...], str, OpenCodeModelSettings]:
|
||||
def saved_service_config(project_root: Path) -> tuple[str, str, int, str, str, tuple[str, ...], str, OpenCodeModelSettings]:
|
||||
paths = resolve_service_paths(project_root)
|
||||
values = read_service_env_file(paths.env_path)
|
||||
token = values.get("OBSIDIAN_MCP_TOKEN", "").strip()
|
||||
@@ -334,12 +339,13 @@ def saved_service_config(project_root: Path) -> tuple[str, str, int, str, tuple[
|
||||
host = values.get("OBSIDIAN_MCP_HOST") or DEFAULT_HOST
|
||||
port = int(values.get("OBSIDIAN_MCP_PORT") or DEFAULT_PORT)
|
||||
path = values.get("OBSIDIAN_MCP_PATH") or DEFAULT_PATH
|
||||
public_base_url = values.get(PUBLIC_BASE_URL_ENV, "").strip()
|
||||
enabled_tools = enabled_tools_from_config(values)
|
||||
opencode_path = (
|
||||
values.get("OBSIDIAN_MCP_OPENCODE_PATH", "").strip()
|
||||
or values.get("OBSIDIAN_MCP_INGEST_OPENCODE_PATH", "").strip()
|
||||
)
|
||||
return token, host, port, path, enabled_tools, opencode_path, model_settings_from_config(values)
|
||||
return token, host, port, path, public_base_url, enabled_tools, opencode_path, model_settings_from_config(values)
|
||||
|
||||
|
||||
def write_service_unit(
|
||||
@@ -389,17 +395,32 @@ def detect_remote_hosts() -> list[str]:
|
||||
return cleaned
|
||||
|
||||
|
||||
def public_base_url_from_hosts(hosts: Sequence[str], *, port: int = DEFAULT_PORT, path: str = DEFAULT_PATH) -> str:
|
||||
host = hosts[0] if hosts else ""
|
||||
return f"http://{host}:{port}{path}" if host else ""
|
||||
|
||||
|
||||
def upload_url_from_public_base(public_base_url: str, *, fallback_host: str, port: int = DEFAULT_PORT) -> str:
|
||||
parsed = urlsplit(public_base_url)
|
||||
if parsed.scheme and parsed.netloc:
|
||||
return urlunsplit((parsed.scheme, parsed.netloc, DEFAULT_INGEST_UPLOAD_PATH, "", ""))
|
||||
return f"http://{fallback_host}:{port}{DEFAULT_INGEST_UPLOAD_PATH}"
|
||||
|
||||
|
||||
def render_remote_config(
|
||||
*,
|
||||
hosts: Sequence[str],
|
||||
token: str,
|
||||
port: int = DEFAULT_PORT,
|
||||
path: str = DEFAULT_PATH,
|
||||
public_base_url: str | None = None,
|
||||
config_path: Path | None = None,
|
||||
enabled_tools: Sequence[str] | None = None,
|
||||
) -> str:
|
||||
host = hosts[0] if hosts else "<server-ip>"
|
||||
url = f"http://{host}:{port}{path}"
|
||||
public_url = (public_base_url or url).rstrip("/")
|
||||
upload_url = upload_url_from_public_base(public_url, fallback_host=host, port=port)
|
||||
alternatives = "\n".join(f"- http://{candidate}:{port}{path}" for candidate in hosts[1:])
|
||||
alternatives_section = f"\nOther detected addresses:\n{alternatives}\n" if alternatives else ""
|
||||
config_section = f"\nConfig file:\n{config_path}\n" if config_path is not None else ""
|
||||
@@ -408,7 +429,6 @@ def render_remote_config(
|
||||
enabled_section = f"\nEnabled tool groups:\n{format_enabled_tools(enabled_tools) or 'none'}\n"
|
||||
ingest_upload_section = ""
|
||||
if enabled_tools is not None and TOOL_INGEST in normalize_enabled_tools(enabled_tools):
|
||||
upload_url = f"http://{host}:{port}{DEFAULT_INGEST_UPLOAD_PATH}"
|
||||
upload_alternatives = "\n".join(
|
||||
f"- http://{candidate}:{port}{DEFAULT_INGEST_UPLOAD_PATH}" for candidate in hosts[1:]
|
||||
)
|
||||
@@ -420,7 +440,8 @@ def render_remote_config(
|
||||
"Ingest file upload endpoint:\n"
|
||||
f"{upload_url}\n"
|
||||
f"{upload_alternatives_section}"
|
||||
"Use the same Authorization header. POST multipart/form-data with one or more `files` fields.\n"
|
||||
"Remote agents should upload with create_ingest_upload_ticket and X-Obsidian-Wiki-Upload-Token.\n"
|
||||
"Bearer upload is for local/admin use. POST multipart/form-data with one or more `files` fields.\n"
|
||||
)
|
||||
query_section = ""
|
||||
if enabled_tools is not None and TOOL_QUERY in normalize_enabled_tools(enabled_tools):
|
||||
@@ -462,12 +483,13 @@ def render_remote_config(
|
||||
|
||||
def show_config(project_root: Path) -> str:
|
||||
paths = resolve_service_paths(project_root)
|
||||
token, _host, port, path, enabled_tools, _opencode_path, _models = saved_service_config(project_root)
|
||||
token, _host, port, path, public_base_url, enabled_tools, _opencode_path, _models = saved_service_config(project_root)
|
||||
return render_remote_config(
|
||||
hosts=detect_remote_hosts(),
|
||||
token=token,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
config_path=paths.env_path,
|
||||
enabled_tools=enabled_tools,
|
||||
)
|
||||
@@ -502,10 +524,13 @@ def new_token(
|
||||
enabled_tools = DEFAULT_ENABLED_TOOLS
|
||||
opencode_path = ""
|
||||
model_settings = OpenCodeModelSettings()
|
||||
public_base_url = ""
|
||||
try:
|
||||
_old_token, _old_host, _old_port, _old_path, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
|
||||
_old_token, _old_host, _old_port, _old_path, public_base_url, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
|
||||
except RuntimeError:
|
||||
pass
|
||||
hosts = detect_remote_hosts()
|
||||
public_base_url = public_base_url or public_base_url_from_hosts(hosts, port=port, path=path)
|
||||
token = generate_token(token_bytes)
|
||||
write_token_env_file(
|
||||
paths.env_path,
|
||||
@@ -513,16 +538,18 @@ def new_token(
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
enabled_tools=enabled_tools,
|
||||
opencode_path=opencode_path,
|
||||
model_settings=model_settings,
|
||||
)
|
||||
restart_service_if_installed(paths)
|
||||
return render_remote_config(
|
||||
hosts=detect_remote_hosts(),
|
||||
hosts=hosts,
|
||||
token=token,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
config_path=paths.env_path,
|
||||
enabled_tools=enabled_tools,
|
||||
)
|
||||
@@ -541,7 +568,7 @@ def start_service(
|
||||
) -> str:
|
||||
paths = resolve_service_paths(project_root)
|
||||
try:
|
||||
token, host, port, path, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
|
||||
token, host, port, path, public_base_url, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
|
||||
except RuntimeError:
|
||||
if paths.env_path.exists():
|
||||
raise
|
||||
@@ -549,16 +576,20 @@ def start_service(
|
||||
enabled_tools = DEFAULT_ENABLED_TOOLS
|
||||
opencode_path = ""
|
||||
model_settings = OpenCodeModelSettings()
|
||||
public_base_url = ""
|
||||
enabled_tools = update_ingest_tool(enabled_tools, enable_ingest)
|
||||
enabled_tools = update_query_tool(enabled_tools, enable_query)
|
||||
if ({TOOL_INGEST, TOOL_QUERY} & set(enabled_tools)) and not opencode_path:
|
||||
opencode_path = resolve_opencode_path()
|
||||
hosts = detect_remote_hosts()
|
||||
public_base_url = public_base_url or public_base_url_from_hosts(hosts, port=port, path=path)
|
||||
write_token_env_file(
|
||||
paths.env_path,
|
||||
token,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
enabled_tools=enabled_tools,
|
||||
opencode_path=opencode_path,
|
||||
model_settings=model_settings,
|
||||
@@ -574,10 +605,11 @@ def start_service(
|
||||
run_systemctl(["enable", SERVICE_NAME])
|
||||
run_systemctl(["restart", SERVICE_NAME])
|
||||
return render_remote_config(
|
||||
hosts=detect_remote_hosts(),
|
||||
hosts=hosts,
|
||||
token=token,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
config_path=paths.env_path,
|
||||
enabled_tools=enabled_tools,
|
||||
)
|
||||
@@ -606,15 +638,16 @@ def render_tool_status(
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, int, str, tuple[str, ...], str, OpenCodeModelSettings]:
|
||||
def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, int, str, str, tuple[str, ...], str, OpenCodeModelSettings]:
|
||||
paths = resolve_service_paths(project_root)
|
||||
try:
|
||||
token, host, port, path, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
|
||||
token, host, port, path, public_base_url, enabled_tools, opencode_path, model_settings = saved_service_config(project_root)
|
||||
except RuntimeError:
|
||||
token = generate_token()
|
||||
host = DEFAULT_HOST
|
||||
port = DEFAULT_PORT
|
||||
path = DEFAULT_PATH
|
||||
public_base_url = public_base_url_from_hosts(detect_remote_hosts(), port=port, path=path)
|
||||
enabled_tools = DEFAULT_ENABLED_TOOLS
|
||||
opencode_path = ""
|
||||
model_settings = OpenCodeModelSettings()
|
||||
@@ -624,20 +657,21 @@ def ensure_service_config(project_root: Path) -> tuple[ServicePaths, str, str, i
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
enabled_tools=enabled_tools,
|
||||
model_settings=model_settings,
|
||||
)
|
||||
return paths, token, host, port, path, enabled_tools, opencode_path, model_settings
|
||||
return paths, token, host, port, path, public_base_url, enabled_tools, opencode_path, model_settings
|
||||
|
||||
|
||||
def list_tools(project_root: Path) -> str:
|
||||
paths, _token, _host, _port, _path, enabled_tools, _opencode_path, _models = ensure_service_config(project_root)
|
||||
paths, _token, _host, _port, _path, _public_base_url, enabled_tools, _opencode_path, _models = ensure_service_config(project_root)
|
||||
return render_tool_status(config_path=paths.env_path, enabled_tools=enabled_tools)
|
||||
|
||||
|
||||
def set_tool_enabled(project_root: Path, tool: str, enabled: bool) -> str:
|
||||
tool_name = normalize_tool_name(tool)
|
||||
paths, token, host, port, path, enabled_tools, opencode_path, model_settings = ensure_service_config(project_root)
|
||||
paths, token, host, port, path, public_base_url, enabled_tools, opencode_path, model_settings = ensure_service_config(project_root)
|
||||
current = set(enabled_tools)
|
||||
if enabled:
|
||||
current.add(tool_name)
|
||||
@@ -652,6 +686,7 @@ def set_tool_enabled(project_root: Path, tool: str, enabled: bool) -> str:
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
public_base_url=public_base_url,
|
||||
enabled_tools=updated_tools,
|
||||
opencode_path=opencode_path,
|
||||
model_settings=model_settings,
|
||||
|
||||
Reference in New Issue
Block a user