weaver-kernel 0.4.0__tar.gz → 0.5.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.
Files changed (65) hide show
  1. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/.github/workflows/ci.yml +29 -0
  2. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/AGENTS.md +4 -1
  3. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/CHANGELOG.md +7 -0
  4. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/PKG-INFO +16 -2
  5. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/README.md +13 -0
  6. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/agent-context/invariants.md +4 -1
  7. weaver_kernel-0.5.0/docs/integrations.md +130 -0
  8. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/pyproject.toml +3 -2
  9. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/__init__.py +2 -0
  10. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/drivers/__init__.py +2 -1
  11. weaver_kernel-0.5.0/src/agent_kernel/drivers/mcp.py +236 -0
  12. weaver_kernel-0.5.0/src/agent_kernel/drivers/mcp_support.py +154 -0
  13. weaver_kernel-0.5.0/tests/test_mcp_driver.py +298 -0
  14. weaver_kernel-0.4.0/docs/integrations.md +0 -70
  15. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/.claude/CLAUDE.md +0 -0
  16. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/.github/copilot-instructions.md +0 -0
  17. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/.github/workflows/publish.yml +0 -0
  18. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/.gitignore +0 -0
  19. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/CONTRIBUTING.md +0 -0
  20. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/LICENSE +0 -0
  21. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/Makefile +0 -0
  22. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/RELEASE.md +0 -0
  23. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/agent-context/architecture.md +0 -0
  24. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/agent-context/lessons-learned.md +0 -0
  25. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/agent-context/review-checklist.md +0 -0
  26. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/agent-context/workflows.md +0 -0
  27. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/architecture.md +0 -0
  28. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/capabilities.md +0 -0
  29. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/context_firewall.md +0 -0
  30. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/docs/security.md +0 -0
  31. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/examples/basic_cli.py +0 -0
  32. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/examples/billing_demo.py +0 -0
  33. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/examples/http_driver_demo.py +0 -0
  34. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/drivers/base.py +0 -0
  35. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/drivers/http.py +0 -0
  36. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/drivers/memory.py +0 -0
  37. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/enums.py +0 -0
  38. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/errors.py +0 -0
  39. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/firewall/__init__.py +0 -0
  40. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/firewall/budgets.py +0 -0
  41. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/firewall/redaction.py +0 -0
  42. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/firewall/summarize.py +0 -0
  43. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/firewall/transform.py +0 -0
  44. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/handles.py +0 -0
  45. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/kernel.py +0 -0
  46. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/models.py +0 -0
  47. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/policy.py +0 -0
  48. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/py.typed +0 -0
  49. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/registry.py +0 -0
  50. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/router.py +0 -0
  51. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/tokens.py +0 -0
  52. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/src/agent_kernel/trace.py +0 -0
  53. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/conftest.py +0 -0
  54. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_drivers.py +0 -0
  55. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_firewall.py +0 -0
  56. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_handles.py +0 -0
  57. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_kernel.py +0 -0
  58. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_logging.py +0 -0
  59. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_models.py +0 -0
  60. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_policy.py +0 -0
  61. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_redaction.py +0 -0
  62. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_registry.py +0 -0
  63. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_router.py +0 -0
  64. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_tokens.py +0 -0
  65. {weaver_kernel-0.4.0 → weaver_kernel-0.5.0}/tests/test_trace.py +0 -0
@@ -45,3 +45,32 @@ jobs:
45
45
  python examples/basic_cli.py
46
46
  python examples/billing_demo.py
47
47
  python examples/http_driver_demo.py
48
+
49
+ conformance_stub:
50
+ name: "Weaver Spec Conformance Stub (v0.1.0)"
51
+ runs-on: ubuntu-latest
52
+ needs: test
53
+ permissions:
54
+ contents: read
55
+
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+
59
+ - name: Set up Python
60
+ uses: actions/setup-python@v5
61
+ with:
62
+ python-version: "3.12"
63
+
64
+ - name: Install dependencies
65
+ run: pip install -e ".[dev]"
66
+
67
+ # Placeholder: activate once dgenio/weaver-spec#4 ships the conformance runner.
68
+ # weaver-spec and weaver-contracts are published on PyPI.
69
+ # weaver_contracts.conformance does not yet exist (dgenio/weaver-spec#4).
70
+ # Replace this step with:
71
+ # pip install weaver-contracts # PyPI dist name uses a hyphen
72
+ # python -m weaver_contracts.conformance --target agent_kernel
73
+ - name: weaver-spec conformance suite (stub)
74
+ run: |
75
+ echo "weaver-contracts 0.2.0 is on PyPI; weaver_contracts.conformance runner not yet available (dgenio/weaver-spec#4)."
76
+ echo "Stub passes. Activate when dgenio/weaver-spec#4 ships."
@@ -25,9 +25,12 @@ agent-kernel is part of the **Weaver ecosystem**:
25
25
 
26
26
  This repo must conform to weaver-spec invariants. Key invariants (all equally critical):
27
27
  - **I-01**: Every tool output must pass through a context boundary before reaching the LLM.
28
- - **I-02**: Context boundaries must enforce budgets (size, depth, field count).
28
+ - **I-02**: Every execution must be authorized and auditable (preceded by a policy decision, followed by a trace event).
29
29
  - **I-06**: Tokens must bind principal + capability + constraints; no reuse across principals.
30
30
 
