mcpsmith 0.2.1__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.1 → mcpsmith-0.2.2}/.claude-plugin/plugin.json +2 -2
  2. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/PKG-INFO +1 -1
  3. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/pyproject.toml +1 -1
  4. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/server.json +2 -2
  5. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/skills/design-mcp-tools/SKILL.md +1 -1
  6. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/skills/extract-api/SKILL.md +1 -1
  7. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/__init__.py +1 -1
  8. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/design/_validate.py +7 -0
  9. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/design/guide.md +4 -2
  10. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/model.py +4 -0
  11. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/runtime/http.py +11 -4
  12. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/runtime/interpret.py +5 -1
  13. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_design.py +10 -0
  14. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_runtime.py +23 -2
  15. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/uv.lock +1 -1
  16. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/.claude-plugin/marketplace.json +0 -0
  17. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/.github/workflows/ci.yml +0 -0
  18. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/.github/workflows/publish.yml +0 -0
  19. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/.gitignore +0 -0
  20. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/AGENTS.md +0 -0
  21. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/LICENSE +0 -0
  22. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/README.md +0 -0
  23. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/docs/design.md +0 -0
  24. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/docs/superpowers/plans/2026-10-21-hub-core.md +0 -0
  25. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/docs/superpowers/specs/2026-10-20-mcp-everything-design.md +0 -0
  26. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/skills/build-mcp-server/SKILL.md +0 -0
  27. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/browser.py +0 -0
  28. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/capture.py +0 -0
  29. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/check.py +0 -0
  30. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/cli.py +0 -0
  31. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/codegen.py +0 -0
  32. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/design/__init__.py +0 -0
  33. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/design/_digest.py +0 -0
  34. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/design/_draft.py +0 -0
  35. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/design/llm.py +0 -0
  36. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/hub.py +0 -0
  37. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/importers/__init__.py +0 -0
  38. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/importers/har.py +0 -0
  39. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/importers/openapi.py +0 -0
  40. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/importers/rea.py +0 -0
  41. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/record.py +0 -0
  42. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/runtime/__init__.py +0 -0
  43. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/runtime/server.py +0 -0
  44. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/runtime/shape.py +0 -0
  45. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/runtime/transforms.py +0 -0
  46. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/secrets.py +0 -0
  47. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/server.py +0 -0
  48. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/templates/ci.yml +0 -0
  49. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/templates/conftest.py.tmpl +0 -0
  50. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/templates/gitignore +0 -0
  51. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/src/mcpsmith/templates/publish.yml +0 -0
  52. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/conftest.py +0 -0
  53. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/fake_cdp.py +0 -0
  54. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/fixtures/har/shop.har +0 -0
  55. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/fixtures/openapi/petstore2.yaml +0 -0
  56. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/fixtures/openapi/petstore3.json +0 -0
  57. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/fixtures/openapi/tasks31.json +0 -0
  58. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/fixtures/rea/scenario.json +0 -0
  59. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_browser.py +0 -0
  60. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_cli.py +0 -0
  61. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_codegen.py +0 -0
  62. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_hub.py +0 -0
  63. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_importers.py +0 -0
  64. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_interpret.py +0 -0
  65. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_record.py +0 -0
  66. {mcpsmith-0.2.1 → mcpsmith-0.2.2}/tests/test_secrets.py +0 -0
  67. {mcpsmith-0.2.1 → 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.1",
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.1",
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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcpsmith"
3
- version = "0.2.1"
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"
@@ -8,13 +8,13 @@
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/sepehr071/mcpsmith",
11
- "version": "0.2.1",
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.1",
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.1"
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
 
@@ -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
@@ -91,6 +91,7 @@ class Api:
91
91
  secret: Callable[[str], str | None] | None = None,
92
92
  cookie_provider: Callable[[str], Awaitable[dict[str, str]]] | None = None,
93
93
  login_path: str = "",
94
+ session_cookie: str = "",
94
95
  proxy: str | None = None,
95
96
  ):
96
97
  if auth and (auth.get("type") not in AUTH_TYPES or not (auth.get("env") or auth.get("type") == "browser")):
@@ -108,6 +109,7 @@ class Api:
108
109
  self.secret = secret
109
110
  self.cookie_provider = cookie_provider
110
111
  self.login_path = login_path
112
+ self.session_cookie = session_cookie
111
113
  self.proxy = proxy
112
114
  self._transport: httpx.AsyncBaseTransport | None = None
113
115
  self._client: httpx.AsyncClient | None = None
@@ -149,11 +151,14 @@ class Api:
149
151
  sent = self._add_auth(query, headers, cookies)
150
152
  if self.auth and self.auth["type"] == "browser" and self.cookie_provider:
151
153
  try:
152
- cookies.update(await self.cookie_provider(host))
154
+ jar = await self.cookie_provider(host)
153
155
  except (OSError, httpx.HTTPError, RuntimeError):
154
156
  raise ApiError(
155
157
  "The mcpsmith Chrome is not running. Start it with `mcpsmith browser`, log in, then retry."
156
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)
157
162
  sent = True
158
163
  if any(re.search(r"[;\r\n]", str(v)) for v in cookies.values()) or any(
159
164
  re.search(r"[\r\n]", str(v)) for v in headers.values()
@@ -186,9 +191,7 @@ class Api:
186
191
  break
187
192
 
188
193
  if self.auth and self.auth["type"] == "browser" and sent and self._expired(r):
189
- raise ApiError(
190
- f"{host} says the session has expired. Log in again in the mcpsmith Chrome window, then retry."
191
- )
194
+ raise ApiError(self._expired_message(host))
192
195
  if r.status_code >= 300:
193
196
  raise ApiError(self._status_message(r, content, host, sent), r.status_code)
194
197
  if not content:
@@ -204,6 +207,10 @@ class Api:
204
207
  self._cache[key] = (time.monotonic(), copy.deepcopy(data))
205
208
  return data
206
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
+
207
214
  def _expired(self, r: httpx.Response) -> bool:
208
215
  """A 401, or a redirect to the site's login page."""
209
216
  loc = httpx.URL(r.headers.get("location", "")).path
@@ -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()
@@ -309,6 +309,27 @@ async def test_browser_auth_session_expired(status, loc):
309
309
  assert "TOPSECRET" not in str(e.value)
310
310
 
311
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
+
312
333
  async def test_browser_auth_chrome_not_running():
313
334
  async def down(host):
314
335
  raise ConnectionError("no chrome")
@@ -336,11 +357,11 @@ async def test_make_api_browser_needs_running_chrome(monkeypatch):
336
357
  prefix="x",
337
358
  base_url="https://api.example.com",
338
359
  env_prefix="EX_MCP",
339
- auth=AuthScheme(name="b", type="browser", login_path="/Login"),
360
+ auth=AuthScheme(name="b", type="browser", login_path="/Login", session_cookie="sid"),
340
361
  )
341
362
  api = build(plan, None)
342
363
  api.set_transport(httpx.MockTransport(lambda r: httpx.Response(200, json={})))
343
- assert api.login_path == "/Login"
364
+ assert api.login_path == "/Login" and api.session_cookie == "sid"
344
365
  with pytest.raises(ApiError, match="not running"):
345
366
  await api.call("GET", "/x")
346
367
 
@@ -689,7 +689,7 @@ wheels = [
689
689
 
690
690
  [[package]]
691
691
  name = "mcpsmith"
692
- version = "0.2.1"
692
+ version = "0.2.2"
693
693
  source = { editable = "." }
694
694
  dependencies = [
695
695
  { name = "httpx" },
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