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.
- aevum_mcp-0.2.0/.gitignore +31 -0
- aevum_mcp-0.2.0/PKG-INFO +32 -0
- aevum_mcp-0.2.0/README.md +10 -0
- aevum_mcp-0.2.0/pyproject.toml +63 -0
- aevum_mcp-0.2.0/src/aevum/mcp/__init__.py +22 -0
- aevum_mcp-0.2.0/src/aevum/mcp/__main__.py +29 -0
- aevum_mcp-0.2.0/src/aevum/mcp/a2a.py +95 -0
- aevum_mcp-0.2.0/src/aevum/mcp/py.typed +0 -0
- aevum_mcp-0.2.0/src/aevum/mcp/server.py +209 -0
- aevum_mcp-0.2.0/tests/test_a2a.py +88 -0
- aevum_mcp-0.2.0/tests/test_server.py +143 -0
|
@@ -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
|
aevum_mcp-0.2.0/PKG-INFO
ADDED
|
@@ -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)
|