simlir-mcp 0.1.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 @@
1
+ UNLICENSED — publication and legal licensing gate pending.
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.2
2
+ Name: simlir-mcp
3
+ Version: 0.1.0
4
+ Summary: A thin stdio MCP bridge for the simlir public product API
5
+ Author: simlir engineering
6
+ License: UNLICENSED — publication and legal licensing gate pending.
7
+
8
+ Keywords: mcp,model context protocol,product data,product search,ai
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: simlir<0.2.0,>=0.1.0
16
+
17
+ # simlir-mcp
18
+
19
+ Release candidate for a thin Python stdio adapter around the simlir public
20
+ product API. It is intentionally separate from Simlir’s internal
21
+ repository-intelligence MCP server and contains no ingestion code, customer
22
+ data, database credentials, or internal repository tooling.
23
+
24
+ The PyPI publication workflow is configured, but the package remains
25
+ unpublished pending legal licensing, public repository permissions, an assigned
26
+ engineering-maintainer role, PyPI name/policy checks, stable API deployment,
27
+ and release rollback evidence. Do not use the package name from a public
28
+ registry until those gates are recorded.
29
+
30
+ ## Local use
31
+
32
+ Install the SDK and this adapter from the local release candidates:
33
+
34
+ ```bash
35
+ python -m pip install --no-deps ../python-sdk
36
+ python -m pip install --no-deps .
37
+ export SIMLIR_API_KEY=sk_your_key_here
38
+ python -m simlir_mcp
39
+ ```
40
+
41
+ The adapter keeps JSON-RPC responses on stdout and writes only structured,
42
+ metadata-only diagnostics to stderr. Never put an API key in a checked-in
43
+ configuration file.
44
+
45
+ ## Claude Desktop, Cursor, or VS Code
46
+
47
+ After the external publication and stable-URL gates pass, the intended local
48
+ configuration is:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "simlir": {
54
+ "command": "simlir-mcp",
55
+ "env": {
56
+ "SIMLIR_API_KEY": "${SIMLIR_API_KEY}"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Supported tools
64
+
65
+ | Tool | API operation |
66
+ | --- | --- |
67
+ | `simlir_search_products` | Semantic product search |
68
+ | `simlir_search_products_by_image` | Visual product search with optional natural-language context |
69
+ | `simlir_lookup_products_by_identifier` | Exact GTIN/MPN/SKU lookup |
70
+
71
+ Tool names, required arguments, markets, and response shapes are pinned to the
72
+ checked-in public contract. Update the contract first, then regenerate or
73
+ update both MCP adapters and their tests when the public API changes.
74
+
75
+ The image tool requires `image_url` and `market`. Its optional `query` field
76
+ adds shopper context such as a size, product role, compatibility requirement,
77
+ or use case; Simlir combines that context with the visual signal. Image-only
78
+ calls remain valid.
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ PYTHONPATH=../python-sdk/src:src python -m unittest discover -s tests -p 'test_*.py'
84
+ python -m compileall -q src
85
+ python -m pip wheel --no-deps --no-build-isolation --wheel-dir /tmp/simlir-python-mcp-wheel .
86
+ ```
@@ -0,0 +1,70 @@
1
+ # simlir-mcp
2
+
3
+ Release candidate for a thin Python stdio adapter around the simlir public
4
+ product API. It is intentionally separate from Simlir’s internal
5
+ repository-intelligence MCP server and contains no ingestion code, customer
6
+ data, database credentials, or internal repository tooling.
7
+
8
+ The PyPI publication workflow is configured, but the package remains
9
+ unpublished pending legal licensing, public repository permissions, an assigned
10
+ engineering-maintainer role, PyPI name/policy checks, stable API deployment,
11
+ and release rollback evidence. Do not use the package name from a public
12
+ registry until those gates are recorded.
13
+
14
+ ## Local use
15
+
16
+ Install the SDK and this adapter from the local release candidates:
17
+
18
+ ```bash
19
+ python -m pip install --no-deps ../python-sdk
20
+ python -m pip install --no-deps .
21
+ export SIMLIR_API_KEY=sk_your_key_here
22
+ python -m simlir_mcp
23
+ ```
24
+
25
+ The adapter keeps JSON-RPC responses on stdout and writes only structured,
26
+ metadata-only diagnostics to stderr. Never put an API key in a checked-in
27
+ configuration file.
28
+
29
+ ## Claude Desktop, Cursor, or VS Code
30
+
31
+ After the external publication and stable-URL gates pass, the intended local
32
+ configuration is:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "simlir": {
38
+ "command": "simlir-mcp",
39
+ "env": {
40
+ "SIMLIR_API_KEY": "${SIMLIR_API_KEY}"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Supported tools
48
+
49
+ | Tool | API operation |
50
+ | --- | --- |
51
+ | `simlir_search_products` | Semantic product search |
52
+ | `simlir_search_products_by_image` | Visual product search with optional natural-language context |
53
+ | `simlir_lookup_products_by_identifier` | Exact GTIN/MPN/SKU lookup |
54
+
55
+ Tool names, required arguments, markets, and response shapes are pinned to the
56
+ checked-in public contract. Update the contract first, then regenerate or
57
+ update both MCP adapters and their tests when the public API changes.
58
+
59
+ The image tool requires `image_url` and `market`. Its optional `query` field
60
+ adds shopper context such as a size, product role, compatibility requirement,
61
+ or use case; Simlir combines that context with the visual signal. Image-only
62
+ calls remain valid.
63
+
64
+ ## Development
65
+
66
+ ```bash
67
+ PYTHONPATH=../python-sdk/src:src python -m unittest discover -s tests -p 'test_*.py'
68
+ python -m compileall -q src
69
+ python -m pip wheel --no-deps --no-build-isolation --wheel-dir /tmp/simlir-python-mcp-wheel .
70
+ ```
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68,<77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "simlir-mcp"
7
+ version = "0.1.0"
8
+ description = "A thin stdio MCP bridge for the simlir public product API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "simlir engineering" }]
13
+ keywords = ["mcp", "model context protocol", "product data", "product search", "ai"]
14
+ classifiers = ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3"]
15
+ dependencies = ["simlir>=0.1.0,<0.2.0"]
16
+
17
+ [project.scripts]
18
+ simlir-mcp = "simlir_mcp.server:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+
23
+ [tool.simlir.release]
24
+ publication_enabled = true
25
+ contract_version = "1.0.0"
26
+ contract_source_sha256 = "9c0f28ec07c4208be53335568e0a86d95ebadb617dff7f37909489db382802c4"
27
+ publish_order = "simlir-sdk-before-simlir-mcp"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Python stdio MCP adapter for the simlir public API release candidate."""
2
+
3
+ from .server import SimlirMCPServer, create_client_from_environment, run_stdio
4
+
5
+ __all__ = ["SimlirMCPServer", "create_client_from_environment", "run_stdio"]
@@ -0,0 +1,5 @@
1
+ from .server import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Mapping
6
+ from dataclasses import asdict, is_dataclass
7
+ from typing import Any, Protocol
8
+
9
+ from simlir.errors import SimlirError
10
+
11
+ logger = logging.getLogger("simlir.mcp")
12
+ MCP_PROTOCOL_VERSION = "2025-03-26"
13
+ SERVER_NAME = "simlir"
14
+ SERVER_VERSION = "1.0.0"
15
+ MAX_JSON_LINE_BYTES = 1_000_000
16
+
17
+ MARKETS = ["gb", "us", "de", "fr", "es", "it", "au", "ca"]
18
+
19
+ TOOL_DEFINITIONS: tuple[dict[str, Any], ...] = (
20
+ {
21
+ "name": "simlir_search_products",
22
+ "description": "Search the simlir product database using a natural-language query.",
23
+ "inputSchema": {
24
+ "type": "object",
25
+ "properties": {
26
+ "query": {"type": "string", "minLength": 1, "maxLength": 1000},
27
+ "market": {"type": "string", "enum": MARKETS},
28
+ "category": {"type": "string", "maxLength": 160},
29
+ "brand": {"type": "string", "maxLength": 160},
30
+ "min_price": {"type": "number", "minimum": 0},
31
+ "max_price": {"type": "number", "minimum": 0},
32
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50},
33
+ "offset": {"type": "integer", "minimum": 0, "maximum": 500},
34
+ },
35
+ "required": ["query", "market"],
36
+ "additionalProperties": False,
37
+ },
38
+ },
39
+ {
40
+ "name": "simlir_search_products_by_image",
41
+ "description": "Find products using a hosted HTTPS image URL, optionally guided by natural-language context or requirements.",
42
+ "inputSchema": {
43
+ "type": "object",
44
+ "properties": {
45
+ "image_url": {"type": "string", "format": "uri", "maxLength": 2048},
46
+ "query": {"type": "string", "minLength": 1, "maxLength": 1000},
47
+ "market": {"type": "string", "enum": MARKETS},
48
+ "category": {"type": "string", "maxLength": 160},
49
+ "brand": {"type": "string", "maxLength": 160},
50
+ "min_price": {"type": "number", "minimum": 0},
51
+ "max_price": {"type": "number", "minimum": 0},
52
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50},
53
+ "offset": {"type": "integer", "minimum": 0, "maximum": 500},
54
+ },
55
+ "required": ["image_url", "market"],
56
+ "additionalProperties": False,
57
+ },
58
+ },
59
+ {
60
+ "name": "simlir_lookup_products_by_identifier",
61
+ "description": "Look up products by an exact GTIN, MPN/model number, or retailer SKU.",
62
+ "inputSchema": {
63
+ "type": "object",
64
+ "properties": {
65
+ "identifier": {"type": "string", "minLength": 1, "maxLength": 256},
66
+ "type": {"type": "string", "enum": ["auto", "gtin", "mpn", "sku"]},
67
+ "market": {"type": "string", "enum": MARKETS},
68
+ "brand": {"type": "string", "maxLength": 160},
69
+ "retailer": {"type": "string", "maxLength": 160},
70
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50},
71
+ },
72
+ "required": ["identifier", "market"],
73
+ "additionalProperties": False,
74
+ },
75
+ },
76
+ )
77
+
78
+
79
+ class SimlirClientLike(Protocol):
80
+ def search(self, **kwargs: Any) -> Any: ...
81
+
82
+ def search_by_image(self, **kwargs: Any) -> Any: ...
83
+
84
+ def lookup(self, **kwargs: Any) -> Any: ...
85
+
86
+
87
+ def _jsonable(value: Any) -> Any:
88
+ if is_dataclass(value) and not isinstance(value, type):
89
+ return asdict(value)
90
+ return value
91
+
92
+
93
+ def _safe_error(error: Exception) -> dict[str, Any]:
94
+ if isinstance(error, SimlirError):
95
+ payload: dict[str, Any] = {"error": error.code, "message": error.message}
96
+ if error.retry_after_seconds is not None:
97
+ payload["retry_after_seconds"] = error.retry_after_seconds
98
+ if error.request_id:
99
+ payload["request_id"] = error.request_id
100
+ return payload
101
+ return {"error": "client_error", "message": "The simlir MCP request could not be completed."}
102
+
103
+
104
+ def _tool_result(value: Any, is_error: bool = False) -> dict[str, Any]:
105
+ result: dict[str, Any] = {
106
+ "content": [{"type": "text", "text": json.dumps(_jsonable(value), separators=(",", ":"))}]
107
+ }
108
+ if is_error:
109
+ result["isError"] = True
110
+ return result
111
+
112
+
113
+ def _rpc_ok(request_id: Any, result: Any) -> dict[str, Any]:
114
+ return {"jsonrpc": "2.0", "result": result, "id": request_id}
115
+
116
+
117
+ def _rpc_error(request_id: Any, code: int, message: str) -> dict[str, Any]:
118
+ return {"jsonrpc": "2.0", "error": {"code": code, "message": message, "data": None}, "id": request_id}
119
+
120
+
121
+ def _arguments(value: Any) -> dict[str, Any]:
122
+ if value is None:
123
+ return {}
124
+ if not isinstance(value, Mapping):
125
+ raise SimlirError("invalid_params", "Tool arguments must be an object.")
126
+ return dict(value)
127
+
128
+
129
+ def _only(arguments: Mapping[str, Any], allowed: set[str]) -> dict[str, Any]:
130
+ if set(arguments) - allowed:
131
+ raise SimlirError("invalid_params", "Tool arguments contain an unsupported field.")
132
+ return dict(arguments)
133
+
134
+
135
+ def call_tool(client: SimlirClientLike, name: str, arguments: Any) -> dict[str, Any]:
136
+ try:
137
+ values = _arguments(arguments)
138
+ if name == "simlir_search_products":
139
+ allowed = {"query", "market", "category", "brand", "min_price", "max_price", "limit", "offset"}
140
+ return _tool_result(client.search(**_only(values, allowed)))
141
+ if name == "simlir_search_products_by_image":
142
+ allowed = {"image_url", "query", "market", "category", "brand", "min_price", "max_price", "limit", "offset"}
143
+ return _tool_result(client.search_by_image(**_only(values, allowed)))
144
+ if name == "simlir_lookup_products_by_identifier":
145
+ allowed = {"identifier", "market", "type", "brand", "retailer", "limit"}
146
+ return _tool_result(client.lookup(**_only(values, allowed)))
147
+ raise SimlirError("invalid_params", f"Unknown tool: {name[:120]}.")
148
+ except Exception as error:
149
+ logger.error(
150
+ json.dumps(
151
+ {"event": "tool.call.failed", "tool": name[:120], "code": getattr(error, "code", "client_error")},
152
+ separators=(",", ":"),
153
+ )
154
+ )
155
+ return _tool_result(_safe_error(error), is_error=True)
156
+
157
+
158
+ class SimlirMCPServer:
159
+ def __init__(self, client_factory: Any):
160
+ self._client_factory = client_factory
161
+ self._client: SimlirClientLike | None = None
162
+
163
+ def _client_or_create(self) -> SimlirClientLike:
164
+ if self._client is None:
165
+ self._client = self._client_factory()
166
+ return self._client
167
+
168
+ def handle(self, request: Mapping[str, Any]) -> dict[str, Any] | None:
169
+ request_id = request.get("id")
170
+ if request.get("jsonrpc") != "2.0" or not isinstance(request.get("method"), str):
171
+ return _rpc_error(request_id, -32600, "Invalid Request.")
172
+ method = request["method"]
173
+ if method == "initialize":
174
+ return _rpc_ok(
175
+ request_id,
176
+ {
177
+ "protocolVersion": MCP_PROTOCOL_VERSION,
178
+ "capabilities": {"tools": {"listChanged": False}},
179
+ "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
180
+ },
181
+ )
182
+ if method == "notifications/initialized":
183
+ return None
184
+ if method == "tools/list":
185
+ return _rpc_ok(request_id, {"tools": list(TOOL_DEFINITIONS)})
186
+ if method == "tools/call":
187
+ params = request.get("params")
188
+ if not isinstance(params, Mapping) or not isinstance(params.get("name"), str):
189
+ return _rpc_error(request_id, -32602, "Tool name is required.")
190
+ try:
191
+ client = self._client_or_create()
192
+ except Exception as error:
193
+ return _rpc_ok(request_id, _tool_result(_safe_error(error), is_error=True))
194
+ return _rpc_ok(request_id, call_tool(client, params["name"], params.get("arguments")))
195
+ return _rpc_error(request_id, -32601, f"Unknown method: {method[:120]}.")
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from collections.abc import Iterator, Mapping
7
+ from typing import Any
8
+
9
+ from simlir import Simlir
10
+ from simlir.errors import SimlirError
11
+
12
+ from .protocol import MAX_JSON_LINE_BYTES, SimlirMCPServer, _rpc_error
13
+
14
+
15
+ def create_client_from_environment(env: Mapping[str, str] | None = None) -> Simlir:
16
+ values = os.environ if env is None else env
17
+ api_key = values.get("SIMLIR_API_KEY", "")
18
+ if not api_key:
19
+ raise SimlirError("configuration_error", "SIMLIR_API_KEY is required.")
20
+ kwargs: dict[str, Any] = {"api_key": api_key}
21
+ if values.get("SIMLIR_API_BASE_URL"):
22
+ kwargs["base_url"] = values["SIMLIR_API_BASE_URL"]
23
+ if values.get("SIMLIR_API_TIMEOUT"):
24
+ try:
25
+ kwargs["timeout"] = float(values["SIMLIR_API_TIMEOUT"])
26
+ except ValueError as error:
27
+ raise SimlirError("configuration_error", "SIMLIR_API_TIMEOUT must be numeric.") from error
28
+ if values.get("SIMLIR_API_MAX_RETRIES"):
29
+ try:
30
+ kwargs["max_retries"] = int(values["SIMLIR_API_MAX_RETRIES"])
31
+ except ValueError as error:
32
+ raise SimlirError("configuration_error", "SIMLIR_API_MAX_RETRIES must be an integer.") from error
33
+ return Simlir(**kwargs)
34
+
35
+
36
+ def handle_message(server: SimlirMCPServer, raw: str) -> dict[str, Any] | None:
37
+ try:
38
+ request = json.loads(raw)
39
+ except json.JSONDecodeError:
40
+ return _rpc_error(None, -32700, "Parse error.")
41
+ if not isinstance(request, Mapping):
42
+ return _rpc_error(None, -32600, "Invalid Request.")
43
+ return server.handle(request)
44
+
45
+
46
+ def _lines(stream: Any) -> Iterator[str]:
47
+ for raw in stream:
48
+ if len(raw.encode("utf-8")) > MAX_JSON_LINE_BYTES:
49
+ yield "{\"jsonrpc\":\"2.0\",\"method\":\"__oversize__\"}"
50
+ else:
51
+ yield raw
52
+
53
+
54
+ def run_stdio(server: SimlirMCPServer | None = None, input_stream: Any = None, output_stream: Any = None) -> None:
55
+ active_server = server or SimlirMCPServer(create_client_from_environment)
56
+ input_value = input_stream or sys.stdin
57
+ output_value = output_stream or sys.stdout
58
+ for raw in _lines(input_value):
59
+ response = handle_message(active_server, raw)
60
+ if response is not None:
61
+ output_value.write(json.dumps(response, separators=(",", ":")) + "\n")
62
+ output_value.flush()
63
+
64
+
65
+ def main() -> None:
66
+ run_stdio()
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.2
2
+ Name: simlir-mcp
3
+ Version: 0.1.0
4
+ Summary: A thin stdio MCP bridge for the simlir public product API
5
+ Author: simlir engineering
6
+ License: UNLICENSED — publication and legal licensing gate pending.
7
+
8
+ Keywords: mcp,model context protocol,product data,product search,ai
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: simlir<0.2.0,>=0.1.0
16
+
17
+ # simlir-mcp
18
+
19
+ Release candidate for a thin Python stdio adapter around the simlir public
20
+ product API. It is intentionally separate from Simlir’s internal
21
+ repository-intelligence MCP server and contains no ingestion code, customer
22
+ data, database credentials, or internal repository tooling.
23
+
24
+ The PyPI publication workflow is configured, but the package remains
25
+ unpublished pending legal licensing, public repository permissions, an assigned
26
+ engineering-maintainer role, PyPI name/policy checks, stable API deployment,
27
+ and release rollback evidence. Do not use the package name from a public
28
+ registry until those gates are recorded.
29
+
30
+ ## Local use
31
+
32
+ Install the SDK and this adapter from the local release candidates:
33
+
34
+ ```bash
35
+ python -m pip install --no-deps ../python-sdk
36
+ python -m pip install --no-deps .
37
+ export SIMLIR_API_KEY=sk_your_key_here
38
+ python -m simlir_mcp
39
+ ```
40
+
41
+ The adapter keeps JSON-RPC responses on stdout and writes only structured,
42
+ metadata-only diagnostics to stderr. Never put an API key in a checked-in
43
+ configuration file.
44
+
45
+ ## Claude Desktop, Cursor, or VS Code
46
+
47
+ After the external publication and stable-URL gates pass, the intended local
48
+ configuration is:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "simlir": {
54
+ "command": "simlir-mcp",
55
+ "env": {
56
+ "SIMLIR_API_KEY": "${SIMLIR_API_KEY}"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Supported tools
64
+
65
+ | Tool | API operation |
66
+ | --- | --- |
67
+ | `simlir_search_products` | Semantic product search |
68
+ | `simlir_search_products_by_image` | Visual product search with optional natural-language context |
69
+ | `simlir_lookup_products_by_identifier` | Exact GTIN/MPN/SKU lookup |
70
+
71
+ Tool names, required arguments, markets, and response shapes are pinned to the
72
+ checked-in public contract. Update the contract first, then regenerate or
73
+ update both MCP adapters and their tests when the public API changes.
74
+
75
+ The image tool requires `image_url` and `market`. Its optional `query` field
76
+ adds shopper context such as a size, product role, compatibility requirement,
77
+ or use case; Simlir combines that context with the visual signal. Image-only
78
+ calls remain valid.
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ PYTHONPATH=../python-sdk/src:src python -m unittest discover -s tests -p 'test_*.py'
84
+ python -m compileall -q src
85
+ python -m pip wheel --no-deps --no-build-isolation --wheel-dir /tmp/simlir-python-mcp-wheel .
86
+ ```
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/simlir_mcp/__init__.py
5
+ src/simlir_mcp/__main__.py
6
+ src/simlir_mcp/protocol.py
7
+ src/simlir_mcp/server.py
8
+ src/simlir_mcp.egg-info/PKG-INFO
9
+ src/simlir_mcp.egg-info/SOURCES.txt
10
+ src/simlir_mcp.egg-info/dependency_links.txt
11
+ src/simlir_mcp.egg-info/entry_points.txt
12
+ src/simlir_mcp.egg-info/requires.txt
13
+ src/simlir_mcp.egg-info/top_level.txt
14
+ tests/test_pypi_metadata.spec.py
15
+ tests/test_server.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ simlir-mcp = simlir_mcp.server:main
@@ -0,0 +1 @@
1
+ simlir<0.2.0,>=0.1.0
@@ -0,0 +1 @@
1
+ simlir_mcp
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ import unittest
5
+
6
+
7
+ ROOT = pathlib.Path(__file__).resolve().parents[3]
8
+
9
+
10
+ class PythonMcpPyPiMetadataTests(unittest.TestCase):
11
+ def test_publication_metadata_matches_configured_workflow(self) -> None:
12
+ pyproject = (ROOT / "developer-channels/python-mcp/pyproject.toml").read_text()
13
+ workflow = (ROOT / ".github/workflows/publish-python-mcp.yml").read_text()
14
+
15
+ self.assertIn('name = "simlir-mcp"', pyproject)
16
+ self.assertIn('requires = ["setuptools>=68,<77"]', pyproject)
17
+ self.assertIn("publication_enabled = true", pyproject)
18
+ self.assertIn("workflow_dispatch:", workflow)
19
+ self.assertIn("pypa/gh-action-pypi-publish@", workflow)
20
+ self.assertIn("name: pypi", workflow)
21
+ self.assertIn("Verify Python MCP distribution metadata", workflow)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ unittest.main()
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import json
5
+ import pathlib
6
+ import unittest
7
+
8
+ from simlir_mcp.protocol import MCP_PROTOCOL_VERSION, TOOL_DEFINITIONS
9
+ from simlir_mcp.server import SimlirMCPServer, create_client_from_environment, handle_message, run_stdio
10
+
11
+
12
+ class FakeClient:
13
+ def search(self, **kwargs):
14
+ return {"operation": "search", "arguments": kwargs}
15
+
16
+ def search_by_image(self, **kwargs):
17
+ return {"operation": "image", "arguments": kwargs}
18
+
19
+ def lookup(self, **kwargs):
20
+ return {"operation": "lookup", "arguments": kwargs}
21
+
22
+
23
+ class PythonMcpServerTests(unittest.TestCase):
24
+ def setUp(self):
25
+ self.server = SimlirMCPServer(lambda: FakeClient())
26
+
27
+ def test_initialize_and_tools_list_match_contract(self):
28
+ initialize = self.server.handle({"jsonrpc": "2.0", "id": 1, "method": "initialize"})
29
+ self.assertEqual(initialize["result"]["protocolVersion"], MCP_PROTOCOL_VERSION)
30
+ self.assertEqual([tool["name"] for tool in TOOL_DEFINITIONS], [
31
+ "simlir_search_products",
32
+ "simlir_search_products_by_image",
33
+ "simlir_lookup_products_by_identifier",
34
+ ])
35
+ listed = self.server.handle({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
36
+ self.assertEqual(listed["result"]["tools"], list(TOOL_DEFINITIONS))
37
+
38
+ def test_routes_all_tools_without_exposing_transport(self):
39
+ cases = [
40
+ ("simlir_search_products", {"query": "headphones", "market": "gb"}, "search"),
41
+ ("simlir_search_products_by_image", {"image_url": "https://example.com/item.jpg", "market": "gb", "query": "small flat"}, "image"),
42
+ ("simlir_lookup_products_by_identifier", {"identifier": "123", "market": "gb"}, "lookup"),
43
+ ]
44
+ for name, arguments, operation in cases:
45
+ response = self.server.handle({"jsonrpc": "2.0", "id": name, "method": "tools/call", "params": {"name": name, "arguments": arguments}})
46
+ payload = json.loads(response["result"]["content"][0]["text"])
47
+ self.assertEqual(payload["operation"], operation)
48
+ self.assertEqual(payload["arguments"], arguments)
49
+
50
+ def test_errors_are_safe_and_stdio_keeps_notifications_off_stdout(self):
51
+ unknown = self.server.handle({"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "not_a_tool", "arguments": {}}})
52
+ self.assertTrue(unknown["result"]["isError"])
53
+ self.assertNotIn("sk_", json.dumps(unknown))
54
+
55
+ output = io.StringIO()
56
+ run_stdio(self.server, io.StringIO('{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","id":4,"method":"tools/list"}\n'), output)
57
+ lines = output.getvalue().splitlines()
58
+ self.assertEqual(len(lines), 1)
59
+ self.assertEqual(json.loads(lines[0])["id"], 4)
60
+
61
+ def test_missing_configuration_is_a_safe_tool_error(self):
62
+ server = SimlirMCPServer(lambda: create_client_from_environment({}))
63
+ response = server.handle({"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {"name": "simlir_search_products", "arguments": {}}})
64
+ self.assertTrue(response["result"]["isError"])
65
+ payload = json.loads(response["result"]["content"][0]["text"])
66
+ self.assertEqual(payload["error"], "configuration_error")
67
+ self.assertNotIn("sk_", json.dumps(response))
68
+
69
+ def test_invalid_json_and_oversized_input_are_bounded(self):
70
+ invalid = handle_message(self.server, "not json")
71
+ self.assertEqual(invalid["error"]["code"], -32700)
72
+ output = io.StringIO()
73
+ run_stdio(self.server, io.StringIO("x" * 1_000_001 + "\n"), output)
74
+ self.assertEqual(json.loads(output.getvalue())["error"]["code"], -32601)
75
+
76
+ def test_pypi_build_uses_compatible_metadata_backend(self):
77
+ root = pathlib.Path(__file__).resolve().parents[3]
78
+ pyproject = (root / "developer-channels/python-mcp/pyproject.toml").read_text()
79
+ workflow = (root / ".github/workflows/publish-python-mcp.yml").read_text()
80
+ self.assertIn('requires = ["setuptools>=68,<77"]', pyproject)
81
+ self.assertIn("Verify Python MCP distribution metadata", workflow)
82
+ self.assertIn('metadata.get("Name") != "simlir-mcp"', workflow)
83
+ self.assertIn('metadata.get("Version") != "0.1.0"', workflow)
84
+
85
+
86
+ if __name__ == "__main__":
87
+ unittest.main()