Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

Copy link
Copy Markdown

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.

### Changed and ### Fixed are followed immediately by list items. markdownlint reports MD022 at Lines [10] and [18].

Proposed formatting fix
 ### Changed
+
 - **Adopt MCP revision ...
 
 ### Fixed
+
 - **The `omind node` stdio transport ...

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 10, Add a blank line after the “### Changed” and “###
Fixed” headings in CHANGELOG.md, before their respective list items, to satisfy
Markdown heading-spacing requirements.

Source: Linters/SAST tools

- **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

Copy link
Copy Markdown

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

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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 11 - 16, Update the changelog entry’s conformance
result to explicitly state that 17 of 18 checks passed and injection was skipped
intentionally, replacing the ambiguous “17 contracts” wording while preserving
the surrounding SDK and compatibility details.


### 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
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ dependencies = [
"fastapi>=0.110,<1.0",
"uvicorn[standard]>=0.29,<1.0",
"pyyaml>=6.0,<7.0",
"mcp>=1.28.1,<2.0",
# Deliberately stepped to the mcp 2.x major (the cap above did its job and
# held the fleet at 1.x until this was reviewed): 2.0 implements MCP
# revision 2026-07-28, the stateless revision. Re-capped at the next major.
"mcp>=2.0.0,<3.0",
"tomlkit>=0.12,<1.0",
# Security floors for runtime transitives pulled by fastapi/mcp.
"cryptography>=48.0.1,<50.0",
Expand Down
21 changes: 14 additions & 7 deletions src/omind/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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))
PY

Repository: 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:

modelcontextprotocol python-sdk issue 1732 MCPServer lowlevel_server public runner mcp 2.0.0

💡 Result:

In the MCP Python SDK, issue #1732 tracked the major refactoring associated with the release of version 2.0.0, which included renaming the high-level FastMCP class to MCPServer [1][2]. Key points regarding these changes and the SDK structure include: - Rename and Restructure: The class FastMCP was renamed to MCPServer to clarify its purpose and reduce confusion with external projects [1][2]. As part of this refactor, transport-specific configuration parameters (such as host and port) were moved from the class constructor to the run() method to decouple server definition from transport configuration [1][3]. - MCP 2.0.0 Release: Version 2.0.0 of the MCP Python SDK was released on July 28, 2026 [2][4]. This major update supports the 2026-07-28 revision of the Model Context Protocol [2][5]. - Low-Level Server: The SDK provides a low-level Server class for advanced use cases requiring direct protocol control [6][7]. While MCPServer (the high-level, decorator-based layer) is built on top of this low-level Server, developers can drop down to the low-level implementation when specific fine-grained control is needed [6][8]. Note that utility commands like mcp run and mcp dev generally expect an MCPServer instance and do not directly support the low-level Server class [7]. - Public Runner/Infrastructure: The 2.0.0 release is published via public, GitHub-hosted runner environments [4]. For users transitioning from v1.x, the official migration guide is recommended, as v2.0.0 contains several breaking changes and architectural reworks [2][4]. Version 1.x is currently in maintenance mode [2].

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/workflows

Repository: 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/workflows

Repository: CryptoJones/omind

Length of output: 14225


Constrain the supported mcp API.

MCPServer.run_stdio_async() cannot replace this call because _fd_stdio_server() supplies custom streams. If mcp>=2.0.0,<3.0 remains, pin the tested API range and run run_node against each supported version in CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/omind/server.py` around lines 489 - 492, Constrain the supported mcp
dependency range to the tested API versions while retaining the low-level runner
in _fd_stdio_server(), since MCPServer.run_stdio_async() cannot handle its
custom streams. Update CI so run_node executes against every supported mcp
version within that range, including the existing initialization flow via
_lowlevel_server.

)

anyio.run(run_stdio)
Expand Down
57 changes: 30 additions & 27 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
"""
Expand All @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 tests

Repository: 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' src

Repository: 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' src

Repository: 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.lock

Repository: 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.lock

Repository: 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 || true

Repository: CryptoJones/omind

Length of output: 15461


🌐 Web query:

https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md

💡 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 #3002, server identity information was moved from the DiscoverResult body to the result _meta field (using the key io.modelcontextprotocol/serverInfo). Additionally, the clientInfo field in the request envelope was demoted from a required field to a SHOULD [5][6]. Deprecated Subsystems: As of the 2026-07-28 protocol version (SEP-2577), the roots, sampling, and logging subsystems are deprecated in favor of a multi-round-trip input_required pattern [7][8]. This revision also includes updates to wire codecs, handling of per-session state, and changes to authentication and subscription mechanisms [1][2]. Users currently on MCP SDK v1.x are advised to first complete the upgrade to v2 using the upgrade-to-v2.md guide before proceeding with the 2026-07-28 migration [1][7][3].

Citations:


🌐 Web query:

MCP 2026-07-28 resultType ttlMs cacheScope tools/call

💡 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:

site:github.com/modelcontextprotocol/typescript-sdk "resultType" "ttlMs" "cacheScope"

💡 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 tools/call and assert resultType; assert ttlMs and cacheScope on the cacheable tools/list response. Keep the existing 2025-06-18 initialization and clean-EOF assertions. In-process call_tool() cannot detect serialization regressions in _fd_stdio_server.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_server.py` around lines 57 - 63, Extend the stdio smoke test
around the existing 2025-06-18 initialization and clean-EOF checks to send a
tools/call request and assert its resultType, then assert ttlMs and cacheScope
on the cacheable tools/list response. Exercise the actual _fd_stdio_server wire
path rather than relying on the in-process call() helper, while preserving the
existing initialization and EOF assertions.



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",
Expand Down Expand Up @@ -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"
Expand All @@ -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"})
Expand All @@ -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"}
Expand All @@ -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"]
Expand All @@ -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})
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -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"})
Expand All @@ -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"):
Expand Down Expand Up @@ -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"})
Expand All @@ -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()
Expand Down
Loading