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.
Files changed (42) hide show
  1. {vaultkit-0.1.1 → vaultkit-0.3.0}/PKG-INFO +38 -5
  2. {vaultkit-0.1.1 → vaultkit-0.3.0}/README.md +31 -0
  3. {vaultkit-0.1.1 → vaultkit-0.3.0}/pyproject.toml +11 -4
  4. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/__init__.py +1 -1
  5. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/client.py +41 -0
  6. vaultkit-0.3.0/vaultkit/mcp/__init__.py +32 -0
  7. vaultkit-0.3.0/vaultkit/mcp/__main__.py +116 -0
  8. vaultkit-0.3.0/vaultkit/mcp/identity.py +118 -0
  9. vaultkit-0.3.0/vaultkit/mcp/server.py +130 -0
  10. vaultkit-0.3.0/vaultkit/tools/adapters/mcp.py +41 -0
  11. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/definitions.py +67 -0
  12. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/executor.py +4 -0
  13. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/utils/validation.py +74 -0
  14. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/PKG-INFO +38 -5
  15. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/SOURCES.txt +6 -0
  16. vaultkit-0.3.0/vaultkit.egg-info/entry_points.txt +2 -0
  17. vaultkit-0.3.0/vaultkit.egg-info/requires.txt +4 -0
  18. vaultkit-0.1.1/vaultkit.egg-info/requires.txt +0 -1
  19. {vaultkit-0.1.1 → vaultkit-0.3.0}/setup.cfg +0 -0
  20. {vaultkit-0.1.1 → vaultkit-0.3.0}/tests/test_fetch.py +0 -0
  21. {vaultkit-0.1.1 → vaultkit-0.3.0}/tests/test_query.py +0 -0
  22. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/core/__init__.py +0 -0
  23. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/core/http.py +0 -0
  24. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/core/polling.py +0 -0
  25. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/errors/__init__.py +0 -0
  26. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/errors/base.py +0 -0
  27. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/errors/exceptions.py +0 -0
  28. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/__init__.py +0 -0
  29. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/dataset_info.py +0 -0
  30. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/dataset_schema.py +0 -0
  31. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/fetch_result.py +0 -0
  32. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/models/query_result.py +0 -0
  33. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/__init__.py +0 -0
  34. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/adapters/__init__.py +0 -0
  35. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/adapters/anthropic.py +0 -0
  36. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/adapters/openai.py +0 -0
  37. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/builder.py +0 -0
  38. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/tools/schemas.py +0 -0
  39. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/utils/__init__.py +0 -0
  40. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit/utils/retry.py +0 -0
  41. {vaultkit-0.1.1 → vaultkit-0.3.0}/vaultkit.egg-info/dependency_links.txt +0 -0
  42. {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.1.1
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.8
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.1.1"
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.8"
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"
@@ -53,7 +53,7 @@ from .models import (
53
53
  # Optional (nice DX improvement)
54
54
  from .tools import ToolBuilder, ToolExecutor
55
55
 
56
- __version__ = "0.1.0"
56
+ __version__ = "0.3.0"
57
57
 
58
58
  __all__ = [
59
59
  "VaultKitClient",
@@ -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.1.1
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.8
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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vaultkit-mcp = vaultkit.mcp.__main__:main
@@ -0,0 +1,4 @@
1
+ httpx>=0.24.0
2
+
3
+ [mcp]
4
+ mcp<2,>=1.27
@@ -1 +0,0 @@
1
- httpx>=0.24.0
File without changes
File without changes
File without changes
File without changes