Skip to content

Commit f16bc71

Browse files
committed
Add README, add throw if crypto not available in older Node versions
1 parent 5f99239 commit f16bc71

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,29 @@ await server.connect(transport);
790790

791791
To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
792792

793+
### Node.js Web Crypto (globalThis.crypto) compatibility
794+
795+
Some parts of the SDK (for example, JWT-based client authentication in `auth-extensions.ts` via `jose`) rely on the Web Crypto API exposed as `globalThis.crypto`.
796+
797+
- **Node.js v19.0.0 and later**: `globalThis.crypto` is available by default.
798+
- **Node.js v18.x**: `globalThis.crypto` may not be defined by default; in this repository we polyfill it for tests (see `vitest.setup.ts`), and you should do the same in your app if it is missing - or alternatively, run Node with `--experimental-global-webcrypto` as per your Node version documentation. (See https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#crypto )
799+
800+
If you run tests or applications on Node.js versions where `globalThis.crypto` is missing, you can polyfill it using the built-in `node:crypto` module, similar to the SDK's own `vitest.setup.ts`:
801+
802+
```typescript
803+
import { webcrypto } from 'node:crypto';
804+
805+
if (typeof globalThis.crypto === 'undefined') {
806+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
807+
(globalThis as any).crypto = webcrypto as unknown as Crypto;
808+
}
809+
```
810+
811+
For production use, you can either:
812+
813+
- Run on a Node.js version where `globalThis.crypto` is available by default (recommended), or
814+
- Apply a similar polyfill early in your application's startup code when targeting older Node.js runtimes.
815+
793816
## Examples
794817

795818
### Echo Server
@@ -1430,6 +1453,68 @@ const result = await client.callTool({
14301453
});
14311454
```
14321455

1456+
### OAuth client authentication helpers
1457+
1458+
For OAuth-secured MCP servers, the client `auth` module exposes a generic `OAuthClientProvider` interface, and `src/client/auth-extensions.ts` provides ready-to-use implementations for common machine-to-machine authentication flows:
1459+
1460+
- **ClientCredentialsProvider**: Uses the `client_credentials` grant with `client_secret_basic` authentication.
1461+
- **PrivateKeyJwtProvider**: Uses the `client_credentials` grant with `private_key_jwt` client authentication, signing a JWT assertion on each token request.
1462+
- **StaticPrivateKeyJwtProvider**: Similar to `PrivateKeyJwtProvider`, but accepts a pre-built JWT assertion string via `jwtBearerAssertion` and reuses it for token requests.
1463+
1464+
You can use these providers with the `StreamableHTTPClientTransport` and the high-level `auth()` helper:
1465+
1466+
```typescript
1467+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1468+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
1469+
import {
1470+
ClientCredentialsProvider,
1471+
PrivateKeyJwtProvider,
1472+
StaticPrivateKeyJwtProvider
1473+
} from '@modelcontextprotocol/sdk/client/auth-extensions.js';
1474+
import { auth } from '@modelcontextprotocol/sdk/client/auth.js';
1475+
1476+
const serverUrl = new URL('https://mcp.example.com/');
1477+
1478+
// Example: client_credentials with client_secret_basic
1479+
const basicProvider = new ClientCredentialsProvider({
1480+
clientId: process.env.CLIENT_ID!,
1481+
clientSecret: process.env.CLIENT_SECRET!,
1482+
clientName: 'example-basic-client'
1483+
});
1484+
1485+
// Example: client_credentials with private_key_jwt (JWT signed locally)
1486+
const privateKeyJwtProvider = new PrivateKeyJwtProvider({
1487+
clientId: process.env.CLIENT_ID!,
1488+
privateKey: process.env.CLIENT_PRIVATE_KEY_PEM!,
1489+
algorithm: 'RS256',
1490+
clientName: 'example-private-key-jwt-client',
1491+
jwtLifetimeSeconds: 300
1492+
});
1493+
1494+
// Example: client_credentials with a pre-built JWT assertion
1495+
const staticJwtProvider = new StaticPrivateKeyJwtProvider({
1496+
clientId: process.env.CLIENT_ID!,
1497+
jwtBearerAssertion: process.env.CLIENT_ASSERTION!,
1498+
clientName: 'example-static-private-key-jwt-client'
1499+
});
1500+
1501+
const transport = new StreamableHTTPClientTransport(serverUrl, {
1502+
authProvider: privateKeyJwtProvider
1503+
});
1504+
1505+
const client = new Client({
1506+
name: 'example-client',
1507+
version: '1.0.0'
1508+
});
1509+
1510+
// Perform the OAuth flow (including dynamic client registration if needed)
1511+
await auth(privateKeyJwtProvider, { serverUrl, fetchFn: transport.fetch });
1512+
1513+
await client.connect(transport);
1514+
```
1515+
1516+
If you need lower-level control, you can also use `createPrivateKeyJwtAuth()` directly to implement `addClientAuthentication` on a custom `OAuthClientProvider`.
1517+
14331518
### Proxy Authorization Requests Upstream
14341519

14351520
You can proxy OAuth requests to an external authorization provider:

src/client/auth-extensions.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,30 @@ describe('createPrivateKeyJwtAuth', () => {
218218
expect(parts).toHaveLength(3);
219219
});
220220

221+
it('throws when globalThis.crypto is not available', async () => {
222+
// Temporarily remove globalThis.crypto to simulate older Node.js runtimes
223+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
224+
const globalAny = globalThis as any;
225+
const originalCrypto = globalAny.crypto;
226+
// Use delete so that typeof globalThis.crypto === 'undefined'
227+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
228+
delete globalAny.crypto;
229+
230+
try {
231+
const addClientAuth = createPrivateKeyJwtAuth(baseOptions);
232+
const params = new URLSearchParams();
233+
234+
await expect(
235+
addClientAuth(new Headers(), params, 'https://auth.example.com/token', undefined)
236+
).rejects.toThrow(
237+
'crypto is not available, please ensure you add have Web Crypto API support for older Node.js versions'
238+
);
239+
} finally {
240+
// Restore original crypto to avoid affecting other tests
241+
globalAny.crypto = originalCrypto;
242+
}
243+
});
244+
221245
it('creates a signed JWT when using a Uint8Array HMAC key', async () => {
222246
const secret = new TextEncoder().encode('a-string-secret-at-least-256-bits-long');
223247

src/client/auth-extensions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export function createPrivateKeyJwtAuth(options: {
2727
}): AddClientAuthentication {
2828
return async (_headers, params, url, metadata) => {
2929
// Lazy import to avoid heavy dependency unless used
30+
if (typeof globalThis.crypto === 'undefined') {
31+
throw new TypeError('crypto is not available, please ensure you add have Web Crypto API support for older Node.js versions (see https://github.com/modelcontextprotocol/typescript-sdk#nodejs-web-crypto-globalthiscrypto-compatibility)');
32+
}
33+
3034
const jose = await import('jose');
3135

3236
const audience = String(options.audience ?? metadata?.issuer ?? url);

0 commit comments

Comments
 (0)