"""The planning list, published over MCP. **Why this exists, stated without a claim it cannot support.** A stateful list the model writes to and reads back is a thing users of this network asked for, and it is the deliverable. It is not offered because it makes a small model better at the task, and this repository holds a measurement pointing the other way: ``distinct_tools/skills.py`` and ``distinct_tools/local.py`` both record that the one direct small-model result found has Llama-1B at 23.2 per cent when planning for itself against 25.2 per cent with no plan at all. Those figures are attributed there to ``TOOL_CATEGORIES_REVIEW.md`` section 3. That document is not present in this repository, so the numbers are repeated here as recorded by those modules and not as anything re-measured. Nothing has been measured about this server. Publishing a tool over MCP changes who wrote it and how it was approved; it does not change how well a model uses it. What the literature does support is the narrower half: :mod:`distinct_tools.workspace` sets it out, and it is externalising state rather than delegating judgement. So this tool stores and returns. It does not rank, summarise, infer a next step or advise, because a list that told a 0.6B model what to do next would be an opinion nobody measured. **It reuses the existing store rather than opening a second one.** The tool published here is literally the ``todo@1`` handler and spec from :mod:`distinct_tools.workspace`, taken from a registry, so there is exactly one implementation and one set of bounds. Two properties depend on that and would break under a copy: * the list is keyed by run, and one worker serves several people at once, so a second store would be a second chance to leak one user's list into another user's run; * ``distinct_agent.worker`` erases the run's list in a ``finally`` block by calling ``STATE.forget(job_id)``. A parallel store would not be reached by that call, and prompt-derived text would outlive the run on a volunteer's machine, which is the one thing this network promises does not happen. Reaching the run identity is what needs the ``ToolContext``, which is why this server is added with ``add_context_tool``. Asking the model to supply a list identifier instead would mean one run could read another's list by guessing a string. """ from __future__ import annotations from collections.abc import Mapping from typing import Any from distinct_tools.core import Registry, ToolContext, ToolHandler, ToolRef from distinct_tools.mcp import InProcessMcpServer, McpServerManifest from distinct_tools.workspace import register_workspace_tools from .common import definition_from_spec, structured #: The exact tool this server republishes. Named as a ref, not a string, so a #: rename in the workspace module fails loudly here instead of silently #: publishing nothing. TODO_REF = ToolRef("todo", "1") #: The manifest an operator would approve to install this server. It declares #: no hosts, and ``pinned`` is empty, so the first install records the digests #: and :func:`distinct_tools.mcp.pin_manifest` returns the manifest to persist. #: The timeout is short because every call answers from memory; a list tool #: that took seconds would be a list tool that was doing something else. LIST_MANIFEST = McpServerManifest(slug="plan", version="1", timeout_seconds=5.0) def _same_shape_as_the_direct_path(handler: ToolHandler) -> ToolHandler: """Publish the handler's own mapping as structured content. Without this the in-process transport would wrap the handler's result in a text block, so the model would receive the list as a *string* of JSON on this route and as an object on the direct route. One tool answering in two shapes depending on which one the operator happened to approve is a trap for both the harness and the model, and it costs twice the bytes. """ def call(arguments: Mapping[str, Any], context: ToolContext) -> dict[str, Any]: return structured(handler(arguments, context)) return call def list_mcp_server() -> InProcessMcpServer: """An in-process MCP server publishing the per-run planning list. Install it with ``install_mcp_server(registry, list_mcp_server(), LIST_MANIFEST)``, which produces ``plan.todo@1``. That is a different ref from the directly registered ``todo@1``: the same capability reached two ways, and the operator approves whichever one they want, or neither. """ source = Registry() register_workspace_tools(source, only=frozenset({TODO_REF})) entry = source.resolve(TODO_REF) server = InProcessMcpServer() server.add_context_tool( definition_from_spec(entry.spec), _same_shape_as_the_direct_path(entry.handler) ) return server __all__ = ["LIST_MANIFEST", "TODO_REF", "list_mcp_server"]