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
17 changes: 17 additions & 0 deletions src/powercontext/server/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@
LIST_ARTIFACT_CANDIDATES.operation_id,
GET_ARTIFACT_CANDIDATE.operation_id,
})
_MCP_REVIEW_WRITE_OPERATION_IDS = frozenset({
APPROVE_ARTIFACT_CANDIDATE.operation_id,
REJECT_ARTIFACT_CANDIDATE.operation_id,
REVISE_ARTIFACT_CANDIDATE.operation_id,
})


def _select_mcp_type(route: HTTPRoute, _: MCPType) -> MCPType:
Expand Down Expand Up @@ -143,6 +148,18 @@ def _annotate_mcp_component(
idempotentHint=True,
openWorldHint=False,
)
elif route.operation_id in _MCP_REVIEW_WRITE_OPERATION_IDS:
# Approval and rejection are terminal; a revision replaces the proposal a reviewer last
# inspected. MCP visibility is not an authorization boundary (RFC 0050), so these hints
# only let a host apply its own confirmation policy. An exact replay is rejected by the
# pending-head CAS before anything is written, so repeated identical calls have no
# additional effect and the tools are idempotent in the MCP sense.
component.annotations = ToolAnnotations(
readOnlyHint=False,
destructiveHint=True,
idempotentHint=True,
openWorldHint=False,
)


def create_mcp_server(
Expand Down
22 changes: 22 additions & 0 deletions tests/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,28 @@ async def inspect_annotations() -> dict[str, Any]:
assert resolve.openWorldHint is False


def test_mcp_describes_review_write_side_effects_for_host_approval() -> None:
review_writes = {
"approve_artifact_candidate",
"reject_artifact_candidate",
"revise_artifact_candidate",
}

async def inspect_annotations() -> dict[str, Any]:
async with Client(create_mcp_server(create_app())) as client:
return {tool.name: tool.annotations for tool in await client.list_tools() if tool.name in review_writes}

annotations = run_async(inspect_annotations)

assert set(annotations) == review_writes
for name, decision in annotations.items():
assert decision is not None, f"{name} carries no annotations for an MCP host to prompt on"
assert decision.readOnlyHint is False
assert decision.destructiveHint is True
assert decision.idempotentHint is True
assert decision.openWorldHint is False


def test_mcp_exact_entry_tools_use_nested_citations() -> None:
async def exact_entry_tool_schemas() -> dict[str, dict[str, Any]]:
server = create_mcp_server(create_app())
Expand Down
Loading