fix(mcp): stage remote ingest by default
Default ticket, multipart, and text-note ingest entry points to staging without launching a job. Propagate WIKI_ARCHIVE_SOURCES from project configuration into the managed service environment.\n\nValidated with the full 206-test suite.
This commit is contained in:
@@ -475,9 +475,9 @@ This adds write-capable MCP tools to the same `wiki-file-assets` server and bear
|
|||||||
|
|
||||||
The managed service config sets the OpenCode model server-side. By default ingest uses `OBSIDIAN_MCP_INGEST_OPENCODE_MODEL=openai/gpt-5.5`, `OBSIDIAN_MCP_INGEST_OPENCODE_VARIANT=high`, and an `OBSIDIAN_MCP_INGEST_JOB_TIMEOUT_SECONDS=1800` deadline. It also writes `OBSIDIAN_MCP_PUBLIC_BASE_URL` from the detected remote address so ticket responses can include a complete upload URL; set it manually when the service is behind a tunnel, reverse proxy, or custom DNS name. If the upload endpoint is exposed through a different public URL than `/mcp`, set `OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL` explicitly and it will override the derived upload URL in capabilities, upload instructions, tickets, and `show-config`. Change these values in `.runtime/obsidian-wiki-mcp.env` on the server if needed; remote MCP clients do not choose the model per call.
|
The managed service config sets the OpenCode model server-side. By default ingest uses `OBSIDIAN_MCP_INGEST_OPENCODE_MODEL=openai/gpt-5.5`, `OBSIDIAN_MCP_INGEST_OPENCODE_VARIANT=high`, and an `OBSIDIAN_MCP_INGEST_JOB_TIMEOUT_SECONDS=1800` deadline. It also writes `OBSIDIAN_MCP_PUBLIC_BASE_URL` from the detected remote address so ticket responses can include a complete upload URL; set it manually when the service is behind a tunnel, reverse proxy, or custom DNS name. If the upload endpoint is exposed through a different public URL than `/mcp`, set `OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL` explicitly and it will override the derived upload URL in capabilities, upload instructions, tickets, and `show-config`. Change these values in `.runtime/obsidian-wiki-mcp.env` on the server if needed; remote MCP clients do not choose the model per call.
|
||||||
|
|
||||||
For short text thoughts, notes, findings, or pasted context, call `ingest_text_note(text, title?, project?, doc_type_hint?, note?, start_job?)`. It uses `doc_type_hint="thought"` by default and returns a one-item Bundle, the created upload, and the queued job when `start_job=true`.
|
For short text thoughts, notes, findings, or pasted context, call `ingest_text_note(text, title?, project?, doc_type_hint?, note?, start_job?)`. It uses `doc_type_hint="thought"` and `start_job=false` by default, returning a one-item Bundle and the staged upload; it also returns the queued job when `start_job=true` is explicitly requested.
|
||||||
|
|
||||||
For binary files and larger source documents, remote agents should use upload tickets by default: call `create_ingest_upload_ticket(start_job=false)`, upload with the returned `upload_url` and `X-Obsidian-Wiki-Upload-Token`, call `preview_upload_metadata(upload_ids)`, then call `start_ingest_job(upload_ids)` when the preview looks correct. The same multipart POST can carry `body_text` plus repeated `files`; the body becomes the Bundle's primary Markdown source and the files become attachments:
|
For binary files and larger source documents, remote agents should use upload tickets by default: call `create_ingest_upload_ticket()` (also `start_job=false` by default), upload with the returned `upload_url` and `X-Obsidian-Wiki-Upload-Token`, call `preview_upload_metadata(upload_ids)`, then call `start_ingest_job(upload_ids)` when the preview looks correct. The same multipart POST can carry `body_text` plus repeated `files`; the body becomes the Bundle's primary Markdown source and the files become attachments:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -H "X-Obsidian-Wiki-Upload-Token: <upload-token>" \
|
curl -H "X-Obsidian-Wiki-Upload-Token: <upload-token>" \
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ class IngestManager:
|
|||||||
bundle_id: str | None = None,
|
bundle_id: str | None = None,
|
||||||
source_type: str | None = None,
|
source_type: str | None = None,
|
||||||
label: str | None = None,
|
label: str | None = None,
|
||||||
start_job: bool = True,
|
start_job: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if expires_seconds <= 0 or expires_seconds > MAX_UPLOAD_TICKET_SECONDS:
|
if expires_seconds <= 0 or expires_seconds > MAX_UPLOAD_TICKET_SECONDS:
|
||||||
raise IngestError(
|
raise IngestError(
|
||||||
|
|||||||
@@ -510,11 +510,11 @@ def create_server(
|
|||||||
"note",
|
"note",
|
||||||
"start_job",
|
"start_job",
|
||||||
],
|
],
|
||||||
"start_job_default": True,
|
"start_job_default": False,
|
||||||
"curl_example": (
|
"curl_example": (
|
||||||
"curl -H 'X-Obsidian-Wiki-Upload-Token: <upload-token>' "
|
"curl -H 'X-Obsidian-Wiki-Upload-Token: <upload-token>' "
|
||||||
"-F 'files=@drawing.pdf' -F 'project=bjd' "
|
"-F 'files=@drawing.pdf' -F 'project=bjd' "
|
||||||
"-F 'doc_type_hint=drawing' -F 'start_job=true' "
|
"-F 'doc_type_hint=drawing' -F 'start_job=false' "
|
||||||
f"{public_upload_url or '<upload-url>'}"
|
f"{public_upload_url or '<upload-url>'}"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -530,7 +530,7 @@ def create_server(
|
|||||||
"direct_text_tool": {
|
"direct_text_tool": {
|
||||||
"name": "ingest_text_note",
|
"name": "ingest_text_note",
|
||||||
"best_for": "short remote-agent thoughts, notes, findings, or pasted text",
|
"best_for": "short remote-agent thoughts, notes, findings, or pasted text",
|
||||||
"start_job_default": True,
|
"start_job_default": False,
|
||||||
"notes": "wraps text as a temporary markdown upload, then uses the same fixed wiki-ingest job path",
|
"notes": "wraps text as a temporary markdown upload, then uses the same fixed wiki-ingest job path",
|
||||||
},
|
},
|
||||||
"limits": {
|
"limits": {
|
||||||
@@ -557,7 +557,7 @@ def create_server(
|
|||||||
project: str | None = None,
|
project: str | None = None,
|
||||||
doc_type_hint: str | None = "thought",
|
doc_type_hint: str | None = "thought",
|
||||||
note: str | None = None,
|
note: str | None = None,
|
||||||
start_job: bool = True,
|
start_job: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Ingest a short text note by wrapping it as a temporary markdown upload."""
|
"""Ingest a short text note by wrapping it as a temporary markdown upload."""
|
||||||
payload = text_note_payload(text, title=title)
|
payload = text_note_payload(text, title=title)
|
||||||
@@ -593,7 +593,7 @@ def create_server(
|
|||||||
bundle_id: str | None = None,
|
bundle_id: str | None = None,
|
||||||
source_type: str | None = None,
|
source_type: str | None = None,
|
||||||
label: str | None = None,
|
label: str | None = None,
|
||||||
start_job: bool = True,
|
start_job: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Create a short-lived HTTP upload ticket so remote agents can upload files without the main MCP token."""
|
"""Create a short-lived HTTP upload ticket so remote agents can upload files without the main MCP token."""
|
||||||
ticket = ingest_manager.create_upload_ticket(
|
ticket = ingest_manager.create_upload_ticket(
|
||||||
@@ -1094,7 +1094,7 @@ class IngestUploadRouter:
|
|||||||
bundle_id = ticket_bound_field("bundle_id")
|
bundle_id = ticket_bound_field("bundle_id")
|
||||||
source_type = ticket_bound_field("source_type")
|
source_type = ticket_bound_field("source_type")
|
||||||
label = ticket_bound_field("label")
|
label = ticket_bound_field("label")
|
||||||
start_default = bool((upload_ticket or {}).get("start_job", True))
|
start_default = bool((upload_ticket or {}).get("start_job", False))
|
||||||
requested_start = parse_bool(first_text_field("start_job"), default=start_default)
|
requested_start = parse_bool(first_text_field("start_job"), default=start_default)
|
||||||
start_job = requested_start if upload_ticket is None else start_default and requested_start
|
start_job = requested_start if upload_ticket is None else start_default and requested_start
|
||||||
has_body = bool(body_text and body_text.strip())
|
has_body = bool(body_text and body_text.strip())
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
|||||||
if str(REPO_ROOT) not in sys.path:
|
if str(REPO_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(REPO_ROOT))
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
from obsidian_wiki.config import resolve_vault_path
|
from obsidian_wiki.config import load_config, resolve_vault_path
|
||||||
|
|
||||||
|
|
||||||
SERVICE_NAME = "obsidian-wiki-mcp.service"
|
SERVICE_NAME = "obsidian-wiki-mcp.service"
|
||||||
@@ -37,6 +37,7 @@ PUBLIC_UPLOAD_URL_ENV = "OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL"
|
|||||||
ALLOW_SOURCE_FILES_ENV = "OBSIDIAN_MCP_ALLOW_SOURCE_FILES"
|
ALLOW_SOURCE_FILES_ENV = "OBSIDIAN_MCP_ALLOW_SOURCE_FILES"
|
||||||
ALLOWED_HOSTS_ENV = "OBSIDIAN_MCP_ALLOWED_HOSTS"
|
ALLOWED_HOSTS_ENV = "OBSIDIAN_MCP_ALLOWED_HOSTS"
|
||||||
ALLOWED_ORIGINS_ENV = "OBSIDIAN_MCP_ALLOWED_ORIGINS"
|
ALLOWED_ORIGINS_ENV = "OBSIDIAN_MCP_ALLOWED_ORIGINS"
|
||||||
|
ARCHIVE_SOURCES_ENV = "WIKI_ARCHIVE_SOURCES"
|
||||||
DEFAULT_TOKEN_BYTES = 32
|
DEFAULT_TOKEN_BYTES = 32
|
||||||
DEFAULT_UV_CACHE_DIR = "/tmp/uv-cache"
|
DEFAULT_UV_CACHE_DIR = "/tmp/uv-cache"
|
||||||
DEFAULT_OPENCODE_PATH = "opencode"
|
DEFAULT_OPENCODE_PATH = "opencode"
|
||||||
@@ -86,6 +87,11 @@ def parse_bool(value: str | None) -> bool:
|
|||||||
return str(value or "").strip().lower() in {"1", "true", "yes", "on", "enabled"}
|
return str(value or "").strip().lower() in {"1", "true", "yes", "on", "enabled"}
|
||||||
|
|
||||||
|
|
||||||
|
def project_archive_sources(project_root: Path, *, default: bool = False) -> bool:
|
||||||
|
value = load_config(project_root).get(ARCHIVE_SOURCES_ENV)
|
||||||
|
return default if value in (None, "") else parse_bool(value)
|
||||||
|
|
||||||
|
|
||||||
def parse_config_list(value: str | None) -> tuple[str, ...]:
|
def parse_config_list(value: str | None) -> tuple[str, ...]:
|
||||||
return tuple(item.strip() for item in str(value or "").split(",") if item.strip())
|
return tuple(item.strip() for item in str(value or "").split(",") if item.strip())
|
||||||
|
|
||||||
@@ -155,6 +161,7 @@ class ServiceConfig:
|
|||||||
public_upload_url: str = ""
|
public_upload_url: str = ""
|
||||||
enabled_tools: tuple[str, ...] = DEFAULT_ENABLED_TOOLS
|
enabled_tools: tuple[str, ...] = DEFAULT_ENABLED_TOOLS
|
||||||
allow_source_files: bool = False
|
allow_source_files: bool = False
|
||||||
|
archive_sources: bool = False
|
||||||
allowed_hosts: tuple[str, ...] = ()
|
allowed_hosts: tuple[str, ...] = ()
|
||||||
allowed_origins: tuple[str, ...] = ()
|
allowed_origins: tuple[str, ...] = ()
|
||||||
ingest_job_timeout_seconds: int = DEFAULT_INGEST_JOB_TIMEOUT_SECONDS
|
ingest_job_timeout_seconds: int = DEFAULT_INGEST_JOB_TIMEOUT_SECONDS
|
||||||
@@ -295,6 +302,7 @@ def write_token_env_file(
|
|||||||
enable_ingest: bool | None = None,
|
enable_ingest: bool | None = None,
|
||||||
enable_query: bool | None = None,
|
enable_query: bool | None = None,
|
||||||
allow_source_files: bool | None = None,
|
allow_source_files: bool | None = None,
|
||||||
|
archive_sources: bool = False,
|
||||||
allowed_hosts: Sequence[str] | None = None,
|
allowed_hosts: Sequence[str] | None = None,
|
||||||
allowed_origins: Sequence[str] | None = None,
|
allowed_origins: Sequence[str] | None = None,
|
||||||
ingest_job_timeout_seconds: int = DEFAULT_INGEST_JOB_TIMEOUT_SECONDS,
|
ingest_job_timeout_seconds: int = DEFAULT_INGEST_JOB_TIMEOUT_SECONDS,
|
||||||
@@ -307,6 +315,7 @@ def write_token_env_file(
|
|||||||
tools = update_ingest_tool(tools, enable_ingest)
|
tools = update_ingest_tool(tools, enable_ingest)
|
||||||
tools = update_query_tool(tools, enable_query)
|
tools = update_query_tool(tools, enable_query)
|
||||||
source_files_allowed = bool(allow_source_files)
|
source_files_allowed = bool(allow_source_files)
|
||||||
|
archive_sources_enabled = bool(archive_sources)
|
||||||
models = model_settings or OpenCodeModelSettings()
|
models = model_settings or OpenCodeModelSettings()
|
||||||
normalized_public_base_url = normalize_public_base_url(public_base_url)
|
normalized_public_base_url = normalize_public_base_url(public_base_url)
|
||||||
normalized_public_upload_url = normalize_public_upload_url(public_upload_url)
|
normalized_public_upload_url = normalize_public_upload_url(public_upload_url)
|
||||||
@@ -337,6 +346,7 @@ def write_token_env_file(
|
|||||||
if normalized_public_upload_url:
|
if normalized_public_upload_url:
|
||||||
env_file.write(f"{PUBLIC_UPLOAD_URL_ENV}={normalized_public_upload_url}\n")
|
env_file.write(f"{PUBLIC_UPLOAD_URL_ENV}={normalized_public_upload_url}\n")
|
||||||
env_file.write(f"{ALLOW_SOURCE_FILES_ENV}={1 if source_files_allowed else 0}\n")
|
env_file.write(f"{ALLOW_SOURCE_FILES_ENV}={1 if source_files_allowed else 0}\n")
|
||||||
|
env_file.write(f"{ARCHIVE_SOURCES_ENV}={1 if archive_sources_enabled else 0}\n")
|
||||||
env_file.write(f"{ALLOWED_HOSTS_ENV}={','.join(effective_allowed_hosts)}\n")
|
env_file.write(f"{ALLOWED_HOSTS_ENV}={','.join(effective_allowed_hosts)}\n")
|
||||||
env_file.write(f"{ALLOWED_ORIGINS_ENV}={','.join(effective_allowed_origins)}\n")
|
env_file.write(f"{ALLOWED_ORIGINS_ENV}={','.join(effective_allowed_origins)}\n")
|
||||||
env_file.write(f"OBSIDIAN_MCP_INGEST_JOB_TIMEOUT_SECONDS={max(1, int(ingest_job_timeout_seconds))}\n")
|
env_file.write(f"OBSIDIAN_MCP_INGEST_JOB_TIMEOUT_SECONDS={max(1, int(ingest_job_timeout_seconds))}\n")
|
||||||
@@ -402,6 +412,7 @@ def saved_service_config(project_root: Path) -> ServiceConfig:
|
|||||||
allow_source_files = parse_bool(values[ALLOW_SOURCE_FILES_ENV])
|
allow_source_files = parse_bool(values[ALLOW_SOURCE_FILES_ENV])
|
||||||
else:
|
else:
|
||||||
allow_source_files = TOOL_FILE_ASSETS in enabled_tools
|
allow_source_files = TOOL_FILE_ASSETS in enabled_tools
|
||||||
|
archive_sources = parse_bool(values.get(ARCHIVE_SOURCES_ENV))
|
||||||
allowed_hosts = parse_config_list(values.get(ALLOWED_HOSTS_ENV))
|
allowed_hosts = parse_config_list(values.get(ALLOWED_HOSTS_ENV))
|
||||||
if not allowed_hosts:
|
if not allowed_hosts:
|
||||||
allowed_hosts = derive_allowed_hosts(
|
allowed_hosts = derive_allowed_hosts(
|
||||||
@@ -425,6 +436,7 @@ def saved_service_config(project_root: Path) -> ServiceConfig:
|
|||||||
public_upload_url=public_upload_url,
|
public_upload_url=public_upload_url,
|
||||||
enabled_tools=enabled_tools,
|
enabled_tools=enabled_tools,
|
||||||
allow_source_files=allow_source_files,
|
allow_source_files=allow_source_files,
|
||||||
|
archive_sources=archive_sources,
|
||||||
allowed_hosts=allowed_hosts,
|
allowed_hosts=allowed_hosts,
|
||||||
allowed_origins=allowed_origins,
|
allowed_origins=allowed_origins,
|
||||||
ingest_job_timeout_seconds=max(
|
ingest_job_timeout_seconds=max(
|
||||||
@@ -711,6 +723,7 @@ def write_service_config(env_path: Path, config: ServiceConfig) -> None:
|
|||||||
public_upload_url=config.public_upload_url,
|
public_upload_url=config.public_upload_url,
|
||||||
enabled_tools=config.enabled_tools,
|
enabled_tools=config.enabled_tools,
|
||||||
allow_source_files=config.allow_source_files,
|
allow_source_files=config.allow_source_files,
|
||||||
|
archive_sources=config.archive_sources,
|
||||||
allowed_hosts=config.allowed_hosts,
|
allowed_hosts=config.allowed_hosts,
|
||||||
allowed_origins=config.allowed_origins,
|
allowed_origins=config.allowed_origins,
|
||||||
ingest_job_timeout_seconds=config.ingest_job_timeout_seconds,
|
ingest_job_timeout_seconds=config.ingest_job_timeout_seconds,
|
||||||
@@ -778,6 +791,10 @@ def new_token(
|
|||||||
saved.allowed_origins if has_saved_config else (),
|
saved.allowed_origins if has_saved_config else (),
|
||||||
derive_allowed_origins(effective_public_base_url, effective_public_upload_url),
|
derive_allowed_origins(effective_public_base_url, effective_public_upload_url),
|
||||||
)
|
)
|
||||||
|
archive_sources = project_archive_sources(
|
||||||
|
paths.project_root,
|
||||||
|
default=saved.archive_sources,
|
||||||
|
)
|
||||||
config = replace(
|
config = replace(
|
||||||
saved,
|
saved,
|
||||||
token=generate_token(token_bytes),
|
token=generate_token(token_bytes),
|
||||||
@@ -786,6 +803,7 @@ def new_token(
|
|||||||
path=effective_path,
|
path=effective_path,
|
||||||
public_base_url=effective_public_base_url,
|
public_base_url=effective_public_base_url,
|
||||||
public_upload_url=effective_public_upload_url,
|
public_upload_url=effective_public_upload_url,
|
||||||
|
archive_sources=archive_sources,
|
||||||
allowed_hosts=allowed_hosts,
|
allowed_hosts=allowed_hosts,
|
||||||
allowed_origins=allowed_origins,
|
allowed_origins=allowed_origins,
|
||||||
)
|
)
|
||||||
@@ -871,6 +889,10 @@ def start_service(
|
|||||||
saved.allowed_origins if has_saved_config else (),
|
saved.allowed_origins if has_saved_config else (),
|
||||||
derive_allowed_origins(effective_public_base_url, effective_public_upload_url),
|
derive_allowed_origins(effective_public_base_url, effective_public_upload_url),
|
||||||
)
|
)
|
||||||
|
archive_sources = project_archive_sources(
|
||||||
|
paths.project_root,
|
||||||
|
default=saved.archive_sources,
|
||||||
|
)
|
||||||
config = replace(
|
config = replace(
|
||||||
saved,
|
saved,
|
||||||
host=effective_host,
|
host=effective_host,
|
||||||
@@ -883,6 +905,7 @@ def start_service(
|
|||||||
allow_source_files=(
|
allow_source_files=(
|
||||||
saved.allow_source_files if allow_source_files is None else allow_source_files
|
saved.allow_source_files if allow_source_files is None else allow_source_files
|
||||||
),
|
),
|
||||||
|
archive_sources=archive_sources,
|
||||||
allowed_hosts=allowed_hosts,
|
allowed_hosts=allowed_hosts,
|
||||||
allowed_origins=allowed_origins,
|
allowed_origins=allowed_origins,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -268,7 +268,6 @@ class McpIngestTests(unittest.TestCase):
|
|||||||
bundle_id="drawing-package-1",
|
bundle_id="drawing-package-1",
|
||||||
source_type="report_package",
|
source_type="report_package",
|
||||||
label="Customer drawing package",
|
label="Customer drawing package",
|
||||||
start_job=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIn("upload_token", ticket)
|
self.assertIn("upload_token", ticket)
|
||||||
|
|||||||
@@ -209,6 +209,53 @@ class RemoteMcpServerTests(unittest.TestCase):
|
|||||||
self.assertEqual(data["uploads"][0]["sha256"], "sha256:" + hashlib.sha256(payload).hexdigest())
|
self.assertEqual(data["uploads"][0]["sha256"], "sha256:" + hashlib.sha256(payload).hexdigest())
|
||||||
self.assertNotIn("path", data["uploads"][0])
|
self.assertNotIn("path", data["uploads"][0])
|
||||||
|
|
||||||
|
def test_http_upload_endpoint_stages_without_starting_job_by_default(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
manager = IngestManager(
|
||||||
|
vault_path=root / "vault",
|
||||||
|
project_root=root / "project",
|
||||||
|
runtime_root=root / "runtime",
|
||||||
|
)
|
||||||
|
app = mcp_server.create_http_app(
|
||||||
|
FakeApp(),
|
||||||
|
"secret-token",
|
||||||
|
ingest_manager=manager,
|
||||||
|
)
|
||||||
|
body, boundary = multipart_body(
|
||||||
|
{"project": "bjd"},
|
||||||
|
[("files", "drawing.pdf", "application/pdf", b"drawing")],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
manager,
|
||||||
|
"start_ingest_job",
|
||||||
|
side_effect=AssertionError("default upload staging must not start a job"),
|
||||||
|
) as start_job:
|
||||||
|
messages = run_asgi_http_request(
|
||||||
|
app,
|
||||||
|
method="POST",
|
||||||
|
path="/mcp-ingest/uploads",
|
||||||
|
headers=[
|
||||||
|
(b"authorization", b"Bearer secret-token"),
|
||||||
|
(
|
||||||
|
b"content-type",
|
||||||
|
f"multipart/form-data; boundary={boundary}".encode("ascii"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(messages[0]["status"], 200)
|
||||||
|
response = b"".join(
|
||||||
|
message.get("body", b"")
|
||||||
|
for message in messages
|
||||||
|
if message["type"] == "http.response.body"
|
||||||
|
)
|
||||||
|
data = json.loads(response.decode("utf-8"))
|
||||||
|
self.assertIsNone(data["job"])
|
||||||
|
start_job.assert_not_called()
|
||||||
|
|
||||||
def test_http_upload_endpoint_creates_bundle_from_body_and_attachments(self):
|
def test_http_upload_endpoint_creates_bundle_from_body_and_attachments(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
@@ -411,7 +458,6 @@ class RemoteMcpServerTests(unittest.TestCase):
|
|||||||
bundle_id="drawing-package-1",
|
bundle_id="drawing-package-1",
|
||||||
source_type="report_package",
|
source_type="report_package",
|
||||||
label="Customer drawing package",
|
label="Customer drawing package",
|
||||||
start_job=False,
|
|
||||||
)
|
)
|
||||||
app = mcp_server.create_http_app(FakeApp(), "secret-token", ingest_manager=manager)
|
app = mcp_server.create_http_app(FakeApp(), "secret-token", ingest_manager=manager)
|
||||||
payload = b"ticket uploaded drawing"
|
payload = b"ticket uploaded drawing"
|
||||||
@@ -1419,6 +1465,9 @@ class RemoteMcpServerTests(unittest.TestCase):
|
|||||||
self.assertEqual(ticket["label"], "Customer drawing package")
|
self.assertEqual(ticket["label"], "Customer drawing package")
|
||||||
self.assertFalse(ticket["start_job"])
|
self.assertFalse(ticket["start_job"])
|
||||||
self.assertEqual(instructions["preferred_upload_auth"], "upload_ticket")
|
self.assertEqual(instructions["preferred_upload_auth"], "upload_ticket")
|
||||||
|
self.assertFalse(instructions["http_upload"]["start_job_default"])
|
||||||
|
self.assertFalse(instructions["direct_text_tool"]["start_job_default"])
|
||||||
|
self.assertIn("start_job=false", instructions["http_upload"]["curl_example"])
|
||||||
self.assertEqual(instructions["http_upload"]["url"], "https://wiki.example.test/mcp-ingest/uploads")
|
self.assertEqual(instructions["http_upload"]["url"], "https://wiki.example.test/mcp-ingest/uploads")
|
||||||
self.assertIn("body_text", instructions["http_upload"]["optional_fields"])
|
self.assertIn("body_text", instructions["http_upload"]["optional_fields"])
|
||||||
self.assertIn("bundle_id", instructions["http_upload"]["optional_fields"])
|
self.assertIn("bundle_id", instructions["http_upload"]["optional_fields"])
|
||||||
@@ -1588,14 +1637,19 @@ class RemoteMcpServerTests(unittest.TestCase):
|
|||||||
ingest_runtime_root=root / "runtime",
|
ingest_runtime_root=root / "runtime",
|
||||||
ingest_manager=manager,
|
ingest_manager=manager,
|
||||||
)
|
)
|
||||||
result = FakeMCP.tools["ingest_text_note"](
|
with patch.object(
|
||||||
text="Remote agent thought\n\nNeed to remember this.",
|
manager,
|
||||||
title="Customer thought: ACME / risk",
|
"start_ingest_job",
|
||||||
project="bjd",
|
side_effect=AssertionError("default text staging must not start a job"),
|
||||||
doc_type_hint="thought",
|
) as start_job:
|
||||||
note="captured from remote agent",
|
result = FakeMCP.tools["ingest_text_note"](
|
||||||
start_job=False,
|
text="Remote agent thought\n\nNeed to remember this.",
|
||||||
)
|
title="Customer thought: ACME / risk",
|
||||||
|
project="bjd",
|
||||||
|
doc_type_hint="thought",
|
||||||
|
note="captured from remote agent",
|
||||||
|
)
|
||||||
|
start_job.assert_not_called()
|
||||||
payload_path = Path(manager._load_upload(result["uploads"][0]["upload_id"])["path"])
|
payload_path = Path(manager._load_upload(result["uploads"][0]["upload_id"])["path"])
|
||||||
payload = payload_path.read_text(encoding="utf-8")
|
payload = payload_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class McpServiceConfigTests(unittest.TestCase):
|
|||||||
public_upload_url="https://uploads.example.test/custom/upload",
|
public_upload_url="https://uploads.example.test/custom/upload",
|
||||||
enable_ingest=True,
|
enable_ingest=True,
|
||||||
allow_source_files=False,
|
allow_source_files=False,
|
||||||
|
archive_sources=True,
|
||||||
ingest_job_timeout_seconds=111,
|
ingest_job_timeout_seconds=111,
|
||||||
query_job_timeout_seconds=22,
|
query_job_timeout_seconds=22,
|
||||||
ingest_max_request_bytes=333,
|
ingest_max_request_bytes=333,
|
||||||
@@ -81,6 +82,7 @@ class McpServiceConfigTests(unittest.TestCase):
|
|||||||
self.assertIn("OBSIDIAN_MCP_PUBLIC_BASE_URL=http://192.168.1.10:8765/mcp\n", content)
|
self.assertIn("OBSIDIAN_MCP_PUBLIC_BASE_URL=http://192.168.1.10:8765/mcp\n", content)
|
||||||
self.assertIn("OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL=https://uploads.example.test/custom/upload\n", content)
|
self.assertIn("OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL=https://uploads.example.test/custom/upload\n", content)
|
||||||
self.assertIn("OBSIDIAN_MCP_ALLOW_SOURCE_FILES=0\n", content)
|
self.assertIn("OBSIDIAN_MCP_ALLOW_SOURCE_FILES=0\n", content)
|
||||||
|
self.assertIn("WIKI_ARCHIVE_SOURCES=1\n", content)
|
||||||
self.assertIn("OBSIDIAN_MCP_ALLOWED_HOSTS=", content)
|
self.assertIn("OBSIDIAN_MCP_ALLOWED_HOSTS=", content)
|
||||||
self.assertIn("192.168.1.10:*", content)
|
self.assertIn("192.168.1.10:*", content)
|
||||||
self.assertIn("OBSIDIAN_MCP_ALLOWED_ORIGINS=", content)
|
self.assertIn("OBSIDIAN_MCP_ALLOWED_ORIGINS=", content)
|
||||||
@@ -112,6 +114,7 @@ class McpServiceConfigTests(unittest.TestCase):
|
|||||||
public_upload_url="https://uploads.example.test/saved",
|
public_upload_url="https://uploads.example.test/saved",
|
||||||
enabled_tools=("file-assets", "query"),
|
enabled_tools=("file-assets", "query"),
|
||||||
allow_source_files=False,
|
allow_source_files=False,
|
||||||
|
archive_sources=True,
|
||||||
ingest_job_timeout_seconds=444,
|
ingest_job_timeout_seconds=444,
|
||||||
query_job_timeout_seconds=55,
|
query_job_timeout_seconds=55,
|
||||||
ingest_max_request_bytes=666,
|
ingest_max_request_bytes=666,
|
||||||
@@ -128,6 +131,7 @@ class McpServiceConfigTests(unittest.TestCase):
|
|||||||
self.assertEqual(config.public_upload_url, "https://uploads.example.test/saved")
|
self.assertEqual(config.public_upload_url, "https://uploads.example.test/saved")
|
||||||
self.assertEqual(config.enabled_tools, ("file-assets", "query"))
|
self.assertEqual(config.enabled_tools, ("file-assets", "query"))
|
||||||
self.assertFalse(config.allow_source_files)
|
self.assertFalse(config.allow_source_files)
|
||||||
|
self.assertTrue(config.archive_sources)
|
||||||
self.assertEqual(config.ingest_job_timeout_seconds, 444)
|
self.assertEqual(config.ingest_job_timeout_seconds, 444)
|
||||||
self.assertEqual(config.query_job_timeout_seconds, 55)
|
self.assertEqual(config.query_job_timeout_seconds, 55)
|
||||||
self.assertEqual(config.ingest_max_request_bytes, 666)
|
self.assertEqual(config.ingest_max_request_bytes, 666)
|
||||||
@@ -491,6 +495,11 @@ class McpServiceConfigTests(unittest.TestCase):
|
|||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
project_root = Path(tmp) / "project"
|
project_root = Path(tmp) / "project"
|
||||||
project_root.mkdir()
|
project_root.mkdir()
|
||||||
|
(project_root / ".env").write_text(
|
||||||
|
"OBSIDIAN_VAULT_PATH=/data/bjd-vault\n"
|
||||||
|
"WIKI_ARCHIVE_SOURCES=true\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
config_home = Path(tmp) / "config"
|
config_home = Path(tmp) / "config"
|
||||||
manage_mcp_service.write_token_env_file(
|
manage_mcp_service.write_token_env_file(
|
||||||
project_root / ".runtime" / "obsidian-wiki-mcp.env",
|
project_root / ".runtime" / "obsidian-wiki-mcp.env",
|
||||||
@@ -519,6 +528,7 @@ class McpServiceConfigTests(unittest.TestCase):
|
|||||||
self.assertIn("Bearer saved-token", output)
|
self.assertIn("Bearer saved-token", output)
|
||||||
env_content = (project_root / ".runtime" / "obsidian-wiki-mcp.env").read_text(encoding="utf-8")
|
env_content = (project_root / ".runtime" / "obsidian-wiki-mcp.env").read_text(encoding="utf-8")
|
||||||
self.assertIn("OBSIDIAN_MCP_ENABLED_TOOLS=file-assets\n", env_content)
|
self.assertIn("OBSIDIAN_MCP_ENABLED_TOOLS=file-assets\n", env_content)
|
||||||
|
self.assertIn("WIKI_ARCHIVE_SOURCES=1\n", env_content)
|
||||||
self.assertNotIn("OBSIDIAN_MCP_PUBLIC_BASE_URL=", env_content)
|
self.assertNotIn("OBSIDIAN_MCP_PUBLIC_BASE_URL=", env_content)
|
||||||
self.assertNotIn("OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL=", env_content)
|
self.assertNotIn("OBSIDIAN_MCP_INGEST_PUBLIC_UPLOAD_URL=", env_content)
|
||||||
generate_token.assert_not_called()
|
generate_token.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user