smartapi-mcp 0.3.0__tar.gz → 0.3.2__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.0 → smartapi_mcp-0.3.2}/PKG-INFO +2 -1
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/pyproject.toml +2 -1
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/__init__.py +1 -1
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/cli.py +30 -12
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/server.py +52 -4
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/LICENSE +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/MANIFEST.in +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/README.md +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/requirements-dev.txt +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/requirements.txt +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/setup.cfg +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/setup.py +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/__main__.py +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/biothings.py +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/config.py +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/py.typed +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp/smartapi.py +0 -0
- {smartapi_mcp-0.3.0 → smartapi_mcp-0.3.2}/smartapi_mcp.egg-info/SOURCES.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: smartapi-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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>
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
23
24
|
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "smartapi-mcp"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
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"
|
|
@@ -24,6 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Programming Language :: Python :: 3.11",
|
|
25
25
|
"Programming Language :: Python :: 3.12",
|
|
26
26
|
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
27
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
29
|
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
29
30
|
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
@@ -27,47 +27,63 @@ def main():
|
|
|
27
27
|
help=(
|
|
28
28
|
"A predefined set of SmartAPI APIs to include. One of: "
|
|
29
29
|
"'biothings_core' (5 core BioThings APIs), 'biothings_test' "
|
|
30
|
-
"(core + SemmedDB), or 'biothings_all' (all BioThings APIs)."
|
|
30
|
+
"(core + SemmedDB), or 'biothings_all' (all BioThings APIs). "
|
|
31
|
+
"[env: SMARTAPI_API_SET]"
|
|
31
32
|
),
|
|
32
33
|
)
|
|
33
34
|
parser.add_argument(
|
|
34
35
|
"--smartapi_id",
|
|
35
|
-
help="Pass a single SmartAPI (id) to create a MCP server.",
|
|
36
|
+
help=("Pass a single SmartAPI (id) to create a MCP server. [env: SMARTAPI_ID]"),
|
|
36
37
|
)
|
|
37
38
|
parser.add_argument(
|
|
38
39
|
"--smartapi_ids",
|
|
39
|
-
help=
|
|
40
|
+
help=(
|
|
41
|
+
"Pass a list of SmartAPIs (comma-separated ids) to create a MCP "
|
|
42
|
+
"server. [env: SMARTAPI_IDS]"
|
|
43
|
+
),
|
|
40
44
|
)
|
|
41
45
|
parser.add_argument(
|
|
42
46
|
"--smartapi_q",
|
|
43
47
|
help=(
|
|
44
48
|
"A SmartAPI registry search query selecting which APIs to include, "
|
|
45
|
-
"e.g. 'tags.name:biothings'."
|
|
49
|
+
"e.g. 'tags.name:biothings'. [env: SMARTAPI_Q]"
|
|
46
50
|
),
|
|
47
51
|
)
|
|
48
52
|
parser.add_argument(
|
|
49
53
|
"--smartapi_exclude_ids",
|
|
50
54
|
help=(
|
|
51
|
-
"Exclude a list of SmartAPIs (comma-separated ids) to create a MCP
|
|
55
|
+
"Exclude a list of SmartAPIs (comma-separated ids) to create a MCP "
|
|
56
|
+
"server. [env: SMARTAPI_EXCLUDE_IDS]"
|
|
52
57
|
),
|
|
53
58
|
)
|
|
54
59
|
parser.add_argument(
|
|
55
60
|
"--host",
|
|
56
|
-
help=
|
|
61
|
+
help=(
|
|
62
|
+
"The host address for the MCP server in HTTP mode. Default is "
|
|
63
|
+
"localhost. [env: SERVER_HOST]"
|
|
64
|
+
),
|
|
57
65
|
)
|
|
58
66
|
parser.add_argument(
|
|
59
67
|
"--port",
|
|
60
68
|
type=int,
|
|
61
69
|
default=8000,
|
|
62
|
-
help=
|
|
70
|
+
help=(
|
|
71
|
+
"The http port for the MCP server in HTTP mode. Default is 8000. "
|
|
72
|
+
"[env: SERVER_PORT]"
|
|
73
|
+
),
|
|
63
74
|
)
|
|
64
75
|
parser.add_argument(
|
|
65
76
|
"--transport",
|
|
66
|
-
help=
|
|
77
|
+
help=(
|
|
78
|
+
"The transport mode for the MCP server, either stdio (default) or "
|
|
79
|
+
"http. [env: SERVER_TRANSPORT]"
|
|
80
|
+
),
|
|
67
81
|
)
|
|
68
82
|
parser.add_argument(
|
|
69
83
|
"--server_name",
|
|
70
|
-
help=
|
|
84
|
+
help=(
|
|
85
|
+
'The name of the MCP server, default is "smartapi_mcp". [env: SERVER_NAME]'
|
|
86
|
+
),
|
|
71
87
|
)
|
|
72
88
|
parser.add_argument(
|
|
73
89
|
"--facade",
|
|
@@ -79,7 +95,8 @@ def main():
|
|
|
79
95
|
"non-BioThings APIs in the set are added as per-API tools (hybrid). "
|
|
80
96
|
"'auto' (default): use the facade once there are enough BioThings "
|
|
81
97
|
"APIs (see --facade-threshold). 'on': always use it for BioThings "
|
|
82
|
-
"APIs. 'off': always emit faithful per-API tools for every API."
|
|
98
|
+
"APIs. 'off': always emit faithful per-API tools for every API. "
|
|
99
|
+
"[env: SMARTAPI_FACADE]"
|
|
83
100
|
),
|
|
84
101
|
)
|
|
85
102
|
parser.add_argument(
|
|
@@ -88,7 +105,7 @@ def main():
|
|
|
88
105
|
default=10,
|
|
89
106
|
help=(
|
|
90
107
|
"Number of BioThings APIs in the set at which 'auto' switches to the "
|
|
91
|
-
"facade (default: 10)."
|
|
108
|
+
"facade (default: 10). [env: FACADE_THRESHOLD]"
|
|
92
109
|
),
|
|
93
110
|
)
|
|
94
111
|
parser.add_argument(
|
|
@@ -97,7 +114,8 @@ def main():
|
|
|
97
114
|
help=(
|
|
98
115
|
"Inspect BioThings specs and serve any API that has non-standard "
|
|
99
116
|
"endpoints (e.g. SemmedDB's /query/ngd) with faithful per-API tools "
|
|
100
|
-
"instead of the facade. Slower startup (downloads specs upfront)."
|
|
117
|
+
"instead of the facade. Slower startup (downloads specs upfront). "
|
|
118
|
+
"[env: FACADE_STRICT]"
|
|
101
119
|
),
|
|
102
120
|
)
|
|
103
121
|
parser.add_argument(
|
|
@@ -4,6 +4,7 @@ SmartAPI MCP Server
|
|
|
4
4
|
Main MCP server implementation for SmartAPI integration.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import hashlib
|
|
7
8
|
import re
|
|
8
9
|
|
|
9
10
|
from awslabs.openapi_mcp_server import logger
|
|
@@ -23,6 +24,15 @@ from .smartapi import (
|
|
|
23
24
|
smartapi_spec_url,
|
|
24
25
|
)
|
|
25
26
|
|
|
27
|
+
# Cap names at 64 characters. The MCP spec (SEP-986) recommends 1-64 chars for
|
|
28
|
+
# *tool* names as a SHOULD, but the limit is enforced as a hard error by the
|
|
29
|
+
# model APIs: both Anthropic (FrontendRemoteMcpToolDefinition.name; 400 on
|
|
30
|
+
# longer names) and OpenAI (^[a-zA-Z0-9_-]{1,64}$) reject names over 64 chars.
|
|
31
|
+
# So prefixed per-API names must be truncated to fit. The spec does not define a
|
|
32
|
+
# length for *prompt* names, but we cap them too as a harmless safeguard against
|
|
33
|
+
# clients that reuse the tool-name validator.
|
|
34
|
+
MAX_TOOL_NAME_LEN = 64
|
|
35
|
+
|
|
26
36
|
|
|
27
37
|
async def get_mcp_server(smartapi_id: str) -> FastMCP:
|
|
28
38
|
config = Config(
|
|
@@ -35,6 +45,33 @@ async def get_mcp_server(smartapi_id: str) -> FastMCP:
|
|
|
35
45
|
return await create_mcp_server_async(config)
|
|
36
46
|
|
|
37
47
|
|
|
48
|
+
def _fit_name(name: str, used: set[str]) -> str:
|
|
49
|
+
"""Return a unique name no longer than :data:`MAX_TOOL_NAME_LEN` chars.
|
|
50
|
+
|
|
51
|
+
Used for both tool and prompt names. Names within the limit (and not already
|
|
52
|
+
in ``used``) are returned unchanged. Longer or colliding names are truncated
|
|
53
|
+
and given a short hash suffix derived from the *full* name, so the result
|
|
54
|
+
stays deterministic and collision-free (two different long names hash
|
|
55
|
+
differently).
|
|
56
|
+
"""
|
|
57
|
+
if len(name) <= MAX_TOOL_NAME_LEN and name not in used:
|
|
58
|
+
return name
|
|
59
|
+
|
|
60
|
+
digest = hashlib.sha1(name.encode()).hexdigest()[:6] # noqa: S324 - non-crypto
|
|
61
|
+
suffix = f"_{digest}"
|
|
62
|
+
truncated = name[: MAX_TOOL_NAME_LEN - len(suffix)].rstrip("_") + suffix
|
|
63
|
+
# Guard against the (unlikely) case where the truncated form still collides.
|
|
64
|
+
while truncated in used:
|
|
65
|
+
digest = hashlib.sha1((name + digest).encode()).hexdigest()[:6] # noqa: S324
|
|
66
|
+
suffix = f"_{digest}"
|
|
67
|
+
truncated = name[: MAX_TOOL_NAME_LEN - len(suffix)].rstrip("_") + suffix
|
|
68
|
+
logger.debug(
|
|
69
|
+
f"Name '{name}' exceeds {MAX_TOOL_NAME_LEN} chars or collides; "
|
|
70
|
+
f"renamed to '{truncated}'."
|
|
71
|
+
)
|
|
72
|
+
return truncated
|
|
73
|
+
|
|
74
|
+
|
|
38
75
|
async def _merge_servers_into(
|
|
39
76
|
target: FastMCP, list_of_servers: list[FastMCP]
|
|
40
77
|
) -> FastMCP:
|
|
@@ -43,6 +80,11 @@ async def _merge_servers_into(
|
|
|
43
80
|
Tool and prompt names are prefixed with the source server's (API) name to
|
|
44
81
|
avoid conflicts. ``target`` is mutated in place and returned.
|
|
45
82
|
"""
|
|
83
|
+
# Seed with names already in the target (e.g. facade tools in the hybrid
|
|
84
|
+
# path) so merged per-API tools/prompts never collide with them. Tools and
|
|
85
|
+
# prompts have separate namespaces, so each gets its own set.
|
|
86
|
+
used_tool_names: set[str] = set(await target.get_tools())
|
|
87
|
+
used_prompt_names: set[str] = set(await target.get_prompts())
|
|
46
88
|
for server in list_of_servers:
|
|
47
89
|
api_name = re.sub(
|
|
48
90
|
r"[^a-z0-9_-]", "_", getattr(server, "name", "unknown_api").lower()
|
|
@@ -51,8 +93,11 @@ async def _merge_servers_into(
|
|
|
51
93
|
tools = await server.get_tools()
|
|
52
94
|
if tools:
|
|
53
95
|
for original_name, tool in tools.items():
|
|
54
|
-
# Rename the tool by prefixing with API name
|
|
55
|
-
|
|
96
|
+
# Rename the tool by prefixing with API name, keeping it within
|
|
97
|
+
# the 64-char limit that MCP clients enforce.
|
|
98
|
+
prefixed = f"{api_name}_{original_name}"
|
|
99
|
+
tool.name = _fit_name(prefixed, used_tool_names)
|
|
100
|
+
used_tool_names.add(tool.name)
|
|
56
101
|
target.add_tool(tool)
|
|
57
102
|
else:
|
|
58
103
|
err_msg = f"Server {server} does not have accessible tools."
|
|
@@ -62,8 +107,11 @@ async def _merge_servers_into(
|
|
|
62
107
|
prompts = await server.get_prompts()
|
|
63
108
|
if prompts:
|
|
64
109
|
for original_name, prompt in prompts.items():
|
|
65
|
-
# Rename the prompt by prefixing with API name
|
|
66
|
-
|
|
110
|
+
# Rename the prompt by prefixing with API name, keeping it
|
|
111
|
+
# within the 64-char limit that MCP clients enforce.
|
|
112
|
+
prefixed = f"{api_name}_{original_name}"
|
|
113
|
+
prompt.name = _fit_name(prefixed, used_prompt_names)
|
|
114
|
+
used_prompt_names.add(prompt.name)
|
|
67
115
|
target.add_prompt(prompt)
|
|
68
116
|
logger.debug(f"Merged {len(prompts)} prompts from {api_name}")
|
|
69
117
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|