Follow Discord
Sweep 22 Oct 2026 · 21:04Z Build v2.1.309 528 read Stable v2.1.308 Latest v2.1.309 Next v2.1.309 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One capture · mcp

One read of Model Context Protocolmcp-20261020T200724Z

306 pages moved out of 357 read.

Pages moved 306 significant first
Pages read 357 in this capture
Captured 20:07 UTC
Corpus hash 077a51951f96 corpus-hash

What this read moved

101-125 of 306, page 5 of 13

This capture is too large to show at once. Changes 101-125 of 306 are below, significant first; the rest are on the following screens.

docs/draft/learn/architecture Changed · +399 / -407 lines

#### Understanding the Discovery Exchange #### How This Works in AI Applications #### Understanding the Tool Discovery Request #### Understanding the Tool Discovery Response #### How This Works in AI Applications #### Understanding the Tool Execution Request #### Key Elements of Tool Execution #### Understanding the Tool Execution Response #### How This Works in AI Applications #### Subscribing to Changes #### Understanding Tool List Change Notifications #### Key Features of MCP Notifications #### Client Response to Notifications #### Why Notifications Matter #### How This Works in AI Applications

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 15
1515* **MCP Development Tools**: Tools for developing MCP servers and clients, including the [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
1616* [MCP Reference Server Implementations](https://github.com/modelcontextprotocol/servers): Reference implementations of MCP servers.
1717 
18<Note>
19 MCP focuses solely on the protocol for context exchange—it does not dictate
20 how AI applications use LLMs or manage the provided context.
21</Note>
18> [!NOTE]
19>
20> MCP focuses solely on the protocol for context exchange—it does not dictate
21> how AI applications use LLMs or manage the provided context.
2222 
2323## Concepts of MCP
2424 
from line 104
104104 
105105#### Statelessness and discovery
106106 
107MCP is a <Tooltip tip="Every request contains all the information needed to process it, so servers infer nothing from previous requests">stateless protocol</Tooltip>. Every request carries the protocol version and the <Tooltip tip="Features and operations that a client or server supports, such as tools, resources, or prompts">capabilities</Tooltip> relevant to that request in its `_meta` field, so the server can process each request on its own. Clients should also identify themselves in the same field unless configured not to. Servers advertise their supported versions and capabilities through the mandatory [`server/discover`](/specification/draft/server/discover) request, which clients may send before any other request. Detailed information can be found in the [specification](/specification/draft/basic/index#statelessness), and the [example](#example) showcases the per-request metadata and the discovery sequence.
107MCP is a stateless protocol (Every request contains all the information needed to process it, so servers infer nothing from previous requests). Every request carries the protocol version and the capabilities (Features and operations that a client or server supports, such as tools, resources, or prompts) relevant to that request in its `_meta` field, so the server can process each request on its own. Clients should also identify themselves in the same field unless configured not to. Servers advertise their supported versions and capabilities through the mandatory [`server/discover`](/specification/draft/server/discover) request, which clients may send before any other request. Detailed information can be found in the [specification](/specification/draft/basic/index#statelessness), and the [example](#example) showcases the per-request metadata and the discovery sequence.
108108 
109109#### Primitives
110110 
from line 148
148148 
149149This section provides a step-by-step walkthrough of an MCP client-server interaction, focusing on the data layer protocol. We'll demonstrate discovery, tool operations, and notifications using JSON-RPC 2.0 messages.
150150 
151<Steps>
152 <Step title="Discovery">
153 As described in the [statelessness and discovery](#statelessness-and-discovery) section, every MCP request carries the protocol version and client capabilities in its `_meta` field, and clients should also include their identity there. A client that wants to learn what a server supports before issuing other requests sends a `server/discover` request, which every server must implement. The discovery response is typically cacheable, meaning it can be re-used so the discovery flow does not need to be performed for every request.
154 
155 <CodeGroup>
156 ```json Discover Request theme={null}
151**1. Discovery**
152 
153As described in the [statelessness and discovery](#statelessness-and-discovery) section, every MCP request carries the protocol version and client capabilities in its `_meta` field, and clients should also include their identity there. A client that wants to learn what a server supports before issuing other requests sends a `server/discover` request, which every server must implement. The discovery response is typically cacheable, meaning it can be re-used so the discovery flow does not need to be performed for every request.
154 
155```json Discover Request theme={null}
156{
157 "jsonrpc": "2.0",
158 "id": 1,
159 "method": "server/discover",
160 "params": {
161 "_meta": {
162 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
163 "io.modelcontextprotocol/clientInfo": {
164 "name": "example-client",
165 "version": "1.0.0"
166 },
167 "io.modelcontextprotocol/clientCapabilities": {
168 "elicitation": {}
169 }
170 }
171 }
172}
173```
174 
175```json Discover Response theme={null}
176{
177 "jsonrpc": "2.0",
178 "id": 1,
179 "result": {
180 "resultType": "complete",
181 "supportedVersions": ["2026-07-28"],
182 "capabilities": {
183 "tools": {
184 "listChanged": true
185 },
186 "resources": {}
187 },
188 "_meta": {
189 "io.modelcontextprotocol/serverInfo": {
190 "name": "example-server",
191 "version": "1.0.0"
192 }
193 },
194 "ttlMs": 3600000,
195 "cacheScope": "public"
196 }
197}
198```
199 
200#### Understanding the Discovery Exchange
201 
202The `_meta` fields and the discovery response together serve several purposes:
203 
2041. **Protocol Version Selection**: The `io.modelcontextprotocol/protocolVersion` field declares the version the client is speaking on this request, and `supportedVersions` in the response lists the versions the server accepts. If a server does not support the requested version, it rejects the request with an `UnsupportedProtocolVersionError` listing the versions it does support, and the client retries with a mutually supported version.
205 
2062. **Capability Discovery**: The client declares its capabilities in `io.modelcontextprotocol/clientCapabilities` on every request, and the server returns its own `capabilities` object from `server/discover`. This tells each party which [primitives](#primitives) the other can handle (tools, resources, prompts) and whether change [notifications](#notifications) are available, so unsupported operations are never attempted.
207 
2083. **Identity Exchange**: The `io.modelcontextprotocol/clientInfo` field in the request's `_meta` and the `io.modelcontextprotocol/serverInfo` field in the result's `_meta` provide identification and versioning information for debugging and compatibility purposes.
209 
210In this example, the exchange demonstrates how MCP capabilities are declared:
211 
212**Client Capabilities**:
213 
214* `"elicitation": {}` - The client declares it can gather additional input from the user when the server requests it
215 
216**Server Capabilities**:
217 
218* `"tools": {"listChanged": true}` - The server supports the tools primitive and can honor a `toolsListChanged` filter in [`subscriptions/listen`](/specification/draft/basic/patterns/subscriptions). Clients that request this filter receive `notifications/tools/list_changed` when the tool list changes.
219* `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
220 
221Calling `server/discover` is optional. Because every request carries the same `_meta` fields, a client is free to send any request directly and handle a version error if one comes back. Discovery is a convenient way to fetch the server's identity, capabilities, and supported versions in a single request.
222 
223#### How This Works in AI Applications
224 
225The AI application's MCP client manager connects to configured servers and stores their discovered capabilities for later use. The application uses this information to determine which servers can provide specific types of functionality (tools, resources, prompts) and whether they support real-time updates. In the Python SDK, discovery happens while the client connects. The results are then available on the client object.
226 
227```python Pseudo-code for AI application discovery theme={null}
228# Pseudo Code
229async with Client(stdio_client(server_config)) as client:
230 if client.server_capabilities.tools:
231 app.register_mcp_server(client, supports_tools=True)
232 app.set_server_ready(client)
233```
234 
235**2. Tool Discovery (Primitives)**
236 
237The client can discover available tools by sending a `tools/list` request. This request is fundamental to MCP's tool discovery mechanism: it allows clients to understand what tools are available on the server before attempting to use them.
238 
239```json Tools List Request theme={null}
240{
241 "jsonrpc": "2.0",
242 "id": 2,
243 "method": "tools/list",
244 "params": {
245 "_meta": {
246 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
247 "io.modelcontextprotocol/clientInfo": {
248 "name": "example-client",
249 "version": "1.0.0"
250 },
251 "io.modelcontextprotocol/clientCapabilities": {
252 "elicitation": {}
253 }
254 }
255 }
256}
257```
258 
259```json Tools List Response theme={null}
260{
261 "jsonrpc": "2.0",
262 "id": 2,
263 "result": {
264 "resultType": "complete",
265 "tools": [
157266 {
158 "jsonrpc": "2.0",
159 "id": 1,
160 "method": "server/discover",
161 "params": {
162 "_meta": {
163 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
164 "io.modelcontextprotocol/clientInfo": {
165 "name": "example-client",
166 "version": "1.0.0"
167 },
168 "io.modelcontextprotocol/clientCapabilities": {
169 "elicitation": {}
170 }
171 }
172 }
173 }
174 ```
175 
176 ```json Discover Response theme={null}
177 {
178 "jsonrpc": "2.0",
179 "id": 1,
180 "result": {
181 "resultType": "complete",
182 "supportedVersions": ["2026-07-28"],
183 "capabilities": {
184 "tools": {
185 "listChanged": true
186 },
187 "resources": {}
188 },
189 "_meta": {
190 "io.modelcontextprotocol/serverInfo": {
191 "name": "example-server",
192 "version": "1.0.0"
267 "name": "calculator_arithmetic",
268 "title": "Calculator",
269 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
270 "inputSchema": {
271 "type": "object",
272 "properties": {
273 "expression": {
274 "type": "string",
275 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
193276 }
194277 },
195 "ttlMs": 3600000,
196 "cacheScope": "public"
278 "required": ["expression"]
197279 }
198 }
199 ```
200 </CodeGroup>
201 
202 #### Understanding the Discovery Exchange
203 
204 The `_meta` fields and the discovery response together serve several purposes:
205 
206 1. **Protocol Version Selection**: The `io.modelcontextprotocol/protocolVersion` field declares the version the client is speaking on this request, and `supportedVersions` in the response lists the versions the server accepts. If a server does not support the requested version, it rejects the request with an `UnsupportedProtocolVersionError` listing the versions it does support, and the client retries with a mutually supported version.
207 
208 2. **Capability Discovery**: The client declares its capabilities in `io.modelcontextprotocol/clientCapabilities` on every request, and the server returns its own `capabilities` object from `server/discover`. This tells each party which [primitives](#primitives) the other can handle (tools, resources, prompts) and whether change [notifications](#notifications) are available, so unsupported operations are never attempted.
209 
210 3. **Identity Exchange**: The `io.modelcontextprotocol/clientInfo` field in the request's `_meta` and the `io.modelcontextprotocol/serverInfo` field in the result's `_meta` provide identification and versioning information for debugging and compatibility purposes.
211 
212 In this example, the exchange demonstrates how MCP capabilities are declared:
213 
214 **Client Capabilities**:
215 
216 * `"elicitation": {}` - The client declares it can gather additional input from the user when the server requests it
217 
218 **Server Capabilities**:
219 
220 * `"tools": {"listChanged": true}` - The server supports the tools primitive and can honor a `toolsListChanged` filter in [`subscriptions/listen`](/specification/draft/basic/patterns/subscriptions). Clients that request this filter receive `notifications/tools/list_changed` when the tool list changes.
221 * `"resources": {}` - The server also supports the resources primitive (can handle `resources/list` and `resources/read` methods)
222 
223 Calling `server/discover` is optional. Because every request carries the same `_meta` fields, a client is free to send any request directly and handle a version error if one comes back. Discovery is a convenient way to fetch the server's identity, capabilities, and supported versions in a single request.
224 
225 #### How This Works in AI Applications
226 
227 The AI application's MCP client manager connects to configured servers and stores their discovered capabilities for later use. The application uses this information to determine which servers can provide specific types of functionality (tools, resources, prompts) and whether they support real-time updates. In the Python SDK, discovery happens while the client connects. The results are then available on the client object.
228 
229 ```python Pseudo-code for AI application discovery theme={null}
230 # Pseudo Code
231 async with Client(stdio_client(server_config)) as client:
232 if client.server_capabilities.tools:
233 app.register_mcp_server(client, supports_tools=True)
234 app.set_server_ready(client)
235 ```
236 </Step>
237 
238 <Step title="Tool Discovery (Primitives)">
239 The client can discover available tools by sending a `tools/list` request. This request is fundamental to MCP's tool discovery mechanism: it allows clients to understand what tools are available on the server before attempting to use them.
240 
241 <CodeGroup>
242 ```json Tools List Request theme={null}
280 },
243281 {
244 "jsonrpc": "2.0",
245 "id": 2,
246 "method": "tools/list",
247 "params": {
248 "_meta": {
249 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
250 "io.modelcontextprotocol/clientInfo": {
251 "name": "example-client",
252 "version": "1.0.0"
282 "name": "weather_current",
283 "title": "Weather Information",
284 "description": "Get current weather information for any location worldwide",
285 "inputSchema": {
286 "type": "object",
287 "properties": {
288 "location": {
289 "type": "string",
290 "description": "City name, address, or coordinates (latitude,longitude)"
253291 },
254 "io.modelcontextprotocol/clientCapabilities": {
255 "elicitation": {}
292 "units": {
293 "type": "string",
294 "enum": ["metric", "imperial", "kelvin"],
295 "description": "Temperature units to use in response",
296 "default": "metric"
256297 }
257 }
298 },
299 "required": ["location"]
258300 }
259301 }
260 ```
261 
262 ```json Tools List Response theme={null}
302 ],
303 "ttlMs": 300000,
304 "cacheScope": "public"
305 }
306}
307```
308 
309#### Understanding the Tool Discovery Request
310 
311The `tools/list` request requires no parameters beyond the standard `_meta` fields that accompany every MCP request. It also accepts an optional `cursor` parameter for [pagination](/specification/draft/server/utilities/pagination), which the example above omits.
312 
313#### Understanding the Tool Discovery Response
314 
315The response contains a `tools` array that provides comprehensive metadata about each available tool. This array-based structure allows servers to expose multiple tools simultaneously while maintaining clear boundaries between different functionalities.
316 
317Each tool object in the response includes several key fields:
318 
319* **`name`**: A unique identifier for the tool within the server's namespace. This serves as the primary key for tool execution and should follow a clear naming pattern (e.g., `calculator_arithmetic` rather than just `calculate`)
320* **`title`**: A human-readable display name for the tool that clients can show to users
321* **`description`**: Detailed explanation of what the tool does and when to use it
322* **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
323 
324The result is marked `"resultType": "complete"` and carries two caching fields. `ttlMs` is a freshness hint in milliseconds, so this tool list can be cached for five minutes. `cacheScope` indicates who may reuse the response. The specification's [caching utility](/specification/draft/server/utilities/caching) defines the full rules.
325 
326#### How This Works in AI Applications
327 
328The AI application fetches available tools from all connected MCP servers and combines them into a unified tool registry that the language model can access. This allows the LLM to understand what actions it can perform and automatically generates the appropriate tool calls during conversations.
329 
330```python Pseudo-code for AI application tool discovery theme={null}
331# Pseudo-code using MCP Python SDK patterns
332available_tools = []
333for client in app.mcp_clients():
334 tools_response = await client.list_tools()
335 available_tools.extend(tools_response.tools)
336conversation.register_available_tools(available_tools)
337```
338 
339Clients that federate many servers can use [progressive tool discovery](/docs/draft/develop/clients/client-best-practices#progressive-tool-discovery) rather than loading every tool upfront.
340 
341**3. Tool Execution (Primitives)**
342 
343The client can now execute a tool using the `tools/call` method. This demonstrates how MCP primitives are used in practice: after discovering available tools, the client can invoke them with appropriate arguments.
344 
345#### Understanding the Tool Execution Request
346 
347The `tools/call` request follows a structured format that ensures type safety and clear communication between client and server. Note that we're using the proper tool name from the discovery response (`weather_current`) rather than a simplified name:
348 
349```json Tool Call Request theme={null}
350{
351 "jsonrpc": "2.0",
352 "id": 3,
353 "method": "tools/call",
354 "params": {
355 "name": "weather_current",
356 "arguments": {
357 "location": "San Francisco",
358 "units": "imperial"
359 },
360 "_meta": {
361 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
362 "io.modelcontextprotocol/clientInfo": {
363 "name": "example-client",
364 "version": "1.0.0"
365 },
366 "io.modelcontextprotocol/clientCapabilities": {
367 "elicitation": {}
368 }
369 }
370 }
371}
372```
373 
374```json Tool Call Response theme={null}
375{
376 "jsonrpc": "2.0",
377 "id": 3,
378 "result": {
379 "resultType": "complete",
380 "content": [
263381 {
264 "jsonrpc": "2.0",
265 "id": 2,
266 "result": {
267 "resultType": "complete",
268 "tools": [
269 {
270 "name": "calculator_arithmetic",
271 "title": "Calculator",
272 "description": "Perform mathematical calculations including basic arithmetic, trigonometric functions, and algebraic operations",
273 "inputSchema": {
274 "type": "object",
275 "properties": {
276 "expression": {
277 "type": "string",
278 "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', 'sin(30)', 'sqrt(16)')"
279 }
280 },
281 "required": ["expression"]
282 }
283 },
284 {
285 "name": "weather_current",
286 "title": "Weather Information",
287 "description": "Get current weather information for any location worldwide",
288 "inputSchema": {
289 "type": "object",
290 "properties": {
291 "location": {
292 "type": "string",
293 "description": "City name, address, or coordinates (latitude,longitude)"
294 },
295 "units": {
296 "type": "string",
297 "enum": ["metric", "imperial", "kelvin"],
298 "description": "Temperature units to use in response",
299 "default": "metric"
300 }
301 },
302 "required": ["location"]
303 }
304 }
305 ],
306 "ttlMs": 300000,
307 "cacheScope": "public"
308 }
309 }
310 ```
311 </CodeGroup>
312 
313 #### Understanding the Tool Discovery Request
314 
315 The `tools/list` request requires no parameters beyond the standard `_meta` fields that accompany every MCP request. It also accepts an optional `cursor` parameter for [pagination](/specification/draft/server/utilities/pagination), which the example above omits.
316 
317 #### Understanding the Tool Discovery Response
318 
319 The response contains a `tools` array that provides comprehensive metadata about each available tool. This array-based structure allows servers to expose multiple tools simultaneously while maintaining clear boundaries between different functionalities.
320 
321 Each tool object in the response includes several key fields:
322 
323 * **`name`**: A unique identifier for the tool within the server's namespace. This serves as the primary key for tool execution and should follow a clear naming pattern (e.g., `calculator_arithmetic` rather than just `calculate`)
324 * **`title`**: A human-readable display name for the tool that clients can show to users
325 * **`description`**: Detailed explanation of what the tool does and when to use it
326 * **`inputSchema`**: A JSON Schema that defines the expected input parameters, enabling type validation and providing clear documentation about required and optional parameters
327 
328 The result is marked `"resultType": "complete"` and carries two caching fields. `ttlMs` is a freshness hint in milliseconds, so this tool list can be cached for five minutes. `cacheScope` indicates who may reuse the response. The specification's [caching utility](/specification/draft/server/utilities/caching) defines the full rules.
329 
330 #### How This Works in AI Applications
331 
332 The AI application fetches available tools from all connected MCP servers and combines them into a unified tool registry that the language model can access. This allows the LLM to understand what actions it can perform and automatically generates the appropriate tool calls during conversations.
333 
334 ```python Pseudo-code for AI application tool discovery theme={null}
335 # Pseudo-code using MCP Python SDK patterns
336 available_tools = []
337 for client in app.mcp_clients():
338 tools_response = await client.list_tools()
339 available_tools.extend(tools_response.tools)
340 conversation.register_available_tools(available_tools)
341 ```
342 
343 Clients that federate many servers can use [progressive tool discovery](/docs/draft/develop/clients/client-best-practices#progressive-tool-discovery) rather than loading every tool upfront.
344 </Step>
345 
346 <Step title="Tool Execution (Primitives)">
347 The client can now execute a tool using the `tools/call` method. This demonstrates how MCP primitives are used in practice: after discovering available tools, the client can invoke them with appropriate arguments.
348 
349 #### Understanding the Tool Execution Request
350 
351 The `tools/call` request follows a structured format that ensures type safety and clear communication between client and server. Note that we're using the proper tool name from the discovery response (`weather_current`) rather than a simplified name:
352 
353 <CodeGroup>
354 ```json Tool Call Request theme={null}
355 {
356 "jsonrpc": "2.0",
357 "id": 3,
358 "method": "tools/call",
359 "params": {
360 "name": "weather_current",
361 "arguments": {
362 "location": "San Francisco",
363 "units": "imperial"
364 },
365 "_meta": {
366 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
367 "io.modelcontextprotocol/clientInfo": {
368 "name": "example-client",
369 "version": "1.0.0"
370 },
371 "io.modelcontextprotocol/clientCapabilities": {
372 "elicitation": {}
373 }
374 }
375 }
376 }
377 ```
378 
379 ```json Tool Call Response theme={null}
380 {
381 "jsonrpc": "2.0",
382 "id": 3,
383 "result": {
384 "resultType": "complete",
385 "content": [
386 {
387 "type": "text",
388 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
389 }
390 ]
391 }
392 }
393 ```
394 </CodeGroup>
395 
396 #### Key Elements of Tool Execution
397 
398 The request structure includes several important components:
399 
400 1. **`name`**: Must match exactly the tool name from the discovery response (`weather_current`). This ensures the server can correctly identify which tool to execute.
401 
402 2. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
403 * `location`: "San Francisco" (required parameter)
404 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
405 
406 3. **`_meta`**: Carries the standard per-request fields: the protocol version and client capabilities that every MCP request must include, plus the client's identity, which clients should include unless configured not to.
407 
408 4. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
409 
410 #### Understanding the Tool Execution Response
411 
412 The response demonstrates MCP's flexible content system:
413 
414 1. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
415 
416 2. **Content Types**: Each content object has a `type` field. In this example, `"type": "text"` indicates plain text content, but MCP supports various content types for different use cases.
417 
418 3. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
419 
420 This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
421 
422 #### How This Works in AI Applications
423 
424 When the language model decides to use a tool during a conversation, the AI application intercepts the tool call, routes it to the appropriate MCP server, executes it, and returns the results back to the LLM as part of the conversation flow. This enables the LLM to access real-time data and perform actions in the external world.
425 
426 ```python theme={null}
427 # Pseudo-code for AI application tool execution
428 async def handle_tool_call(conversation, tool_name, arguments):
429 client = app.find_mcp_client_for_tool(tool_name)
430 result = await client.call_tool(tool_name, arguments)
431 conversation.add_tool_result(result.content)
432 ```
433 </Step>
434 
435 <Step title="Real-time Updates (Notifications)">
436 MCP supports real-time notifications that enable servers to inform clients about changes without being polled for them. This demonstrates the notification system, a key feature that keeps clients synchronized and responsive.
437 
438 #### Subscribing to Changes
439 
440 Change notifications are opt-in. To receive them, the client opens a long-lived notification stream by sending a [`subscriptions/listen`](/specification/draft/basic/patterns/subscriptions) request with a `notifications` filter naming the event types it wants. Here the client asks for tool list changes:
441 
442 ```json Listen Request theme={null}
443 {
444 "jsonrpc": "2.0",
445 "id": 4,
446 "method": "subscriptions/listen",
447 "params": {
448 "_meta": {
449 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
450 "io.modelcontextprotocol/clientInfo": {
451 "name": "example-client",
452 "version": "1.0.0"
453 },
454 "io.modelcontextprotocol/clientCapabilities": {
455 "elicitation": {}
456 }
457 },
458 "notifications": {
459 "toolsListChanged": true
460 }
461 }
462 }
463 ```
464 
465 Every client request carries the `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` fields in `_meta`, and normally `io.modelcontextprotocol/clientInfo` as well, so the server can identify the client without relying on connection state.
466 
467 The server acknowledges the subscription with `notifications/subscriptions/acknowledged`, which is the first message carrying that subscription's ID in `_meta` (the server sends no other notification for that subscription before it). Its `notifications` field reflects the subset of the requested filter the server agreed to honor, with unsupported notification types omitted:
468 
469 ```json Acknowledgment theme={null}
470 {
471 "jsonrpc": "2.0",
472 "method": "notifications/subscriptions/acknowledged",
473 "params": {
474 "_meta": {
475 "io.modelcontextprotocol/subscriptionId": 4
476 },
477 "notifications": {
478 "toolsListChanged": true
479 }
480 }
481 }
482 ```
483 
484 #### Understanding Tool List Change Notifications
485 
486 After the acknowledgment, when the server's available tools change (for example, when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable), the server delivers a notification on that stream:
487 
488 ```json Notification theme={null}
489 {
490 "jsonrpc": "2.0",
491 "method": "notifications/tools/list_changed",
492 "params": {
493 "_meta": {
494 "io.modelcontextprotocol/subscriptionId": 4
495 }
496 }
497 }
498 ```
499 
500 #### Key Features of MCP Notifications
501 
502 1. **No Response Required**: Notice there's no `id` field in the notification. This follows JSON-RPC 2.0 notification semantics where no response is expected or sent.
503 
504 2. **Opt-In Based**: This notification is only sent to clients that requested `"toolsListChanged": true` in their `subscriptions/listen` filter, and it is only available from servers that declared `"listChanged": true` in their tools capability (as shown in Step 1).
505 
506 3. **Subscription-ID Tagging**: Every notification on the stream carries `io.modelcontextprotocol/subscriptionId` in `_meta`. The value is the JSON-RPC ID of the `subscriptions/listen` request that opened the stream (`4` in this example), so clients can correlate each notification with the subscription that produced it.
507 
508 4. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
509 
510 5. **Best Effort**: There are no guarantees that every notification will be sent or received, particularly across transport reconnects. Clients should also rely on polling to preserve freshness of results.
511 
512 #### Client Response to Notifications
513 
514 Upon receiving this notification, the client typically reacts by requesting the updated tool list. This creates a refresh cycle that keeps the client's understanding of available tools current:
515 
516 ```json Request theme={null}
517 {
518 "jsonrpc": "2.0",
519 "id": 5,
520 "method": "tools/list",
521 "params": {
522 "_meta": {
523 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
524 "io.modelcontextprotocol/clientInfo": {
525 "name": "example-client",
526 "version": "1.0.0"
527 },
528 "io.modelcontextprotocol/clientCapabilities": {
529 "elicitation": {}
530 }
531 }
532 }
533 }
534 ```
535 
536 #### Why Notifications Matter
537 
538 This notification system is crucial for several reasons:
539 
540 1. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
541 2. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
542 3. **Consistency**: Ensures clients always have accurate information about available server capabilities
543 4. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
544 
545 This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
546 
547 #### How This Works in AI Applications
548 
549 The AI application keeps a notification stream open for the changes it cares about. When one arrives, it immediately refreshes its tool registry and updates the LLM's available capabilities. This ensures that ongoing conversations always have access to the most current set of tools, and the LLM can dynamically adapt to new functionality as it becomes available.
550 
551 ```python theme={null}
552 # Pseudo-code for AI application notification handling
553 async def follow_tool_changes(client):
554 async with client.listen(tools_list_changed=True) as sub:
555 async for _event in sub:
556 tools_response = await client.list_tools()
557 app.update_available_tools(client, tools_response.tools)
558 if app.conversation.is_active():
559 app.conversation.notify_llm_of_new_capabilities()
560 ```
561 </Step>
562</Steps>
382 "type": "text",
383 "text": "Current weather in San Francisco: 68°F, partly cloudy with light winds from the west at 8 mph. Humidity: 65%"
384 }
385 ]
386 }
387}
388```
389 
390#### Key Elements of Tool Execution
391 
392The request structure includes several important components:
393 
3941. **`name`**: Must match exactly the tool name from the discovery response (`weather_current`). This ensures the server can correctly identify which tool to execute.
395 
3962. **`arguments`**: Contains the input parameters as defined by the tool's `inputSchema`. In this example:
397 * `location`: "San Francisco" (required parameter)
398 * `units`: "imperial" (optional parameter, defaults to "metric" if not specified)
399 
4003. **`_meta`**: Carries the standard per-request fields: the protocol version and client capabilities that every MCP request must include, plus the client's identity, which clients should include unless configured not to.
401 
4024. **JSON-RPC Structure**: Uses standard JSON-RPC 2.0 format with unique `id` for request-response correlation.
403 
404#### Understanding the Tool Execution Response
405 
406The response demonstrates MCP's flexible content system:
407 
4081. **`content` Array**: Tool responses return an array of content objects, allowing for rich, multi-format responses (text, images, resources, etc.)
409 
4102. **Content Types**: Each content object has a `type` field. In this example, `"type": "text"` indicates plain text content, but MCP supports various content types for different use cases.
411 
4123. **Structured Output**: The response provides actionable information that the AI application can use as context for language model interactions.
413 
414This execution pattern allows AI applications to dynamically invoke server functionality and receive structured responses that can be integrated into conversations with language models.
415 
416#### How This Works in AI Applications
417 
418When the language model decides to use a tool during a conversation, the AI application intercepts the tool call, routes it to the appropriate MCP server, executes it, and returns the results back to the LLM as part of the conversation flow. This enables the LLM to access real-time data and perform actions in the external world.
419 
420```python theme={null}
421# Pseudo-code for AI application tool execution
422async def handle_tool_call(conversation, tool_name, arguments):
423 client = app.find_mcp_client_for_tool(tool_name)
424 result = await client.call_tool(tool_name, arguments)
425 conversation.add_tool_result(result.content)
426```
427 
428**4. Real-time Updates (Notifications)**
429 
430MCP supports real-time notifications that enable servers to inform clients about changes without being polled for them. This demonstrates the notification system, a key feature that keeps clients synchronized and responsive.
431 
432#### Subscribing to Changes
433 
434Change notifications are opt-in. To receive them, the client opens a long-lived notification stream by sending a [`subscriptions/listen`](/specification/draft/basic/patterns/subscriptions) request with a `notifications` filter naming the event types it wants. Here the client asks for tool list changes:
435 
436```json Listen Request theme={null}
437{
438 "jsonrpc": "2.0",
439 "id": 4,
440 "method": "subscriptions/listen",
441 "params": {
442 "_meta": {
443 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
444 "io.modelcontextprotocol/clientInfo": {
445 "name": "example-client",
446 "version": "1.0.0"
447 },
448 "io.modelcontextprotocol/clientCapabilities": {
449 "elicitation": {}
450 }
451 },
452 "notifications": {
453 "toolsListChanged": true
454 }
455 }
456}
457```
458 
459Every client request carries the `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` fields in `_meta`, and normally `io.modelcontextprotocol/clientInfo` as well, so the server can identify the client without relying on connection state.
460 
461The server acknowledges the subscription with `notifications/subscriptions/acknowledged`, which is the first message carrying that subscription's ID in `_meta` (the server sends no other notification for that subscription before it). Its `notifications` field reflects the subset of the requested filter the server agreed to honor, with unsupported notification types omitted:
462 
463```json Acknowledgment theme={null}
464{
465 "jsonrpc": "2.0",
466 "method": "notifications/subscriptions/acknowledged",
467 "params": {
468 "_meta": {
469 "io.modelcontextprotocol/subscriptionId": 4
470 },
471 "notifications": {
472 "toolsListChanged": true
473 }
474 }
475}
476```
477 
478#### Understanding Tool List Change Notifications
479 
480After the acknowledgment, when the server's available tools change (for example, when new functionality becomes available, existing tools are modified, or tools become temporarily unavailable), the server delivers a notification on that stream:
481 
482```json Notification theme={null}
483{
484 "jsonrpc": "2.0",
485 "method": "notifications/tools/list_changed",
486 "params": {
487 "_meta": {
488 "io.modelcontextprotocol/subscriptionId": 4
489 }
490 }
491}
492```
493 
494#### Key Features of MCP Notifications
495 
4961. **No Response Required**: Notice there's no `id` field in the notification. This follows JSON-RPC 2.0 notification semantics where no response is expected or sent.
497 
4982. **Opt-In Based**: This notification is only sent to clients that requested `"toolsListChanged": true` in their `subscriptions/listen` filter, and it is only available from servers that declared `"listChanged": true` in their tools capability (as shown in Step 1).
499 
5003. **Subscription-ID Tagging**: Every notification on the stream carries `io.modelcontextprotocol/subscriptionId` in `_meta`. The value is the JSON-RPC ID of the `subscriptions/listen` request that opened the stream (`4` in this example), so clients can correlate each notification with the subscription that produced it.
501 
5024. **Event-Driven**: The server decides when to send notifications based on internal state changes, making MCP connections dynamic and responsive.
503 
5045. **Best Effort**: There are no guarantees that every notification will be sent or received, particularly across transport reconnects. Clients should also rely on polling to preserve freshness of results.
505 
506#### Client Response to Notifications
507 
508Upon receiving this notification, the client typically reacts by requesting the updated tool list. This creates a refresh cycle that keeps the client's understanding of available tools current:
509 
510```json Request theme={null}
511{
512 "jsonrpc": "2.0",
513 "id": 5,
514 "method": "tools/list",
515 "params": {
516 "_meta": {
517 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
518 "io.modelcontextprotocol/clientInfo": {
519 "name": "example-client",
520 "version": "1.0.0"
521 },
522 "io.modelcontextprotocol/clientCapabilities": {
523 "elicitation": {}
524 }
525 }
526 }
527}
528```
529 
530#### Why Notifications Matter
531 
532This notification system is crucial for several reasons:
533 
5341. **Dynamic Environments**: Tools may come and go based on server state, external dependencies, or user permissions
5352. **Efficiency**: Clients don't need to poll for changes; they're notified when updates occur
5363. **Consistency**: Ensures clients always have accurate information about available server capabilities
5374. **Real-time Collaboration**: Enables responsive AI applications that can adapt to changing contexts
538 
539This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers.
540 
541#### How This Works in AI Applications
542 
543The AI application keeps a notification stream open for the changes it cares about. When one arrives, it immediately refreshes its tool registry and updates the LLM's available capabilities. This ensures that ongoing conversations always have access to the most current set of tools, and the LLM can dynamically adapt to new functionality as it becomes available.
544 
545```python theme={null}
546# Pseudo-code for AI application notification handling
547async def follow_tool_changes(client):
548 async with client.listen(tools_list_changed=True) as sub:
549 async for _event in sub:
550 tools_response = await client.list_tools()
551 app.update_available_tools(client, tools_response.tools)
552 if app.conversation.is_active():
553 app.conversation.notify_llm_of_new_capabilities()
554```
563555 

docs/draft/learn/client-concepts Changed · +11 / -11 lines

from line 113
113113 
114114### Roots
115115 
116<Warning>
117 Roots are [deprecated](/specification/draft/deprecated) as of protocol version
118 `2026-07-28` and scheduled for removal. New implementations should pass
119 directories or files via tool parameters, resource URIs, or server
120 configuration instead.
121</Warning>
116> [!WARNING]
117>
118> Roots are [deprecated](/specification/draft/deprecated) as of protocol version
119> `2026-07-28` and scheduled for removal. New implementations should pass
120> directories or files via tool parameters, resource URIs, or server
121> configuration instead.
122122 
123123Roots define filesystem boundaries for server operations, allowing clients to specify which directories servers should focus on.
124124 
from line 169
169169 
170170### Sampling
171171 
172<Warning>
173 Sampling is [deprecated](/specification/draft/deprecated) as of protocol
174 version `2026-07-28` and scheduled for removal. New implementations should
175 integrate directly with LLM provider APIs instead.
176</Warning>
172> [!WARNING]
173>
174> Sampling is [deprecated](/specification/draft/deprecated) as of protocol
175> version `2026-07-28` and scheduled for removal. New implementations should
176> integrate directly with LLM provider APIs instead.
177177 
178178Sampling allows servers to request language model completions through the client, enabling agentic behaviors while maintaining security and user control.
179179 

docs/draft/learn/server-concepts Changed · +3 / -3 lines

from line 10
1010 
1111| Feature | Explanation | Examples | Who controls it |
1212| - | - | - | - |
13| **Tools** | Functions that your LLM can actively call, and decides when to use them based on user requests. Tools can write to databases, call external APIs, modify files, or trigger other logic. | Search flights<br />Send messages<br />Create calendar events | Model |
14| **Resources** | Passive data sources that provide read-only access to information for context, such as file contents, database schemas, or API documentation. | Retrieve documents<br />Access knowledge bases<br />Read calendars | Application |
15| **Prompts** | Pre-built instruction templates that tell the model to work with specific tools and resources. | Plan a vacation<br />Summarize my meetings<br />Draft an email | User |
13| **Tools** | Functions that your LLM can actively call, and decides when to use them based on user requests. Tools can write to databases, call external APIs, modify files, or trigger other logic. | Search flights Send messages Create calendar events | Model |
14| **Resources** | Passive data sources that provide read-only access to information for context, such as file contents, database schemas, or API documentation. | Retrieve documents Access knowledge bases Read calendars | Application |
15| **Prompts** | Pre-built instruction templates that tell the model to work with specific tools and resources. | Plan a vacation Summarize my meetings Draft an email | User |
1616 
1717We will use a hypothetical scenario to demonstrate the role of each of these features, and show how they can work together.
1818 

docs/draft/learn/versioning Changed · +5 / -5 lines

from line 3
33The Model Context Protocol uses string-based version identifiers following the format
44`YYYY-MM-DD`, to indicate the last date backwards incompatible changes were made.
55 
6<Info>
7 The protocol version will *not* be incremented when the
8 protocol is updated, as long as the changes maintain backwards compatibility. This allows
9 for incremental improvements while preserving interoperability.
10</Info>
6> [!NOTE]
7>
8> The protocol version will *not* be incremented when the
9> protocol is updated, as long as the changes maintain backwards compatibility. This allows
10> for incremental improvements while preserving interoperability.
1111 
1212## Revisions
1313 

docs/draft/sdk Changed · +16 / -18 lines

from line 8
88 
99| SDK | Repository | Tier |
1010| :- | :- | -: |
11| <Icon icon="square-js" size={24} />   [TypeScript](https://ts.sdk.modelcontextprotocol.io) | [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
12| <Icon icon="python" size={24} />   [Python](https://py.sdk.modelcontextprotocol.io) | [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
13| <Icon icon="square-c" size={24} />   [C#](https://csharp.sdk.modelcontextprotocol.io) | [modelcontextprotocol/csharp-sdk](https://github.com/modelcontextprotocol/csharp-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
14| <Icon icon="golang" size={24} />   [Go](https://go.sdk.modelcontextprotocol.io) | [modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
15| <Icon icon="rust" size={24} />   [Rust](https://rust.sdk.modelcontextprotocol.io) | [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
16| <Icon icon="gem" size={24} />   [Ruby](https://ruby.sdk.modelcontextprotocol.io) | [modelcontextprotocol/ruby-sdk](https://github.com/modelcontextprotocol/ruby-sdk) | <Badge color="blue" shape="pill">Tier 1</Badge> |
17| <Icon icon="java" size={24} />   [Java](https://java.sdk.modelcontextprotocol.io) | [modelcontextprotocol/java-sdk](https://github.com/modelcontextprotocol/java-sdk) | <Badge color="purple" shape="pill">Tier 2</Badge> |
18| <Icon icon="swift" size={24} />   Swift | [modelcontextprotocol/swift-sdk](https://github.com/modelcontextprotocol/swift-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
19| <Icon icon="php" size={24} />   [PHP](https://php.sdk.modelcontextprotocol.io) | [modelcontextprotocol/php-sdk](https://github.com/modelcontextprotocol/php-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
20| <Icon icon="square-k" size={24} />   [Kotlin](https://kotlin.sdk.modelcontextprotocol.io) | [modelcontextprotocol/kotlin-sdk](https://github.com/modelcontextprotocol/kotlin-sdk) | <Badge color="orange" shape="pill">Tier 3</Badge> |
11|   [TypeScript](https://ts.sdk.modelcontextprotocol.io) | [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) | Tier 1 |
12|   [Python](https://py.sdk.modelcontextprotocol.io) | [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) | Tier 1 |
13|   [C#](https://csharp.sdk.modelcontextprotocol.io) | [modelcontextprotocol/csharp-sdk](https://github.com/modelcontextprotocol/csharp-sdk) | Tier 1 |
14|   [Go](https://go.sdk.modelcontextprotocol.io) | [modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) | Tier 1 |
15|   [Rust](https://rust.sdk.modelcontextprotocol.io) | [modelcontextprotocol/rust-sdk](https://github.com/modelcontextprotocol/rust-sdk) | Tier 1 |
16|   [Ruby](https://ruby.sdk.modelcontextprotocol.io) | [modelcontextprotocol/ruby-sdk](https://github.com/modelcontextprotocol/ruby-sdk) | Tier 1 |
17|   [Java](https://java.sdk.modelcontextprotocol.io) | [modelcontextprotocol/java-sdk](https://github.com/modelcontextprotocol/java-sdk) | Tier 2 |
18|   Swift | [modelcontextprotocol/swift-sdk](https://github.com/modelcontextprotocol/swift-sdk) | Tier 3 |
19|   [PHP](https://php.sdk.modelcontextprotocol.io) | [modelcontextprotocol/php-sdk](https://github.com/modelcontextprotocol/php-sdk) | Tier 3 |
20|   [Kotlin](https://kotlin.sdk.modelcontextprotocol.io) | [modelcontextprotocol/kotlin-sdk](https://github.com/modelcontextprotocol/kotlin-sdk) | Tier 3 |
2121 
2222See [SDK Tiering System](/community/sdk-tiers) for details on what each tier means.
2323 
from line 36
3636 
3737Ready to start building with MCP? Choose your path:
3838 
39<CardGroup cols={2}>
40 <Card title="Build a Server" icon="server" href="/docs/draft/develop/build-server">
41 Learn how to create your first MCP server
42 </Card>
39> **[Build a Server](/docs/draft/develop/build-server)**
40>
41> Learn how to create your first MCP server
4342 
44 <Card title="Build a Client" icon="computer" href="/docs/draft/develop/build-client">
45 Create applications that connect to MCP servers
46 </Card>
47</CardGroup>
43> **[Build a Client](/docs/draft/develop/build-client)**
44>
45> Create applications that connect to MCP servers
4846 

docs/draft/tools/debugging Changed · +57 / -65 lines

from line 33
3333logged to stderr (standard error) will be captured by the host application
3434automatically.
3535 
36<Warning>
37 Local MCP servers should not log messages to stdout (standard out), as this
38 will interfere with protocol operation.
39</Warning>
36> [!WARNING]
37>
38> Local MCP servers should not log messages to stdout (standard out), as this
39> will interfere with protocol operation.
4040 
4141For servers using the
4242[Streamable HTTP transport](/specification/draft/basic/transports/streamable-http),
from line 45
4545tooling (curl, browser DevTools Network panel) to inspect requests and SSE
4646streams.
4747 
48<Warning>
49 The `notifications/message` mechanism below is deprecated as of protocol
50 version `2026-07-28`. It remains available during the deprecation window.
51</Warning>
48> [!WARNING]
49>
50> The `notifications/message` mechanism below is deprecated as of protocol
51> version `2026-07-28`. It remains available during the deprecation window.
5252 
5353For all [transports](/specification/latest/basic/transports), record what the
5454server is doing as it runs:
5555 
56<CodeGroup>
57 ```python Python theme={null}
58 import logging
56```python Python theme={null}
57import logging
5958 
60 from mcp.server import MCPServer
59from mcp.server import MCPServer
6160 
62 logger = logging.getLogger(__name__)
61logger = logging.getLogger(__name__)
6362 
64 mcp = MCPServer("reports")
63mcp = MCPServer("reports")
6564 
6665 
67 @mcp.tool()
68 async def fetch_report(report_id: str) -> str:
69 """Fetch a report by id."""
70 logger.info("Fetching report %s", report_id)
71 return f"Report {report_id} is ready."
72 ```
66@mcp.tool()
67async def fetch_report(report_id: str) -> str:
68 """Fetch a report by id."""
69 logger.info("Fetching report %s", report_id)
70 return f"Report {report_id} is ready."
71```
7372 
74 ```typescript TypeScript theme={null}
75 await server.sendLoggingMessage({
76 level: "info",
77 data: "Server started successfully",
78 });
79 ```
80</CodeGroup>
73```typescript TypeScript theme={null}
74await server.sendLoggingMessage({
75 level: "info",
76 data: "Server started successfully",
77});
78```
8179 
8280MCP defines eight
8381[RFC 5424 severity levels](/specification/latest/server/utilities/logging#log-levels)
from line 209
211209hover over the **Connectors** menu to see connected servers and available
212210tools.
213211 
214<img src="https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max&auto=format&n=zNouQwo2h8cbxlDS&q=85&s=e2ace1ac88895a5fe30ebd8d01456bc3" alt="Available MCP tools" width="437" height="244" data-path="images/available-mcp-tools.png" />
212![Available MCP tools](https://mintcdn.com/mcp/zNouQwo2h8cbxlDS/images/available-mcp-tools.png?fit=max\&auto=format\&n=zNouQwo2h8cbxlDS\&q=85\&s=e2ace1ac88895a5fe30ebd8d01456bc3)
215213 
216214### Viewing logs
217215 
from line 218
220218* macOS: `~/Library/Logs/Claude`
221219* Windows: `%APPDATA%\Claude\logs`
222220 
223<CodeGroup>
224 ```bash macOS theme={null}
225 tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
226 ```
221```bash macOS theme={null}
222tail -n 20 -F ~/Library/Logs/Claude/mcp*.log
223```
227224 
228 ```powershell Windows theme={null}
229 type "$env:AppData\Claude\logs\mcp*.log"
230 ```
231</CodeGroup>
225```powershell Windows theme={null}
226type "$env:AppData\Claude\logs\mcp*.log"
227```
232228 
233229The logs capture:
234230 
from line 240
244240 
2452411. Create a `developer_settings.json` file with `allowDevTools` set to true:
246242 
247<CodeGroup>
248 ```bash macOS theme={null}
249 echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
250 ```
243```bash macOS theme={null}
244echo '{"allowDevTools": true}' > ~/Library/Application\ Support/Claude/developer_settings.json
245```
251246 
252 ```powershell Windows theme={null}
253 '{"allowDevTools": true}' | Set-Content "$env:AppData\Claude\developer_settings.json"
254 ```
255</CodeGroup>
247```powershell Windows theme={null}
248'{"allowDevTools": true}' | Set-Content "$env:AppData\Claude\developer_settings.json"
249```
256250 
2572512. Open DevTools: `Command-Option-I` (macOS) or `Ctrl+Alt+I` (Windows)
258252 
from line 347
353347 
354348## Next steps
355349 
356<CardGroup cols={2}>
357 <Card title="MCP Inspector" icon="magnifying-glass" href="/docs/draft/tools/inspector">
358 Learn to use the MCP Inspector
359 </Card>
350> **[MCP Inspector](/docs/draft/tools/inspector)**
351>
352> Learn to use the MCP Inspector
360353 
361 <Card title="Build an MCP server" icon="code" href="/docs/draft/develop/build-server">
362 Walk through building a server from scratch
363 </Card>
354> **[Build an MCP server](/docs/draft/develop/build-server)**
355>
356> Walk through building a server from scratch
364357 
365 <Card title="Connect local servers" icon="plug" href="/docs/draft/develop/connect-local-servers">
366 Full claude\_desktop\_config.json reference and troubleshooting
367 </Card>
368</CardGroup>
358> **[Connect local servers](/docs/draft/develop/connect-local-servers)**
359>
360> Full claude\_desktop\_config.json reference and troubleshooting
369361 

docs/draft/tools/inspector Changed · +99 / -107 lines

from line 14
1414 
1515The package also installs **`mcpdo`**, an experimental [connection client](/docs/draft/tools/inspector/mcpdo) that connects to a server once and keeps it open across many commands, on the same core.
1616 
17<Frame caption="The MCP Inspector web client after a tool call, with the monitoring sidebar pinned and widened so the JSON-RPC exchange stays readable while you work.">
18 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
19</Frame>
17![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=2ced9eeefa020be53fe4fe957b7d07c6)
2018 
2119## Quickstart
2220 
2321The Inspector requires **Node 22.19.0 or newer** and runs directly through `npx`. No installation is required:
2422 
25<Tabs>
26 <Tab title="Web">
27 ```bash theme={null}
28 # Launch the web UI and connect to a local stdio server
29 npx @modelcontextprotocol/inspector node path/to/server/index.js
23**Web**
3024 
31 # Or launch with no target and add servers from the UI
32 npx @modelcontextprotocol/inspector
33 ```
25```bash theme={null}
26# Launch the web UI and connect to a local stdio server
27npx @modelcontextprotocol/inspector node path/to/server/index.js
3428 
35 The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](/docs/draft/tools/inspector/web).
36 </Tab>
29# Or launch with no target and add servers from the UI
30npx @modelcontextprotocol/inspector
31```
3732 
38 <Tab title="CLI">
39 ```bash theme={null}
40 # List a server's tools and exit
41 npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list
33The command prints a URL carrying a per-launch API token and opens it in your browser (set `MCP_AUTO_OPEN_ENABLED=false` to stop it opening). See [Web client](/docs/draft/tools/inspector/web).
4234 
43 # Call a tool and pipe the result into jq
44 npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http \
45 --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result
46 ```
35**CLI**
4736 
48 See [CLI client](/docs/draft/tools/inspector/cli).
49 </Tab>
37```bash theme={null}
38# List a server's tools and exit
39npx @modelcontextprotocol/inspector --cli node path/to/server/index.js --method tools/list
5040 
51 <Tab title="TUI">
52 ```bash theme={null}
53 npx @modelcontextprotocol/inspector --tui node path/to/server/index.js
54 ```
41# Call a tool and pipe the result into jq
42npx @modelcontextprotocol/inspector --cli https://api.example.com/mcp --transport http \
43 --method tools/call --tool-name get_weather --tool-arg city=Boston --format json | jq .result
44```
5545 
56 See [TUI client](/docs/draft/tools/inspector/tui).
57 </Tab>
58</Tabs>
46See [CLI client](/docs/draft/tools/inspector/cli).
5947 
48**TUI**
49 
50```bash theme={null}
51npx @modelcontextprotocol/inspector --tui node path/to/server/index.js
52```
53 
54See [TUI client](/docs/draft/tools/inspector/tui).
55 
6056### Inspecting published servers
6157 
6258Pass the command that launches the server as the Inspector's arguments, or point it at a remote server with `--server-url`:
6359 
64<Tabs>
65 <Tab title="npm package">
66 ```bash theme={null}
67 npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Desktop
68 ```
69 </Tab>
60**npm package**
7061 
71 <Tab title="PyPI package">
72 ```bash theme={null}
73 npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
74 ```
75 </Tab>
62```bash theme={null}
63npx -y @modelcontextprotocol/inspector npx @modelcontextprotocol/server-filesystem ~/Desktop
64```
7665 
77 <Tab title="Remote HTTP server">
78 ```bash theme={null}
79 npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
80 ```
81 </Tab>
82</Tabs>
66**PyPI package**
8367 
68```bash theme={null}
69npx @modelcontextprotocol/inspector uvx mcp-server-git --repository ~/code/mcp/servers.git
70```
71 
72**Remote HTTP server**
73 
74```bash theme={null}
75npx @modelcontextprotocol/inspector --server-url https://api.example.com/mcp --transport http
76```
77 
8478Always read a server's own README first, since every server requires different commands and arguments.
8579 
86<Warning>
87 **The Inspector stores secrets (OAuth tokens, OAuth client secrets, and stdio
88 `env:` values) in the OS keychain when one is available.** On a machine with
89 no keychain (Linux without libsecret or a Secret Service, headless and SSH
90 sessions, Termux, and containers with a mounted volume) they are saved to
91 `~/.mcp-inspector/secrets.json` instead, **unencrypted unless you supply a
92 key**. See [Where secrets are
93 stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored)
94 for how to get a keychain back, encrypt the file, or keep secrets in memory
95 only.
96</Warning>
80> [!WARNING]
81>
82> **The Inspector stores secrets (OAuth tokens, OAuth client secrets, and stdio
83> `env:` values) in the OS keychain when one is available.** On a machine with
84> no keychain (Linux without libsecret or a Secret Service, headless and SSH
85> sessions, Termux, and containers with a mounted volume) they are saved to
86> `~/.mcp-inspector/secrets.json` instead, **unencrypted unless you supply a
87> key**. See [Where secrets are
88> stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored)
89> for how to get a keychain back, encrypt the file, or keep secrets in memory
90> only.
9791 
9892## Launcher flags vs. client flags
9993 
from line 98
10498 
10599Everything else (`--catalog`, `--config`, `--server-url`, `--transport`, `--method`, the OAuth flags) is defined by the *client*, not the launcher, and the clients do not all define the same set. The [Configuration and flags](/docs/draft/tools/inspector/configuration) page is organized that way, by owner.
106100 
107<Note>
108 Mode flags are recognized only at the front of the command line: the first token that isn't `--web` / `--cli` / `--tui` ends launcher parsing, and everything after it is forwarded to the client unchanged. That's what lets a literal `--cli` appear later as one of your server's own arguments:
101> [!NOTE]
102>
103> Mode flags are recognized only at the front of the command line: the first token that isn't `--web` / `--cli` / `--tui` ends launcher parsing, and everything after it is forwarded to the client unchanged. That's what lets a literal `--cli` appear later as one of your server's own arguments:
104>
105> ```bash theme={null}
106> mcp-inspector --cli node server.js --cli # mode is CLI; the trailing --cli goes to server.js
107> ```
109108 
110 ```bash theme={null}
111 mcp-inspector --cli node server.js --cli # mode is CLI; the trailing --cli goes to server.js
112 ```
113</Note>
109> [!NOTE]
110>
111> `--help` behaves differently with and without a mode flag. Without one,
112> `-h`/`--help` anywhere on the command line (even after a server command)
113> prints the launcher's help and exits. With a mode flag it is forwarded, so
114> `mcp-inspector --cli --help` prints the CLI's full flag reference instead.
114115 
115<Note>
116 `--help` behaves differently with and without a mode flag. Without one,
117 `-h`/`--help` anywhere on the command line (even after a server command)
118 prints the launcher's help and exits. With a mode flag it is forwarded, so
119 `mcp-inspector --cli --help` prints the CLI's full flag reference instead.
120</Note>
121 
122116## Where to go next
123117 
124<CardGroup cols={2}>
125 <Card title="Web client" icon="browser" href="/docs/draft/tools/inspector/web">
126 A tab-by-tab walkthrough of the graphical inspector.
127 </Card>
118> **[Web client](/docs/draft/tools/inspector/web)**
119>
120> A tab-by-tab walkthrough of the graphical inspector.
128121 
129 <Card title="CLI client" icon="terminal" href="/docs/draft/tools/inspector/cli">
130 Method reference, output formats, exit codes, and CI recipes.
131 </Card>
122> **[CLI client](/docs/draft/tools/inspector/cli)**
123>
124> Method reference, output formats, exit codes, and CI recipes.
132125 
133 <Card title="TUI client" icon="table-columns" href="/docs/draft/tools/inspector/tui">
134 Terminal navigation and keyboard reference.
135 </Card>
126> **[TUI client](/docs/draft/tools/inspector/tui)**
127>
128> Terminal navigation and keyboard reference.
136129 
137 <Card title="Configuration and flags" icon="sliders" href="/docs/draft/tools/inspector/configuration">
138 Catalog vs. config files, the full per-client flag reference, and
139 environment variables.
140 </Card>
130> **[Configuration and flags](/docs/draft/tools/inspector/configuration)**
131>
132> Catalog vs. config files, the full per-client flag reference, and
133> environment variables.
141134 
142 <Card title="Authorization" icon="lock" href="/docs/draft/tools/inspector/authorization">
143 The OAuth flow end to end, mid-session re-authorization, and loopback
144 callbacks.
145 </Card>
135> **[Authorization](/docs/draft/tools/inspector/authorization)**
136>
137> The OAuth flow end to end, mid-session re-authorization, and loopback
138> callbacks.
146139 
147 <Card title="Protocol eras" icon="code-branch" href="/docs/draft/tools/inspector/protocol-eras">
148 Legacy vs. modern (2026-07-28) operation, and how every tab changes between
149 protocol eras.
150 </Card>
140> **[Protocol eras](/docs/draft/tools/inspector/protocol-eras)**
141>
142> Legacy vs. modern (2026-07-28) operation, and how every tab changes between
143> protocol eras.
151144 
152 <Card title="mcpdo connection client" icon="plug" href="/docs/draft/tools/inspector/mcpdo">
153 Connect once, then run many commands against a named connection.
154 </Card>
145> **[mcpdo connection client](/docs/draft/tools/inspector/mcpdo)**
146>
147> Connect once, then run many commands against a named connection.
155148 
156 <Card title="Security" icon="shield-halved" href="/docs/draft/tools/inspector/security">
157 The threat model: the web backend's token, Docker, secret storage, stdio
158 servers, and the mcpdo daemon.
159 </Card>
149> **[Security](/docs/draft/tools/inspector/security)**
150>
151> The threat model: the web backend's token, Docker, secret storage, stdio
152> servers, and the mcpdo daemon.
160153 
161 <Card title="Recipes" icon="book" href="/docs/draft/tools/inspector/recipes">
162 Importing client configs, reviewing MCP Apps, Docker, and network hosting.
163 </Card>
154> **[Recipes](/docs/draft/tools/inspector/recipes)**
155>
156> Importing client configs, reviewing MCP Apps, Docker, and network hosting.
164157 
165 <Card title="Debugging guide" icon="bug" href="/docs/draft/tools/debugging">
166 Broader debugging strategies beyond the Inspector.
167 </Card>
168</CardGroup>
158> **[Debugging guide](/docs/draft/tools/debugging)**
159>
160> Broader debugging strategies beyond the Inspector.
169161 

docs/draft/tools/inspector/authorization Changed · +58 / -62 lines

from line 6
66 
77## The flow, end to end
88 
9<Steps>
10 <Step title="Connect, and get refused">
11 The Inspector connects to the server URL. The server answers `401`. When the
12 response carries a `WWW-Authenticate` header, it points at the
13 protected-resource metadata URL (`resource_metadata`) and, optionally, the
14 scopes the request requires.
15 </Step>
9**1. Connect, and get refused**
1610 
17 <Step title="Discover the authorization server">
18 The Inspector fetches the server's [protected-resource and
19 authorization-server
20 metadata](/specification/latest/basic/authorization/authorization-server-discovery)
21 to learn the endpoints and the supported grants.
22 </Step>
11The Inspector connects to the server URL. The server answers `401`. When the
12response carries a `WWW-Authenticate` header, it points at the
13protected-resource metadata URL (`resource_metadata`) and, optionally, the
14scopes the request requires.
2315 
24 <Step title="Register or identify the client">
25 The Inspector identifies itself to the authorization server through
26 whichever mechanism is configured: [dynamic client
27 registration](/specification/latest/basic/authorization/client-registration#dynamic-client-registration),
28 a pre-registered static client (`--client-id` / `--client-secret`), a
29 [Client ID Metadata
30 Document](/specification/latest/basic/authorization/client-registration#client-id-metadata-documents)
31 (`--client-metadata-url`), or an [enterprise-managed
32 IdP](/extensions/auth/enterprise-managed-authorization).
33 </Step>
16**2. Discover the authorization server**
3417 
35 <Step title="Authorize in the browser">
36 The Inspector opens the authorization URL. You sign in and consent.
37 </Step>
18The Inspector fetches the server's [protected-resource and
19authorization-server
20metadata](/specification/latest/basic/authorization/authorization-server-discovery)
21to learn the endpoints and the supported grants.
3822 
39 <Step title="Receive the callback">
40 The authorization server redirects to the Inspector's callback URL, carrying
41 the authorization code.
42 </Step>
23**3. Register or identify the client**
4324 
44 <Step title="Exchange and retry">
45 The code is exchanged for tokens, the tokens are persisted, and the original
46 connect is retried. For a [mid-session
47 challenge](#mid-session-re-authorization), the CLI retries the refused
48 request automatically, and the web client asks you to retry the action.
49 </Step>
50</Steps>
25The Inspector identifies itself to the authorization server through
26whichever mechanism is configured: [dynamic client
27registration](/specification/latest/basic/authorization/client-registration#dynamic-client-registration),
28a pre-registered static client (`--client-id` / `--client-secret`), a
29[Client ID Metadata
30Document](/specification/latest/basic/authorization/client-registration#client-id-metadata-documents)
31(`--client-metadata-url`), or an [enterprise-managed
32IdP](/extensions/auth/enterprise-managed-authorization).
5133 
52<Frame caption="Connection Info after a completed OAuth flow: the authorization status, the dynamically registered client, and the granted scopes.">
53 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/auth-connection-info.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=01a8d11058c2d33e0069b8dec98591b5" width="3840" height="2160" data-path="images/inspector/auth-connection-info.png" />
54</Frame>
34**4. Authorize in the browser**
5535 
36The Inspector opens the authorization URL. You sign in and consent.
37 
38**5. Receive the callback**
39 
40The authorization server redirects to the Inspector's callback URL, carrying
41the authorization code.
42 
43**6. Exchange and retry**
44 
45The code is exchanged for tokens, the tokens are persisted, and the original
46connect is retried. For a [mid-session
47challenge](#mid-session-re-authorization), the CLI retries the refused
48request automatically, and the web client asks you to retry the action.
49 
50![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/auth-connection-info.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=01a8d11058c2d33e0069b8dec98591b5)
51 
5652## Callback URLs
5753 
5854The web app listens for the OAuth callback on its own URL, while the CLI and TUI deliberately share a second one:
from line 63
6763 
6864For the CLI and TUI, override it with `--callback-url` or `MCP_OAUTH_CALLBACK_URL`. The web callback always follows the page's origin, so `localhost` and `127.0.0.1` produce different redirect URIs there too.
6965 
70<Warning>
71 The callback URL **must bind a loopback host**: `localhost`, `127.0.0.0/8`, or
72 `[::1]`. The listener receives the authorization code over plaintext `http`,
73 so a non-loopback host is rejected with an error and there is no flag to
74 override that. If your browser runs on a different machine, forward the
75 callback port to it, or complete the login in a web Inspector instead:
76 `--print-handoff` (below) prints a `portForwardCmd` for the web Inspector's
77 ports.
78</Warning>
66> [!WARNING]
67>
68> The callback URL **must bind a loopback host**: `localhost`, `127.0.0.0/8`, or
69> `[::1]`. The listener receives the authorization code over plaintext `http`,
70> so a non-loopback host is rejected with an error and there is no flag to
71> override that. If your browser runs on a different machine, forward the
72> callback port to it, or complete the login in a web Inspector instead:
73> `--print-handoff` (below) prints a `portForwardCmd` for the web Inspector's
74> ports.
7975 
80<Note>
81 Redirect URIs must match your registration **exactly**. `http://localhost:6276/...` and `http://127.0.0.1:6276/...` are different URIs to an authorization server, even though they reach the same listener.
76> [!NOTE]
77>
78> Redirect URIs must match your registration **exactly**. `http://localhost:6276/...` and `http://127.0.0.1:6276/...` are different URIs to an authorization server, even though they reach the same listener.
79>
80> Only one process can hold the default port at a time; a second concurrent flow fails with `EADDRINUSE`. Use a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when your authorization server supports dynamic redirect-URI registration.
8281 
83 Only one process can hold the default port at a time; a second concurrent flow fails with `EADDRINUSE`. Use a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when your authorization server supports dynamic redirect-URI registration.
84</Note>
85 
8682## Where credentials live
8783 
8884| File | Contents |
from line 143
147143 
148144The `deepLink` in the handoff block navigates a browser straight to a *connected* Inspector; see [Deep links](/docs/draft/tools/inspector/web#deep-links).
149145 
150<Note>
151 Because the stored entry records no expiry, a stored refresh token is
152 exercised on **every** `--use-stored-auth` run. With rotating (single-use)
153 refresh tokens that opens two narrow failure windows: two concurrent
154 invocations against the same state file can race for the token, and a crash
155 between a successful refresh and the write-back leaves the rotated token
156 unsaved. Both are unlikely; re-authorize in the web client to recover.
157</Note>
146> [!NOTE]
147>
148> Because the stored entry records no expiry, a stored refresh token is
149> exercised on **every** `--use-stored-auth` run. With rotating (single-use)
150> refresh tokens that opens two narrow failure windows: two concurrent
151> invocations against the same state file can race for the token, and a crash
152> between a successful refresh and the write-back leaves the rotated token
153> unsaved. Both are unlikely; re-authorize in the web client to recover.
158154 
159155## Inspecting auth state
160156 

docs/draft/tools/inspector/cli Changed · +25 / -25 lines

from line 8
88npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
99```
1010 
11<Tip>
12 Running several commands against the same server, or a flow that spans one
13 session (tasks, subscriptions, elicitation)? The [mcpdo connection
14 client](/docs/draft/tools/inspector/mcpdo) connects once and keeps the
15 connection open between commands.
16</Tip>
11> [!TIP]
12>
13> Running several commands against the same server, or a flow that spans one
14> session (tasks, subscriptions, elicitation)? The [mcpdo connection
15> client](/docs/draft/tools/inspector/mcpdo) connects once and keeps the
16> connection open between commands.
1717 
1818The examples below use the installed `mcp-inspector` binary. Without a global install, prefix each command with `npx @modelcontextprotocol/inspector` instead, as above.
1919 
from line 36
3636 
3737Later examples abbreviate whichever of these forms you use, along with its `--transport` or `--config`/`--server` flags, as `<server>`.
3838 
39<Note>
40 **The config file is the only durable way to give a run its
41 [roots](/specification/draft/client/roots):** there is no roots flag, and the
42 CLI has no `roots/set` method. Roots configured for a server are advertised at
43 connect, so a server that calls `roots/list` (as
44 `@modelcontextprotocol/server-filesystem` does, to learn its allowed
45 directories) gets them.
46</Note>
39> [!NOTE]
40>
41> **The config file is the only durable way to give a run its
42> [roots](/specification/draft/client/roots):** there is no roots flag, and the
43> CLI has no `roots/set` method. Roots configured for a server are advertised at
44> connect, so a server that calls `roots/list` (as
45> `@modelcontextprotocol/server-filesystem` does, to learn its allowed
46> directories) gets them.
4747 
4848See [Configuration and flags](/docs/draft/tools/inspector/configuration) for `--catalog` vs. `--config`, the `--` separator, and the shared server-selection flags.
4949 
from line 112
112112 
113113The CLI can't render an App, so by default it doesn't advertise the MCP Apps extension at `initialize`. A server that exposes its App tools only to clients claiming App support will then look app-less (exit `2`). Add `--advertise-apps` to claim the extension for that run.
114114 
115<Note>
116 `tools/list --app-info` always emits NDJSON (one line per tool) regardless of
117 `--format`; `--format json` reshapes only the single-tool output of
118 `tools/call --app-info`.
119</Note>
115> [!NOTE]
116>
117> `tools/list --app-info` always emits NDJSON (one line per tool) regardless of
118> `--format`; `--format json` reshapes only the single-tool output of
119> `tools/call --app-info`.
120120 
121121## Exit codes and error envelopes
122122 
from line 219
219219mcp-inspector --cli --catalog ~/.mcp-inspector/mcp.json --method servers/show --server my-server
220220```
221221 
222<Warning>
223 `servers/show` redacts secret-bearing fields (`env` values, sensitive headers,
224 OAuth client secrets), but it does **not** scrub credentials embedded in a
225 server `url` (userinfo or query tokens) or in stdio `args`. Treat raw URL and
226 `detail` fields as sensitive before pasting them into an issue.
227</Warning>
222> [!WARNING]
223>
224> `servers/show` redacts secret-bearing fields (`env` values, sensitive headers,
225> OAuth client secrets), but it does **not** scrub credentials embedded in a
226> server `url` (userinfo or query tokens) or in stdio `args`. Treat raw URL and
227> `detail` fields as sensitive before pasting them into an issue.
228228 
229229### Editing the catalog
230230 

docs/draft/tools/inspector/configuration Changed · +55 / -55 lines

from line 13
1313 
1414Everything below belongs to a client.
1515 
16<Note>
17 The package also installs a second, separate bin, `mcpdo`, the experimental
18 [connection client](/docs/draft/tools/inspector/mcpdo). It is not reached
19 through the launcher and takes no mode flag.
20</Note>
16> [!NOTE]
17>
18> The package also installs a second, separate bin, `mcpdo`, the experimental
19> [connection client](/docs/draft/tools/inspector/mcpdo). It is not reached
20> through the launcher and takes no mode flag.
2121 
2222## Choosing servers
2323 
from line 35
3535 
3636The two are **mutually exclusive**, and neither combines with an ad-hoc target. Passing both is rejected identically by all three clients. The web client is stricter in one respect: it also rejects `--header` and `--protocol-era` alongside a file, where the CLI and TUI apply them on top of the file's settings.
3737 
38<Note>
39 **What a freshly seeded catalog contains depends on the client.** The web backend seeds three sample servers, so a first launch has something to connect to immediately:
38> [!NOTE]
39>
40> **What a freshly seeded catalog contains depends on the client.** The web backend seeds three sample servers, so a first launch has something to connect to immediately:
41>
42> ```json theme={null}
43> {
44> "mcpServers": {
45> "filesystem-server-default": {
46> "type": "stdio",
47> "command": "npx",
48> "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
49> },
50> "everything-server-default": {
51> "type": "stdio",
52> "command": "npx",
53> "args": ["-y", "@modelcontextprotocol/server-everything"]
54> },
55> "example-server-default": {
56> "type": "streamable-http",
57> "url": "https://example-server.modelcontextprotocol.io/mcp"
58> }
59> }
60> }
61> ```
62>
63> The CLI and TUI seed an empty `{ "mcpServers": {} }` instead: they are non-interactive or list-driven, so sample entries would be noise rather than a starting point.
64>
65> Either way, seeding happens only when the file does not exist yet, and a read-only `--config` is never seeded at all.
4066 
41 ```json theme={null}
42 {
43 "mcpServers": {
44 "filesystem-server-default": {
45 "type": "stdio",
46 "command": "npx",
47 "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
48 },
49 "everything-server-default": {
50 "type": "stdio",
51 "command": "npx",
52 "args": ["-y", "@modelcontextprotocol/server-everything"]
53 },
54 "example-server-default": {
55 "type": "streamable-http",
56 "url": "https://example-server.modelcontextprotocol.io/mcp"
57 }
58 }
59 }
60 ```
67> [!NOTE]
68>
69> `--config` is what you want when pointing the Inspector at a config file you
70> didn't write: a coworker's, a client application's, or one checked into a
71> repo. It guarantees the Inspector will not touch the file.
6172 
62 The CLI and TUI seed an empty `{ "mcpServers": {} }` instead: they are non-interactive or list-driven, so sample entries would be noise rather than a starting point.
63 
64 Either way, seeding happens only when the file does not exist yet, and a read-only `--config` is never seeded at all.
65</Note>
66 
67<Note>
68 `--config` is what you want when pointing the Inspector at a config file you
69 didn't write: a coworker's, a client application's, or one checked into a
70 repo. It guarantees the Inspector will not touch the file.
71</Note>
72 
7373### Ad-hoc targets
7474 
7575Instead of a file you can name one server directly, either as a positional command (stdio) or a URL:
from line 186
186186| `MCP_CATALOG_PATH`, `MCP_STORAGE_DIR` | As described under [CLI and TUI](#cli-and-tui). |
187187| `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Standard proxy routing for outbound MCP connections. |
188188 
189<Warning>
190 Never combine `DANGEROUSLY_OMIT_AUTH` and `DANGEROUSLY_BIND_ALL_INTERFACES`.
191 The web backend spawns processes and holds OAuth tokens, so anyone who can
192 reach it can drive it.
193</Warning>
189> [!WARNING]
190>
191> Never combine `DANGEROUSLY_OMIT_AUTH` and `DANGEROUSLY_BIND_ALL_INTERFACES`.
192> The web backend spawns processes and holds OAuth tokens, so anyone who can
193> reach it can drive it.
194194 
195195What the token does and does not protect is laid out under [The web backend and its API token](/docs/draft/tools/inspector/security#the-web-backend-and-its-api-token).
196196 
from line 235
235235| Container with **no volume** on the secrets directory | Memory | No, this session only |
236236| Container **with** a volume on the secrets directory | File | Yes |
237237 
238<Warning>
239 **With no keychain, secrets go to a plaintext file, and you did not have to ask for it.** On a host where the keychain probe fails (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. Unless you supply a key, that file is **unencrypted**. Mode `0600` keeps out other non-root users, but not root, not backups or copies of your home directory, and not any program running as you, including the stdio servers the Inspector starts.
240 
241 Pick one:
242 
243 * **Get a keychain back:** install libsecret and run a Secret Service (for example `gnome-keyring`), or run the Inspector inside a desktop session. On the next start the Inspector moves the file's secrets into the keychain and deletes the file.
244 * **Encrypt the file:** supply a generated key with `MCP_INSPECTOR_SECRET_KEY_FILE` (preferred) or `MCP_INSPECTOR_SECRET_KEY`.
245 * **Don't write secrets to disk at all:** `MCP_INSPECTOR_SECRET_STORE=memory`, and re-enter them each session.
246 
247 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/draft/tools/inspector/security#what-the-file-store-protects-against).
248</Warning>
238> [!WARNING]
239>
240> **With no keychain, secrets go to a plaintext file, and you did not have to ask for it.** On a host where the keychain probe fails (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. Unless you supply a key, that file is **unencrypted**. Mode `0600` keeps out other non-root users, but not root, not backups or copies of your home directory, and not any program running as you, including the stdio servers the Inspector starts.
241>
242> Pick one:
243>
244> * **Get a keychain back:** install libsecret and run a Secret Service (for example `gnome-keyring`), or run the Inspector inside a desktop session. On the next start the Inspector moves the file's secrets into the keychain and deletes the file.
245> * **Encrypt the file:** supply a generated key with `MCP_INSPECTOR_SECRET_KEY_FILE` (preferred) or `MCP_INSPECTOR_SECRET_KEY`.
246> * **Don't write secrets to disk at all:** `MCP_INSPECTOR_SECRET_STORE=memory`, and re-enter them each session.
247>
248> Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/draft/tools/inspector/security#what-the-file-store-protects-against).
249249 
250250[mcpdo](/docs/draft/tools/inspector/mcpdo) never falls back to `memory`: its commands and its daemon are separate processes, so it uses the file store instead.
251251 

docs/draft/tools/inspector/protocol-eras Changed · +87 / -115 lines

from line 14
1414| `auto` | Probe `server/discover` first, and fall back to `initialize` on any non-modern outcome. |
1515| `modern` | Pin exactly `2026-07-28`. No fallback, so a non-modern server fails loudly. |
1616 
17<Note>
18 **Why `legacy` is the default, and not `auto`.** A debugging tool must not
19 auto-probe. A `server/discover` probe stalls against silent legacy stdio
20 servers, and it pollutes the recorded transcript you came here to read. Opting
21 into `auto` or `modern` is a deliberate act, so what you see in the Protocol
22 tab is what your server would have seen from a client behaving the way you
23 configured.
24</Note>
17> [!NOTE]
18>
19> **Why `legacy` is the default, and not `auto`.** A debugging tool must not
20> auto-probe. A `server/discover` probe stalls against silent legacy stdio
21> servers, and it pollutes the recorded transcript you came here to read. Opting
22> into `auto` or `modern` is a deliberate act, so what you see in the Protocol
23> tab is what your server would have seen from a client behaving the way you
24> configured.
2525 
2626Once connected, the negotiated era is shown as a badge on the **Protocol** tab's Messages header and in **Connection Info**. On a modern connection, `server/discover` also supplies `capabilities` (including `extensions`), `instructions`, and the list of `supportedVersions`. The server's name and version arrive in the result `_meta` under `io.modelcontextprotocol/serverInfo`.
2727 
28<Frame caption="Server Settings: the Protocol Era selector, with all three choices.">
29 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/settings-protocol-era.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=34566c45f97c8af0e2c0d9ee0493b572" width="3840" height="2160" data-path="images/inspector/settings-protocol-era.png" />
30</Frame>
28![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/settings-protocol-era.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=34566c45f97c8af0e2c0d9ee0493b572)
3129 
3230## Reproducing each era locally
3331 
from line 49
5149 
5250## Logging
5351 
54<Tabs>
55 <Tab title="Legacy">
56 Logging is **session-scoped**. The client sends `logging/setLevel` once, and the server emits `notifications/message` at or above that level for the rest of the session.
52**Legacy**
5753 
58 The **Logs** tab shows a **Set Active Level** selector plus a **Set** button. Choose a level, click Set, and subsequent server logs stream into the panel.
54Logging is **session-scoped**. The client sends `logging/setLevel` once, and the server emits `notifications/message` at or above that level for the rest of the session.
5955 
60 Reproduce with `test-servers/configs/logging-legacy-http.json`.
61 </Tab>
56The **Logs** tab shows a **Set Active Level** selector plus a **Set** button. Choose a level, click Set, and subsequent server logs stream into the panel.
6257 
63 <Tab title="Modern">
64 `logging/setLevel` is **gone**. Instead the client opts in **per request**, by stamping `_meta["io.modelcontextprotocol/logLevel"]` on each outgoing request. A server MUST NOT emit `notifications/message` for a request that did not opt in.
58Reproduce with `test-servers/configs/logging-legacy-http.json`.
6559 
66 The **Logs** tab therefore shows a **Log Level per Request** control instead. Pick a level and every subsequent request carries the stamp, visible in the Network tab's request body. Logs emitted while handling a request ride that request's SSE response stream.
60**Modern**
6761 
68 Set the control to **Off** and the `logLevel` key is omitted entirely, so the same tool call produces no logs at all. That silence is correct behavior, not a bug.
62`logging/setLevel` is **gone**. Instead the client opts in **per request**, by stamping `_meta["io.modelcontextprotocol/logLevel"]` on each outgoing request. A server MUST NOT emit `notifications/message` for a request that did not opt in.
6963 
70 The per-server default is `debug` (opted in at the most verbose level, since the Inspector is a debugging tool); set `modernLogLevel: "off"` on a server to opt back out by default.
64The **Logs** tab therefore shows a **Log Level per Request** control instead. Pick a level and every subsequent request carries the stamp, visible in the Network tab's request body. Logs emitted while handling a request ride that request's SSE response stream.
7165 
72 Reproduce with `test-servers/configs/logging-modern-http.json`.
73 </Tab>
74</Tabs>
66Set the control to **Off** and the `logLevel` key is omitted entirely, so the same tool call produces no logs at all. That silence is correct behavior, not a bug.
7567 
76<Frame caption="Legacy: the Logs tab offers a session-scoped Set Active Level control, and a log arrives after calling send_notification.">
77 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-legacy.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=ec5fddd86d1fe7ecc1ef83549798f33e" width="3840" height="2160" data-path="images/inspector/logs-legacy.png" />
78</Frame>
68The per-server default is `debug` (opted in at the most verbose level, since the Inspector is a debugging tool); set `modernLogLevel: "off"` on a server to opt back out by default.
7969 
80<Frame caption="Modern: the same tab instead offers Log Level per Request. The level is stamped on every outgoing request, and the log rides that request's stream.">
81 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-modern.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=85a1d7118a1e693930aefab8ca6b21a3" width="3840" height="2160" data-path="images/inspector/logs-modern.png" />
82</Frame>
70Reproduce with `test-servers/configs/logging-modern-http.json`.
8371 
72![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-legacy.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=ec5fddd86d1fe7ecc1ef83549798f33e)
73 
74![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/logs-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=85a1d7118a1e693930aefab8ca6b21a3)
75 
8476***
8577 
8678## Resource subscriptions
8779 
88<Tabs>
89 <Tab title="Legacy">
90 Clicking **Subscribe** on a resource sends `resources/subscribe`. The Subscriptions section lists the URI with no stream chrome. When the resource changes, the server emits `notifications/resources/updated` and the subscribed tile's last-updated time is stamped.
80**Legacy**
9181 
92 Reproduce with `test-servers/configs/subscriptions-legacy-http.json`, which also serves an `update_resource` tool so you can drive the notification round-trip yourself.
93 </Tab>
82Clicking **Subscribe** on a resource sends `resources/subscribe`. The Subscriptions section lists the URI with no stream chrome. When the resource changes, the server emits `notifications/resources/updated` and the subscribed tile's last-updated time is stamped.
9483 
95 <Tab title="Modern">
96 The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
84Reproduce with `test-servers/configs/subscriptions-legacy-http.json`, which also serves an `update_resource` tool so you can drive the notification round-trip yourself.
9785 
98 Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
86**Modern**
9987 
100 Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
101 </Tab>
102</Tabs>
88The same **Subscribe** button instead sends **`subscriptions/listen`**, with a filter carrying `resourceSubscriptions` plus whichever `*ListChanged` opt-ins apply (such as `resourcesListChanged`). The subscription is confirmed when the server sends `notifications/subscriptions/acknowledged`.
10389 
104<Frame caption="A modern subscription: the Subscriptions section carries a LISTENING stream-status badge that a legacy subscription has no need for.">
105 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/resources-subscriptions-modern.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=5ca4c64c6783eb210fe54248f03de144" width="3840" height="2160" data-path="images/inspector/resources-subscriptions-modern.png" />
106</Frame>
90Because the subscription is now a long-lived stream rather than a session flag, the Subscriptions section grows a **stream-status badge** in its header that moves from `Connecting...` to `Listening`. If the stream drops, the badge shows `Reconnecting...` while the Inspector re-sends `subscriptions/listen`. It shows `Stream ended` once the stream closes for good. It shows `Not acknowledged` when the server answers the listen with a plain result and never sends the acknowledgement; the Inspector does not retry in that case.
10791 
92Reproduce with `test-servers/configs/subscriptions-modern-http.json`. To see the `Not acknowledged` state, use `test-servers/configs/subscriptions-never-acknowledged-http.json`. That server acknowledges your first subscription, then refuses every later listen, so subscribing to a second resource trips the badge.
93 
94![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/resources-subscriptions-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=5ca4c64c6783eb210fe54248f03de144)
95 
10896***
10997 
11098## Tasks
from line 99
11199 
112100Tasks change the most between protocol eras, including *how the Inspector UI tab is gated*.
113101 
114<Tabs>
115 <Tab title="Legacy">
116 The **Tasks** tab appears when the server advertises `capabilities.tasks`. Run a tool with **Run as task** enabled and the tab lists it, populated by `tasks/list` and polled with `tasks/get`. The completed payload is fetched with a **blocking `tasks/result`**, and **Cancel** sends `tasks/cancel`.
102**Legacy**
117103 
118 Reproduce with `test-servers/configs/tasks-legacy-http.json`.
119 </Tab>
104The **Tasks** tab appears when the server advertises `capabilities.tasks`. Run a tool with **Run as task** enabled and the tab lists it, populated by `tasks/list` and polled with `tasks/get`. The completed payload is fetched with a **blocking `tasks/result`**, and **Cancel** sends `tasks/cancel`.
120105 
121 <Tab title="Modern">
122 Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
106Reproduce with `test-servers/configs/tasks-legacy-http.json`.
123107 
124 Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
108**Modern**
125109 
126 A task that needs more information moves to `input_required` and surfaces an embedded [elicitation](/specification/draft/client/elicitation) in the pending-request modal (the dialog the web client opens whenever a request is waiting on you). Answering it sends **`tasks/update`** carrying the `inputResponses`, and the next poll completes.
110Tasks are an **extension** (`io.modelcontextprotocol/tasks`, [SEP-2663](/seps/2663-tasks-extension)), so the tab is gated on the server *advertising that extension* rather than on `capabilities.tasks`.
127111 
128 Reproduce with `test-servers/configs/tasks-modern-http.json` (tools `modern_task` and `modern_input_task`).
129 </Tab>
130</Tabs>
112Run a tool as a task and `tools/call` returns a `CreateTaskResult` (`resultType: "task"`, visible in the Protocol and Network tabs). The Inspector polls **`tasks/get`** only; there is no `tasks/list`, so **Refresh** re-polls the handles the client already knows about. A completed task **inlines its result**, with no blocking `tasks/result` call.
131113 
132<Frame caption="Legacy: the Tasks tab is populated from tasks/list, and the payload is fetched with a blocking tasks/result.">
133 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-legacy.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=777729bfc1b58b359808c8120d9597d3" width="3840" height="2160" data-path="images/inspector/tasks-legacy.png" />
134</Frame>
114A task that needs more information moves to `input_required` and surfaces an embedded [elicitation](/specification/draft/client/elicitation) in the pending-request modal (the dialog the web client opens whenever a request is waiting on you). Answering it sends **`tasks/update`** carrying the `inputResponses`, and the next poll completes.
135115 
136<Frame caption="Modern: the client polls tasks/get on handles it already holds, and the completed task inlines its result; note resultType: complete in the full task object.">
137 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-modern.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=8e90d128528463fd672537c79db2e1fe" width="3840" height="2160" data-path="images/inspector/tasks-modern.png" />
138</Frame>
116Reproduce with `test-servers/configs/tasks-modern-http.json` (tools `modern_task` and `modern_input_task`).
139117 
118![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-legacy.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=777729bfc1b58b359808c8120d9597d3)
119 
120![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tasks-modern.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=8e90d128528463fd672537c79db2e1fe)
121 
140122***
141123 
142124## Multi-round tool results (MRTR)
from line 139
157139| `mrtr_empty` | One elicitation round, then completes with an empty result (no `content`, no `structuredContent`). |
158140| `mrtr_loop` | Never completes, so the client stops at its `MRTR_MAX_ROUNDS` limit. |
159141 
160<Note>
161 The legacy pattern of a server calling `server.elicitInput` (the test servers'
162 `collect_elicitation` preset) **errors** on a 2026-07-28 connection, because
163 server-to-client requests aren't allowed there. MRTR is its modern
164 replacement.
165</Note>
142> [!NOTE]
143>
144> The legacy pattern of a server calling `server.elicitInput` (the test servers'
145> `collect_elicitation` preset) **errors** on a 2026-07-28 connection, because
146> server-to-client requests aren't allowed there. MRTR is its modern
147> replacement.
166148 
167<Frame caption="An MRTR round paused at the pending-request modal, tagged input_required. Answering it retries the original request.">
168 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/mrtr-pending-request.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=99f8acb7f845a12aed42bcea4d310fee" width="3840" height="2160" data-path="images/inspector/mrtr-pending-request.png" />
169</Frame>
149![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/mrtr-pending-request.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=99f8acb7f845a12aed42bcea4d310fee)
170150 
171<Frame caption="The monitoring sidebar's Protocol tab after mrtr_confirm completes. Both tools/call rounds sit inside one MRTR conversation, Round 1 tagged input_required and Round 2 complete, while unrelated traffic stays outside it.">
172 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/mrtr-protocol-conversation.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=b083d8127a7a67028fcd9702b25aeac1" width="3840" height="2160" data-path="images/inspector/mrtr-protocol-conversation.png" />
173</Frame>
151![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/mrtr-protocol-conversation.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=b083d8127a7a67028fcd9702b25aeac1)
174152 
175153***
176154 
from line 163
185163 
186164Reproduce with `test-servers/configs/xmcpheader-modern-http.json`.
187165 
188<Note>
189 On a modern connection the Inspector mirrors `x-mcp-header` arguments into
190 `Mcp-Param-*` headers itself, in all three clients. In the **web** client the
191 headers are added by the Inspector's Node backend, which issues the upstream
192 request, so they reach the server even though the browser never sends them.
193</Note>
166> [!NOTE]
167>
168> On a modern connection the Inspector mirrors `x-mcp-header` arguments into
169> `Mcp-Param-*` headers itself, in all three clients. In the **web** client the
170> headers are added by the Inspector's Node backend, which issues the upstream
171> request, so they reach the server even though the browser never sends them.
194172 
195<Frame caption="get_weather shows its mirrored city -> Mcp-Param-City header, and the Network sidebar confirms the call carried mcp-param-city: Boston. invalid_header_tool is struck through under the Excluded (SEP-2243) divider.">
196 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/tools-sep2243.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=0e011c3012332c0a74d0f57fc84a5cf2" width="3840" height="2160" data-path="images/inspector/tools-sep2243.png" />
197</Frame>
173![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/tools-sep2243.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=0e011c3012332c0a74d0f57fc84a5cf2)
198174 
199175### `-32602` error panels
200176 
from line 199
223199| `trigger_unsupported_version` | `400` | `-32022` | Unsupported version; supported versions in `data.supported`. |
224200| `trigger_method_not_found` | `404` | `-32601` | Method not found. |
225201 
226<Frame caption="The Network tab shows the HTTP layer; here, the 400 Bad Request the strict server answered with.">
227 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/network-modern-headers.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=7df4c01f5ab68aa7632ac3b5a5866b42" width="3840" height="2160" data-path="images/inspector/network-modern-headers.png" />
228</Frame>
202![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/network-modern-headers.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=7df4c01f5ab68aa7632ac3b5a5866b42)
229203 
230<Frame caption="The Protocol tab renders the same failure as a typed spec error: -32022 UnsupportedProtocolVersion, with the versions the server does support.">
231 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/protocol-modern-error.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=d578ea8262ff327e4d61d3697937900b" width="3840" height="2160" data-path="images/inspector/protocol-modern-error.png" />
232</Frame>
204![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/protocol-modern-error.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=d578ea8262ff327e4d61d3697937900b)
233205 
234206***
235207 

docs/draft/tools/inspector/recipes Changed · +148 / -156 lines

from line 52
5252 
5353`--config` guarantees the file is served as-is and never written, seeded, or migrated.
5454 
55<Frame caption="Add Servers offers import from an existing client config or from a registry server.json.">
56 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/import-config.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=c9f5229c2d827f4bcab37938879f21f2" width="3840" height="2160" data-path="images/inspector/import-config.png" />
57</Frame>
55![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/import-config.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=c9f5229c2d827f4bcab37938879f21f2)
5856 
5957## Reviewing an MCP App
6058 
6159[MCP Apps](/extensions/apps/overview) are tools that carry a UI widget. For an automated reviewer (CI or an agent), use the CLI for every check that returns JSON, and open a browser only to inspect the rendered widget.
6260 
63<Steps>
64 <Step title="Probe the security posture without calling the tool">
65 ```bash theme={null}
66 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
67 --method tools/call --tool-name <tool> --app-info --advertise-apps
68 ```
61**1. Probe the security posture without calling the tool**
6962 
70 `--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
63```bash theme={null}
64mcp-inspector --cli --transport http --server-url https://example.com/mcp \
65 --method tools/call --tool-name <tool> --app-info --advertise-apps
66```
7167 
72 One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
68`--advertise-apps` makes the CLI claim MCP Apps support at `initialize`. It is off by default because the CLI cannot render an app, but a server that shows its app tools only to app-capable clients would otherwise report no app.
7369 
74 ```json theme={null}
75 {
76 "hasApp": true,
77 "toolName": "get_pros",
78 "resourceUri": "ui://pros/view.html",
79 "csp": { "connectDomains": ["https://api.example.com"] },
80 "permissions": { "clipboard": false },
81 "prefersBorder": true,
82 "resourceMimeType": "text/html"
83 }
84 ```
70One JSON line on stdout; exit `0` if the tool has an app, `2` if not, so an `&&` chain short-circuits:
8571 
86 `csp` and `permissions` (and `domain`, when the resource declares one) live on the UI **resource** rather than the tool, so `--app-info` reads that resource. The tool is never called.
87 </Step>
72```json theme={null}
73{
74 "hasApp": true,
75 "toolName": "get_pros",
76 "resourceUri": "ui://pros/view.html",
77 "csp": { "connectDomains": ["https://api.example.com"] },
78 "permissions": { "clipboard": false },
79 "prefersBorder": true,
80 "resourceMimeType": "text/html"
81}
82```
8883 
89 <Step title="Get the full result payload, still with no browser">
90 ```bash theme={null}
91 mcp-inspector --cli --transport http --server-url https://example.com/mcp \
92 --method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
93 ```
94 </Step>
84`csp` and `permissions` (and `domain`, when the resource declares one) live on the UI **resource** rather than the tool, so `--app-info` reads that resource. The tool is never called.
9585 
96 <Step title="Launch the web Inspector once, loopback-only">
97 ```bash theme={null}
98 TOKEN="$(openssl rand -hex 24)"
99 HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \
100 MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \
101 mcp-inspector --web &
102 ```
86**2. Get the full result payload, still with no browser**
10387 
104 Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
105 </Step>
88```bash theme={null}
89mcp-inspector --cli --transport http --server-url https://example.com/mcp \
90 --method tools/call --tool-name <tool> --tool-args-json '{"zip":"10001"}' --format json
91```
10692 
107 <Step title="Navigate one deep link to a rendered widget">
108 ```
109 http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
110 ```
93**3. Launch the web Inspector once, loopback-only**
11194 
112 `appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/draft/tools/inspector/web#deep-links). `autoConnect` and `autoOpen` must both equal the session token, since `autoOpen` fires a tool call straight from the URL and needs the same gate as `autoConnect`.
113 </Step>
95```bash theme={null}
96TOKEN="$(openssl rand -hex 24)"
97HOST=127.0.0.1 CLIENT_PORT=6274 MCP_SANDBOX_PORT=6275 \
98MCP_AUTO_OPEN_ENABLED=false MCP_INSPECTOR_API_TOKEN="$TOKEN" \
99mcp-inspector --web &
100```
114101 
115 <Step title="Wait on a deterministic signal instead of sleeping">
116 The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
102Pinning `MCP_SANDBOX_PORT` keeps the address explicit: the app's UI is served from a separate sandbox port, and your automation needs to know where it is.
117103 
118 | Selector | Attribute | Values |
119 | - | - | - |
120 | `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
121 | `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
122 | `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
123 </Step>
124</Steps>
104**4. Navigate one deep link to a rendered widget**
125105 
106```
107http://127.0.0.1:6274/?serverUrl=<encoded url>&transport=http&autoConnect=<TOKEN>&openApp=<tool>&appArgs=<base64url(JSON)>&autoOpen=<TOKEN>
108```
109 
110`appArgs` is the tool's arguments as base64url-encoded JSON, and every deep-link parameter is described under [Deep links](/docs/draft/tools/inspector/web#deep-links). `autoConnect` and `autoOpen` must both equal the session token, since `autoOpen` fires a tool call straight from the URL and needs the same gate as `autoConnect`.
111 
112**5. Wait on a deterministic signal instead of sleeping**
113 
114The Apps screen exposes a stable automation contract. Poll these attributes instead of sleeping:
115 
116| Selector | Attribute | Values |
117| - | - | - |
118| `[data-testid="apps-form"]` | `data-app-status` | `idle`, then `loading`, then `ready` or `error` (on `error`, `data-app-error` carries the reason) |
119| `[data-testid="connection-status"]` | `data-status` | `disconnected`, `connecting`, then `connected` or `error` (`data-error-message` has the detail) |
120| `[data-testid="connection-status"]` | `data-deeplink` | `parsed`, `rejected`, or `none` (`none` means no deep link was given, `rejected` means one was refused) |
121 
126122## Docker
127123 
128124A container image is published to GitHub Container Registry for `linux/amd64` and `linux/arm64`:
from line 131
135131 
136132The image defaults to `--web`, bound to `0.0.0.0:6274` with browser auto-open off, and runs as the non-root `node` user (uid `1000`). It sets `DANGEROUSLY_BIND_ALL_INTERFACES=true` because a container must bind the wildcard address to be reachable through `-p`.
137133 
138<Warning>
139 **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
140 6274:6274` publishes on every host interface, putting a backend that spawns
141 processes, and the page that discloses its token, on your local network. The
142 image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
143 not the host's. See [Publish the port on loopback
144 only](/docs/draft/tools/inspector/security#publish-the-port-on-loopback-only).
145</Warning>
134> [!WARNING]
135>
136> **Keep the `127.0.0.1:` prefix on every published port.** A bare `-p
137> 6274:6274` publishes on every host interface, putting a backend that spawns
138> processes, and the page that discloses its token, on your local network. The
139> image's `DANGEROUSLY_BIND_ALL_INTERFACES` covers the container's interfaces,
140> not the host's. See [Publish the port on loopback
141> only](/docs/draft/tools/inspector/security#publish-the-port-on-loopback-only).
146142 
147143To use the **Apps** tab, also publish the MCP Apps sandbox port, `6275`, and `6278` for an app that declares `_meta.ui.domain`. Publish each on the same port number inside and out, since the browser is handed the in-container port:
148144 
from line 166
170166 
171167If you bind-mount a host directory instead of a named volume, it keeps its host ownership, so on Linux add `--user "$(id -u):$(id -g)"` or `chown` it to uid `1000`, or saves fail with `EACCES`. Don't bind-mount the secrets file on its own: it isn't recognized as durable, and it can't be replaced atomically.
172168 
173<Warning>
174 **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
169> [!WARNING]
170>
171> **Mounting that volume turns on file storage of secrets, and without a key the file is plaintext.** Every OAuth token acquired, and every client secret and stdio `env:` value you save, is then written to `secrets.json` on the volume. It is readable by root and every member of the host's `docker` group, and by anyone who gets a backup, snapshot or copy of the volume.
172>
173> Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
174>
175> ```bash theme={null}
176> mkdir -p ~/.config/mcp-inspector
177> (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
178> ```
179>
180> Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/draft/tools/inspector/security#what-the-file-store-protects-against).
175181 
176 Give it a key, generated into a file that only you can read and that sits outside the volume, its backups and any repository:
182Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
177183 
178 ```bash theme={null}
179 mkdir -p ~/.config/mcp-inspector
180 (umask 077 && openssl rand -base64 32 > ~/.config/mcp-inspector/secret-key)
181 ```
184**docker run**
182185 
183 Even encrypted, secrets on disk carry moderate risk. See [what the file store protects against](/docs/draft/tools/inspector/security#what-the-file-store-protects-against).
184</Warning>
186```bash theme={null}
187docker run --rm -p 127.0.0.1:6274:6274 \
188 -v mcp-inspector-data:/home/node/.mcp-inspector \
189 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
190 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
191 ghcr.io/modelcontextprotocol/inspector
192```
185193 
186Hand the key to the container **as a file** with `MCP_INSPECTOR_SECRET_KEY_FILE`, not as an environment variable. A key passed with `-e MCP_INSPECTOR_SECRET_KEY=…` is readable by anyone who can run `docker inspect` or `docker exec`.
194**Compose secrets**
187195 
188<Tabs>
189 <Tab title="docker run">
190 ```bash theme={null}
191 docker run --rm -p 127.0.0.1:6274:6274 \
192 -v mcp-inspector-data:/home/node/.mcp-inspector \
193 -v "$HOME/.config/mcp-inspector/secret-key:/run/secrets/mcp_inspector_secret_key:ro" \
194 -e MCP_INSPECTOR_SECRET_KEY_FILE=/run/secrets/mcp_inspector_secret_key \
195 ghcr.io/modelcontextprotocol/inspector
196 ```
197 </Tab>
196```yaml theme={null}
197services:
198 inspector:
199 image: ghcr.io/modelcontextprotocol/inspector
200 ports: ["127.0.0.1:6274:6274"]
201 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
202 environment:
203 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
204 secrets: [mcp_inspector_secret_key]
205secrets:
206 mcp_inspector_secret_key:
207 file: ${HOME}/.config/mcp-inspector/secret-key
208volumes:
209 mcp-inspector-data:
210```
198211 
199 <Tab title="Compose secrets">
200 ```yaml theme={null}
201 services:
202 inspector:
203 image: ghcr.io/modelcontextprotocol/inspector
204 ports: ["127.0.0.1:6274:6274"]
205 volumes: ["mcp-inspector-data:/home/node/.mcp-inspector"]
206 environment:
207 MCP_INSPECTOR_SECRET_KEY_FILE: /run/secrets/mcp_inspector_secret_key
208 secrets: [mcp_inspector_secret_key]
209 secrets:
210 mcp_inspector_secret_key:
211 file: ${HOME}/.config/mcp-inspector/secret-key
212 volumes:
213 mcp-inspector-data:
214 ```
215 </Tab>
216</Tabs>
217 
218212Without Swarm, Compose secrets are bind mounts that keep the host file's owner and mode, so the `0600` key file must be owned by uid `1000`. On a Linux host where your uid is different, run `sudo chown 1000 ~/.config/mcp-inspector/secret-key` rather than loosening its mode. Supply the **same** key on every run.
219213 
220214If the key file is missing, unreadable or empty, or both key variables are set, the Inspector **refuses to read or write the secrets file** rather than falling back to plaintext, and says why in the log and in the settings dialogs' footer. Everything else about the store (selection order, location, permissions) is under [Where secrets are stored](/docs/draft/tools/inspector/configuration#where-secrets-are-stored).
from line 223
229223docker run --rm ghcr.io/modelcontextprotocol/inspector --cli <target> --method tools/list
230224```
231225 
232<Warning>
233 **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
234 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
235 which no longer matches the in-container port, and connects will `403`. Either
236 run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
237 ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
238</Warning>
226> [!WARNING]
227>
228> **If you remap the published port, set `ALLOWED_ORIGINS`.** With `-p
229> 127.0.0.1:8080:6274` the browser's origin becomes `http://localhost:8080`,
230> which no longer matches the in-container port, and connects will `403`. Either
231> run `-e CLIENT_PORT=8080 -p 127.0.0.1:8080:8080`, or set `-e
232> ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080`.
239233 
240234## Hosting on a network
241235 
from line 243
249243| **Behind TLS or a reverse proxy** | The browser's `Origin` becomes the public origin, which won't match the bind host. Set `ALLOWED_ORIGINS=https://inspector.example.com`, and for MCP Apps set `MCP_SANDBOX_FULL_ADDRESS` (see below). |
250244| **Wildcard bind (containers)** | Set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Loopback access still works out of the box; reaching it at a non-loopback address needs `ALLOWED_ORIGINS`. |
251245 
252<Warning>
253 `ALLOWED_ORIGINS` **replaces** the default list rather than merging with it. List every origin you'll browse from, including the loopback forms you want to keep:
246> [!WARNING]
247>
248> `ALLOWED_ORIGINS` **replaces** the default list rather than merging with it. List every origin you'll browse from, including the loopback forms you want to keep:
249>
250> ```
251> ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
252> ```
253>
254> Each entry must include the scheme; a scheme-less value is dropped with a warning. A blank value does **not** disable the check; it falls back to the default. There is no knob to turn origin validation off.
254255 
255 ```
256 ALLOWED_ORIGINS=http://localhost:6274,http://127.0.0.1:6274,http://192.168.1.50:6274
257 ```
258 
259 Each entry must include the scheme; a scheme-less value is dropped with a warning. A blank value does **not** disable the check; it falls back to the default. There is no knob to turn origin validation off.
260</Warning>
261 
262256Two further caveats when going off loopback:
263257 
264258* **MCP Apps need their sandbox port reachable too.** It's a separate listener (`6275` by default, set with `MCP_SANDBOX_PORT`), so expose or forward it alongside the web port.
from line 281
287281 
288282A loop that works well in practice:
289283 
290<Steps>
291 <Step title="Start with the CLI">
292 `--method initialize` confirms the server starts, handshakes, and reports
293 the capabilities you expect, in one second, with a machine-readable answer.
294 Most "it doesn't work" turns out to be here.
295 </Step>
284**1. Start with the CLI**
296285 
297 <Step title="Move to the web client for exploration">
298 Schema-driven forms, rendered results, and the Protocol tab beside them make
299 it fast to find the case where a tool misbehaves.
300 </Step>
286`--method initialize` confirms the server starts, handshakes, and reports
287the capabilities you expect, in one second, with a machine-readable answer.
288Most "it doesn't work" turns out to be here.
301289 
302 <Step title="Test the edges">
303 Invalid inputs, missing required prompt arguments, concurrent calls, and,
304 for HTTP servers, both protocol eras. Verify the *errors* are as intentional
305 as the successes.
306 </Step>
290**2. Move to the web client for exploration**
307291 
308 <Step title="Lock it in with the CLI">
309 Turn what you found into a CI assertion: pipe the CLI's `--format json`
310 output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
311 instead of starting interactive OAuth. See [Verify a server in
312 CI](/docs/draft/tools/inspector/cli#verify-a-server-in-ci) for the full
313 command.
314 </Step>
315</Steps>
292Schema-driven forms, rendered results, and the Protocol tab beside them make
293it fast to find the case where a tool misbehaves.
294 
295**3. Test the edges**
296 
297Invalid inputs, missing required prompt arguments, concurrent calls, and,
298for HTTP servers, both protocol eras. Verify the *errors* are as intentional
299as the successes.
300 
301**4. Lock it in with the CLI**
302 
303Turn what you found into a CI assertion: pipe the CLI's `--format json`
304output to `jq -e` with `--stored-auth-only`, so a missing token fails fast
305instead of starting interactive OAuth. See [Verify a server in
306CI](/docs/draft/tools/inspector/cli#verify-a-server-in-ci) for the full
307command.
316308 

docs/draft/tools/inspector/security Changed · +16 / -16 lines

from line 26
2626 
2727The backend binds `127.0.0.1` by default. Binding every interface (`0.0.0.0`, `::` and equivalent spellings) is refused unless you set `DANGEROUSLY_BIND_ALL_INTERFACES=true`. Binding one specific address is allowed without that opt-in, because it is one deliberate exposure rather than all of them at once. See [Hosting on a network](/docs/draft/tools/inspector/recipes#hosting-on-a-network).
2828 
29<Warning>
30 Never combine `DANGEROUSLY_OMIT_AUTH` with a non-loopback bind. If you need
31 the Inspector reachable by others, put a real access-control boundary in front
32 of it: an authenticating reverse proxy, an SSH tunnel, or a private network.
33</Warning>
29> [!WARNING]
30>
31> Never combine `DANGEROUSLY_OMIT_AUTH` with a non-loopback bind. If you need
32> the Inspector reachable by others, put a real access-control boundary in front
33> of it: an authenticating reverse proxy, an SSH tunnel, or a private network.
3434 
3535## Docker
3636 
from line 68
6868 
6969### The automatic plaintext fallback
7070 
71<Warning>
72 On a host where the OS keychain cannot be reached (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. You do not have to ask for it, and unless you supply a key that file is **unencrypted**.
71> [!WARNING]
72>
73> On a host where the OS keychain cannot be reached (Linux without libsecret or a running Secret Service, a headless server or SSH session with no D-Bus session, Android/Termux), the Inspector **automatically** stores secrets in `~/.mcp-inspector/secrets.json`. You do not have to ask for it, and unless you supply a key that file is **unencrypted**.
74>
75> The only signs are a warning on stderr when the store is selected and a footer in the web client's settings dialogs.
7376 
74 The only signs are a warning on stderr when the store is selected and a footer in the web client's settings dialogs.
75</Warning>
76 
7777Treat that as a risk, not just a configuration fact. The file is written with mode `0600`, which keeps out other non-root users and nothing else. To close it, do one of the following:
7878 
7979* get a keychain back (install libsecret and run a Secret Service such as `gnome-keyring`, or run inside a desktop session);
from line 145
145145 
146146Tokens are compared in constant time. A request line larger than 1 MiB is rejected. A command presenting the wrong token to a live daemon fails loudly. It never replaces that daemon.
147147 
148<Note>
149 Private mode separates connections and daemon state between shells. It is
150 **not** a security boundary against other processes running as your user:
151 anything with your UID that learns the daemon directory can read its token.
152 For a hard boundary, use a separate user account or a container.
153</Note>
148> [!NOTE]
149>
150> Private mode separates connections and daemon state between shells. It is
151> **not** a security boundary against other processes running as your user:
152> anything with your UID that learns the daemon directory can read its token.
153> For a hard boundary, use a separate user account or a container.
154154 
155155### What it holds in memory
156156 

docs/draft/tools/inspector/tui Changed · +6 / -10 lines

from line 8
88npx @modelcontextprotocol/inspector --tui node build/index.js # with an ad-hoc stdio server
99```
1010 
11<Frame caption="The TUI connected to a server, on the Tools tab, showing a tool's input schema.">
12 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-tools.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=5d11d64f4b98df8c26e7ac048f38576f" width="2986" height="1832" data-path="images/inspector/tui-tools.png" />
13</Frame>
11![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-tools.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=5d11d64f4b98df8c26e7ac048f38576f)
1412 
1513## Choosing servers
1614 
from line 72
7472 
7573The trade-off is that only one TUI OAuth flow can hold the port at a time; a second concurrent flow fails with `EADDRINUSE`. To override it, pass `--callback-url` or set `MCP_OAUTH_CALLBACK_URL`: use a different fixed port per instance, or `http://127.0.0.1:0/oauth/callback` for an OS-assigned ephemeral port when your authorization server registers redirect URIs dynamically.
7674 
77<Warning>
78 Redirect URIs must match **exactly** what you registered. `localhost` and
79 `127.0.0.1` are different URIs as far as an authorization server is concerned.
80</Warning>
75> [!WARNING]
76>
77> Redirect URIs must match **exactly** what you registered. `localhost` and
78> `127.0.0.1` are different URIs as far as an authorization server is concerned.
8179 
8280Per-server OAuth fields in the catalog (static client id/secret, scopes, the enterprise-managed flag) are applied automatically. Install-wide settings (CIMD, enterprise IdP) come from `~/.mcp-inspector/storage/client.json`, the same file the web client's **Client Settings** dialog writes. Point at a different one with `--client-config` or `MCP_CLIENT_CONFIG_PATH`.
8381 
8482See [Authorization](/docs/draft/tools/inspector/authorization) for the full picture.
8583 
86<Frame caption="The Auth tab. It shows the same OAuth fields as the web client's Connection Info or, before any authorization has happened, says there is no OAuth information yet.">
87 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-auth.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=2e5ef574e80e81c4b49fa2ef0eac0528" width="2986" height="1832" data-path="images/inspector/tui-auth.png" />
88</Frame>
84![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/tui-auth.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=2e5ef574e80e81c4b49fa2ef0eac0528)
8985 
9086## Requirements
9187 

docs/draft/tools/inspector/web Changed · +8 / -24 lines

from line 50
5050 
5151**Network** and **Console** never appear together. Legacy and modern eras are described in [Protocol eras](/docs/draft/tools/inspector/protocol-eras).
5252 
53<Frame caption="The tab bar on a connected server. Which tabs appear depends on the capabilities the server reported.">
54 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tab-bar.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=04bb61c4a45ff12e8337c701e195c386" width="3840" height="2400" data-path="images/inspector/web-tab-bar.png" />
55</Frame>
53![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tab-bar.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=04bb61c4a45ff12e8337c701e195c386)
5654 
5755### The monitoring sidebar
5856 
5957**Tasks**, **Logs**, **Protocol**, **Network**, and **Console** form a *monitor group*. Pin the group and they leave the tab bar and move into a resizable right-hand column, so you can watch traffic while working in Tools or Resources. Drag the column's edge to resize it, from 320 to 720 pixels wide. The column width and the selected monitor tab persist across reloads.
6058 
61<Frame caption="The monitoring sidebar pinned beside the Tools screen and dragged to its full width, with the tool call's request and response expanded in the Protocol stream.">
62 <img src="https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max&auto=format&n=pUebPdrb6PY5_mfH&q=85&s=2ced9eeefa020be53fe4fe957b7d07c6" width="3840" height="2160" data-path="images/inspector/web-monitor-sidebar.png" />
63</Frame>
59![](https://mintcdn.com/mcp/pUebPdrb6PY5_mfH/images/inspector/web-monitor-sidebar.png?fit=max\&auto=format\&n=pUebPdrb6PY5_mfH\&q=85\&s=2ced9eeefa020be53fe4fe957b7d07c6)
6460 
6561## Servers
6662 
from line 85
8985 
9086A footer at the bottom of **Server Settings**, **Client Settings** and the **Add / Edit / Clone server** dialogs names the secret store in use, so you see it where you type a secret. It turns into a warning when secrets are memory-only (lost on restart), in an unencrypted file, in a file with loose permissions, or in a file that can't be read.
9187 
92<Frame caption="Server Settings with Advertised Extensions expanded. Unchecking one changes what the Inspector declares at connect.">
93 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-server-settings.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=d42be09ee8de7e45e58a8ff1a444ba52" width="3840" height="2160" data-path="images/inspector/web-server-settings.png" />
94</Frame>
88![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-server-settings.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=d42be09ee8de7e45e58a8ff1a444ba52)
9589 
9690## Tools
9791 
from line 93
9993 
10094On modern-era servers this screen also shows mirrored `Mcp-Param-*` headers, excluded tools, and distinct `-32602` error panels, all covered in [Protocol eras](/docs/draft/tools/inspector/protocol-eras#tools-mirrored-headers-and-excluded-tools).
10195 
102<Frame caption="A tool call and its rendered result. The argument form collapses into the result panel once the call returns.">
103 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tools.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=7ef469a969f398ac0ec70cf019c133da" width="3840" height="2160" data-path="images/inspector/web-tools.png" />
104</Frame>
96![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-tools.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=7ef469a969f398ac0ec70cf019c133da)
10597 
10698## Resources
10799 
108100Lists resources and resource templates with their MIME types and descriptions, reads content on selection, and offers **Subscribe** on servers that support subscriptions. The subscription mechanics differ by era; see [Resource subscriptions](/docs/draft/tools/inspector/protocol-eras#resource-subscriptions).
109101 
110<Frame caption="A resource read, with an active subscription listed below the resource list.">
111 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-resources.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=1a94ea452e1ef8aaf2c9f486ed810b28" width="3840" height="2160" data-path="images/inspector/web-resources.png" />
112</Frame>
102![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-resources.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=1a94ea452e1ef8aaf2c9f486ed810b28)
113103 
114104## Prompts
115105 
116106Lists prompt templates with their arguments, and renders the generated messages for the arguments you supply, which is the fastest way to confirm a prompt produces what you intended.
117107 
118<Frame caption="A prompt rendered with the arguments supplied.">
119 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-prompts.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=81b16312b1adff5601622a72444b0f92" width="3840" height="2160" data-path="images/inspector/web-prompts.png" />
120</Frame>
108![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-prompts.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=81b16312b1adff5601622a72444b0f92)
121109 
122110## Apps
123111 
from line 117
129117 
130118See [Recipes](/docs/draft/tools/inspector/recipes#reviewing-an-mcp-app) for the CLI-first automated review flow.
131119 
132<Frame caption="An MCP App rendered in its sandboxed frame, with the app's own logs below it.">
133 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-apps.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=bd311848514f7d251640012986abfc4d" width="3840" height="2160" data-path="images/inspector/web-apps.png" />
134</Frame>
120![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-apps.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=bd311848514f7d251640012986abfc4d)
135121 
136122## Protocol, Network, and Console
137123 
from line 129
143129 
144130Secrets in Network headers and bodies are masked, with a control to reveal them; Protocol and Console show traffic as sent. Entries can be cleared or exported.
145131 
146<Frame caption="The Protocol tab with an entry expanded, showing the full JSON-RPC exchange.">
147 <img src="https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-protocol.png?fit=max&auto=format&n=gk28X8wi_tbRYzej&q=85&s=f31338c83a389c5588f11c0d5b2b97ed" width="3840" height="2160" data-path="images/inspector/web-protocol.png" />
148</Frame>
132![](https://mintcdn.com/mcp/gk28X8wi_tbRYzej/images/inspector/web-protocol.png?fit=max\&auto=format\&n=gk28X8wi_tbRYzej\&q=85\&s=f31338c83a389c5588f11c0d5b2b97ed)
149133 
150134## Deep links
151135 

docs/draft/tutorials/security/authorization Changed · +893 / -913 lines

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 16
1616* You're building for enterprise environments with strict access controls
1717* You want to implement rate limiting or usage tracking per user
1818 
19<Tip>
20 **Authorization for Local MCP Servers**
21 
22 For MCP servers using the [STDIO transport](/specification/latest/basic/transports#stdio), you can use environment-based credentials or credentials provided by third-party libraries embedded directly in the MCP server instead. Because a STDIO-built MCP server runs locally, it has access to a range of flexible options when it comes to acquiring user credentials that may or may not rely on in-browser authentication and authorization flows.
23 
24 OAuth flows, in turn, are designed for HTTP-based transports where the MCP server is remotely-hosted and the client uses OAuth to establish that a user is authorized to access said remote server.
25</Tip>
19> [!TIP]
20>
21> **Authorization for Local MCP Servers**
22>
23> For MCP servers using the [STDIO transport](/specification/latest/basic/transports#stdio), you can use environment-based credentials or credentials provided by third-party libraries embedded directly in the MCP server instead. Because a STDIO-built MCP server runs locally, it has access to a range of flexible options when it comes to acquiring user credentials that may or may not rely on in-browser authentication and authorization flows.
24>
25> OAuth flows, in turn, are designed for HTTP-based transports where the MCP server is remotely-hosted and the client uses OAuth to establish that a user is authorized to access said remote server.
2626 
2727## The Authorization Flow: Step by Step
2828 
2929Let's walk through what happens when a client wants to connect to your protected MCP server:
3030 
31<Steps>
32 <Step title="Initial Handshake">
33 When your MCP client first tries to connect, your server responds with a `401 Unauthorized` and tells the client where to find authorization information, captured in a [Protected Resource Metadata (PRM) document](https://datatracker.ietf.org/doc/html/rfc9728). The document is hosted by the MCP server, follows a predictable path pattern, and is provided to the client in the `resource_metadata` parameter within the `WWW-Authenticate` header.
34 
35 ```http theme={null}
36 HTTP/1.1 401 Unauthorized
37 WWW-Authenticate: Bearer realm="mcp",
38 resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
39 ```
40 
41 This tells the client that authorization is required for the MCP server and where to get the necessary information to kickstart the authorization flow.
42 </Step>
43 
44 <Step title="Protected Resource Metadata Discovery">
45 With the URI pointer to the PRM document, the client will fetch the metadata to learn about the authorization server, supported scopes, and other resource information. The data is typically encapsulated in a JSON blob, similar to the one below.
46 
47 ```json theme={null}
48 {
49 "resource": "https://your-server.com/mcp",
50 "authorization_servers": ["https://auth.your-server.com"],
51 "scopes_supported": ["mcp:tools", "mcp:resources"]
52 }
53 ```
54 
55 You can see a more comprehensive example in [RFC 9728 Section 3.2](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r).
56 </Step>
57 
58 <Step title="Authorization Server Discovery">
59 Next, the client discovers what the authorization server can do by fetching its metadata. If the PRM document lists more than one authorization server, the client can decide which one to use.
60 
61 With an authorization server selected, the client will then construct a standard metadata URI and issue a request to the [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) or [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) endpoints (depending on authorization server support)
62 and retrieve another set of metadata properties that will allow it to know the endpoints it needs to complete the authorization flow.
63 
64 ```json theme={null}
65 {
66 "issuer": "https://auth.your-server.com",
67 "authorization_endpoint": "https://auth.your-server.com/authorize",
68 "token_endpoint": "https://auth.your-server.com/token",
69 "registration_endpoint": "https://auth.your-server.com/register"
70 }
71 ```
72 </Step>
73 
74 <Step title="Client Registration">
75 With all the metadata out of the way, the client now needs to make sure that it's registered with the authorization server. This can be done in two ways.
76 
77 First, the client can be **pre-registered** with a given authorization server, in which case it can have embedded client registration information that it uses to complete the authorization flow.
78 
79 Alternatively, the client can use **Dynamic Client Registration** (DCR) to dynamically register itself with the authorization server. The latter scenario requires the authorization server to support DCR. If the authorization server does support DCR, the client will send a request to the `registration_endpoint` with its information:
80 
81 ```json theme={null}
82 {
83 "client_name": "My MCP Client",
84 "redirect_uris": ["http://localhost:3000/callback"],
85 "grant_types": ["authorization_code", "refresh_token"],
86 "response_types": ["code"]
87 }
88 ```
89 
90 If the registration succeeds, the authorization server will return a JSON blob with client registration information.
91 
92 <Tip>
93 **No DCR or Pre-Registration**
94 
95 In case an MCP client connects to an MCP server that doesn't use an authorization server that supports DCR and the client is not pre-registered with said authorization server, it's the responsibility of the client developer to provide an affordance for the end-user to enter client information manually.
96 </Tip>
97 </Step>
98 
99 <Step title="User Authorization">
100 The client will now need to open a browser to the `/authorize` endpoint, where the user can log in and grant the required permissions. The authorization server will then redirect back to the client with an authorization code that the client exchanges for tokens:
101 
102 ```json theme={null}
103 {
104 "access_token": "eyJhbGciOiJSUzI1NiIs...",
105 "refresh_token": "def502...",
106 "token_type": "Bearer",
107 "expires_in": 3600
108 }
109 ```
110 
111 The access token is what the client will use to authenticate requests to the MCP server. This step follows standard [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) conventions.
112 </Step>
113 
114 <Step title="Making Authenticated Requests">
115 Finally, the client can make requests to your MCP server using the access token embedded in the `Authorization` header:
116 
117 ```http theme={null}
118 GET /mcp HTTP/1.1
119 Host: your-server.com
120 Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
121 ```
122 
123 The MCP server will need to validate the token and process the request if the token is valid and has the required permissions.
124 </Step>
125</Steps>
31**1. Initial Handshake**
32 
33When your MCP client first tries to connect, your server responds with a `401 Unauthorized` and tells the client where to find authorization information, captured in a [Protected Resource Metadata (PRM) document](https://datatracker.ietf.org/doc/html/rfc9728). The document is hosted by the MCP server, follows a predictable path pattern, and is provided to the client in the `resource_metadata` parameter within the `WWW-Authenticate` header.
34 
35```http theme={null}
36HTTP/1.1 401 Unauthorized
37WWW-Authenticate: Bearer realm="mcp",
38 resource_metadata="https://your-server.com/.well-known/oauth-protected-resource"
39```
40 
41This tells the client that authorization is required for the MCP server and where to get the necessary information to kickstart the authorization flow.
42 
43**2. Protected Resource Metadata Discovery**
44 
45With the URI pointer to the PRM document, the client will fetch the metadata to learn about the authorization server, supported scopes, and other resource information. The data is typically encapsulated in a JSON blob, similar to the one below.
46 
47```json theme={null}
48{
49 "resource": "https://your-server.com/mcp",
50 "authorization_servers": ["https://auth.your-server.com"],
51 "scopes_supported": ["mcp:tools", "mcp:resources"]
52}
53```
54 
55You can see a more comprehensive example in [RFC 9728 Section 3.2](https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata-r).
56 
57**3. Authorization Server Discovery**
58 
59Next, the client discovers what the authorization server can do by fetching its metadata. If the PRM document lists more than one authorization server, the client can decide which one to use.
60 
61With an authorization server selected, the client will then construct a standard metadata URI and issue a request to the [OpenID Connect (OIDC) Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) or [OAuth 2.0 Auth Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414) endpoints (depending on authorization server support)
62and retrieve another set of metadata properties that will allow it to know the endpoints it needs to complete the authorization flow.
63 
64```json theme={null}
65{
66 "issuer": "https://auth.your-server.com",
67 "authorization_endpoint": "https://auth.your-server.com/authorize",
68 "token_endpoint": "https://auth.your-server.com/token",
69 "registration_endpoint": "https://auth.your-server.com/register"
70}
71```
72 
73**4. Client Registration**
74 
75With all the metadata out of the way, the client now needs to make sure that it's registered with the authorization server. This can be done in two ways.
76 
77First, the client can be **pre-registered** with a given authorization server, in which case it can have embedded client registration information that it uses to complete the authorization flow.
78 
79Alternatively, the client can use **Dynamic Client Registration** (DCR) to dynamically register itself with the authorization server. The latter scenario requires the authorization server to support DCR. If the authorization server does support DCR, the client will send a request to the `registration_endpoint` with its information:
80 
81```json theme={null}
82{
83 "client_name": "My MCP Client",
84 "redirect_uris": ["http://localhost:3000/callback"],
85 "grant_types": ["authorization_code", "refresh_token"],
86 "response_types": ["code"]
87}
88```
89 
90If the registration succeeds, the authorization server will return a JSON blob with client registration information.
91 
92> [!TIP]
93>
94> **No DCR or Pre-Registration**
95>
96> In case an MCP client connects to an MCP server that doesn't use an authorization server that supports DCR and the client is not pre-registered with said authorization server, it's the responsibility of the client developer to provide an affordance for the end-user to enter client information manually.
97 
98**5. User Authorization**
99 
100The client will now need to open a browser to the `/authorize` endpoint, where the user can log in and grant the required permissions. The authorization server will then redirect back to the client with an authorization code that the client exchanges for tokens:
101 
102```json theme={null}
103{
104 "access_token": "eyJhbGciOiJSUzI1NiIs...",
105 "refresh_token": "def502...",
106 "token_type": "Bearer",
107 "expires_in": 3600
108}
109```
110 
111The access token is what the client will use to authenticate requests to the MCP server. This step follows standard [OAuth 2.1 authorization code with PKCE](https://oauth.net/2/grant-types/authorization-code/) conventions.
112 
113**6. Making Authenticated Requests**
114 
115Finally, the client can make requests to your MCP server using the access token embedded in the `Authorization` header:
116 
117```http theme={null}
118GET /mcp HTTP/1.1
119Host: your-server.com
120Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
121```
122 
123The MCP server will need to validate the token and process the request if the token is valid and has the required permissions.
126124 
127125## Implementation Example
128126 
from line 138
140138 
141139This command will pull the Keycloak container image locally and bootstrap the basic configuration. It will run on port `8080` and have an `admin` user with `admin` password.
142140 
143<Warning>
144 **Not for Production**
145 
146 The configuration above may be suitable for testing and experimentation; however, you should never use it in production. Refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) guide for additional details on how to deploy the authorization server for scenarios that require reliability, security, and high availability.
147</Warning>
141> [!WARNING]
142>
143> **Not for Production**
144>
145> The configuration above may be suitable for testing and experimentation; however, you should never use it in production. Refer to the [Configuring Keycloak for production](https://www.keycloak.org/server/configuration-production) guide for additional details on how to deploy the authorization server for scenarios that require reliability, security, and high availability.
148146 
149147You will be able to access the Keycloak authorization server from your browser at `http://localhost:8080`.
150148 
151<Frame>
152 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-browser.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=cba689d986e113cbe937d732ac0558b6" alt="Keycloak admin dashboard authentication dialog." width="1834" height="1450" data-path="images/tutorial-authorization/keycloak-browser.png" />
153</Frame>
149![Keycloak admin dashboard authentication dialog.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-browser.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=cba689d986e113cbe937d732ac0558b6)
154150 
155151When running with the default configuration, Keycloak will already support many of the capabilities that we need for MCP servers, including Dynamic Client Registration. You can check this by looking at the OIDC configuration, available at:
156152 
from line 158
162158 
163159Go to **Client scopes** in the Keycloak dashboard and create a new `mcp:tools` scope. We will use this to access all of the tools on our MCP server.
164160 
165<Frame>
166 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-scopes.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=3cd49dc2e070027609ae495751e0db58" alt="Configuring Keycloak scopes." width="1999" height="1710" data-path="images/tutorial-authorization/keycloak-scopes.png" />
167</Frame>
161![Configuring Keycloak scopes.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-scopes.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=3cd49dc2e070027609ae495751e0db58)
168162 
169163After creating the scope, make sure that you assign its type to **Default** and have flipped the **Include in token scope** switch, as this will be needed for token validation.
170164 
from line 166
172166 
173167To do this, open your `mcp:tools` client scope and click on **Mappers**, followed by **Configure a new mapper**. Select **Audience**.
174168 
175<Frame>
176 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/scope-add-audience.gif?s=6ea9cf20c397f4c79c491c2e39019272" alt="Configuring an audience for a token in Keycloak." width="1080" height="921" data-path="images/tutorial-authorization/scope-add-audience.gif" />
177</Frame>
169![Configuring an audience for a token in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/scope-add-audience.gif?s=6ea9cf20c397f4c79c491c2e39019272)
178170 
179171For **Name**, use `audience-config`. Add a value for **Included Custom Audience**, set to `http://localhost:3000`. This will be the URI of our test server.
180172 
181<Warning>
182 **Not for Production**
183 
184 The audience configuration above is meant for testing. For production scenarios, additional set-up and configuration will be required to ensure that audiences are properly constrained for issued tokens. Specifically, the audience needs to be based on the resource parameter passed from the client, not a fixed value.
185</Warning>
173> [!WARNING]
174>
175> **Not for Production**
176>
177> The audience configuration above is meant for testing. For production scenarios, additional set-up and configuration will be required to ensure that audiences are properly constrained for issued tokens. Specifically, the audience needs to be based on the resource parameter passed from the client, not a fixed value.
186178 
187179Now, navigate to **Clients**, then **Client registration**, and then **Trusted Hosts**. Disable the **Client URIs Must Match** setting and add the hosts from which you're testing. You can get your current host IP by running the `ifconfig` command on Linux or macOS, or `ipconfig` on Windows. You can see the IP address you need to add by looking at the keycloak logs for a line that looks like `Failed to verify remote host : 192.168.215.1`. Check that the IP address is associated with your host. This may be for a bridge network depending on your docker setup.
188180 
189<Frame>
190 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client.gif?s=b5d40b36a5f1ea1e818821bb8ea77f6b" alt="Setting up client registration details in Keycloak." width="1199" height="1027" data-path="images/tutorial-authorization/keycloak-client.gif" />
191</Frame>
192 
193<Warning>
194 **Getting the Host**
195 
196 If you are running Keycloak from a container, you will also be able to see the host IP from the Terminal in the container logs.
197</Warning>
181![Setting up client registration details in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client.gif?s=b5d40b36a5f1ea1e818821bb8ea77f6b)
182 
183> [!WARNING]
184>
185> **Getting the Host**
186>
187> If you are running Keycloak from a container, you will also be able to see the host IP from the Terminal in the container logs.
198188 
199189Lastly, we need to register a new client that we can use with the **MCP server itself** to talk to Keycloak for things like [token introspection](https://oauth.net/2/token-introspection/). To do that:
200190 
from line 198
208198 
209199When you open the client details, go to **Credentials** and take note of the **Client Secret**.
210200 
211<Frame>
212 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client-auth.gif?s=7152c41a5746994fd399024bc4659e40" alt="Creating a new client in Keycloak." width="1200" height="1023" data-path="images/tutorial-authorization/keycloak-client-auth.gif" />
213</Frame>
214 
215<Warning>
216 **Handling Secrets**
217 
218 Never embed client credentials directly in your code. We recommend using environment variables or specialized solutions for secret storage.
219</Warning>
201![Creating a new client in Keycloak.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-client-auth.gif?s=7152c41a5746994fd399024bc4659e40)
202 
203> [!WARNING]
204>
205> **Handling Secrets**
206>
207> Never embed client credentials directly in your code. We recommend using environment variables or specialized solutions for secret storage.
220208 
221209With Keycloak configured, every time the authorization flow is triggered, your MCP server will receive a token like this:
222210 
from line 234
246234}.[Signature]
247235```
248236 
249<Warning>
250 **Embedded Audience**
251 
252 Notice the `aud` claim embedded in the token - it's currently set to be the URI of the test MCP server and it's inferred from the scope that we've previously configured. This will be important in our implementation to validate.
253</Warning>
237> [!WARNING]
238>
239> **Embedded Audience**
240>
241> Notice the `aud` claim embedded in the token - it's currently set to be the URI of the test MCP server and it's inferred from the scope that we've previously configured. This will be important in our implementation to validate.
254242 
255243### MCP Server Setup
256244 
from line 246
258246 
259247For our testing purposes, we will create an extremely simple MCP server that exposes two tools - one for addition and another for multiplication. The server will require authorization to access these.
260248 
261<Tabs>
262 <Tab title="TypeScript">
263 You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
264 
265 Prior to running the code below, ensure that you have a `.env` file with the following content:
266 
267 ```env theme={null}
268 # Server host/port
269 HOST=localhost
270 PORT=3000
271 
272 # Auth server location
273 AUTH_HOST=localhost
274 AUTH_PORT=8080
275 AUTH_REALM=master
276 
277 # Keycloak OAuth client credentials
278 OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
279 OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
280 ```
281 
282 `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
283 
284 In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
285 
286 ```typescript theme={null}
287 import "dotenv/config";
288 import express from "express";
289 import { randomUUID } from "node:crypto";
290 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
291 import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
292 import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
293 import { z } from "zod";
294 import cors from "cors";
295 import {
296 mcpAuthMetadataRouter,
297 getOAuthProtectedResourceMetadataUrl,
298 } from "@modelcontextprotocol/sdk/server/auth/router.js";
299 import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
300 import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
301 import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
302 const CONFIG = {
303 host: process.env.HOST || "localhost",
304 port: Number(process.env.PORT) || 3000,
305 auth: {
306 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
307 port: Number(process.env.AUTH_PORT) || 8080,
308 realm: process.env.AUTH_REALM || "master",
309 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
310 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
311 },
312 };
313 
314 function createOAuthUrls() {
315 const authBaseUrl = new URL(
316 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
317 );
318 return {
319 issuer: authBaseUrl.toString(),
320 introspection_endpoint: new URL(
321 "protocol/openid-connect/token/introspect",
322 authBaseUrl,
323 ).toString(),
324 authorization_endpoint: new URL(
325 "protocol/openid-connect/auth",
326 authBaseUrl,
327 ).toString(),
328 token_endpoint: new URL(
329 "protocol/openid-connect/token",
330 authBaseUrl,
331 ).toString(),
332 };
333 }
334 
335 function createRequestLogger() {
336 return (req: any, res: any, next: any) => {
337 const start = Date.now();
338 res.on("finish", () => {
339 const ms = Date.now() - start;
340 console.log(
341 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
342 );
343 });
344 next();
345 };
346 }
347 
348 const app = express();
349 
350 app.use(
351 express.json({
352 verify: (req: any, _res, buf) => {
353 req.rawBody = buf?.toString() ?? "";
354 },
355 }),
356 );
357 
358 app.use(
359 cors({
360 origin: "*",
361 exposedHeaders: ["Mcp-Session-Id"],
362 }),
363 );
364 
365 app.use(createRequestLogger());
366 
367 const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
368 const oauthUrls = createOAuthUrls();
369 
370 const oauthMetadata: OAuthMetadata = {
371 ...oauthUrls,
372 response_types_supported: ["code"],
373 };
374 
375 const tokenVerifier = {
376 verifyAccessToken: async (token: string) => {
377 const endpoint = oauthMetadata.introspection_endpoint;
378 
379 if (!endpoint) {
380 console.error("[auth] no introspection endpoint in metadata");
381 throw new Error("No token verification endpoint available in metadata");
382 }
383 
384 const params = new URLSearchParams({
385 token: token,
386 client_id: CONFIG.auth.clientId,
387 });
388 
389 if (CONFIG.auth.clientSecret) {
390 params.set("client_secret", CONFIG.auth.clientSecret);
391 }
392 
393 let response: Response;
394 try {
395 response = await fetch(endpoint, {
396 method: "POST",
397 headers: {
398 "Content-Type": "application/x-www-form-urlencoded",
399 },
400 body: params.toString(),
401 });
402 } catch (e) {
403 console.error("[auth] introspection fetch threw", e);
404 throw e;
405 }
406 
407 if (!response.ok) {
408 const txt = await response.text();
409 console.error("[auth] introspection non-OK", { status: response.status });
410 
411 try {
412 const obj = JSON.parse(txt);
413 console.log(JSON.stringify(obj, null, 2));
414 } catch {
415 console.error(txt);
416 }
417 throw new Error(`Invalid or expired token: ${txt}`);
418 }
419 
420 let data: any;
421 try {
422 data = await response.json();
423 } catch (e) {
424 const txt = await response.text();
425 console.error("[auth] failed to parse introspection JSON", {
426 error: String(e),
427 body: txt,
428 });
429 throw e;
430 }
431 
432 if (data.active === false) {
433 throw new Error("Inactive token");
434 }
435 
436 if (!data.aud) {
437 throw new Error("Resource indicator (aud) missing");
438 }
439 
440 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
441 const allowed = audiences.some((a) => {
442 try {
443 return checkResourceAllowed({
444 requestedResource: a,
445 configuredResource: mcpServerUrl,
446 });
447 } catch {
448 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
449 // Those are never our resource, so treat them as "no match" instead of crashing.
450 return false;
451 }
452 });
453 if (!allowed) {
454 throw new Error(
455 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
456 );
457 }
458 
459 return {
460 token,
461 clientId: data.client_id,
462 scopes: data.scope ? data.scope.split(" ") : [],
463 expiresAt: data.exp,
464 };
465 },
466 };
467 app.use(
468 mcpAuthMetadataRouter({
469 oauthMetadata,
470 resourceServerUrl: mcpServerUrl,
471 scopesSupported: ["mcp:tools"],
472 resourceName: "MCP Demo Server",
473 }),
474 );
475 
476 const authMiddleware = requireBearerAuth({
477 verifier: tokenVerifier,
478 requiredScopes: [],
479 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
480 });
481 
482 const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
483 
484 function createMcpServer() {
485 const server = new McpServer({
486 name: "example-server",
487 version: "1.0.0",
488 });
489 
490 server.registerTool(
491 "add",
492 {
493 title: "Addition Tool",
494 description: "Add two numbers together",
495 inputSchema: {
496 a: z.number().describe("First number to add"),
497 b: z.number().describe("Second number to add"),
498 },
499 },
500 async ({ a, b }) => ({
501 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
502 }),
503 );
504 
505 server.registerTool(
506 "multiply",
507 {
508 title: "Multiplication Tool",
509 description: "Multiply two numbers together",
510 inputSchema: {
511 x: z.number().describe("First number to multiply"),
512 y: z.number().describe("Second number to multiply"),
513 },
514 },
515 async ({ x, y }) => ({
516 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
517 }),
518 );
519 
520 return server;
521 }
522 
523 const mcpPostHandler = async (req: express.Request, res: express.Response) => {
524 const sessionId = req.headers["mcp-session-id"] as string | undefined;
525 let transport: StreamableHTTPServerTransport;
526 
527 if (sessionId && transports[sessionId]) {
528 transport = transports[sessionId];
529 } else if (!sessionId && isInitializeRequest(req.body)) {
530 transport = new StreamableHTTPServerTransport({
531 sessionIdGenerator: () => randomUUID(),
532 onsessioninitialized: (sessionId) => {
533 transports[sessionId] = transport;
534 },
535 });
536 
537 transport.onclose = () => {
538 if (transport.sessionId) {
539 delete transports[transport.sessionId];
540 }
541 };
542 
543 const server = createMcpServer();
544 await server.connect(transport);
545 } else {
546 res.status(400).json({
547 jsonrpc: "2.0",
548 error: {
549 code: -32000,
550 message: "Bad Request: No valid session ID provided",
551 },
552 id: null,
553 });
554 return;
555 }
556 
557 await transport.handleRequest(req, res, req.body);
558 };
559 
560 const handleSessionRequest = async (
561 req: express.Request,
562 res: express.Response,
563 ) => {
564 const sessionId = req.headers["mcp-session-id"] as string | undefined;
565 if (!sessionId || !transports[sessionId]) {
566 res.status(400).send("Invalid or missing session ID");
567 return;
568 }
569 
570 const transport = transports[sessionId];
571 await transport.handleRequest(req, res);
572 };
573 
574 app.post("/", authMiddleware, mcpPostHandler);
575 app.get("/", authMiddleware, handleSessionRequest);
576 app.delete("/", authMiddleware, handleSessionRequest);
577 
578 app.listen(CONFIG.port, CONFIG.host, () => {
579 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
580 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
249**TypeScript**
250 
251You can see the complete TypeScript project in the [sample repository](https://github.com/localden/min-ts-mcp-auth).
252 
253Prior to running the code below, ensure that you have a `.env` file with the following content:
254 
255```env theme={null}
256# Server host/port
257HOST=localhost
258PORT=3000
259 
260# Auth server location
261AUTH_HOST=localhost
262AUTH_PORT=8080
263AUTH_REALM=master
264 
265# Keycloak OAuth client credentials
266OAUTH_CLIENT_ID=<YOUR_SERVER_CLIENT_ID>
267OAUTH_CLIENT_SECRET=<YOUR_SERVER_CLIENT_SECRET>
268```
269 
270`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier.
271 
272In addition to implementing the MCP authorization specification, the server below also does token introspection via Keycloak to make sure that the token it receives from the client is valid. It also implements basic logging to allow you to easily diagnose any issues.
273 
274```typescript theme={null}
275import "dotenv/config";
276import express from "express";
277import { randomUUID } from "node:crypto";
278import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
279import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281import { z } from "zod";
282import cors from "cors";
283import {
284 mcpAuthMetadataRouter,
285 getOAuthProtectedResourceMetadataUrl,
286} from "@modelcontextprotocol/sdk/server/auth/router.js";
287import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
288import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
289import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
290const CONFIG = {
291 host: process.env.HOST || "localhost",
292 port: Number(process.env.PORT) || 3000,
293 auth: {
294 host: process.env.AUTH_HOST || process.env.HOST || "localhost",
295 port: Number(process.env.AUTH_PORT) || 8080,
296 realm: process.env.AUTH_REALM || "master",
297 clientId: process.env.OAUTH_CLIENT_ID || "mcp-server",
298 clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
299 },
300};
301 
302function createOAuthUrls() {
303 const authBaseUrl = new URL(
304 `http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`,
305 );
306 return {
307 issuer: authBaseUrl.toString(),
308 introspection_endpoint: new URL(
309 "protocol/openid-connect/token/introspect",
310 authBaseUrl,
311 ).toString(),
312 authorization_endpoint: new URL(
313 "protocol/openid-connect/auth",
314 authBaseUrl,
315 ).toString(),
316 token_endpoint: new URL(
317 "protocol/openid-connect/token",
318 authBaseUrl,
319 ).toString(),
320 };
321}
322 
323function createRequestLogger() {
324 return (req: any, res: any, next: any) => {
325 const start = Date.now();
326 res.on("finish", () => {
327 const ms = Date.now() - start;
581328 console.log(
582 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
329 `${req.method} ${req.originalUrl} -> ${res.statusCode} ${ms}ms`,
583330 );
584331 });
585 ```
586 
587 When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
588 
589 For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
590 </Tab>
591 
592 <Tab title="Python">
593 You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
594 
595 To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
596 
597 Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
598 
599 ```python theme={null}
600 """Configuration settings for the MCP auth server."""
601 
602 import os
603 
604 
605 class Config:
606 """Configuration class that loads from environment variables with sensible defaults."""
607 
608 # Server settings
609 HOST: str = os.getenv("HOST", "localhost")
610 PORT: int = int(os.getenv("PORT", "3000"))
611 
612 # Auth server settings
613 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
614 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
615 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
616 
617 # OAuth client settings
618 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
619 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
620 
621 # Scope required on every token
622 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
623 
624 @property
625 def server_url(self) -> str:
626 """Build the server URL."""
627 return f"http://{self.HOST}:{self.PORT}"
628 
629 @property
630 def auth_base_url(self) -> str:
631 """Build the auth server base URL."""
632 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
633 
634 
635 # Global configuration instance
636 config = Config()
637 ```
638 
639 `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
640 
641 The server implementation is as follows:
642 
643 ```python theme={null}
644 import datetime
645 import logging
646 from typing import Any
647 from urllib.parse import urljoin
648 
649 from pydantic import AnyHttpUrl
650 
651 from mcp.server import MCPServer
652 from mcp.server.auth.settings import AuthSettings
653 
654 from .config import config
655 from .token_verifier import IntrospectionTokenVerifier
656 
657 logger = logging.getLogger(__name__)
658 
659 
660 def create_oauth_urls() -> dict[str, str]:
661 """Create OAuth URLs based on configuration (Keycloak-style)."""
662 auth_base_url = config.auth_base_url
663 
332 next();
333 };
334}
335 
336const app = express();
337 
338app.use(
339 express.json({
340 verify: (req: any, _res, buf) => {
341 req.rawBody = buf?.toString() ?? "";
342 },
343 }),
344);
345 
346app.use(
347 cors({
348 origin: "*",
349 exposedHeaders: ["Mcp-Session-Id"],
350 }),
351);
352 
353app.use(createRequestLogger());
354 
355const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
356const oauthUrls = createOAuthUrls();
357 
358const oauthMetadata: OAuthMetadata = {
359 ...oauthUrls,
360 response_types_supported: ["code"],
361};
362 
363const tokenVerifier = {
364 verifyAccessToken: async (token: string) => {
365 const endpoint = oauthMetadata.introspection_endpoint;
366 
367 if (!endpoint) {
368 console.error("[auth] no introspection endpoint in metadata");
369 throw new Error("No token verification endpoint available in metadata");
370 }
371 
372 const params = new URLSearchParams({
373 token: token,
374 client_id: CONFIG.auth.clientId,
375 });
376 
377 if (CONFIG.auth.clientSecret) {
378 params.set("client_secret", CONFIG.auth.clientSecret);
379 }
380 
381 let response: Response;
382 try {
383 response = await fetch(endpoint, {
384 method: "POST",
385 headers: {
386 "Content-Type": "application/x-www-form-urlencoded",
387 },
388 body: params.toString(),
389 });
390 } catch (e) {
391 console.error("[auth] introspection fetch threw", e);
392 throw e;
393 }
394 
395 if (!response.ok) {
396 const txt = await response.text();
397 console.error("[auth] introspection non-OK", { status: response.status });
398 
399 try {
400 const obj = JSON.parse(txt);
401 console.log(JSON.stringify(obj, null, 2));
402 } catch {
403 console.error(txt);
404 }
405 throw new Error(`Invalid or expired token: ${txt}`);
406 }
407 
408 let data: any;
409 try {
410 data = await response.json();
411 } catch (e) {
412 const txt = await response.text();
413 console.error("[auth] failed to parse introspection JSON", {
414 error: String(e),
415 body: txt,
416 });
417 throw e;
418 }
419 
420 if (data.active === false) {
421 throw new Error("Inactive token");
422 }
423 
424 if (!data.aud) {
425 throw new Error("Resource indicator (aud) missing");
426 }
427 
428 const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
429 const allowed = audiences.some((a) => {
430 try {
431 return checkResourceAllowed({
432 requestedResource: a,
433 configuredResource: mcpServerUrl,
434 });
435 } catch {
436 // Keycloak tokens include non-URL audiences (e.g. "account", "test-client").
437 // Those are never our resource, so treat them as "no match" instead of crashing.
438 return false;
439 }
440 });
441 if (!allowed) {
442 throw new Error(
443 `None of the provided audiences are allowed. Expected ${mcpServerUrl}, got: ${audiences.join(", ")}`,
444 );
445 }
446 
447 return {
448 token,
449 clientId: data.client_id,
450 scopes: data.scope ? data.scope.split(" ") : [],
451 expiresAt: data.exp,
452 };
453 },
454};
455app.use(
456 mcpAuthMetadataRouter({
457 oauthMetadata,
458 resourceServerUrl: mcpServerUrl,
459 scopesSupported: ["mcp:tools"],
460 resourceName: "MCP Demo Server",
461 }),
462);
463 
464const authMiddleware = requireBearerAuth({
465 verifier: tokenVerifier,
466 requiredScopes: [],
467 resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
468});
469 
470const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
471 
472function createMcpServer() {
473 const server = new McpServer({
474 name: "example-server",
475 version: "1.0.0",
476 });
477 
478 server.registerTool(
479 "add",
480 {
481 title: "Addition Tool",
482 description: "Add two numbers together",
483 inputSchema: {
484 a: z.number().describe("First number to add"),
485 b: z.number().describe("Second number to add"),
486 },
487 },
488 async ({ a, b }) => ({
489 content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
490 }),
491 );
492 
493 server.registerTool(
494 "multiply",
495 {
496 title: "Multiplication Tool",
497 description: "Multiply two numbers together",
498 inputSchema: {
499 x: z.number().describe("First number to multiply"),
500 y: z.number().describe("Second number to multiply"),
501 },
502 },
503 async ({ x, y }) => ({
504 content: [{ type: "text", text: `${x} × ${y} = ${x * y}` }],
505 }),
506 );
507 
508 return server;
509}
510 
511const mcpPostHandler = async (req: express.Request, res: express.Response) => {
512 const sessionId = req.headers["mcp-session-id"] as string | undefined;
513 let transport: StreamableHTTPServerTransport;
514 
515 if (sessionId && transports[sessionId]) {
516 transport = transports[sessionId];
517 } else if (!sessionId && isInitializeRequest(req.body)) {
518 transport = new StreamableHTTPServerTransport({
519 sessionIdGenerator: () => randomUUID(),
520 onsessioninitialized: (sessionId) => {
521 transports[sessionId] = transport;
522 },
523 });
524 
525 transport.onclose = () => {
526 if (transport.sessionId) {
527 delete transports[transport.sessionId];
528 }
529 };
530 
531 const server = createMcpServer();
532 await server.connect(transport);
533 } else {
534 res.status(400).json({
535 jsonrpc: "2.0",
536 error: {
537 code: -32000,
538 message: "Bad Request: No valid session ID provided",
539 },
540 id: null,
541 });
542 return;
543 }
544 
545 await transport.handleRequest(req, res, req.body);
546};
547 
548const handleSessionRequest = async (
549 req: express.Request,
550 res: express.Response,
551) => {
552 const sessionId = req.headers["mcp-session-id"] as string | undefined;
553 if (!sessionId || !transports[sessionId]) {
554 res.status(400).send("Invalid or missing session ID");
555 return;
556 }
557 
558 const transport = transports[sessionId];
559 await transport.handleRequest(req, res);
560};
561 
562app.post("/", authMiddleware, mcpPostHandler);
563app.get("/", authMiddleware, handleSessionRequest);
564app.delete("/", authMiddleware, handleSessionRequest);
565 
566app.listen(CONFIG.port, CONFIG.host, () => {
567 console.log(`🚀 MCP Server running on ${mcpServerUrl.origin}`);
568 console.log(`📡 MCP endpoint available at ${mcpServerUrl.origin}`);
569 console.log(
570 `🔐 OAuth metadata available at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`,
571 );
572});
573```
574 
575When you run the server, you can add it to your MCP client, such as Visual Studio Code, by providing the MCP server endpoint.
576 
577For more details about implementing MCP servers in TypeScript, refer to the [TypeScript SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk).
578 
579**Python**
580 
581You can see the complete Python project in the [sample repository](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-auth).
582 
583To simplify our authorization interaction, in Python scenarios we rely on the `MCPServer` class from the [Python SDK](https://py.sdk.modelcontextprotocol.io/v2/run/authorization/). It publishes the Protected Resource Metadata document, answers unauthenticated requests with a `401` whose `WWW-Authenticate` header points back at that document, and hands every bearer token to a verifier that we supply. Many of the conventions around authorization, like the endpoints and token validation logic, are consistent across languages, but some offer simpler ways of integrating them in production scenarios.
584 
585Prior to writing the actual server, we need to set up our configuration in `config.py` - the contents are entirely based on your local server setup:
586 
587```python theme={null}
588"""Configuration settings for the MCP auth server."""
589 
590import os
591 
592 
593class Config:
594 """Configuration class that loads from environment variables with sensible defaults."""
595 
596 # Server settings
597 HOST: str = os.getenv("HOST", "localhost")
598 PORT: int = int(os.getenv("PORT", "3000"))
599 
600 # Auth server settings
601 AUTH_HOST: str = os.getenv("AUTH_HOST", "localhost")
602 AUTH_PORT: int = int(os.getenv("AUTH_PORT", "8080"))
603 AUTH_REALM: str = os.getenv("AUTH_REALM", "master")
604 
605 # OAuth client settings
606 OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID", "test-client")
607 OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET", "")
608 
609 # Scope required on every token
610 MCP_SCOPE: str = os.getenv("MCP_SCOPE", "mcp:tools")
611 
612 @property
613 def server_url(self) -> str:
614 """Build the server URL."""
615 return f"http://{self.HOST}:{self.PORT}"
616 
617 @property
618 def auth_base_url(self) -> str:
619 """Build the auth server base URL."""
620 return f"http://{self.AUTH_HOST}:{self.AUTH_PORT}/realms/{self.AUTH_REALM}/"
621 
622 
623# Global configuration instance
624config = Config()
625```
626 
627`OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are associated with the MCP server client we created earlier. Set them in your environment before starting the server.
628 
629The server implementation is as follows:
630 
631```python theme={null}
632import datetime
633import logging
634from typing import Any
635from urllib.parse import urljoin
636 
637from pydantic import AnyHttpUrl
638 
639from mcp.server import MCPServer
640from mcp.server.auth.settings import AuthSettings
641 
642from .config import config
643from .token_verifier import IntrospectionTokenVerifier
644 
645logger = logging.getLogger(__name__)
646 
647 
648def create_oauth_urls() -> dict[str, str]:
649 """Create OAuth URLs based on configuration (Keycloak-style)."""
650 auth_base_url = config.auth_base_url
651 
652 return {
653 "issuer": auth_base_url,
654 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
655 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
656 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
657 }
658 
659 
660def create_server() -> MCPServer:
661 """Create and configure the MCP server."""
662 
663 oauth_urls = create_oauth_urls()
664 
665 token_verifier = IntrospectionTokenVerifier(
666 introspection_endpoint=oauth_urls["introspection_endpoint"],
667 server_url=config.server_url,
668 client_id=config.OAUTH_CLIENT_ID,
669 client_secret=config.OAUTH_CLIENT_SECRET,
670 )
671 
672 app = MCPServer(
673 name="MCP Resource Server",
674 instructions="Resource Server that validates tokens via Authorization Server introspection",
675 debug=True,
676 token_verifier=token_verifier,
677 auth=AuthSettings(
678 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
679 required_scopes=[config.MCP_SCOPE],
680 resource_server_url=AnyHttpUrl(config.server_url),
681 ),
682 )
683 
684 @app.tool()
685 async def add_numbers(a: float, b: float) -> dict[str, Any]:
686 """
687 Add two numbers together.
688 This tool demonstrates basic arithmetic operations with OAuth authentication.
689 
690 Args:
691 a: The first number to add
692 b: The second number to add
693 """
694 result = a + b
664695 return {
665 "issuer": auth_base_url,
666 "introspection_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token/introspect"),
667 "authorization_endpoint": urljoin(auth_base_url, "protocol/openid-connect/auth"),
668 "token_endpoint": urljoin(auth_base_url, "protocol/openid-connect/token"),
696 "operation": "addition",
697 "operand_a": a,
698 "operand_b": b,
699 "result": result,
700 "timestamp": datetime.datetime.now().isoformat(),
669701 }
670702 
671 
672 def create_server() -> MCPServer:
673 """Create and configure the MCP server."""
674 
675 oauth_urls = create_oauth_urls()
676 
677 token_verifier = IntrospectionTokenVerifier(
678 introspection_endpoint=oauth_urls["introspection_endpoint"],
679 server_url=config.server_url,
680 client_id=config.OAUTH_CLIENT_ID,
681 client_secret=config.OAUTH_CLIENT_SECRET,
703 @app.tool()
704 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
705 """
706 Multiply two numbers together.
707 This tool demonstrates basic arithmetic operations with OAuth authentication.
708 
709 Args:
710 x: The first number to multiply
711 y: The second number to multiply
712 """
713 result = x * y
714 return {
715 "operation": "multiplication",
716 "operand_x": x,
717 "operand_y": y,
718 "result": result,
719 "timestamp": datetime.datetime.now().isoformat(),
720 }
721 
722 return app
723 
724 
725def main() -> int:
726 """
727 Run the MCP Resource Server.
728 
729 This server:
730 - Provides RFC 9728 Protected Resource Metadata
731 - Validates tokens via Authorization Server introspection
732 - Serves MCP tools requiring authentication
733 
734 Configuration is loaded from config.py and environment variables.
735 """
736 logging.basicConfig(level=logging.INFO)
737 
738 oauth_urls = create_oauth_urls()
739 
740 try:
741 mcp_server = create_server()
742 
743 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
744 logger.info("Authorization Server: %s", oauth_urls["issuer"])
745 
746 mcp_server.run(
747 transport="streamable-http",
748 host=config.HOST,
749 port=config.PORT,
750 streamable_http_path="/",
682751 )
683 
684 app = MCPServer(
685 name="MCP Resource Server",
686 instructions="Resource Server that validates tokens via Authorization Server introspection",
687 debug=True,
688 token_verifier=token_verifier,
689 auth=AuthSettings(
690 issuer_url=AnyHttpUrl(oauth_urls["issuer"]),
691 required_scopes=[config.MCP_SCOPE],
692 resource_server_url=AnyHttpUrl(config.server_url),
693 ),
694 )
695 
696 @app.tool()
697 async def add_numbers(a: float, b: float) -> dict[str, Any]:
698 """
699 Add two numbers together.
700 This tool demonstrates basic arithmetic operations with OAuth authentication.
701 
702 Args:
703 a: The first number to add
704 b: The second number to add
705 """
706 result = a + b
707 return {
708 "operation": "addition",
709 "operand_a": a,
710 "operand_b": b,
711 "result": result,
712 "timestamp": datetime.datetime.now().isoformat(),
752 return 0
753 
754 except Exception:
755 logger.exception("Server error")
756 return 1
757 
758 
759if __name__ == "__main__":
760 exit(main())
761```
762 
763Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
764 
765```python theme={null}
766"""Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
767 
768import logging
769from typing import Any
770 
771import httpx2
772 
773from mcp.server.auth.provider import AccessToken, TokenVerifier
774from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
775 
776logger = logging.getLogger(__name__)
777 
778 
779class IntrospectionTokenVerifier(TokenVerifier):
780 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
781 
782 def __init__(
783 self,
784 introspection_endpoint: str,
785 server_url: str,
786 client_id: str,
787 client_secret: str,
788 ):
789 self.introspection_endpoint = introspection_endpoint
790 self.server_url = server_url
791 self.client_id = client_id
792 self.client_secret = client_secret
793 self.resource_url = resource_url_from_server_url(server_url)
794 
795 async def verify_token(self, token: str) -> AccessToken | None:
796 """Verify token via introspection endpoint."""
797 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
798 return None
799 
800 timeout = httpx2.Timeout(10.0, connect=5.0)
801 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
802 
803 async with httpx2.AsyncClient(
804 timeout=timeout,
805 limits=limits,
806 verify=True,
807 ) as client:
808 try:
809 form_data = {
810 "token": token,
811 "client_id": self.client_id,
812 }
813 # Only send client_secret when one is configured
814 # Public clients authenticate with client_id alone.
815 if self.client_secret:
816 form_data["client_secret"] = self.client_secret
817 headers = {"Content-Type": "application/x-www-form-urlencoded"}
818 
819 response = await client.post(
820 self.introspection_endpoint,
821 data=form_data,
822 headers=headers,
823 )
824 
825 if response.status_code != 200:
826 return None
827 
828 data = response.json()
829 if not data.get("active", False):
830 return None
831 
832 if not self._validate_resource(data):
833 return None
834 
835 return AccessToken(
836 token=token,
837 client_id=data.get("client_id", "unknown"),
838 scopes=data.get("scope", "").split() if data.get("scope") else [],
839 expires_at=data.get("exp"),
840 # AccessToken.resource is `str | None`. Keycloak returns `aud`
841 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
842 # "account"]); passing that list straight in raises a pydantic
843 # ValidationError that the broad `except` below turns into a
844 # silent 401. We already confirmed this server's resource is a
845 # valid audience in `_validate_resource`, so record that.
846 resource=self.resource_url,
847 subject=data.get("sub"), # RFC 7662 subject (resource owner)
848 claims=data,
849 )
850 
851 except Exception:
852 logger.exception("Token introspection failed")
853 return None
854 
855 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
856 """Validate token was issued for this resource server.
857 
858 Rules:
859 - Reject if 'aud' missing.
860 - Accept if any audience entry matches the derived resource URL.
861 - Supports string or list forms per JWT spec.
862 """
863 if not self.server_url or not self.resource_url:
864 return False
865 
866 aud: list[str] | str | None = token_data.get("aud")
867 if isinstance(aud, list):
868 return any(self._is_valid_resource(a) for a in aud)
869 if isinstance(aud, str):
870 return self._is_valid_resource(aud)
871 return False
872 
873 def _is_valid_resource(self, resource: str) -> bool:
874 """Check if the given resource matches our server."""
875 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
876```
877 
878For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
879 
880**Python MCP Server**
881 
882In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
883 
884```toml theme={null}
885[project]
886name = "mcp-simple-auth"
887version = "0.1.0"
888description = "A simple MCP server demonstrating OAuth authentication"
889requires-python = ">=3.10"
890authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
891license = { text = "MIT" }
892dependencies = [
893 "httpx2>=2.5.0",
894 "mcp>=2.0.0rc1",
895 "pydantic>=2.0",
896]
897 
898[project.scripts]
899mcp-simple-auth-rs = "mcp_server.server:main"
900 
901[build-system]
902requires = ["hatchling"]
903build-backend = "hatchling.build"
904 
905[tool.hatch.build.targets.wheel]
906packages = ["mcp_server"]
907 
908[dependency-groups]
909dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
910```
911 
912Then run the commands below to start the server.
913 
914```bash theme={null}
915uv sync
916uv run mcp-simple-auth-rs
917```
918 
919**C#**
920 
921You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
922 
923To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
924 
925```csharp theme={null}
926using Microsoft.AspNetCore.Authentication.JwtBearer;
927using Microsoft.IdentityModel.Tokens;
928using ModelContextProtocol.AspNetCore.Authentication;
929using ProtectedMcpServer.Tools;
930using System.Security.Claims;
931 
932var builder = WebApplication.CreateBuilder(args);
933 
934var serverUrl = "http://localhost:3000/";
935var authorizationServerUrl = "http://localhost:8080/realms/master/";
936 
937builder.Services.AddAuthentication(options =>
938{
939 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
940 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
941})
942.AddJwtBearer(options =>
943{
944 options.Authority = authorizationServerUrl;
945 var normalizedServerAudience = serverUrl.TrimEnd('/');
946 options.TokenValidationParameters = new TokenValidationParameters
947 {
948 ValidIssuer = authorizationServerUrl,
949 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
950 AudienceValidator = (audiences, securityToken, validationParameters) =>
951 {
952 if (audiences == null) return false;
953 foreach (var aud in audiences)
954 {
955 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
956 {
957 return true;
958 }
713959 }
714 
715 @app.tool()
716 async def multiply_numbers(x: float, y: float) -> dict[str, Any]:
717 """
718 Multiply two numbers together.
719 This tool demonstrates basic arithmetic operations with OAuth authentication.
720 
721 Args:
722 x: The first number to multiply
723 y: The second number to multiply
724 """
725 result = x * y
726 return {
727 "operation": "multiplication",
728 "operand_x": x,
729 "operand_y": y,
730 "result": result,
731 "timestamp": datetime.datetime.now().isoformat(),
732 }
733 
734 return app
735 
736 
737 def main() -> int:
738 """
739 Run the MCP Resource Server.
740 
741 This server:
742 - Provides RFC 9728 Protected Resource Metadata
743 - Validates tokens via Authorization Server introspection
744 - Serves MCP tools requiring authentication
745 
746 Configuration is loaded from config.py and environment variables.
747 """
748 logging.basicConfig(level=logging.INFO)
749 
750 oauth_urls = create_oauth_urls()
751 
752 try:
753 mcp_server = create_server()
754 
755 logger.info("Starting MCP Server on %s:%s", config.HOST, config.PORT)
756 logger.info("Authorization Server: %s", oauth_urls["issuer"])
757 
758 mcp_server.run(
759 transport="streamable-http",
760 host=config.HOST,
761 port=config.PORT,
762 streamable_http_path="/",
763 )
764 return 0
765 
766 except Exception:
767 logger.exception("Server error")
768 return 1
769 
770 
771 if __name__ == "__main__":
772 exit(main())
773 ```
774 
775 Lastly, the token verification logic is delegated entirely to `token_verifier.py`, ensuring that we can use the Keycloak introspection endpoint to verify the validity of any credential artifacts.
776 
777 ```python theme={null}
778 """Token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
779 
780 import logging
781 from typing import Any
782 
783 import httpx2
784 
785 from mcp.server.auth.provider import AccessToken, TokenVerifier
786 from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
787 
788 logger = logging.getLogger(__name__)
789 
790 
791 class IntrospectionTokenVerifier(TokenVerifier):
792 """Token verifier that uses OAuth 2.0 Token Introspection (RFC 7662)."""
793 
794 def __init__(
795 self,
796 introspection_endpoint: str,
797 server_url: str,
798 client_id: str,
799 client_secret: str,
800 ):
801 self.introspection_endpoint = introspection_endpoint
802 self.server_url = server_url
803 self.client_id = client_id
804 self.client_secret = client_secret
805 self.resource_url = resource_url_from_server_url(server_url)
806 
807 async def verify_token(self, token: str) -> AccessToken | None:
808 """Verify token via introspection endpoint."""
809 if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
810 return None
811 
812 timeout = httpx2.Timeout(10.0, connect=5.0)
813 limits = httpx2.Limits(max_connections=10, max_keepalive_connections=5)
814 
815 async with httpx2.AsyncClient(
816 timeout=timeout,
817 limits=limits,
818 verify=True,
819 ) as client:
820 try:
821 form_data = {
822 "token": token,
823 "client_id": self.client_id,
824 }
825 # Only send client_secret when one is configured
826 # Public clients authenticate with client_id alone.
827 if self.client_secret:
828 form_data["client_secret"] = self.client_secret
829 headers = {"Content-Type": "application/x-www-form-urlencoded"}
830 
831 response = await client.post(
832 self.introspection_endpoint,
833 data=form_data,
834 headers=headers,
835 )
836 
837 if response.status_code != 200:
838 return None
839 
840 data = response.json()
841 if not data.get("active", False):
842 return None
843 
844 if not self._validate_resource(data):
845 return None
846 
847 return AccessToken(
848 token=token,
849 client_id=data.get("client_id", "unknown"),
850 scopes=data.get("scope", "").split() if data.get("scope") else [],
851 expires_at=data.get("exp"),
852 # AccessToken.resource is `str | None`. Keycloak returns `aud`
853 # as a *list* here (e.g. ["test-client", "http://localhost:3000",
854 # "account"]); passing that list straight in raises a pydantic
855 # ValidationError that the broad `except` below turns into a
856 # silent 401. We already confirmed this server's resource is a
857 # valid audience in `_validate_resource`, so record that.
858 resource=self.resource_url,
859 subject=data.get("sub"), # RFC 7662 subject (resource owner)
860 claims=data,
861 )
862 
863 except Exception:
864 logger.exception("Token introspection failed")
865 return None
866 
867 def _validate_resource(self, token_data: dict[str, Any]) -> bool:
868 """Validate token was issued for this resource server.
869 
870 Rules:
871 - Reject if 'aud' missing.
872 - Accept if any audience entry matches the derived resource URL.
873 - Supports string or list forms per JWT spec.
874 """
875 if not self.server_url or not self.resource_url:
876 return False
877 
878 aud: list[str] | str | None = token_data.get("aud")
879 if isinstance(aud, list):
880 return any(self._is_valid_resource(a) for a in aud)
881 if isinstance(aud, str):
882 return self._is_valid_resource(aud)
883 return False
884 
885 def _is_valid_resource(self, resource: str) -> bool:
886 """Check if the given resource matches our server."""
887 return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)
888 ```
889 
890 For more details, see below or the [Python SDK documentation](https://github.com/modelcontextprotocol/python-sdk).
891 
892 **Python MCP Server**
893 
894 In the server's root have a `pyproject.toml` file and a `mcp_server` folder. Put all the Python files in the `mcp_server` folder, and fill the `pyproject.toml` file like:
895 
896 ```toml theme={null}
897 [project]
898 name = "mcp-simple-auth"
899 version = "0.1.0"
900 description = "A simple MCP server demonstrating OAuth authentication"
901 requires-python = ">=3.10"
902 authors = [{ name = "Model Context Protocol a Series of LF Projects, LLC." }]
903 license = { text = "MIT" }
904 dependencies = [
905 "httpx2>=2.5.0",
906 "mcp>=2.0.0rc1",
907 "pydantic>=2.0",
908 ]
909 
910 [project.scripts]
911 mcp-simple-auth-rs = "mcp_server.server:main"
912 
913 [build-system]
914 requires = ["hatchling"]
915 build-backend = "hatchling.build"
916 
917 [tool.hatch.build.targets.wheel]
918 packages = ["mcp_server"]
919 
920 [dependency-groups]
921 dev = ["pyright>=1.1.391", "pytest>=8.3.4", "ruff>=0.8.5"]
922 ```
923 
924 Then run the commands below to start the server.
925 
926 ```bash theme={null}
927 uv sync
928 uv run mcp-simple-auth-rs
929 ```
930 </Tab>
931 
932 <Tab title="C#">
933 You can see the complete C# project in the [sample repository](https://github.com/localden/min-cs-mcp-auth).
934 
935 To set up authorization in your MCP server using the MCP C# SDK, you can lean on the standard ASP.NET Core builder pattern. Instead of using the introspection endpoint provided by Keycloak, we will use built-in ASP.NET Core capabilities for token validation.
936 
937 ```csharp theme={null}
938 using Microsoft.AspNetCore.Authentication.JwtBearer;
939 using Microsoft.IdentityModel.Tokens;
940 using ModelContextProtocol.AspNetCore.Authentication;
941 using ProtectedMcpServer.Tools;
942 using System.Security.Claims;
943 
944 var builder = WebApplication.CreateBuilder(args);
945 
946 var serverUrl = "http://localhost:3000/";
947 var authorizationServerUrl = "http://localhost:8080/realms/master/";
948 
949 builder.Services.AddAuthentication(options =>
960 return false;
961 }
962 };
963 
964 options.RequireHttpsMetadata = false; // Set to true in production
965 
966 options.Events = new JwtBearerEvents
950967 {
951 options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
952 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
953 })
954 .AddJwtBearer(options =>
968 OnTokenValidated = context =>
969 {
970 var name = context.Principal?.Identity?.Name ?? "unknown";
971 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
972 Console.WriteLine($"Token validated for: {name} ({email})");
973 return Task.CompletedTask;
974 },
975 OnAuthenticationFailed = context =>
976 {
977 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
978 return Task.CompletedTask;
979 },
980 };
981})
982.AddMcp(options =>
983{
984 options.ResourceMetadata = new()
955985 {
956 options.Authority = authorizationServerUrl;
957 var normalizedServerAudience = serverUrl.TrimEnd('/');
958 options.TokenValidationParameters = new TokenValidationParameters
959 {
960 ValidIssuer = authorizationServerUrl,
961 ValidAudiences = new[] { normalizedServerAudience, serverUrl },
962 AudienceValidator = (audiences, securityToken, validationParameters) =>
963 {
964 if (audiences == null) return false;
965 foreach (var aud in audiences)
966 {
967 if (string.Equals(aud.TrimEnd('/'), normalizedServerAudience, StringComparison.OrdinalIgnoreCase))
968 {
969 return true;
970 }
971 }
972 return false;
973 }
974 };
975 
976 options.RequireHttpsMetadata = false; // Set to true in production
977 
978 options.Events = new JwtBearerEvents
979 {
980 OnTokenValidated = context =>
981 {
982 var name = context.Principal?.Identity?.Name ?? "unknown";
983 var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
984 Console.WriteLine($"Token validated for: {name} ({email})");
985 return Task.CompletedTask;
986 },
987 OnAuthenticationFailed = context =>
988 {
989 Console.WriteLine($"Authentication failed: {context.Exception.Message}");
990 return Task.CompletedTask;
991 },
992 };
993 })
994 .AddMcp(options =>
995 {
996 options.ResourceMetadata = new()
997 {
998 Resource = new Uri(serverUrl),
999 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
1000 AuthorizationServers = { new Uri(authorizationServerUrl) },
1001 ScopesSupported = ["mcp:tools"]
1002 };
1003 });
1004 
1005 builder.Services.AddAuthorization();
1006 
1007 builder.Services.AddHttpContextAccessor();
1008 builder.Services.AddMcpServer()
1009 .WithTools<MathTools>()
1010 .WithHttpTransport();
1011 
1012 var app = builder.Build();
1013 
1014 app.UseAuthentication();
1015 app.UseAuthorization();
1016 
1017 app.MapMcp().RequireAuthorization();
1018 
1019 Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1020 Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1021 Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1022 Console.WriteLine("Exposed Math tools: Add, Multiply");
1023 Console.WriteLine("Press Ctrl+C to stop the server");
1024 
1025 app.Run(serverUrl);
1026 ```
1027 
1028 For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
1029 </Tab>
1030</Tabs>
986 Resource = new Uri(serverUrl),
987 ResourceDocumentation = new Uri("https://docs.example.com/api/math"),
988 AuthorizationServers = { new Uri(authorizationServerUrl) },
989 ScopesSupported = ["mcp:tools"]
990 };
991});
992 
993builder.Services.AddAuthorization();
994 
995builder.Services.AddHttpContextAccessor();
996builder.Services.AddMcpServer()
997 .WithTools<MathTools>()
998 .WithHttpTransport();
999 
1000var app = builder.Build();
1001 
1002app.UseAuthentication();
1003app.UseAuthorization();
1004 
1005app.MapMcp().RequireAuthorization();
1006 
1007Console.WriteLine($"Starting MCP server with authorization at {serverUrl}");
1008Console.WriteLine($"Using Keycloak server at {authorizationServerUrl}");
1009Console.WriteLine($"Protected Resource Metadata URL: {serverUrl}.well-known/oauth-protected-resource");
1010Console.WriteLine("Exposed Math tools: Add, Multiply");
1011Console.WriteLine("Press Ctrl+C to stop the server");
1012 
1013app.Run(serverUrl);
1014```
1015 
1016For more details, see the [C# SDK documentation](https://github.com/modelcontextprotocol/csharp-sdk).
10311017 
10321018## Testing the MCP Server
10331019 
from line 1030
10441030 
10451031On connection, you will be taken to the browser, where you will be prompted to consent to Visual Studio Code having access to the `mcp:tools` scope.
10461032 
1047<Frame>
1048 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-vscode.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=d5183fb7c257993aed1b2246f0bbbb27" alt="Keycloak consent form for VS Code." width="1915" height="1536" data-path="images/tutorial-authorization/keycloak-vscode.png" />
1049</Frame>
1033![Keycloak consent form for VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/keycloak-vscode.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=d5183fb7c257993aed1b2246f0bbbb27)
10501034 
10511035After consenting, you will see the tools listed right above the server entry in `mcp.json`.
10521036 
1053<Frame>
1054 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=f7c34d1bf115fe6934e01b4a5a91168b" alt="Tools listed in VS Code." width="496" height="160" data-path="images/tutorial-authorization/tools-vs-code.png" />
1055</Frame>
1037![Tools listed in VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=f7c34d1bf115fe6934e01b4a5a91168b)
10561038 
10571039You will be able to invoke individual tools with the help of the `#` sign in the chat view.
10581040 
1059<Frame>
1060 <img src="https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code-invoke.png?fit=max&auto=format&n=sAd4SGUO-cEUqgzn&q=85&s=76cbef68e48821a3c5467bd20c7e89fe" alt="Invoking MCP tools in VS Code." width="1276" height="396" data-path="images/tutorial-authorization/tools-vs-code-invoke.png" />
1061</Frame>
1041![Invoking MCP tools in VS Code.](https://mintcdn.com/mcp/sAd4SGUO-cEUqgzn/images/tutorial-authorization/tools-vs-code-invoke.png?fit=max\&auto=format\&n=sAd4SGUO-cEUqgzn\&q=85\&s=76cbef68e48821a3c5467bd20c7e89fe)
10621042 
10631043## Common Pitfalls and How to Avoid Them
10641044 
10651045 

docs/draft/tutorials/security/local-server-security Changed · +45 / -47 lines

from line 15
1515network connections to anywhere. None of that is governed by the protocol.
1616It is governed by how you run the process.
1717 
18<Note>
19 **Scope**: this guide is about servers running on a machine you (or your
20 organization's users) control — a laptop or workstation, typically launched
21 over stdio. Deploying and hardening remote or hosted servers is a different
22 topic with different controls.
23</Note>
18> [!NOTE]
19>
20> **Scope**: this guide is about servers running on a machine you (or your
21> organization's users) control — a laptop or workstation, typically launched
22> over stdio. Deploying and hardening remote or hosted servers is a different
23> topic with different controls.
2424 
25<Tip>
26 **If you do one thing after reading this**: open your client's MCP
27 configuration, remove the servers you no longer use, and make sure you know
28 where each remaining one comes from and that its version is pinned. Everything
29 else in this guide builds on knowing what you are actually running.
30</Tip>
25> [!TIP]
26>
27> **If you do one thing after reading this**: open your client's MCP
28> configuration, remove the servers you no longer use, and make sure you know
29> where each remaining one comes from and that its version is pinned. Everything
30> else in this guide builds on knowing what you are actually running.
3131 
3232## What a Local Server Can Touch
3333 
from line 285
285285 directory to do a narrow job is overreaching. Grant a narrower directory,
286286 or pick a different server.
287287 
288<Warning>
289 Configuration-level allowlists are **cooperative boundaries**: a well-behaved
290 server respects them, but nothing in the protocol stops a malicious or
291 compromised one from reading whatever the process can reach. Treat them as
292 guardrails for honest servers. Enforcement comes from the isolation layer
293 above — a sandbox or container where the allowed directory is the only
294 filesystem that exists.
295</Warning>
288> [!WARNING]
289>
290> Configuration-level allowlists are **cooperative boundaries**: a well-behaved
291> server respects them, but nothing in the protocol stops a malicious or
292> compromised one from reading whatever the process can reach. Treat them as
293> guardrails for honest servers. Enforcement comes from the isolation layer
294> above — a sandbox or container where the allowed directory is the only
295> filesystem that exists.
296296 
297297**Going further**: combine allowlists with isolation, so the only filesystem
298298the server can even see is the mount you chose.
from line 316
316316 restricts them to their expected destinations and logs the attempts that
317317 fall outside it.
318318 
319<Note>
320 Egress is not the only network concern. Local MCP plumbing that listens on a
321 port — bridges to remote servers, development tools, locally hosted HTTP
322 servers — is reachable by other local processes and, through your browser, by
323 websites. Binding to 127.0.0.1 is not an authentication boundary: such
324 components need real authentication and origin validation, and they need
325 prompt updates when advisories land.
326</Note>
319> [!NOTE]
320>
321> Egress is not the only network concern. Local MCP plumbing that listens on a
322> port — bridges to remote servers, development tools, locally hosted HTTP
323> servers — is reachable by other local processes and, through your browser, by
324> websites. Binding to 127.0.0.1 is not an authentication boundary: such
325> components need real authentication and origin validation, and they need
326> prompt updates when advisories land.
327327 
328328**Going further**: give each server its own egress allowlist via a proxy,
329329and alert on destinations outside it — unexpected egress is often the first
from line 419
419419 
420420## Next Steps
421421 
422<CardGroup cols={2}>
423 <Card title="Security Best Practices" icon="shield" href="/docs/draft/tutorials/security/security_best_practices">
424 The protocol threat model: confused deputy, token passthrough, and session
425 hijacking
426 </Card>
422> **[Security Best Practices](/docs/draft/tutorials/security/security_best_practices)**
423>
424> The protocol threat model: confused deputy, token passthrough, and session
425> hijacking
427426 
428 <Card title="Authorization" icon="key" href="/docs/draft/tutorials/security/authorization">
429 How OAuth-based authorization works for remote MCP servers
430 </Card>
427> **[Authorization](/docs/draft/tutorials/security/authorization)**
428>
429> How OAuth-based authorization works for remote MCP servers
431430 
432 <Card title="SEP-1024" icon="file-shield" href="/seps/1024-mcp-client-security-requirements-for-local-server-">
433 Client security requirements for local server installation consent
434 </Card>
431> **[SEP-1024](/seps/1024-mcp-client-security-requirements-for-local-server-)**
432>
433> Client security requirements for local server installation consent
435434 
436 <Card title="Connect to local servers" icon="plug" href="/docs/draft/develop/connect-local-servers">
437 Set up local servers in your client — the workflow this guide secures
438 </Card>
439</CardGroup>
435> **[Connect to local servers](/docs/draft/develop/connect-local-servers)**
436>
437> Set up local servers in your client — the workflow this guide secures
440438 

docs/draft/tutorials/security/security_best_practices Changed · +26 / -26 lines

from line 442
442442* Link-local: `169.254.0.0/16` (including cloud metadata endpoints)
443443* Private IPv6 ranges: `fc00::/7`, `fe80::/10`
444444 
445<Note>
446 Avoid implementing IP validation manually. Attackers exploit encoding tricks
447 (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
448</Note>
445> [!NOTE]
446>
447> Avoid implementing IP validation manually. Attackers exploit encoding tricks
448> (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.
449449 
450450**Validate Redirect Targets**
451451 
from line 946
946946process, minimizing user friction while following the principle of
947947least privilege.
948948 
949<Note>
950 Scope accumulation across operations is a client-side responsibility. Clients
951 **SHOULD** compute the union of previously requested scopes and newly
952 challenged scopes when initiating re-authorization, as described in [Step-Up
953 Authorization
954 Flow](/specification/draft/basic/authorization#step-up-authorization-flow).
955 This allows servers to remain stateless with respect to client scope sets
956 while ensuring clients do not lose previously granted permissions.
957</Note>
949> [!NOTE]
950>
951> Scope accumulation across operations is a client-side responsibility. Clients
952> **SHOULD** compute the union of previously requested scopes and newly
953> challenged scopes when initiating re-authorization, as described in [Step-Up
954> Authorization
955> Flow](/specification/draft/basic/authorization#step-up-authorization-flow).
956> This allows servers to remain stateless with respect to client scope sets
957> while ensuring clients do not lose previously granted permissions.
958958 
959<Note>
960 **Hierarchical scopes**: Some authorization servers define scope hierarchies
961 where a broader scope implies narrower ones (for example, an `admin` scope
962 that subsumes `read`). When accumulating scopes, the client's union may
963 contain semantically redundant entries. For example, a token previously
964 granted a broad scope may be challenged with a narrower one it already
965 implies. Clients need not deduplicate hierarchically; authorization servers
966 typically normalize such redundancy during token issuance. Servers, for their
967 part, must account for hierarchy when deciding whether a token is sufficient
968 for an operation, but this does not affect the scopes they emit in a
969 challenge.
970</Note>
959> [!NOTE]
960>
961> **Hierarchical scopes**: Some authorization servers define scope hierarchies
962> where a broader scope implies narrower ones (for example, an `admin` scope
963> that subsumes `read`). When accumulating scopes, the client's union may
964> contain semantically redundant entries. For example, a token previously
965> granted a broad scope may be challenged with a narrower one it already
966> implies. Clients need not deduplicate hierarchically; authorization servers
967> typically normalize such redundancy during token issuance. Servers, for their
968> part, must account for hierarchy when deciding whether a token is sufficient
969> for an operation, but this does not affect the scopes they emit in a
970> challenge.
971971 
972972#### Common Mistakes
973973 

extensions/apps/build Changed · +218 / -248 lines

from line 27
2727The `create-mcp-app` skill includes architecture guidance, best practices, and
2828working examples that the agent uses to generate your project.
2929 
30<Steps>
31 <Step title="Install the skill">
32 If you are using Claude Code, you can install the skill directly with:
30**1. Install the skill**
3331 
34 ```
35 /plugin marketplace add modelcontextprotocol/ext-apps
36 /plugin install mcp-apps@modelcontextprotocol-ext-apps
37 ```
32If you are using Claude Code, you can install the skill directly with:
3833 
39 You can also use the [Vercel Skills CLI](https://skills.sh/) to install skills across different AI coding agents:
34```
35/plugin marketplace add modelcontextprotocol/ext-apps
36/plugin install mcp-apps@modelcontextprotocol-ext-apps
37```
4038 
41 ```bash theme={null}
42 npx skills add modelcontextprotocol/ext-apps
43 ```
39You can also use the [Vercel Skills CLI](https://skills.sh/) to install skills across different AI coding agents:
4440 
45 Alternatively, you can install the skill manually by cloning the ext-apps repository:
41```bash theme={null}
42npx skills add modelcontextprotocol/ext-apps
43```
4644 
47 ```bash theme={null}
48 git clone https://github.com/modelcontextprotocol/ext-apps.git
49 ```
45Alternatively, you can install the skill manually by cloning the ext-apps repository:
5046 
51 And then copying the skill to the appropriate location for your agent:
47```bash theme={null}
48git clone https://github.com/modelcontextprotocol/ext-apps.git
49```
5250 
53 | Agent | Skills directory (macOS/Linux) | Skills directory (Windows) |
54 | - | - | - |
55 | [Claude Code](https://docs.anthropic.com/en/docs/claude-code/skills) | `~/.claude/skills/` | `%USERPROFILE%\.claude\skills\` |
56 | [VS Code](https://code.visualstudio.com/docs/copilot/customization/agent-skills) and [GitHub Copilot](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills) | `~/.copilot/skills/` | `%USERPROFILE%\.copilot\skills\` |
57 | [Gemini CLI](https://geminicli.com/docs/cli/skills/) | `~/.gemini/skills/` | `%USERPROFILE%\.gemini\skills\` |
58 | [Cline](https://cline.bot/blog/cline-3-48-0-skills-and-websearch-make-cline-smarter) | `~/.cline/skills/` | `%USERPROFILE%\.cline\skills\` |
59 | [Goose](https://goose-docs.ai/docs/guides/context-engineering/using-skills/) | `~/.config/goose/skills/` | `%USERPROFILE%\.config\goose\skills\` |
60 | [Codex](https://developers.openai.com/codex/skills/) | `~/.codex/skills/` | `%USERPROFILE%\.codex\skills\` |
61 | [Cursor](https://cursor.com/docs/context/skills) | `~/.cursor/skills/` | `%USERPROFILE%\.cursor\skills\` |
51And then copying the skill to the appropriate location for your agent:
6252 
63 <Note>
64 This list is not comprehensive. Other agents may support skills in different locations; check your agent's documentation.
65 </Note>
53| Agent | Skills directory (macOS/Linux) | Skills directory (Windows) |
54| - | - | - |
55| [Claude Code](https://docs.anthropic.com/en/docs/claude-code/skills) | `~/.claude/skills/` | `%USERPROFILE%\.claude\skills\` |
56| [VS Code](https://code.visualstudio.com/docs/copilot/customization/agent-skills) and [GitHub Copilot](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills) | `~/.copilot/skills/` | `%USERPROFILE%\.copilot\skills\` |
57| [Gemini CLI](https://geminicli.com/docs/cli/skills/) | `~/.gemini/skills/` | `%USERPROFILE%\.gemini\skills\` |
58| [Cline](https://cline.bot/blog/cline-3-48-0-skills-and-websearch-make-cline-smarter) | `~/.cline/skills/` | `%USERPROFILE%\.cline\skills\` |
59| [Goose](https://goose-docs.ai/docs/guides/context-engineering/using-skills/) | `~/.config/goose/skills/` | `%USERPROFILE%\.config\goose\skills\` |
60| [Codex](https://developers.openai.com/codex/skills/) | `~/.codex/skills/` | `%USERPROFILE%\.codex\skills\` |
61| [Cursor](https://cursor.com/docs/context/skills) | `~/.cursor/skills/` | `%USERPROFILE%\.cursor\skills\` |
6662 
67 For example, with Claude Code you can install the skill globally (available in all projects):
63> [!NOTE]
64>
65> This list is not comprehensive. Other agents may support skills in different locations; check your agent's documentation.
6866 
69 <CodeGroup>
70 ```bash macOS/Linux theme={null}
71 cp -r ext-apps/plugins/mcp-apps/skills/create-mcp-app ~/.claude/skills/create-mcp-app
72 ```
67For example, with Claude Code you can install the skill globally (available in all projects):
7368 
74 ```powershell Windows theme={null}
75 Copy-Item -Recurse ext-apps\plugins\mcp-apps\skills\create-mcp-app $env:USERPROFILE\.claude\skills\create-mcp-app
76 ```
77 </CodeGroup>
69```bash macOS/Linux theme={null}
70cp -r ext-apps/plugins/mcp-apps/skills/create-mcp-app ~/.claude/skills/create-mcp-app
71```
7872 
79 Or install it for a single project only by copying to `.claude/skills/` in your project directory:
73```powershell Windows theme={null}
74Copy-Item -Recurse ext-apps\plugins\mcp-apps\skills\create-mcp-app $env:USERPROFILE\.claude\skills\create-mcp-app
75```
8076 
81 <CodeGroup>
82 ```bash macOS/Linux theme={null}
83 mkdir -p .claude/skills && cp -r ext-apps/plugins/mcp-apps/skills/create-mcp-app .claude/skills/create-mcp-app
84 ```
77Or install it for a single project only by copying to `.claude/skills/` in your project directory:
8578 
86 ```powershell Windows theme={null}
87 New-Item -ItemType Directory -Force -Path .claude\skills | Out-Null; Copy-Item -Recurse ext-apps\plugins\mcp-apps\skills\create-mcp-app .claude\skills\create-mcp-app
88 ```
89 </CodeGroup>
79```bash macOS/Linux theme={null}
80mkdir -p .claude/skills && cp -r ext-apps/plugins/mcp-apps/skills/create-mcp-app .claude/skills/create-mcp-app
81```
9082 
91 To verify the skill is installed, ask your agent "What skills do you have access to?" — you should see `create-mcp-app` as one of the available skills.
92 </Step>
83```powershell Windows theme={null}
84New-Item -ItemType Directory -Force -Path .claude\skills | Out-Null; Copy-Item -Recurse ext-apps\plugins\mcp-apps\skills\create-mcp-app .claude\skills\create-mcp-app
85```
9386 
94 <Step title="Create your app">
95 Ask your AI coding agent to build it:
87To verify the skill is installed, ask your agent "What skills do you have access to?" — you should see `create-mcp-app` as one of the available skills.
9688 
97 ```
98 Create an MCP App that displays a color picker
99 ```
89**2. Create your app**
10090 
101 The agent will recognize the `create-mcp-app` skill is relevant, load its instructions, then scaffold a complete project with server, UI, and configuration files.
91Ask your AI coding agent to build it:
10292 
103 <Frame caption="Creating a new MCP App with Claude Code">
104 <img src="https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/create-mcp-app-skill.gif?s=6c3a3b8a7590b5e97b5c3d8480a9ab12" alt="Creating a new MCP App with Claude Code" width="800" height="563" data-path="images/quickstart-apps/create-mcp-app-skill.gif" />
105 </Frame>
106 </Step>
93```
94Create an MCP App that displays a color picker
95```
10796 
108 <Step title="Run your app">
109 <CodeGroup>
110 ```bash macOS/Linux theme={null}
111 npm install && npm run build && npm run serve
112 ```
97The agent will recognize the `create-mcp-app` skill is relevant, load its instructions, then scaffold a complete project with server, UI, and configuration files.
11398 
114 ```powershell Windows theme={null}
115 npm install; npm run build; npm run serve
116 ```
117 </CodeGroup>
99![Creating a new MCP App with Claude Code](https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/create-mcp-app-skill.gif?s=6c3a3b8a7590b5e97b5c3d8480a9ab12)
118100 
119 <Tip>
120 You might need to make sure that you are first in the **app folder** before running the commands above.
121 </Tip>
122 </Step>
101**3. Run your app**
123102 
124 <Step title="Test your app">
125 Follow the instructions in [Testing your app](#testing-your-app) below. For the color picker example, start a new chat and ask Claude to provide you a color picker.
103```bash macOS/Linux theme={null}
104npm install && npm run build && npm run serve
105```
126106 
127 <Frame caption="Testing the color picker in Claude">
128 <img src="https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/test-color-picker.gif?s=09413b99bc31d7edc7f9aa22df4faa6a" alt="Testing the color picker in Claude" width="800" height="544" data-path="images/quickstart-apps/test-color-picker.gif" />
129 </Frame>
130 </Step>
131</Steps>
107```powershell Windows theme={null}
108npm install; npm run build; npm run serve
109```
132110 
111> [!TIP]
112>
113> You might need to make sure that you are first in the **app folder** before running the commands above.
114 
115**4. Test your app**
116 
117Follow the instructions in [Testing your app](#testing-your-app) below. For the color picker example, start a new chat and ask Claude to provide you a color picker.
118 
119![Testing the color picker in Claude](https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/test-color-picker.gif?s=09413b99bc31d7edc7f9aa22df4faa6a)
120 
133121### Manual setup
134122 
135123If you're not using an AI coding agent, or prefer to understand the setup
136124process, follow these steps.
137125 
138<Steps>
139 <Step title="Create the project structure">
140 A typical MCP App project separates the server code from the UI code:
126**1. Create the project structure**
141127 
142 <Tree>
143 <Tree.Folder name="my-mcp-app" defaultOpen>
144 <Tree.File name="package.json" />
128A typical MCP App project separates the server code from the UI code:
145129 
146 <Tree.File name="tsconfig.json" />
130**my-mcp-app**
147131 
148 <Tree.File name="vite.config.ts" />
132**package.json**
149133 
150 <Tree.File name="server.ts" comment="MCP server with tool + resource" />
134**tsconfig.json**
151135 
152 <Tree.File name="mcp-app.html" comment="UI entry point" />
136**vite.config.ts**
153137 
154 <Tree.Folder name="src" defaultOpen>
155 <Tree.File name="mcp-app.ts" comment="UI logic" />
156 </Tree.Folder>
157 </Tree.Folder>
158 </Tree>
138**server.ts**
159139 
160 The server registers the tool and serves the UI resource. The UI resource will eventually be rendered in a secure iframe with deny-by-default CSP configuration. If your app has CSS and JS assets, you will need to [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors), or you can bundle your assets into the HTML with a tool like `vite-plugin-singlefile`, which is what we will do in this tutorial.
161 </Step>
140**mcp-app.html**
162141 
163 <Step title="Install dependencies">
164 ```bash theme={null}
165 npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
166 npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx
167 ```
142**src**
168143 
169 The `ext-apps` package provides helpers for both the server side (registering tools and resources) and the client side (the `App` class for UI-to-host communication). Vite with the `vite-plugin-singlefile` plugin is used here to bundle your UI and assets into a single HTML file for convenience, but this is optional — you can use any bundler or serve unbundled files if you [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors).
170 </Step>
144**mcp-app.ts**
171145 
172 <Step title="Configure the project">
173 <Tabs>
174 <Tab title="package.json">
175 The `"type": "module"` setting enables ES module syntax. The `build` script uses the `INPUT` environment variable to tell Vite which HTML file to bundle. The `serve` script runs your server using `tsx` for TypeScript execution.
146The server registers the tool and serves the UI resource. The UI resource will eventually be rendered in a secure iframe with deny-by-default CSP configuration. If your app has CSS and JS assets, you will need to [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors), or you can bundle your assets into the HTML with a tool like `vite-plugin-singlefile`, which is what we will do in this tutorial.
176147 
177 ```json theme={null}
178 {
179 "type": "module",
180 "scripts": {
181 "build": "INPUT=mcp-app.html vite build",
182 "serve": "npx tsx server.ts"
183 }
184 }
185 ```
186 </Tab>
148**2. Install dependencies**
187149 
188 <Tab title="tsconfig.json">
189 The TypeScript configuration targets modern JavaScript (`ES2022`) and uses ESNext modules with bundler resolution, which works well with Vite. The `include` array covers both the server code in the root and UI code in `src/`.
150```bash theme={null}
151npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
152npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx
153```
190154 
191 ```json theme={null}
192 {
193 "compilerOptions": {
194 "target": "ES2022",
195 "module": "ESNext",
196 "moduleResolution": "bundler",
197 "strict": true,
198 "esModuleInterop": true,
199 "skipLibCheck": true,
200 "outDir": "dist"
201 },
202 "include": ["*.ts", "src/**/*.ts"]
203 }
204 ```
205 </Tab>
155The `ext-apps` package provides helpers for both the server side (registering tools and resources) and the client side (the `App` class for UI-to-host communication). Vite with the `vite-plugin-singlefile` plugin is used here to bundle your UI and assets into a single HTML file for convenience, but this is optional — you can use any bundler or serve unbundled files if you [configure CSP](https://apps.extensions.modelcontextprotocol.io/api/documents/Patterns.html#configuring-csp-and-cors).
206156 
207 <Tab title="vite.config.ts">
208 ```typescript theme={null}
209 import { defineConfig } from "vite";
210 import { viteSingleFile } from "vite-plugin-singlefile";
157**3. Configure the project**
211158 
212 export default defineConfig({
213 plugins: [viteSingleFile()],
214 build: {
215 outDir: "dist",
216 rollupOptions: {
217 input: process.env.INPUT,
218 },
219 },
220 });
221 ```
222 </Tab>
223 </Tabs>
224 </Step>
159**package.json**
225160 
226 <Step title="Build the project">
227 With the project structure and configuration in place, continue to [Building an MCP App](#building-an-mcp-app) below to implement the server and UI.
228 </Step>
229</Steps>
161The `"type": "module"` setting enables ES module syntax. The `build` script uses the `INPUT` environment variable to tell Vite which HTML file to bundle. The `serve` script runs your server using `tsx` for TypeScript execution.
230162 
163```json theme={null}
164{
165 "type": "module",
166 "scripts": {
167 "build": "INPUT=mcp-app.html vite build",
168 "serve": "npx tsx server.ts"
169 }
170}
171```
172 
173**tsconfig.json**
174 
175The TypeScript configuration targets modern JavaScript (`ES2022`) and uses ESNext modules with bundler resolution, which works well with Vite. The `include` array covers both the server code in the root and UI code in `src/`.
176 
177```json theme={null}
178{
179 "compilerOptions": {
180 "target": "ES2022",
181 "module": "ESNext",
182 "moduleResolution": "bundler",
183 "strict": true,
184 "esModuleInterop": true,
185 "skipLibCheck": true,
186 "outDir": "dist"
187 },
188 "include": ["*.ts", "src/**/*.ts"]
189}
190```
191 
192**vite.config.ts**
193 
194```typescript theme={null}
195import { defineConfig } from "vite";
196import { viteSingleFile } from "vite-plugin-singlefile";
197 
198export default defineConfig({
199 plugins: [viteSingleFile()],
200 build: {
201 outDir: "dist",
202 rollupOptions: {
203 input: process.env.INPUT,
204 },
205 },
206});
207```
208 
209**4. Build the project**
210 
211With the project structure and configuration in place, continue to [Building an MCP App](#building-an-mcp-app) below to implement the server and UI.
212 
231213## Building an MCP App
232214 
233215Let's build a simple app that displays the current server time. This example
from line 390
408390 
409391To test your MCP App, build the UI and start your local server:
410392 
411<CodeGroup>
412 ```bash macOS/Linux theme={null}
413 npm run build && npm run serve
414 ```
393```bash macOS/Linux theme={null}
394npm run build && npm run serve
395```
415396 
416 ```powershell Windows theme={null}
417 npm run build; npm run serve
418 ```
419</CodeGroup>
397```powershell Windows theme={null}
398npm run build; npm run serve
399```
420400 
421401In the default configuration, your server will be available at
422402`http://localhost:3001/mcp`. However, to see your app render, you need an MCP
from line 420
440420in Claude - click on your profile, go to **Settings**, **Connectors**, and
441421finally **Add custom connector**.
442422 
443<Note>
444 Custom connectors are available on paid Claude plans (Pro, Max, or Team).
445</Note>
423> [!NOTE]
424>
425> Custom connectors are available on paid Claude plans (Pro, Max, or Team).
446426 
447<Frame caption="Adding a custom connector in Claude">
448 <img src="https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/add-custom-connector.gif?s=c4ec0750413ff7575c7f9492e2713212" alt="Adding a custom connector in Claude" width="800" height="543" data-path="images/quickstart-apps/add-custom-connector.gif" />
449</Frame>
427![Adding a custom connector in Claude](https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/add-custom-connector.gif?s=c4ec0750413ff7575c7f9492e2713212)
450428 
451429### Testing with the basic-host
452430 
from line 431
453431The `ext-apps` repository includes a test host for development. Clone the repo and
454432install dependencies:
455433 
456<CodeGroup>
457 ```bash macOS/Linux theme={null}
458 git clone https://github.com/modelcontextprotocol/ext-apps.git
459 cd ext-apps/examples/basic-host
460 npm install
461 ```
434```bash macOS/Linux theme={null}
435git clone https://github.com/modelcontextprotocol/ext-apps.git
436cd ext-apps/examples/basic-host
437npm install
438```
462439 
463 ```powershell Windows theme={null}
464 git clone https://github.com/modelcontextprotocol/ext-apps.git
465 cd ext-apps\examples\basic-host
466 npm install
467 ```
468</CodeGroup>
440```powershell Windows theme={null}
441git clone https://github.com/modelcontextprotocol/ext-apps.git
442cd ext-apps\examples\basic-host
443npm install
444```
469445 
470446Running `npm start` from `ext-apps/examples/basic-host/` will start the basic-host
471447test interface. To connect it to a specific server (e.g., one you're developing),
472448pass the `SERVERS` environment variable inline:
473449 
474<CodeGroup>
475 ```bash macOS/Linux theme={null}
476 SERVERS='["http://localhost:3001/mcp"]' npm start
477 ```
450```bash macOS/Linux theme={null}
451SERVERS='["http://localhost:3001/mcp"]' npm start
452```
478453 
479 ```powershell Windows theme={null}
480 $env:SERVERS='["http://localhost:3001/mcp"]'; npm start
481 ```
482</CodeGroup>
454```powershell Windows theme={null}
455$env:SERVERS='["http://localhost:3001/mcp"]'; npm start
456```
483457 
484458Navigate to `http://localhost:8080`. You'll see a simple interface where you can
485459select a tool and call it. When you call your tool, the host fetches the UI
from line 460
486460resource and renders it in a sandboxed iframe. You can then interact with your
487461app and verify that tool calls work correctly.
488462 
489<Frame caption="Testing the QR code MCP App with the basic host">
490 <img src="https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/qr-code-server.gif?s=48a3b47239b8394017c0949162d63de9" alt="Example of the QR code MCP App running with the basic host" width="800" height="596" data-path="images/quickstart-apps/qr-code-server.gif" />
491</Frame>
463![Example of the QR code MCP App running with the basic host](https://mintcdn.com/mcp/GU_E-622SLWFdCrP/images/quickstart-apps/qr-code-server.gif?s=48a3b47239b8394017c0949162d63de9)
492464 
493465## Learn more
494466 
495<CardGroup cols={2}>
496 <Card title="API Documentation" icon="book" href="https://apps.extensions.modelcontextprotocol.io/api/">
497 Full SDK reference and API details
498 </Card>
467> **[API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)**
468>
469> Full SDK reference and API details
499470 
500 <Card title="GitHub Repository" icon="github" href="https://github.com/modelcontextprotocol/ext-apps">
501 Source code, examples, and issue tracker
502 </Card>
471> **[GitHub Repository](https://github.com/modelcontextprotocol/ext-apps)**
472>
473> Source code, examples, and issue tracker
503474 
504 <Card title="Specification" icon="file-lines" href="https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx">
505 Technical specification for implementers
506 </Card>
507</CardGroup>
475> **[Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)**
476>
477> Technical specification for implementers
508478 
509479## Feedback
510480 

extensions/apps/overview Changed · +7 / -9 lines

from line 2
22 
33> Interactive UI applications that render inside MCP hosts like Claude Desktop
44 
5<Tip>
6 For comprehensive API documentation, advanced patterns, and the full specification, visit the [official MCP Apps documentation](https://apps.extensions.modelcontextprotocol.io).
7</Tip>
5> [!TIP]
6>
7> For comprehensive API documentation, advanced patterns, and the full specification, visit the [official MCP Apps documentation](https://apps.extensions.modelcontextprotocol.io).
88 
99Text responses can only go so far. Sometimes users need to interact with data, not
1010just read about it. MCP Apps let servers return interactive HTML interfaces (data
1111visualizations, forms, dashboards) that render directly in the chat.
1212 
13<Frame caption="Excalidraw MCP App, running in Claude">
14 <img src="https://mintcdn.com/mcp/jbEkS7m9mV8aY_kB/images/apps/excalidraw.gif?s=c783b088f3c687e4dd4cf05c0d20f325" alt="An Excalidraw MCP App drawing a sequence diagram inside a Claude conversation" width="620" height="510" data-path="images/apps/excalidraw.gif" />
15</Frame>
13![An Excalidraw MCP App drawing a sequence diagram inside a Claude conversation](https://mintcdn.com/mcp/jbEkS7m9mV8aY_kB/images/apps/excalidraw.gif?s=c783b088f3c687e4dd4cf05c0d20f325)
1614 
1715## Why not just build a web app?
1816 
from line 163
165163 
166164## Client support
167165 
168<Note>
169 MCP Apps is an extension to the [core MCP specification](/specification/latest). Host support varies by client.
170</Note>
166> [!NOTE]
167>
168> MCP Apps is an extension to the [core MCP specification](/specification/latest). Host support varies by client.
171169 
172170MCP Apps are currently supported by [Claude](https://claude.ai),
173171[Claude Desktop](https://claude.ai/download),

extensions/auth/enterprise-managed-authorization Changed · +19 / -21 lines

from line 4
44 
55The Enterprise-Managed Authorization extension (`io.modelcontextprotocol/enterprise-managed-authorization`) enables organizations to control MCP server access centrally through their existing identity provider (IdP). Instead of each employee authorizing each MCP server individually, the organization's IT or security team manages access policies in one place.
66 
7<Card title="Specification" icon="file-lines" href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx">
8 Full technical specification for the Enterprise-Managed Authorization
9 extension.
10</Card>
7> **[Specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx)**
8>
9> Full technical specification for the Enterprise-Managed Authorization
10> extension.
1111 
1212## What it is
1313 
from line 131
131131 
132132## Client support
133133 
134<Note>
135 Support for this extension varies by client. Extensions are opt-in and never active by default.
136</Note>
134> [!NOTE]
135>
136> Support for this extension varies by client. Extensions are opt-in and never active by default.
137137 
138138Check the [client matrix](/extensions/client-matrix) for current implementation status across MCP clients. Enterprise-Managed Authorization typically requires client-level support from the organization's IT team in addition to the MCP client application.
139139 
140140## Related resources
141141 
142<CardGroup cols={2}>
143 <Card title="ext-auth repository" icon="github" href="https://github.com/modelcontextprotocol/ext-auth">
144 Source code and reference implementations
145 </Card>
142> **[ext-auth repository](https://github.com/modelcontextprotocol/ext-auth)**
143>
144> Source code and reference implementations
146145 
147 <Card title="Full specification" icon="file-lines" href="https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx">
148 Technical specification with normative requirements
149 </Card>
146> **[Full specification](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx)**
147>
148> Technical specification with normative requirements
150149 
151 <Card title="SEP-990" icon="file-lines" href="/seps/990-enable-enterprise-idp-policy-controls-during-mcp-o">
152 Original proposal: Enable Enterprise IdP Policy Controls
153 </Card>
150> **[SEP-990](/seps/990-enable-enterprise-idp-policy-controls-during-mcp-o)**
151>
152> Original proposal: Enable Enterprise IdP Policy Controls
154153 
155 <Card title="MCP Authorization" icon="lock" href="/specification/latest/basic/authorization">
156 Core MCP authorization specification
157 </Card>
158</CardGroup>
154> **[MCP Authorization](/specification/latest/basic/authorization)**
155>
156> Core MCP authorization specification
159157 

extensions/auth/oauth-client-credentials Changed · +309 / -323 lines

#### Using a client secret #### Using a JWT private key

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 

extensions/auth/overview Changed · +12 / -14 lines

from line 4
44 
55The [ext-auth repository](https://github.com/modelcontextprotocol/ext-auth) contains official MCP extensions that add authorization capabilities beyond the core MCP specification. These extensions address specific real-world scenarios where the standard OAuth 2.0 authorization code flow isn't the right fit.
66 
7<Card title="modelcontextprotocol/ext-auth" icon="github" href="https://github.com/modelcontextprotocol/ext-auth">
8 Source code, specifications, and reference implementations for MCP
9 authorization extensions.
10</Card>
7> **[modelcontextprotocol/ext-auth](https://github.com/modelcontextprotocol/ext-auth)**
8>
9> Source code, specifications, and reference implementations for MCP
10> authorization extensions.
1111 
1212## Why authorization extensions?
1313 
from line 22
2222 
2323## Available extensions
2424 
25<CardGroup cols={2}>
26 <Card title="OAuth Client Credentials" icon="robot" href="/extensions/auth/oauth-client-credentials">
27 Machine-to-machine authentication using the OAuth 2.0 client credentials
28 flow. No user interaction required.
29 </Card>
25> **[OAuth Client Credentials](/extensions/auth/oauth-client-credentials)**
26>
27> Machine-to-machine authentication using the OAuth 2.0 client credentials
28> flow. No user interaction required.
3029 
31 <Card title="Enterprise-Managed Authorization" icon="building" href="/extensions/auth/enterprise-managed-authorization">
32 Centralized access control via enterprise identity providers. Employees
33 access MCP servers through their organization's IdP.
34 </Card>
35</CardGroup>
30> **[Enterprise-Managed Authorization](/extensions/auth/enterprise-managed-authorization)**
31>
32> Centralized access control via enterprise identity providers. Employees
33> access MCP servers through their organization's IdP.
3634 
3735## Choosing the right extension
3836 

extensions/client-matrix Changed · +18 / -22 lines

from line 2
22 
33> Which MCP clients implement which official extensions
44 
5export const CHECK = () => <span className="flex justify-center">
6 <Icon icon="check" iconType="solid" size={18} className="text-green-500" />
7 </span>;
8 
95This matrix shows which MCP clients support each [official extension](/extensions/overview). Extensions are always opt-in: a client only uses an extension if both client and server declare support in the `extensions` field of their [capabilities](/extensions/overview#negotiation).
106 
11<Note>
12 This list is maintained by the community. If you notice any inaccuracies or would like to add or update information, please [submit a pull request](https://github.com/modelcontextprotocol/modelcontextprotocol/pulls).
13</Note>
7> [!NOTE]
8>
9> This list is maintained by the community. If you notice any inaccuracies or would like to add or update information, please [submit a pull request](https://github.com/modelcontextprotocol/modelcontextprotocol/pulls).
1410 
1511## Extension overview
1612 
from line 21
2521 
2622| Client | [MCP Apps](/extensions/apps/overview) | [OAuth Client Credentials](/extensions/auth/oauth-client-credentials) | [Enterprise Auth](/extensions/auth/enterprise-managed-authorization) | [Skills](/extensions/skills/overview) |
2723| - | :-: | :-: | :-: | :-: |
28| [Claude (web)](https://claude.ai) | <CHECK /> | | | |
29| [Claude Desktop](https://claude.ai/download) | <CHECK /> | | | |
30| [VS Code GitHub Copilot](https://code.visualstudio.com/) | <CHECK /> | | | |
31| [Microsoft 365 Copilot](https://www.microsoft.com/microsoft-365-copilot) | <CHECK /> | | | |
32| [Goose](https://block.github.io/goose/) | <CHECK /> | | | |
33| [Postman](https://postman.com) | <CHECK /> | | | |
34| [MCPJam](https://www.mcpjam.com/) | <CHECK /> | | | |
35| [ChatGPT](https://chatgpt.com/) | <CHECK /> | | | [Partial](https://developers.openai.com/plugins/build/mcp-server#import-skills-from-the-mcp-server) |
36| [Cursor](https://cursor.com/) | <CHECK /> | | | |
37| [Archestra.AI](https://www.archestra.ai/) | <CHECK /> | | <CHECK /> | |
38| [PostHog Code](https://posthog.com/code/) | <CHECK /> | | | |
24| [Claude (web)](https://claude.ai) | | | | |
25| [Claude Desktop](https://claude.ai/download) | | | | |
26| [VS Code GitHub Copilot](https://code.visualstudio.com/) | | | | |
27| [Microsoft 365 Copilot](https://www.microsoft.com/microsoft-365-copilot) | | | | |
28| [Goose](https://block.github.io/goose/) | | | | |
29| [Postman](https://postman.com) | | | | |
30| [MCPJam](https://www.mcpjam.com/) | | | | |
31| [ChatGPT](https://chatgpt.com/) | | | | [Partial](https://developers.openai.com/plugins/build/mcp-server#import-skills-from-the-mcp-server) |
32| [Cursor](https://cursor.com/) | | | | |
33| [Archestra.AI](https://www.archestra.ai/) | | | | |
34| [PostHog Code](https://posthog.com/code/) | | | | |
3935| [fast-agent](https://github.com/evalstate/fast-agent) | | | | [Partial](https://github.com/evalstate/fast-agent/blob/main/docs/docs/mcp/skills-over-mcp.md) |
4036| [MCP Inspector](https://github.com/modelcontextprotocol/inspector) | | | | [Partial](https://github.com/modelcontextprotocol/inspector/blob/main/clients/cli/README.md#skill-verification---verify) |
41| [mcpc MCP CLI](https://github.com/apify/mcpc) | | <CHECK /> | <CHECK /> | <CHECK /> |
37| [mcpc MCP CLI](https://github.com/apify/mcpc) | | | | |
4238 
43<Note>
44 Auth extension support (OAuth Client Credentials and Enterprise-Managed Authorization) is tracked separately from the core MCP authorization features (DCR, CIMD). Check each extension's specification and the [ext-auth repository](https://github.com/modelcontextprotocol/ext-auth) for the latest implementation status.
45</Note>
39> [!NOTE]
40>
41> Auth extension support (OAuth Client Credentials and Enterprise-Managed Authorization) is tracked separately from the core MCP authorization features (DCR, CIMD). Check each extension's specification and the [ext-auth repository](https://github.com/modelcontextprotocol/ext-auth) for the latest implementation status.
4642 
4743## Adding extension support to your client
4844 

extensions/overview Changed · +16 / -16 lines

from line 8
88 
99Extensions are identified using a unique *extension identifier* with the format: `{vendor-prefix}/{extension-name}`, e.g. `io.modelcontextprotocol/oauth-client-credentials`. Identifiers follow the same rules as [`_meta` keys](/specification/draft/basic/index#meta), with a mandatory prefix. Official extensions use the `io.modelcontextprotocol` vendor prefix.
1010 
11<Tip>
12 If you're building a third-party extension, use a reversed domain name you own as the vendor prefix to avoid collisions (similar to Java package naming). For example, a company owning `example.com` would use `com.example/` as their prefix (e.g., `com.example/my-extension`).
13</Tip>
11> [!TIP]
12>
13> If you're building a third-party extension, use a reversed domain name you own as the vendor prefix to avoid collisions (similar to Java package naming). For example, a company owning `example.com` would use `com.example/` as their prefix (e.g., `com.example/my-extension`).
1414 
1515## Official Extension Repositories
1616 
from line 18
1818 
1919### MCP Authorization Extensions
2020 
21<Card title="modelcontextprotocol/ext-auth" icon="github" href="https://github.com/modelcontextprotocol/ext-auth">
22 Extensions for supplementary authorization mechanisms beyond the core
23 specification.
24</Card>
21> **[modelcontextprotocol/ext-auth](https://github.com/modelcontextprotocol/ext-auth)**
22>
23> Extensions for supplementary authorization mechanisms beyond the core
24> specification.
2525 
2626| Extension | Description |
2727| - | - |
from line 30
3030 
3131### MCP Apps
3232 
33<Card title="modelcontextprotocol/ext-apps" icon="github" href="https://github.com/modelcontextprotocol/ext-apps">
34 Extensions for interactive UI elements in conversational MCP clients.
35</Card>
33> **[modelcontextprotocol/ext-apps](https://github.com/modelcontextprotocol/ext-apps)**
34>
35> Extensions for interactive UI elements in conversational MCP clients.
3636 
3737| Extension | Description |
3838| - | - |
from line 48
4848 
4949### Skills over MCP
5050 
51<Card title="modelcontextprotocol/ext-skills" icon="github" href="https://github.com/modelcontextprotocol/ext-skills">
52 Discover and read Agent Skills from MCP servers.
53</Card>
51> **[modelcontextprotocol/ext-skills](https://github.com/modelcontextprotocol/ext-skills)**
52>
53> Discover and read Agent Skills from MCP servers.
5454 
5555| Extension | Description |
5656| - | - |
from line 94
9494 
9595SDKs can choose to implement extensions, but it's not required for protocol conformance. SDK maintainers have full autonomy over which extensions they support. Where an SDK does support extensions, SDK documentation should list which extensions are supported.
9696 
97<Note>
98 Extensions are always disabled by default and require explicit opt-in from the developer.
99</Note>
97> [!NOTE]
98>
99> Extensions are always disabled by default and require explicit opt-in from the developer.
100100 
101101### Evolution
102102 
Feedback