contexory-sdk 0.1.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.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .coverage
5
+ *.egg-info/
6
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Contexory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.5
2
+ Name: contexory-sdk
3
+ Version: 0.1.0
4
+ Summary: Resolve a Contexory agent's released context at runtime: cached, signature-verified, and safe when Contexory is unreachable.
5
+ Project-URL: Homepage, https://github.com/wedgey/contexory/tree/main/packages/sdk-python#readme
6
+ Project-URL: Repository, https://github.com/wedgey/contexory
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: agent-skills,ai-agents,claude-agent-sdk,contexory,langchain,prompt-management,pydantic-ai
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: cryptography>=42
15
+ Provides-Extra: claude-agent-sdk
16
+ Requires-Dist: claude-agent-sdk>=0.1; extra == 'claude-agent-sdk'
17
+ Provides-Extra: dev
18
+ Requires-Dist: claude-agent-sdk>=0.1; extra == 'dev'
19
+ Requires-Dist: langchain>=1.0; extra == 'dev'
20
+ Requires-Dist: opentelemetry-api>=1.25; extra == 'dev'
21
+ Requires-Dist: opentelemetry-sdk>=1.25; extra == 'dev'
22
+ Requires-Dist: pydantic-ai-slim>=1.0; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Requires-Dist: ruff>=0.6; extra == 'dev'
25
+ Provides-Extra: langchain
26
+ Requires-Dist: langchain>=1.0; extra == 'langchain'
27
+ Provides-Extra: otel
28
+ Requires-Dist: opentelemetry-api>=1.25; extra == 'otel'
29
+ Requires-Dist: opentelemetry-sdk>=1.25; extra == 'otel'
30
+ Provides-Extra: pydantic-ai
31
+ Requires-Dist: pydantic-ai-slim>=1.0; extra == 'pydantic-ai'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # contexory-sdk (Python)
35
+
36
+ Resolve a Contexory agent's released context at runtime: the system prompt, skills and tool
37
+ descriptions under a label such as `production`. Cached, signature-verified, and safe when Contexory is
38
+ unreachable. It is the Python twin of [`@contexory/sdk`](../sdk): same wire format, same cache layout,
39
+ same failure rules, and a shared parity suite that fails if they drift.
40
+
41
+ ```bash
42
+ pip install contexory-sdk # resolve + apply_tools
43
+ pip install 'contexory-sdk[otel]' # instrument()
44
+ pip install 'contexory-sdk[langchain]' # + adapters, per framework
45
+ ```
46
+
47
+ Python 3.10 or newer. The only hard dependency is `cryptography`, for Ed25519.
48
+
49
+ ## resolve
50
+
51
+ ```python
52
+ from contexory import Contexory
53
+
54
+ contexory = Contexory() # CONTEXORY_API_TOKEN holds a con_rt_ runtime key
55
+ ctx = contexory.resolve("support-agent") # label defaults to "production"
56
+
57
+ system = ctx.system
58
+ ctx.release.number # 43
59
+ ctx.source # "network" | "cache" | "stale-cache"
60
+ ctx.stale # True when Contexory failed and this is the last verified release
61
+ ```
62
+
63
+ `await contexory.aresolve(...)` runs the same call off the event loop.
64
+
65
+ - Every response is verified with Ed25519 against the keys at `/api/v1/signing-keys`. A bad or missing
66
+ signature raises `ContexorySignatureError` and never falls back to the cache.
67
+ - A refusal (401, 403, 404) raises `ContexoryResolveError` with `.status`. Any other failure serves the
68
+ last verified release and sets `ctx.stale` and `ctx.error`; with nothing cached it raises.
69
+ - Releases are cached on disk (`CONTEXORY_CACHE_DIR`, default `~/.cache/contexory/sdk`) for `ttl_ms`
70
+ (60s), then revalidated with `If-None-Match`. The layout is byte-compatible with the TypeScript SDK's.
71
+ - Missing key: `resolve` raises immediately. The TypeScript SDK would serve a cache written under the same
72
+ empty key; there is no such cache to serve.
73
+
74
+ ## apply_tools
75
+
76
+ ```python
77
+ tools = ctx.apply_tools(my_tools) # dict keyed by name, or list of tools
78
+ tools = ctx.apply_tools(my_tools, renames={"refund": "issue_refund"})
79
+ ctx.original_tool_name("issue_refund") # "refund"
80
+ ```
81
+
82
+ Tools may be dicts, dataclasses, Pydantic models or plain objects with `name`/`description`. Hosted tools
83
+ (no `description`) pass through untouched.
84
+
85
+ ## Upstream MCP descriptions
86
+
87
+ A toolset override can be pinned to the MCP server it overrides (`upstream: {server, hash}` in `tools.json`).
88
+ `apply_tools` then applies it only while the server's description still hashes to the one Contexory
89
+ accepted; otherwise the server's own text stands and the tool is listed in `held` (`ctx.held` after
90
+ `ctx.apply_tools`).
91
+
92
+ ```python
93
+ from contexory import watch_mcp_tools
94
+
95
+ watch = watch_mcp_tools(
96
+ server="zendesk", # the name the toolset uses in upstream.server
97
+ list_tools=lambda: {"tools": mcp.list_tools(), "ttlMs": None}, # your MCP client's tools/list
98
+ report={"ingest_key": "con_ing_...", "agent": "support-agent", "release": ctx.release.number},
99
+ )
100
+ watch.start() # polls on the server's ttlMs, clamped to 5s..1h (default 5min)
101
+ tools = ctx.apply_tools(my_tools, upstream_hashes=watch.latest())
102
+ ```
103
+
104
+ `hash_mcp_tools`, `mcp_tools_hash`, `description_hash`, `schema_hash` and `canonical_json` are the same
105
+ hashes the server and the TypeScript SDK compute, held to `packages/shared/src/mcp-upstream/vectors.json`.
106
+ `report_upstream_hashes` posts with a write-only `con_ing_` ingest key, never a runtime key.
107
+
108
+ ## instrument
109
+
110
+ ```python
111
+ from opentelemetry.sdk.trace import TracerProvider
112
+ from contexory import instrument
113
+
114
+ handle = instrument(ctx) # or instrument(43), instrument("r43")
115
+ provider = TracerProvider()
116
+ provider.add_span_processor(handle.processor)
117
+
118
+ with handle.run_with_release(44): # per-request scope; concurrent scopes never cross
119
+ ...
120
+ ```
121
+
122
+ Stamps `gen_ai.agent.version` and the Langfuse, LangSmith and Braintrust metadata attributes on every
123
+ span. `vendors=[]` writes only `gen_ai.agent.version`; `baggage=True` carries the scope in W3C baggage.
124
+
125
+ ## Adapters
126
+
127
+ | Framework | Use |
128
+ |---|---|
129
+ | LangChain / LangGraph | `create_agent(model, tools, middleware=[ContexoryMiddleware(ctx)])` from `contexory.adapters.langchain`; implements `wrap_model_call`. `apply_system=True` also swaps in the released system prompt |
130
+ | Pydantic AI | `Agent(model, prepare_tools=prepare_tools(ctx))` from `contexory.adapters.pydantic_ai`; descriptions only |
131
+ | Claude Agent SDK | `create_proxy_server(ctx, upstream_tools)` from `contexory.adapters.claude_agent_sdk` returns an in-process MCP server that re-exposes external tools with the released descriptions and forwards calls. Pass it in `mcp_servers` and disallow the originals; the model sees `mcp__contexory__<tool>` |
132
+
133
+ ## Develop
134
+
135
+ ```bash
136
+ cd packages/sdk-python
137
+ python -m venv .venv && . .venv/bin/activate
138
+ pip install -e '.[dev]'
139
+ ruff check . && ruff format --check . && pytest
140
+ ```
141
+
142
+ `parity/fixtures.json` is generated by the TypeScript SDK (`UPDATE_PARITY=1 pnpm exec vitest run --project
143
+ sdk-python`) and replayed by `tests/test_parity.py`. Change `parity/cases.ts`, regenerate, and both suites
144
+ must pass. `pnpm test` runs the TypeScript half.
145
+
146
+ ## Releasing
147
+
148
+ Releases go to PyPI as `contexory-sdk` through trusted publishing (OIDC): no token exists. The
149
+ `publish` job in `.github/workflows/sdk-python.yml` runs only for a pushed `sdk-python-v*` tag, after
150
+ the test matrix and build pass.
151
+
152
+ 1. Bump `version` in `pyproject.toml` and merge it to `main`.
153
+ 2. Tag that commit and push the tag. The tag must equal the version, or the job fails:
154
+
155
+ ```bash
156
+ git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0
157
+ ```
158
+
159
+ One-time setup (pending trusted publisher on PyPI, GitHub environment `pypi`):
160
+ [`docs/RELEASING.md`](../../docs/RELEASING.md#python-sdk-pypi).
@@ -0,0 +1,127 @@
1
+ # contexory-sdk (Python)
2
+
3
+ Resolve a Contexory agent's released context at runtime: the system prompt, skills and tool
4
+ descriptions under a label such as `production`. Cached, signature-verified, and safe when Contexory is
5
+ unreachable. It is the Python twin of [`@contexory/sdk`](../sdk): same wire format, same cache layout,
6
+ same failure rules, and a shared parity suite that fails if they drift.
7
+
8
+ ```bash
9
+ pip install contexory-sdk # resolve + apply_tools
10
+ pip install 'contexory-sdk[otel]' # instrument()
11
+ pip install 'contexory-sdk[langchain]' # + adapters, per framework
12
+ ```
13
+
14
+ Python 3.10 or newer. The only hard dependency is `cryptography`, for Ed25519.
15
+
16
+ ## resolve
17
+
18
+ ```python
19
+ from contexory import Contexory
20
+
21
+ contexory = Contexory() # CONTEXORY_API_TOKEN holds a con_rt_ runtime key
22
+ ctx = contexory.resolve("support-agent") # label defaults to "production"
23
+
24
+ system = ctx.system
25
+ ctx.release.number # 43
26
+ ctx.source # "network" | "cache" | "stale-cache"
27
+ ctx.stale # True when Contexory failed and this is the last verified release
28
+ ```
29
+
30
+ `await contexory.aresolve(...)` runs the same call off the event loop.
31
+
32
+ - Every response is verified with Ed25519 against the keys at `/api/v1/signing-keys`. A bad or missing
33
+ signature raises `ContexorySignatureError` and never falls back to the cache.
34
+ - A refusal (401, 403, 404) raises `ContexoryResolveError` with `.status`. Any other failure serves the
35
+ last verified release and sets `ctx.stale` and `ctx.error`; with nothing cached it raises.
36
+ - Releases are cached on disk (`CONTEXORY_CACHE_DIR`, default `~/.cache/contexory/sdk`) for `ttl_ms`
37
+ (60s), then revalidated with `If-None-Match`. The layout is byte-compatible with the TypeScript SDK's.
38
+ - Missing key: `resolve` raises immediately. The TypeScript SDK would serve a cache written under the same
39
+ empty key; there is no such cache to serve.
40
+
41
+ ## apply_tools
42
+
43
+ ```python
44
+ tools = ctx.apply_tools(my_tools) # dict keyed by name, or list of tools
45
+ tools = ctx.apply_tools(my_tools, renames={"refund": "issue_refund"})
46
+ ctx.original_tool_name("issue_refund") # "refund"
47
+ ```
48
+
49
+ Tools may be dicts, dataclasses, Pydantic models or plain objects with `name`/`description`. Hosted tools
50
+ (no `description`) pass through untouched.
51
+
52
+ ## Upstream MCP descriptions
53
+
54
+ A toolset override can be pinned to the MCP server it overrides (`upstream: {server, hash}` in `tools.json`).
55
+ `apply_tools` then applies it only while the server's description still hashes to the one Contexory
56
+ accepted; otherwise the server's own text stands and the tool is listed in `held` (`ctx.held` after
57
+ `ctx.apply_tools`).
58
+
59
+ ```python
60
+ from contexory import watch_mcp_tools
61
+
62
+ watch = watch_mcp_tools(
63
+ server="zendesk", # the name the toolset uses in upstream.server
64
+ list_tools=lambda: {"tools": mcp.list_tools(), "ttlMs": None}, # your MCP client's tools/list
65
+ report={"ingest_key": "con_ing_...", "agent": "support-agent", "release": ctx.release.number},
66
+ )
67
+ watch.start() # polls on the server's ttlMs, clamped to 5s..1h (default 5min)
68
+ tools = ctx.apply_tools(my_tools, upstream_hashes=watch.latest())
69
+ ```
70
+
71
+ `hash_mcp_tools`, `mcp_tools_hash`, `description_hash`, `schema_hash` and `canonical_json` are the same
72
+ hashes the server and the TypeScript SDK compute, held to `packages/shared/src/mcp-upstream/vectors.json`.
73
+ `report_upstream_hashes` posts with a write-only `con_ing_` ingest key, never a runtime key.
74
+
75
+ ## instrument
76
+
77
+ ```python
78
+ from opentelemetry.sdk.trace import TracerProvider
79
+ from contexory import instrument
80
+
81
+ handle = instrument(ctx) # or instrument(43), instrument("r43")
82
+ provider = TracerProvider()
83
+ provider.add_span_processor(handle.processor)
84
+
85
+ with handle.run_with_release(44): # per-request scope; concurrent scopes never cross
86
+ ...
87
+ ```
88
+
89
+ Stamps `gen_ai.agent.version` and the Langfuse, LangSmith and Braintrust metadata attributes on every
90
+ span. `vendors=[]` writes only `gen_ai.agent.version`; `baggage=True` carries the scope in W3C baggage.
91
+
92
+ ## Adapters
93
+
94
+ | Framework | Use |
95
+ |---|---|
96
+ | LangChain / LangGraph | `create_agent(model, tools, middleware=[ContexoryMiddleware(ctx)])` from `contexory.adapters.langchain`; implements `wrap_model_call`. `apply_system=True` also swaps in the released system prompt |
97
+ | Pydantic AI | `Agent(model, prepare_tools=prepare_tools(ctx))` from `contexory.adapters.pydantic_ai`; descriptions only |
98
+ | Claude Agent SDK | `create_proxy_server(ctx, upstream_tools)` from `contexory.adapters.claude_agent_sdk` returns an in-process MCP server that re-exposes external tools with the released descriptions and forwards calls. Pass it in `mcp_servers` and disallow the originals; the model sees `mcp__contexory__<tool>` |
99
+
100
+ ## Develop
101
+
102
+ ```bash
103
+ cd packages/sdk-python
104
+ python -m venv .venv && . .venv/bin/activate
105
+ pip install -e '.[dev]'
106
+ ruff check . && ruff format --check . && pytest
107
+ ```
108
+
109
+ `parity/fixtures.json` is generated by the TypeScript SDK (`UPDATE_PARITY=1 pnpm exec vitest run --project
110
+ sdk-python`) and replayed by `tests/test_parity.py`. Change `parity/cases.ts`, regenerate, and both suites
111
+ must pass. `pnpm test` runs the TypeScript half.
112
+
113
+ ## Releasing
114
+
115
+ Releases go to PyPI as `contexory-sdk` through trusted publishing (OIDC): no token exists. The
116
+ `publish` job in `.github/workflows/sdk-python.yml` runs only for a pushed `sdk-python-v*` tag, after
117
+ the test matrix and build pass.
118
+
119
+ 1. Bump `version` in `pyproject.toml` and merge it to `main`.
120
+ 2. Tag that commit and push the tag. The tag must equal the version, or the job fails:
121
+
122
+ ```bash
123
+ git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0
124
+ ```
125
+
126
+ One-time setup (pending trusted publisher on PyPI, GitHub environment `pypi`):
127
+ [`docs/RELEASING.md`](../../docs/RELEASING.md#python-sdk-pypi).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "contexory-sdk"
7
+ version = "0.1.0"
8
+ description = "Resolve a Contexory agent's released context at runtime: cached, signature-verified, and safe when Contexory is unreachable."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["contexory", "ai-agents", "agent-skills", "prompt-management", "langchain", "pydantic-ai", "claude-agent-sdk"]
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Typing :: Typed",
17
+ ]
18
+ dependencies = ["cryptography>=42"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/wedgey/contexory/tree/main/packages/sdk-python#readme"
22
+ Repository = "https://github.com/wedgey/contexory"
23
+
24
+ [project.optional-dependencies]
25
+ otel = ["opentelemetry-api>=1.25", "opentelemetry-sdk>=1.25"]
26
+ langchain = ["langchain>=1.0"]
27
+ pydantic-ai = ["pydantic-ai-slim>=1.0"]
28
+ claude-agent-sdk = ["claude-agent-sdk>=0.1"]
29
+ dev = [
30
+ "pytest>=8",
31
+ "ruff>=0.6",
32
+ "opentelemetry-api>=1.25",
33
+ "opentelemetry-sdk>=1.25",
34
+ "langchain>=1.0",
35
+ "pydantic-ai-slim>=1.0",
36
+ "claude-agent-sdk>=0.1",
37
+ ]
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/contexory"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src/contexory", "README.md", "LICENSE"]
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+ addopts = "-q"
48
+
49
+ [tool.ruff]
50
+ line-length = 120
51
+ target-version = "py310"
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "B", "UP"]
55
+
56
+ [tool.ruff.lint.isort]
57
+ known-first-party = ["contexory", "conftest"]
@@ -0,0 +1,67 @@
1
+ from .apply_tools import AppliedTools, apply_tools
2
+ from .client import (
3
+ DEFAULT_BASE_URL,
4
+ DEFAULT_LABEL,
5
+ Contexory,
6
+ FetchResponse,
7
+ ResolvedContext,
8
+ default_fetch,
9
+ )
10
+ from .errors import ContexoryResolveError, ContexorySignatureError
11
+ from .instrument import (
12
+ AGENT_VERSION_ATTRIBUTE,
13
+ RELEASE_BAGGAGE_KEY,
14
+ VENDOR_ATTRIBUTES,
15
+ VENDORS,
16
+ Instrumentation,
17
+ instrument,
18
+ open_inference_metadata,
19
+ open_llmetry_association_properties,
20
+ openai_agents_trace_metadata,
21
+ )
22
+ from .mcp_upstream import (
23
+ canonical_json,
24
+ clamp_ttl,
25
+ description_hash,
26
+ hash_mcp_tools,
27
+ mcp_tools_hash,
28
+ report_upstream_hashes,
29
+ schema_hash,
30
+ watch_mcp_tools,
31
+ )
32
+ from .release import normalize_release
33
+ from .types import ResolvedTool, ResolvePayload, Upstream
34
+
35
+ __all__ = [
36
+ "AGENT_VERSION_ATTRIBUTE",
37
+ "DEFAULT_BASE_URL",
38
+ "DEFAULT_LABEL",
39
+ "RELEASE_BAGGAGE_KEY",
40
+ "VENDORS",
41
+ "VENDOR_ATTRIBUTES",
42
+ "AppliedTools",
43
+ "Contexory",
44
+ "ContexoryResolveError",
45
+ "ContexorySignatureError",
46
+ "FetchResponse",
47
+ "Instrumentation",
48
+ "ResolvePayload",
49
+ "ResolvedContext",
50
+ "ResolvedTool",
51
+ "Upstream",
52
+ "canonical_json",
53
+ "clamp_ttl",
54
+ "description_hash",
55
+ "hash_mcp_tools",
56
+ "mcp_tools_hash",
57
+ "report_upstream_hashes",
58
+ "schema_hash",
59
+ "watch_mcp_tools",
60
+ "apply_tools",
61
+ "default_fetch",
62
+ "instrument",
63
+ "normalize_release",
64
+ "open_inference_metadata",
65
+ "open_llmetry_association_properties",
66
+ "openai_agents_trace_metadata",
67
+ ]
File without changes
@@ -0,0 +1,58 @@
1
+ """Claude Agent SDK: an in-process MCP proxy that re-exposes external tools with released descriptions.
2
+
3
+ Claude Code's built-in tools and external MCP servers describe themselves; their
4
+ descriptions cannot be rewritten in flight. The proxy registers each upstream tool again
5
+ under an in-process MCP server with the released description and forwards the call, so
6
+ the model reads Contexory's text. Name the proxy server so the model sees
7
+ `mcp__<server>__<tool>` and disallow the upstream originals with `disallowed_tools`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Awaitable, Callable, Iterable, Mapping
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class UpstreamTool:
19
+ name: str
20
+ description: str
21
+ input_schema: Any
22
+ #: Forwards one call: `await call(args)` returns the MCP result dict (`{"content": [...]}`).
23
+ call: Callable[[Mapping[str, Any]], Awaitable[dict[str, Any]]]
24
+
25
+
26
+ def proxied_tools(context: Any, upstream: Iterable[UpstreamTool], *, include_unmatched: bool = True) -> list[Any]:
27
+ """`UpstreamTool`s with the released description applied.
28
+
29
+ A tool with no released description is kept as-is unless `include_unmatched=False`.
30
+ """
31
+ tools = list(upstream)
32
+ applied = context.apply_tools({t.name: {"name": t.name, "description": t.description} for t in tools})
33
+ out: list[UpstreamTool] = []
34
+ released = {t.name for t in context.tools}
35
+ for tool in tools:
36
+ if tool.name not in released and not include_unmatched:
37
+ continue
38
+ description = applied[tool.name]["description"]
39
+ out.append(UpstreamTool(tool.name, description, tool.input_schema, tool.call))
40
+ return out
41
+
42
+
43
+ def create_proxy_server(
44
+ context: Any, upstream: Iterable[UpstreamTool], *, name: str = "contexory", include_unmatched: bool = True
45
+ ) -> Any:
46
+ """An SDK MCP server config for `ClaudeAgentOptions(mcp_servers={name: server})`."""
47
+ try:
48
+ from claude_agent_sdk import create_sdk_mcp_server, tool
49
+ except ImportError as error:
50
+ raise ImportError(
51
+ "create_proxy_server() needs the Claude Agent SDK. pip install 'contexory-sdk[claude-agent-sdk]'"
52
+ ) from error
53
+
54
+ wrapped = [
55
+ tool(t.name, t.description, t.input_schema)(t.call)
56
+ for t in proxied_tools(context, upstream, include_unmatched=include_unmatched)
57
+ ]
58
+ return create_sdk_mcp_server(name=name, tools=wrapped)
@@ -0,0 +1,36 @@
1
+ """LangChain / LangGraph: apply released tool descriptions and the system prompt per model call."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ try:
8
+ from langchain.agents.middleware import AgentMiddleware as _Base
9
+ except ImportError: # pragma: no cover - the adapter still works duck-typed against any object with `override`
10
+ _Base = object # type: ignore[assignment,misc]
11
+
12
+
13
+ class ContexoryMiddleware(_Base): # type: ignore[misc,valid-type]
14
+ """Agent middleware: `create_agent(model, tools, middleware=[ContexoryMiddleware(ctx)])`.
15
+
16
+ Rewrites each model request's tools to the released descriptions. `apply_system=True`
17
+ also replaces the system message with the release's system prompt.
18
+ """
19
+
20
+ def __init__(self, context: Any, *, apply_system: bool = False):
21
+ self.context = context
22
+ self.apply_system = apply_system
23
+
24
+ def _rewrite(self, request: Any) -> Any:
25
+ overrides: dict[str, Any] = {}
26
+ if request.tools:
27
+ overrides["tools"] = self.context.apply_tools(list(request.tools))
28
+ if self.apply_system:
29
+ overrides["system_prompt"] = self.context.system
30
+ return request.override(**overrides) if overrides else request
31
+
32
+ def wrap_model_call(self, request: Any, handler: Any) -> Any:
33
+ return handler(self._rewrite(request))
34
+
35
+ async def awrap_model_call(self, request: Any, handler: Any) -> Any:
36
+ return await handler(self._rewrite(request))
@@ -0,0 +1,27 @@
1
+ """Pydantic AI: apply released tool descriptions through `prepare_tools`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ from typing import Any
7
+
8
+ from ..apply_tools import describe_tool
9
+
10
+
11
+ def prepare_tools(context: Any) -> Any:
12
+ """`Agent(model, prepare_tools=prepare_tools(ctx))`.
13
+
14
+ Descriptions only: Pydantic AI routes a call back by the tool name it registered,
15
+ so renames are not offered here.
16
+ """
17
+ released = {tool.name: tool for tool in context.tools}
18
+
19
+ async def prepare(_run_context: Any, tool_defs: list[Any]) -> list[Any]:
20
+ return [
21
+ dataclasses.replace(definition, description=describe_tool(released[definition.name]))
22
+ if definition.name in released
23
+ else definition
24
+ for definition in tool_defs
25
+ ]
26
+
27
+ return prepare