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.
Files changed (38) hide show
  1. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/CHANGELOG.md +10 -0
  2. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/PKG-INFO +23 -3
  3. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/README.md +22 -2
  4. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/index.md +1 -1
  5. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/manifest.md +1 -1
  6. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/python-api.md +1 -1
  7. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/quickstart.md +1 -1
  8. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/reviewable-actions.md +1 -1
  9. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/pyproject.toml +1 -1
  10. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/__init__.py +14 -1
  11. nutria_plugin-0.3.2/src/nutria_plugin/mcp_results.py +104 -0
  12. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/packaging.py +1 -0
  13. nutria_plugin-0.3.2/tests/test_mcp_results.py +70 -0
  14. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_packaging.py +11 -0
  15. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/uv.lock +1 -1
  16. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/.github/workflows/publish.yml +0 -0
  17. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/.gitignore +0 -0
  18. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/admin-extensions.md +0 -0
  19. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/admin-flows.md +0 -0
  20. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/cli.md +0 -0
  21. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/connection-types.md +0 -0
  22. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/security.md +0 -0
  23. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/docs/skill-format.md +0 -0
  24. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/README.md +0 -0
  25. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/hooks/hooks.json +0 -0
  26. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/plugin.json +0 -0
  27. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/examples/my-first-plugin/settings.schema.json +0 -0
  28. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/bundle.py +0 -0
  29. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/capabilities.py +0 -0
  30. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/cli.py +0 -0
  31. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/manifest.py +0 -0
  32. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/src/nutria_plugin/signing.py +0 -0
  33. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_bundle.py +0 -0
  34. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_capability_contracts.py +0 -0
  35. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_cli.py +0 -0
  36. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_manifest.py +0 -0
  37. {nutria_plugin-0.3.0 → nutria_plugin-0.3.2}/tests/test_reviewable_actions.py +0 -0
  38. {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.0
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.0` is intentionally breaking. It accepts only manifest schema
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.0
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.0` is intentionally breaking. It accepts only manifest schema
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.0
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,6 +1,6 @@
1
1
  # nutria-plugin SDK documentation
2
2
 
3
- Developer reference for SDK `0.3.0` and manifest schema `3.0`.
3
+ Developer reference for SDK `0.3.1` and manifest schema `3.0`.
4
4
 
5
5
  | Document | Purpose |
6
6
  |---|---|
@@ -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.0 accepts exactly schema
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
  # Python API reference
2
2
 
3
- SDK 0.3.0 exposes the strict schema 3.0 API.
3
+ SDK 0.3.1 exposes the strict schema 3.0 API.
4
4
 
5
5
  ```python
6
6
  from nutria_plugin import (
@@ -3,7 +3,7 @@
3
3
  ## Install and scaffold
4
4
 
5
5
  ```bash
6
- uv add nutria-plugin==0.3.0
6
+ uv add nutria-plugin==0.3.1
7
7
  nutria-plugin new inventory-lookup --name "Inventory Lookup"
8
8
  ```
9
9
 
@@ -1,6 +1,6 @@
1
1
  # Reviewable actions and external writes
2
2
 
3
- SDK 0.3.0 schema 3.0 defines one reviewable-action architecture:
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nutria-plugin"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "SDK for building, validating, signing, and packaging Nutria plugins"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -15,7 +15,7 @@ Signing:
15
15
  generate_keypair, sign_manifest, verify_manifest, SignatureStatus
16
16
  """
17
17
 
18
- __version__ = "0.3.0"
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
@@ -283,6 +283,7 @@ def pack_plugin(
283
283
 
284
284
  if output_path is None:
285
285
  output_path = Path(f"{manifest.id}-{manifest.version}.zip")
286
+ output_path.parent.mkdir(parents=True, exist_ok=True)
286
287
  output_path.write_bytes(data)
287
288
  return output_path
288
289
 
@@ -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")
@@ -262,7 +262,7 @@ wheels = [
262
262
 
263
263
  [[package]]
264
264
  name = "nutria-plugin"
265
- version = "0.3.0"
265
+ version = "0.3.2"
266
266
  source = { editable = "." }
267
267
  dependencies = [
268
268
  { name = "cryptography" },
File without changes
File without changes