api-mapper-client 1.0.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,397 @@
1
+ ## Ignore Visual Studio temporary files, build results, and
2
+ ## files generated by popular Visual Studio add-ons.
3
+ ##
4
+ ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5
+
6
+ # User-specific files
7
+ *.rsuser
8
+ *.suo
9
+ *.user
10
+ *.userosscache
11
+ *.sln.docstates
12
+
13
+ # User-specific files (MonoDevelop/Xamarin Studio)
14
+ *.userprefs
15
+
16
+ # Mono auto generated files
17
+ mono_crash.*
18
+
19
+ # Build results
20
+ [Dd]ebug/
21
+ [Dd]ebugPublic/
22
+ [Rr]elease/
23
+ [Rr]eleases/
24
+ x64/
25
+ x86/
26
+ [Ww][Ii][Nn]32/
27
+ [Aa][Rr][Mm]/
28
+ [Aa][Rr][Mm]64/
29
+ bld/
30
+ [Bb]in/
31
+ [Oo]bj/
32
+ [Ll]og/
33
+ [Ll]ogs/
34
+
35
+ # Visual Studio 2015/2017 cache/options directory
36
+ .vs/
37
+ # Uncomment if you have tasks that create the project's static files in wwwroot
38
+ #wwwroot/
39
+
40
+ # Visual Studio 2017 auto generated files
41
+ Generated\ Files/
42
+
43
+ # MSTest test Results
44
+ [Tt]est[Rr]esult*/
45
+ [Bb]uild[Ll]og.*
46
+
47
+ # NUnit
48
+ *.VisualState.xml
49
+ TestResult.xml
50
+ nunit-*.xml
51
+
52
+ # Build Results of an ATL Project
53
+ [Dd]ebugPS/
54
+ [Rr]eleasePS/
55
+ dlldata.c
56
+
57
+ # Benchmark Results
58
+ BenchmarkDotNet.Artifacts/
59
+
60
+ # .NET Core
61
+ project.lock.json
62
+ project.fragment.lock.json
63
+ artifacts/
64
+
65
+ # ASP.NET Scaffolding
66
+ ScaffoldingReadMe.txt
67
+
68
+ # StyleCop
69
+ StyleCopReport.xml
70
+
71
+ # Files built by Visual Studio
72
+ *_i.c
73
+ *_p.c
74
+ *_h.h
75
+ *.ilk
76
+ *.meta
77
+ *.obj
78
+ *.iobj
79
+ *.pch
80
+ *.pdb
81
+ *.ipdb
82
+ *.pgc
83
+ *.pgd
84
+ *.rsp
85
+ *.sbr
86
+ *.tlb
87
+ *.tli
88
+ *.tlh
89
+ *.tmp
90
+ *.tmp_proj
91
+ *_wpftmp.csproj
92
+ *.log
93
+ *.vspscc
94
+ *.vssscc
95
+ .builds
96
+ *.pidb
97
+ *.svclog
98
+ *.scc
99
+
100
+ # Chutzpah Test files
101
+ _Chutzpah*
102
+
103
+ # Visual C++ cache files
104
+ ipch/
105
+ *.aps
106
+ *.ncb
107
+ *.opendb
108
+ *.opensdf
109
+ *.sdf
110
+ *.cachefile
111
+ *.VC.db
112
+ *.VC.VC.opendb
113
+
114
+ # Visual Studio profiler
115
+ *.psess
116
+ *.vsp
117
+ *.vspx
118
+ *.sap
119
+
120
+ # Visual Studio Trace Files
121
+ *.e2e
122
+
123
+ # TFS 2012 Local Workspace
124
+ $tf/
125
+
126
+ # Guidance Automation Toolkit
127
+ *.gpState
128
+
129
+ # ReSharper is a .NET coding add-in
130
+ _ReSharper*/
131
+ *.[Rr]e[Ss]harper
132
+ *.DotSettings.user
133
+
134
+ # TeamCity is a build add-in
135
+ _TeamCity*
136
+
137
+ # DotCover is a Code Coverage Tool
138
+ *.dotCover
139
+
140
+ # AxoCover is a Code Coverage Tool
141
+ .axoCover/*
142
+ !.axoCover/settings.json
143
+
144
+ # Coverlet is a free, cross platform Code Coverage Tool
145
+ coverage*.json
146
+ coverage*.xml
147
+ coverage*.info
148
+
149
+ # Visual Studio code coverage results
150
+ *.coverage
151
+ *.coveragexml
152
+
153
+ # NCrunch
154
+ _NCrunch_*
155
+ .*crunch*.local.xml
156
+ nCrunchTemp_*
157
+
158
+ # MightyMoose
159
+ *.mm.*
160
+ AutoTest.Net/
161
+
162
+ # Web workbench (sass)
163
+ .sass-cache/
164
+
165
+ # Installshield output folder
166
+ [Ee]xpress/
167
+
168
+ # DocProject is a documentation generator add-in
169
+ DocProject/buildhelp/
170
+ DocProject/Help/*.HxT
171
+ DocProject/Help/*.HxC
172
+ DocProject/Help/*.hhc
173
+ DocProject/Help/*.hhk
174
+ DocProject/Help/*.hhp
175
+ DocProject/Help/Html2
176
+ DocProject/Help/html
177
+
178
+ # Click-Once directory
179
+ publish/
180
+
181
+ # Publish Web Output
182
+ *.[Pp]ublish.xml
183
+ *.azurePubxml
184
+ # Note: Comment the next line if you want to checkin your web deploy settings,
185
+ # but database connection strings (with potential passwords) will be unencrypted
186
+ *.pubxml
187
+ *.publishproj
188
+
189
+ # Microsoft Azure Web App publish settings. Comment the next line if you want to
190
+ # checkin your Azure Web App publish settings, but sensitive information contained
191
+ # in these scripts will be unencrypted
192
+ PublishScripts/
193
+
194
+ # Local development secrets and environment files
195
+ .env
196
+ .env.*
197
+ !.env.example
198
+ !**/.env.example
199
+ !.env.*.example
200
+ !**/.env.*.example
201
+ set-platform-development-secrets.sh
202
+
203
+ # Node / Next.js
204
+ node_modules/
205
+ .next/
206
+ out/
207
+ npm-debug.log*
208
+ yarn-debug.log*
209
+ yarn-error.log*
210
+ pnpm-debug.log*
211
+
212
+ # Generated public site export
213
+ src/PublicSite/CodedProjects.AI.ApiMapper.Public.Web/wwwroot/
214
+
215
+ # NuGet Packages
216
+ *.nupkg
217
+ # NuGet Symbol Packages
218
+ *.snupkg
219
+ # The packages folder can be ignored because of Package Restore
220
+ **/[Pp]ackages/*
221
+ # except build/, which is used as an MSBuild target.
222
+ !**/[Pp]ackages/build/
223
+ # Uncomment if necessary however generally it will be regenerated when needed
224
+ #!**/[Pp]ackages/repositories.config
225
+ # NuGet v3's project.json files produces more ignorable files
226
+ *.nuget.props
227
+ *.nuget.targets
228
+
229
+ # Microsoft Azure Build Output
230
+ csx/
231
+ *.build.csdef
232
+
233
+ # Microsoft Azure Emulator
234
+ ecf/
235
+ rcf/
236
+
237
+ # Windows Store app package directories and files
238
+ AppPackages/
239
+ BundleArtifacts/
240
+ Package.StoreAssociation.xml
241
+ _pkginfo.txt
242
+ *.appx
243
+ *.appxbundle
244
+ *.appxupload
245
+
246
+ # Visual Studio cache files
247
+ # files ending in .cache can be ignored
248
+ *.[Cc]ache
249
+ # but keep track of directories ending in .cache
250
+ !?*.[Cc]ache/
251
+
252
+ # Others
253
+ ClientBin/
254
+ ~$*
255
+ *~
256
+ *.dbmdl
257
+ *.dbproj.schemaview
258
+ *.jfm
259
+ *.pfx
260
+ *.publishsettings
261
+ orleans.codegen.cs
262
+
263
+ # Including strong name files can present a security risk
264
+ # (https://github.com/github/gitignore/pull/2483#issue-259490424)
265
+ #*.snk
266
+
267
+ # Since there are multiple workflows, uncomment next line to ignore bower_components
268
+ # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
269
+ #bower_components/
270
+
271
+ # RIA/Silverlight projects
272
+ Generated_Code/
273
+
274
+ # Backup & report files from converting an old project file
275
+ # to a newer Visual Studio version. Backup files are not needed,
276
+ # because we have git ;-)
277
+ _UpgradeReport_Files/
278
+ Backup*/
279
+ UpgradeLog*.XML
280
+ UpgradeLog*.htm
281
+ ServiceFabricBackup/
282
+ *.rptproj.bak
283
+
284
+ # SQL Server files
285
+ *.mdf
286
+ *.ldf
287
+ *.ndf
288
+
289
+ # Business Intelligence projects
290
+ *.rdl.data
291
+ *.bim.layout
292
+ *.bim_*.settings
293
+ *.rptproj.rsuser
294
+ *- [Bb]ackup.rdl
295
+ *- [Bb]ackup ([0-9]).rdl
296
+ *- [Bb]ackup ([0-9][0-9]).rdl
297
+
298
+ # Microsoft Fakes
299
+ FakesAssemblies/
300
+
301
+ # GhostDoc plugin setting file
302
+ *.GhostDoc.xml
303
+
304
+ # Node.js Tools for Visual Studio
305
+ .ntvs_analysis.dat
306
+ node_modules/
307
+
308
+ # Visual Studio 6 build log
309
+ *.plg
310
+
311
+ # Visual Studio 6 workspace options file
312
+ *.opt
313
+
314
+ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
315
+ *.vbw
316
+
317
+ # Visual Studio LightSwitch build output
318
+ **/*.HTMLClient/GeneratedArtifacts
319
+ **/*.DesktopClient/GeneratedArtifacts
320
+ **/*.DesktopClient/ModelManifest.xml
321
+ **/*.Server/GeneratedArtifacts
322
+ **/*.Server/ModelManifest.xml
323
+ _Pvt_Extensions
324
+
325
+ # Paket dependency manager
326
+ .paket/paket.exe
327
+ paket-files/
328
+
329
+ # FAKE - F# Make
330
+ .fake/
331
+
332
+ # CodeRush personal settings
333
+ .cr/personal
334
+
335
+ # Python Tools for Visual Studio (PTVS)
336
+ __pycache__/
337
+ *.pyc
338
+
339
+ # Cake - Uncomment if you are using it
340
+ # tools/**
341
+ # !tools/packages.config
342
+
343
+ # Tabs Studio
344
+ *.tss
345
+
346
+ # Telerik's JustMock configuration file
347
+ *.jmconfig
348
+
349
+ # BizTalk build output
350
+ *.btp.cs
351
+ *.btm.cs
352
+ *.odx.cs
353
+ *.xsd.cs
354
+
355
+ # OpenCover UI analysis results
356
+ OpenCover/
357
+
358
+ # Azure Stream Analytics local run output
359
+ ASALocalRun/
360
+
361
+ # MSBuild Binary and Structured Log
362
+ *.binlog
363
+
364
+ # NVidia Nsight GPU debugger configuration file
365
+ *.nvuser
366
+
367
+ # MFractors (Xamarin productivity tool) working folder
368
+ .mfractor/
369
+
370
+ # Local History for Visual Studio
371
+ .localhistory/
372
+
373
+ # BeatPulse healthcheck temp database
374
+ healthchecksdb
375
+
376
+ # Backup folder for Package Reference Convert tool in Visual Studio 2017
377
+ MigrationBackup/
378
+
379
+ # Ionide (cross platform F# VS Code tools) working folder
380
+ .ionide/
381
+
382
+ # Fody - auto-generated XML schema
383
+ FodyWeavers.xsd
384
+
385
+ #sandbox
386
+ .dotnet/
387
+
388
+ #nuget
389
+ .nuget/
390
+
391
+ #macos
392
+ .DS_Store
393
+
394
+ #reactjs
395
+ dist/
396
+ # k6 performance test output
397
+ tests/PerformanceTests/reports/*.json
@@ -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,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,50 @@
1
+ # api-mapper-client
2
+
3
+ Python SDK for discovering and invoking AI ApiMapper Runtime tools from Python applications and agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install api-mapper-client
9
+ ```
10
+
11
+ Optional integrations:
12
+
13
+ ```bash
14
+ pip install "api-mapper-client[langchain]"
15
+ pip install "api-mapper-client[langgraph]"
16
+ pip install "api-mapper-client[all]"
17
+ ```
18
+
19
+ ## What it provides
20
+
21
+ - runtime tool discovery,
22
+ - runtime system prompt loading,
23
+ - tool invocation over MCP-compatible HTTP endpoints,
24
+ - API key, delegated bearer token, and OAuth2 client credentials authentication helpers,
25
+ - optional LangChain and LangGraph integration helpers.
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ import uuid
31
+
32
+ from api_mapper_client import ApiMapperClient, ApiMapperClientOptions
33
+ from api_mapper_client.auth.api_key import ApiKeyCredentialProvider
34
+
35
+ client = ApiMapperClient(
36
+ ApiMapperClientOptions(
37
+ base_url="https://runtime.example.com",
38
+ tenant_id=uuid.UUID("11111111-1111-1111-1111-111111111111"),
39
+ client_id="my-ai-client",
40
+ credentials=ApiKeyCredentialProvider("replace-with-your-api-key"),
41
+ )
42
+ )
43
+
44
+ tools = await client.get_tools()
45
+ print([tool.name for tool in tools])
46
+ ```
47
+
48
+ ## License
49
+
50
+ MIT
@@ -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,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)
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "api-mapper-client"
7
+ version = "1.0.0"
8
+ description = "Client SDK for the CodedProjects AI ApiMapper Runtime"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "Coded Projects" },
13
+ ]
14
+ requires-python = ">=3.12"
15
+ dependencies = [
16
+ "httpx>=0.27.0",
17
+ ]
18
+ keywords = ["ai", "mcp", "apimapper", "llm", "tools", "python", "sdk", "client"]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+ urls = { Homepage = "https://apimapper.ai" }
30
+
31
+ [project.optional-dependencies]
32
+ langchain = [
33
+ "langchain-core>=0.3.0",
34
+ "pydantic>=2.0.0",
35
+ ]
36
+ langgraph = [
37
+ "langgraph>=0.2.0",
38
+ "langchain-core>=0.3.0",
39
+ "pydantic>=2.0.0",
40
+ ]
41
+ all = [
42
+ "langchain-core>=0.3.0",
43
+ "langgraph>=0.2.0",
44
+ "pydantic>=2.0.0",
45
+ ]
46
+
47
+ [tool.hatch.build]
48
+ include = [
49
+ "README.md",
50
+ "LICENSE",
51
+ "api_mapper_client/**/*.py",
52
+ "api_mapper_langchain/**/*.py",
53
+ "api_mapper_langgraph/**/*.py",
54
+ ]
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = [
58
+ "api_mapper_client",
59
+ "api_mapper_langchain",
60
+ "api_mapper_langgraph",
61
+ ]