vaultkit 0.1.1__tar.gz → 0.3.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.
- {vaultkit-0.1.1 → vaultkit-0.3.0}/PKG-INFO +38 -5
- {vaultkit-0.1.1 → vaultkit-0.3.0}/README.md +31 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/pyproject.toml +11 -4
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/__init__.py +1 -1
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/client.py +41 -0
- vaultkit-0.3.0/vaultkit/mcp/__init__.py +32 -0
- vaultkit-0.3.0/vaultkit/mcp/__main__.py +116 -0
- vaultkit-0.3.0/vaultkit/mcp/identity.py +118 -0
- vaultkit-0.3.0/vaultkit/mcp/server.py +130 -0
- vaultkit-0.3.0/vaultkit/tools/adapters/mcp.py +41 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/definitions.py +67 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/executor.py +4 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/utils/validation.py +74 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/PKG-INFO +38 -5
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/SOURCES.txt +6 -0
- vaultkit-0.3.0/vaultkit.egg-info/entry_points.txt +2 -0
- vaultkit-0.3.0/vaultkit.egg-info/requires.txt +4 -0
- vaultkit-0.1.1/vaultkit.egg-info/requires.txt +0 -1
- {vaultkit-0.1.1 → vaultkit-0.3.0}/setup.cfg +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/tests/test_fetch.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/tests/test_query.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/core/__init__.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/core/http.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/core/polling.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/errors/__init__.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/errors/base.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/errors/exceptions.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/__init__.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/dataset_info.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/dataset_schema.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/fetch_result.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/query_result.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/__init__.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/adapters/__init__.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/adapters/anthropic.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/adapters/openai.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/builder.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/schemas.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/utils/__init__.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/utils/retry.py +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/dependency_links.txt +0 -0
- {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vaultkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: VaultKit Python SDK for policy-driven, runtime governed data access
|
|
5
5
|
Author-email: VaultKit <founders@vaultkit.io>
|
|
6
6
|
License: MIT
|
|
@@ -8,19 +8,21 @@ Project-URL: Homepage, https://github.com/vaultkit-inc/vaultkit-sdk-python
|
|
|
8
8
|
Project-URL: Documentation, https://docs.vaultkit.io
|
|
9
9
|
Project-URL: Source, https://github.com/vaultkit-inc/vaultkit-sdk-python
|
|
10
10
|
Project-URL: Issues, https://github.com/vaultkit-inc/vaultkit-sdk-python/issues
|
|
11
|
-
Keywords: ai,data,sdk,governance,security,agents,runtime
|
|
11
|
+
Keywords: ai,data,sdk,governance,security,agents,runtime,mcp
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: httpx>=0.24.0
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp<2,>=1.27; extra == "mcp"
|
|
24
26
|
|
|
25
27
|
# VaultKit Python SDK
|
|
26
28
|
|
|
@@ -72,6 +74,37 @@ print(result.rows)
|
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
77
|
+
## Analytical Queries
|
|
78
|
+
|
|
79
|
+
Aggregate, group, filter on aggregates and sort within a single dataset:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
result = client.execute(
|
|
83
|
+
dataset="orders",
|
|
84
|
+
fields=["region"],
|
|
85
|
+
aggregates=[{"func": "sum", "field": "amount", "alias": "total"}],
|
|
86
|
+
having=[{"field": "SUM(amount)", "operator": "gt", "value": 1000}],
|
|
87
|
+
order_by={"column": "total", "direction": "desc"},
|
|
88
|
+
limit=10,
|
|
89
|
+
purpose="Quarterly revenue by region",
|
|
90
|
+
)
|
|
91
|
+
# [{"region": "US", "total": 18250}, {"region": "CA", "total": 4100}, ...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
95
|
+
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
96
|
+
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
97
|
+
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
98
|
+
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
99
|
+
|
|
100
|
+
Policies apply to every field these clauses use. `min`/`max`/`sum`/`avg` over a masked
|
|
101
|
+
field are denied; `count` is allowed. Joins are not supported.
|
|
102
|
+
|
|
103
|
+
> Requires a VaultKit control plane with intent analytics support. Older control
|
|
104
|
+
> planes ignore these parameters and return ungrouped rows.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
75
108
|
## AI Agent Usage
|
|
76
109
|
|
|
77
110
|
VaultKit provides built-in tools for LLM agents.
|
|
@@ -48,6 +48,37 @@ print(result.rows)
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
+
## Analytical Queries
|
|
52
|
+
|
|
53
|
+
Aggregate, group, filter on aggregates and sort within a single dataset:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
result = client.execute(
|
|
57
|
+
dataset="orders",
|
|
58
|
+
fields=["region"],
|
|
59
|
+
aggregates=[{"func": "sum", "field": "amount", "alias": "total"}],
|
|
60
|
+
having=[{"field": "SUM(amount)", "operator": "gt", "value": 1000}],
|
|
61
|
+
order_by={"column": "total", "direction": "desc"},
|
|
62
|
+
limit=10,
|
|
63
|
+
purpose="Quarterly revenue by region",
|
|
64
|
+
)
|
|
65
|
+
# [{"region": "US", "total": 18250}, {"region": "CA", "total": 4100}, ...]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
69
|
+
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
70
|
+
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
71
|
+
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
72
|
+
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
73
|
+
|
|
74
|
+
Policies apply to every field these clauses use. `min`/`max`/`sum`/`avg` over a masked
|
|
75
|
+
field are denied; `count` is allowed. Joins are not supported.
|
|
76
|
+
|
|
77
|
+
> Requires a VaultKit control plane with intent analytics support. Older control
|
|
78
|
+
> planes ignore these parameters and return ungrouped rows.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
51
82
|
## AI Agent Usage
|
|
52
83
|
|
|
53
84
|
VaultKit provides built-in tools for LLM agents.
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vaultkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "VaultKit Python SDK for policy-driven, runtime governed data access"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "VaultKit", email = "founders@vaultkit.io" }
|
|
11
11
|
]
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
license = { text = "MIT" }
|
|
15
15
|
|
|
16
16
|
dependencies = [
|
|
@@ -25,6 +25,7 @@ keywords = [
|
|
|
25
25
|
"security",
|
|
26
26
|
"agents",
|
|
27
27
|
"runtime",
|
|
28
|
+
"mcp",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
classifiers = [
|
|
@@ -32,13 +33,19 @@ classifiers = [
|
|
|
32
33
|
"Intended Audience :: Developers",
|
|
33
34
|
"License :: OSI Approved :: MIT License",
|
|
34
35
|
"Programming Language :: Python :: 3",
|
|
35
|
-
"Programming Language :: Python :: 3.8",
|
|
36
|
-
"Programming Language :: Python :: 3.9",
|
|
37
36
|
"Programming Language :: Python :: 3.10",
|
|
38
37
|
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Programming Language :: Python :: 3.12",
|
|
39
|
+
"Programming Language :: Python :: 3.13",
|
|
39
40
|
"Topic :: Software Development :: Libraries",
|
|
40
41
|
]
|
|
41
42
|
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
mcp = ["mcp>=1.27,<2"]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
vaultkit-mcp = "vaultkit.mcp.__main__:main"
|
|
48
|
+
|
|
42
49
|
[project.urls]
|
|
43
50
|
Homepage = "https://github.com/vaultkit-inc/vaultkit-sdk-python"
|
|
44
51
|
Documentation = "https://docs.vaultkit.io"
|
|
@@ -19,7 +19,11 @@ from vaultkit.models.query_result import QueryResult
|
|
|
19
19
|
from vaultkit.utils.retry import RetryConfig, with_retries
|
|
20
20
|
from vaultkit.utils.validation import (
|
|
21
21
|
require_str,
|
|
22
|
+
validate_aggregates,
|
|
22
23
|
validate_filters,
|
|
24
|
+
validate_group_by,
|
|
25
|
+
validate_having,
|
|
26
|
+
validate_order_by,
|
|
23
27
|
validate_limit,
|
|
24
28
|
)
|
|
25
29
|
|
|
@@ -98,6 +102,10 @@ class VaultKitClient:
|
|
|
98
102
|
fields: Optional[List[str]] = None,
|
|
99
103
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
100
104
|
limit: Optional[int] = None,
|
|
105
|
+
aggregates: Optional[List[Dict[str, Any]]] = None,
|
|
106
|
+
group_by: Optional[List[str]] = None,
|
|
107
|
+
having: Optional[List[Dict[str, Any]]] = None,
|
|
108
|
+
order_by: Optional[Dict[str, Any]] = None,
|
|
101
109
|
purpose: Optional[str] = None,
|
|
102
110
|
requester_region: Optional[str] = None,
|
|
103
111
|
requester_clearance: Optional[str] = None,
|
|
@@ -106,6 +114,18 @@ class VaultKitClient:
|
|
|
106
114
|
"""
|
|
107
115
|
Submit an intent request and return data — full grant lifecycle internal.
|
|
108
116
|
|
|
117
|
+
Analytical queries (single dataset, no joins):
|
|
118
|
+
aggregates [{"func": "sum", "field": "amount", "alias": "total"}]
|
|
119
|
+
func is one of count, sum, avg, min, max; "*" only with count.
|
|
120
|
+
group_by ["region"] — every field in `fields` must be listed when given.
|
|
121
|
+
having [{"field": "SUM(amount)", "operator": "gt", "value": 1000}]
|
|
122
|
+
field must be an aggregate expression, not an alias.
|
|
123
|
+
order_by {"column": "total", "direction": "desc"}
|
|
124
|
+
|
|
125
|
+
`fields` may be omitted when `aggregates` is given (e.g. a single COUNT(*)).
|
|
126
|
+
Requires a control plane that supports intent analytics; older ones
|
|
127
|
+
ignore these parameters.
|
|
128
|
+
|
|
109
129
|
Raises:
|
|
110
130
|
DeniedError Policy rejected. Do not retry.
|
|
111
131
|
ApprovalRequiredError Needs human approval (request_id attached).
|
|
@@ -126,6 +146,10 @@ class VaultKitClient:
|
|
|
126
146
|
fields=fields,
|
|
127
147
|
filters=filters,
|
|
128
148
|
limit=limit,
|
|
149
|
+
aggregates=aggregates,
|
|
150
|
+
group_by=group_by,
|
|
151
|
+
having=having,
|
|
152
|
+
order_by=order_by,
|
|
129
153
|
purpose=purpose,
|
|
130
154
|
requester_region=requester_region,
|
|
131
155
|
requester_clearance=requester_clearance,
|
|
@@ -214,6 +238,10 @@ class VaultKitClient:
|
|
|
214
238
|
fields: Optional[List[str]] = None,
|
|
215
239
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
216
240
|
limit: Optional[int] = None,
|
|
241
|
+
aggregates: Optional[List[Dict[str, Any]]] = None,
|
|
242
|
+
group_by: Optional[List[str]] = None,
|
|
243
|
+
having: Optional[List[Dict[str, Any]]] = None,
|
|
244
|
+
order_by: Optional[Dict[str, Any]] = None,
|
|
217
245
|
purpose: Optional[str] = None,
|
|
218
246
|
requester_region: Optional[str] = None,
|
|
219
247
|
requester_clearance: Optional[str] = None,
|
|
@@ -223,6 +251,10 @@ class VaultKitClient:
|
|
|
223
251
|
dataset = require_str("dataset", dataset)
|
|
224
252
|
limit = validate_limit(limit)
|
|
225
253
|
filters = validate_filters(filters)
|
|
254
|
+
aggregates = validate_aggregates(aggregates)
|
|
255
|
+
group_by = validate_group_by(group_by)
|
|
256
|
+
having = validate_having(having)
|
|
257
|
+
order_by = validate_order_by(order_by)
|
|
226
258
|
|
|
227
259
|
req: Dict[str, Any] = {"dataset": dataset}
|
|
228
260
|
body: Dict[str, Any] = {"request": req}
|
|
@@ -235,6 +267,14 @@ class VaultKitClient:
|
|
|
235
267
|
req["purpose"] = str(purpose)
|
|
236
268
|
if filters is not None:
|
|
237
269
|
req["filters"] = filters
|
|
270
|
+
if aggregates is not None:
|
|
271
|
+
req["aggregates"] = aggregates
|
|
272
|
+
if group_by is not None:
|
|
273
|
+
req["group_by"] = group_by
|
|
274
|
+
if having is not None:
|
|
275
|
+
req["having"] = having
|
|
276
|
+
if order_by is not None:
|
|
277
|
+
req["order_by"] = order_by
|
|
238
278
|
if requester_region is not None:
|
|
239
279
|
body["requester_region"] = requester_region
|
|
240
280
|
if requester_clearance is not None:
|
|
@@ -248,6 +288,7 @@ class VaultKitClient:
|
|
|
248
288
|
"poll": poll,
|
|
249
289
|
"has_fields": fields is not None,
|
|
250
290
|
"has_filters": filters is not None,
|
|
291
|
+
"has_aggregates": aggregates is not None,
|
|
251
292
|
"limit": limit,
|
|
252
293
|
},
|
|
253
294
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
VaultKit MCP server (the ``[mcp]`` extra).
|
|
5
|
+
|
|
6
|
+
This subpackage exposes VaultKit's governed tools over the Model Context
|
|
7
|
+
Protocol so any MCP host (Claude Desktop, Cursor, custom agents) can query
|
|
8
|
+
governed data.
|
|
9
|
+
|
|
10
|
+
No policy evaluation happens here. This layer advertises tools by reusing
|
|
11
|
+
``ToolBuilder`` and dispatches calls by reusing ``ToolExecutor``. Every policy
|
|
12
|
+
decision, mask, grant and audit entry is performed by the control plane.
|
|
13
|
+
|
|
14
|
+
Two deployments share one codebase:
|
|
15
|
+
- stdio : one principal from environment variables (local use).
|
|
16
|
+
- http : one principal per authenticated session (not yet implemented;
|
|
17
|
+
see identity.HttpClientResolver).
|
|
18
|
+
|
|
19
|
+
The only difference between them is how the caller's identity is resolved into
|
|
20
|
+
a VaultKitClient; everything else is shared.
|
|
21
|
+
|
|
22
|
+
Install:
|
|
23
|
+
pip install vaultkit[mcp]
|
|
24
|
+
|
|
25
|
+
Run:
|
|
26
|
+
vaultkit-mcp # stdio (default)
|
|
27
|
+
vaultkit-mcp --transport http --host 0.0.0.0 --port 8000 # not yet implemented
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from .server import McpServerConfig, build_server
|
|
31
|
+
|
|
32
|
+
__all__ = ["McpServerConfig", "build_server"]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
``vaultkit-mcp`` entrypoint.
|
|
5
|
+
|
|
6
|
+
vaultkit-mcp # stdio (default), local use
|
|
7
|
+
vaultkit-mcp --transport http --port 8000 # hosted (not yet implemented)
|
|
8
|
+
|
|
9
|
+
stdio is the local transport: an MCP host (Claude Desktop, Cursor, ...) launches
|
|
10
|
+
this as a subprocess and pipes JSON-RPC over stdin/stdout. Credentials come from
|
|
11
|
+
the environment (see StdioClientResolver).
|
|
12
|
+
|
|
13
|
+
http (streamable HTTP) is the hosted transport. It is not wired up yet: it
|
|
14
|
+
requires the per-principal OAuth exchange in HttpClientResolver first.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from .identity import HttpClientResolver, StdioClientResolver
|
|
23
|
+
from .server import McpServerConfig, build_server
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _config_from_env() -> McpServerConfig:
|
|
27
|
+
def _flag(name: str, default: bool) -> bool:
|
|
28
|
+
raw = os.environ.get(name)
|
|
29
|
+
if raw is None:
|
|
30
|
+
return default
|
|
31
|
+
return raw.strip().lower() in ("1", "true", "yes", "on")
|
|
32
|
+
|
|
33
|
+
return McpServerConfig(
|
|
34
|
+
environment=os.environ.get("VAULTKIT_ENVIRONMENT", "production"),
|
|
35
|
+
include_discover=_flag("VAULTKIT_MCP_INCLUDE_DISCOVER", True),
|
|
36
|
+
include_query=_flag("VAULTKIT_MCP_INCLUDE_QUERY", True),
|
|
37
|
+
include_check_approval=_flag("VAULTKIT_MCP_INCLUDE_CHECK_APPROVAL", True),
|
|
38
|
+
fetch_schema_hints=_flag("VAULTKIT_MCP_SCHEMA_HINTS", True),
|
|
39
|
+
default_purpose=os.environ.get("VAULTKIT_MCP_DEFAULT_PURPOSE"),
|
|
40
|
+
default_requester_region=os.environ.get("VAULTKIT_MCP_DEFAULT_REGION"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _run_stdio(server) -> None:
|
|
45
|
+
from mcp.server.stdio import stdio_server
|
|
46
|
+
|
|
47
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
48
|
+
await server.run(
|
|
49
|
+
read_stream,
|
|
50
|
+
write_stream,
|
|
51
|
+
server.create_initialization_options(),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _run_http(server, host: str, port: int) -> None:
|
|
56
|
+
# Not implemented. Streamable HTTP requires a session manager and an ASGI
|
|
57
|
+
# app, and — before that — the per-principal OAuth exchange in
|
|
58
|
+
# HttpClientResolver. Reference for the intended wiring:
|
|
59
|
+
#
|
|
60
|
+
# from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
61
|
+
# import uvicorn
|
|
62
|
+
# from starlette.applications import Starlette
|
|
63
|
+
# ... mount the session manager, add OAuth token validation middleware,
|
|
64
|
+
# then uvicorn.run(app, host=host, port=port)
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
"Hosted HTTP transport is not implemented yet. Implement "
|
|
67
|
+
"HttpClientResolver (per-principal identity) first, then wire the "
|
|
68
|
+
"streamable-HTTP ASGI app here. Use --transport stdio for now."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: Optional[list] = None) -> int:
|
|
73
|
+
parser = argparse.ArgumentParser(
|
|
74
|
+
prog="vaultkit-mcp",
|
|
75
|
+
description="Expose VaultKit's governed tools over the Model Context Protocol.",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--transport",
|
|
79
|
+
choices=("stdio", "http"),
|
|
80
|
+
default=os.environ.get("VAULTKIT_MCP_TRANSPORT", "stdio"),
|
|
81
|
+
help="Transport to serve on (default: stdio).",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument("--host", default=os.environ.get("VAULTKIT_MCP_HOST", "127.0.0.1"))
|
|
84
|
+
parser.add_argument("--port", type=int, default=int(os.environ.get("VAULTKIT_MCP_PORT", "8000")))
|
|
85
|
+
args = parser.parse_args(argv)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
import anyio
|
|
89
|
+
except ImportError:
|
|
90
|
+
print(
|
|
91
|
+
"The MCP extra is not installed. Install it with:\n"
|
|
92
|
+
" pip install vaultkit[mcp]",
|
|
93
|
+
file=sys.stderr,
|
|
94
|
+
)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
config = _config_from_env()
|
|
98
|
+
|
|
99
|
+
if args.transport == "stdio":
|
|
100
|
+
resolver = StdioClientResolver()
|
|
101
|
+
server = build_server(resolver, config)
|
|
102
|
+
try:
|
|
103
|
+
anyio.run(_run_stdio, server)
|
|
104
|
+
finally:
|
|
105
|
+
resolver.close()
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
# http
|
|
109
|
+
resolver = HttpClientResolver()
|
|
110
|
+
server = build_server(resolver, config)
|
|
111
|
+
_run_http(server, args.host, args.port)
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Identity resolution for the MCP server.
|
|
5
|
+
|
|
6
|
+
A single interface, ``ClientResolver``, turns an incoming request into a
|
|
7
|
+
``VaultKitClient``. This is the only part of the MCP layer that differs between
|
|
8
|
+
deployments, which lets ``server.py`` be written once and run in either mode:
|
|
9
|
+
|
|
10
|
+
stdio -> StdioClientResolver : one principal, read from env at startup.
|
|
11
|
+
http -> HttpClientResolver : one principal per authenticated request.
|
|
12
|
+
|
|
13
|
+
The request handlers depend only on the ``ClientResolver`` interface and never
|
|
14
|
+
know which implementation they received.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
from vaultkit.client import VaultKitClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ClientResolver:
|
|
24
|
+
"""Resolve an incoming request context into a VaultKitClient.
|
|
25
|
+
|
|
26
|
+
``ctx`` is whatever the transport provides: ``None`` for stdio, or an
|
|
27
|
+
authenticated request context for http. Implementations use as much of it
|
|
28
|
+
as they need.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def resolve(self, ctx: Optional[Any] = None) -> VaultKitClient: # pragma: no cover
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
def close(self) -> None: # pragma: no cover
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class StdioClientResolver(ClientResolver):
|
|
39
|
+
"""Single-principal resolver for local (stdio) use.
|
|
40
|
+
|
|
41
|
+
Reads credentials from the environment once and returns the same client for
|
|
42
|
+
every call. Over stdio there is a single caller (the host process that
|
|
43
|
+
launched the server), so there is a single principal, and ``ctx`` is
|
|
44
|
+
ignored.
|
|
45
|
+
|
|
46
|
+
Required environment variables:
|
|
47
|
+
VAULTKIT_BASE_URL
|
|
48
|
+
VAULTKIT_TOKEN
|
|
49
|
+
VAULTKIT_ORG
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
base_url: Optional[str] = None,
|
|
56
|
+
token: Optional[str] = None,
|
|
57
|
+
org: Optional[str] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
self._base_url = base_url or os.environ.get("VAULTKIT_BASE_URL")
|
|
60
|
+
self._token = token or os.environ.get("VAULTKIT_TOKEN")
|
|
61
|
+
self._org = org or os.environ.get("VAULTKIT_ORG")
|
|
62
|
+
|
|
63
|
+
missing = [
|
|
64
|
+
name
|
|
65
|
+
for name, val in (
|
|
66
|
+
("VAULTKIT_BASE_URL", self._base_url),
|
|
67
|
+
("VAULTKIT_TOKEN", self._token),
|
|
68
|
+
("VAULTKIT_ORG", self._org),
|
|
69
|
+
)
|
|
70
|
+
if not val
|
|
71
|
+
]
|
|
72
|
+
if missing:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
"Missing required VaultKit credentials for the MCP server: "
|
|
75
|
+
+ ", ".join(missing)
|
|
76
|
+
+ ". Set them in the environment (or your MCP host's server "
|
|
77
|
+
"config 'env' block) before starting vaultkit-mcp."
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self._client: Optional[VaultKitClient] = None
|
|
81
|
+
|
|
82
|
+
def resolve(self, ctx: Optional[Any] = None) -> VaultKitClient:
|
|
83
|
+
if self._client is None:
|
|
84
|
+
self._client = VaultKitClient(
|
|
85
|
+
base_url=self._base_url, # type: ignore[arg-type]
|
|
86
|
+
token=self._token, # type: ignore[arg-type]
|
|
87
|
+
org=self._org, # type: ignore[arg-type]
|
|
88
|
+
)
|
|
89
|
+
return self._client
|
|
90
|
+
|
|
91
|
+
def close(self) -> None:
|
|
92
|
+
if self._client is not None:
|
|
93
|
+
self._client.close()
|
|
94
|
+
self._client = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HttpClientResolver(ClientResolver):
|
|
98
|
+
"""Per-principal resolver for hosted (streamable-HTTP) use.
|
|
99
|
+
|
|
100
|
+
Not yet implemented. Over http each request may belong to a different
|
|
101
|
+
principal, so a client cannot be built once at startup; it must be resolved
|
|
102
|
+
per request from the authenticated identity.
|
|
103
|
+
|
|
104
|
+
Intended implementation:
|
|
105
|
+
1. Read the bearer token from ``ctx`` (the request's auth context).
|
|
106
|
+
2. Resolve it to a (token, org) principal, delegating to the control
|
|
107
|
+
plane rather than trusting client-supplied claims.
|
|
108
|
+
3. Construct and return a VaultKitClient for that principal.
|
|
109
|
+
4. Optionally cache clients per principal and close idle ones.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def resolve(self, ctx: Optional[Any] = None) -> VaultKitClient:
|
|
113
|
+
raise NotImplementedError(
|
|
114
|
+
"Hosted HTTP identity resolution is not implemented yet. "
|
|
115
|
+
"Run the server with --transport stdio, or implement "
|
|
116
|
+
"HttpClientResolver.resolve to exchange the request's OAuth token "
|
|
117
|
+
"for a VaultKit principal via the control plane."
|
|
118
|
+
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
The VaultKit MCP server.
|
|
5
|
+
|
|
6
|
+
Two handlers, both thin:
|
|
7
|
+
|
|
8
|
+
list_tools -> reuse ToolBuilder to advertise the caller's authorized tools.
|
|
9
|
+
call_tool -> reuse ToolExecutor to run a call and return its result.
|
|
10
|
+
|
|
11
|
+
This uses the low-level ``mcp.server.lowlevel.Server`` rather than the
|
|
12
|
+
``@mcp.tool`` decorator API because the decorator registers a static tool list
|
|
13
|
+
at startup, whereas VaultKit's tool list is per-principal: different callers are
|
|
14
|
+
authorized for different datasets. ``list_tools`` therefore has to be computed
|
|
15
|
+
per session, which the low-level API allows.
|
|
16
|
+
|
|
17
|
+
The VaultKit SDK is synchronous (blocking HTTP and polling), while MCP handlers
|
|
18
|
+
are async. Blocking work is offloaded to a worker thread via
|
|
19
|
+
``anyio.to_thread.run_sync`` so it does not stall the event loop. This is
|
|
20
|
+
required once multiple callers can be served concurrently, and harmless for the
|
|
21
|
+
single-caller stdio case.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import List, Optional
|
|
27
|
+
|
|
28
|
+
import anyio
|
|
29
|
+
import mcp.types as types
|
|
30
|
+
from mcp.server.lowlevel import Server
|
|
31
|
+
|
|
32
|
+
from vaultkit.tools.adapters import ToolProvider
|
|
33
|
+
from vaultkit.tools.adapters.mcp import to_mcp_tool
|
|
34
|
+
from vaultkit.tools.builder import ToolBuilder
|
|
35
|
+
from vaultkit.tools.executor import ToolExecutor
|
|
36
|
+
|
|
37
|
+
from .identity import ClientResolver
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class McpServerConfig:
|
|
42
|
+
"""Server-wide configuration (not per-call; per-call args come from the agent)."""
|
|
43
|
+
|
|
44
|
+
environment: str = "production"
|
|
45
|
+
include_discover: bool = True
|
|
46
|
+
include_query: bool = True
|
|
47
|
+
# check_approval is exposed by default: the approval flow is asynchronous,
|
|
48
|
+
# so the agent needs a way to poll a pending request to completion.
|
|
49
|
+
include_check_approval: bool = True
|
|
50
|
+
# Schema hints feed the agent the exact valid field names per dataset, which
|
|
51
|
+
# guards against field-name hallucination. Keep on unless a very large
|
|
52
|
+
# catalog makes startup slow.
|
|
53
|
+
fetch_schema_hints: bool = True
|
|
54
|
+
# Optional fallbacks if an agent omits them (agents normally supply purpose).
|
|
55
|
+
default_purpose: Optional[str] = None
|
|
56
|
+
default_requester_region: Optional[str] = None
|
|
57
|
+
server_name: str = "vaultkit"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_server(resolver: ClientResolver, config: Optional[McpServerConfig] = None) -> Server:
|
|
61
|
+
"""Wire a low-level MCP Server around the given identity resolver.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
resolver: Turns a request context into a VaultKitClient, and thereby
|
|
65
|
+
determines whether this is a single-principal (stdio) or
|
|
66
|
+
per-principal (http) server.
|
|
67
|
+
config: Server-wide options. Defaults are suitable for local (stdio) use.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A configured ``mcp.server.lowlevel.Server`` ready to run over any
|
|
71
|
+
transport.
|
|
72
|
+
"""
|
|
73
|
+
cfg = config or McpServerConfig()
|
|
74
|
+
server: Server = Server(cfg.server_name)
|
|
75
|
+
|
|
76
|
+
@server.list_tools()
|
|
77
|
+
async def _list_tools() -> List[types.Tool]:
|
|
78
|
+
# Resolve the caller -> client, then build their authorized tool list.
|
|
79
|
+
# RAW gives canonical defs; those are mapped to MCP Tools by to_mcp_tool.
|
|
80
|
+
client = resolver.resolve(_current_ctx(server))
|
|
81
|
+
|
|
82
|
+
def _build():
|
|
83
|
+
builder = ToolBuilder(client)
|
|
84
|
+
return builder.build(
|
|
85
|
+
provider=ToolProvider.RAW,
|
|
86
|
+
environment=cfg.environment,
|
|
87
|
+
include_discover=cfg.include_discover,
|
|
88
|
+
include_query=cfg.include_query,
|
|
89
|
+
include_check_approval=cfg.include_check_approval,
|
|
90
|
+
fetch_schema_hints=cfg.fetch_schema_hints,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
canonical = await anyio.to_thread.run_sync(_build)
|
|
94
|
+
return [to_mcp_tool(t) for t in canonical]
|
|
95
|
+
|
|
96
|
+
@server.call_tool()
|
|
97
|
+
async def _call_tool(name: str, arguments: dict) -> List[types.ContentBlock]:
|
|
98
|
+
# Resolve the caller -> client, then dispatch through the executor.
|
|
99
|
+
# The executor never raises: denials, pending-approval and masking all
|
|
100
|
+
# come back as a structured dict with a `status` field, which is
|
|
101
|
+
# forwarded verbatim. A policy denial is a valid governed answer, not a
|
|
102
|
+
# protocol error, so it is not raised or marked isError.
|
|
103
|
+
client = resolver.resolve(_current_ctx(server))
|
|
104
|
+
|
|
105
|
+
def _run():
|
|
106
|
+
executor = ToolExecutor(
|
|
107
|
+
client,
|
|
108
|
+
default_purpose=cfg.default_purpose,
|
|
109
|
+
default_requester_region=cfg.default_requester_region,
|
|
110
|
+
)
|
|
111
|
+
return executor.execute(name, arguments or {})
|
|
112
|
+
|
|
113
|
+
result = await anyio.to_thread.run_sync(_run)
|
|
114
|
+
text = json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
|
115
|
+
return [types.TextContent(type="text", text=text)]
|
|
116
|
+
|
|
117
|
+
return server
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _current_ctx(server: Server):
|
|
121
|
+
"""Return the active request context, or None if there is none.
|
|
122
|
+
|
|
123
|
+
stdio resolvers ignore this. The http resolver uses it to reach the
|
|
124
|
+
authenticated identity. Accessing ``request_context`` outside a request
|
|
125
|
+
raises, so it is guarded to keep the single-principal case working.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
return server.request_context
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
MCP tool adapter.
|
|
5
|
+
|
|
6
|
+
Converts a canonical VaultKit tool definition into an ``mcp.types.Tool``,
|
|
7
|
+
exactly the way ``to_anthropic_tool`` and ``to_openai_tool`` convert into
|
|
8
|
+
their provider shapes.
|
|
9
|
+
|
|
10
|
+
Canonical shape (from vaultkit.tools.definitions):
|
|
11
|
+
{"name": str, "description": str, "input_schema": {...}}
|
|
12
|
+
|
|
13
|
+
MCP shape (mcp.types.Tool):
|
|
14
|
+
Tool(name=..., description=..., inputSchema={...})
|
|
15
|
+
|
|
16
|
+
The only real work is the ``input_schema`` -> ``inputSchema`` rename and
|
|
17
|
+
wrapping the dict in the MCP type. No governance logic lives here — this is
|
|
18
|
+
pure translation, the same as every other adapter in this package.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
import mcp.types as types
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_mcp_tool(canonical: Dict[str, Any]) -> types.Tool:
|
|
27
|
+
"""Convert one canonical tool def into an MCP Tool.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
canonical: A dict with ``name``, ``description`` and ``input_schema``,
|
|
31
|
+
as produced by ``vaultkit.tools.definitions``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
An ``mcp.types.Tool`` suitable for returning from a ``list_tools``
|
|
35
|
+
handler.
|
|
36
|
+
"""
|
|
37
|
+
return types.Tool(
|
|
38
|
+
name=canonical["name"],
|
|
39
|
+
description=canonical.get("description"),
|
|
40
|
+
inputSchema=canonical["input_schema"],
|
|
41
|
+
)
|
|
@@ -115,6 +115,8 @@ def query_tool_def(
|
|
|
115
115
|
"description": (
|
|
116
116
|
"Query a governed dataset through VaultKit. Policies are enforced automatically: "
|
|
117
117
|
"sensitive fields may be masked and some datasets require approval before data is returned. "
|
|
118
|
+
"Supports aggregation (count/sum/avg/min/max), grouping, having and sorting on a single dataset; "
|
|
119
|
+
"joins are not supported. "
|
|
118
120
|
"Returns data or a pending status if approval is required."
|
|
119
121
|
),
|
|
120
122
|
"input_schema": {
|
|
@@ -143,6 +145,71 @@ def query_tool_def(
|
|
|
143
145
|
"maximum": 10000,
|
|
144
146
|
"description": "Maximum number of rows to return.",
|
|
145
147
|
},
|
|
148
|
+
"aggregates": {
|
|
149
|
+
"type": "array",
|
|
150
|
+
"minItems": 1,
|
|
151
|
+
"items": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"properties": {
|
|
154
|
+
"func": {"type": "string", "enum": ["count", "sum", "avg", "min", "max"]},
|
|
155
|
+
"field": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "A field of the selected dataset, or '*' with count.",
|
|
158
|
+
},
|
|
159
|
+
"alias": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
|
|
162
|
+
"description": "Name of the result column. Use it in order_by.",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
"required": ["func", "field"],
|
|
166
|
+
},
|
|
167
|
+
"description": (
|
|
168
|
+
"Aggregate functions over the selected dataset. When aggregating, every entry in "
|
|
169
|
+
"'fields' is a grouping column. 'fields' may be omitted for a single total.\n\n"
|
|
170
|
+
"Example: [{func: 'sum', field: 'amount', alias: 'total'}]\n"
|
|
171
|
+
"min/max/sum/avg over masked fields are denied by policy; count is allowed."
|
|
172
|
+
),
|
|
173
|
+
},
|
|
174
|
+
"group_by": {
|
|
175
|
+
"type": "array",
|
|
176
|
+
"items": {"type": "string"},
|
|
177
|
+
"description": (
|
|
178
|
+
"Fields to group by. Optional: defaults to 'fields'. If given, it must include "
|
|
179
|
+
"every entry in 'fields'."
|
|
180
|
+
),
|
|
181
|
+
},
|
|
182
|
+
"having": {
|
|
183
|
+
"type": "array",
|
|
184
|
+
"items": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"properties": {
|
|
187
|
+
"field": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "An aggregate expression such as 'SUM(amount)'. Aliases are not allowed here.",
|
|
190
|
+
},
|
|
191
|
+
"operator": {"type": "string", "enum": ["gt", "lt", "eq", "gte", "lte"]},
|
|
192
|
+
"value": {"type": "number"},
|
|
193
|
+
},
|
|
194
|
+
"required": ["field", "operator", "value"],
|
|
195
|
+
},
|
|
196
|
+
"description": (
|
|
197
|
+
"Filters applied after aggregation. Requires 'aggregates'.\n\n"
|
|
198
|
+
"Example: [{field: 'SUM(amount)', operator: 'gt', value: 1000}]"
|
|
199
|
+
),
|
|
200
|
+
},
|
|
201
|
+
"order_by": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"properties": {
|
|
204
|
+
"column": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "A field, an aggregate alias, or an aggregate expression.",
|
|
207
|
+
},
|
|
208
|
+
"direction": {"type": "string", "enum": ["asc", "desc"], "default": "asc"},
|
|
209
|
+
},
|
|
210
|
+
"required": ["column"],
|
|
211
|
+
"description": "Sort order of the results.",
|
|
212
|
+
},
|
|
146
213
|
"purpose": {
|
|
147
214
|
"type": "string",
|
|
148
215
|
"description": (
|
|
@@ -105,6 +105,10 @@ class ToolExecutor:
|
|
|
105
105
|
fields=args.get("fields"),
|
|
106
106
|
filters=args.get("filters"),
|
|
107
107
|
limit=args.get("limit"),
|
|
108
|
+
aggregates=args.get("aggregates"),
|
|
109
|
+
group_by=args.get("group_by"),
|
|
110
|
+
having=args.get("having"),
|
|
111
|
+
order_by=args.get("order_by"),
|
|
108
112
|
purpose=purpose,
|
|
109
113
|
requester_region=args.get("requester_region") or self._default_requester_region,
|
|
110
114
|
)
|
|
@@ -85,3 +85,77 @@ def validate_filters(value: Optional[Any]) -> Optional[list]:
|
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Analytical clauses. Only the shape is checked here; the control plane
|
|
91
|
+
# validates functions, fields and operators and returns a clear error.
|
|
92
|
+
|
|
93
|
+
def _require_list_of_dicts(name: str, value: Optional[Any]) -> Optional[list]:
|
|
94
|
+
if value is None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if not isinstance(value, list):
|
|
98
|
+
raise ValidationError(
|
|
99
|
+
f"{name} must be a list",
|
|
100
|
+
context=ErrorContext(raw=value),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for item in value:
|
|
104
|
+
if not isinstance(item, dict):
|
|
105
|
+
raise ValidationError(
|
|
106
|
+
f"each {name} entry must be an object",
|
|
107
|
+
context=ErrorContext(raw=item),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_aggregates(value: Optional[Any]) -> Optional[list]:
|
|
114
|
+
value = _require_list_of_dicts("aggregates", value)
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
if not value:
|
|
119
|
+
raise ValidationError(
|
|
120
|
+
"aggregates cannot be empty",
|
|
121
|
+
context=ErrorContext(raw=value),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for agg in value:
|
|
125
|
+
if not agg.get("func") or not agg.get("field"):
|
|
126
|
+
raise ValidationError(
|
|
127
|
+
"each aggregate needs 'func' and 'field'",
|
|
128
|
+
context=ErrorContext(raw=agg),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_group_by(value: Optional[Any]) -> Optional[list]:
|
|
135
|
+
if value is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if not isinstance(value, list) or not all(isinstance(v, str) and v.strip() for v in value):
|
|
139
|
+
raise ValidationError(
|
|
140
|
+
"group_by must be a list of field names",
|
|
141
|
+
context=ErrorContext(raw=value),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def validate_having(value: Optional[Any]) -> Optional[list]:
|
|
148
|
+
return _require_list_of_dicts("having", value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def validate_order_by(value: Optional[Any]) -> Optional[Dict[str, Any]]:
|
|
152
|
+
if value is None:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
if not isinstance(value, dict) or not value.get("column"):
|
|
156
|
+
raise ValidationError(
|
|
157
|
+
"order_by must be an object with a 'column'",
|
|
158
|
+
context=ErrorContext(raw=value),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vaultkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: VaultKit Python SDK for policy-driven, runtime governed data access
|
|
5
5
|
Author-email: VaultKit <founders@vaultkit.io>
|
|
6
6
|
License: MIT
|
|
@@ -8,19 +8,21 @@ Project-URL: Homepage, https://github.com/vaultkit-inc/vaultkit-sdk-python
|
|
|
8
8
|
Project-URL: Documentation, https://docs.vaultkit.io
|
|
9
9
|
Project-URL: Source, https://github.com/vaultkit-inc/vaultkit-sdk-python
|
|
10
10
|
Project-URL: Issues, https://github.com/vaultkit-inc/vaultkit-sdk-python/issues
|
|
11
|
-
Keywords: ai,data,sdk,governance,security,agents,runtime
|
|
11
|
+
Keywords: ai,data,sdk,governance,security,agents,runtime,mcp
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: httpx>=0.24.0
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp<2,>=1.27; extra == "mcp"
|
|
24
26
|
|
|
25
27
|
# VaultKit Python SDK
|
|
26
28
|
|
|
@@ -72,6 +74,37 @@ print(result.rows)
|
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
77
|
+
## Analytical Queries
|
|
78
|
+
|
|
79
|
+
Aggregate, group, filter on aggregates and sort within a single dataset:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
result = client.execute(
|
|
83
|
+
dataset="orders",
|
|
84
|
+
fields=["region"],
|
|
85
|
+
aggregates=[{"func": "sum", "field": "amount", "alias": "total"}],
|
|
86
|
+
having=[{"field": "SUM(amount)", "operator": "gt", "value": 1000}],
|
|
87
|
+
order_by={"column": "total", "direction": "desc"},
|
|
88
|
+
limit=10,
|
|
89
|
+
purpose="Quarterly revenue by region",
|
|
90
|
+
)
|
|
91
|
+
# [{"region": "US", "total": 18250}, {"region": "CA", "total": 4100}, ...]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `aggregates`: `func` is one of `count`, `sum`, `avg`, `min`, `max`; use `field="*"` with `count`.
|
|
95
|
+
- `group_by`: optional. Defaults to `fields`; if given, it must include every entry in `fields`.
|
|
96
|
+
- `having`: needs an aggregate expression such as `SUM(amount)`, not an alias.
|
|
97
|
+
- `order_by`: a field, an aggregate alias or an aggregate expression, `asc` (default) or `desc`.
|
|
98
|
+
- `fields` can be omitted for a single total, e.g. `aggregates=[{"func": "count", "field": "*"}]`.
|
|
99
|
+
|
|
100
|
+
Policies apply to every field these clauses use. `min`/`max`/`sum`/`avg` over a masked
|
|
101
|
+
field are denied; `count` is allowed. Joins are not supported.
|
|
102
|
+
|
|
103
|
+
> Requires a VaultKit control plane with intent analytics support. Older control
|
|
104
|
+
> planes ignore these parameters and return ungrouped rows.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
75
108
|
## AI Agent Usage
|
|
76
109
|
|
|
77
110
|
VaultKit provides built-in tools for LLM agents.
|
|
@@ -7,6 +7,7 @@ vaultkit/client.py
|
|
|
7
7
|
vaultkit.egg-info/PKG-INFO
|
|
8
8
|
vaultkit.egg-info/SOURCES.txt
|
|
9
9
|
vaultkit.egg-info/dependency_links.txt
|
|
10
|
+
vaultkit.egg-info/entry_points.txt
|
|
10
11
|
vaultkit.egg-info/requires.txt
|
|
11
12
|
vaultkit.egg-info/top_level.txt
|
|
12
13
|
vaultkit/core/__init__.py
|
|
@@ -15,6 +16,10 @@ vaultkit/core/polling.py
|
|
|
15
16
|
vaultkit/errors/__init__.py
|
|
16
17
|
vaultkit/errors/base.py
|
|
17
18
|
vaultkit/errors/exceptions.py
|
|
19
|
+
vaultkit/mcp/__init__.py
|
|
20
|
+
vaultkit/mcp/__main__.py
|
|
21
|
+
vaultkit/mcp/identity.py
|
|
22
|
+
vaultkit/mcp/server.py
|
|
18
23
|
vaultkit/models/__init__.py
|
|
19
24
|
vaultkit/models/dataset_info.py
|
|
20
25
|
vaultkit/models/dataset_schema.py
|
|
@@ -27,6 +32,7 @@ vaultkit/tools/executor.py
|
|
|
27
32
|
vaultkit/tools/schemas.py
|
|
28
33
|
vaultkit/tools/adapters/__init__.py
|
|
29
34
|
vaultkit/tools/adapters/anthropic.py
|
|
35
|
+
vaultkit/tools/adapters/mcp.py
|
|
30
36
|
vaultkit/tools/adapters/openai.py
|
|
31
37
|
vaultkit/utils/__init__.py
|
|
32
38
|
vaultkit/utils/retry.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
httpx>=0.24.0
|
|
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
|
|
File without changes
|