-
Notifications
You must be signed in to change notification settings - Fork 1
feat!: adopt MCP revision 2026-07-28 via the mcp 2.x SDK #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |
|
|
||
| ## [Unreleased] | ||
|
|
||
| ### Changed | ||
| - **Adopt MCP revision `2026-07-28` (the stateless revision) by moving to the | ||
| `mcp` 2.x SDK** (`mcp>=2.0.0,<3.0`; the previous `<2.0` cap did its job and | ||
| held the fleet at 1.x until this was reviewed). `FastMCP` is now `MCPServer`; | ||
| the tool decorators and every tool's behaviour are unchanged, and a v2 server | ||
| still serves 2025-era clients from the same process, so existing MCP clients | ||
| keep working. Verified green against `mcp-conformance` 0.2.0 (17 contracts). | ||
|
Comment on lines
+11
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Document the skipped conformance check. The PR reports 17 of 18 checks passing, with injection deliberately skipped. “17 contracts” omits the denominator and can imply complete conformance. State the result as “17 of 18 checks passed; injection was skipped intentionally.” 🤖 Prompt for AI Agents |
||
|
|
||
| ### Fixed | ||
| - **The `omind node` stdio transport went silent under the 2.x SDK.** Our | ||
| fd-readiness transport parsed lines with | ||
| `mcp.types.JSONRPCMessage.model_validate_json`, but in 2.x `JSONRPCMessage` is | ||
| a plain union alias with no such method; the resulting `AttributeError` was | ||
| swallowed into the read stream and every request hung until timeout. It now | ||
| parses through the SDK's `jsonrpc_message_adapter`, matching the SDK's own | ||
| stdio transport. | ||
| - The same transport serialized replies with `exclude_none=True`; switched to | ||
| `exclude_unset=True` (what the SDK's transport uses), which is what keeps the | ||
| 2026-07-28 envelope fields — `resultType`, `ttlMs`, `cacheScope` — on the wire. | ||
|
|
||
| ### Removed | ||
| - Remove the deprecated `graph-path`, `graph-orphans`, `graph-dangling`, and | ||
| `graph-stats` MCP compatibility aliases after the 5.0 bridge release. Use the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,7 @@ | |
| import anyio | ||
| import mcp.types as mcp_types | ||
| from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream | ||
| from mcp.server.fastmcp import FastMCP | ||
| from mcp.server.mcpserver import MCPServer | ||
| from mcp.shared.message import SessionMessage | ||
|
|
||
| from omind import graph | ||
|
|
@@ -63,7 +63,10 @@ async def _fd_stdio_server() -> AsyncIterator[ | |
|
|
||
| async def send_line(line: str) -> None: | ||
| try: | ||
| message = mcp_types.JSONRPCMessage.model_validate_json(line) | ||
| # mcp 2.x: JSONRPCMessage is a plain union alias, not a RootModel — | ||
| # it has no .model_validate_json. Parse through the SDK's TypeAdapter, | ||
| # with the same flags the SDK's own stdio transport uses. | ||
| message = mcp_types.jsonrpc_message_adapter.validate_json(line, by_name=False) | ||
| except Exception as exc: | ||
| await read_stream_writer.send(exc) | ||
| return | ||
|
|
@@ -121,8 +124,12 @@ async def stdout_writer() -> None: | |
| try: | ||
| async with write_stream_reader: | ||
| async for session_message in write_stream_reader: | ||
| # exclude_unset (not exclude_none) matches the SDK's own | ||
| # stdio transport. It is what keeps the 2026-07-28 envelope | ||
| # fields the server explicitly set — resultType, ttlMs, | ||
| # cacheScope — on the wire. | ||
| payload = session_message.message.model_dump_json( | ||
| by_alias=True, exclude_none=True | ||
| by_alias=True, exclude_unset=True | ||
| ) | ||
| await write_all((payload + "\n").encode("utf-8")) | ||
| except (anyio.ClosedResourceError, BrokenPipeError): # pragma: no cover | ||
|
|
@@ -171,7 +178,7 @@ def _parse_action_items(items: list[str]) -> list[ActionItem]: | |
| return parsed | ||
|
|
||
|
|
||
| def build_server(omi_dir: Path | str, node_id: str | None = None) -> FastMCP: | ||
| def build_server(omi_dir: Path | str, node_id: str | None = None) -> MCPServer: | ||
| """Build the node MCP server over one OMI folder. | ||
|
|
||
| ``node_id`` (from the mesh config, when initialized) turns on Lamport | ||
|
|
@@ -182,7 +189,7 @@ def build_server(omi_dir: Path | str, node_id: str | None = None) -> FastMCP: | |
| # the mesh daemon, not just this server's tools. | ||
| store = OmiStore(omi_dir, node_id=node_id) | ||
|
|
||
| mcp = FastMCP(SERVER_NAME, instructions=_INSTRUCTIONS) | ||
| mcp = MCPServer(SERVER_NAME, instructions=_INSTRUCTIONS) | ||
|
|
||
| # The five graph tools each rebuilt the whole [[wikilink]] graph from disk | ||
| # (a full-vault read+parse) on every call. Cache it, invalidated by a cheap | ||
|
|
@@ -479,10 +486,10 @@ def run_node(omi_dir: Path, node_id: str | None = None) -> int: | |
| async def run_stdio() -> None: | ||
| mcp = build_server(omi_dir, node_id=node_id) | ||
| async with _fd_stdio_server() as (read_stream, write_stream): | ||
| await mcp._mcp_server.run( # noqa: SLF001 - FastMCP exposes no public lower-level runner. | ||
| await mcp._lowlevel_server.run( # noqa: SLF001 - MCPServer exposes no public lower-level runner. | ||
| read_stream, | ||
| write_stream, | ||
| mcp._mcp_server.create_initialization_options(), # noqa: SLF001 | ||
| mcp._lowlevel_server.create_initialization_options(), # noqa: SLF001 | ||
|
Comment on lines
+489
to
+492
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -a '^uv\.lock$' | xargs -r rg -n 'name = "mcp"|version = "2\.0\.0"|mcp-types'
rg -n 'mcp\s*[<=>]' pyproject.toml
python - <<'PY'
from importlib.metadata import version
from mcp.server.mcpserver import MCPServer
print("mcp:", version("mcp"))
server = MCPServer("compatibility-probe")
lowlevel = getattr(server, "_lowlevel_server", None)
assert callable(getattr(lowlevel, "run", None))
assert callable(getattr(lowlevel, "create_initialization_options", None))
PYRepository: CryptoJones/omind Length of output: 517 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- server.py imports and runner context ---'
sed -n '1,90p' src/omind/server.py
sed -n '450,515p' src/omind/server.py
printf '%s\n' '--- dependency declarations and lock metadata ---'
sed -n '30,55p' pyproject.toml
sed -n '2385,2410p' uv.lock
sed -n '1845,1890p' uv.lock
printf '%s\n' '--- MCP references ---'
rg -n 'MCPServer|lowlevel_server|create_initialization_options|\.run\(' src tests pyproject.toml uv.lock 2>/dev/null || true
printf '%s\n' '--- repository test/config files ---'
git ls-files | rg '(^|/)(test|tests|pyproject|uv\.lock|server).*'Repository: CryptoJones/omind Length of output: 18727 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import zipfile
lock = Path("uv.lock").read_text()
for needle in ("mcp-2.0.0-py3-none-any.whl", "mcp-2.0.0.tar.gz"):
print(needle, needle in lock)
PY
printf '%s\n' '--- SDK source metadata from PyPI ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL -o "$tmpdir/mcp.whl" \
https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl
python3 - "$tmpdir/mcp.whl" <<'PY'
import sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
names = [n for n in z.namelist() if "mcpserver" in n or n.endswith("server.py")]
print("\n".join(names))
for name in names:
if name.endswith("mcpserver.py") or name.endswith("server.py"):
text = z.read(name).decode()
for i, line in enumerate(text.splitlines(), 1):
if "lowlevel_server" in line or "def run" in line or "create_initialization_options" in line:
print(f"{name}:{i}:{line}")
PY
printf '%s\n' '--- upstream references ---'
curl -fsSL https://api.github.com/repos/modelcontextprotocol/python-sdk/issues/1732 |
python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("title")); print(d.get("state")); print(d.get("body",""))' |
sed -n '1,160p'Repository: CryptoJones/omind Length of output: 4803 🌐 Web query:
💡 Result: In the MCP Python SDK, issue Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL -o "$tmpdir/pypi.json" https://pypi.org/pypi/mcp/json
python3 - "$tmpdir/pypi.json" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
versions = []
for version, files in data["releases"].items():
parts = version.split(".")
if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit() and int(parts[0]) == 2:
versions.append(version)
print("2.x releases:", ", ".join(sorted(versions, key=lambda v: tuple(int(x) if x.isdigit() else 0 for x in v.split(".")))))
PY
curl -fsSL -o "$tmpdir/mcp-2.0.0.whl" \
https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl
python3 - "$tmpdir/mcp-2.0.0.whl" <<'PY'
import sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
name = "mcp/server/mcpserver/server.py"
lines = z.read(name).decode().splitlines()
for start, end in ((340, 405), (1008, 1030)):
print(f"--- {name}:{start}-{end} ---")
for i in range(start, min(end, len(lines)) + 1):
print(f"{i}:{lines[i-1]}")
PY
printf '%s\n' '--- local call site and tests ---'
sed -n '1,85p' tests/test_server.py
rg -n -C 4 'run_node|_fd_stdio_server|stdio' tests src/omind/server.py .github/workflowsRepository: CryptoJones/omind Length of output: 14225 🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL -o "$tmpdir/pypi.json" https://pypi.org/pypi/mcp/json
python3 - "$tmpdir/pypi.json" <<'PY'
import json, sys
data = json.load(open(sys.argv[1]))
versions = []
for version in data["releases"]:
parts = version.split(".")
if len(parts) >= 2 and parts[0].isdigit() and parts[1].isdigit() and int(parts[0]) == 2:
versions.append(version)
print("2.x releases:", ", ".join(sorted(versions, key=lambda v: tuple(int(x) if x.isdigit() else 0 for x in v.split(".")))))
PY
curl -fsSL -o "$tmpdir/mcp-2.0.0.whl" \
https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl
python3 - "$tmpdir/mcp-2.0.0.whl" <<'PY'
import sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
name = "mcp/server/mcpserver/server.py"
lines = z.read(name).decode().splitlines()
for start, end in ((340, 405), (1008, 1030)):
print(f"--- {name}:{start}-{end} ---")
for i in range(start, min(end, len(lines)) + 1):
print(f"{i}:{lines[i-1]}")
PY
printf '%s\n' '--- local call site and tests ---'
sed -n '1,85p' tests/test_server.py
rg -n -C 4 'run_node|_fd_stdio_server|stdio' tests src/omind/server.py .github/workflowsRepository: CryptoJones/omind Length of output: 14225 Constrain the supported
🤖 Prompt for AI Agents |
||
| ) | ||
|
|
||
| anyio.run(run_stdio) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
| # Copyright 2026 Aaron K. Clark | ||
| """Tests for omind.server: the `omind node` mesh-node MCP server. | ||
|
|
||
| In-process tests drive FastMCP's tool layer directly; one subprocess smoke | ||
| In-process tests drive MCPServer's tool layer directly; one subprocess smoke | ||
| test does a real stdio handshake and asserts the clean-exit-on-EOF contract | ||
| (the regression test for the obsidian-mcp hang class, issue #49). | ||
| """ | ||
|
|
@@ -19,8 +19,8 @@ | |
| from typing import Any | ||
|
|
||
| import pytest | ||
| from mcp.server.fastmcp import FastMCP | ||
| from mcp.server.fastmcp.exceptions import ToolError | ||
| from mcp.server.mcpserver import MCPServer | ||
| from mcp.server.mcpserver.exceptions import ToolError | ||
|
|
||
| from omind.paths import sync_signal_path | ||
| from omind.server import build_server | ||
|
|
@@ -50,23 +50,26 @@ def omi_dir(tmp_path: Path) -> Path: | |
|
|
||
|
|
||
| @pytest.fixture | ||
| def server(omi_dir: Path) -> FastMCP: | ||
| def server(omi_dir: Path) -> MCPServer: | ||
| return build_server(omi_dir, node_id="testnode-abc123") | ||
|
|
||
|
|
||
| def call(server: FastMCP, name: str, args: dict[str, Any]) -> Any: | ||
| """Invoke a tool in-process and return its structured result.""" | ||
| _content, structured = asyncio.run(server.call_tool(name, args)) | ||
| return structured | ||
| def call(server: MCPServer, name: str, args: dict[str, Any]) -> Any: | ||
| """Invoke a tool in-process and return its structured result. | ||
|
|
||
| v2's ``call_tool`` returns a ``CallToolResult`` rather than v1's | ||
| ``(content, structured)`` tuple. | ||
| """ | ||
| return asyncio.run(server.call_tool(name, args)).structured_content | ||
|
Comment on lines
+57
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 \
'run_node|_fd_stdio_server|jsonrpc_message_adapter|exclude_unset|resultType|ttlMs|cacheScope|subprocess' \
src testsRepository: CryptoJones/omind Length of output: 50374 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- server symbols ---'
rg -n -C 6 'class MCPServer|call_tool|_fd_stdio_server|jsonrpc_message_adapter|exclude_unset|resultType|ttlMs|cacheScope|2026|2025' src/omind tests/test_server.py
printf '%s\n' '--- test_server.py structure ---'
ast-grep outline tests/test_server.py
printf '%s\n' '--- smoke test ---'
sed -n '330,435p' tests/test_server.py
printf '%s\n' '--- server implementation ---'
fd -i -t f 'server.py' srcRepository: CryptoJones/omind Length of output: 43385 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- server symbols ---'
rg -n -C 6 'class MCPServer|call_tool|_fd_stdio_server|jsonrpc_message_adapter|exclude_unset|resultType|ttlMs|cacheScope|2026|2025' src/omind tests/test_server.py
printf '%s\n' '--- test_server.py structure ---'
ast-grep outline tests/test_server.py
printf '%s\n' '--- smoke test ---'
sed -n '330,435p' tests/test_server.py
printf '%s\n' '--- server implementation ---'
fd -i -t f 'server.py' srcRepository: CryptoJones/omind Length of output: 43385 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- server transport and imports ---'
sed -n '1,155p' src/omind/server.py
printf '%s\n' '--- server construction and tool handlers ---'
sed -n '430,510p' src/omind/server.py
rg -n -C 5 'resultType|ttlMs|cacheScope|protocolVersion|2026-07-28|2025-06-18|CallToolResult|ToolAnnotations|structuredContent|structured_content' src tests pyproject.toml uv.lock
printf '%s\n' '--- dependency declarations ---'
rg -n -C 4 'mcp|model-context-protocol|pydantic' pyproject.toml uv.lockRepository: CryptoJones/omind Length of output: 50373 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- server transport and imports ---'
sed -n '1,155p' src/omind/server.py
printf '%s\n' '--- server construction and tool handlers ---'
sed -n '430,510p' src/omind/server.py
printf '%s\n' '--- protocol and result fields ---'
rg -n -C 5 'resultType|ttlMs|cacheScope|protocolVersion|2026-07-28|2025-06-18|CallToolResult|ToolAnnotations|structuredContent|structured_content' src tests pyproject.toml uv.lock
printf '%s\n' '--- dependency declarations ---'
rg -n -C 4 'mcp|model-context-protocol|pydantic' pyproject.toml uv.lockRepository: CryptoJones/omind Length of output: 50373 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- build_server and tool result declarations ---'
sed -n '150,430p' src/omind/server.py
printf '%s\n' '--- all protocol-field references, excluding lockfile noise ---'
rg -n 'resultType|ttlMs|cacheScope|protocolVersion|2026-07-28|2025-06-18|model_dump_json|exclude_unset' \
src tests --glob '*.py' --glob '*.md'
printf '%s\n' '--- package metadata and tracked test configuration ---'
rg -n -C 3 'mcp|pytest|test_server|pythonpath|addopts' pyproject.toml pytest.ini setup.cfg tox.ini .github 2>/dev/null || trueRepository: CryptoJones/omind Length of output: 15461 🌐 Web query:
💡 Result: The document support-2026-07-28.md serves as a technical migration guide for developers already using the Model Context Protocol (MCP) TypeScript SDK v2 packages who wish to adopt the 2026-07-28 protocol revision [1][2]. This migration path is architectural and is not supported by automated codemods [3][4]. Key aspects of the 2026-07-28 revision include: Explicit Opt-in: The SDK does not use the 2026-07-28 protocol by default. Adopting it requires explicit configuration, such as using ClientOptions.versionNegotiation on the client side [1][2]. Server Entry Points: The revision introduces specific handlers to support the new protocol era. For HTTP, developers use createMcpHandler, which by default can serve both 2025-era and 2026-era traffic. For stdio or long-lived connections, the serveStdio entry point is used to pin the connection to the 2026-07-28 protocol [1][2]. Server Identity and Meta: Based on spec PR Citations:
🌐 Web query:
💡 Result: In the Model Context Protocol (MCP) as of the July 28, 2026, specification, resultType, ttlMs, and cacheScope are key components introduced to improve protocol efficiency, specifically regarding caching and request handling [1][2]. ResultType The resultType field is a required property in all MCP result responses that allows clients to determine how to parse and handle the result object [1][3]. - "complete": Indicates the request finished successfully and the result contains the final content [1][3]. - "input_required": Indicates the request is incomplete and requires additional information (often associated with the Multi Round-Trip Requests or MRTR pattern) [1][3]. For backward compatibility, clients must treat absent resultType fields as "complete" [1][3]. Caching Fields (ttlMs and cacheScope) These fields are part of a CacheableResult interface and are required on results from specific operations, such as tools/list, prompts/list, and various resource-related calls [1][4]. They provide caching hints to clients to reduce redundant polling [1][5]. - ttlMs (Time-to-live): An integer value in milliseconds specifying how long a client may consider the result fresh [6][1]. It acts as a freshness hint, not a guarantee, and may be invalidated by other notification mechanisms like list_changed [4]. - cacheScope: Defines the intended scope of the cached response, similar to HTTP Cache-Control headers [6][5]. - "public": The response contains no user-specific data and may be shared/stored by any client or proxy [6][5]. - "private": The response contains private data that must not be shared across different authorization contexts [6][5]. tools/call The tools/call method is used to invoke a specific tool [7][8]. While the tool list results (returned via tools/list) carry the caching metadata described above, the actual execution result of a tools/call itself typically follows the standard response structure containing the final content (marked as resultType: "complete") or signals the need for further input via an InputRequiredResult [7][1]. Top results: [7][6][1][2][4] Citations:
🌐 Web query:
💡 Result: In the context of the Model Context Protocol (MCP) as of the 2026-07-28 protocol revision, resultType, ttlMs, and cacheScope are wire-level fields used to manage result handling, discrimination, and caching behavior [1][2]. resultType This is a required discriminator field on the wire that enables the receiver to classify the nature of a result before performing schema validation [1][3][2]. - For standard successful operations, it is typically set to 'complete' [1][4][5]. - Other values, such as 'input_required', trigger specific client-side behaviors (e.g., auto-fulfillment) [1][3]. - If the field is absent or contains an unknown value in the 2026-era protocol, it is treated as a protocol violation or an error [1][3][2]. - In many implementations, this field is consumed and stripped at the protocol layer, meaning it is often not visible to application-level code [1][2]. ttlMs (Time-to-Live in milliseconds) This field specifies the duration for which a cacheable result remains valid [1][2]. - It is a non-negative integer (defaults to 0 if omitted or malformed) [1][4][6]. - It informs the client's response-cache layer on how long to store the result [1]. cacheScope This field defines the visibility or sharing policy of the cached result [1][2]. - It must be either 'public' or 'private' [7][4]. - It defaults to 'private' (the most conservative policy) if absent or malformed [1][4][6]. These fields are essential components of the "cache hints" mechanism in modern MCP revisions, allowing servers to advertise caching policies to clients [1][2]. While these fields are mandatory on the wire for cacheable operations, they are often handled automatically by the SDK's encoding and decoding layers, ensuring that application code can operate with a cleaner, more abstract result type [1][7][3]. Citations:
Extend the stdio smoke test to cover the negotiated wire contract. Send 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_exposes_exactly_the_designed_tools(server: FastMCP) -> None: | ||
| def test_exposes_exactly_the_designed_tools(server: MCPServer) -> None: | ||
| tools = asyncio.run(server.list_tools()) | ||
| assert {t.name for t in tools} == EXPECTED_TOOLS | ||
| assert all(t.description for t in tools) | ||
|
|
||
|
|
||
| def test_create_read_round_trip(server: FastMCP, omi_dir: Path) -> None: | ||
| def test_create_read_round_trip(server: MCPServer, omi_dir: Path) -> None: | ||
| created = call( | ||
| server, | ||
| "create-note", | ||
|
|
@@ -98,7 +101,7 @@ def test_create_read_round_trip(server: FastMCP, omi_dir: Path) -> None: | |
| assert "fields" not in raw | ||
|
|
||
|
|
||
| def test_edit_note_partial_update(server: FastMCP) -> None: | ||
| def test_edit_note_partial_update(server: MCPServer) -> None: | ||
| call(server, "create-note", {"title": "Partial", "summary": "old", "tags": ["keep"]}) | ||
| edited = call(server, "edit-note", {"name": "Partial.md", "summary": "new"}) | ||
| assert edited["filename"] == "Partial.md" | ||
|
|
@@ -107,7 +110,7 @@ def test_edit_note_partial_update(server: FastMCP) -> None: | |
| assert got["fields"]["tags"] == ["keep"] # omitted fields untouched | ||
|
|
||
|
|
||
| def test_edit_note_version_conflict(server: FastMCP) -> None: | ||
| def test_edit_note_version_conflict(server: MCPServer) -> None: | ||
| call(server, "create-note", {"title": "Versioned", "summary": "v1"}) | ||
| stale = call(server, "read-note", {"name": "Versioned.md"})["version"] | ||
| call(server, "edit-note", {"name": "Versioned.md", "summary": "v2"}) | ||
|
|
@@ -119,7 +122,7 @@ def test_edit_note_version_conflict(server: FastMCP) -> None: | |
| ) | ||
|
|
||
|
|
||
| def test_delete_archives_and_restore(server: FastMCP, omi_dir: Path) -> None: | ||
| def test_delete_archives_and_restore(server: MCPServer, omi_dir: Path) -> None: | ||
| call(server, "create-note", {"title": "Archived", "summary": "s"}) | ||
| deleted = call(server, "delete-note", {"name": "Archived.md"}) | ||
| assert deleted == {"filename": "Archived.md", "status": "archived"} | ||
|
|
@@ -136,7 +139,7 @@ def test_delete_archives_and_restore(server: FastMCP, omi_dir: Path) -> None: | |
| assert "Archived.md" in names | ||
|
|
||
|
|
||
| def test_search_vault(server: FastMCP) -> None: | ||
| def test_search_vault(server: MCPServer) -> None: | ||
| call(server, "create-note", {"title": "Alpha", "summary": "quantum cats", "tags": ["pets"]}) | ||
| call(server, "create-note", {"title": "Beta", "details": "classical dogs", "tags": ["pets"]}) | ||
| hits = call(server, "search-vault", {"query": "quantum"})["result"] | ||
|
|
@@ -145,7 +148,7 @@ def test_search_vault(server: FastMCP) -> None: | |
| assert {h["filename"] for h in by_tag} == {"Alpha.md", "Beta.md"} | ||
|
|
||
|
|
||
| def test_search_vault_is_bounded_and_pageable(server: FastMCP) -> None: | ||
| def test_search_vault_is_bounded_and_pageable(server: MCPServer) -> None: | ||
| for number in range(7): | ||
| call(server, "create-note", {"title": f"Page {number}", "summary": "shared"}) | ||
| first = call(server, "search-vault", {"query": "shared", "limit": 2}) | ||
|
|
@@ -161,7 +164,7 @@ def test_search_vault_is_bounded_and_pageable(server: FastMCP) -> None: | |
| ) | ||
|
|
||
|
|
||
| def test_every_list_tool_is_bounded(server: FastMCP) -> None: | ||
| def test_every_list_tool_is_bounded(server: MCPServer) -> None: | ||
| """No tool may return the whole vault in one result. | ||
|
|
||
| `list-notes` used to: ~348 KB / 87k tokens on a 744-note vault, in a single | ||
|
|
@@ -195,7 +198,7 @@ def test_every_list_tool_is_bounded(server: FastMCP) -> None: | |
| assert page["count"] <= 1, tool | ||
|
|
||
|
|
||
| def test_search_hits_carry_an_excerpt_of_the_matched_text(server: FastMCP) -> None: | ||
| def test_search_hits_carry_an_excerpt_of_the_matched_text(server: MCPServer) -> None: | ||
| """The excerpt is why a search result is often enough on its own — it shows | ||
| the matched text even when the match is in a section `summary` never shows.""" | ||
| call( | ||
|
|
@@ -209,7 +212,7 @@ def test_search_hits_carry_an_excerpt_of_the_matched_text(server: FastMCP) -> No | |
| assert hit["score"] > 0 | ||
|
|
||
|
|
||
| def test_recall_note_returns_one_bounded_representation(server: FastMCP) -> None: | ||
| def test_recall_note_returns_one_bounded_representation(server: MCPServer) -> None: | ||
| call( | ||
| server, | ||
| "create-note", | ||
|
|
@@ -243,7 +246,7 @@ def test_recall_note_returns_one_bounded_representation(server: FastMCP) -> None | |
| assert section["section"] == "Details" | ||
|
|
||
|
|
||
| def test_help_tool_is_generated_from_live_cli(server: FastMCP) -> None: | ||
| def test_help_tool_is_generated_from_live_cli(server: MCPServer) -> None: | ||
| result = call(server, "help", {"command": "/omind help ai usage"}) | ||
| assert result["ok"] is True | ||
| assert result["command"] == "omind ai usage" | ||
|
|
@@ -253,15 +256,15 @@ def test_help_tool_is_generated_from_live_cli(server: FastMCP) -> None: | |
| assert "usage" in unknown["error"] | ||
|
|
||
|
|
||
| def test_backlinks_and_tags(server: FastMCP) -> None: | ||
| def test_backlinks_and_tags(server: MCPServer) -> None: | ||
| call(server, "create-note", {"title": "Hub", "summary": "s", "tags": ["one"]}) | ||
| call(server, "create-note", {"title": "Spoke", "summary": "see [[Hub]]", "tags": ["two"]}) | ||
| links = call(server, "backlinks", {"name": "Hub.md"})["result"] | ||
| assert [n["filename"] for n in links] == ["Spoke.md"] | ||
| assert call(server, "list-tags", {})["result"] == ["one", "two"] | ||
|
|
||
|
|
||
| def test_graph_tools(server: FastMCP) -> None: | ||
| def test_graph_tools(server: MCPServer) -> None: | ||
| call(server, "create-note", {"title": "A", "summary": "s", "connections": ["B"]}) | ||
| call(server, "create-note", {"title": "B", "summary": "s", "connections": ["C"]}) | ||
| call(server, "create-note", {"title": "C", "summary": "s"}) | ||
|
|
@@ -282,7 +285,7 @@ def test_graph_tools(server: FastMCP) -> None: | |
| assert call(server, "graph", {"op": "stats"})["notes"] == 4 | ||
|
|
||
|
|
||
| def test_unified_graph_validates_operation_and_path_arguments(server: FastMCP) -> None: | ||
| def test_unified_graph_validates_operation_and_path_arguments(server: MCPServer) -> None: | ||
| with pytest.raises(ToolError, match="one of"): | ||
| call(server, "graph", {"op": "unknown"}) | ||
| with pytest.raises(ToolError, match="requires source and target"): | ||
|
|
@@ -312,22 +315,22 @@ def counting(omi: Path) -> Any: | |
| assert calls["n"] == 2 # cache busted, rebuilt once | ||
|
|
||
|
|
||
| def test_graph_neighbors_unknown_note_is_a_tool_error(server: FastMCP) -> None: | ||
| def test_graph_neighbors_unknown_note_is_a_tool_error(server: MCPServer) -> None: | ||
| with pytest.raises(ToolError, match="not found"): | ||
| call(server, "graph-neighbors", {"name": "Nope"}) | ||
|
|
||
|
|
||
| def test_missing_note_is_a_tool_error(server: FastMCP) -> None: | ||
| def test_missing_note_is_a_tool_error(server: MCPServer) -> None: | ||
| with pytest.raises(ToolError, match="not found"): | ||
| call(server, "read-note", {"name": "Nope.md"}) | ||
|
|
||
|
|
||
| def test_traversal_is_a_tool_error(server: FastMCP) -> None: | ||
| def test_traversal_is_a_tool_error(server: MCPServer) -> None: | ||
| with pytest.raises(ToolError, match="path separators"): | ||
| call(server, "read-note", {"name": "../escape.md"}) | ||
|
|
||
|
|
||
| def test_writes_touch_the_sync_signal(server: FastMCP, omi_dir: Path) -> None: | ||
| def test_writes_touch_the_sync_signal(server: MCPServer, omi_dir: Path) -> None: | ||
| signal = sync_signal_path(omi_dir) | ||
| assert not signal.exists() | ||
| call(server, "create-note", {"title": "Trigger", "summary": "s"}) | ||
|
|
@@ -337,7 +340,7 @@ def test_writes_touch_the_sync_signal(server: FastMCP, omi_dir: Path) -> None: | |
| assert signal.stat().st_mtime_ns >= first | ||
|
|
||
|
|
||
| def test_reads_do_not_touch_the_sync_signal(server: FastMCP, omi_dir: Path) -> None: | ||
| def test_reads_do_not_touch_the_sync_signal(server: MCPServer, omi_dir: Path) -> None: | ||
| call(server, "create-note", {"title": "Quiet", "summary": "s"}) | ||
| signal = sync_signal_path(omi_dir) | ||
| signal.unlink() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add blank lines after both headings.
### Changedand### Fixedare followed immediately by list items. markdownlint reports MD022 at Lines [10] and [18].Proposed formatting fix
Also applies to: 18-18
🧰 Tools
🪛 markdownlint-cli2 (0.23.1)
[warning] 10-10: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Source: Linters/SAST tools