mcpsmith 0.2.0__tar.gz → 0.2.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.
Files changed (67) hide show
  1. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.claude-plugin/plugin.json +2 -2
  2. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/PKG-INFO +2 -1
  3. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/pyproject.toml +2 -1
  4. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/server.json +2 -2
  5. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/skills/design-mcp-tools/SKILL.md +1 -1
  6. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/skills/extract-api/SKILL.md +1 -1
  7. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/__init__.py +1 -1
  8. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/_validate.py +7 -0
  9. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/guide.md +4 -2
  10. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/__init__.py +2 -1
  11. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/model.py +4 -0
  12. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/http.py +22 -5
  13. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/interpret.py +5 -1
  14. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_design.py +10 -0
  15. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_runtime.py +32 -2
  16. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/uv.lock +3 -1
  17. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.claude-plugin/marketplace.json +0 -0
  18. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.github/workflows/ci.yml +0 -0
  19. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.github/workflows/publish.yml +0 -0
  20. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.gitignore +0 -0
  21. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/AGENTS.md +0 -0
  22. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/LICENSE +0 -0
  23. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/README.md +0 -0
  24. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/design.md +0 -0
  25. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/superpowers/plans/2026-10-21-hub-core.md +0 -0
  26. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/superpowers/specs/2026-10-20-mcp-everything-design.md +0 -0
  27. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/skills/build-mcp-server/SKILL.md +0 -0
  28. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/browser.py +0 -0
  29. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/capture.py +0 -0
  30. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/check.py +0 -0
  31. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/cli.py +0 -0
  32. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/codegen.py +0 -0
  33. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/__init__.py +0 -0
  34. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/_digest.py +0 -0
  35. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/_draft.py +0 -0
  36. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/llm.py +0 -0
  37. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/hub.py +0 -0
  38. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/har.py +0 -0
  39. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/openapi.py +0 -0
  40. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/rea.py +0 -0
  41. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/record.py +0 -0
  42. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/__init__.py +0 -0
  43. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/server.py +0 -0
  44. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/shape.py +0 -0
  45. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/transforms.py +0 -0
  46. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/secrets.py +0 -0
  47. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/server.py +0 -0
  48. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/ci.yml +0 -0
  49. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/conftest.py.tmpl +0 -0
  50. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/gitignore +0 -0
  51. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/publish.yml +0 -0
  52. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/conftest.py +0 -0
  53. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fake_cdp.py +0 -0
  54. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/har/shop.har +0 -0
  55. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/openapi/petstore2.yaml +0 -0
  56. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/openapi/petstore3.json +0 -0
  57. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/openapi/tasks31.json +0 -0
  58. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/rea/scenario.json +0 -0
  59. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_browser.py +0 -0
  60. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_cli.py +0 -0
  61. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_codegen.py +0 -0
  62. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_hub.py +0 -0
  63. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_importers.py +0 -0
  64. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_interpret.py +0 -0
  65. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_record.py +0 -0
  66. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_secrets.py +0 -0
  67. {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_transforms.py +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcpsmith",
3
3
  "displayName": "mcpsmith",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "description": "Turn an OpenAPI file or a recorded website session into a curated, tested MCP server. Skills and an MCP server for your coding agent.",
6
6
  "author": {
7
7
  "name": "Sepehr Radmard",
@@ -23,7 +23,7 @@
23
23
  "mcpsmith": {
24
24
  "command": "uvx",
25
25
  "args": [
26
- "mcpsmith@0.2.0",
26
+ "mcpsmith@0.2.2",
27
27
  "mcp"
28
28
  ]
29
29
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcpsmith
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Turn an OpenAPI file or a recorded website session into a curated, tested MCP server, with an LLM designing the tools.
5
5
  Project-URL: Homepage, https://github.com/sepehr071/mcpsmith
6
6
  Project-URL: Issues, https://github.com/sepehr071/mcpsmith/issues
@@ -22,6 +22,7 @@ Requires-Dist: httpx>=0.27
22
22
  Requires-Dist: keyring>=25
23
23
  Requires-Dist: mcp<3,>=2.2
24
24
  Requires-Dist: pyyaml>=6
25
+ Requires-Dist: truststore>=0.9
25
26
  Requires-Dist: tzdata>=2024.1; sys_platform == 'win32'
26
27
  Requires-Dist: websockets>=12
27
28
  Provides-Extra: capture
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcpsmith"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Turn an OpenAPI file or a recorded website session into a curated, tested MCP server, with an LLM designing the tools."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,6 +22,7 @@ classifiers = [
22
22
  dependencies = [
23
23
  "mcp>=2.2,<3",
24
24
  "httpx>=0.27",
25
+ "truststore>=0.9",
25
26
  "pyyaml>=6",
26
27
  "websockets>=12",
27
28
  "keyring>=25",
@@ -8,13 +8,13 @@
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/sepehr071/mcpsmith",
11
- "version": "0.2.0",
11
+ "version": "0.2.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "pypi",
15
15
  "registryBaseUrl": "https://pypi.org",
16
16
  "identifier": "mcpsmith",
17
- "version": "0.2.0",
17
+ "version": "0.2.2",
18
18
  "runtimeHint": "uvx",
19
19
  "transport": {
20
20
  "type": "stdio"
@@ -49,7 +49,7 @@ Safety and server
49
49
  - `example` for every tool with required args; real values that return data.
50
50
  - `instructions`: the cross-tool workflow ("search, then details by id"), units, currency, language, rate limits, limits of the data.
51
51
  - Secrets only via env vars (`<P>_...`) or `mcpsmith secret set`, never in args or descriptions.
52
- - Site that needs the user's login: `auth: {type: browser, login_path: /login}` (hub only; `login_path` is where the site redirects when logged out). `generate` refuses it: switch to token auth to export.
52
+ - Site that needs the user's login: `auth: {type: browser, login_path: /login}` (hub only; `login_path` is where the site redirects when logged out). Also set `session_cookie`, the login cookie's name: compare the cookie NAMES Chrome holds logged in vs logged out (`mcpsmith.browser.cookies_for`, names only, never print values). Some sites answer an ended session with HTTP 200 and an empty body. `generate` refuses it: switch to token auth to export.
53
53
 
54
54
  ## Measure
55
55
 
@@ -49,7 +49,7 @@ Read the digest and the guide fully, then edit `<workdir>/plan.yaml` yourself (s
49
49
  - Keep only tools for the goals from step 1; drop analytics, menus, config, auth plumbing.
50
50
  - Rename cryptic fields and args to what a person says (`emp_id` to `employee_id`, `Item_dev_sort_date` through `map: {newest: ...}`).
51
51
  - Add `transform` on args and `output.transforms` for dates, unix times, minutes, currency. Set `timezone` (IANA name; empty means the machine's local zone).
52
- - Set `auth: {type: browser, login_path: /Account/Login}`; `login_path` is the path the site redirects to when logged out.
52
+ - Set `auth: {type: browser, login_path: /Account/Login}`; `login_path` is the path the site redirects to when logged out. Also set `session_cookie` to the login cookie's name: compare the cookie NAMES Chrome holds logged in vs logged out (`mcpsmith.browser.cookies_for`, names only, never print values). Some sites answer an ended session with HTTP 200 and an empty body.
53
53
  - Fill `example` for every tool with values seen in the recording; `read_only: false` for anything that writes.
54
54
 
55
55
  ```
@@ -1,3 +1,3 @@
1
1
  """mcpsmith: turn an OpenAPI file or a recorded website session into a curated, tested MCP server."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
@@ -119,6 +119,13 @@ def validate(plan: Plan, source: Source | None = None) -> list[Issue]:
119
119
  check("plan.timezone", check_timezone, plan.timezone)
120
120
  if plan.auth and plan.auth.type == "browser":
121
121
  warn("plan.auth", "browser auth needs the hub's Chrome; switch to token auth before exporting.")
122
+ if not plan.auth.session_cookie:
123
+ warn(
124
+ "plan.auth.session_cookie",
125
+ "set auth.session_cookie so an ended session is reported instead of returning empty data.",
126
+ )
127
+ elif plan.auth and plan.auth.session_cookie:
128
+ err("plan.auth.session_cookie", "session_cookie only applies to browser auth.")
122
129
  if len(plan.tools) > 20:
123
130
  warn("plan.tools", f"{len(plan.tools)} tools; merge near-duplicates and drop noise to stay at 20 or fewer.")
124
131
 
@@ -89,10 +89,12 @@ Raw API responses waste the agent's context and bury the answer. For every tool:
89
89
 
90
90
  Plan: `name` (e.g. `petstore-mcp`), `title`, `description` (<= 100 chars), `instructions`, `prefix` (tool name
91
91
  prefix, e.g. `pet`), `version`, `base_url`, `env_prefix` (upper case, `^[A-Z][A-Z0-9_]*$`, ending in `_MCP`; `auth.env` must start with it; e.g.
92
- `PETSTORE_MCP`), `headers`, `auth` (`{name, type: bearer|header|query|cookie|basic|browser, param, env, login_path}`;
92
+ `PETSTORE_MCP`), `headers`, `auth` (`{name, type: bearer|header|query|cookie|basic|browser, param, env, login_path, session_cookie}`;
93
93
  `browser` and `login_path` only work in the hub, not in exported servers: `browser` reuses the developer's
94
94
  logged-in shared Chrome session, `login_path` is the path prefix the site redirects to when logged out, e.g.
95
- `/login`; `generate` refuses browser-auth plans, switch to bearer/header/cookie auth to export),
95
+ `/login`; `session_cookie` is the name of the cookie that proves the login (some sites answer an ended session with
96
+ HTTP 200 and an empty body, so no redirect shows it; find it by comparing the cookie names Chrome holds logged in vs
97
+ logged out, never their values); `generate` refuses browser-auth plans, switch to bearer/header/cookie auth to export),
96
98
  `impersonate`, `concurrency`, `cache_ttl`, `timezone` (IANA name for date transforms; empty = system local),
97
99
  `author`, `repository`, `tools`.
98
100
 
@@ -10,13 +10,14 @@ import yaml
10
10
 
11
11
  from mcpsmith.importers import har, openapi, rea
12
12
  from mcpsmith.model import Source
13
+ from mcpsmith.runtime.http import tls_context
13
14
 
14
15
 
15
16
  def load(path_or_url: str, hosts: list[str] | None = None) -> Source:
16
17
  """`hosts` filters HAR traffic to those hosts (exact or subdomain); ignored for OpenAPI."""
17
18
  origin = None
18
19
  if path_or_url.startswith(("http://", "https://")):
19
- resp = httpx.get(path_or_url, follow_redirects=True, timeout=60)
20
+ resp = httpx.get(path_or_url, follow_redirects=True, timeout=60, verify=tls_context())
20
21
  resp.raise_for_status()
21
22
  text, origin = resp.text, str(resp.url)
22
23
  else:
@@ -150,6 +150,10 @@ class AuthScheme(BaseModel):
150
150
  login_path: str = Field(
151
151
  "", description="Browser auth: path prefix whose redirect means logged out, e.g. '/Account/Login'."
152
152
  )
153
+ session_cookie: str = Field(
154
+ "",
155
+ description="Browser auth: name of the cookie that proves the user is logged in; absent means session ended.",
156
+ )
153
157
 
154
158
  @field_validator("env")
155
159
  @classmethod
@@ -16,12 +16,14 @@ import logging
16
16
  import os
17
17
  import random
18
18
  import re
19
+ import ssl
19
20
  import time
20
21
  from collections.abc import Awaitable, Callable
21
22
  from typing import Any
22
23
  from urllib.parse import quote
23
24
 
24
25
  import httpx
26
+ import truststore
25
27
  from mcp.server.mcpserver.exceptions import ToolError
26
28
 
27
29
  log = logging.getLogger("mcpsmith")
@@ -38,6 +40,12 @@ class ApiError(ToolError):
38
40
  self.status = status
39
41
 
40
42
 
43
+ @functools.cache
44
+ def tls_context() -> ssl.SSLContext:
45
+ """Verify with the OS trust store (fixes incomplete chains, honors corporate CAs); verification stays on."""
46
+ return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
47
+
48
+
41
49
  @functools.cache
42
50
  def _curl() -> tuple[type, Any] | None:
43
51
  """The curl_cffi transport class (Chrome TLS/HTTP2 fingerprint) and CurlOpt, or None without the extra."""
@@ -83,6 +91,7 @@ class Api:
83
91
  secret: Callable[[str], str | None] | None = None,
84
92
  cookie_provider: Callable[[str], Awaitable[dict[str, str]]] | None = None,
85
93
  login_path: str = "",
94
+ session_cookie: str = "",
86
95
  proxy: str | None = None,
87
96
  ):
88
97
  if auth and (auth.get("type") not in AUTH_TYPES or not (auth.get("env") or auth.get("type") == "browser")):
@@ -100,6 +109,7 @@ class Api:
100
109
  self.secret = secret
101
110
  self.cookie_provider = cookie_provider
102
111
  self.login_path = login_path
112
+ self.session_cookie = session_cookie
103
113
  self.proxy = proxy
104
114
  self._transport: httpx.AsyncBaseTransport | None = None
105
115
  self._client: httpx.AsyncClient | None = None
@@ -141,11 +151,14 @@ class Api:
141
151
  sent = self._add_auth(query, headers, cookies)
142
152
  if self.auth and self.auth["type"] == "browser" and self.cookie_provider:
143
153
  try:
144
- cookies.update(await self.cookie_provider(host))
154
+ jar = await self.cookie_provider(host)
145
155
  except (OSError, httpx.HTTPError, RuntimeError):
146
156
  raise ApiError(
147
157
  "The mcpsmith Chrome is not running. Start it with `mcpsmith browser`, log in, then retry."
148
158
  ) from None
159
+ if self.session_cookie and self.session_cookie not in jar:
160
+ raise ApiError(self._expired_message(host))
161
+ cookies.update(jar)
149
162
  sent = True
150
163
  if any(re.search(r"[;\r\n]", str(v)) for v in cookies.values()) or any(
151
164
  re.search(r"[\r\n]", str(v)) for v in headers.values()
@@ -178,9 +191,7 @@ class Api:
178
191
  break
179
192
 
180
193
  if self.auth and self.auth["type"] == "browser" and sent and self._expired(r):
181
- raise ApiError(
182
- f"{host} says the session has expired. Log in again in the mcpsmith Chrome window, then retry."
183
- )
194
+ raise ApiError(self._expired_message(host))
184
195
  if r.status_code >= 300:
185
196
  raise ApiError(self._status_message(r, content, host, sent), r.status_code)
186
197
  if not content:
@@ -196,6 +207,10 @@ class Api:
196
207
  self._cache[key] = (time.monotonic(), copy.deepcopy(data))
197
208
  return data
198
209
 
210
+ @staticmethod
211
+ def _expired_message(host: str) -> str:
212
+ return f"{host} says the session has expired. Log in again in the mcpsmith Chrome window, then retry."
213
+
199
214
  def _expired(self, r: httpx.Response) -> bool:
200
215
  """A 401, or a redirect to the site's login page."""
201
216
  loc = httpx.URL(r.headers.get("location", "")).path
@@ -283,7 +298,9 @@ class Api:
283
298
  proxy=proxy,
284
299
  max_connections=2 * self.concurrency,
285
300
  )
286
- return httpx.AsyncHTTPTransport(proxy=proxy, limits=httpx.Limits(max_connections=2 * self.concurrency))
301
+ return httpx.AsyncHTTPTransport(
302
+ verify=tls_context(), proxy=proxy, limits=httpx.Limits(max_connections=2 * self.concurrency)
303
+ )
287
304
 
288
305
  async def _request(self, method, url, query, headers, payload, host) -> tuple[httpx.Response, bytes]:
289
306
  client, limit = self._get_client()
@@ -65,7 +65,11 @@ def make_api(plan: Plan, source: Source | None, *, proxy: str | None = None, coo
65
65
  raise ConnectionError("mcpsmith Chrome is not running")
66
66
  return await browser.cookies_for(host, st["port"])
67
67
 
68
- extra = {"cookie_provider": cookie_provider or from_chrome, "login_path": plan.auth.login_path}
68
+ extra = {
69
+ "cookie_provider": cookie_provider or from_chrome,
70
+ "login_path": plan.auth.login_path,
71
+ "session_cookie": plan.auth.session_cookie,
72
+ }
69
73
  return Api(**api_kwargs(plan, source), secret=secret, proxy=proxy, **extra)
70
74
 
71
75
 
@@ -225,6 +225,16 @@ def test_browser_auth_warns():
225
225
  assert any(i.level == "warning" and "token" in i.message for i in issues)
226
226
 
227
227
 
228
+ def test_session_cookie_validation():
229
+ plan = valid_plan()
230
+ plan.auth = AuthScheme(name="b", type="browser", login_path="/Login")
231
+ assert any(i.level == "warning" and "session_cookie" in i.message for i in validate(plan))
232
+ plan.auth.session_cookie = "sid"
233
+ assert not [i for i in validate(plan) if "session_cookie" in i.message]
234
+ plan.auth = AuthScheme(name="t", type="bearer", env="EX_MCP_TOKEN", session_cookie="sid")
235
+ assert any("only applies to browser" in e.message for e in _errors(plan))
236
+
237
+
228
238
  def test_plan_schema_and_new_fields():
229
239
  assert "transform" in Plan.model_json_schema()["$defs"]["ToolArg"]["properties"]
230
240
  plan = valid_plan()
@@ -1,8 +1,10 @@
1
1
  import json
2
+ import ssl
2
3
  from typing import Annotated
3
4
 
4
5
  import httpx
5
6
  import pytest
7
+ import truststore
6
8
  from mcp import Client
7
9
  from pydantic import Field
8
10
 
@@ -307,6 +309,27 @@ async def test_browser_auth_session_expired(status, loc):
307
309
  assert "TOPSECRET" not in str(e.value)
308
310
 
309
311
 
312
+ @pytest.mark.parametrize("jar,sent", [({"tok": "CSRF-SECRET"}, 0), ({"tok": "CSRF-SECRET", "login": "x"}, 1)])
313
+ async def test_browser_auth_session_cookie(jar, sent):
314
+ seen = []
315
+
316
+ async def provider(host):
317
+ return jar
318
+
319
+ def handler(request):
320
+ seen.append(request)
321
+ return httpx.Response(200, json={})
322
+
323
+ api = make_api(handler, auth={"type": "browser"}, cookie_provider=provider, session_cookie="login")
324
+ if sent:
325
+ await api.call("GET", "/x")
326
+ else:
327
+ with pytest.raises(ApiError, match="says the session has expired") as e:
328
+ await api.call("GET", "/x")
329
+ assert "CSRF-SECRET" not in str(e.value)
330
+ assert len(seen) == sent
331
+
332
+
310
333
  async def test_browser_auth_chrome_not_running():
311
334
  async def down(host):
312
335
  raise ConnectionError("no chrome")
@@ -334,11 +357,11 @@ async def test_make_api_browser_needs_running_chrome(monkeypatch):
334
357
  prefix="x",
335
358
  base_url="https://api.example.com",
336
359
  env_prefix="EX_MCP",
337
- auth=AuthScheme(name="b", type="browser", login_path="/Login"),
360
+ auth=AuthScheme(name="b", type="browser", login_path="/Login", session_cookie="sid"),
338
361
  )
339
362
  api = build(plan, None)
340
363
  api.set_transport(httpx.MockTransport(lambda r: httpx.Response(200, json={})))
341
- assert api.login_path == "/Login"
364
+ assert api.login_path == "/Login" and api.session_cookie == "sid"
342
365
  with pytest.raises(ApiError, match="not running"):
343
366
  await api.call("GET", "/x")
344
367
 
@@ -351,3 +374,10 @@ async def test_browser_auth_cookie_errors_map_to_not_running(exc):
351
374
  api = make_api(lambda r: httpx.Response(200, json={}), auth={"type": "browser"}, cookie_provider=down)
352
375
  with pytest.raises(ApiError, match="The mcpsmith Chrome is not running"):
353
376
  await api.call("GET", "/x")
377
+
378
+
379
+ def test_plain_transport_verifies_with_os_trust_store():
380
+ transport = Api(base_url="https://api.example.com", env_prefix="EX_MCP")._make_transport(None)
381
+ ctx = transport._pool._ssl_context
382
+ assert isinstance(ctx, truststore.SSLContext) and ctx is http.tls_context()
383
+ assert ctx.verify_mode == ssl.CERT_REQUIRED
@@ -689,13 +689,14 @@ wheels = [
689
689
 
690
690
  [[package]]
691
691
  name = "mcpsmith"
692
- version = "0.2.0"
692
+ version = "0.2.2"
693
693
  source = { editable = "." }
694
694
  dependencies = [
695
695
  { name = "httpx" },
696
696
  { name = "keyring" },
697
697
  { name = "mcp" },
698
698
  { name = "pyyaml" },
699
+ { name = "truststore" },
699
700
  { name = "tzdata", marker = "sys_platform == 'win32'" },
700
701
  { name = "websockets", version = "16.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
701
702
  { name = "websockets", version = "17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@@ -726,6 +727,7 @@ requires-dist = [
726
727
  { name = "mcp", specifier = ">=2.2,<3" },
727
728
  { name = "playwright", marker = "extra == 'capture'", specifier = ">=1.45" },
728
729
  { name = "pyyaml", specifier = ">=6" },
730
+ { name = "truststore", specifier = ">=0.9" },
729
731
  { name = "tzdata", marker = "sys_platform == 'win32'", specifier = ">=2024.1" },
730
732
  { name = "websockets", specifier = ">=12" },
731
733
  ]
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
File without changes
File without changes
File without changes