31
+ Note: Budget enforcement (size, depth, field count) is an agent-kernel implementation
32
+ constraint that satisfies I-01 — it is not a separate weaver-spec invariant number.
33
+
31
34
  Full spec: [dgenio/weaver-spec](https://github.com/dgenio/weaver-spec)
32
35
 
33
36
  ## Domain vocabulary
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-04-12
11
+
12
+ ### Added
13
+ - Built-in `MCPDriver` with stdio and Streamable HTTP transports, tool auto-discovery, normalized MCP result handling, and optional dependency guardrails.
14
+ - Declared weaver-spec v0.1.0 compatibility in README: invariants I-01 (firewall), I-02 (authorization + audit), and I-06 (scoped tokens) are satisfied.
15
+ - Added placeholder `conformance_stub` CI job that will activate once the weaver-spec conformance suite ships (dgenio/weaver-spec#4).
16
+
10
17
  ## [0.4.0] - 2026-03-14
11
18
 
12
19
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weaver-kernel
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Capability-based security kernel for AI agents operating in large tool ecosystems
5
5
  Project-URL: Homepage, https://github.com/dgenio/agent-kernel
6
6
  Project-URL: Repository, https://github.com/dgenio/agent-kernel
@@ -223,13 +223,14 @@ Requires-Python: >=3.10
223
223
  Requires-Dist: httpx>=0.27
224
224
  Provides-Extra: dev
225
225
  Requires-Dist: httpx>=0.27; extra == 'dev'
226
+ Requires-Dist: mcp>=1.6; extra == 'dev'
226
227
  Requires-Dist: mypy>=1.10; extra == 'dev'
227
228
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
228
229
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
229
230
  Requires-Dist: pytest>=8.0; extra == 'dev'
230
231
  Requires-Dist: ruff>=0.4; extra == 'dev'
231
232
  Provides-Extra: mcp
232
- Requires-Dist: mcp>=1.0; extra == 'mcp'
233
+ Requires-Dist: mcp>=1.6; extra == 'mcp'
233
234
  Provides-Extra: otel
234
235
  Requires-Dist: opentelemetry-api>=1.20; extra == 'otel'
235
236
  Description-Content-Type: text/markdown
@@ -346,6 +347,19 @@ asyncio.run(main())
346
347
 
347
348
  `agent-kernel` sits **above** `contextweaver` (context compilation) and **above** raw tool execution. It provides the authorization, execution, and audit layer.
348
349
 
350
+ ## Weaver Spec Compatibility: v0.1.0
351
+
352
+ agent-kernel is a compliant implementation of [weaver-spec v0.1.0](https://github.com/dgenio/weaver-spec).
353
+ The following invariants are satisfied:
354
+
355
+ | Invariant | Description | How agent-kernel satisfies it |
356
+ |-----------|-------------|-------------------------------|
357
+ | **I-01** | LLM never sees raw tool output by default | `Context Firewall` always transforms `RawResult → Frame`; raw driver output is not returned by default, and non-admin principals cannot obtain `raw` response mode |
358
+ | **I-02** | Every execution is authorized and auditable | `PolicyEngine` authorizes at grant time; a valid `CapabilityToken` (HMAC-verified on every `invoke()`) carries the authorization decision; `TraceStore` records every `ActionTrace` |
359
+ | **I-06** | CapabilityTokens are scoped | Tokens bind `principal_id + capability_id + constraints` with an explicit TTL; `revoke(token_id)` / `revoke_all(principal_id)` are supported |
360
+
361
+ See [docs/agent-context/invariants.md](docs/agent-context/invariants.md) for the full internal invariant list and [weaver-spec INVARIANTS.md](https://github.com/dgenio/weaver-spec/blob/main/docs/INVARIANTS.md) for the specification.
362
+
349
363
  ## Security disclaimers
350
364
 
351
365
  > **v0.1 is not production-hardened for real authentication.**
@@ -110,6 +110,19 @@ asyncio.run(main())
110
110
 
111
111
  `agent-kernel` sits **above** `contextweaver` (context compilation) and **above** raw tool execution. It provides the authorization, execution, and audit layer.
112
112
 
113
+ ## Weaver Spec Compatibility: v0.1.0
114
+
115
+ agent-kernel is a compliant implementation of [weaver-spec v0.1.0](https://github.com/dgenio/weaver-spec).
116
+ The following invariants are satisfied:
117
+
118
+ | Invariant | Description | How agent-kernel satisfies it |
119
+ |-----------|-------------|-------------------------------|
120
+ | **I-01** | LLM never sees raw tool output by default | `Context Firewall` always transforms `RawResult → Frame`; raw driver output is not returned by default, and non-admin principals cannot obtain `raw` response mode |
121
+ | **I-02** | Every execution is authorized and auditable | `PolicyEngine` authorizes at grant time; a valid `CapabilityToken` (HMAC-verified on every `invoke()`) carries the authorization decision; `TraceStore` records every `ActionTrace` |
122
+ | **I-06** | CapabilityTokens are scoped | Tokens bind `principal_id + capability_id + constraints` with an explicit TTL; `revoke(token_id)` / `revoke_all(principal_id)` are supported |
123
+
124
+ See [docs/agent-context/invariants.md](docs/agent-context/invariants.md) for the full internal invariant list and [weaver-spec INVARIANTS.md](https://github.com/dgenio/weaver-spec/blob/main/docs/INVARIANTS.md) for the specification.
125
+
113
126
  ## Security disclaimers
114
127
 
115
128
  > **v0.1 is not production-hardened for real authentication.**
@@ -11,9 +11,12 @@ All three are equally critical — there is no priority ordering.
11
11
  | Invariant | Requirement | Where enforced |
12
12
  |-----------|-------------|----------------|
13
13
  | **I-01** | Every tool output must pass through a context boundary before reaching the LLM | `Firewall.transform()` in `firewall/transform.py` |
14
- | **I-02** | Context boundaries must enforce budgets (size, depth, field count) | `Budgets` in `firewall/budgets.py` |
14
+ | **I-02** | Every execution must be authorized and auditable (CapabilityToken validated before execution; TraceEvent recorded after) | `HMACTokenProvider.verify()` + `TraceStore.record()` in `kernel.py`; `PolicyEngine.evaluate()` at grant time in `grant_capability()` |
15
15
  | **I-06** | Tokens must bind principal + capability + constraints; no reuse across principals | `HMACTokenProvider.verify()` in `tokens.py` |
16
16
 
17
+ > **Budget enforcement** (size, depth, field count via `Budgets` in `firewall/budgets.py`) is an
18
+ > implementation constraint that strengthens I-01. It has no separate invariant number in weaver-spec.
19
+
17
20
  ## Forbidden shortcuts — "never do" list
18
21
 
19
22
  These constraints are non-negotiable. Violating any one silently degrades security.
@@ -0,0 +1,130 @@
1
+ # Integrations
2
+
3
+ ## MCP (Model Context Protocol)
4
+
5
+ The built-in `MCPDriver` supports both local stdio servers and remote Streamable HTTP servers.
6
+
7
+ Install the optional dependency first:
8
+
9
+ ```bash
10
+ pip install "weaver-kernel[mcp]"
11
+ ```
12
+
13
+ ### Stdio transport
14
+
15
+ ```python
16
+ import asyncio
17
+
18
+ from agent_kernel import CapabilityRegistry, Kernel, StaticRouter
19
+ from agent_kernel.drivers.mcp import MCPDriver
20
+
21
+
22
+ async def main() -> None:
23
+ registry = CapabilityRegistry()
24
+ router = StaticRouter(fallback=[])
25
+ kernel = Kernel(registry=registry, router=router)
26
+
27
+ # Connect to a local MCP server process.
28
+ driver = MCPDriver.from_stdio(
29
+ command="python",
30
+ args=["-m", "my_mcp_server"],
31
+ server_name="local-tools",
32
+ )
33
+ kernel.register_driver(driver)
34
+
35
+ # Discover tools and register them as capabilities.
36
+ capabilities = await driver.discover(namespace="local")
37
+ registry.register_many(capabilities)
38
+
39
+ # Route each discovered capability to this MCP driver.
40
+ for capability in capabilities:
41
+ router.add_route(capability.capability_id, [driver.driver_id])
42
+
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ ### Streamable HTTP transport
48
+
49
+ ```python
50
+ import asyncio
51
+
52
+ from agent_kernel import CapabilityRegistry, Kernel, StaticRouter
53
+ from agent_kernel.drivers.mcp import MCPDriver
54
+
55
+
56
+ async def main() -> None:
57
+ registry = CapabilityRegistry()
58
+ router = StaticRouter(fallback=[])
59
+ kernel = Kernel(registry=registry, router=router)
60
+
61
+ # Connect to a remote Streamable HTTP MCP server.
62
+ # Note: max_retries > 0 creates at-least-once delivery semantics for
63
+ # tools/call — if a connection drops after the server processes the
64
+ # request but before the response arrives, the call will be repeated.
65
+ # Ensure target tools are idempotent, or set max_retries=0 for
66
+ # WRITE/DESTRUCTIVE capabilities.
67
+ driver = MCPDriver.from_http(
68
+ url="https://example.com/mcp",
69
+ server_name="remote-tools",
70
+ max_retries=1,
71
+ )
72
+ kernel.register_driver(driver)
73
+
74
+ # Discover tools and register them as capabilities.
75
+ capabilities = await driver.discover(namespace="remote")
76
+ registry.register_many(capabilities)
77
+
78
+ # Route each discovered capability to this MCP driver.
79
+ for capability in capabilities:
80
+ router.add_route(capability.capability_id, [driver.driver_id])
81
+
82
+
83
+ asyncio.run(main())
84
+ ```
85
+
86
+ ### Notes
87
+
88
+ - `discover()` converts `tools/list` results into `Capability` objects.
89
+ - `execute()` calls `tools/call` and normalizes MCP content blocks for the firewall.
90
+ - MCP `isError` responses raise `DriverError` with the server-provided detail.
91
+ - If `mcp` is not installed, factory methods raise a helpful `ImportError`.
92
+
93
+ ## HTTPDriver
94
+
95
+ The built-in `HTTPDriver` supports GET, POST, PUT, DELETE:
96
+
97
+ ```python
98
+ from agent_kernel.drivers.http import HTTPDriver, HTTPEndpoint
99
+
100
+ driver = HTTPDriver(driver_id="my_api")
101
+ driver.register_endpoint("users.list", HTTPEndpoint(
102
+ url="https://api.example.com/users",
103
+ method="GET",
104
+ headers={"Authorization": "Bearer ..."},
105
+ ))
106
+ kernel.register_driver(driver)
107
+ ```
108
+
109
+ ## Custom drivers
110
+
111
+ Any object implementing the `Driver` protocol can be registered:
112
+
113
+ ```python
114
+ class Driver(Protocol):
115
+ @property
116
+ def driver_id(self) -> str: ...
117
+ async def execute(self, ctx: ExecutionContext) -> RawResult: ...
118
+ ```
119
+
120
+ ## Capability mapping
121
+
122
+ When mapping MCP tools to capabilities, prefer task-shaped names:
123
+
124
+ | MCP tool | Capability ID | Safety class |
125
+ |----------|--------------|--------------|
126
+ | `list_files` | `fs.list_files` | READ |
127
+ | `read_file` | `fs.read_file` | READ |
128
+ | `write_file` | `fs.write_file` | WRITE |
129
+ | `delete_file` | `fs.delete_file` | DESTRUCTIVE |
130
+ | `execute_code` | `sandbox.run_code` | DESTRUCTIVE |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "weaver-kernel"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "Capability-based security kernel for AI agents operating in large tool ecosystems"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -38,8 +38,9 @@ dev = [
38
38
  "ruff>=0.4",
39
39
  "mypy>=1.10",
40
40
  "httpx>=0.27",
41
+ "mcp>=1.6",
41
42
  ]
42
- mcp = ["mcp>=1.0"]
43
+ mcp = ["mcp>=1.6"]
43
44
  otel = ["opentelemetry-api>=1.20"]
44
45
 
45
46
  [tool.hatch.build.targets.wheel]
@@ -37,6 +37,7 @@ Errors::
37
37
 
38
38
  from .drivers.base import Driver, ExecutionContext
39
39
  from .drivers.http import HTTPDriver
40
+ from .drivers.mcp import MCPDriver
40
41
  from .drivers.memory import InMemoryDriver, make_billing_driver
41
42
  from .enums import SafetyClass, SensitivityTag
42
43
  from .errors import (
@@ -129,6 +130,7 @@ __all__ = [
129
130
  "ExecutionContext",
130
131
  "InMemoryDriver",
131
132
  "HTTPDriver",
133
+ "MCPDriver",
132
134
  "make_billing_driver",
133
135
  # firewall
134
136
  "Firewall",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from .base import Driver, ExecutionContext
4
4
  from .http import HTTPDriver
5
+ from .mcp import MCPDriver
5
6
  from .memory import InMemoryDriver
6
7
 
7
- __all__ = ["Driver", "ExecutionContext", "HTTPDriver", "InMemoryDriver"]
8
+ __all__ = ["Driver", "ExecutionContext", "HTTPDriver", "MCPDriver", "InMemoryDriver"]
@@ -0,0 +1,236 @@
1
+ """MCP driver: execute capabilities against Model Context Protocol servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any
7
+
8
+ from ..enums import SafetyClass
9
+ from ..errors import DriverError
10
+ from ..models import Capability, ImplementationRef, RawResult
11
+ from .base import ExecutionContext
12
+ from .mcp_support import (
13
+ SessionFactory,
14
+ ToolSpec,
15
+ build_http_session_factory,
16
+ build_stdio_session_factory,
17
+ call_tool,
18
+ extract_tool_specs,
19
+ normalize_call_result,
20
+ )
21
+
22
+ # Lazy import of McpError — only available when the mcp optional dep is installed.
23
+ # If mcp is absent, factory methods raise ImportError before any session is created,
24
+ # so _McpError will never be None on a live driver instance.
25
+ try:
26
+ from mcp.shared.exceptions import McpError as _McpError
27
+ except ImportError: # pragma: no cover
28
+ _McpError = None # type: ignore[assignment,misc]
29
+
30
+
31
+ def _infer_safety_class(spec: ToolSpec) -> SafetyClass:
32
+ """Infer a SafetyClass from MCP ToolAnnotations hints.
33
+
34
+ Uses a conservative default of READ when annotations are absent.
35
+ The caller's safety_class_map takes precedence over the inferred value.
36
+ """
37
+ if spec.destructive_hint:
38
+ return SafetyClass.DESTRUCTIVE
39
+ if spec.read_only_hint:
40
+ return SafetyClass.READ
41
+ return SafetyClass.READ
42
+
43
+
44
+ class MCPDriver:
45
+ """A driver that invokes capabilities via MCP tools/call."""
46
+
47
+ def __init__(
48
+ self,
49
+ *,
50
+ driver_id: str,
51
+ session_factory: SessionFactory,
52
+ server_name: str,
53
+ transport: str,
54
+ max_http_retries: int = 1,
55
+ ) -> None:
56
+ self._driver_id = driver_id
57
+ self._session_factory = session_factory
58
+ self._server_name = server_name
59
+ self._transport = transport
60
+ self._max_http_retries = max(max_http_retries, 0)
61
+
62
+ @property
63
+ def driver_id(self) -> str:
64
+ """Unique identifier for this driver instance."""
65
+ return self._driver_id
66
+
67
+ @classmethod
68
+ def from_stdio(
69
+ cls,
70
+ command: str,
71
+ args: list[str] | None = None,
72
+ *,
73
+ server_name: str = "stdio",
74
+ ) -> MCPDriver:
75
+ """Create an MCP driver using stdio transport.
76
+
77
+ Raises:
78
+ ImportError: If the optional ``mcp`` dependency is not installed.
79
+ """
80
+ session_factory = build_stdio_session_factory(command=command, args=args or [])
81
+ return cls(
82
+ driver_id=f"mcp:{server_name}",
83
+ session_factory=session_factory,
84
+ server_name=server_name,
85
+ transport="stdio",
86
+ max_http_retries=0,
87
+ )
88
+
89
+ @classmethod
90
+ def from_http(
91
+ cls,
92
+ url: str,
93
+ *,
94
+ server_name: str = "http",
95
+ max_retries: int = 1,
96
+ ) -> MCPDriver:
97
+ """Create an MCP driver using Streamable HTTP transport.
98
+
99
+ Raises:
100
+ ImportError: If the optional ``mcp`` dependency is not installed.
101
+ """
102
+ session_factory = build_http_session_factory(url=url)
103
+ return cls(
104
+ driver_id=f"mcp:{server_name}",
105
+ session_factory=session_factory,
106
+ server_name=server_name,
107
+ transport="http",
108
+ max_http_retries=max_retries,
109
+ )
110
+
111
+ async def discover(
112
+ self,
113
+ *,
114
+ namespace: str | None = None,
115
+ safety_class_map: dict[str, SafetyClass] | None = None,
116
+ ) -> list[Capability]:
117
+ """Discover MCP tools across all pages and convert them to capabilities."""
118
+ tools = await self._run_with_retry(
119
+ operation_name="tools/list",
120
+ action=self._fetch_all_tools,
121
+ )
122
+
123
+ capabilities: list[Capability] = []
124
+ for spec in extract_tool_specs(tools):
125
+ capability_id = f"{namespace}.{spec.name}" if namespace else spec.name
126
+ inferred = _infer_safety_class(spec)
127
+ safety_class = (
128
+ safety_class_map.get(spec.name, inferred)
129
+ if safety_class_map is not None
130
+ else inferred
131
+ )
132
+ capabilities.append(
133
+ Capability(
134
+ capability_id=capability_id,
135
+ name=spec.name,
136
+ description=spec.description,
137
+ safety_class=safety_class,
138
+ tags=["mcp", self._server_name],
139
+ impl=ImplementationRef(
140
+ driver_id=self._driver_id,
141
+ operation=spec.name,
142
+ ),
143
+ )
144
+ )
145
+ return capabilities
146
+
147
+ async def _fetch_all_tools(self, session: Any) -> list[Any]:
148
+ """Paginate tools/list to exhaustion and return a flat list of Tool objects."""
149
+ all_tools: list[Any] = []
150
+ cursor: str | None = None
151
+ while True:
152
+ result = await session.list_tools(cursor=cursor)
153
+ all_tools.extend(getattr(result, "tools", []) or [])
154
+ cursor = getattr(result, "nextCursor", None)
155
+ if not cursor:
156
+ break
157
+ return all_tools
158
+
159
+ async def execute(self, ctx: ExecutionContext) -> RawResult:
160
+ """Execute an MCP tool call for the given capability context."""
161
+ operation = str(ctx.args.get("operation", ctx.capability_id))
162
+ params = {k: v for k, v in ctx.args.items() if k != "operation"}
163
+
164
+ # Apply policy constraints as default arguments, without overriding explicit args.
165
+ # read_timeout_seconds is an SDK control parameter — applied to the session call
166
+ # directly rather than forwarded to the tool as an argument.
167
+ read_timeout_seconds_raw = ctx.constraints.get("read_timeout_seconds")
168
+ for key, value in ctx.constraints.items():
169
+ if key != "read_timeout_seconds":
170
+ params.setdefault(key, value)
171
+
172
+ read_timeout_seconds: float | None = (
173
+ float(read_timeout_seconds_raw) if read_timeout_seconds_raw is not None else None
174
+ )
175
+
176
+ result = await self._run_with_retry(
177
+ operation_name=f"tools/call:{operation}",
178
+ action=lambda session: call_tool(
179
+ session,
180
+ operation=operation,
181
+ params=params,
182
+ read_timeout_seconds=read_timeout_seconds,
183
+ ),
184
+ )
185
+
186
+ data = normalize_call_result(
187
+ result,
188
+ operation=operation,
189
+ driver_id=self._driver_id,
190
+ )
191
+ return RawResult(
192
+ capability_id=ctx.capability_id,
193
+ data=data,
194
+ metadata={
195
+ "driver_id": self._driver_id,
196
+ "transport": self._transport,
197
+ "operation": operation,
198
+ },
199
+ )
200
+
201
+ async def _run_with_retry(
202
+ self,
203
+ *,
204
+ operation_name: str,
205
+ action: Callable[[Any], Awaitable[Any]],
206
+ ) -> Any:
207
+ attempts = 1 + self._max_http_retries if self._transport == "http" else 1
208
+ last_exc: Exception | None = None
209
+
210
+ for _attempt in range(attempts):
211
+ try:
212
+ async with self._session_factory() as session:
213
+ return await action(session)
214
+ except DriverError:
215
+ raise
216
+ except Exception as exc:
217
+ # McpError is a protocol-level rejection (tool not found, auth
218
+ # failure, invalid params) — the server processed and rejected the
219
+ # request. It is not retryable; surface it immediately as DriverError.
220
+ if _McpError is not None and isinstance(exc, _McpError):
221
+ raise DriverError(
222
+ f"MCPDriver '{self._driver_id}' received a protocol error "
223
+ f"during {operation_name}: {exc}"
224
+ ) from exc
225
+ # All other exceptions are session/transport failures (connection
226
+ # refused, EOF, timeout) and are retryable for HTTP transport.
227
+ # Note: HTTP retries create at-least-once delivery semantics for
228
+ # tools/call. Callers using WRITE/DESTRUCTIVE capabilities over HTTP
229
+ # should ensure the target tool is idempotent, or set max_retries=0.
230
+ last_exc = exc
231
+
232
+ reason = str(last_exc) if last_exc is not None else "unknown transport failure"
233
+ raise DriverError(
234
+ f"MCPDriver '{self._driver_id}' failed during {operation_name} over "
235
+ f"{self._transport}: {reason}"
236
+ ) from last_exc
@@ -0,0 +1,154 @@
1
+ """Internal helpers for MCP transport wiring and result normalization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from collections.abc import AsyncIterator, Callable
7
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
8
+ from dataclasses import dataclass
9
+ from datetime import timedelta
10
+ from typing import Any
11
+
12
+ from ..errors import DriverError
13
+
14
+ SessionFactory = Callable[[], AbstractAsyncContextManager[Any]]
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class ToolSpec:
19
+ """Normalized MCP tool metadata for capability conversion."""
20
+
21
+ name: str
22
+ description: str
23
+ read_only_hint: bool = False
24
+ destructive_hint: bool = False
25
+ idempotent_hint: bool = False
26
+ output_schema: dict[str, Any] | None = None
27
+
28
+
29
+ async def call_tool(
30
+ session: Any,
31
+ *,
32
+ operation: str,
33
+ params: dict[str, Any],
34
+ read_timeout_seconds: float | None = None,
35
+ ) -> Any:
36
+ """Call an MCP tool via tools/call."""
37
+ timeout = timedelta(seconds=read_timeout_seconds) if read_timeout_seconds is not None else None
38
+ return await session.call_tool(operation, arguments=params, read_timeout_seconds=timeout)
39
+
40
+
41
+ def extract_tool_specs(tools: list[Any]) -> list[ToolSpec]:
42
+ """Extract tool metadata from a flat list of MCP Tool objects."""
43
+ if not isinstance(tools, list):
44
+ return []
45
+ specs: list[ToolSpec] = []
46
+ for tool in tools:
47
+ name = getattr(tool, "name", None)
48
+ if not isinstance(name, str) or not name:
49
+ continue
50
+ ann = getattr(tool, "annotations", None)
51
+ specs.append(
52
+ ToolSpec(
53
+ name=name,
54
+ description=str(getattr(tool, "description", "") or ""),
55
+ read_only_hint=bool(getattr(ann, "readOnlyHint", False)),
56
+ destructive_hint=bool(getattr(ann, "destructiveHint", False)),
57
+ idempotent_hint=bool(getattr(ann, "idempotentHint", False)),
58
+ output_schema=getattr(tool, "outputSchema", None),
59
+ )
60
+ )
61
+ return specs
62
+
63
+
64
+ def normalize_call_result(result: Any, *, operation: str, driver_id: str) -> Any:
65
+ """Normalize an MCP CallToolResult into plain Python data."""
66
+ is_error = bool(getattr(result, "isError", False))
67
+ content = [_normalize_content_item(c) for c in (getattr(result, "content", None) or [])]
68
+
69
+ if is_error:
70
+ detail = next(
71
+ (b["text"] for b in content if b.get("type") == "text" and b.get("text", "").strip()),
72
+ "MCP server returned isError=true",
73
+ )
74
+ raise DriverError(
75
+ f"MCPDriver '{driver_id}' tool '{operation}' returned an error: {detail}"
76
+ )
77
+
78
+ structured: dict[str, Any] | None = getattr(result, "structuredContent", None)
79
+ if structured is not None and content:
80
+ return {"structured_content": structured, "content": content}
81
+ if structured is not None:
82
+ return structured
83
+ return content
84
+
85
+
86
+ def import_optional(module_name: str) -> Any:
87
+ """Import optional MCP SDK module with a consistent guidance message."""
88
+ try:
89
+ return importlib.import_module(module_name)
90
+ except ModuleNotFoundError as exc:
91
+ raise ImportError(
92
+ "MCP support requires the optional dependency 'mcp>=1.0'. "
93
+ "Install it with: pip install 'weaver-kernel[mcp]'"
94
+ ) from exc
95
+
96
+
97
+ def build_stdio_session_factory(*, command: str, args: list[str]) -> SessionFactory:
98
+ """Build a stdio-backed MCP session factory."""
99
+ stdio_mod = import_optional("mcp.client.stdio")
100
+ session_mod = import_optional("mcp.client.session")
101
+
102
+ stdio_client = stdio_mod.stdio_client
103
+ server_params_cls = stdio_mod.StdioServerParameters
104
+ session_cls = session_mod.ClientSession
105
+
106
+ @asynccontextmanager
107
+ async def factory() -> AsyncIterator[Any]:
108
+ params = server_params_cls(command=command, args=args)
109
+ async with stdio_client(params) as streams:
110
+ read_stream, write_stream = streams
111
+ async with session_cls(read_stream, write_stream) as session:
112
+ await session.initialize()
113
+ yield session
114
+
115
+ return factory
116
+
117
+
118
+ def build_http_session_factory(*, url: str) -> SessionFactory:
119
+ """Build a Streamable HTTP-backed MCP session factory."""
120
+ streamable_mod = import_optional("mcp.client.streamable_http")
121
+ session_mod = import_optional("mcp.client.session")
122
+
123
+ streamable_http_client = streamable_mod.streamable_http_client
124
+ session_cls = session_mod.ClientSession
125
+
126
+ @asynccontextmanager
127
+ async def factory() -> AsyncIterator[Any]:
128
+ async with streamable_http_client(url) as streams:
129
+ read_stream, write_stream = streams
130
+ async with session_cls(read_stream, write_stream) as session:
131
+ await session.initialize()
132
+ yield session
133
+
134
+ return factory
135
+
136
+
137
+ def _normalize_content_item(item: Any) -> dict[str, Any]:
138
+ item_type = str(getattr(item, "type", "")).lower()
139
+ if item_type == "text":
140
+ return {"type": "text", "text": str(getattr(item, "text", ""))}
141
+ if item_type == "image":
142
+ return {
143
+ "type": "image",
144
+ "data": getattr(item, "data", None),
145
+ "mime_type": getattr(item, "mimeType", None),
146
+ }
147
+ if item_type in {"resource", "resourcelink"}:
148
+ resource = getattr(item, "resource", item)
149
+ return {
150
+ "type": "resource",
151
+ "resource": resource.model_dump() if hasattr(resource, "model_dump") else resource,
152
+ }
153
+ # AudioContent or any future type - fall back to model_dump
154
+ return item.model_dump() if hasattr(item, "model_dump") else {"type": "value", "value": item}
@@ -0,0 +1,298 @@
1
+ """Tests for the built-in MCPDriver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any
8
+ from unittest.mock import patch
9
+
10
+ import pytest
11
+ from mcp.types import CallToolResult, ListToolsResult, TextContent, Tool
12
+
13
+ from agent_kernel import (
14
+ CapabilityRegistry,
15
+ CapabilityRequest,
16
+ DriverError,
17
+ Kernel,
18
+ MCPDriver,
19
+ Principal,
20
+ SafetyClass,
21
+ StaticRouter,
22
+ )
23
+
24
+
25
+ class _FakeSession:
26
+ """Small async session stub for MCPDriver tests."""
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ tools: list[Tool] | None = None,
32
+ call_result: CallToolResult | None = None,
33
+ call_error: Exception | None = None,
34
+ ) -> None:
35
+ self._tools = tools or []
36
+ self._call_result = call_result or CallToolResult(content=[])
37
+ self._call_error = call_error
38
+ self.calls: list[tuple[str, dict[str, Any]]] = []
39
+
40
+ async def list_tools(self, cursor: str | None = None) -> ListToolsResult:
41
+ return ListToolsResult(tools=self._tools)
42
+
43
+ async def call_tool(
44
+ self,
45
+ operation: str,
46
+ arguments: dict[str, Any],
47
+ read_timeout_seconds: Any = None,
48
+ ) -> CallToolResult:
49
+ self.calls.append((operation, arguments))
50
+ if self._call_error is not None:
51
+ raise self._call_error
52
+ return self._call_result
53
+
54
+
55
+ def _reusable_factory(session: _FakeSession) -> Any:
56
+ @asynccontextmanager
57
+ async def _factory() -> AsyncIterator[_FakeSession]:
58
+ yield session
59
+
60
+ return _factory
61
+
62
+
63
+ def _sequence_factory(sessions: list[_FakeSession]) -> Any:
64
+ @asynccontextmanager
65
+ async def _factory() -> AsyncIterator[_FakeSession]:
66
+ if not sessions:
67
+ raise RuntimeError("no fake sessions left")
68
+ yield sessions.pop(0)
69
+
70
+ return _factory
71
+
72
+
73
+ def test_from_stdio_missing_dependency_raises_helpful_import_error() -> None:
74
+ with patch("agent_kernel.drivers.mcp_support.importlib.import_module") as import_module:
75
+ import_module.side_effect = ModuleNotFoundError("No module named 'mcp'")
76
+ with pytest.raises(ImportError, match=r"weaver-kernel\[mcp\]"):
77
+ MCPDriver.from_stdio("python", ["server.py"])
78
+
79
+
80
+ def test_from_http_missing_dependency_raises_helpful_import_error() -> None:
81
+ with patch("agent_kernel.drivers.mcp_support.importlib.import_module") as import_module:
82
+ import_module.side_effect = ModuleNotFoundError("No module named 'mcp'")
83
+ with pytest.raises(ImportError, match=r"weaver-kernel\[mcp\]"):
84
+ MCPDriver.from_http("http://localhost:8080/mcp")
85
+
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_discover_converts_tools_to_capabilities() -> None:
89
+ session = _FakeSession(
90
+ tools=[
91
+ Tool(name="list_files", description="List files", inputSchema={}),
92
+ Tool(name="write_file", description="Write file", inputSchema={}),
93
+ ]
94
+ )
95
+ driver = MCPDriver(
96
+ driver_id="mcp:local",
97
+ session_factory=_reusable_factory(session),
98
+ server_name="local",
99
+ transport="stdio",
100
+ )
101
+
102
+ capabilities = await driver.discover(
103
+ namespace="fs", safety_class_map={"write_file": SafetyClass.WRITE}
104
+ )
105
+
106
+ assert [cap.capability_id for cap in capabilities] == [
107
+ "fs.list_files",
108
+ "fs.write_file",
109
+ ]
110
+ assert capabilities[0].safety_class == SafetyClass.READ
111
+ assert capabilities[1].safety_class == SafetyClass.WRITE
112
+ assert capabilities[0].impl is not None
113
+ assert capabilities[0].impl.driver_id == "mcp:local"
114
+ assert capabilities[0].impl.operation == "list_files"
115
+
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_execute_calls_tool_and_applies_constraints_defaults() -> None:
119
+ session = _FakeSession(
120
+ call_result=CallToolResult(content=[TextContent(type="text", text="ok")])
121
+ )
122
+ driver = MCPDriver(
123
+ driver_id="mcp:local",
124
+ session_factory=_reusable_factory(session),
125
+ server_name="local",
126
+ transport="stdio",
127
+ )
128
+
129
+ from agent_kernel.drivers.base import ExecutionContext
130
+
131
+ ctx = ExecutionContext(
132
+ capability_id="fs.list_files",
133
+ principal_id="u1",
134
+ args={"operation": "list_files", "path": "/tmp", "max_rows": 5},
135
+ constraints={"max_rows": 2, "allowed_fields": ["name"]},
136
+ )
137
+
138
+ result = await driver.execute(ctx)
139
+
140
+ assert result.capability_id == "fs.list_files"
141
+ assert result.data == [{"type": "text", "text": "ok"}]
142
+ assert session.calls[0][0] == "list_files"
143
+ # Explicit args are preserved; missing constraints are merged in.
144
+ assert session.calls[0][1]["max_rows"] == 5
145
+ assert session.calls[0][1]["allowed_fields"] == ["name"]
146
+
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_execute_prefers_structured_content_when_available() -> None:
150
+ session = _FakeSession(
151
+ call_result=CallToolResult(
152
+ structuredContent={"total": 3},
153
+ content=[TextContent(type="text", text="computed")],
154
+ )
155
+ )
156
+ driver = MCPDriver(
157
+ driver_id="mcp:local",
158
+ session_factory=_reusable_factory(session),
159
+ server_name="local",
160
+ transport="stdio",
161
+ )
162
+
163
+ from agent_kernel.drivers.base import ExecutionContext
164
+
165
+ ctx = ExecutionContext(capability_id="math.sum", principal_id="u1")
166
+ result = await driver.execute(ctx)
167
+
168
+ assert result.data == {
169
+ "structured_content": {"total": 3},
170
+ "content": [{"type": "text", "text": "computed"}],
171
+ }
172
+
173
+
174
+ @pytest.mark.asyncio
175
+ async def test_execute_raises_driver_error_on_mcp_is_error() -> None:
176
+ session = _FakeSession(
177
+ call_result=CallToolResult(
178
+ isError=True,
179
+ content=[TextContent(type="text", text="permission denied")],
180
+ )
181
+ )
182
+ driver = MCPDriver(
183
+ driver_id="mcp:local",
184
+ session_factory=_reusable_factory(session),
185
+ server_name="local",
186
+ transport="stdio",
187
+ )
188
+
189
+ from agent_kernel.drivers.base import ExecutionContext
190
+
191
+ ctx = ExecutionContext(capability_id="secrets.read", principal_id="u1")
192
+ with pytest.raises(DriverError, match="permission denied"):
193
+ await driver.execute(ctx)
194
+
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_http_transport_retries_after_connection_drop() -> None:
198
+ first = _FakeSession(call_error=RuntimeError("connection dropped"))
199
+ second = _FakeSession(
200
+ call_result=CallToolResult(content=[TextContent(type="text", text="ok")])
201
+ )
202
+
203
+ driver = MCPDriver(
204
+ driver_id="mcp:http",
205
+ session_factory=_sequence_factory([first, second]),
206
+ server_name="remote",
207
+ transport="http",
208
+ max_http_retries=1,
209
+ )
210
+
211
+ from agent_kernel.drivers.base import ExecutionContext
212
+
213
+ ctx = ExecutionContext(capability_id="echo", principal_id="u1")
214
+ result = await driver.execute(ctx)
215
+
216
+ assert result.data == [{"type": "text", "text": "ok"}]
217
+ assert len(first.calls) == 1
218
+ assert len(second.calls) == 1
219
+
220
+
221
+ @pytest.mark.asyncio
222
+ async def test_kernel_pipeline_with_discover_register_grant_invoke() -> None:
223
+ session = _FakeSession(
224
+ tools=[Tool(name="math.sum", description="Sum two values", inputSchema={})],
225
+ call_result=CallToolResult(structuredContent={"total": 3}, content=[]),
226
+ )
227
+ driver = MCPDriver(
228
+ driver_id="mcp:demo",
229
+ session_factory=_reusable_factory(session),
230
+ server_name="demo",
231
+ transport="stdio",
232
+ )
233
+
234
+ capabilities = await driver.discover()
235
+ registry = CapabilityRegistry()
236
+ registry.register_many(capabilities)
237
+
238
+ router = StaticRouter(routes={"math.sum": ["mcp:demo"]}, fallback=[])
239
+ kernel = Kernel(registry=registry, router=router)
240
+ kernel.register_driver(driver)
241
+
242
+ principal = Principal(principal_id="u1", roles=["reader"])
243
+ request = CapabilityRequest(capability_id="math.sum", goal="sum numbers")
244
+
245
+ token = kernel.get_token(request, principal, justification="")
246
+ frame = await kernel.invoke(
247
+ token,
248
+ principal=principal,
249
+ args={"operation": "math.sum", "a": 1, "b": 2},
250
+ )
251
+
252
+ assert frame.response_mode == "summary"
253
+ assert any("total" in fact.lower() for fact in frame.facts)
254
+
255
+
256
+ @pytest.mark.asyncio
257
+ async def test_real_fastmcp_in_process_discover_and_execute() -> None:
258
+ """Full discover→execute cycle driven by a real FastMCP server in-process."""
259
+ from mcp.client.session import ClientSession
260
+ from mcp.server.fastmcp import FastMCP
261
+ from mcp.shared.memory import create_connected_server_and_client_session
262
+
263
+ mcp_srv = FastMCP("math")
264
+
265
+ @mcp_srv.tool()
266
+ def add(a: int, b: int) -> int:
267
+ """Add two integers."""
268
+ return a + b
269
+
270
+ @asynccontextmanager
271
+ async def in_memory_factory() -> AsyncIterator[ClientSession]:
272
+ async with create_connected_server_and_client_session(mcp_srv) as session:
273
+ yield session
274
+
275
+ driver = MCPDriver(
276
+ driver_id="mcp:math",
277
+ session_factory=in_memory_factory,
278
+ server_name="math",
279
+ transport="stdio",
280
+ )
281
+
282
+ capabilities = await driver.discover(namespace="math")
283
+ assert any(cap.capability_id == "math.add" for cap in capabilities)
284
+ add_cap = next(c for c in capabilities if c.capability_id == "math.add")
285
+ assert add_cap.impl is not None
286
+ assert add_cap.impl.operation == "add"
287
+
288
+ from agent_kernel.drivers.base import ExecutionContext
289
+
290
+ ctx = ExecutionContext(
291
+ capability_id="math.add",
292
+ principal_id="u1",
293
+ args={"operation": "add", "a": 3, "b": 4},
294
+ )
295
+ result = await driver.execute(ctx)
296
+ assert isinstance(result.data, dict)
297
+ assert result.data["structured_content"]["result"] == 7
298
+ assert result.data["content"][0]["text"] == "7"
@@ -1,70 +0,0 @@
1
- # Integrations
2
-
3
- ## MCP (Model Context Protocol)
4
-
5
- To integrate with an MCP server, implement a custom driver that wraps the MCP client:
6
-
7
- ```python
8
- from agent_kernel.drivers.base import Driver, ExecutionContext
9
- from agent_kernel.models import RawResult
10
-
11
- class MCPDriver:
12
- def __init__(self, mcp_client, driver_id: str = "mcp"):
13
- self._client = mcp_client
14
- self._driver_id = driver_id
15
-
16
- @property
17
- def driver_id(self) -> str:
18
- return self._driver_id
19
-
20
- async def execute(self, ctx: ExecutionContext) -> RawResult:
21
- operation = ctx.args.get("operation", ctx.capability_id)
22
- result = await self._client.call_tool(operation, ctx.args)
23
- return RawResult(capability_id=ctx.capability_id, data=result)
24
- ```
25
-
26
- Then register it:
27
-
28
- ```python
29
- kernel.register_driver(MCPDriver(mcp_client))
30
- router.add_route("mcp.my_tool", ["mcp"])
31
- ```
32
-
33
- ## HTTPDriver
34
-
35
- The built-in `HTTPDriver` supports GET, POST, PUT, DELETE:
36
-
37
- ```python
38
- from agent_kernel.drivers.http import HTTPDriver, HTTPEndpoint
39
-
40
- driver = HTTPDriver(driver_id="my_api")
41
- driver.register_endpoint("users.list", HTTPEndpoint(
42
- url="https://api.example.com/users",
43
- method="GET",
44
- headers={"Authorization": "Bearer ..."},
45
- ))
46
- kernel.register_driver(driver)
47
- ```
48
-
49
- ## Custom drivers
50
-
51
- Any object implementing the `Driver` protocol can be registered:
52
-
53
- ```python
54
- class Driver(Protocol):
55
- @property
56
- def driver_id(self) -> str: ...
57
- async def execute(self, ctx: ExecutionContext) -> RawResult: ...
58
- ```
59
-
60
- ## Capability mapping
61
-
62
- When mapping MCP tools to capabilities, prefer task-shaped names:
63
-
64
- | MCP tool | Capability ID | Safety class |
65
- |----------|--------------|--------------|
66
- | `list_files` | `fs.list_files` | READ |
67
- | `read_file` | `fs.read_file` | READ |
68
- | `write_file` | `fs.write_file` | WRITE |
69
- | `delete_file` | `fs.delete_file` | DESTRUCTIVE |
70
- | `execute_code` | `sandbox.run_code` | DESTRUCTIVE |
File without changes
File without changes
File without changes
File without changes