diff --git a/AGENTS.md b/AGENTS.md index d4c856f..9c848a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,7 +130,7 @@ See `wiki-query` and `wiki-export` skills for how the filter is applied. - **Keep product lookup fields explicit.** Carry forward `bjd_part_numbers`, `competitor_part_numbers`, `series`, `pitch`, `pin_count`, `product_lines`, `customers`, `competitors`, `standards`, `business_categories`, `related_pages`, and `catalog_paths` whenever they are known. - **Merge, do not overwrite.** Automatic identifier extraction may add terms, but it must not discard curated registry metadata. Registry values are the durable source for future index rebuilds and remote MCP lookup. - **Prefer precise identifiers in search ranking.** Exact or prefix matches in structured part-number and series fields should rank above incidental filename substrings such as timestamps or archive hashes. -- **Keep remote ingest gated.** MCP ingest tools are write-capable and must remain disabled unless the service config explicitly enables the `ingest` tool group via `OBSIDIAN_MCP_ENABLED_TOOLS=file-assets,ingest`, `mcp_service.sh tools enable ingest`, or `mcp_service.sh start --enable-ingest`. Uploaded files must stage outside the vault under the MCP runtime root; `wiki-ingest` is responsible for accepting, archiving, and indexing sources inside the vault. Use the HTTP multipart endpoint `/mcp-ingest/uploads` for remote file transfer; if the remote agent cannot read the main MCP bearer token, it must call `create_ingest_upload_ticket` and send the returned single-use `X-Obsidian-Wiki-Upload-Token` header. Use `ingest_file_base64` only for small files when a client cannot make multipart HTTP requests. Do not expose chunked upload tools in the MCP tool surface. They may launch local OpenCode only through the fixed `/wiki-ingest` prompt; never expose arbitrary prompt or shell execution through MCP. Remote ingest must keep the OpenCode readiness preflight in front of job launch, and `check_ingest_opencode_readiness(run_smoke_test=true)` should stay available for operators to verify that OpenCode can load the project `wiki-ingest` skill before accepting uploads. +- **Keep remote ingest gated.** MCP ingest tools are write-capable and must remain disabled unless the service config explicitly enables the `ingest` tool group via `OBSIDIAN_MCP_ENABLED_TOOLS=file-assets,ingest`, `mcp_service.sh tools enable ingest`, or `mcp_service.sh start --enable-ingest`. Uploaded files must stage outside the vault under the MCP runtime root; `wiki-ingest` is responsible for accepting, archiving, and indexing sources inside the vault. Use the HTTP multipart endpoint `/mcp-ingest/uploads` for remote file transfer; if the remote agent cannot read the main MCP bearer token, it must call `create_ingest_upload_ticket` and send the returned single-use `X-Obsidian-Wiki-Upload-Token` header. Do not expose base64 or chunked upload tools in the MCP tool surface. They may launch local OpenCode only through the fixed `/wiki-ingest` prompt; never expose arbitrary prompt or shell execution through MCP. Remote ingest must keep the OpenCode readiness preflight in front of job launch, and `check_ingest_opencode_readiness(run_smoke_test=true)` should stay available for operators to verify that OpenCode can load the project `wiki-ingest` skill before accepting uploads. ## Core Principles diff --git a/README.md b/README.md index a3cdd75..71aedf8 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,6 @@ This adds write-capable MCP tools to the same `wiki-file-assets` server and bear - `get_ingest_upload_instructions` — returns the preferred upload methods, HTTP endpoint path, fields, limits, and curl example for remote agents. - `create_ingest_upload_ticket` — returns a short-lived, single-use upload token for the HTTP endpoint so remote agents can upload with curl without reading the main MCP bearer token. -- `ingest_file_base64` — uploads one complete base64 file and optionally starts the local OpenCode ingest job; useful for small files or MCP clients that cannot make multipart HTTP requests. - `check_ingest_opencode_readiness` — verifies the local OpenCode binary, required run flags, project ingest skills, and optionally runs a no-write smoke test that confirms `wiki-ingest` can be loaded. - `start_ingest_job` — queues a local `opencode run --auto --format json` job with a fixed `/wiki-ingest` prompt for the completed uploads. - `get_ingest_job_status`, `get_ingest_job_result`, `cancel_ingest_job` — inspect or cancel the local job. @@ -418,7 +417,7 @@ curl -H "X-Obsidian-Wiki-Upload-Token: " \ http://127.0.0.1:8765/mcp-ingest/uploads ``` -The endpoint accepts repeated `files` fields plus optional `project`, `doc_type_hint`, `note`, and `start_job` fields. It accepts either the same bearer token as `/mcp` or the single-use upload ticket header. For MCP clients that cannot make HTTP multipart requests, `ingest_file_base64` remains available for small files, but large base64 arguments are fragile in many agent tool-call implementations. +The endpoint accepts repeated `files` fields plus optional `project`, `doc_type_hint`, `note`, and `start_job` fields. It accepts either the same bearer token as `/mcp` or the single-use upload ticket header. Base64 file upload is intentionally not exposed as an MCP tool because large JSON arguments are fragile in many agent tool-call implementations. Run `check_ingest_opencode_readiness` before the first remote upload. Use `run_smoke_test: true` when you want OpenCode itself to prove it can load the project `wiki-ingest` skill; otherwise the static preflight is also run automatically before every `start_ingest_job`. diff --git a/obsidian_wiki/mcp_server.py b/obsidian_wiki/mcp_server.py index 8d023ef..6c2be95 100644 --- a/obsidian_wiki/mcp_server.py +++ b/obsidian_wiki/mcp_server.py @@ -3,8 +3,6 @@ from __future__ import annotations import argparse -import base64 -import binascii import json import hmac import os @@ -248,11 +246,6 @@ def create_server( "-F 'files=@drawing.pdf' http://:/mcp-ingest/uploads" ), }, - "mcp_base64_tool": { - "name": "ingest_file_base64", - "best_for": "small files or MCP clients that cannot make HTTP multipart requests", - "start_job_default": True, - }, "limits": { "max_file_bytes": ingest_manager.max_file_bytes, "max_chunk_bytes": ingest_manager.max_chunk_bytes, @@ -265,29 +258,6 @@ def create_server( }, } - @server.tool() - def ingest_file_base64( - file_name: str, - data_base64: str, - project: str | None = None, - doc_type_hint: str | None = None, - note: str | None = None, - start_job: bool = True, - ) -> dict[str, Any]: - """Upload one complete base64 file and optionally start the local OpenCode ingest job.""" - try: - payload = base64.b64decode(str(data_base64).strip(), validate=True) - except (binascii.Error, ValueError) as exc: - raise IngestError("data_base64 is not valid base64") from exc - upload = ingest_manager.create_completed_upload( - file_name=file_name, - payload=payload, - project=project, - doc_type_hint=doc_type_hint, - ) - job = ingest_manager.start_ingest_job([upload["upload_id"]], note=note) if start_job else None - return {"upload": upload, "job": job} - @server.tool() def create_ingest_upload_ticket( expires_seconds: int = DEFAULT_UPLOAD_TICKET_SECONDS, diff --git a/tests/test_mcp_server_remote.py b/tests/test_mcp_server_remote.py index 79afdc8..d809de2 100644 --- a/tests/test_mcp_server_remote.py +++ b/tests/test_mcp_server_remote.py @@ -1,5 +1,4 @@ import asyncio -import base64 import hashlib import json import os @@ -354,7 +353,7 @@ class RemoteMcpServerTests(unittest.TestCase): self.assertIn("check_ingest_opencode_readiness", FakeMCP.tool_names) self.assertIn("get_ingest_upload_instructions", FakeMCP.tool_names) self.assertIn("create_ingest_upload_ticket", FakeMCP.tool_names) - self.assertIn("ingest_file_base64", FakeMCP.tool_names) + self.assertNotIn("ingest_file_base64", FakeMCP.tool_names) def test_create_ingest_upload_ticket_tool_returns_http_only_token(self): class FakeMCP: @@ -407,57 +406,6 @@ class RemoteMcpServerTests(unittest.TestCase): self.assertEqual(ticket["max_files"], 1) self.assertFalse(ticket["start_job"]) - def test_ingest_file_base64_tool_uploads_without_chunking(self): - class FakeMCP: - tools = {} - - def __init__(self, _name, **_kwargs): - pass - - def tool(self): - def decorator(func): - self.tools[func.__name__] = func - return func - - return decorator - - def resource(self, *_args, **_kwargs): - def decorator(func): - return func - - return decorator - - def prompt(self, *_args, **_kwargs): - def decorator(func): - return func - - return decorator - - with ( - tempfile.TemporaryDirectory() as tmp, - patch.dict("sys.modules", {"mcp.server.fastmcp": type("Mod", (), {"FastMCP": FakeMCP})()}), - ): - root = Path(tmp) - mcp_server.create_server( - vault_path=root / "vault", - enable_ingest=True, - project_root=root / "project", - ingest_runtime_root=root / "runtime", - ) - payload = b"one shot upload" - result = FakeMCP.tools["ingest_file_base64"]( - file_name="source.pdf", - data_base64=base64.b64encode(payload).decode("ascii"), - project="bjd", - doc_type_hint="drawing", - start_job=False, - ) - - self.assertEqual(result["job"], None) - self.assertEqual(result["upload"]["state"], "complete") - self.assertEqual(result["upload"]["file_name"], "source.pdf") - self.assertEqual(result["upload"]["sha256"], "sha256:" + hashlib.sha256(payload).hexdigest()) - def test_create_server_can_disable_file_asset_tools(self): class FakeMCP: tool_names = []