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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smartapi-mcp
3
- Version: 0.3.0
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.0"
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",
@@ -4,7 +4,7 @@ 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.3.0"
7
+ __version__ = "0.3.2"
8
8
  __author__ = "BioThings Team"
9
9
  __email__ = "help@biothings.io"
10
10
 
@@ -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="Pass a list of SmartAPIs (comma-separated ids) to create a MCP server.",
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 server."
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="The host address for the MCP server in HTTP mode. Default is localhost.",
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="The http port for the MCP server in HTTP mode. Default is 8000.",
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="The transport mode for the MCP server, either stdio (default) or http.",
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='The name of the MCP server, default is "smartapi_mcp".',
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
- tool.name = f"{api_name}_{original_name}"
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
- prompt.name = f"{api_name}_{original_name}"
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