Skip to content

Commit a036707

Browse files
authored
Merge branch 'main' into fix_typo
2 parents 0317fee + ace9b62 commit a036707

33 files changed

+1634
-195
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: 14 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 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",
@@ -120,6 +121,7 @@
120121
"@types/cross-spawn": "^6.0.6",
121122
"@types/eventsource": "^1.1.15",
122123
"@types/express": "^5.0.0",
124+
"@types/express-serve-static-core": "^5.1.0",
123125
"@types/node": "^22.12.0",
124126
"@types/supertest": "^6.0.2",
125127
"@types/ws": "^8.5.12",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { setInterval } from 'node:timers';
2+
import process from 'node:process';
3+
import { McpServer } from '../server/mcp.js';
4+
import { StdioServerTransport } from '../server/stdio.js';
5+
6+
const transport = new StdioServerTransport();
7+
8+
const server = new McpServer(
9+
{
10+
name: 'server-that-hangs',
11+
title: 'Test Server that hangs',
12+
version: '1.0.0'
13+
},
14+
{
15+
capabilities: {
16+
logging: {}
17+
}
18+
}
19+
);
20+
21+
await server.connect(transport);
22+
23+
// Keep process alive even after stdin closes
24+
const keepAlive = setInterval(() => {}, 60_000);
25+
26+
// Prevent transport close from exiting
27+
transport.onclose = () => {
28+
// Intentionally ignore - we want to test the signal handling
29+
};
30+
31+
const doNotExitImmediately = async (signal: NodeJS.Signals) => {
32+
await server.sendLoggingMessage({
33+
level: 'debug',
34+
data: `received signal ${signal}`
35+
});
36+
// Clear keepalive but delay exit to simulate slow shutdown
37+
clearInterval(keepAlive);
38+
setInterval(() => {}, 30_000);
39+
};
40+
41+
process.on('SIGINT', doNotExitImmediately);
42+
process.on('SIGTERM', doNotExitImmediately);

src/__fixtures__/testServer.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { McpServer } from '../server/mcp.js';
2+
import { StdioServerTransport } from '../server/stdio.js';
3+
4+
const transport = new StdioServerTransport();
5+
6+
const server = new McpServer({
7+
name: 'test-server',
8+
version: '1.0.0'
9+
});
10+
11+
await server.connect(transport);
12+
13+
const exit = async () => {
14+
await server.close();
15+
process.exit(0);
16+
};
17+
18+
process.on('SIGINT', exit);
19+
process.on('SIGTERM', exit);

0 commit comments

Comments
 (0)