smartapi-mcp 0.3.2__tar.gz → 0.5.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.
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/PKG-INFO +57 -17
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/README.md +52 -13
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/pyproject.toml +19 -14
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp/__init__.py +11 -1
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp/biothings.py +199 -12
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp/cli.py +68 -17
- smartapi_mcp-0.5.0/smartapi_mcp/config.py +175 -0
- smartapi_mcp-0.5.0/smartapi_mcp/log.py +42 -0
- smartapi_mcp-0.5.0/smartapi_mcp/openapi.py +426 -0
- smartapi_mcp-0.5.0/smartapi_mcp/server.py +524 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp/smartapi.py +30 -33
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp.egg-info/SOURCES.txt +2 -0
- smartapi_mcp-0.3.2/smartapi_mcp/config.py +0 -119
- smartapi_mcp-0.3.2/smartapi_mcp/server.py +0 -304
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/LICENSE +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/MANIFEST.in +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/requirements-dev.txt +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/requirements.txt +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/setup.cfg +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/setup.py +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp/__main__.py +0 -0
- {smartapi_mcp-0.3.2 → smartapi_mcp-0.5.0}/smartapi_mcp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartapi-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Create MCP servers for one or multiple APIs registered in SmartAPI registry
|
|
5
5
|
Author-email: BioThings Team <help@biothings.io>
|
|
6
6
|
Maintainer-email: BioThings Team <help@biothings.io>
|
|
@@ -25,13 +25,14 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: fastmcp<4,>=3.3.1
|
|
29
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
30
|
+
Requires-Dist: loguru<1,>=0.7.3
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
32
33
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
33
34
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
34
|
-
Requires-Dist: ruff
|
|
35
|
+
Requires-Dist: ruff<0.17,>=0.14; extra == "dev"
|
|
35
36
|
Requires-Dist: build>=0.8.0; extra == "dev"
|
|
36
37
|
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
37
38
|
Provides-Extra: test
|
|
@@ -56,15 +57,17 @@ Create MCP (Model Context Protocol) servers for one or multiple APIs registered
|
|
|
56
57
|
|
|
57
58
|
The SmartAPI MCP Server enables integration between MCP-compatible clients and APIs registered in the SmartAPI registry. This allows for seamless discovery and interaction with bioinformatics and life sciences APIs through standardized MCP protocols.
|
|
58
59
|
|
|
59
|
-
Built on
|
|
60
|
+
Built directly on [FastMCP](https://github.com/jlowin/fastmcp), this project extends MCP support to the extensive collection of APIs available in the SmartAPI registry, with special focus on bioinformatics and life sciences APIs.
|
|
61
|
+
|
|
62
|
+
Earlier releases (through 0.3.2) wrapped the [AWS Labs OpenAPI MCP Server](https://github.com/awslabs/openapi-mcp-server); parts of `smartapi_mcp/openapi.py` are derived from that project's Apache-2.0 code. Dropping that wrapper reduced a clean install from 87 packages / 91 MB to 68 / 56 MB.
|
|
60
63
|
|
|
61
64
|
## Requirements
|
|
62
65
|
|
|
63
66
|
- Python 3.10 or higher
|
|
64
67
|
- Network access to SmartAPI registry (https://smart-api.info)
|
|
65
|
-
- Dependencies: `
|
|
66
|
-
|
|
67
|
-
`pyproject.toml`)
|
|
68
|
+
- Dependencies: `fastmcp>=3.3.1,<4`, `httpx`, `loguru` (fastmcp 4.x is not yet
|
|
69
|
+
supported — it moves to the MCP 2.x SDK and `httpx2`; see the dependency notes
|
|
70
|
+
in `pyproject.toml`)
|
|
68
71
|
|
|
69
72
|
## Features
|
|
70
73
|
|
|
@@ -247,6 +250,41 @@ inspect each BioThings spec at startup and serve any API that has extra
|
|
|
247
250
|
endpoints with faithful per-API tools instead — at the cost of slower startup
|
|
248
251
|
(it downloads the specs upfront).
|
|
249
252
|
|
|
253
|
+
#### Tool search (`--tool-search`)
|
|
254
|
+
|
|
255
|
+
The facade solves the tool explosion for BioThings APIs. `--tool-search` solves
|
|
256
|
+
it for everything else, including per-API tools in a hybrid server: instead of
|
|
257
|
+
listing every tool, the server lists two synthetic tools — `search_tools` and
|
|
258
|
+
`call_tool` — and the model discovers what it needs on demand.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
smartapi-mcp --api_set biothings_all --tool-search bm25
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Every tool stays *callable* through `call_tool`; only the listing changes. Facade
|
|
265
|
+
tools (`biothings_query`, `biothings_get`, …) stay listed, so the common path
|
|
266
|
+
remains directly callable and only the per-API long tail is collapsed:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Tool search (bm25) enabled: 13 tools collapsed to 7 listed
|
|
270
|
+
(5 pinned + search_tools/call_tool); max_results=5.
|
|
271
|
+
All 13 tools stay callable via call_tool.
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
| Mode | Behaviour |
|
|
275
|
+
| --- | --- |
|
|
276
|
+
| `off` (default) | List every tool |
|
|
277
|
+
| `bm25` | Rank matches by keyword relevance |
|
|
278
|
+
| `regex` | Match tool names/descriptions by pattern |
|
|
279
|
+
|
|
280
|
+
`--tool-search-max-results` (default 5) caps the hits per search. Both options
|
|
281
|
+
have environment equivalents: `SMARTAPI_TOOL_SEARCH` and
|
|
282
|
+
`TOOL_SEARCH_MAX_RESULTS`.
|
|
283
|
+
|
|
284
|
+
Note that discovery-on-demand costs the model an extra round trip per unfamiliar
|
|
285
|
+
tool, so leave it `off` for small sets where the full list already fits
|
|
286
|
+
comfortably.
|
|
287
|
+
|
|
250
288
|
#### Development/Testing Setup
|
|
251
289
|
|
|
252
290
|
```json
|
|
@@ -451,9 +489,10 @@ from smartapi_mcp import (
|
|
|
451
489
|
load_api_spec,
|
|
452
490
|
get_mcp_server,
|
|
453
491
|
get_merged_mcp_server,
|
|
454
|
-
PREDEFINED_API_SETS
|
|
492
|
+
PREDEFINED_API_SETS,
|
|
455
493
|
)
|
|
456
494
|
|
|
495
|
+
|
|
457
496
|
async def main():
|
|
458
497
|
# Get SmartAPI IDs using a query
|
|
459
498
|
smartapi_ids = await get_smartapi_ids("tags.name=biothings")
|
|
@@ -463,16 +502,15 @@ async def main():
|
|
|
463
502
|
api_spec = load_api_spec("59dce17363dce279d389100834e43648") # MyGene.info
|
|
464
503
|
print(f"Loaded API: {api_spec.get('info', {}).get('title', 'Unknown')}")
|
|
465
504
|
|
|
466
|
-
# Create MCP server for a single API
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
)
|
|
505
|
+
# Create MCP server for a single API. The server is named after the
|
|
506
|
+
# spec's info.title; pass server_name to the merged/`build_server_for_set`
|
|
507
|
+
# entry points below if you want to choose the name yourself.
|
|
508
|
+
server = await get_mcp_server("59dce17363dce279d389100834e43648")
|
|
471
509
|
|
|
472
510
|
# Create merged MCP server for multiple APIs (recommended approach)
|
|
473
511
|
merged_server = await get_merged_mcp_server(
|
|
474
512
|
api_set="biothings_core", # Use predefined set
|
|
475
|
-
server_name="BioThings Core MCP Server"
|
|
513
|
+
server_name="BioThings Core MCP Server",
|
|
476
514
|
)
|
|
477
515
|
|
|
478
516
|
# Or with specific SmartAPI IDs
|
|
@@ -481,7 +519,7 @@ async def main():
|
|
|
481
519
|
"59dce17363dce279d389100834e43648", # MyGene.info
|
|
482
520
|
"09c8782d9f4027712e65b95424adba79", # MyVariant.info
|
|
483
521
|
],
|
|
484
|
-
server_name="Custom MCP Server"
|
|
522
|
+
server_name="Custom MCP Server",
|
|
485
523
|
)
|
|
486
524
|
|
|
487
525
|
# Show available predefined API sets
|
|
@@ -493,6 +531,7 @@ async def main():
|
|
|
493
531
|
# Or run with HTTP transport
|
|
494
532
|
# merged_server.run(transport="http", host="localhost", port=8000)
|
|
495
533
|
|
|
534
|
+
|
|
496
535
|
if __name__ == "__main__":
|
|
497
536
|
asyncio.run(main())
|
|
498
537
|
```
|
|
@@ -715,7 +754,8 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS
|
|
|
715
754
|
- **[SmartAPI Registry](https://smart-api.info/)** - Registry of biomedical and life sciences APIs
|
|
716
755
|
- **[Model Context Protocol (MCP)](https://modelcontextprotocol.io/)** - Standard protocol for AI model-tool integration
|
|
717
756
|
- **[BioThings APIs](https://biothings.io/)** - High-performance bioinformatics APIs
|
|
718
|
-
- **[
|
|
757
|
+
- **[FastMCP](https://github.com/jlowin/fastmcp)** - MCP server framework this package builds on
|
|
758
|
+
- **[AWS Labs OpenAPI MCP Server](https://github.com/awslabs/openapi-mcp-server)** - Wrapped by releases through 0.3.2; some code in `smartapi_mcp/openapi.py` is derived from it
|
|
719
759
|
|
|
720
760
|
## Citation
|
|
721
761
|
|
|
@@ -11,15 +11,17 @@ Create MCP (Model Context Protocol) servers for one or multiple APIs registered
|
|
|
11
11
|
|
|
12
12
|
The SmartAPI MCP Server enables integration between MCP-compatible clients and APIs registered in the SmartAPI registry. This allows for seamless discovery and interaction with bioinformatics and life sciences APIs through standardized MCP protocols.
|
|
13
13
|
|
|
14
|
-
Built on
|
|
14
|
+
Built directly on [FastMCP](https://github.com/jlowin/fastmcp), this project extends MCP support to the extensive collection of APIs available in the SmartAPI registry, with special focus on bioinformatics and life sciences APIs.
|
|
15
|
+
|
|
16
|
+
Earlier releases (through 0.3.2) wrapped the [AWS Labs OpenAPI MCP Server](https://github.com/awslabs/openapi-mcp-server); parts of `smartapi_mcp/openapi.py` are derived from that project's Apache-2.0 code. Dropping that wrapper reduced a clean install from 87 packages / 91 MB to 68 / 56 MB.
|
|
15
17
|
|
|
16
18
|
## Requirements
|
|
17
19
|
|
|
18
20
|
- Python 3.10 or higher
|
|
19
21
|
- Network access to SmartAPI registry (https://smart-api.info)
|
|
20
|
-
- Dependencies: `
|
|
21
|
-
|
|
22
|
-
`pyproject.toml`)
|
|
22
|
+
- Dependencies: `fastmcp>=3.3.1,<4`, `httpx`, `loguru` (fastmcp 4.x is not yet
|
|
23
|
+
supported — it moves to the MCP 2.x SDK and `httpx2`; see the dependency notes
|
|
24
|
+
in `pyproject.toml`)
|
|
23
25
|
|
|
24
26
|
## Features
|
|
25
27
|
|
|
@@ -202,6 +204,41 @@ inspect each BioThings spec at startup and serve any API that has extra
|
|
|
202
204
|
endpoints with faithful per-API tools instead — at the cost of slower startup
|
|
203
205
|
(it downloads the specs upfront).
|
|
204
206
|
|
|
207
|
+
#### Tool search (`--tool-search`)
|
|
208
|
+
|
|
209
|
+
The facade solves the tool explosion for BioThings APIs. `--tool-search` solves
|
|
210
|
+
it for everything else, including per-API tools in a hybrid server: instead of
|
|
211
|
+
listing every tool, the server lists two synthetic tools — `search_tools` and
|
|
212
|
+
`call_tool` — and the model discovers what it needs on demand.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
smartapi-mcp --api_set biothings_all --tool-search bm25
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Every tool stays *callable* through `call_tool`; only the listing changes. Facade
|
|
219
|
+
tools (`biothings_query`, `biothings_get`, …) stay listed, so the common path
|
|
220
|
+
remains directly callable and only the per-API long tail is collapsed:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
Tool search (bm25) enabled: 13 tools collapsed to 7 listed
|
|
224
|
+
(5 pinned + search_tools/call_tool); max_results=5.
|
|
225
|
+
All 13 tools stay callable via call_tool.
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
| Mode | Behaviour |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| `off` (default) | List every tool |
|
|
231
|
+
| `bm25` | Rank matches by keyword relevance |
|
|
232
|
+
| `regex` | Match tool names/descriptions by pattern |
|
|
233
|
+
|
|
234
|
+
`--tool-search-max-results` (default 5) caps the hits per search. Both options
|
|
235
|
+
have environment equivalents: `SMARTAPI_TOOL_SEARCH` and
|
|
236
|
+
`TOOL_SEARCH_MAX_RESULTS`.
|
|
237
|
+
|
|
238
|
+
Note that discovery-on-demand costs the model an extra round trip per unfamiliar
|
|
239
|
+
tool, so leave it `off` for small sets where the full list already fits
|
|
240
|
+
comfortably.
|
|
241
|
+
|
|
205
242
|
#### Development/Testing Setup
|
|
206
243
|
|
|
207
244
|
```json
|
|
@@ -406,9 +443,10 @@ from smartapi_mcp import (
|
|
|
406
443
|
load_api_spec,
|
|
407
444
|
get_mcp_server,
|
|
408
445
|
get_merged_mcp_server,
|
|
409
|
-
PREDEFINED_API_SETS
|
|
446
|
+
PREDEFINED_API_SETS,
|
|
410
447
|
)
|
|
411
448
|
|
|
449
|
+
|
|
412
450
|
async def main():
|
|
413
451
|
# Get SmartAPI IDs using a query
|
|
414
452
|
smartapi_ids = await get_smartapi_ids("tags.name=biothings")
|
|
@@ -418,16 +456,15 @@ async def main():
|
|
|
418
456
|
api_spec = load_api_spec("59dce17363dce279d389100834e43648") # MyGene.info
|
|
419
457
|
print(f"Loaded API: {api_spec.get('info', {}).get('title', 'Unknown')}")
|
|
420
458
|
|
|
421
|
-
# Create MCP server for a single API
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
)
|
|
459
|
+
# Create MCP server for a single API. The server is named after the
|
|
460
|
+
# spec's info.title; pass server_name to the merged/`build_server_for_set`
|
|
461
|
+
# entry points below if you want to choose the name yourself.
|
|
462
|
+
server = await get_mcp_server("59dce17363dce279d389100834e43648")
|
|
426
463
|
|
|
427
464
|
# Create merged MCP server for multiple APIs (recommended approach)
|
|
428
465
|
merged_server = await get_merged_mcp_server(
|
|
429
466
|
api_set="biothings_core", # Use predefined set
|
|
430
|
-
server_name="BioThings Core MCP Server"
|
|
467
|
+
server_name="BioThings Core MCP Server",
|
|
431
468
|
)
|
|
432
469
|
|
|
433
470
|
# Or with specific SmartAPI IDs
|
|
@@ -436,7 +473,7 @@ async def main():
|
|
|
436
473
|
"59dce17363dce279d389100834e43648", # MyGene.info
|
|
437
474
|
"09c8782d9f4027712e65b95424adba79", # MyVariant.info
|
|
438
475
|
],
|
|
439
|
-
server_name="Custom MCP Server"
|
|
476
|
+
server_name="Custom MCP Server",
|
|
440
477
|
)
|
|
441
478
|
|
|
442
479
|
# Show available predefined API sets
|
|
@@ -448,6 +485,7 @@ async def main():
|
|
|
448
485
|
# Or run with HTTP transport
|
|
449
486
|
# merged_server.run(transport="http", host="localhost", port=8000)
|
|
450
487
|
|
|
488
|
+
|
|
451
489
|
if __name__ == "__main__":
|
|
452
490
|
asyncio.run(main())
|
|
453
491
|
```
|
|
@@ -670,7 +708,8 @@ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENS
|
|
|
670
708
|
- **[SmartAPI Registry](https://smart-api.info/)** - Registry of biomedical and life sciences APIs
|
|
671
709
|
- **[Model Context Protocol (MCP)](https://modelcontextprotocol.io/)** - Standard protocol for AI model-tool integration
|
|
672
710
|
- **[BioThings APIs](https://biothings.io/)** - High-performance bioinformatics APIs
|
|
673
|
-
- **[
|
|
711
|
+
- **[FastMCP](https://github.com/jlowin/fastmcp)** - MCP server framework this package builds on
|
|
712
|
+
- **[AWS Labs OpenAPI MCP Server](https://github.com/awslabs/openapi-mcp-server)** - Wrapped by releases through 0.3.2; some code in `smartapi_mcp/openapi.py` is derived from it
|
|
674
713
|
|
|
675
714
|
## Citation
|
|
676
715
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smartapi-mcp"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Create MCP servers for one or multiple APIs registered in SmartAPI registry"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -32,12 +32,15 @@ classifiers = [
|
|
|
32
32
|
keywords = ["mcp", "smartapi", "api", "server", "bioinformatics"]
|
|
33
33
|
requires-python = ">=3.10"
|
|
34
34
|
dependencies = [
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
"
|
|
35
|
+
# Upper bound: fastmcp 4.x moves to the MCP 2.x SDK and httpx2, which this
|
|
36
|
+
# package has not been migrated to or tested against.
|
|
37
|
+
"fastmcp>=3.3.1,<4",
|
|
38
|
+
# Spec fetching. Also a fastmcp[client] dependency, but declared here
|
|
39
|
+
# because smartapi_mcp.openapi and smartapi_mcp.smartapi import it directly.
|
|
40
|
+
"httpx>=0.28.1,<1",
|
|
41
|
+
# Logging. Kept after dropping awslabs_openapi_mcp_server, which supplied
|
|
42
|
+
# the preconfigured logger this package's modules were written against.
|
|
43
|
+
"loguru>=0.7.3,<1",
|
|
41
44
|
]
|
|
42
45
|
|
|
43
46
|
[project.optional-dependencies]
|
|
@@ -45,7 +48,10 @@ dev = [
|
|
|
45
48
|
"pytest>=7.0.0",
|
|
46
49
|
"pytest-asyncio>=0.21.0",
|
|
47
50
|
"pytest-cov>=4.0.0",
|
|
48
|
-
|
|
51
|
+
# Upper bound deliberately: an unpinned linter makes CI results drift
|
|
52
|
+
# with upstream releases, failing on unchanged code. Raise it
|
|
53
|
+
# intentionally, with a lint/format pass, rather than by surprise.
|
|
54
|
+
"ruff>=0.14,<0.17",
|
|
49
55
|
"build>=0.8.0",
|
|
50
56
|
"twine>=4.0.0",
|
|
51
57
|
]
|
|
@@ -92,7 +98,6 @@ asyncio_mode = "auto"
|
|
|
92
98
|
[tool.coverage.run]
|
|
93
99
|
source = ["smartapi_mcp"]
|
|
94
100
|
omit = [
|
|
95
|
-
"smartapi_mcp/awslabs_server.py",
|
|
96
101
|
"smartapi_mcp/__main__.py",
|
|
97
102
|
"tests/*",
|
|
98
103
|
]
|
|
@@ -199,8 +204,11 @@ ignore = [
|
|
|
199
204
|
"B027",
|
|
200
205
|
# Allow boolean positional values in function calls
|
|
201
206
|
"FBT003",
|
|
202
|
-
# Ignore complexity
|
|
203
|
-
|
|
207
|
+
# Ignore complexity. PLR0917 (too-many-positional-arguments) is the same
|
|
208
|
+
# complaint as PLR0913 for the build entry points, which take a set of
|
|
209
|
+
# mutually exclusive API selectors; making them keyword-only would be a
|
|
210
|
+
# breaking change to the public API.
|
|
211
|
+
"C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PLR0917",
|
|
204
212
|
# Allow print statements (useful for CLI tools)
|
|
205
213
|
"T201",
|
|
206
214
|
# Allow assert statements
|
|
@@ -246,8 +254,5 @@ max-complexity = 10
|
|
|
246
254
|
"tests/*" = ["PLR2004", "S101", "PT011", "PT012", "ARG002", "PT019"]
|
|
247
255
|
# CLI module can use print statements and sys.exit
|
|
248
256
|
"smartapi_mcp/cli.py" = ["T201", "PLR0913"]
|
|
249
|
-
# Do not fix linting errors in awslabs_server.py as it's
|
|
250
|
-
# based on the original codebase
|
|
251
|
-
"smartapi_mcp/awslabs_server.py" = ["ALL"]
|
|
252
257
|
# Allow star imports and unused imports in __init__.py
|
|
253
258
|
"__init__.py" = ["F401", "F403", "F405"]
|
|
@@ -4,13 +4,19 @@ SmartAPI MCP Server Package
|
|
|
4
4
|
Create MCP servers for one or multiple APIs registered in SmartAPI registry.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.5.0"
|
|
8
8
|
__author__ = "BioThings Team"
|
|
9
9
|
__email__ = "help@biothings.io"
|
|
10
10
|
|
|
11
11
|
# Optional imports for when dependencies are available
|
|
12
12
|
try:
|
|
13
13
|
from .biothings import build_biothings_facade, build_registry
|
|
14
|
+
from .openapi import (
|
|
15
|
+
SpecError,
|
|
16
|
+
build_openapi_server,
|
|
17
|
+
fetch_spec,
|
|
18
|
+
resolve_internal_refs,
|
|
19
|
+
)
|
|
14
20
|
from .server import (
|
|
15
21
|
build_server_for_set,
|
|
16
22
|
get_mcp_server,
|
|
@@ -28,9 +34,12 @@ try:
|
|
|
28
34
|
|
|
29
35
|
__all__ = [
|
|
30
36
|
"PREDEFINED_API_SETS",
|
|
37
|
+
"SpecError",
|
|
31
38
|
"build_biothings_facade",
|
|
39
|
+
"build_openapi_server",
|
|
32
40
|
"build_registry",
|
|
33
41
|
"build_server_for_set",
|
|
42
|
+
"fetch_spec",
|
|
34
43
|
"get_base_server_url",
|
|
35
44
|
"get_mcp_server",
|
|
36
45
|
"get_merged_mcp_server",
|
|
@@ -39,6 +48,7 @@ try:
|
|
|
39
48
|
"get_smartapi_registry",
|
|
40
49
|
"load_api_spec",
|
|
41
50
|
"merge_mcp_servers",
|
|
51
|
+
"resolve_internal_refs",
|
|
42
52
|
]
|
|
43
53
|
except ImportError:
|
|
44
54
|
# Dependencies not available, only export version info
|
|
@@ -12,15 +12,17 @@ the set, so the server works on every MCP client without depending on runtime
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
|
+
import math
|
|
15
16
|
import re
|
|
16
17
|
from dataclasses import dataclass, field
|
|
17
18
|
|
|
18
19
|
import httpx
|
|
19
|
-
from awslabs.openapi_mcp_server import logger
|
|
20
20
|
from fastmcp import FastMCP
|
|
21
21
|
from fastmcp.tools import Tool
|
|
22
22
|
|
|
23
|
+
from .log import logger
|
|
23
24
|
from .smartapi import (
|
|
25
|
+
CORE_BIOTHINGS_API_IDS,
|
|
24
26
|
HTTP_TIMEOUT,
|
|
25
27
|
get_base_server_url,
|
|
26
28
|
get_smartapi_registry,
|
|
@@ -37,6 +39,20 @@ _MAX_DESC_LEN = 200
|
|
|
37
39
|
# HTTP methods recognized when enumerating spec operations.
|
|
38
40
|
_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "head", "options"}
|
|
39
41
|
|
|
42
|
+
# Tags that disqualify an API from the BioThings *annotation-API family* even
|
|
43
|
+
# though it carries the "biothings" tag. TRAPI services (BioThings Explorer,
|
|
44
|
+
# Service Provider) are built by the same team and tagged accordingly, but they
|
|
45
|
+
# speak the Translator Reasoner API -- a query-graph protocol -- not the
|
|
46
|
+
# BioThings annotation interface, so none of the generic facade tools apply to
|
|
47
|
+
# them. They are served with faithful per-API tools instead.
|
|
48
|
+
#
|
|
49
|
+
# This is not a cosmetic distinction: the facade infers an entity type from the
|
|
50
|
+
# first ``/{type}/{id}``-shaped path, and BTE's ``GET /asyncquery_status/{id}``
|
|
51
|
+
# matches that shape. Without this exclusion, ``biothings_get`` would request
|
|
52
|
+
# ``/asyncquery_status/<id>`` and return a job status as though it were an
|
|
53
|
+
# annotation record -- a wrong answer with no error.
|
|
54
|
+
NON_FAMILY_TAGS = frozenset({"trapi"})
|
|
55
|
+
|
|
40
56
|
|
|
41
57
|
@dataclass
|
|
42
58
|
class BioThingsAPIEntry:
|
|
@@ -96,11 +112,21 @@ async def build_registry(
|
|
|
96
112
|
return build_registry_from_entries(entries)
|
|
97
113
|
|
|
98
114
|
|
|
115
|
+
def is_biothings_family(entry: BioThingsAPIEntry) -> bool:
|
|
116
|
+
"""Whether ``entry`` is a BioThings annotation API the facade can serve.
|
|
117
|
+
|
|
118
|
+
Requires the ``biothings`` tag and the absence of any
|
|
119
|
+
:data:`NON_FAMILY_TAGS`.
|
|
120
|
+
"""
|
|
121
|
+
tags = {str(tag).strip().lower() for tag in entry.tags}
|
|
122
|
+
return "biothings" in tags and not (tags & NON_FAMILY_TAGS)
|
|
123
|
+
|
|
124
|
+
|
|
99
125
|
def is_biothings_registry(registry: dict[str, BioThingsAPIEntry]) -> bool:
|
|
100
|
-
"""True only if every API in the registry is
|
|
126
|
+
"""True only if every API in the registry is a facade-servable BioThings API."""
|
|
101
127
|
if not registry:
|
|
102
128
|
return False
|
|
103
|
-
return all(
|
|
129
|
+
return all(is_biothings_family(entry) for entry in registry.values())
|
|
104
130
|
|
|
105
131
|
|
|
106
132
|
def _resolve_endpoints(entry: BioThingsAPIEntry) -> None:
|
|
@@ -225,12 +251,146 @@ async def partition_biothings(
|
|
|
225
251
|
return facade_entries, extra_ids
|
|
226
252
|
|
|
227
253
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
# Words too common to discriminate between APIs. Without this, a natural-language
|
|
255
|
+
# intent like "get a gene annotation by its Entrez gene id" is dominated by
|
|
256
|
+
# "get"/"a"/"by"/"its"/"id" rather than by "gene" and "entrez".
|
|
257
|
+
_STOPWORDS = frozenset(
|
|
258
|
+
{
|
|
259
|
+
"a",
|
|
260
|
+
"an",
|
|
261
|
+
"and",
|
|
262
|
+
"are",
|
|
263
|
+
"as",
|
|
264
|
+
"at",
|
|
265
|
+
"be",
|
|
266
|
+
"by",
|
|
267
|
+
"for",
|
|
268
|
+
"from",
|
|
269
|
+
"get",
|
|
270
|
+
"how",
|
|
271
|
+
"in",
|
|
272
|
+
"into",
|
|
273
|
+
"is",
|
|
274
|
+
"it",
|
|
275
|
+
"its",
|
|
276
|
+
"of",
|
|
277
|
+
"on",
|
|
278
|
+
"or",
|
|
279
|
+
"that",
|
|
280
|
+
"the",
|
|
281
|
+
"their",
|
|
282
|
+
"them",
|
|
283
|
+
"there",
|
|
284
|
+
"these",
|
|
285
|
+
"this",
|
|
286
|
+
"to",
|
|
287
|
+
"what",
|
|
288
|
+
"when",
|
|
289
|
+
"where",
|
|
290
|
+
"which",
|
|
291
|
+
"who",
|
|
292
|
+
"with",
|
|
293
|
+
"api",
|
|
294
|
+
"apis",
|
|
295
|
+
"data",
|
|
296
|
+
"database",
|
|
297
|
+
"dataset",
|
|
298
|
+
"info",
|
|
299
|
+
"information",
|
|
300
|
+
"record",
|
|
301
|
+
"records",
|
|
302
|
+
"service",
|
|
303
|
+
"services",
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _tokenize(text: str) -> list[str]:
|
|
309
|
+
"""Lower-case word tokens. Word-based, not substring-based, on purpose.
|
|
310
|
+
|
|
311
|
+
The previous scorer used ``str.count`` on the raw text, so a query term
|
|
312
|
+
like "id" also matched inside "identifier", "candidate" and "provide".
|
|
313
|
+
"""
|
|
314
|
+
return re.findall(r"[a-z0-9]+", text.lower())
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# How much more a term in the API's name/title counts than one buried in its
|
|
318
|
+
# description. An API *named* for a concept is a far stronger answer to a query
|
|
319
|
+
# about that concept than one merely mentioning it in prose; without this,
|
|
320
|
+
# scores tie constantly and the tie-break (alphabetical) decides, which
|
|
321
|
+
# systematically favours the "biothings_*"-prefixed names over MyGene/MyChem.
|
|
322
|
+
_NAME_FIELD_WEIGHT = 3.0
|
|
323
|
+
|
|
324
|
+
# Score multiplier for the core BioThings APIs (:data:`CORE_BIOTHINGS_API_IDS`).
|
|
325
|
+
#
|
|
326
|
+
# These are the canonical broad-coverage services, and they are the *worst*
|
|
327
|
+
# served by pure lexical scoring: being general means their descriptions carry
|
|
328
|
+
# the least distinctive vocabulary, while single-source satellite APIs read as
|
|
329
|
+
# highly specific. So a lexical ranker systematically under-ranks exactly the
|
|
330
|
+
# APIs a user most often wants, and needs a prior to correct for it.
|
|
331
|
+
#
|
|
332
|
+
# Measured on 20 BioThings intents. With the registry descriptions as they were
|
|
333
|
+
# before the core-API enrichment, no boost gave recall@5 16/20 (MRR 0.71) and
|
|
334
|
+
# only 3 of the 6 core-API intents were answered; 1.2 gives 19/20 (MRR 0.82).
|
|
335
|
+
# With the enriched descriptions live, no boost already gives 19/20 (MRR 0.92)
|
|
336
|
+
# and 1.2 gives 20/20 (MRR 0.90). Larger values buy nothing on recall and cost
|
|
337
|
+
# ranking quality once the metadata is good -- at 3.0 the post-enrichment MRR
|
|
338
|
+
# falls to 0.81 -- so this is deliberately the smallest value that captures the
|
|
339
|
+
# benefit, and it stays close to neutral as the metadata improves.
|
|
340
|
+
CORE_API_BOOST = 1.2
|
|
341
|
+
|
|
342
|
+
_CORE_API_IDS = frozenset(CORE_BIOTHINGS_API_IDS)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _entry_terms(entry: BioThingsAPIEntry) -> tuple[set[str], set[str]]:
|
|
346
|
+
"""Return ``(name_terms, all_terms)`` for one API.
|
|
347
|
+
|
|
348
|
+
``name_terms`` covers the short name, title and tags -- the curated labels;
|
|
349
|
+
``all_terms`` adds the free-text description.
|
|
350
|
+
"""
|
|
351
|
+
name_terms = set(
|
|
352
|
+
_tokenize(" ".join([entry.name, entry.title, " ".join(map(str, entry.tags))]))
|
|
353
|
+
)
|
|
354
|
+
return name_terms, name_terms | set(_tokenize(entry.description))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _score_entry(
|
|
358
|
+
terms: tuple[set[str], set[str]],
|
|
359
|
+
query_terms: list[str],
|
|
360
|
+
idf: dict[str, float],
|
|
361
|
+
) -> float:
|
|
362
|
+
"""Score one API against a query. Higher is more relevant.
|
|
363
|
+
|
|
364
|
+
``terms`` is the ``(name_terms, all_terms)`` pair from :func:`_entry_terms`.
|
|
365
|
+
|
|
366
|
+
Three deliberate properties:
|
|
367
|
+
|
|
368
|
+
* **Binary term frequency.** A term counts once however often it appears, so
|
|
369
|
+
an API with a long, repetitive description no longer outranks a precisely
|
|
370
|
+
matching one. This was the concrete defect in the previous scorer:
|
|
371
|
+
searching "get a gene annotation by its Entrez gene id" ranked MyGeneSet
|
|
372
|
+
above MyGene, because summed substring counts reward verbosity.
|
|
373
|
+
* **IDF weighting.** A term shared by most APIs ("gene", "translator")
|
|
374
|
+
contributes almost nothing, while a rare one ("entrez", "ngd", "taxonomy")
|
|
375
|
+
dominates -- which is what makes an intent select the API it names.
|
|
376
|
+
* **Field weighting.** A hit in the name/title/tags counts
|
|
377
|
+
:data:`_NAME_FIELD_WEIGHT` times one that is only in the description. An
|
|
378
|
+
API *named* for a concept answers a query about it far better than one
|
|
379
|
+
merely mentioning it in prose, and without this, scores tie constantly and
|
|
380
|
+
the alphabetical tie-break decides -- which systematically favoured the
|
|
381
|
+
``biothings_*``-prefixed names over MyGene/MyChem/MyVariant.
|
|
382
|
+
"""
|
|
383
|
+
name_terms, all_terms = terms
|
|
384
|
+
score = 0.0
|
|
385
|
+
for term in query_terms:
|
|
386
|
+
weight = idf.get(term, 0.0)
|
|
387
|
+
if not weight:
|
|
388
|
+
continue
|
|
389
|
+
if term in name_terms:
|
|
390
|
+
score += weight * _NAME_FIELD_WEIGHT
|
|
391
|
+
elif term in all_terms:
|
|
392
|
+
score += weight
|
|
393
|
+
return score
|
|
234
394
|
|
|
235
395
|
|
|
236
396
|
def rank_apis(
|
|
@@ -258,11 +418,38 @@ def rank_apis(
|
|
|
258
418
|
if not keyword or not keyword.strip():
|
|
259
419
|
return [_as_dict(registry[name]) for name in sorted(registry)]
|
|
260
420
|
|
|
261
|
-
|
|
262
|
-
|
|
421
|
+
query_terms = [t for t in _tokenize(keyword) if t not in _STOPWORDS]
|
|
422
|
+
if not query_terms:
|
|
423
|
+
# Nothing discriminating left (e.g. "what data is there?"): fall back to
|
|
424
|
+
# the full catalog rather than returning an arbitrary subset.
|
|
425
|
+
return [_as_dict(registry[name]) for name in sorted(registry)]
|
|
426
|
+
|
|
427
|
+
terms_by_name = {name: _entry_terms(entry) for name, entry in registry.items()}
|
|
428
|
+
total = len(registry)
|
|
429
|
+
idf = {}
|
|
430
|
+
for term in set(query_terms):
|
|
431
|
+
seen_in = sum(1 for _, all_terms in terms_by_name.values() if term in all_terms)
|
|
432
|
+
# Robertson/Sparck-Jones IDF. Chosen over the plain log(N/n) form
|
|
433
|
+
# because it stays strictly positive even for a term present in every
|
|
434
|
+
# API: with a small registry (say two APIs that both mention "gene"),
|
|
435
|
+
# a zero weight would drop every candidate and the search would answer
|
|
436
|
+
# "nothing found" for a query that in fact matches everything.
|
|
437
|
+
idf[term] = math.log(1 + (total - seen_in + 0.5) / (seen_in + 0.5))
|
|
438
|
+
|
|
439
|
+
scored = []
|
|
440
|
+
for name, entry in registry.items():
|
|
441
|
+
score = _score_entry(terms_by_name[name], query_terms, idf)
|
|
442
|
+
if entry.smartapi_id in _CORE_API_IDS:
|
|
443
|
+
# Multiplicative, not additive: a zero score stays zero, so the
|
|
444
|
+
# boost reorders results that already match and never promotes a
|
|
445
|
+
# core API into a query it has nothing to do with.
|
|
446
|
+
score *= CORE_API_BOOST
|
|
447
|
+
scored.append((score, entry))
|
|
263
448
|
scored = [pair for pair in scored if pair[0] > 0]
|
|
264
449
|
scored.sort(key=lambda pair: (-pair[0], pair[1].name))
|
|
265
|
-
return [
|
|
450
|
+
return [
|
|
451
|
+
{**_as_dict(entry), "score": round(score, 3)} for score, entry in scored[:limit]
|
|
452
|
+
]
|
|
266
453
|
|
|
267
454
|
|
|
268
455
|
def build_biothings_facade(
|