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.
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.claude-plugin/plugin.json +2 -2
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/PKG-INFO +2 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/pyproject.toml +2 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/server.json +2 -2
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/skills/design-mcp-tools/SKILL.md +1 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/skills/extract-api/SKILL.md +1 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/__init__.py +1 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/_validate.py +7 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/guide.md +4 -2
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/__init__.py +2 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/model.py +4 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/http.py +22 -5
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/interpret.py +5 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_design.py +10 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_runtime.py +32 -2
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/uv.lock +3 -1
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.claude-plugin/marketplace.json +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.github/workflows/ci.yml +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.github/workflows/publish.yml +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/.gitignore +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/AGENTS.md +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/LICENSE +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/README.md +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/design.md +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/superpowers/plans/2026-10-21-hub-core.md +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/superpowers/specs/2026-10-20-mcp-everything-design.md +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/skills/build-mcp-server/SKILL.md +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/browser.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/capture.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/check.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/cli.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/codegen.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/__init__.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/_digest.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/_draft.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/design/llm.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/hub.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/har.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/openapi.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/importers/rea.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/record.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/__init__.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/server.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/shape.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/runtime/transforms.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/secrets.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/server.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/ci.yml +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/conftest.py.tmpl +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/gitignore +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/src/mcpsmith/templates/publish.yml +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/conftest.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fake_cdp.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/har/shop.har +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/openapi/petstore2.yaml +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/openapi/petstore3.json +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/openapi/tasks31.json +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/fixtures/rea/scenario.json +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_browser.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_cli.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_codegen.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_hub.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_importers.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_interpret.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_record.py +0 -0
- {mcpsmith-0.2.0 → mcpsmith-0.2.2}/tests/test_secrets.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
```
|
|
@@ -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`; `
|
|
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
|
-
|
|
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(
|
|
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 = {
|
|
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.
|
|
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
|
{mcpsmith-0.2.0 → mcpsmith-0.2.2}/docs/superpowers/specs/2026-10-20-mcp-everything-design.md
RENAMED
|
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
|
|
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
|
|
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
|