OAuth Client Credentials changedextensions/auth/oauth-client-credentials
Upstream edited this page at 28 Jul 2026 15:42 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 20:07 UTC.
Upstream edited
Recorded here
Lines+309added
Lines−323removed
From line
4
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits2to this page, all time
#### Using a client secret #### Using a JWT private key
The whole hunk
from line 4, old and new numbered
/
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 4
44
55The OAuth Client Credentials extension (`io.modelcontextprotocol/oauth-client-credentials`) adds support for the [OAuth 2.0 client credentials flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4) to MCP. This enables automated systems to connect to MCP servers without interactive user authorization.
66
7<Card title="Specification" icon="file-lines" href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/oauth-client-credentials.mdx">
8 Full technical specification for the OAuth Client Credentials extension.
9</Card>
7> **[Specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/oauth-client-credentials.mdx)**
8>
9> Full technical specification for the OAuth Client Credentials extension.
1010
1111## What it is
1212
from line 67
6767 Client->>MCP: MCP request (Bearer token)
6868```
6969
70<Warning>
71 Client secrets are **long-lived credentials** that grant access without user interaction. If a secret is leaked, an attacker can silently authenticate as your application until the secret is rotated. To reduce risk:
72
73 * Store secrets in a secrets manager, never in source code or environment files checked into version control.
74 * Rotate secrets on a regular schedule and immediately after any suspected compromise.
75 * Scope credentials to the minimum permissions required.
76 * Prefer JWT assertions when possible — they are short-lived and do not require transmitting the signing key.
77</Warning>
70> [!WARNING]
71>
72> Client secrets are **long-lived credentials** that grant access without user interaction. If a secret is leaked, an attacker can silently authenticate as your application until the secret is rotated. To reduce risk:
73>
74> * Store secrets in a secrets manager, never in source code or environment files checked into version control.
75> * Rotate secrets on a regular schedule and immediately after any suspected compromise.
76> * Scope credentials to the minimum permissions required.
77> * Prefer JWT assertions when possible — they are short-lived and do not require transmitting the signing key.
7878
7979## Implementation guide
8080
from line 82
8282
8383To use the OAuth Client Credentials extension, your client must:
8484
85<Steps>
86 <Step title="Declare support">
87 Include the extension in its per-request capabilities:
88
89 ```jsonc theme={null}
90 {
91 "jsonrpc": "2.0",
92 "id": 1,
93 "method": "...",
94 "params": {
95 // Other fields...
96 "_meta": {
97 // Other fields...
98 "io.modelcontextprotocol/clientCapabilities": {
99 "extensions": {
100 "io.modelcontextprotocol/oauth-client-credentials": {},
101 },
102 },
85**1. Declare support**
86
87Include the extension in its per-request capabilities:
88
89```jsonc theme={null}
90{
91 "jsonrpc": "2.0",
92 "id": 1,
93 "method": "...",
94 "params": {
95 // Other fields...
96 "_meta": {
97 // Other fields...
98 "io.modelcontextprotocol/clientCapabilities": {
99 "extensions": {
100 "io.modelcontextprotocol/oauth-client-credentials": {},
103101 },
104102 },
105 }
106 ```
107 </Step>
108
109 <Step title="Obtain an access token">
110 Request a token from the authorization server using the client credentials grant before connecting to the MCP server.
111 </Step>
112
113 <Step title="Include the token">
114 Pass the token in the `Authorization` header of HTTP requests to the MCP server:
115
116 ```
117 Authorization: Bearer <access_token>
118 ```
119 </Step>
120
121 <Step title="Handle token refresh">
122 Client credentials tokens typically have shorter lifetimes than user-delegated tokens. Implement token refresh logic to obtain a new token before expiry.
123 </Step>
124</Steps>
103 },
104 },
105}
106```
107
108**2. Obtain an access token**
109
110Request a token from the authorization server using the client credentials grant before connecting to the MCP server.
111
112**3. Include the token**
113
114Pass the token in the `Authorization` header of HTTP requests to the MCP server:
115
116```
117Authorization: Bearer <access_token>
118```
119
120**4. Handle token refresh**
121
122Client credentials tokens typically have shorter lifetimes than user-delegated tokens. Implement token refresh logic to obtain a new token before expiry.
125123
126124### For MCP servers
127125
128126To accept client credentials tokens, your server must:
129127
130<Steps>
131 <Step title="Validate the token">
132 On each request, verify the JWT signature and claims against your authorization server's public keys (usually via a JWKS endpoint).
133 </Step>
134
135 <Step title="Check scopes">
136 Ensure the token includes the required scopes for the requested operation.
137 </Step>
138
139 <Step title="Advertise support">
140 Optionally (but recommended for discoverability), include the extension in the `server/discover` response:
141
142 ```jsonc theme={null}
143 {
144 "jsonrpc": "2.0",
145 "id": 1,
146 "result": {
147 // Other fields...
148 "capabilities": {
149 "extensions": {
150 "io.modelcontextprotocol/oauth-client-credentials": {},
151 },
152 },
128**1. Validate the token**
129
130On each request, verify the JWT signature and claims against your authorization server's public keys (usually via a JWKS endpoint).
131
132**2. Check scopes**
133
134Ensure the token includes the required scopes for the requested operation.
135
136**3. Advertise support**
137
138Optionally (but recommended for discoverability), include the extension in the `server/discover` response:
139
140```jsonc theme={null}
141{
142 "jsonrpc": "2.0",
143 "id": 1,
144 "result": {
145 // Other fields...
146 "capabilities": {
147 "extensions": {
148 "io.modelcontextprotocol/oauth-client-credentials": {},
153149 },
154 }
155 ```
156 </Step>
157</Steps>
150 },
151 },
152}
153```
158154
159155## SDK examples
160156
161157The official MCP SDKs provide built-in support for client credentials authentication. Both handle token acquisition and refresh automatically.
162158
163<Steps>
164 <Step title="Install the SDK">
165 <Tabs>
166 <Tab title="TypeScript">
167 ```bash theme={null}
168 npm install @modelcontextprotocol/client
169 ```
170 </Tab>
171
172 <Tab title="Python">
173 ```bash theme={null}
174 pip install mcp
175 ```
176 </Tab>
177 </Tabs>
178 </Step>
179
180 <Step title="Create a provider and connect">
181 Choose the credential format that matches your setup:
182
183 #### Using a client secret
184
185 <Tabs>
186 <Tab title="TypeScript">
187 ```typescript theme={null}
188 import {
189 Client,
190 ClientCredentialsProvider,
191 StreamableHTTPClientTransport,
192 } from "@modelcontextprotocol/client";
193
194 const provider = new ClientCredentialsProvider({
195 clientId: "my-service",
196 clientSecret: "s3cr3t",
197 });
198
199 const client = new Client(
200 { name: "my-service", version: "1.0.0" },
201 { capabilities: {} },
202 );
203
204 const transport = new StreamableHTTPClientTransport(
205 new URL("https://mcp.example.com/mcp"),
206 { authProvider: provider },
207 );
208
209 await client.connect(transport);
210
211 // Use the client
212 const tools = await client.listTools();
213 console.log(
214 "Available tools:",
215 tools.tools.map((t) => t.name),
216 );
217
218 await transport.close();
219 ```
220 </Tab>
221
222 <Tab title="Python">
223 ```python theme={null}
224 import asyncio
225
226 import httpx2
227
228 from mcp import Client
229 from mcp.client.auth.extensions.client_credentials import (
230 ClientCredentialsOAuthProvider,
159**1. Install the SDK**
160
161**TypeScript**
162
163```bash theme={null}
164npm install @modelcontextprotocol/client
165```
166
167**Python**
168
169```bash theme={null}
170pip install mcp
171```
172
173**2. Create a provider and connect**
174
175Choose the credential format that matches your setup:
176
177#### Using a client secret
178
179**TypeScript**
180
181```typescript theme={null}
182import {
183 Client,
184 ClientCredentialsProvider,
185 StreamableHTTPClientTransport,
186} from "@modelcontextprotocol/client";
187
188const provider = new ClientCredentialsProvider({
189 clientId: "my-service",
190 clientSecret: "s3cr3t",
191});
192
193const client = new Client(
194 { name: "my-service", version: "1.0.0" },
195 { capabilities: {} },
196);
197
198const transport = new StreamableHTTPClientTransport(
199 new URL("https://mcp.example.com/mcp"),
200 { authProvider: provider },
201);
202
203await client.connect(transport);
204
205// Use the client
206const tools = await client.listTools();
207console.log(
208 "Available tools:",
209 tools.tools.map((t) => t.name),
210);
211
212await transport.close();
213```
214
215**Python**
216
217```python theme={null}
218import asyncio
219
220import httpx2
221
222from mcp import Client
223from mcp.client.auth.extensions.client_credentials import (
224 ClientCredentialsOAuthProvider,
225)
226from mcp.client.streamable_http import streamable_http_client
227from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
228
229
230class InMemoryTokenStorage:
231 def __init__(self) -> None:
232 self.tokens: OAuthToken | None = None
233 self.client_info: OAuthClientInformationFull | None = None
234
235 async def get_tokens(self) -> OAuthToken | None:
236 return self.tokens
237
238 async def set_tokens(self, tokens: OAuthToken) -> None:
239 self.tokens = tokens
240
241 async def get_client_info(self) -> OAuthClientInformationFull | None:
242 return self.client_info
243
244 async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
245 self.client_info = client_info
246
247
248provider = ClientCredentialsOAuthProvider(
249 server_url="https://mcp.example.com/mcp",
250 storage=InMemoryTokenStorage(),
251 client_id="my-service",
252 client_secret="s3cr3t",
253 scopes="read write",
254)
255
256
257async def main() -> None:
258 async with httpx2.AsyncClient(auth=provider) as http_client:
259 transport = streamable_http_client(
260 "https://mcp.example.com/mcp",
261 http_client=http_client,
231262 )
232 from mcp.client.streamable_http import streamable_http_client
233 from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
234
235
236 class InMemoryTokenStorage:
237 def __init__(self) -> None:
238 self.tokens: OAuthToken | None = None
239 self.client_info: OAuthClientInformationFull | None = None
240
241 async def get_tokens(self) -> OAuthToken | None:
242 return self.tokens
243
244 async def set_tokens(self, tokens: OAuthToken) -> None:
245 self.tokens = tokens
246
247 async def get_client_info(self) -> OAuthClientInformationFull | None:
248 return self.client_info
249
250 async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
251 self.client_info = client_info
252
253
254 provider = ClientCredentialsOAuthProvider(
255 server_url="https://mcp.example.com/mcp",
256 storage=InMemoryTokenStorage(),
257 client_id="my-service",
258 client_secret="s3cr3t",
259 scopes="read write",
263 async with Client(transport) as client:
264 # Use the client
265 tools = await client.list_tools()
266 print("Available tools:", [t.name for t in tools.tools])
267
268
269if __name__ == "__main__":
270 asyncio.run(main())
271```
272
273#### Using a JWT private key
274
275**TypeScript**
276
277```typescript theme={null}
278import {
279 Client,
280 PrivateKeyJwtProvider,
281 StreamableHTTPClientTransport,
282} from "@modelcontextprotocol/client";
283
284const provider = new PrivateKeyJwtProvider({
285 clientId: "my-service",
286 privateKey: process.env.CLIENT_PRIVATE_KEY_PEM,
287 algorithm: "RS256",
288});
289
290const client = new Client(
291 { name: "my-service", version: "1.0.0" },
292 { capabilities: {} },
293);
294
295const transport = new StreamableHTTPClientTransport(
296 new URL("https://mcp.example.com/mcp"),
297 { authProvider: provider },
298);
299
300await client.connect(transport);
301
302// Use the client
303const tools = await client.listTools();
304console.log(
305 "Available tools:",
306 tools.tools.map((t) => t.name),
307);
308
309await transport.close();
310```
311
312**Python**
313
314```python theme={null}
315import asyncio
316from pathlib import Path
317
318import httpx2
319
320from mcp import Client
321from mcp.client.auth.extensions.client_credentials import (
322 PrivateKeyJWTOAuthProvider,
323 SignedJWTParameters,
324)
325from mcp.client.streamable_http import streamable_http_client
326from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
327
328
329class InMemoryTokenStorage:
330 def __init__(self) -> None:
331 self.tokens: OAuthToken | None = None
332 self.client_info: OAuthClientInformationFull | None = None
333
334 async def get_tokens(self) -> OAuthToken | None:
335 return self.tokens
336
337 async def set_tokens(self, tokens: OAuthToken) -> None:
338 self.tokens = tokens
339
340 async def get_client_info(self) -> OAuthClientInformationFull | None:
341 return self.client_info
342
343 async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
344 self.client_info = client_info
345
346
347# Create a signed JWT assertion provider from key parameters
348jwt_params = SignedJWTParameters(
349 issuer="my-service",
350 subject="my-service",
351 signing_key=Path("private_key.pem").read_text(),
352 signing_algorithm="RS256",
353 lifetime_seconds=300,
354)
355
356provider = PrivateKeyJWTOAuthProvider(
357 server_url="https://mcp.example.com/mcp",
358 storage=InMemoryTokenStorage(),
359 client_id="my-service",
360 assertion_provider=jwt_params.create_assertion_provider(),
361 scopes="read write",
362)
363
364
365async def main() -> None:
366 async with httpx2.AsyncClient(auth=provider) as http_client:
367 transport = streamable_http_client(
368 "https://mcp.example.com/mcp",
369 http_client=http_client,
260370 )
261
262
263 async def main() -> None:
264 async with httpx2.AsyncClient(auth=provider) as http_client:
265 transport = streamable_http_client(
266 "https://mcp.example.com/mcp",
267 http_client=http_client,
268 )
269 async with Client(transport) as client:
270 # Use the client
271 tools = await client.list_tools()
272 print("Available tools:", [t.name for t in tools.tools])
273
274
275 if __name__ == "__main__":
276 asyncio.run(main())
277 ```
278 </Tab>
279 </Tabs>
280
281 #### Using a JWT private key
282
283 <Tabs>
284 <Tab title="TypeScript">
285 ```typescript theme={null}
286 import {
287 Client,
288 PrivateKeyJwtProvider,
289 StreamableHTTPClientTransport,
290 } from "@modelcontextprotocol/client";
291
292 const provider = new PrivateKeyJwtProvider({
293 clientId: "my-service",
294 privateKey: process.env.CLIENT_PRIVATE_KEY_PEM,
295 algorithm: "RS256",
296 });
297
298 const client = new Client(
299 { name: "my-service", version: "1.0.0" },
300 { capabilities: {} },
301 );
302
303 const transport = new StreamableHTTPClientTransport(
304 new URL("https://mcp.example.com/mcp"),
305 { authProvider: provider },
306 );
307
308 await client.connect(transport);
309
310 // Use the client
311 const tools = await client.listTools();
312 console.log(
313 "Available tools:",
314 tools.tools.map((t) => t.name),
315 );
316
317 await transport.close();
318 ```
319 </Tab>
320
321 <Tab title="Python">
322 ```python theme={null}
323 import asyncio
324 from pathlib import Path
325
326 import httpx2
327
328 from mcp import Client
329 from mcp.client.auth.extensions.client_credentials import (
330 PrivateKeyJWTOAuthProvider,
331 SignedJWTParameters,
332 )
333 from mcp.client.streamable_http import streamable_http_client
334 from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
335
336
337 class InMemoryTokenStorage:
338 def __init__(self) -> None:
339 self.tokens: OAuthToken | None = None
340 self.client_info: OAuthClientInformationFull | None = None
341
342 async def get_tokens(self) -> OAuthToken | None:
343 return self.tokens
344
345 async def set_tokens(self, tokens: OAuthToken) -> None:
346 self.tokens = tokens
347
348 async def get_client_info(self) -> OAuthClientInformationFull | None:
349 return self.client_info
350
351 async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
352 self.client_info = client_info
353
354
355 # Create a signed JWT assertion provider from key parameters
356 jwt_params = SignedJWTParameters(
357 issuer="my-service",
358 subject="my-service",
359 signing_key=Path("private_key.pem").read_text(),
360 signing_algorithm="RS256",
361 lifetime_seconds=300,
362 )
363
364 provider = PrivateKeyJWTOAuthProvider(
365 server_url="https://mcp.example.com/mcp",
366 storage=InMemoryTokenStorage(),
367 client_id="my-service",
368 assertion_provider=jwt_params.create_assertion_provider(),
369 scopes="read write",
370 )
371
372
373 async def main() -> None:
374 async with httpx2.AsyncClient(auth=provider) as http_client:
375 transport = streamable_http_client(
376 "https://mcp.example.com/mcp",
377 http_client=http_client,
378 )
379 async with Client(transport) as client:
380 # Use the client
381 tools = await client.list_tools()
382 print("Available tools:", [t.name for t in tools.tools])
383
384
385 if __name__ == "__main__":
386 asyncio.run(main())
387 ```
388 </Tab>
389 </Tabs>
390 </Step>
391</Steps>
371 async with Client(transport) as client:
372 # Use the client
373 tools = await client.list_tools()
374 print("Available tools:", [t.name for t in tools.tools])
375
376
377if __name__ == "__main__":
378 asyncio.run(main())
379```
392380
393381## Client support
394382
395<Note>
396 Support for this extension varies by client. Extensions are opt-in and never active by default.
397</Note>
383> [!NOTE]
384>
385> Support for this extension varies by client. Extensions are opt-in and never active by default.
398386
399387Check the [client matrix](/extensions/client-matrix) for current implementation status across MCP clients.
400388
401389## Related resources
402390
403<CardGroup cols={2}>
404 <Card title="ext-auth repository" icon="github" href="https://github.com/modelcontextprotocol/ext-auth">
405 Source code and reference implementations
406 </Card>
407
408 <Card title="Full specification" icon="file-lines" href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/oauth-client-credentials.mdx">
409 Technical specification with normative requirements
410 </Card>
411
412 <Card title="RFC 6749 — Client Credentials Grant" icon="link" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.4">
413 The underlying OAuth 2.0 specification
414 </Card>
415
416 <Card title="RFC 7523 — JWT Bearer Assertions" icon="link" href="https://datatracker.ietf.org/doc/html/rfc7523">
417 JWT assertion format specification
418 </Card>
419</CardGroup>
391> **[ext-auth repository](https://github.com/modelcontextprotocol/ext-auth)**
392>
393> Source code and reference implementations
394
395> **[Full specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/draft/oauth-client-credentials.mdx)**
396>
397> Technical specification with normative requirements
398
399> **[RFC 6749 — Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)**
400>
401> The underlying OAuth 2.0 specification
402
403> **[RFC 7523 — JWT Bearer Assertions](https://datatracker.ietf.org/doc/html/rfc7523)**
404>
405> JWT assertion format specification
420406
No line in this hunk matches that.