nutria-plugin 0.3.0__tar.gz → 0.3.2__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.
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/CHANGELOG.md +10 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/PKG-INFO +23 -3
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/README.md +22 -2
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/index.md +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/manifest.md +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/python-api.md +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/quickstart.md +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/reviewable-actions.md +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/pyproject.toml +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/__init__.py +14 -1
- nutria_plugin-0.3.2/src/nutria_plugin/mcp_results.py +104 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/packaging.py +1 -0
- nutria_plugin-0.3.2/tests/test_mcp_results.py +70 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_packaging.py +11 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/uv.lock +1 -1
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/.github/workflows/publish.yml +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/.gitignore +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/admin-extensions.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/admin-flows.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/cli.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/connection-types.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/security.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/skill-format.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/README.md +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/hooks/hooks.json +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/plugin.json +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/settings.schema.json +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/bundle.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/capabilities.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/cli.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/manifest.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/signing.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_bundle.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_capability_contracts.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_cli.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_manifest.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_reviewable_actions.py +0 -0
- {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_signing.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
- Add transport-neutral MCP result normalization for FastMCP scalar JSON envelopes.
|
|
6
|
+
- Add declared-output projection and cardinality conformance helpers for plugin and host tests.
|
|
7
|
+
- Document protocol-level validation in addition to static manifest validation.
|
|
8
|
+
|
|
9
|
+
## 0.3.1
|
|
10
|
+
|
|
11
|
+
- Create missing parent directories for explicit plugin package output paths.
|
|
12
|
+
|
|
3
13
|
## 0.3.0
|
|
4
14
|
|
|
5
15
|
- Replace adapter-specific prepared actions with one portable exact-preview protocol.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nutria-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: SDK for building, validating, signing, and packaging Nutria plugins
|
|
5
5
|
Project-URL: Homepage, https://github.com/AlRos14/nutria-plugin-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/AlRos14/nutria-plugin-sdk
|
|
@@ -21,7 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
SDK for building, validating, signing, and packaging Nutria plugins.
|
|
23
23
|
|
|
24
|
-
Release `0.3.
|
|
24
|
+
Release `0.3.2` uses the intentionally breaking schema introduced in 0.3.0. It accepts only manifest schema
|
|
25
25
|
`3.0`, requires typed capabilities and world providers, and makes authority,
|
|
26
26
|
audience, task-context requirements, and exposure explicit. There is no runtime
|
|
27
27
|
migration for schema 2.0/2.1 manifests and no compatibility/model-callability
|
|
@@ -34,7 +34,7 @@ and exact delivery of an approved snapshot.
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
uv add nutria-plugin==0.3.
|
|
37
|
+
uv add nutria-plugin==0.3.2
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Scaffold and validate
|
|
@@ -119,6 +119,26 @@ errors = validate_plugin_dir(Path("."))
|
|
|
119
119
|
archive = pack_plugin(Path("."), Path("dist/plugin.zip"))
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### Validate MCP results against declared outputs
|
|
123
|
+
|
|
124
|
+
Manifest validation checks the descriptor itself. Protocol-level plugin tests
|
|
125
|
+
must also validate the payload Nutria receives after MCP transport:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from nutria_plugin import validate_capability_result
|
|
129
|
+
|
|
130
|
+
payload = validate_capability_result(
|
|
131
|
+
capability,
|
|
132
|
+
structured_content=call_result.structuredContent,
|
|
133
|
+
text_content=call_result.content[0].text,
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This normalizes FastMCP's scalar JSON envelope and raises
|
|
138
|
+
`MCPResultContractError` when a declared path such as `.items` does not
|
|
139
|
+
materialize. It does not execute the tool and should be used in a bounded,
|
|
140
|
+
fixture-backed or read-only MCP integration test.
|
|
141
|
+
|
|
122
142
|
The package exports the strict manifest, capability, provider, reviewable-action,
|
|
123
143
|
admin extension/flow, bundle, packaging, and signing models/functions documented
|
|
124
144
|
in [`docs/python-api.md`](docs/python-api.md).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
SDK for building, validating, signing, and packaging Nutria plugins.
|
|
4
4
|
|
|
5
|
-
Release `0.3.
|
|
5
|
+
Release `0.3.2` uses the intentionally breaking schema introduced in 0.3.0. It accepts only manifest schema
|
|
6
6
|
`3.0`, requires typed capabilities and world providers, and makes authority,
|
|
7
7
|
audience, task-context requirements, and exposure explicit. There is no runtime
|
|
8
8
|
migration for schema 2.0/2.1 manifests and no compatibility/model-callability
|
|
@@ -15,7 +15,7 @@ and exact delivery of an approved snapshot.
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
uv add nutria-plugin==0.3.
|
|
18
|
+
uv add nutria-plugin==0.3.2
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Scaffold and validate
|
|
@@ -100,6 +100,26 @@ errors = validate_plugin_dir(Path("."))
|
|
|
100
100
|
archive = pack_plugin(Path("."), Path("dist/plugin.zip"))
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### Validate MCP results against declared outputs
|
|
104
|
+
|
|
105
|
+
Manifest validation checks the descriptor itself. Protocol-level plugin tests
|
|
106
|
+
must also validate the payload Nutria receives after MCP transport:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from nutria_plugin import validate_capability_result
|
|
110
|
+
|
|
111
|
+
payload = validate_capability_result(
|
|
112
|
+
capability,
|
|
113
|
+
structured_content=call_result.structuredContent,
|
|
114
|
+
text_content=call_result.content[0].text,
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This normalizes FastMCP's scalar JSON envelope and raises
|
|
119
|
+
`MCPResultContractError` when a declared path such as `.items` does not
|
|
120
|
+
materialize. It does not execute the tool and should be used in a bounded,
|
|
121
|
+
fixture-backed or read-only MCP integration test.
|
|
122
|
+
|
|
103
123
|
The package exports the strict manifest, capability, provider, reviewable-action,
|
|
104
124
|
admin extension/flow, bundle, packaging, and signing models/functions documented
|
|
105
125
|
in [`docs/python-api.md`](docs/python-api.md).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# plugin.json manifest reference
|
|
2
2
|
|
|
3
3
|
`plugin.json` is the single source of truth for plugin identity, runtime,
|
|
4
|
-
capability authority, and provider topology. SDK 0.3.
|
|
4
|
+
capability authority, and provider topology. SDK 0.3.1 accepts exactly schema
|
|
5
5
|
`3.0`; older schemas and unknown fields fail validation.
|
|
6
6
|
|
|
7
7
|
## Required top-level fields
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Reviewable actions and external writes
|
|
2
2
|
|
|
3
|
-
SDK 0.3.
|
|
3
|
+
SDK 0.3.1 schema 3.0 defines one reviewable-action architecture:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
ChatBotNutralia -> task context, encrypted draft, revision, approval, idempotency, receipts
|
|
@@ -15,7 +15,7 @@ Signing:
|
|
|
15
15
|
generate_keypair, sign_manifest, verify_manifest, SignatureStatus
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "0.3.
|
|
18
|
+
__version__ = "0.3.2"
|
|
19
19
|
|
|
20
20
|
from .capabilities import (
|
|
21
21
|
CapabilityDescriptor,
|
|
@@ -55,6 +55,13 @@ from .manifest import (
|
|
|
55
55
|
from .bundle import PluginBundleError, extract_plugin_bundle, load_plugin_bundle, validate_zip
|
|
56
56
|
from .packaging import PackagingError, pack_plugin, scaffold_plugin, validate_plugin_dir
|
|
57
57
|
from .signing import SignatureStatus, generate_keypair, sign_manifest, verify_manifest
|
|
58
|
+
from .mcp_results import (
|
|
59
|
+
MCPResultContractError,
|
|
60
|
+
normalize_mcp_result,
|
|
61
|
+
project_result_path,
|
|
62
|
+
validate_capability_result,
|
|
63
|
+
validate_declared_outputs,
|
|
64
|
+
)
|
|
58
65
|
|
|
59
66
|
__all__ = [
|
|
60
67
|
"__version__",
|
|
@@ -103,4 +110,10 @@ __all__ = [
|
|
|
103
110
|
"generate_keypair",
|
|
104
111
|
"sign_manifest",
|
|
105
112
|
"verify_manifest",
|
|
113
|
+
# MCP result contracts
|
|
114
|
+
"MCPResultContractError",
|
|
115
|
+
"normalize_mcp_result",
|
|
116
|
+
"project_result_path",
|
|
117
|
+
"validate_capability_result",
|
|
118
|
+
"validate_declared_outputs",
|
|
106
119
|
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""MCP result normalization and declared-output conformance helpers.
|
|
2
|
+
|
|
3
|
+
These helpers intentionally do not import the MCP package. Plugin tests and
|
|
4
|
+
Nutria hosts can pass protocol values they already received while the SDK keeps
|
|
5
|
+
one definition of the payload against which manifest ``result_path`` bindings
|
|
6
|
+
are evaluated.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Iterable, Mapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MCPResultContractError(ValueError):
|
|
17
|
+
"""Raised when a successful MCP result violates declared output bindings."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _decode_json_container(value: Any) -> Any:
|
|
21
|
+
if not isinstance(value, str):
|
|
22
|
+
return value
|
|
23
|
+
try:
|
|
24
|
+
decoded = json.loads(value)
|
|
25
|
+
except (TypeError, json.JSONDecodeError):
|
|
26
|
+
return value
|
|
27
|
+
return decoded if isinstance(decoded, dict | list) else value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def normalize_mcp_result(
|
|
31
|
+
*,
|
|
32
|
+
structured_content: Any = None,
|
|
33
|
+
text_content: str | None = None,
|
|
34
|
+
) -> Any:
|
|
35
|
+
"""Return the canonical provider payload from an MCP tool response.
|
|
36
|
+
|
|
37
|
+
Structured content is authoritative. FastMCP wraps tools annotated as
|
|
38
|
+
returning ``str`` in an exact ``{"result": <string>}`` object; when that
|
|
39
|
+
scalar is a complete JSON object or array, it represents the tool payload
|
|
40
|
+
and is safely unwrapped. All other structured objects are preserved.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
if structured_content is not None:
|
|
44
|
+
if isinstance(structured_content, Mapping) and set(structured_content) == {"result"}:
|
|
45
|
+
decoded = _decode_json_container(structured_content.get("result"))
|
|
46
|
+
if isinstance(decoded, dict | list):
|
|
47
|
+
return decoded
|
|
48
|
+
return structured_content
|
|
49
|
+
return _decode_json_container(text_content or "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def project_result_path(payload: Any, result_path: str) -> Any:
|
|
53
|
+
"""Resolve one manifest result path or raise a conformance error."""
|
|
54
|
+
|
|
55
|
+
declared_path = str(result_path or "").strip()
|
|
56
|
+
if not declared_path:
|
|
57
|
+
raise MCPResultContractError("declared output is missing result_path")
|
|
58
|
+
if declared_path == "$":
|
|
59
|
+
return payload
|
|
60
|
+
if not declared_path.startswith("."):
|
|
61
|
+
raise MCPResultContractError(f"invalid declared output path: {declared_path}")
|
|
62
|
+
|
|
63
|
+
current = payload
|
|
64
|
+
for part in declared_path[1:].split("."):
|
|
65
|
+
if not part or not isinstance(current, Mapping) or part not in current:
|
|
66
|
+
raise MCPResultContractError(
|
|
67
|
+
f"declared output path did not materialize: {declared_path}"
|
|
68
|
+
)
|
|
69
|
+
current = current[part]
|
|
70
|
+
if current is None:
|
|
71
|
+
raise MCPResultContractError(
|
|
72
|
+
f"declared output path did not materialize: {declared_path}"
|
|
73
|
+
)
|
|
74
|
+
return current
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validate_declared_outputs(
|
|
78
|
+
payload: Any,
|
|
79
|
+
produces: Iterable[Mapping[str, Any]],
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Assert that every declared output exists with compatible cardinality."""
|
|
82
|
+
|
|
83
|
+
for binding in produces:
|
|
84
|
+
value = project_result_path(payload, str(binding.get("result_path") or ""))
|
|
85
|
+
if binding.get("many") and not isinstance(value, list):
|
|
86
|
+
raise MCPResultContractError(
|
|
87
|
+
f"declared many output is not a list: {binding.get('result_path')}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_capability_result(
|
|
92
|
+
capability: Mapping[str, Any],
|
|
93
|
+
*,
|
|
94
|
+
structured_content: Any = None,
|
|
95
|
+
text_content: str | None = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""Normalize an MCP result and validate one capability's output contract."""
|
|
98
|
+
|
|
99
|
+
payload = normalize_mcp_result(
|
|
100
|
+
structured_content=structured_content,
|
|
101
|
+
text_content=text_content,
|
|
102
|
+
)
|
|
103
|
+
validate_declared_outputs(payload, capability.get("produces") or ())
|
|
104
|
+
return payload
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from nutria_plugin.mcp_results import (
|
|
6
|
+
MCPResultContractError,
|
|
7
|
+
normalize_mcp_result,
|
|
8
|
+
validate_capability_result,
|
|
9
|
+
validate_declared_outputs,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_normalizes_fastmcp_json_string_result_envelope() -> None:
|
|
14
|
+
payload = normalize_mcp_result(
|
|
15
|
+
structured_content={
|
|
16
|
+
"result": '{"ok":true,"chats":[{"chat_ref":"chat-1"}]}'
|
|
17
|
+
},
|
|
18
|
+
text_content="ignored",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
assert payload == {"ok": True, "chats": [{"chat_ref": "chat-1"}]}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_preserves_provider_owned_result_object() -> None:
|
|
25
|
+
payload = normalize_mcp_result(
|
|
26
|
+
structured_content={"result": {"id": "op-1"}, "status": "complete"}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
assert payload == {"result": {"id": "op-1"}, "status": "complete"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_validates_nested_many_output() -> None:
|
|
33
|
+
validate_declared_outputs(
|
|
34
|
+
{"data": {"items": [{"id": "item-1"}]}},
|
|
35
|
+
[{"result_path": ".data.items", "many": True}],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_rejects_missing_declared_output() -> None:
|
|
40
|
+
with pytest.raises(
|
|
41
|
+
MCPResultContractError,
|
|
42
|
+
match=r"declared output path did not materialize: \.chats",
|
|
43
|
+
):
|
|
44
|
+
validate_declared_outputs(
|
|
45
|
+
{"result": '{"chats":[]}'},
|
|
46
|
+
[{"result_path": ".chats", "many": True}],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_rejects_non_list_many_output() -> None:
|
|
51
|
+
with pytest.raises(MCPResultContractError, match="many output is not a list"):
|
|
52
|
+
validate_declared_outputs(
|
|
53
|
+
{"messages": {"id": "message-1"}},
|
|
54
|
+
[{"result_path": ".messages", "many": True}],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_validates_capability_against_protocol_values() -> None:
|
|
59
|
+
capability = {
|
|
60
|
+
"produces": [
|
|
61
|
+
{"result_path": ".chats", "resource_type": "whatsapp_chat", "many": True}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
payload = validate_capability_result(
|
|
66
|
+
capability,
|
|
67
|
+
structured_content={"result": '{"ok":true,"chats":[]}'},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert payload == {"ok": True, "chats": []}
|
|
@@ -56,6 +56,17 @@ def test_pack_plugin_produces_zip(tmp_path):
|
|
|
56
56
|
assert out.stat().st_size > 0
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def test_pack_plugin_creates_output_directory(tmp_path):
|
|
60
|
+
src = tmp_path / "myplugin"
|
|
61
|
+
scaffold_plugin(src, "my-plugin", "My Plugin")
|
|
62
|
+
out = tmp_path / "dist" / "nested" / "output.zip"
|
|
63
|
+
|
|
64
|
+
result = pack_plugin(src, out)
|
|
65
|
+
|
|
66
|
+
assert result == out
|
|
67
|
+
assert out.is_file()
|
|
68
|
+
|
|
69
|
+
|
|
59
70
|
def test_pack_plugin_default_output_name(tmp_path, monkeypatch):
|
|
60
71
|
src = tmp_path / "myplugin"
|
|
61
72
|
scaffold_plugin(src, "my-plugin")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|