Repository navigation
Declare reviewed ThreadDesk MCP read-only tools for dynamic discovery #85
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 |
|---|---|---|
|
|
@@ -242,6 +242,29 @@ def _management_schema() -> dict[str, Any]: | |
| ] | ||
|
|
||
|
|
||
| # Explicit, source-owned capability metadata. Unannotated future tools remain | ||
| # unexposed by dynamic read proxies until their actual effects are reviewed. | ||
| _READ_ONLY_TOOLS = frozenset({ | ||
| "get_reentry", | ||
| "get_issued_handoff", | ||
| "thread_operation_status", | ||
| "list_threads", | ||
| "get_thread", | ||
| "current_thread", | ||
| "list_snapshots", | ||
| "list_gnom_jobs", | ||
| "check_gate", | ||
| }) | ||
| for _tool in TOOLS: | ||
| if _tool["name"] in _READ_ONLY_TOOLS: | ||
| _tool["annotations"] = { | ||
| "readOnlyHint": True, | ||
| "destructiveHint": False, | ||
| "openWorldHint": False, | ||
|
Comment on lines
+260
to
+263
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.
Every stdio initialization still advertises Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
|
|
||
|
|
||
| def wrap_untrusted(label: str, text: str) -> str: | ||
| body = (text or "").replace("</untrusted>", "</ untrusted>") | ||
| return f"<untrusted source=\"threaddesk.{label}\">\n{body}\n</untrusted>" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """MCP read-effect annotations are explicit, not inferred from tool names.""" | ||
|
|
||
| from threaddesk.services.mcp import McpBridge, TOOLS | ||
|
|
||
|
|
||
| def test_tools_advertise_only_reviewed_read_only_calls(): | ||
| # Listing must preserve the original schemas, names and function availability. | ||
| listed = McpBridge(None).list_tools() | ||
| assert listed is TOOLS | ||
| by_name = {tool["name"]: tool for tool in listed} | ||
| for name in ( | ||
| "list_threads", "get_thread", "current_thread", "get_reentry", | ||
| "list_snapshots", "thread_operation_status", "check_gate", | ||
| ): | ||
| assert by_name[name]["annotations"]["readOnlyHint"] is True | ||
| assert by_name[name]["annotations"]["destructiveHint"] is False | ||
|
|
||
| # A function that writes, changes current context, saves HTML, or dispatches | ||
| # another operation must not accidentally become a proxy read. | ||
| for name in ( | ||
| "thread_manage", "knowledge_manage", "add_note", "switch_thread", | ||
| "save_snapshot", "dashboard", "issue_handoff", "generate_prompt", | ||
| ): | ||
| assert by_name[name].get("annotations", {}).get("readOnlyHint") is not True |
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.
For a JSON workspace containing an unanchored legacy thread, or one changed by an older writer, calls such as
list_threads,get_thread,current_thread,get_reentry,list_snapshots, andcheck_gatewith an ID reachJsonStore.get_thread(), which persists a revision anchor and can rewrite the thread while incrementing its revision (json_store.py:472-499). That contradicts the MCP definition thatreadOnlyHint: truemeans the tool does not modify its environment (official schema), allowing the new read proxy to cause persistent workspace changes; withhold the hint for these paths or perform the repair before exposing them as read-only.Useful? React with 👍 / 👎.