Skip to content

Commit 4fb4d4d

Browse files
SEP-1046: Client credentials flow for M2M without user interaction (#1157)
Co-authored-by: Paul Carleton <paulc@anthropic.com>
1 parent b37f08a commit 4fb4d4d

File tree

14 files changed

+1298
-116
lines changed

14 files changed

+1298
-116
lines changed

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,30 @@ await server.connect(transport);
796796

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

799+
### Node.js Web Crypto (globalThis.crypto) compatibility
800+
801+
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`.
802+
803+
- **Node.js v19.0.0 and later**: `globalThis.crypto` is available by default.
804+
- **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
805+
Node version documentation. (See https://nodejs.org/dist/latest-v18.x/docs/api/globals.html#crypto )
806+
807+
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`:
808+
809+
```typescript
810+
import { webcrypto } from 'node:crypto';
811+
812+
if (typeof globalThis.crypto === 'undefined') {
813+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
814+
(globalThis as any).crypto = webcrypto as unknown as Crypto;
815+
}
816+
```
817+
818+
For production use, you can either:
819+
820+
- Run on a Node.js version where `globalThis.crypto` is available by default (recommended), or
821+
- Apply a similar polyfill early in your application's startup code when targeting older Node.js runtimes.
822+
799823
## Examples
800824

801825
### Echo Server
@@ -1636,6 +1660,64 @@ const result = await client.callTool({
16361660
});
16371661
```
16381662

1663+
### OAuth client authentication helpers
1664+
1665+
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:
1666+
1667+
- **ClientCredentialsProvider**: Uses the `client_credentials` grant with `client_secret_basic` authentication.
1668+
- **PrivateKeyJwtProvider**: Uses the `client_credentials` grant with `private_key_jwt` client authentication, signing a JWT assertion on each token request.
1669+
- **StaticPrivateKeyJwtProvider**: Similar to `PrivateKeyJwtProvider`, but accepts a pre-built JWT assertion string via `jwtBearerAssertion` and reuses it for token requests.
1670+
1671+
You can use these providers with the `StreamableHTTPClientTransport` and the high-level `auth()` helper:
1672+
1673+
```typescript
1674+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1675+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
1676+
import { ClientCredentialsProvider, PrivateKeyJwtProvider, StaticPrivateKeyJwtProvider } from '@modelcontextprotocol/sdk/client/auth-extensions.js';
1677+
import { auth } from '@modelcontextprotocol/sdk/client/auth.js';
1678+
1679+
const serverUrl = new URL('https://mcp.example.com/');
1680+
1681+
// Example: client_credentials with client_secret_basic
1682+
const basicProvider = new ClientCredentialsProvider({
1683+
clientId: process.env.CLIENT_ID!,
1684+
clientSecret: process.env.CLIENT_SECRET!,
1685+
clientName: 'example-basic-client'
1686+
});
1687+
1688+
// Example: client_credentials with private_key_jwt (JWT signed locally)
1689+
const privateKeyJwtProvider = new PrivateKeyJwtProvider({
1690+
clientId: process.env.CLIENT_ID!,
1691+
privateKey: process.env.CLIENT_PRIVATE_KEY_PEM!,
1692+
algorithm: 'RS256',
1693+
clientName: 'example-private-key-jwt-client',
1694+
jwtLifetimeSeconds: 300
1695+
});
1696+
1697+
// Example: client_credentials with a pre-built JWT assertion
1698+
const staticJwtProvider = new StaticPrivateKeyJwtProvider({
1699+
clientId: process.env.CLIENT_ID!,
1700+
jwtBearerAssertion: process.env.CLIENT_ASSERTION!,
1701+
clientName: 'example-static-private-key-jwt-client'
1702+
});
1703+
1704+
const transport = new StreamableHTTPClientTransport(serverUrl, {
1705+
authProvider: privateKeyJwtProvider
1706+
});
1707+
1708+
const client = new Client({
1709+
name: 'example-client',
1710+
version: '1.0.0'
1711+
});
1712+
1713+
// Perform the OAuth flow (including dynamic client registration if needed)
1714+
await auth(privateKeyJwtProvider, { serverUrl, fetchFn: transport.fetch });
1715+
1716+
await client.connect(transport);
1717+
```
1718+
1719+
If you need lower-level control, you can also use `createPrivateKeyJwtAuth()` directly to implement `addClientAuthentication` on a custom `OAuthClientProvider`.
1720+
16391721
### Proxy Authorization Requests Upstream
16401722

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

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"eventsource-parser": "^3.0.0",
9696
"express": "^5.0.1",
9797
"express-rate-limit": "^7.5.0",
98+
"jose": "^6.1.1",
9899
"pkce-challenge": "^5.0.0",
99100
"raw-body": "^3.0.0",
100101
"zod": "^3.25 || ^4.0",

0 commit comments

Comments
 (0)