aevum-mcp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Build
10
+ dist/
11
+ build/
12
+
13
+ # Tools
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .pytest_cache/
17
+ .hypothesis/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Verify scripts (run locally, never commit)
30
+ verify_phase*.py
31
+ scripts/verify_phase*.py
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: aevum-mcp
3
+ Version: 0.2.0
4
+ Summary: Aevum — MCP server exposing the five functions as tools.
5
+ Project-URL: Homepage, https://aevum.build
6
+ Project-URL: Repository, https://github.com/aevum-labs/aevum
7
+ License: Apache-2.0
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: aevum-core
15
+ Requires-Dist: mcp>=1.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: mypy>=1.10; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.9; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # aevum-mcp
24
+
25
+ MCP server for Aevum — exposes all five governed functions (`ingest`, `query`, `review`, `commit`, `replay`) as MCP tools, plus A2A task management.
26
+
27
+ ```bash
28
+ pip install aevum-mcp
29
+ python -m aevum.mcp
30
+ ```
31
+
32
+ Compatible with Claude Desktop and any MCP client. See the [main repository README](https://github.com/aevum-labs/aevum) for configuration.
@@ -0,0 +1,10 @@
1
+ # aevum-mcp
2
+
3
+ MCP server for Aevum — exposes all five governed functions (`ingest`, `query`, `review`, `commit`, `replay`) as MCP tools, plus A2A task management.
4
+
5
+ ```bash
6
+ pip install aevum-mcp
7
+ python -m aevum.mcp
8
+ ```
9
+
10
+ Compatible with Claude Desktop and any MCP client. See the [main repository README](https://github.com/aevum-labs/aevum) for configuration.
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "aevum-mcp"
3
+ version = "0.2.0"
4
+ description = "Aevum — MCP server exposing the five functions as tools."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "Apache-2.0" }
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Typing :: Typed",
14
+ ]
15
+ dependencies = [
16
+ "aevum-core",
17
+ "mcp>=1.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ aevum-mcp = "aevum.mcp.__main__:main"
22
+
23
+ [project.urls]
24
+ Homepage = "https://aevum.build"
25
+ Repository = "https://github.com/aevum-labs/aevum"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/aevum"]
33
+
34
+ [tool.uv.sources]
35
+ aevum-core = { workspace = true }
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ asyncio_mode = "auto"
40
+ addopts = "--tb=short"
41
+ pythonpath = ["src", "tests"]
42
+
43
+ [tool.mypy]
44
+ strict = true
45
+ python_version = "3.11"
46
+ mypy_path = "src"
47
+ explicit_package_bases = true
48
+ ignore_missing_imports = true
49
+
50
+ [tool.ruff]
51
+ line-length = 130
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
55
+ ignore = ["ANN401"]
56
+
57
+ [project.optional-dependencies]
58
+ dev = [
59
+ "pytest>=8.0",
60
+ "pytest-asyncio>=0.23",
61
+ "mypy>=1.10",
62
+ "ruff>=0.9",
63
+ ]
@@ -0,0 +1,22 @@
1
+ """
2
+ aevum.mcp — MCP server exposing Aevum's five functions as tools.
3
+
4
+ Claude Desktop config (~/.claude/claude_desktop_config.json):
5
+ {
6
+ "mcpServers": {
7
+ "aevum": {
8
+ "command": "python",
9
+ "args": ["-m", "aevum.mcp"],
10
+ "env": {
11
+ "AEVUM_API_KEY": "your-key-here"
12
+ }
13
+ }
14
+ }
15
+ }
16
+ """
17
+
18
+ from aevum.mcp.server import create_server
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__ = ["create_server"]
@@ -0,0 +1,29 @@
1
+ """
2
+ python -m aevum.mcp — start the Aevum MCP server (stdio transport).
3
+
4
+ For Claude Desktop, register in ~/.claude/claude_desktop_config.json:
5
+ {
6
+ "mcpServers": {
7
+ "aevum": {
8
+ "command": "python",
9
+ "args": ["-m", "aevum.mcp"]
10
+ }
11
+ }
12
+ }
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from aevum.core.engine import Engine
18
+
19
+ from aevum.mcp.server import create_server
20
+
21
+
22
+ def main() -> None:
23
+ engine = Engine()
24
+ mcp = create_server(engine)
25
+ mcp.run(transport="stdio")
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,95 @@
1
+ """
2
+ A2A task format — Pydantic model and state machine for agent-to-agent task exchange.
3
+
4
+ Task states: created, working, input_required, completed, failed, cancelled.
5
+ Task IDs map directly to Aevum audit_ids for provenance tracking.
6
+ A full A2A HTTP server is not included; the MCP tools provide the integration surface.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Literal
12
+
13
+ from pydantic import BaseModel, ConfigDict
14
+
15
+ A2ATaskState = Literal[
16
+ "created", "working", "input_required", "completed", "failed", "cancelled"
17
+ ]
18
+
19
+
20
+ class A2AMessage(BaseModel):
21
+ model_config = ConfigDict(frozen=True)
22
+ role: Literal["user", "agent"]
23
+ content: str
24
+ timestamp: str | None = None
25
+
26
+
27
+ class A2AArtifact(BaseModel):
28
+ model_config = ConfigDict(frozen=True)
29
+ artifact_id: str
30
+ name: str
31
+ content: str
32
+ mime_type: str = "text/plain"
33
+
34
+
35
+ class A2ATask(BaseModel):
36
+ """
37
+ A2A-compatible task representation.
38
+
39
+ The aevum audit_id maps to the A2A task_id.
40
+ Task state maps to ledger event types:
41
+ created -> commit(event_type="agent.task.created")
42
+ working -> commit(event_type="agent.task.working")
43
+ input_required -> pending_review (review_required=True)
44
+ completed -> commit(event_type="agent.task.completed")
45
+ failed -> commit(event_type="agent.task.failed")
46
+ cancelled -> commit(event_type="agent.task.cancelled")
47
+ """
48
+
49
+ model_config = ConfigDict(frozen=True)
50
+
51
+ task_id: str # maps to aevum audit_id
52
+ name: str
53
+ state: A2ATaskState
54
+ description: str = ""
55
+ messages: list[A2AMessage] = []
56
+ artifacts: list[A2AArtifact] = []
57
+ metadata: dict[str, Any] = {}
58
+
59
+ @classmethod
60
+ def created(cls, task_id: str, name: str, description: str = "") -> A2ATask:
61
+ return cls(task_id=task_id, name=name, state="created", description=description)
62
+
63
+ @classmethod
64
+ def completed(
65
+ cls,
66
+ task_id: str,
67
+ name: str,
68
+ result: str,
69
+ artifacts: list[A2AArtifact] | None = None,
70
+ ) -> A2ATask:
71
+ return cls(
72
+ task_id=task_id,
73
+ name=name,
74
+ state="completed",
75
+ messages=[A2AMessage(role="agent", content=result)],
76
+ artifacts=artifacts or [],
77
+ )
78
+
79
+ @classmethod
80
+ def input_required(cls, task_id: str, name: str, prompt: str) -> A2ATask:
81
+ return cls(
82
+ task_id=task_id,
83
+ name=name,
84
+ state="input_required",
85
+ messages=[A2AMessage(role="agent", content=prompt)],
86
+ )
87
+
88
+ @classmethod
89
+ def failed(cls, task_id: str, name: str, error: str) -> A2ATask:
90
+ return cls(
91
+ task_id=task_id,
92
+ name=name,
93
+ state="failed",
94
+ messages=[A2AMessage(role="agent", content=f"Task failed: {error}")],
95
+ )
File without changes
@@ -0,0 +1,209 @@
1
+ """
2
+ Aevum MCP server — five governed functions exposed as MCP tools, plus two
3
+ A2A task tools (create_task, get_task) backed by the episodic ledger.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from aevum.core.engine import Engine
11
+
12
+ from aevum.mcp.a2a import A2ATask
13
+ from mcp.server.fastmcp import FastMCP
14
+
15
+
16
+ def create_server(engine: Engine | None = None) -> FastMCP:
17
+ """
18
+ Create the Aevum MCP server.
19
+
20
+ Args:
21
+ engine: Aevum kernel. Uses Engine() with in-memory defaults if None.
22
+
23
+ Returns:
24
+ Configured FastMCP server instance.
25
+ """
26
+ _engine = engine or Engine()
27
+ mcp = FastMCP("aevum")
28
+
29
+ @mcp.tool()
30
+ def ingest(
31
+ data: dict[str, Any],
32
+ provenance: dict[str, Any],
33
+ purpose: str,
34
+ subject_id: str,
35
+ actor: str = "mcp-user",
36
+ idempotency_key: str | None = None,
37
+ ) -> dict[str, Any]:
38
+ """
39
+ Move data through the governed membrane into the Aevum knowledge graph.
40
+
41
+ Requires an active consent grant for the actor + subject_id + purpose.
42
+ Returns an OutputEnvelope with status (ok/error/crisis) and audit_id.
43
+ """
44
+ result = _engine.ingest(
45
+ data=data,
46
+ provenance=provenance,
47
+ purpose=purpose,
48
+ subject_id=subject_id,
49
+ actor=actor,
50
+ idempotency_key=idempotency_key,
51
+ )
52
+ return result.model_dump(mode="json")
53
+
54
+ @mcp.tool()
55
+ def query(
56
+ purpose: str,
57
+ subject_ids: list[str],
58
+ actor: str = "mcp-user",
59
+ classification_max: int = 0,
60
+ ) -> dict[str, Any]:
61
+ """
62
+ Traverse the Aevum knowledge graph and return context for the declared purpose.
63
+
64
+ Requires consent grants for all subject_ids. Results are filtered by
65
+ classification_max (Barrier 2). Active complications contribute to results.
66
+ Returns an OutputEnvelope with the assembled context.
67
+ """
68
+ result = _engine.query(
69
+ purpose=purpose,
70
+ subject_ids=subject_ids,
71
+ actor=actor,
72
+ classification_max=classification_max,
73
+ )
74
+ return result.model_dump(mode="json")
75
+
76
+ @mcp.tool()
77
+ def review(
78
+ audit_id: str,
79
+ action: str | None = None,
80
+ actor: str = "mcp-user",
81
+ ) -> dict[str, Any]:
82
+ """
83
+ Get status of or act on a pending human review gate.
84
+
85
+ action: None = poll status, "approve" = approve, "veto" = veto.
86
+ Veto-as-default: if a deadline was set and has elapsed, silence = veto.
87
+ Returns an OutputEnvelope with review status.
88
+ """
89
+ result = _engine.review(
90
+ audit_id=audit_id,
91
+ actor=actor,
92
+ action=action,
93
+ )
94
+ return result.model_dump(mode="json")
95
+
96
+ @mcp.tool()
97
+ def commit(
98
+ event_type: str,
99
+ payload: dict[str, Any],
100
+ actor: str = "mcp-user",
101
+ idempotency_key: str | None = None,
102
+ ) -> dict[str, Any]:
103
+ """
104
+ Append an event to the Aevum episodic ledger.
105
+
106
+ event_type must not use kernel-reserved prefixes (ingest., query., etc.).
107
+ Use a namespaced prefix: "myapp.user_action", "myapp.decision", etc.
108
+ Returns an OutputEnvelope with the new entry's audit_id.
109
+ """
110
+ result = _engine.commit(
111
+ event_type=event_type,
112
+ payload=payload,
113
+ actor=actor,
114
+ idempotency_key=idempotency_key,
115
+ )
116
+ return result.model_dump(mode="json")
117
+
118
+ @mcp.tool()
119
+ def replay(
120
+ audit_id: str,
121
+ actor: str = "mcp-user",
122
+ ) -> dict[str, Any]:
123
+ """
124
+ Reconstruct a past Aevum decision faithfully.
125
+
126
+ Returns the original ledger entry payload as it existed at the time
127
+ it was recorded. The reconstruction is deterministic and read-only.
128
+ Requires consent for the replay operation on the original subject.
129
+ """
130
+ result = _engine.replay(
131
+ audit_id=audit_id,
132
+ actor=actor,
133
+ )
134
+ return result.model_dump(mode="json")
135
+
136
+ @mcp.tool()
137
+ def create_task(
138
+ name: str,
139
+ description: str = "",
140
+ payload: dict[str, Any] | None = None,
141
+ actor: str = "mcp-user",
142
+ ) -> dict[str, Any]:
143
+ """
144
+ Create an A2A-compatible task and record it in the Aevum ledger.
145
+
146
+ The returned task_id is an aevum audit_id (urn:aevum:audit:...).
147
+ Poll task status with get_task(task_id).
148
+ Task state transitions are tracked via ledger events.
149
+ """
150
+ committed = _engine.commit(
151
+ event_type="agent.task.created",
152
+ payload={
153
+ "name": name,
154
+ "description": description,
155
+ "task_payload": payload or {},
156
+ },
157
+ actor=actor,
158
+ )
159
+ task = A2ATask.created(
160
+ task_id=committed.audit_id,
161
+ name=name,
162
+ description=description,
163
+ )
164
+ return task.model_dump(mode="json")
165
+
166
+ @mcp.tool()
167
+ def get_task(
168
+ task_id: str,
169
+ actor: str = "mcp-user",
170
+ ) -> dict[str, Any]:
171
+ """
172
+ Get the current state of an A2A task by its audit_id.
173
+
174
+ Replays the ledger entry to reconstruct task state.
175
+ Returns an A2ATask with the current state and any messages.
176
+ """
177
+ replayed = _engine.replay(audit_id=task_id, actor=actor)
178
+
179
+ if replayed.status == "error":
180
+ task = A2ATask.failed(
181
+ task_id=task_id,
182
+ name="unknown",
183
+ error=replayed.data.get("error_detail", "Task not found"),
184
+ )
185
+ return task.model_dump(mode="json")
186
+
187
+ original_payload = replayed.data.get("replayed_payload", {})
188
+ name = original_payload.get("name", "task")
189
+
190
+ # Map ledger event_type to A2A state
191
+ event_type = original_payload.get("event_type", "agent.task.created")
192
+ state_map = {
193
+ "agent.task.created": "created",
194
+ "agent.task.working": "working",
195
+ "agent.task.completed": "completed",
196
+ "agent.task.failed": "failed",
197
+ "agent.task.cancelled": "cancelled",
198
+ }
199
+ a2a_state = state_map.get(event_type, "created")
200
+
201
+ task = A2ATask(
202
+ task_id=task_id,
203
+ name=name,
204
+ state=a2a_state, # type: ignore[arg-type]
205
+ description=original_payload.get("description", ""),
206
+ )
207
+ return task.model_dump(mode="json")
208
+
209
+ return mcp
@@ -0,0 +1,88 @@
1
+ """
2
+ Tests for A2A task format and MCP tools.
3
+ Uses direct tool invocation -- no real MCP protocol.
4
+
5
+ NO tests/__init__.py (standing rule).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ from aevum.core.engine import Engine
13
+
14
+ from aevum.mcp.a2a import A2ATask
15
+ from aevum.mcp.server import create_server
16
+
17
+
18
+ class TestA2AModels:
19
+ def test_created_task(self) -> None:
20
+ task = A2ATask.created("urn:aevum:audit:abc", "Test Task", "desc")
21
+ assert task.task_id == "urn:aevum:audit:abc"
22
+ assert task.state == "created"
23
+ assert task.name == "Test Task"
24
+
25
+ def test_completed_task(self) -> None:
26
+ task = A2ATask.completed("urn:aevum:audit:abc", "Test", "Done successfully")
27
+ assert task.state == "completed"
28
+ assert len(task.messages) == 1
29
+ assert task.messages[0].role == "agent"
30
+
31
+ def test_failed_task(self) -> None:
32
+ task = A2ATask.failed("urn:aevum:audit:abc", "Test", "Something went wrong")
33
+ assert task.state == "failed"
34
+
35
+ def test_input_required_task(self) -> None:
36
+ task = A2ATask.input_required("urn:aevum:audit:abc", "Test", "Need approval")
37
+ assert task.state == "input_required"
38
+
39
+ def test_task_serializable(self) -> None:
40
+ task = A2ATask.created("urn:aevum:audit:abc", "Test")
41
+ # Must be JSON-serializable (MCP protocol requirement)
42
+ json.dumps(task.model_dump(mode="json"))
43
+
44
+ def test_valid_states(self) -> None:
45
+ valid = ["created", "working", "input_required", "completed", "failed", "cancelled"]
46
+ for state in valid:
47
+ task = A2ATask(task_id="urn:aevum:audit:abc", name="t", state=state) # type: ignore[arg-type]
48
+ assert task.state == state
49
+
50
+
51
+ class TestA2AMcpTools:
52
+ def test_create_task_tool(self) -> None:
53
+ mcp = create_server(Engine())
54
+ tool_fn = mcp._tool_manager._tools["create_task"].fn # type: ignore[attr-defined]
55
+ result = tool_fn(name="Test Task", description="A test", payload={"x": 1})
56
+ assert result["state"] == "created"
57
+ assert result["task_id"].startswith("urn:aevum:audit:")
58
+ assert result["name"] == "Test Task"
59
+
60
+ def test_get_task_tool(self) -> None:
61
+ mcp = create_server(Engine())
62
+ create_fn = mcp._tool_manager._tools["create_task"].fn # type: ignore[attr-defined]
63
+ get_fn = mcp._tool_manager._tools["get_task"].fn # type: ignore[attr-defined]
64
+
65
+ created = create_fn(name="Fetch Me", description="test")
66
+ task_id = created["task_id"]
67
+
68
+ fetched = get_fn(task_id=task_id)
69
+ assert fetched["task_id"] == task_id
70
+
71
+ def test_get_task_not_found(self) -> None:
72
+ mcp = create_server(Engine())
73
+ get_fn = mcp._tool_manager._tools["get_task"].fn # type: ignore[attr-defined]
74
+ result = get_fn(task_id="urn:aevum:audit:00000000-0000-7000-8000-000000000999")
75
+ assert result["state"] == "failed"
76
+
77
+ def test_seven_tools_registered(self) -> None:
78
+ mcp = create_server(Engine())
79
+ tools = set(mcp._tool_manager._tools.keys()) # type: ignore[attr-defined]
80
+ for expected in ["ingest", "query", "review", "commit", "replay",
81
+ "create_task", "get_task"]:
82
+ assert expected in tools, f"Missing tool: {expected}"
83
+
84
+ def test_create_task_result_json_serializable(self) -> None:
85
+ mcp = create_server(Engine())
86
+ tool_fn = mcp._tool_manager._tools["create_task"].fn # type: ignore[attr-defined]
87
+ result = tool_fn(name="Test", payload={})
88
+ json.dumps(result) # Must not raise
@@ -0,0 +1,143 @@
1
+ """
2
+ Tests for aevum-mcp server.
3
+
4
+ Tests call tool functions directly — no stdio protocol in CI.
5
+ The MCP server registration and tool schema are verified separately.
6
+
7
+ NO tests/__init__.py (standing rule).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+
14
+ from aevum.core.consent.models import ConsentGrant
15
+ from aevum.core.engine import Engine
16
+
17
+ from aevum.mcp.server import create_server
18
+
19
+
20
+ def _engine_with_consent() -> Engine:
21
+ engine = Engine()
22
+ engine.add_consent_grant(ConsentGrant(
23
+ grant_id="g1",
24
+ subject_id="subject-1",
25
+ grantee_id="mcp-user",
26
+ operations=["ingest", "query", "replay", "export"],
27
+ purpose="mcp-testing",
28
+ classification_max=3,
29
+ granted_at="2026-01-01T00:00:00Z",
30
+ expires_at="2030-01-01T00:00:00Z",
31
+ ))
32
+ return engine
33
+
34
+
35
+ def _prov() -> dict: # type: ignore[type-arg]
36
+ return {
37
+ "source_id": "test-src",
38
+ "chain_of_custody": ["test-src"],
39
+ "classification": 0,
40
+ "ingest_audit_id": "urn:aevum:audit:00000000-0000-7000-8000-000000000001",
41
+ "model_id": None,
42
+ }
43
+
44
+
45
+ def test_server_creates_successfully() -> None:
46
+ mcp = create_server()
47
+ assert mcp is not None
48
+ assert mcp.name == "aevum"
49
+
50
+
51
+ def test_five_tools_registered() -> None:
52
+ mcp = create_server()
53
+ tool_names = list(mcp._tool_manager._tools.keys()) # type: ignore[attr-defined]
54
+ for expected in ["ingest", "query", "review", "commit", "replay"]:
55
+ assert expected in tool_names, f"Tool '{expected}' not registered"
56
+
57
+
58
+ def test_commit_tool_returns_envelope() -> None:
59
+ mcp = create_server(engine=Engine())
60
+ tool_fn = mcp._tool_manager._tools["commit"].fn # type: ignore[attr-defined]
61
+ result = tool_fn(
62
+ event_type="app.test",
63
+ payload={"k": "v"},
64
+ actor="mcp-user",
65
+ )
66
+ assert result["status"] == "ok"
67
+ assert result["audit_id"].startswith("urn:aevum:audit:")
68
+
69
+
70
+ def test_ingest_tool_requires_consent() -> None:
71
+ mcp = create_server(engine=Engine()) # No consent grants
72
+ tool_fn = mcp._tool_manager._tools["ingest"].fn # type: ignore[attr-defined]
73
+ result = tool_fn(
74
+ data={"content": "test"},
75
+ provenance=_prov(),
76
+ purpose="mcp-testing",
77
+ subject_id="subject-1",
78
+ actor="mcp-user",
79
+ )
80
+ assert result["status"] == "error"
81
+ assert result["data"]["error_code"] == "consent_required"
82
+
83
+
84
+ def test_ingest_tool_with_consent() -> None:
85
+ mcp = create_server(engine=_engine_with_consent())
86
+ tool_fn = mcp._tool_manager._tools["ingest"].fn # type: ignore[attr-defined]
87
+ result = tool_fn(
88
+ data={"content": "hello"},
89
+ provenance=_prov(),
90
+ purpose="mcp-testing",
91
+ subject_id="subject-1",
92
+ actor="mcp-user",
93
+ )
94
+ assert result["status"] == "ok"
95
+
96
+
97
+ def test_query_tool_with_consent() -> None:
98
+ engine = _engine_with_consent()
99
+ mcp = create_server(engine=engine)
100
+
101
+ ingest_fn = mcp._tool_manager._tools["ingest"].fn # type: ignore[attr-defined]
102
+ ingest_fn(data={"x": 1}, provenance=_prov(),
103
+ purpose="mcp-testing", subject_id="subject-1")
104
+
105
+ query_fn = mcp._tool_manager._tools["query"].fn # type: ignore[attr-defined]
106
+ result = query_fn(purpose="mcp-testing", subject_ids=["subject-1"])
107
+ assert result["status"] == "ok"
108
+
109
+
110
+ def test_replay_tool() -> None:
111
+ engine = Engine()
112
+ mcp = create_server(engine=engine)
113
+
114
+ commit_fn = mcp._tool_manager._tools["commit"].fn # type: ignore[attr-defined]
115
+ committed = commit_fn(event_type="app.replayable", payload={"v": 42})
116
+ audit_id = committed["audit_id"]
117
+
118
+ replay_fn = mcp._tool_manager._tools["replay"].fn # type: ignore[attr-defined]
119
+ result = replay_fn(audit_id=audit_id)
120
+ assert result["status"] == "ok"
121
+ assert result["data"]["replayed_payload"]["v"] == 42
122
+
123
+
124
+ def test_crisis_content_returns_crisis() -> None:
125
+ mcp = create_server(engine=Engine())
126
+ tool_fn = mcp._tool_manager._tools["ingest"].fn # type: ignore[attr-defined]
127
+ result = tool_fn(
128
+ data={"content": "I want to kill myself"},
129
+ provenance=_prov(),
130
+ purpose="test",
131
+ subject_id="subject-1",
132
+ )
133
+ assert result["status"] == "crisis"
134
+
135
+
136
+ def test_all_tools_return_serializable_dicts() -> None:
137
+ """MCP protocol requires JSON-serializable return values."""
138
+ engine = Engine()
139
+ mcp = create_server(engine=engine)
140
+
141
+ commit_fn = mcp._tool_manager._tools["commit"].fn # type: ignore[attr-defined]
142
+ result = commit_fn(event_type="app.ser_test", payload={"k": "v"})
143
+ json.dumps(result)