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.
- api_mapper_client/__init__.py +17 -0
- api_mapper_client/auth/__init__.py +12 -0
- api_mapper_client/auth/api_key.py +13 -0
- api_mapper_client/auth/base.py +7 -0
- api_mapper_client/auth/delegated_bearer.py +18 -0
- api_mapper_client/auth/oauth2_client_credentials.py +71 -0
- api_mapper_client/client.py +220 -0
- api_mapper_client/contracts.py +28 -0
- api_mapper_client-1.0.0.dist-info/METADATA +82 -0
- api_mapper_client-1.0.0.dist-info/RECORD +16 -0
- api_mapper_client-1.0.0.dist-info/WHEEL +4 -0
- api_mapper_client-1.0.0.dist-info/licenses/LICENSE +21 -0
- api_mapper_langchain/__init__.py +3 -0
- api_mapper_langchain/toolkit.py +53 -0
- api_mapper_langgraph/__init__.py +3 -0
- api_mapper_langgraph/nodes.py +25 -0
|
@@ -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,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,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,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,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)
|