You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
pyjwt[crypto] as an unconditional dependency blocks installation on platforms without a cryptography wheel (e.g. Windows on ARM) #3373
pyjwt[crypto] is declared as an unconditional Requires-Dist in mcp's package metadata, even though the crypto extra (and the jwt import it enables) is only used by one file: mcp/client/auth/extensions/client_credentials.py, an OAuth client-credentials flow. Every consumer of this package — including a pure MCP server implementation that never acts as an OAuth client — is forced to install pyjwt[crypto], which pulls in cryptography as a hard dependency.
Why this matters
cryptography ships prebuilt wheels for most platforms, but not all — e.g. there is currently no PyPI wheel for win_arm64 (Windows on ARM). On a platform without a prebuilt wheel, pip/uv fall back to building cryptography from source via maturin/cargo/rustc, which additionally requires a working Rust toolchain and the MSVC linker (link.exe, part of Visual Studio Build Tools) to be present. A user with a plain Python install (no Visual Studio, no Rust) hits a hard installation failure:
error: linker `link.exe` not found
note: the msvc targets depend on the msvc linker but `link.exe` was not found
note: please ensure that Visual Studio 2017 or later, or Build Tools for
Visual Studio were installed with the Visual C++ option
This blocks installing mcp (and anything depending on it) entirely on that platform, for a feature (OAuth client-credentials auth) the user may never use — in our case, a Community-Edition-facing MCP server package that has no OAuth-client code path at all.
Suggested fix
mcp already uses the extras pattern for other optional functionality (cli, rich in the current metadata). Moving the OAuth client-credentials code's pyjwt[crypto] requirement behind its own extra (e.g. oauth or similar) would let server-only/non-OAuth-client consumers install mcp without pulling in cryptography at all, while OAuth-client users opt in explicitly with mcp[oauth].
Environment where this was found
Windows on ARM64 (win_arm64), Python 3.14 (win_arm64 build)
Both pip (venv fallback) and uv sync hit the identical build failure
Confirmed cryptography has no win_arm64 wheel on PyPI as of writing; only win_amd64 prebuilt wheels exist for this release
No Visual Studio / Build Tools installed (a normal state for an end user who is not a C/C++ developer)
The packaging fix is straightforward: pyjwt[crypto] belongs in an extra, not in the unconditional Requires-Dist. The only consumer is mcp/client/auth/extensions/client_credentials.py, and a server-only install never touches that path. Move it to something like mcp[client-credentials] (or fold it into an existing oauth extra) and declare the base dependency as pyjwt without the crypto extra, so plain installs stay free of cryptography.
Two details that make this clean: (1) make the jwt import lazy inside the client-credentials class rather than module-level, so importing mcp never triggers a jwt import; (2) keep pyjwt[crypto] in the extra because RS256 verification genuinely needs cryptography, so the extra stays as-is for the users who do run that flow.
If you want a fast interim while the metadata change lands: pip install mcp plus pyjwt[crypto] explicitly works on platforms with wheels, and the pyproject can pin an environment marker for platforms where cryptography lacks wheels. But the extras split is the durable fix.
We ship Python connectors that must install cleanly on Windows ARM and Apple Silicon (corpusiq.io), and the rule we enforce is optional crypto dependencies only behind extras with lazy imports, so the core package never hard-depends on a wheel that does not exist everywhere.
There is one packaging interaction worth resolving before treating the proposed extra split as complete.
MCPServer imports RequestStateSecurity from mcp.server.request_state at module import time, and that module imports cryptography unconditionally. MCPServer also installs RequestStateSecurity.ephemeral() by default. Therefore:
moving only pyjwt[crypto] behind an OAuth extra would make the server installation lose the dependency needed by the default request-state path; or
adding cryptography as a separate base dependency would preserve imports but leave the Windows ARM build blocker unchanged.
Would the intended scope include making request-state crypto optional/lazy, or should this issue be narrowed to a client-only packaging split? I verified the import chain against current main before commenting.
AI-assisted analysis: I reviewed the current source and packaging metadata, then checked the dependency boundary manually.
Confirming @jstar0's import-chain analysis, with one detail that makes the
split sharper: cryptography isn't declared in pyproject.toml at all mcp.server.request_state imports it directly (module level, lines 22-25)
but only receives it transitively via pyjwt[crypto].
So moving pyjwt[crypto] behind an oauth extra wouldn't degrade the
default request-state path, it would break it outright with an ImportError
on any server-only install.
Also worth noting for scoping: client_credentials.py defaults to RS256,
so it genuinely needs the crypto extra rather than plain pyjwt there's
no narrow win available there.
That seems to leave only two coherent options: declare cryptography as a
base dependency and accept that win_arm64 stays blocked, or make the
request-state crypto lazy so cryptography can become optional. Happy to
implement whichever you prefer.
cryptography's win_arm64 wheels are likely coming back on their own. Dropped in January after unstable ARM64 GitHub runners (pyca/cryptography#14216), restore PR is open and mergeable (pyca/cryptography#15350), maintainer targeting release 51, likely Sept/Oct. Once that lands the original install failure is gone either way.
Ouch, this one's rough did this block a fresh install for you, or break an existing env after an upgrade? I've hit near-identical pain from an unconditional extra silently breaking installs before, curious how you first noticed it (build failure vs. runtime error).
It was a fresh install, not an upgrade. I was setting up on a Windows ARM VM in Parallels, and both uv sync and pip install failed the same way: build error, not runtime. It tries to build cryptography from source because there's no win_arm64 wheel, and that needs Rust plus the MSVC linker (link.exe), which a normal Python setup doesn't have. Full error is in the issue body above.
Quick update from the pyca/cryptography side: the win_arm64 wheel restore PR (pyca/cryptography#15350) got approved and merged. They confirmed wheels are coming back in release 51, no date yet. So the underlying blocker should resolve itself once that ships, independent of what this issue ends up doing on the packaging side. If anyone's blocked right now, there's also a build workaround posted in that PR thread (vendored OpenSSL recipe) that apparently works.
Appreciate the detail, glad it's getting fixed properly upstream. Since you're clearly deep in this ecosystem: have you run into breaking changes on the protocol/contract side too (an MCP server or API changing its schema/params under you, rather than a packaging issue)? That's actually the exact thing I've been building a tool around, curious if it's a pain you've felt.
One other finding I dont think another comment has added to the conversation RE: fixing this. As of 2.3.0 cryptography is no longer only pulled in by the OAuth client. mcp/server/request_state.py imports it at module level:
mcp.server.mcpserver imports request_state, so every server that does from mcp.server.mcpserver import MCPServer imports cryptography, including a stdio-only one. mcp's metadata doesn't declare cryptography directly. It only arrives through pyjwt[crypto].
So if/when pyjwt[crypto] simply moves to an extra, import mcp.server fails. Either declare cryptography as a direct dependency or make request_state's import lazy/optional..
Activity
Note win_arm64 as a known install limitation
Ben-Home commentedon Aug 24, 2026
The packaging fix is straightforward: pyjwt[crypto] belongs in an extra, not in the unconditional Requires-Dist. The only consumer is mcp/client/auth/extensions/client_credentials.py, and a server-only install never touches that path. Move it to something like mcp[client-credentials] (or fold it into an existing oauth extra) and declare the base dependency as pyjwt without the crypto extra, so plain installs stay free of cryptography.
Two details that make this clean: (1) make the jwt import lazy inside the client-credentials class rather than module-level, so importing mcp never triggers a jwt import; (2) keep pyjwt[crypto] in the extra because RS256 verification genuinely needs cryptography, so the extra stays as-is for the users who do run that flow.
If you want a fast interim while the metadata change lands: pip install mcp plus pyjwt[crypto] explicitly works on platforms with wheels, and the pyproject can pin an environment marker for platforms where cryptography lacks wheels. But the extras split is the durable fix.
We ship Python connectors that must install cleanly on Windows ARM and Apple Silicon (corpusiq.io), and the rule we enforce is optional crypto dependencies only behind extras with lazy imports, so the core package never hard-depends on a wheel that does not exist everywhere.
jstar0 commentedon Aug 24, 2026
There is one packaging interaction worth resolving before treating the proposed extra split as complete.
MCPServerimportsRequestStateSecurityfrommcp.server.request_stateat module import time, and that module importscryptographyunconditionally.MCPServeralso installsRequestStateSecurity.ephemeral()by default. Therefore:pyjwt[crypto]behind an OAuth extra would make the server installation lose the dependency needed by the default request-state path; orcryptographyas a separate base dependency would preserve imports but leave the Windows ARM build blocker unchanged.Would the intended scope include making request-state crypto optional/lazy, or should this issue be narrowed to a client-only packaging split? I verified the import chain against current
mainbefore commenting.AI-assisted analysis: I reviewed the current source and packaging metadata, then checked the dependency boundary manually.
iam-kira commentedon Aug 24, 2026
Confirming @jstar0's import-chain analysis, with one detail that makes the
split sharper:
cryptographyisn't declared inpyproject.tomlat allmcp.server.request_stateimports it directly (module level, lines 22-25)but only receives it transitively via
pyjwt[crypto].So moving
pyjwt[crypto]behind anoauthextra wouldn't degrade thedefault request-state path, it would break it outright with an ImportError
on any server-only install.
Also worth noting for scoping:
client_credentials.pydefaults toRS256,so it genuinely needs the
cryptoextra rather than plainpyjwtthere'sno narrow win available there.
That seems to leave only two coherent options: declare
cryptographyas abase dependency and accept that win_arm64 stays blocked, or make the
request-state crypto lazy so
cryptographycan become optional. Happy toimplement whichever you prefer.
jtauschl commentedon Aug 25, 2026
cryptography's win_arm64 wheels are likely coming back on their own. Dropped in January after unstable ARM64 GitHub runners (pyca/cryptography#14216), restore PR is open and mergeable (pyca/cryptography#15350), maintainer targeting release 51, likely Sept/Oct. Once that lands the original install failure is gone either way.
anythingoah commentedon Sep 3, 2026
Ouch, this one's rough did this block a fresh install for you, or break an existing env after an upgrade? I've hit near-identical pain from an unconditional extra silently breaking installs before, curious how you first noticed it (build failure vs. runtime error).
jtauschl commentedon Sep 4, 2026
It was a fresh install, not an upgrade. I was setting up on a Windows ARM VM in Parallels, and both uv sync and pip install failed the same way: build error, not runtime. It tries to build cryptography from source because there's no win_arm64 wheel, and that needs Rust plus the MSVC linker (link.exe), which a normal Python setup doesn't have. Full error is in the issue body above.
Quick update from the pyca/cryptography side: the win_arm64 wheel restore PR (pyca/cryptography#15350) got approved and merged. They confirmed wheels are coming back in release 51, no date yet. So the underlying blocker should resolve itself once that ships, independent of what this issue ends up doing on the packaging side. If anyone's blocked right now, there's also a build workaround posted in that PR thread (vendored OpenSSL recipe) that apparently works.
anythingoah commentedon Sep 4, 2026
Appreciate the detail, glad it's getting fixed properly upstream. Since you're clearly deep in this ecosystem: have you run into breaking changes on the protocol/contract side too (an MCP server or API changing its schema/params under you, rather than a packaging issue)? That's actually the exact thing I've been building a tool around, curious if it's a pain you've felt.
woodrowpearson commentedon Oct 28, 2026
One other finding I dont think another comment has added to the conversation RE: fixing this. As of 2.3.0
cryptographyis no longer only pulled in by the OAuth client.mcp/server/request_state.pyimports it at module level:mcp.server.mcpserverimportsrequest_state, so every server that doesfrom mcp.server.mcpserver import MCPServerimportscryptography, including a stdio-only one. mcp's metadata doesn't declarecryptographydirectly. It only arrives throughpyjwt[crypto].So if/when
pyjwt[crypto]simply moves to an extra,import mcp.serverfails. Either declarecryptographyas a direct dependency or makerequest_state's import lazy/optional..