api-mapper-client 1.0.0__py3-none-any.whl

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,17 @@
1
+ from .client import ApiMapperClient, ApiMapperClientOptions
2
+ from .auth.api_key import ApiKeyCredentialProvider
3
+ from .auth.delegated_bearer import DelegatedBearerCredentialProvider
4
+ from .auth.oauth2_client_credentials import OAuth2ClientCredentialsProvider, OAuth2ClientCredentialsOptions
5
+ from .contracts import McpTool, McpToolCallResult, McpToolContent
6
+
7
+ __all__ = [
8
+ "ApiMapperClient",
9
+ "ApiMapperClientOptions",
10
+ "ApiKeyCredentialProvider",
11
+ "DelegatedBearerCredentialProvider",
12
+ "OAuth2ClientCredentialsProvider",
13
+ "OAuth2ClientCredentialsOptions",
14
+ "McpTool",
15
+ "McpToolCallResult",
16
+ "McpToolContent",
17
+ ]
@@ -0,0 +1,12 @@
1
+ from .base import RuntimeCredentialProvider
2
+ from .api_key import ApiKeyCredentialProvider
3
+ from .delegated_bearer import DelegatedBearerCredentialProvider
4
+ from .oauth2_client_credentials import OAuth2ClientCredentialsProvider, OAuth2ClientCredentialsOptions
5
+
6
+ __all__ = [
7
+ "RuntimeCredentialProvider",
8
+ "ApiKeyCredentialProvider",
9
+ "DelegatedBearerCredentialProvider",
10
+ "OAuth2ClientCredentialsProvider",
11
+ "OAuth2ClientCredentialsOptions",
12
+ ]
@@ -0,0 +1,13 @@
1
+ from .base import RuntimeCredentialProvider
2
+
3
+
4
+ class ApiKeyCredentialProvider(RuntimeCredentialProvider):
5
+ """Authenticates using a static API key issued from the ApiMapper Portal."""
6
+
7
+ def __init__(self, api_key: str) -> None:
8
+ if not api_key:
9
+ raise ValueError("api_key must not be empty.")
10
+ self._api_key = api_key
11
+
12
+ async def enrich(self, headers: dict[str, str]) -> None:
13
+ headers["X-Api-Key"] = self._api_key
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class RuntimeCredentialProvider(ABC):
5
+ @abstractmethod
6
+ async def enrich(self, headers: dict[str, str]) -> None:
7
+ """Add the appropriate authentication header(s) to the outbound request."""
@@ -0,0 +1,18 @@
1
+ from collections.abc import Callable, Awaitable
2
+ from .base import RuntimeCredentialProvider
3
+
4
+
5
+ class DelegatedBearerCredentialProvider(RuntimeCredentialProvider):
6
+ """
7
+ Forwards a Bearer token obtained from a caller-supplied factory.
8
+ The factory is called on every request — no caching is performed.
9
+ """
10
+
11
+ def __init__(self, token_factory: Callable[[], str | Awaitable[str]]) -> None:
12
+ self._factory = token_factory
13
+
14
+ async def enrich(self, headers: dict[str, str]) -> None:
15
+ import asyncio
16
+ result = self._factory()
17
+ token = await result if asyncio.iscoroutine(result) else result
18
+ headers["Authorization"] = f"Bearer {token}"
@@ -0,0 +1,71 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+ import httpx
4
+ from .base import RuntimeCredentialProvider
5
+
6
+
7
+ @dataclass
8
+ class OAuth2ClientCredentialsOptions:
9
+ client_id: str
10
+ client_secret: str
11
+ scope: str
12
+ authority: str | None = None
13
+ token_endpoint: str | None = None
14
+ expiry_buffer_seconds: int = 30
15
+
16
+ def __post_init__(self) -> None:
17
+ if not self.authority and not self.token_endpoint:
18
+ raise ValueError("Either authority or token_endpoint must be set.")
19
+
20
+
21
+ class OAuth2ClientCredentialsProvider(RuntimeCredentialProvider):
22
+ """Authenticates using the OAuth 2.0 Client Credentials flow with automatic token caching."""
23
+
24
+ def __init__(self, opts: OAuth2ClientCredentialsOptions) -> None:
25
+ self._opts = opts
26
+ self._cached_token: str | None = None
27
+ self._expires_at: float = 0.0
28
+ self._resolved_endpoint: str | None = None
29
+
30
+ async def enrich(self, headers: dict[str, str]) -> None:
31
+ token = await self._get_token()
32
+ headers["Authorization"] = f"Bearer {token}"
33
+
34
+ async def _get_token(self) -> str:
35
+ if self._cached_token and time.monotonic() < self._expires_at:
36
+ return self._cached_token
37
+
38
+ endpoint = await self._resolve_endpoint()
39
+
40
+ async with httpx.AsyncClient() as client:
41
+ response = await client.post(
42
+ endpoint,
43
+ data={
44
+ "grant_type": "client_credentials",
45
+ "client_id": self._opts.client_id,
46
+ "client_secret": self._opts.client_secret,
47
+ "scope": self._opts.scope,
48
+ },
49
+ )
50
+ response.raise_for_status()
51
+ payload = response.json()
52
+
53
+ self._cached_token = payload["access_token"]
54
+ self._expires_at = time.monotonic() + payload["expires_in"] - self._opts.expiry_buffer_seconds
55
+ return self._cached_token
56
+
57
+ async def _resolve_endpoint(self) -> str:
58
+ if self._resolved_endpoint:
59
+ return self._resolved_endpoint
60
+
61
+ if self._opts.token_endpoint:
62
+ self._resolved_endpoint = self._opts.token_endpoint
63
+ return self._resolved_endpoint
64
+
65
+ discovery_url = f"{self._opts.authority!.rstrip('/')}/.well-known/openid-configuration"
66
+ async with httpx.AsyncClient() as client:
67
+ res = await client.get(discovery_url)
68
+ res.raise_for_status()
69
+ self._resolved_endpoint = res.json()["token_endpoint"]
70
+
71
+ return self._resolved_endpoint
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import uuid
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from .auth.base import RuntimeCredentialProvider
11
+ from .contracts import McpTool, McpToolCallResult, McpToolContent
12
+
13
+ MCP_PROTOCOL_VERSION = "2025-03-26"
14
+ JSONRPC_VERSION = "2.0"
15
+
16
+ # Auth header values (Bearer tokens, API keys) are never logged.
17
+ # Tool arguments are not logged to avoid inadvertent PII/secret exposure.
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class ApiMapperClientOptions:
23
+ base_url: str
24
+ tenant_id: uuid.UUID
25
+ client_id: str
26
+ credentials: RuntimeCredentialProvider
27
+ system_prompt_resource_uri: str | None = None
28
+ client_name: str = "ApiMapper.Client"
29
+ client_version: str = "1.0.0"
30
+ cache_tools: bool = False
31
+ cache_system_prompt: bool = False
32
+
33
+
34
+ class ApiMapperClient:
35
+ def __init__(self, opts: ApiMapperClientOptions) -> None:
36
+ self._opts = opts
37
+ mcp_path = f"/runtime/{opts.tenant_id}/{opts.client_id}/mcp"
38
+ self._mcp_url = f"{opts.base_url.rstrip('/')}{mcp_path}"
39
+ self._session_id: str | None = None
40
+ self._initialized = False
41
+ self._cached_tools: list[McpTool] | None = None
42
+ self._cached_prompt: str | None | object = _UNSET
43
+ self._http = httpx.AsyncClient()
44
+
45
+ # ── Public API ────────────────────────────────────────────────────────
46
+
47
+ async def get_system_prompt(self) -> str | None:
48
+ if not self._opts.system_prompt_resource_uri:
49
+ return None
50
+
51
+ if self._opts.cache_system_prompt and self._cached_prompt is not _UNSET:
52
+ logger.debug("System prompt returned from cache.")
53
+ return self._cached_prompt # type: ignore[return-value]
54
+
55
+ logger.debug("Fetching system prompt from resource '%s'.", self._opts.system_prompt_resource_uri)
56
+
57
+ result = await self._send("resources/read", {"uri": self._opts.system_prompt_resource_uri})
58
+ contents = (result or {}).get("contents", [])
59
+ text = "\n\n".join(
60
+ c["text"].strip() for c in contents if c.get("text", "").strip()
61
+ )
62
+ self._cached_prompt = text or None
63
+ logger.info("System prompt fetched (%d chars).", len(self._cached_prompt or ""))
64
+ return self._cached_prompt # type: ignore[return-value]
65
+
66
+ async def get_tools(self) -> list[McpTool]:
67
+ if self._opts.cache_tools and self._cached_tools is not None:
68
+ logger.debug("Tool list returned from cache (%d tools).", len(self._cached_tools))
69
+ return self._cached_tools
70
+
71
+ logger.debug("Fetching tool list from Runtime.")
72
+
73
+ tools: list[McpTool] = []
74
+ cursor: str | None = None
75
+ page = 0
76
+
77
+ while True:
78
+ params: dict[str, Any] = {}
79
+ if cursor:
80
+ params["cursor"] = cursor
81
+ result = await self._send("tools/list", params or None)
82
+ if not result:
83
+ logger.warning("tools/list returned None on page %d; stopping pagination.", page)
84
+ break
85
+ for t in result.get("tools", []):
86
+ tools.append(McpTool(
87
+ name=t["name"],
88
+ description=t.get("description"),
89
+ input_schema=t.get("inputSchema", {}),
90
+ ))
91
+ cursor = result.get("nextCursor")
92
+ page += 1
93
+ if not cursor:
94
+ break
95
+
96
+ self._cached_tools = tools
97
+ logger.info("Loaded %d tools from Runtime.", len(tools))
98
+ return tools
99
+
100
+ async def invoke_tool(self, tool_name: str, arguments: dict[str, Any] | None = None) -> McpToolCallResult | None:
101
+ # Arguments are intentionally not logged to avoid PII/secret exposure.
102
+ logger.debug("Invoking tool '%s'.", tool_name)
103
+
104
+ result = await self._send("tools/call", {"name": tool_name, "arguments": arguments})
105
+ if result is None:
106
+ return None
107
+
108
+ contents = [
109
+ McpToolContent(type=c.get("type", "text"), text=c.get("text"), data=c.get("data"))
110
+ for c in result.get("content", [])
111
+ ]
112
+ call_result = McpToolCallResult(
113
+ content=contents,
114
+ structured_content=result.get("structuredContent"),
115
+ is_error=result.get("isError"),
116
+ )
117
+
118
+ if call_result.is_error:
119
+ logger.warning("Tool '%s' returned is_error=True.", tool_name)
120
+ else:
121
+ logger.debug("Tool '%s' invoked successfully.", tool_name)
122
+
123
+ return call_result
124
+
125
+ def reset_session(self) -> None:
126
+ self._session_id = None
127
+ self._initialized = False
128
+ self._cached_tools = None
129
+ self._cached_prompt = _UNSET
130
+ logger.info("ApiMapper client session reset.")
131
+
132
+ async def close(self) -> None:
133
+ await self._http.aclose()
134
+
135
+ async def __aenter__(self) -> "ApiMapperClient":
136
+ return self
137
+
138
+ async def __aexit__(self, *_: Any) -> None:
139
+ await self.close()
140
+
141
+ # ── MCP internals ─────────────────────────────────────────────────────
142
+
143
+ async def _send(self, method: str, params: Any = None) -> dict[str, Any] | None:
144
+ await self._ensure_initialized()
145
+ return await self._send_core(method, params)
146
+
147
+ async def _ensure_initialized(self) -> None:
148
+ if self._initialized:
149
+ return
150
+
151
+ logger.debug("Sending MCP initialize handshake.")
152
+ result = await self._send_core("initialize", {
153
+ "protocolVersion": MCP_PROTOCOL_VERSION,
154
+ "capabilities": {},
155
+ "clientInfo": {"name": self._opts.client_name, "version": self._opts.client_version},
156
+ }, skip_init=True)
157
+ if result is None:
158
+ raise RuntimeError("MCP initialize failed.")
159
+ await self._notify("notifications/initialized")
160
+ self._initialized = True
161
+ logger.info("MCP session initialized (session: %s).", self._session_id or "none")
162
+
163
+ async def _send_core(self, method: str, params: Any, skip_init: bool = False) -> dict[str, Any] | None:
164
+ if not skip_init:
165
+ await self._ensure_initialized()
166
+
167
+ # Auth headers are added by the credential provider — their values are never logged.
168
+ headers = await self._build_headers()
169
+ body = {
170
+ "jsonrpc": JSONRPC_VERSION,
171
+ "id": str(uuid.uuid4()),
172
+ "method": method,
173
+ "params": params,
174
+ }
175
+
176
+ response = await self._http.post(self._mcp_url, json=body, headers=headers)
177
+ self._capture_session(response)
178
+
179
+ if not response.is_success:
180
+ logger.warning("MCP request '%s' failed with HTTP %d.", method, response.status_code)
181
+ return None
182
+
183
+ envelope = response.json()
184
+ if "error" in envelope:
185
+ err = envelope["error"]
186
+ logger.warning(
187
+ "MCP request '%s' returned JSON-RPC error %s: %s.",
188
+ method, err.get("code"), err.get("message"),
189
+ )
190
+ return None
191
+
192
+ return envelope.get("result")
193
+
194
+ async def _notify(self, method: str, params: Any = None) -> None:
195
+ headers = await self._build_headers()
196
+ body = {"jsonrpc": JSONRPC_VERSION, "method": method, "params": params}
197
+ response = await self._http.post(self._mcp_url, json=body, headers=headers)
198
+ self._capture_session(response)
199
+ if not response.is_success:
200
+ logger.warning("MCP notification '%s' failed with HTTP %d.", method, response.status_code)
201
+
202
+ async def _build_headers(self) -> dict[str, str]:
203
+ headers: dict[str, str] = {
204
+ "Content-Type": "application/json",
205
+ "Accept": "application/json, text/event-stream",
206
+ "MCP-Protocol-Version": MCP_PROTOCOL_VERSION,
207
+ }
208
+ if self._session_id:
209
+ headers["MCP-Session-Id"] = self._session_id
210
+ # Credential provider adds auth headers — values are never logged.
211
+ await self._opts.credentials.enrich(headers)
212
+ return headers
213
+
214
+ def _capture_session(self, response: httpx.Response) -> None:
215
+ sid = response.headers.get("MCP-Session-Id")
216
+ if sid:
217
+ self._session_id = sid
218
+
219
+
220
+ _UNSET = object()
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any
3
+
4
+
5
+ @dataclass
6
+ class McpToolContent:
7
+ type: str
8
+ text: str | None = None
9
+ data: Any = None
10
+
11
+
12
+ @dataclass
13
+ class McpTool:
14
+ name: str
15
+ description: str | None
16
+ input_schema: dict[str, Any]
17
+
18
+
19
+ @dataclass
20
+ class McpToolCallResult:
21
+ content: list[McpToolContent] = field(default_factory=list)
22
+ structured_content: Any = None
23
+ is_error: bool | None = None
24
+
25
+ def to_text(self) -> str:
26
+ return "\n\n".join(
27
+ c.text.strip() for c in self.content if c.type == "text" and c.text and c.text.strip()
28
+ )
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: api-mapper-client
3
+ Version: 1.0.0
4
+ Summary: Client SDK for the CodedProjects AI ApiMapper Runtime
5
+ Project-URL: Homepage, https://apimapper.ai
6
+ Author: Coded Projects
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai,apimapper,client,llm,mcp,python,sdk,tools
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: httpx>=0.27.0
20
+ Provides-Extra: all
21
+ Requires-Dist: langchain-core>=0.3.0; extra == 'all'
22
+ Requires-Dist: langgraph>=0.2.0; extra == 'all'
23
+ Requires-Dist: pydantic>=2.0.0; extra == 'all'
24
+ Provides-Extra: langchain
25
+ Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
26
+ Requires-Dist: pydantic>=2.0.0; extra == 'langchain'
27
+ Provides-Extra: langgraph
28
+ Requires-Dist: langchain-core>=0.3.0; extra == 'langgraph'
29
+ Requires-Dist: langgraph>=0.2.0; extra == 'langgraph'
30
+ Requires-Dist: pydantic>=2.0.0; extra == 'langgraph'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # api-mapper-client
34
+
35
+ Python SDK for discovering and invoking AI ApiMapper Runtime tools from Python applications and agents.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install api-mapper-client
41
+ ```
42
+
43
+ Optional integrations:
44
+
45
+ ```bash
46
+ pip install "api-mapper-client[langchain]"
47
+ pip install "api-mapper-client[langgraph]"
48
+ pip install "api-mapper-client[all]"
49
+ ```
50
+
51
+ ## What it provides
52
+
53
+ - runtime tool discovery,
54
+ - runtime system prompt loading,
55
+ - tool invocation over MCP-compatible HTTP endpoints,
56
+ - API key, delegated bearer token, and OAuth2 client credentials authentication helpers,
57
+ - optional LangChain and LangGraph integration helpers.
58
+
59
+ ## Quick start
60
+
61
+ ```python
62
+ import uuid
63
+
64
+ from api_mapper_client import ApiMapperClient, ApiMapperClientOptions
65
+ from api_mapper_client.auth.api_key import ApiKeyCredentialProvider
66
+
67
+ client = ApiMapperClient(
68
+ ApiMapperClientOptions(
69
+ base_url="https://runtime.example.com",
70
+ tenant_id=uuid.UUID("11111111-1111-1111-1111-111111111111"),
71
+ client_id="my-ai-client",
72
+ credentials=ApiKeyCredentialProvider("replace-with-your-api-key"),
73
+ )
74
+ )
75
+
76
+ tools = await client.get_tools()
77
+ print([tool.name for tool in tools])
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,16 @@
1
+ api_mapper_client/__init__.py,sha256=hmOMfUuQI22zcTQZLsu0ryWTSKHWt4gFBUuurZ9HTmk,634
2
+ api_mapper_client/client.py,sha256=N3xK9JQkH_ArR5UfO7A5XQiYu4uEVoeHB-aapXeFy3c,8405
3
+ api_mapper_client/contracts.py,sha256=qrGenVYueHXMJ5jNLPBB8wBjHABFz4dEdoGR578QxQY,603
4
+ api_mapper_client/auth/__init__.py,sha256=kxHvk9gqQGmiOQVWi7P5rRSkemXrToeuDZ8SjHBE-wc,455
5
+ api_mapper_client/auth/api_key.py,sha256=-D6WDvZ1umZ1oySIoP2pZYMGgzbInDDOOQY78GVWLF0,455
6
+ api_mapper_client/auth/base.py,sha256=rjZ2XuVaewJLBT90lZOfSFUcmY-epYIoohlPmjh8l60,241
7
+ api_mapper_client/auth/delegated_bearer.py,sha256=sHwv9rtYeliDQ4qjMg98Q9Tn0v9SI_97Xd0YQ1JHoe0,684
8
+ api_mapper_client/auth/oauth2_client_credentials.py,sha256=COeppNy_XtMQINHhg-N8iivv5P5Xjyy067xZfBkYb6k,2546
9
+ api_mapper_langchain/__init__.py,sha256=Lgv0INhm9BxAt5gT-XyVRuLT5UtG3yJMd67Em1W81bQ,122
10
+ api_mapper_langchain/toolkit.py,sha256=RiAOn1hrsiYCP_JIZokvfog2VTs32QizfFsB-aK5RTQ,1909
11
+ api_mapper_langgraph/__init__.py,sha256=YbraACO__xbAOow-zQR028tOW8HCskJzQC-TiorXdvE,138
12
+ api_mapper_langgraph/nodes.py,sha256=RwOXnJyq8g0r6-MJFxueNqJ78CSaGSKoUFR0Rst-Rew,856
13
+ api_mapper_client-1.0.0.dist-info/METADATA,sha256=O507UdbwKCdYMC0FHvi-LShXSkeJTA8dKnrVfJGqUb0,2471
14
+ api_mapper_client-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
15
+ api_mapper_client-1.0.0.dist-info/licenses/LICENSE,sha256=eAmhzxjhD-Lp6J1PjVEwoZ_Zf6LVo65nSQt6qzxUSiE,1071
16
+ api_mapper_client-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coded Projects
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,3 @@
1
+ from .toolkit import ApiMapperToolkit, create_api_mapper_tools
2
+
3
+ __all__ = ["ApiMapperToolkit", "create_api_mapper_tools"]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from langchain_core.tools import BaseTool, StructuredTool
4
+ from pydantic import BaseModel, Field, create_model
5
+ from typing import Any
6
+
7
+ from api_mapper_client import ApiMapperClient, McpTool
8
+
9
+
10
+ def _build_pydantic_model(tool: McpTool) -> type[BaseModel]:
11
+ """Builds a Pydantic model from the tool's JSON Schema properties."""
12
+ props: dict[str, Any] = tool.input_schema.get("properties", {})
13
+ required: list[str] = tool.input_schema.get("required", [])
14
+ fields: dict[str, Any] = {}
15
+ for name, schema in props.items():
16
+ desc = schema.get("description", "")
17
+ default = ... if name in required else None
18
+ fields[name] = (Any, Field(default, description=desc))
19
+ return create_model(f"{tool.name}Args", **fields)
20
+
21
+
22
+ def _to_structured_tool(mapper: ApiMapperClient, tool: McpTool) -> StructuredTool:
23
+ model = _build_pydantic_model(tool)
24
+
25
+ async def _run(**kwargs: Any) -> str:
26
+ result = await mapper.invoke_tool(tool.name, kwargs)
27
+ return result.to_text() if result else ""
28
+
29
+ return StructuredTool.from_function(
30
+ coroutine=_run,
31
+ name=tool.name,
32
+ description=tool.description or "",
33
+ args_schema=model,
34
+ )
35
+
36
+
37
+ async def create_api_mapper_tools(mapper: ApiMapperClient) -> list[BaseTool]:
38
+ """Loads all ApiMapper tools and returns them as LangChain StructuredTool instances."""
39
+ tools = await mapper.get_tools()
40
+ return [_to_structured_tool(mapper, t) for t in tools]
41
+
42
+
43
+ class ApiMapperToolkit:
44
+ """Wraps an ApiMapperClient as a LangChain toolkit (lazy-loaded)."""
45
+
46
+ def __init__(self, client: ApiMapperClient) -> None:
47
+ self._client = client
48
+ self._tools: list[BaseTool] | None = None
49
+
50
+ async def get_tools(self) -> list[BaseTool]:
51
+ if self._tools is None:
52
+ self._tools = await create_api_mapper_tools(self._client)
53
+ return self._tools
@@ -0,0 +1,3 @@
1
+ from .nodes import create_api_mapper_tool_node, bind_api_mapper_tools
2
+
3
+ __all__ = ["create_api_mapper_tool_node", "bind_api_mapper_tools"]
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from langgraph.prebuilt import ToolNode
4
+ from langchain_core.language_models import BaseChatModel
5
+
6
+ from api_mapper_client import ApiMapperClient
7
+ from api_mapper_langchain import create_api_mapper_tools
8
+
9
+
10
+ async def create_api_mapper_tool_node(client: ApiMapperClient) -> ToolNode:
11
+ """
12
+ Creates a LangGraph ToolNode backed by ApiMapper tools.
13
+ Place this node in your StateGraph to handle tool call messages.
14
+ """
15
+ tools = await create_api_mapper_tools(client)
16
+ return ToolNode(tools)
17
+
18
+
19
+ async def bind_api_mapper_tools(model: BaseChatModel, client: ApiMapperClient) -> BaseChatModel:
20
+ """
21
+ Binds ApiMapper tools to a LangChain chat model.
22
+ Returns the model with tools bound — pass to an agent node.
23
+ """
24
+ tools = await create_api_mapper_tools(client)
25
+ return model.bind_tools(tools)