@mastra/mcp 2.0.0 → 2.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,9 @@ The `MCPServer` class provides the functionality to expose your existing Mastra
8
8
 
9
9
  Note that if you only need to use your tools or agents directly within your Mastra application, you don't necessarily need to create an MCP server. This API is specifically for exposing your Mastra tools and agents to _external_ MCP clients.
10
10
 
11
- It supports both [stdio (subprocess) and SSE (HTTP) MCP transports](https://modelcontextprotocol.io/docs/concepts/transports).
11
+ It supports the [stdio (subprocess) and Streamable HTTP MCP transports](https://modelcontextprotocol.io/docs/concepts/transports) of the MCP `2026-07-28` revision.
12
+
13
+ > **Note:** Upgrading from `@mastra/mcp` 1.x? See the [migration guide](https://mastra.ai/reference/migrations/mcp-v2).
12
14
 
13
15
  ## Constructor
14
16
 
@@ -53,7 +55,7 @@ const server = new MCPServer({
53
55
 
54
56
  ## Tool schemas and structured results
55
57
 
56
- MCP tool input and output schemas are advertised as JSON Schema 2020-12, the dialect the `2026-07-28` revision assumes, with the dialect declared in `$schema`. Advertised Mastra tool schemas preserve supported references, composition keywords, and tuple (`prefixItems`) shapes.
58
+ MCP tool input and output schemas are advertised as JSON Schema 2020-12 with the dialect declared in `$schema`. Advertised Mastra tool schemas preserve supported references, composition keywords, and tuple (`prefixItems`) shapes.
57
59
 
58
60
  A tool with an `outputSchema` can return any JSON value, including an object, array, string, number, boolean, or `null`. The value is sent as `structuredContent` without wrapping it in an object. The server validates successful structured output against the tool's output schema before returning it.
59
61
 
@@ -81,6 +83,12 @@ The constructor accepts an `MCPServerConfig` object with the following propertie
81
83
 
82
84
  **fga** (`{ resourceMapping?: Partial<Record<'tool' | 'tools', { fgaResourceType: string; deriveId?: ({ user, resourceId, requestContext }) => string | undefined }>>; permissionMapping?: Record<string, string> }`): Overrides resource and permission mappings for this MCP server's tools/list and tools/call FGA checks. Use this when MCP authorization should be scoped differently from internal agent or workflow tool execution.
83
85
 
86
+ **requestState** (`{ key?: string | Uint8Array; ttlSeconds?: number }`): Integrity protection for input\_required continuation state. key is an HMAC key of at least 32 bytes that every instance able to answer a continuation must share; ttlSeconds (default 600) is how long a suspended round stays answerable. Without a key the server generates one per process. See the Asking the caller for input section.
87
+
88
+ **cacheHints** (`MCPServerCacheHints`): Cache hints (ttlMs / cacheScope) advertised on cacheable results, keyed by operation: 'tools/list', 'prompts/list', 'resources/list', 'resources/templates/list', 'resources/read' and 'server/discover'.
89
+
90
+ **jsonSchemaValidator** (`jsonSchemaValidator`): Custom JSON Schema validator used for tool input, output and continuation answers, for runtimes where the SDK default is unavailable.
91
+
84
92
  **repository** (`Repository`): Optional repository information for the server's source code.
85
93
 
86
94
  **releaseDate** (`string`): Optional release date of this server version (ISO 8601 string). Defaults to the time of instantiation if not provided.
@@ -99,35 +107,6 @@ The constructor accepts an `MCPServerConfig` object with the following propertie
99
107
 
100
108
  **appResources** (`AppResources`): A map of ui:// URIs to app resource configurations. Each entry defines an interactive HTML UI served via the MCP Apps extension (SEP-1865). See the MCP Apps section for details.
101
109
 
102
- **protocolVersion** (`'2025-11-25' | '2026-07-28'`): Opt-in MCP protocol revision. Omitted (or '2025-11-25') keeps the legacy behavior exactly. Set to '2026-07-28' to serve the stateless MCP revision. See the Protocol versions section for details.
103
-
104
- **cacheHints** (`MCPServerCacheHints`): Cache hints (ttlMs / cacheScope) advertised on cacheable results of the '2026-07-28' protocol revision, keyed by operation (e.g. 'tools/list'). Only applied when protocolVersion: '2026-07-28' is set.
105
-
106
- ## Protocol versions
107
-
108
- By default, `MCPServer` speaks the legacy (2025-era) MCP protocol: sessionful streamable HTTP with an `initialize` handshake. Set `protocolVersion: '2026-07-28'` to serve the stateless MCP revision instead:
109
-
110
- - HTTP and serverless requests go through a dual-era handler: clients that speak `2026-07-28` are served natively (stateless, per-request envelope), and legacy clients are served through a built-in stateless fallback on the same endpoint.
111
- - `startStdio()` serves both eras: the opening exchange selects the era for the connection.
112
- - Tool list, prompt list, resource list, and resource update notifications also reach `2026-07-28` clients through `subscriptions/listen`.
113
- - Tool log messages honor the caller's per-request `logLevel` opt-in instead of the session-level `logging/setLevel`.
114
- - Configured `cacheHints` are advertised on cacheable results such as `tools/list`.
115
- - Tool elicitation (`options.mcp.elicitation.sendRequest()`) works on both eras. On `2026-07-28` requests, it uses the protocol's multi round-trip mechanism. The tool call first returns an `input_required` result. After the client answers, the call retries with the answer attached. The `sendRequest()` promise API is unchanged, but the tool function re-executes from the top on each retry, so keep side effects idempotent (or place them after the last elicitation) and keep the order of `sendRequest()` calls deterministic for an input.
116
-
117
- ```typescript
118
- const server = new MCPServer({
119
- name: 'My Server',
120
- version: '1.0.0',
121
- tools: { weatherTool },
122
- protocolVersion: '2026-07-28',
123
- cacheHints: {
124
- 'tools/list': { ttlMs: 60_000, cacheScope: 'private' },
125
- },
126
- })
127
- ```
128
-
129
- Omitting `protocolVersion` keeps the current behavior unchanged.
130
-
131
110
  ## Exposing agents as tools
132
111
 
133
112
  A powerful feature of `MCPServer` is its ability to automatically expose your Mastra Agents as callable tools. When you provide agents in the `agents` property of the configuration:
@@ -155,23 +134,27 @@ For an agent to be converted into a tool, it **must** have a non-empty `descript
155
134
 
156
135
  Clients can use MCP to access your agents' generative capabilities and ask them questions directly.
157
136
 
158
- ### Accessing MCP Context in Tools
137
+ ### Accessing MCP context in tools
159
138
 
160
- Tools exposed through `MCPServer` can access MCP request context (authentication, session IDs, etc.) via two different properties depending on how the tool is invoked:
139
+ Tools exposed through `MCPServer` receive the protocol context of the current request as `context.mcp`:
161
140
 
162
- | Call Pattern | Access Method |
163
- | ---------------- | ------------------------------------------- |
164
- | Direct tool call | `context?.mcp?.extra` |
165
- | Agent tool call | `context?.requestContext?.get("mcp.extra")` |
141
+ | Member | Description |
142
+ | ----------------------------- | ---------------------------------------------------------------------------- |
143
+ | `context.mcp.extra.signal` | Aborts when the client cancels the request or disconnects |
144
+ | `context.mcp.extra.requestId` | The JSON-RPC id of the request |
145
+ | `context.mcp.extra.authInfo` | Whatever the transport authenticated (see Authentication context) |
146
+ | `context.mcp.extra._meta` | Request metadata: trace headers, the log-level opt-in and the progress token |
147
+ | `context.mcp.log()` | Sends a log message to the calling client (see Logging) |
148
+ | `context.mcp.progress()` | Reports progress to the calling client (see Progress notifications) |
149
+ | `context.mcp.protocolVersion` | `'2026-07-28'` |
166
150
 
167
- **Universal pattern** (works in both contexts):
151
+ The same request also populates `context.requestContext`, the trusted application context, with `authInfo`, the `user` returned by `mapAuthInfoToUser` and the W3C `traceContext` the client sent. Tools invoked by an agent that an MCP client asked (`ask_<agent>`) don't receive `context.mcp`, but the request context is forwarded to the agent, so read auth data from there when a tool can be reached both ways:
168
152
 
169
153
  ```typescript
170
- const mcpExtra = context?.mcp?.extra ?? context?.requestContext?.get('mcp.extra')
171
- const authInfo = mcpExtra?.authInfo
154
+ const authInfo = context.mcp?.extra.authInfo ?? context.requestContext?.get('authInfo')
172
155
  ```
173
156
 
174
- #### Example: Tool that works in both contexts
157
+ #### Example: Tool that reads the caller's token
175
158
 
176
159
  ```typescript
177
160
  import { createTool } from '@mastra/core/tools'
@@ -184,11 +167,7 @@ const fetchUserData = createTool({
184
167
  userId: z.string().describe('The ID of the user to fetch'),
185
168
  }),
186
169
  execute: async (inputData, context) => {
187
- // Access MCP authentication context
188
- // When called directly via MCP: context.mcp.extra
189
- // When called via agent: context.requestContext.get('mcp.extra')
190
- const mcpExtra = context?.mcp?.extra || context?.requestContext?.get('mcp.extra')
191
- const authInfo = mcpExtra?.authInfo
170
+ const authInfo = context.mcp?.extra.authInfo ?? context.requestContext?.get('authInfo')
192
171
 
193
172
  if (!authInfo?.token) {
194
173
  throw new Error('Authentication required')
@@ -198,6 +177,7 @@ const fetchUserData = createTool({
198
177
  headers: {
199
178
  Authorization: `Bearer ${authInfo.token}`,
200
179
  },
180
+ signal: context.mcp?.extra.signal,
201
181
  })
202
182
 
203
183
  return response.json()
@@ -227,7 +207,7 @@ The authenticated user is mapped to `authInfo` as:
227
207
  | `scopes` | `user.scopes`, `user.scope`, or `user.permissions`, normalized to an array |
228
208
  | `extra.user` | The full user object returned by the auth provider |
229
209
 
230
- If your own middleware performs the verification, set `server.mcpOptions.setRequestAuth` to build `authInfo` yourself. The hook replaces the default mapping and applies to both the streamable HTTP and SSE transports:
210
+ If your own middleware performs the verification, set `server.mcpOptions.setRequestAuth` to build `authInfo` yourself. The hook replaces the default mapping:
231
211
 
232
212
  ```typescript
233
213
  export const mastra = new Mastra({
@@ -250,141 +230,81 @@ export const mastra = new Mastra({
250
230
 
251
231
  Leaving `req.auth` unset inside the hook opts the request out of auth info entirely.
252
232
 
253
- ## Methods
254
-
255
- These are the functions you can call on an `MCPServer` instance to control its behavior and get information.
256
-
257
- ### `startStdio()`
233
+ ## Asking the caller for input
258
234
 
259
- Use this method to start the server so it communicates using standard input and output (stdio). This is typical when running the server as a command-line program.
235
+ A tool that needs something from the user before it can finish calls `context.suspend(payload)` and returns, exactly as it would inside an agent or a workflow. The server ends the request as an `input_required` result that carries the form described by the tool's `resumeSchema`. When the client answers, the server runs the tool again with the answer in `context.resumeData` and the payload it suspended with in `context.suspendPayload`. Each round is a separate request: the server never replays earlier rounds, so put the state the next round needs in the payload and branch on it.
260
236
 
261
237
  ```typescript
262
- async startStdio(): Promise<void>
263
- ```
264
-
265
- Here's how you would start the server using stdio:
238
+ import { createTool } from '@mastra/core/tools'
239
+ import { z } from 'zod'
266
240
 
267
- ```typescript
268
- const server = new MCPServer({
269
- id: 'my-server',
270
- name: 'My Server',
271
- version: '1.0.0',
272
- tools: {/* ... */},
241
+ export const bookDelivery = createTool({
242
+ id: 'bookDelivery',
243
+ description: 'Books a delivery for an order after confirming the address.',
244
+ inputSchema: z.object({ orderId: z.string() }),
245
+ outputSchema: z.object({ confirmed: z.boolean() }),
246
+ suspendSchema: z.object({ phase: z.literal('address'), message: z.string() }),
247
+ resumeSchema: z.object({ address: z.string() }),
248
+ execute: async ({ orderId }, context) => {
249
+ if (!context.resumeData) {
250
+ await context.suspend?.({ phase: 'address', message: 'Delivery address?' })
251
+ return
252
+ }
253
+ await book(orderId, context.resumeData.address)
254
+ return { confirmed: true }
255
+ },
273
256
  })
274
- await server.startStdio()
275
257
  ```
276
258
 
277
- ### `startSSE()`
259
+ `resumeSchema` becomes the form the caller fills in, so it must describe a flat object of primitives (strings, numbers, booleans, enums). A caller that declines or cancels the form ends the call with an error, and the tool doesn't run again. A tool can suspend more than once by changing the phase in its payload.
278
260
 
279
- > **Warning:** The HTTP+SSE transport is deprecated in the MCP specification. Use `startHTTP()` (streamable HTTP) instead.
261
+ ### Continuation state
280
262
 
281
- This method helps you integrate the MCP server with an existing web server to use Server-Sent Events (SSE) for communication. You'll call this from your web server's code when it receives a request for the SSE or message paths.
263
+ The continuation travels as an opaque `requestState` string that the client echoes back with its answer. The server signs it with `requestState.key` and rejects a tampered, expired or foreign state (a different tool, different arguments or a different caller) before your handler runs. The caller is the token subject when the authorization layer provides one, otherwise the `id` of the user `mapAuthInfoToUser` returns, otherwise the bearer token itself, so two users behind the same OAuth client can't resume each other's rounds.
282
264
 
283
- ```typescript
284
- async startSSE({
285
- url,
286
- ssePath,
287
- messagePath,
288
- req,
289
- res,
290
- }: {
291
- url: URL;
292
- ssePath: string;
293
- messagePath: string;
294
- req: any;
295
- res: any;
296
- }): Promise<void>
297
- ```
265
+ On a server without authorization every caller shares one anonymous principal, so a `requestState` behaves like a bearer credential until its `ttlSeconds` expire: anyone who obtains it can answer the round. Put tools whose suspensions carry authority (writes, purchases, account changes) behind authorization.
298
266
 
299
- Here's an example of how you might use `startSSE` within an HTTP server request handler. In this example an MCP client could connect to your MCP server at `http://localhost:1234/sse`:
267
+ The payload is signed, not encrypted, so keep it small and non-secret (IDs and phase, not confidential data). Set `requestState.key` from the environment so every instance that may answer a continuation shares the key. Without it the server generates a key per process and continuations only succeed on that process.
300
268
 
301
269
  ```typescript
302
- import http from 'http'
303
-
304
- const httpServer = http.createServer(async (req, res) => {
305
- await server.startSSE({
306
- url: new URL(req.url || '', `http://localhost:1234`),
307
- ssePath: '/sse',
308
- messagePath: '/message',
309
- req,
310
- res,
311
- })
312
- })
313
-
314
- httpServer.listen(PORT, () => {
315
- console.log(`HTTP server listening on port ${PORT}`)
270
+ const server = new MCPServer({
271
+ id: 'booking',
272
+ name: 'Booking',
273
+ version: '1.0.0',
274
+ tools: { bookDelivery },
275
+ requestState: { key: process.env.MCP_REQUEST_STATE_KEY!, ttlSeconds: 600 },
316
276
  })
317
277
  ```
318
278
 
319
- Here are the details for the values needed by the `startSSE` method:
320
-
321
- **url** (`URL`): The web address the user is requesting.
322
-
323
- **ssePath** (`string`): The specific part of the URL where clients will connect for SSE (e.g., '/sse').
324
-
325
- **messagePath** (`string`): The specific part of the URL where clients will send messages (e.g., '/message').
326
-
327
- **req** (`any`): The incoming request object from your web server.
279
+ Resource and prompt callbacks can suspend the same way. See [Resource handling](#resource-handling) and [Prompt handling](#prompt-handling).
328
280
 
329
- **res** (`any`): The response object from your web server, used to send data back.
281
+ ## Methods
330
282
 
331
- ### `startHonoSSE()`
283
+ These are the functions you can call on an `MCPServer` instance to control its behavior and get information.
332
284
 
333
- > **Warning:** The HTTP+SSE transport is deprecated in the MCP specification. Use `startHTTP()` (streamable HTTP) instead.
285
+ ### `startStdio()`
334
286
 
335
- This method helps you integrate the MCP server with an existing web server to use Server-Sent Events (SSE) for communication. You'll call this from your web server's code when it receives a request for the SSE or message paths.
287
+ Use this method to start the server so it communicates using standard input and output (stdio). This is typical when running the server as a command-line program.
336
288
 
337
289
  ```typescript
338
- async startHonoSSE({
339
- url,
340
- ssePath,
341
- messagePath,
342
- req,
343
- res,
344
- }: {
345
- url: URL;
346
- ssePath: string;
347
- messagePath: string;
348
- req: any;
349
- res: any;
350
- }): Promise<void>
290
+ async startStdio(): Promise<void>
351
291
  ```
352
292
 
353
- Here's an example of how you might use `startHonoSSE` within an HTTP server request handler. In this example an MCP client could connect to your MCP server at `http://localhost:1234/hono-sse`:
293
+ Here's how you would start the server using stdio:
354
294
 
355
295
  ```typescript
356
- import http from 'http'
357
-
358
- const httpServer = http.createServer(async (req, res) => {
359
- await server.startHonoSSE({
360
- url: new URL(req.url || '', `http://localhost:1234`),
361
- ssePath: '/hono-sse',
362
- messagePath: '/message',
363
- req,
364
- res,
365
- })
366
- })
367
-
368
- httpServer.listen(PORT, () => {
369
- console.log(`HTTP server listening on port ${PORT}`)
296
+ const server = new MCPServer({
297
+ id: 'my-server',
298
+ name: 'My Server',
299
+ version: '1.0.0',
300
+ tools: {/* ... */},
370
301
  })
302
+ await server.startStdio()
371
303
  ```
372
304
 
373
- Here are the details for the values needed by the `startHonoSSE` method:
374
-
375
- **url** (`URL`): The web address the user is requesting.
376
-
377
- **ssePath** (`string`): The specific part of the URL where clients will connect for SSE (e.g., '/hono-sse').
378
-
379
- **messagePath** (`string`): The specific part of the URL where clients will send messages (e.g., '/message').
380
-
381
- **req** (`any`): The incoming request object from your web server.
382
-
383
- **res** (`any`): The response object from your web server, used to send data back.
384
-
385
305
  ### `startHTTP()`
386
306
 
387
- This method helps you integrate the MCP server with an existing web server to use streamable HTTP for communication. You'll call this from your web server's code when it receives HTTP requests.
307
+ This method helps you integrate the MCP server with an existing web server to use Streamable HTTP for communication. You'll call this from your web server's code when it receives HTTP requests.
388
308
 
389
309
  ```typescript
390
310
  async startHTTP({
@@ -431,7 +351,7 @@ httpServer.listen(PORT, () => {
431
351
  })
432
352
  ```
433
353
 
434
- Because every request is self-contained, nothing needs to persist between invocations, so `startHTTP` works in serverless environments (Supabase Edge Functions, Cloudflare Workers, Vercel Edge, AWS Lambda, Deno Deploy). The method still takes Node-style `http.IncomingMessage` and `http.ServerResponse` objects, so a Fetch-based runtime has to convert its `Request` with an adapter such as `fetch-to-node` and turn the result back into a `Response`:
354
+ Because every request is self-contained, nothing needs to persist between invocations, so `startHTTP` works in serverless environments (Supabase Edge Functions, Cloudflare Workers, Vercel Edge, AWS Lambda, Deno Deploy). The method takes Node-style `http.IncomingMessage` and `http.ServerResponse` objects, so a Fetch-based runtime has to convert its `Request` with an adapter such as `fetch-to-node` and turn the result back into a `Response`:
435
355
 
436
356
  ```typescript
437
357
  // Supabase Edge Function example
@@ -475,15 +395,7 @@ Here are the details for the values needed by the `startHTTP` method:
475
395
 
476
396
  **res** (`http.ServerResponse`): The response object from your web server, used to send data back.
477
397
 
478
- **options** (`MCPServerHTTPRequestOptions`): Optional request guards. See the options table below for more details.
479
-
480
- The `MCPServerHTTPRequestOptions` object carries request guards:
481
-
482
- **enableDnsRebindingProtection** (`boolean`): If true, the Host and Origin headers are validated against allowedHosts and allowedOrigins before the request is handled. Defaults to false.
483
-
484
- **allowedHosts** (`string[]`): Hosts (host\[:port]) accepted when DNS rebinding protection is enabled.
485
-
486
- **allowedOrigins** (`string[]`): Origins accepted when DNS rebinding protection is enabled.
398
+ **options** (`MCPServerHTTPRequestOptions`): Optional request guards. See the options table above for more details.
487
399
 
488
400
  ### `close()`
489
401
 
@@ -506,7 +418,7 @@ getServerInfo(): ServerInfo
506
418
  The method returns details about the server's information.
507
419
 
508
420
  ```typescript
509
- getServerDetail(): ServerDetail
421
+ getServerDetail(): ServerDetailInfo
510
422
  ```
511
423
 
512
424
  ### `getToolListInfo()`
@@ -514,7 +426,7 @@ getServerDetail(): ServerDetail
514
426
  The method returns the tools that were set up when you created the server. It's a read-only list, useful for debugging purposes.
515
427
 
516
428
  ```typescript
517
- getToolListInfo(): ToolListInfo
429
+ getToolListInfo(): { tools: ToolInfo[] }
518
430
  ```
519
431
 
520
432
  ### `getToolInfo()`
@@ -522,67 +434,55 @@ getToolListInfo(): ToolListInfo
522
434
  The method returns details about a specific tool.
523
435
 
524
436
  ```typescript
525
- getToolInfo(toolName: string): ToolInfo
437
+ getToolInfo(toolId: string): ToolInfo | undefined
526
438
  ```
527
439
 
528
440
  ### `executeTool()`
529
441
 
530
- This method executes a specific tool and returns the result.
531
-
532
- ```typescript
533
- executeTool(toolName: string, input: any): Promise<any>
534
- ```
535
-
536
- ### `getStdioTransport()`
537
-
538
- If you started the server with `startStdio()`, you can use this to get the object that manages the stdio communication. This is mostly for checking things internally or for testing.
539
-
540
- ```typescript
541
- getStdioTransport(): StdioServerTransport | undefined
542
- ```
543
-
544
- ### `getSseTransport()`
545
-
546
- If you started the server with `startSSE()`, you can use this to get the object that manages the SSE communication. Like `getStdioTransport`, this is mainly for internal checks or testing.
442
+ Runs a tool without a protocol client, which is how the Mastra REST route `POST /api/mcp/:serverId/tools/:toolId/execute` and Studio invoke tools.
547
443
 
548
444
  ```typescript
549
- getSseTransport(): SSEServerTransport | undefined
445
+ async executeTool(
446
+ toolId: string,
447
+ args: unknown,
448
+ executionContext?: {
449
+ messages?: any[];
450
+ toolCallId?: string;
451
+ requestContext?: RequestContext;
452
+ resumeData?: unknown;
453
+ suspendPayload?: unknown;
454
+ },
455
+ ): Promise<MCPToolExecutionResultV2>
550
456
  ```
551
457
 
552
- ### `getSseHonoTransport()`
553
-
554
- If you started the server with `startHonoSSE()`, you can use this to get the object that manages the SSE communication. Like `getSseTransport`, this is mainly for internal checks or testing.
458
+ The result is `{ status: 'completed', output }` for a finished call, or `{ status: 'suspended', suspendPayload, resumeSchema }` when the tool asked for input. Continue by calling again with the same `args` plus `resumeData` and the echoed `suspendPayload`. A tool that throws, or input or resume data that fails its schema, rejects the promise.
555
459
 
556
460
  ```typescript
557
- getSseHonoTransport(): SSETransport | undefined
461
+ const result = await server.executeTool('bookDelivery', { orderId })
462
+ if (result.status === 'suspended') {
463
+ const answer = await askUser(result.suspendPayload, result.resumeSchema)
464
+ await server.executeTool(
465
+ 'bookDelivery',
466
+ { orderId },
467
+ { resumeData: answer, suspendPayload: result.suspendPayload },
468
+ )
469
+ }
558
470
  ```
559
471
 
560
- ### `getStreamableHTTPTransport()`
472
+ **toolId** (`string`): The ID/name of the tool to execute.
561
473
 
562
- If you started the server with `startHTTP()`, you can use this to get the object that manages the HTTP communication. Like `getSseTransport`, this is mainly for internal checks or testing.
474
+ **args** (`unknown`): The arguments to pass to the tool's execute function, validated against its input schema.
563
475
 
564
- ```typescript
565
- getStreamableHTTPTransport(): StreamableHTTPServerTransport | undefined
566
- ```
476
+ **executionContext** (`object`): Optional context for the tool execution. Pass resumeData and suspendPayload to continue a suspended tool.
567
477
 
568
478
  ### `tools()`
569
479
 
570
- Executes a specific tool provided by this MCP server.
480
+ Returns the registered tool registry, keyed by tool ID.
571
481
 
572
482
  ```typescript
573
- async executeTool(
574
- toolId: string,
575
- args: any,
576
- executionContext?: { messages?: any[]; toolCallId?: string },
577
- ): Promise<any>
483
+ tools(): Readonly<Record<string, ConvertedTool>>
578
484
  ```
579
485
 
580
- **toolId** (`string`): The ID/name of the tool to execute.
581
-
582
- **args** (`any`): The arguments to pass to the tool's execute function.
583
-
584
- **executionContext** (`object`): Optional context for the tool execution, like messages or a toolCallId.
585
-
586
486
  ## Resource handling
587
487
 
588
488
  ### What are MCP Resources?
@@ -603,7 +503,7 @@ Clients can discover resources through:
603
503
  1. **Direct resources**: Servers expose a list of concrete resources via a `resources/list` endpoint.
604
504
  2. **Resource templates**: For runtime-defined resources, servers can expose URI templates (RFC 6570) that clients use to construct resource URIs.
605
505
 
606
- To read a resource, clients make a `resources/read` request with the URI. Servers can also notify clients about changes to the resource list (`notifications/resources/list_changed`) or updates to specific resource content (`notifications/resources/updated`) if a client has subscribed to that resource.
506
+ To read a resource, clients make a `resources/read` request with the URI. Servers can also notify clients about changes to the resource list (`notifications/resources/list_changed`) or updates to specific resource content (`notifications/resources/updated`) if a client is listening for that resource.
607
507
 
608
508
  For more detailed information, refer to the [official MCP documentation on Resources](https://modelcontextprotocol.io/docs/concepts/resources).
609
509
 
@@ -614,27 +514,43 @@ The `resources` option takes an object of type `MCPServerResources`. This type d
614
514
  ```typescript
615
515
  export type MCPServerResources = {
616
516
  // Callback to list available resources
617
- listResources: () => Promise<Resource[]>
517
+ listResources: (params: {
518
+ extra: MCPRequestHandlerExtra
519
+ requestContext: RequestContext
520
+ }) => Promise<Resource[]>
618
521
 
619
522
  // Callback to get the content of a specific resource
620
- getResourceContent: ({
621
- uri,
622
- }: {
623
- uri: string
624
- }) => Promise<MCPServerResourceContent | MCPServerResourceContent[]>
523
+ getResourceContent: (
524
+ params: { uri: string } & MCPServerRequest,
525
+ ) => Promise<MCPServerResourceContent | MCPServerResourceContent[] | void>
625
526
 
626
527
  // Optional callback to list available resource templates
627
- resourceTemplates?: () => Promise<ResourceTemplate[]>
528
+ resourceTemplates?: (params: {
529
+ extra: MCPRequestHandlerExtra
530
+ requestContext: RequestContext
531
+ }) => Promise<ResourceTemplate[]>
532
+
533
+ // Shape of the answer a suspended getResourceContent expects
534
+ resumeSchema?: StandardSchemaWithJSON
628
535
  }
629
536
 
630
537
  export type MCPServerResourceContent = { text?: string } | { blob?: string }
631
538
  ```
632
539
 
540
+ Every callback receives `extra`, the same protocol context tools see as `context.mcp.extra` (cancellation `signal`, `requestId`, `authInfo`, `_meta`), and `requestContext`, the trusted application context that carries `authInfo` and the user mapped by `mapAuthInfoToUser`. Use them to scope what a caller can list and read.
541
+
542
+ `getResourceContent` also receives `suspend`, `resumeData` and `suspendPayload` (the `MCPServerRequest` members). Declare `resumeSchema` to make `suspend` usable. It works exactly as it does for tools.
543
+
633
544
  Example:
634
545
 
635
546
  ```typescript
636
547
  import { MCPServer } from '@mastra/mcp'
637
- import type { MCPServerResourceContent, Resource, ResourceTemplate } from '@mastra/mcp'
548
+ import type {
549
+ MCPServerResourceContent,
550
+ MCPServerResources,
551
+ Resource,
552
+ ResourceTemplate,
553
+ } from '@mastra/mcp'
638
554
 
639
555
  // Resources/resource templates will generally be dynamically fetched.
640
556
  const myResources: Resource[] = [
@@ -656,7 +572,7 @@ const myResourceTemplates: ResourceTemplate[] = [
656
572
 
657
573
  const myResourceHandlers: MCPServerResources = {
658
574
  listResources: async () => myResources,
659
- getResourceContent: async ({ uri }) => {
575
+ getResourceContent: async ({ uri, extra }) => {
660
576
  if (myResourceContents[uri]) {
661
577
  return myResourceContents[uri]
662
578
  }
@@ -676,11 +592,11 @@ const serverWithResources = new MCPServer({
676
592
 
677
593
  ### Notifying Clients of Resource Changes
678
594
 
679
- If the available resources or their content change, your server can notify connected clients that are subscribed to the specific resource.
595
+ If the available resources or their content change, your server can notify clients that are listening for the specific resource.
680
596
 
681
597
  #### `server.resources.notifyUpdated({ uri: string })`
682
598
 
683
- Call this method when the content of a specific resource (identified by its `uri`) has been updated. If any clients are subscribed to this URI, they will receive a `notifications/resources/updated` message.
599
+ Call this method when the content of a specific resource (identified by its `uri`) has been updated. Clients subscribed to this URI receive a `notifications/resources/updated` message on their `subscriptions/listen` stream.
684
600
 
685
601
  ```typescript
686
602
  async server.resources.notifyUpdated({ uri: string }): Promise<void>
@@ -695,7 +611,7 @@ await serverWithResources.resources.notifyUpdated({ uri: 'file://data.txt' })
695
611
 
696
612
  #### `server.resources.notifyListChanged()`
697
613
 
698
- Call this method when the list of available resources has changed (e.g., a resource was added or removed). This will send a `notifications/resources/list_changed` message to clients, prompting them to re-fetch the list of resources.
614
+ Call this method when the list of available resources has changed (e.g., a resource was added or removed). This will send a `notifications/resources/list_changed` message to listening clients, prompting them to re-fetch the list of resources.
699
615
 
700
616
  ```typescript
701
617
  async server.resources.notifyListChanged(): Promise<void>
@@ -712,9 +628,9 @@ await serverWithResources.resources.notifyListChanged()
712
628
 
713
629
  ### What are MCP Prompts?
714
630
 
715
- Prompts are reusable templates or workflows that MCP servers expose to clients. They can accept arguments and include resource context. They also support versioning and standardize LLM interactions.
631
+ Prompts are reusable templates or workflows that MCP servers expose to clients. They can accept arguments and include resource context. They standardize LLM interactions.
716
632
 
717
- Prompts are identified by a unique name (and optional version) and can be runtime-defined or static.
633
+ Prompts are identified by a unique name and can be runtime-defined or static.
718
634
 
719
635
  ### `MCPServerPrompts` Type
720
636
 
@@ -723,21 +639,23 @@ The `prompts` option takes an object of type `MCPServerPrompts`. This type defin
723
639
  ```typescript
724
640
  export type MCPServerPrompts = {
725
641
  // Callback to list available prompts
726
- listPrompts: () => Promise<Prompt[]>
727
-
728
- // Callback to get the messages/content for a specific prompt
729
- getPromptMessages?: ({
730
- name,
731
- version,
732
- args,
733
- }: {
734
- name: string
735
- version?: string
736
- args?: any
737
- }) => Promise<{ prompt: Prompt; messages: PromptMessage[] }>
642
+ listPrompts: (params: {
643
+ extra: MCPRequestHandlerExtra
644
+ requestContext: RequestContext
645
+ }) => Promise<Prompt[]>
646
+
647
+ // Callback to get the messages for a specific prompt
648
+ getPromptMessages?: (
649
+ params: { name: string; args?: Record<string, unknown> } & MCPServerRequest,
650
+ ) => Promise<PromptMessage[] | void>
651
+
652
+ // Shape of the answer a suspended getPromptMessages expects
653
+ resumeSchema?: StandardSchemaWithJSON
738
654
  }
739
655
  ```
740
656
 
657
+ The callbacks receive the same `extra` and `requestContext` as resource callbacks, and `getPromptMessages` can `suspend` in the same way when `resumeSchema` is declared. The server validates required prompt arguments before calling `getPromptMessages`.
658
+
741
659
  Example:
742
660
 
743
661
  ```typescript
@@ -748,68 +666,44 @@ const prompts: Prompt[] = [
748
666
  {
749
667
  name: 'analyze-code',
750
668
  description: 'Analyze code for improvements',
751
- version: 'v1',
752
- },
753
- {
754
- name: 'analyze-code',
755
- description: 'Analyze code for improvements (new logic)',
756
- version: 'v2',
669
+ arguments: [{ name: 'code', description: 'The code to analyze', required: true }],
757
670
  },
758
671
  ]
759
672
 
760
673
  const myPromptHandlers: MCPServerPrompts = {
761
674
  listPrompts: async () => prompts,
762
- getPromptMessages: async ({ name, version, args }) => {
675
+ getPromptMessages: async ({ name, args }) => {
763
676
  if (name === 'analyze-code') {
764
- if (version === 'v2') {
765
- const prompt = prompts.find(p => p.name === name && p.version === 'v2')
766
- if (!prompt) throw new Error('Prompt version not found')
767
- return {
768
- prompt,
769
- messages: [
770
- {
771
- role: 'user',
772
- content: {
773
- type: 'text',
774
- text: `Analyze this code with the new logic: ${args.code}`,
775
- },
776
- },
777
- ],
778
- }
779
- }
780
- // Default or v1
781
- const prompt = prompts.find(p => p.name === name && p.version === 'v1')
782
- if (!prompt) throw new Error('Prompt version not found')
783
- return {
784
- prompt,
785
- messages: [
786
- {
787
- role: 'user',
788
- content: { type: 'text', text: `Analyze this code: ${args.code}` },
677
+ return [
678
+ {
679
+ role: 'user',
680
+ content: {
681
+ type: 'text',
682
+ text: `Analyze this code: ${args?.code}`,
789
683
  },
790
- ],
791
- }
684
+ },
685
+ ]
792
686
  }
793
687
  throw new Error('Prompt not found')
794
688
  },
795
689
  }
796
690
 
797
691
  const serverWithPrompts = new MCPServer({
798
- id: 'promptful-server',
799
- name: 'Promptful Server',
692
+ id: 'prompt-server',
693
+ name: 'Prompt Server',
800
694
  version: '1.0.0',
801
- tools: {/* ... */},
695
+ tools: {/* ... your tools ... */},
802
696
  prompts: myPromptHandlers,
803
697
  })
804
698
  ```
805
699
 
806
700
  ### Notifying Clients of Prompt Changes
807
701
 
808
- If the available prompts change, your server can notify connected clients:
702
+ If the available prompts change, your server can notify listening clients.
809
703
 
810
704
  #### `server.prompts.notifyListChanged()`
811
705
 
812
- Call this method when the list of available prompts has changed (e.g., a prompt was added or removed). This will send a `notifications/prompts/list_changed` message to clients, prompting them to re-fetch the list of prompts.
706
+ Call this method when the list of available prompts has changed (e.g., a prompt was added or removed). This will send a `notifications/prompts/list_changed` message to listening clients, prompting them to re-fetch the list of prompts.
813
707
 
814
708
  ```typescript
815
709
  await serverWithPrompts.prompts.notifyListChanged()
@@ -819,15 +713,12 @@ await serverWithPrompts.prompts.notifyListChanged()
819
713
 
820
714
  - Use clear, descriptive prompt names and descriptions.
821
715
  - Validate all required arguments in `getPromptMessages`.
822
- - Include a `version` field if you expect to make breaking changes.
823
- - Use the `version` parameter to select the correct prompt logic.
824
- - Notify clients when prompt lists change.
825
- - Handle errors with informative messages.
826
- - Document argument expectations and available versions.
716
+ - Return an error for unknown prompts or missing required arguments.
717
+ - Notify clients whenever prompts change.
827
718
 
828
719
  ## Dynamic tool management
829
720
 
830
- Tools are usually provided when constructing the `MCPServer`, but you can also add or remove tools while the server is running. The server exposes these operations through the `toolActions` property. When the tool list changes, connected clients receive a `notifications/tools/list_changed` message prompting them to re-fetch the tool list.
721
+ Add and remove tools on a running server. Connected clients are notified with `notifications/tools/list_changed`.
831
722
 
832
723
  The property is `toolActions` because `tools()` is the method that returns the registered tool registry.
833
724
 
@@ -883,28 +774,9 @@ When the server is registered with a Mastra instance, `toolActions.add()` and `t
883
774
 
884
775
  ## Logging
885
776
 
886
- MCP servers can send structured log messages to clients using `notifications/message`. Clients control verbosity by sending a `logging/setLevel` request. The server drops messages below the requested minimum level (following RFC 5424 severity ordering). The level is tracked per session, so different clients can request different verbosity.
887
-
888
- ### `sendLoggingMessage()`
777
+ Tools send structured log messages to the calling client with `notifications/message`. Delivery is opted into per request: the client attaches the `io.modelcontextprotocol/logLevel` metadata key to its request, and the server delivers messages at or above that severity (following RFC 5424 ordering) for that request only. Without the opt-in nothing is delivered. A later round of the same tool call is a new request and must opt in again. The Mastra `MCPClient` sends the key on every request when `enableServerLogs` is on.
889
778
 
890
- Sends a log notification to all connected clients, honoring each client's minimum logging level.
891
-
892
- ```typescript
893
- async server.sendLoggingMessage(params: {
894
- level: LoggingLevel;
895
- data: unknown;
896
- logger?: string;
897
- }): Promise<void>
898
- ```
899
-
900
- Example:
901
-
902
- ```typescript
903
- await server.sendLoggingMessage({
904
- level: 'info',
905
- data: { message: 'Sync completed', itemsProcessed: 42 },
906
- })
907
- ```
779
+ The Mastra logger and observability are unaffected: `context.mcp.log()` only controls what the MCP client receives.
908
780
 
909
781
  ### `context.mcp.log()`
910
782
 
@@ -922,16 +794,16 @@ Example:
922
794
 
923
795
  ```typescript
924
796
  execute: async ({ location }, context) => {
925
- await context.mcp.log('debug', 'Fetching weather', { location })
797
+ await context.mcp?.log?.('debug', 'Fetching weather', { location })
926
798
  const weather = await fetchWeather(location)
927
- await context.mcp.log('info', 'Weather fetched')
799
+ await context.mcp?.log?.('info', 'Weather fetched')
928
800
  return weather
929
801
  }
930
802
  ```
931
803
 
932
804
  ## Progress notifications
933
805
 
934
- Long-running tools can report progress to the calling client with `notifications/progress`. Progress is only sent when the caller requested progress tracking by including a `progressToken` in the request (the Mastra `MCPClient` does this when `enableProgressTracking` is set). When no token was sent, `context.mcp.progress()` is a no-op.
806
+ Long-running tools can report progress to the calling client with `notifications/progress`. Progress is only sent when the caller requested progress tracking by including a `progressToken` in the request's `_meta` (the Mastra `MCPClient` does this when `enableProgressTracking` is set). When no token was sent, `context.mcp.progress()` is a no-op.
935
807
 
936
808
  ### `context.mcp.progress()`
937
809
 
@@ -949,7 +821,7 @@ Example:
949
821
  execute: async ({ items }, context) => {
950
822
  for (const [index, item] of items.entries()) {
951
823
  await processItem(item)
952
- await context.mcp.progress({
824
+ await context.mcp?.progress?.({
953
825
  progress: index + 1,
954
826
  total: items.length,
955
827
  message: `Processed ${item.name}`,
@@ -969,7 +841,7 @@ A standalone `MCPServer` with no `mastra` instance produces no spans.
969
841
 
970
842
  ## Notification delivery
971
843
 
972
- Notification methods (`resources.notifyListChanged()`, `prompts.notifyListChanged()`, `toolActions.notifyListChanged()`, and `sendLoggingMessage()`) broadcast to every connected client across all transports: the stdio/SSE connection and each streamable HTTP session. `resources.notifyUpdated()` is the exception: it only notifies clients that subscribed to the resource URI via `resources/subscribe`. Subscriptions are tracked per session for streamable HTTP clients; legacy SSE clients share the main server instance and therefore share one subscription set. Clients using the stateless serverless mode can't receive notifications because each request uses a transient server instance.
844
+ Request-scoped notifications (`context.mcp.log()`, `context.mcp.progress()`) stream inside the request that triggered them, so they reach exactly the caller. Notifications that outlive a request (`resources.notifyListChanged()`, `prompts.notifyListChanged()`, `toolActions.notifyListChanged()`, `resources.notifyUpdated()`) are delivered on the `subscriptions/listen` stream each interested client keeps open. `resources.notifyUpdated()` only reaches clients whose stream includes that resource URI. List-changed notifications reach every client listening for that list. Over stdio the connection itself carries the stream.
973
845
 
974
846
  ## Examples
975
847
 
@@ -977,213 +849,9 @@ For a practical example of packaging a stdio server, see [Publish a stdio server
977
849
 
978
850
  The example at the beginning of this page also demonstrates how to instantiate `MCPServer` with both tools and agents.
979
851
 
980
- ## Elicitation
981
-
982
- ### What's Elicitation?
983
-
984
- Elicitation is a feature in the Model Context Protocol (MCP) that allows servers to request structured information from users. It supports interactive workflows where servers can collect additional data at runtime.
985
-
986
- The `MCPServer` class automatically includes elicitation capabilities. Tools receive a `context.mcp` object in their `execute` function that includes an `elicitation.sendRequest()` method for requesting user input.
987
-
988
- ### Tool Execution Signature
989
-
990
- When tools are executed within an MCP server context, they receive MCP-specific capabilities via the `context.mcp` object:
991
-
992
- ```typescript
993
- execute: async (inputData, context) => {
994
- // input contains the tool's inputData parameters
995
- // context.mcp contains server capabilities like elicitation and authentication info
996
-
997
- // Access authentication information (when available)
998
- if (context.mcp?.extra?.authInfo) {
999
- console.log('Authenticated request from:', context.mcp.extra.authInfo.clientId)
1000
- }
1001
-
1002
- // Use elicitation capabilities
1003
- const result = await context.mcp.elicitation.sendRequest({
1004
- message: 'Please provide information',
1005
- requestedSchema: {/* schema */},
1006
- })
1007
-
1008
- return result
1009
- }
1010
- ```
1011
-
1012
- ### How Elicitation Works
1013
-
1014
- A common use case is during tool execution. When a tool needs user input, it can use the elicitation functionality provided through the context parameter:
1015
-
1016
- 1. The tool calls `context.mcp.elicitation.sendRequest()` with a message and schema
1017
- 2. The request is sent to the connected MCP client
1018
- 3. The client presents the request to the user (via UI, command line, etc.)
1019
- 4. The user provides input, declines, or cancels the request
1020
- 5. The client sends the response back to the server
1021
- 6. The tool receives the response and continues execution
1022
-
1023
- ### Using Elicitation in Tools
1024
-
1025
- Here's an example of a tool that uses elicitation to collect user contact information:
1026
-
1027
- ```typescript
1028
- import { MCPServer } from '@mastra/mcp'
1029
- import { createTool } from '@mastra/core/tools'
1030
- import { z } from 'zod'
1031
-
1032
- const server = new MCPServer({
1033
- id: 'interactive-server',
1034
- name: 'Interactive Server',
1035
- version: '1.0.0',
1036
- tools: {
1037
- collectContactInfo: createTool({
1038
- id: 'collectContactInfo',
1039
- description: 'Collects user contact information through elicitation',
1040
- inputSchema: z.object({
1041
- reason: z.string().optional().describe('Reason for collecting contact info'),
1042
- }),
1043
- execute: async (inputData, context) => {
1044
- const { reason } = inputData
1045
-
1046
- // Log session info if available
1047
- console.log('Request from session:', context.mcp?.extra?.sessionId)
1048
-
1049
- try {
1050
- // Request user input via elicitation
1051
- const result = await context.mcp.elicitation.sendRequest({
1052
- message: reason
1053
- ? `Please provide your contact information. ${reason}`
1054
- : 'Please provide your contact information',
1055
- requestedSchema: {
1056
- type: 'object',
1057
- properties: {
1058
- name: {
1059
- type: 'string',
1060
- title: 'Full Name',
1061
- description: 'Your full name',
1062
- },
1063
- email: {
1064
- type: 'string',
1065
- title: 'Email Address',
1066
- description: 'Your email address',
1067
- format: 'email',
1068
- },
1069
- phone: {
1070
- type: 'string',
1071
- title: 'Phone Number',
1072
- description: 'Your phone number (optional)',
1073
- },
1074
- },
1075
- required: ['name', 'email'],
1076
- },
1077
- })
1078
-
1079
- // Handle the user's response
1080
- if (result.action === 'accept') {
1081
- return `Contact information collected: ${JSON.stringify(result.content, null, 2)}`
1082
- } else if (result.action === 'decline') {
1083
- return 'Contact information collection was declined by the user.'
1084
- } else {
1085
- return 'Contact information collection was cancelled by the user.'
1086
- }
1087
- } catch (error) {
1088
- return `Error collecting contact information: ${error}`
1089
- }
1090
- },
1091
- }),
1092
- },
1093
- })
1094
- ```
1095
-
1096
- ### Elicitation Request Schema
1097
-
1098
- The `requestedSchema` must be a flat object with primitive properties only. Supported types include:
1099
-
1100
- - **String**: `{ type: 'string', title: 'Display Name', description: 'Help text' }`
1101
- - **Number**: `{ type: 'number', minimum: 0, maximum: 100 }`
1102
- - **Boolean**: `{ type: 'boolean', default: false }`
1103
- - **Enum**: `{ type: 'string', enum: ['option1', 'option2'] }`
1104
-
1105
- Example schema:
1106
-
1107
- ```typescript
1108
- {
1109
- type: 'object',
1110
- properties: {
1111
- name: {
1112
- type: 'string',
1113
- title: 'Full Name',
1114
- description: 'Your complete name',
1115
- },
1116
- age: {
1117
- type: 'number',
1118
- title: 'Age',
1119
- minimum: 18,
1120
- maximum: 120,
1121
- },
1122
- newsletter: {
1123
- type: 'boolean',
1124
- title: 'Subscribe to Newsletter',
1125
- default: false,
1126
- },
1127
- },
1128
- required: ['name'],
1129
- }
1130
- ```
1131
-
1132
- ### Response Actions
1133
-
1134
- Users can respond to elicitation requests in three ways:
1135
-
1136
- 1. **Accept** (`action: 'accept'`): User provided data and confirmed submission
1137
- - Contains `content` field with the submitted data
1138
- 2. **Decline** (`action: 'decline'`): User explicitly declined to provide information
1139
- - No content field
1140
- 3. **Cancel** (`action: 'cancel'`): User dismissed the request without deciding
1141
- - No content field
1142
-
1143
- Tools should handle all three response types appropriately.
1144
-
1145
- ### Security Considerations
1146
-
1147
- - **Never request sensitive information** like passwords, SSNs, or credit card numbers
1148
- - Validate all user input against the provided schema
1149
- - Handle declining and cancellation gracefully
1150
- - Provide clear reasons for data collection
1151
- - Respect user privacy and preferences
1152
-
1153
- ### Tool Execution API
1154
-
1155
- The elicitation functionality is available through the `options` parameter in tool execution:
1156
-
1157
- ```typescript
1158
- // Within a tool's execute function
1159
- execute: async (inputData, context) => {
1160
- // Use elicitation for user input
1161
- const result = await context.mcp.elicitation.sendRequest({
1162
- message: string, // Message to display to user
1163
- requestedSchema: object // JSON schema defining expected response structure
1164
- }): Promise<ElicitResult>
1165
-
1166
- // Access authentication info if needed
1167
- if (context.mcp?.extra?.authInfo) {
1168
- // Use context.mcp.extra.authInfo.token, etc.
1169
- }
1170
- }
1171
- ```
1172
-
1173
- Elicitation is **session-aware** when using HTTP-based transports (SSE or HTTP). When multiple clients are connected to the same server, elicitation requests are routed to the client session that initiated the tool execution.
1174
-
1175
- The `ElicitResult` type:
1176
-
1177
- ```typescript
1178
- type ElicitResult = {
1179
- action: 'accept' | 'decline' | 'cancel'
1180
- content?: any // Only present when action is 'accept'
1181
- }
1182
- ```
1183
-
1184
852
  ## OAuth protection
1185
853
 
1186
- To protect your MCP server with OAuth authentication per the [MCP Auth Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization), use the `createOAuthMiddleware` function:
854
+ To protect your MCP server with OAuth authentication per the [MCP Authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization), use the `createOAuthMiddleware` function:
1187
855
 
1188
856
  ```typescript
1189
857
  import http from 'node:http'
@@ -1268,6 +936,8 @@ const customMiddleware = createOAuthMiddleware({
1268
936
  })
1269
937
  ```
1270
938
 
939
+ Return `subject` from your validator when you can. The server binds `input_required` continuations to it, so users sharing one OAuth client never share a continuation.
940
+
1271
941
  ### OAuth Middleware Options
1272
942
 
1273
943
  **oauth.resource** (`string`): The canonical URL of your MCP server. This is returned in Protected Resource Metadata.
@@ -1284,19 +954,19 @@ const customMiddleware = createOAuthMiddleware({
1284
954
 
1285
955
  ## Authentication context
1286
956
 
1287
- Tools can access request metadata via `context.mcp.extra` when using HTTP-based transports. You can pass authentication info and user context, as well as custom data from your HTTP middleware to your MCP tools.
957
+ Tools can access request metadata via `context.mcp.extra` when using the HTTP transport. You can pass authentication info and user context, as well as custom data from your HTTP middleware to your MCP tools.
1288
958
 
1289
959
  ### How it works
1290
960
 
1291
- Whatever you set on `req.auth` in your HTTP middleware becomes available as `context.mcp.extra.authInfo` in your tools:
961
+ Whatever you set on `req.auth` in your HTTP middleware becomes available as `context.mcp.extra.authInfo` in your tools and as `requestContext.get('authInfo')` in resource and prompt callbacks:
1292
962
 
1293
963
  ```text
1294
- req.auth = { ... } → context?.mcp?.extra?.authInfo.extra = { ... }
964
+ req.auth = { ... } → context.mcp.extra.authInfo = { ... }
1295
965
  ```
1296
966
 
1297
967
  ### Map auth data for FGA
1298
968
 
1299
- When an `MCPServer` is registered on a Mastra instance with a fine-grained authorization (FGA) provider, Mastra checks `requestContext.get('user')` before listing or calling tools. HTTP MCP transports pass authenticated data as `extra.authInfo`, so use `mapAuthInfoToUser` to set the user shape expected by your FGA provider.
969
+ When an `MCPServer` is registered on a Mastra instance with a fine-grained authorization (FGA) provider, Mastra checks `requestContext.get('user')` before listing or calling tools. The HTTP transport passes authenticated data as `extra.authInfo`, so use `mapAuthInfoToUser` to set the user shape expected by your FGA provider.
1300
970
 
1301
971
  ```typescript
1302
972
  const server = new MCPServer({
@@ -1408,7 +1078,7 @@ app.use('/mcp', async (req, res, next) => {
1408
1078
  scopes: user.scopes,
1409
1079
  expiresAt: user.expiresAt,
1410
1080
  extra: {
1411
- userId: user.userId,
1081
+ sub: user.userId,
1412
1082
  email: user.email,
1413
1083
  },
1414
1084
  }
@@ -1424,6 +1094,8 @@ app.all('/mcp', async (req, res) => {
1424
1094
  })
1425
1095
  ```
1426
1096
 
1097
+ Set `extra.sub` (or `extra.subject`) to the user's identifier. The server uses it to bind `input_required` continuations to that user.
1098
+
1427
1099
  ### Accessing Auth Data in Tools
1428
1100
 
1429
1101
  The `req.auth` object is available as `context.mcp.extra.authInfo` in your tool's execute function:
@@ -1431,19 +1103,19 @@ The `req.auth` object is available as `context.mcp.extra.authInfo` in your tool'
1431
1103
  ```typescript
1432
1104
  execute: async (inputData, context) => {
1433
1105
  // Access the auth data you set in middleware
1434
- const authInfo = context?.mcp?.extra?.authInfo
1106
+ const authInfo = context.mcp?.extra.authInfo
1435
1107
 
1436
- if (!authInfo?.extra?.userId) {
1108
+ if (!authInfo?.extra?.sub) {
1437
1109
  return { error: 'Authentication required' }
1438
1110
  }
1439
1111
 
1440
1112
  // Use the auth data
1441
- console.log('User ID:', authInfo.extra.userId)
1113
+ console.log('User ID:', authInfo.extra.sub)
1442
1114
  console.log('Email:', authInfo.extra.email)
1443
1115
 
1444
1116
  const response = await fetch('/api/data', {
1445
1117
  headers: { Authorization: `Bearer ${authInfo.token}` },
1446
- signal: context?.mcp?.extra?.signal,
1118
+ signal: context.mcp?.extra.signal,
1447
1119
  })
1448
1120
 
1449
1121
  return response.json()
@@ -1452,28 +1124,23 @@ execute: async (inputData, context) => {
1452
1124
 
1453
1125
  ### Passing `RequestContext` through to agent
1454
1126
 
1127
+ `context.requestContext` already carries `authInfo` and the mapped `user`, so pass it straight through when a tool calls an agent:
1128
+
1455
1129
  ```typescript
1456
1130
  execute: async (inputData, context) => {
1457
- // Access the auth data you set in middleware
1458
- const authInfo = context?.mcp?.extra?.authInfo
1459
-
1460
- const requestContext = context.requestContext || new RequestContext().set('someKey', authInfo)
1131
+ const authInfo = context.mcp?.extra.authInfo
1461
1132
 
1462
- if (!authInfo?.extra?.userId) {
1133
+ if (!authInfo?.extra?.sub) {
1463
1134
  return { error: 'Authentication required' }
1464
1135
  }
1465
1136
 
1466
- // Use the auth data
1467
- console.log('User ID:', authInfo.extra.userId)
1468
- console.log('Email:', authInfo.extra.email)
1469
-
1470
- const agent = context?.mastra?.getAgentById('some-agent-id')
1137
+ const agent = context.mastra?.getAgentById('some-agent-id')
1471
1138
 
1472
1139
  if (!agent) {
1473
1140
  return { error: "Agent 'some-agent-id' not found" }
1474
1141
  }
1475
1142
 
1476
- const response = await agent.generate(prompt, { requestContext })
1143
+ const response = await agent.generate(prompt, { requestContext: context.requestContext })
1477
1144
 
1478
1145
  return response.text
1479
1146
  }
@@ -1483,13 +1150,13 @@ execute: async (inputData, context) => {
1483
1150
 
1484
1151
  The full `context.mcp.extra` object contains:
1485
1152
 
1486
- | Property | Description |
1487
- | ------------------ | ------------------------------------------------- |
1488
- | `authInfo` | Whatever you set on `req.auth` in your middleware |
1489
- | `sessionId` | Session identifier for the MCP connection |
1490
- | `signal` | AbortSignal for request cancellation |
1491
- | `sendNotification` | MCP protocol function for sending notifications |
1492
- | `sendRequest` | MCP protocol function for sending requests |
1153
+ | Property | Description |
1154
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1155
+ | `authInfo` | Whatever you set on `req.auth` in your middleware |
1156
+ | `requestId` | The JSON-RPC id of the current request |
1157
+ | `signal` | AbortSignal for request cancellation |
1158
+ | `_meta` | Request metadata sent by the client: W3C trace fields, the `io.modelcontextprotocol/logLevel` log-level opt-in, and `progressToken` |
1159
+ | `sendNotification`, `sendRequest` | Deprecated. The protocol has no server-initiated requests, so both throw with a message naming the replacement: `context.mcp.log()`, `context.mcp.progress()`, or `context.suspend()`. |
1493
1160
 
1494
1161
  ### Complete Example
1495
1162
 
@@ -1582,15 +1249,15 @@ const getUserData = createTool({
1582
1249
  description: 'Fetches data for the authenticated user',
1583
1250
  inputSchema: z.object({}),
1584
1251
  execute: async (inputData, context) => {
1585
- const authInfo = context?.mcp?.extra?.authInfo
1252
+ const authInfo = context.mcp?.extra.authInfo
1586
1253
 
1587
- if (!authInfo?.extra?.userId) {
1254
+ if (!authInfo?.extra?.sub) {
1588
1255
  return { error: 'Authentication required' }
1589
1256
  }
1590
1257
 
1591
1258
  // Access the data you set in middleware
1592
1259
  return {
1593
- userId: authInfo.extra.userId,
1260
+ userId: authInfo.extra.sub,
1594
1261
  email: authInfo.extra.email,
1595
1262
  }
1596
1263
  },
@@ -1628,7 +1295,7 @@ app.use('/mcp', async (req, res, next) => {
1628
1295
  scopes: user.scopes,
1629
1296
  expiresAt: user.expiresAt,
1630
1297
  extra: {
1631
- userId: user.userId,
1298
+ sub: user.userId,
1632
1299
  email: user.email,
1633
1300
  },
1634
1301
  }
@@ -1712,4 +1379,5 @@ Link a tool to its app resource by setting `mcp._meta.ui.resourceUri` in `create
1712
1379
  ## Related information
1713
1380
 
1714
1381
  - For connecting to MCP servers in Mastra, see the [MCPClient documentation](https://mastra.ai/reference/tools/mcp-client).
1382
+ - Migrating from `@mastra/mcp` 1.x: [Migrate @mastra/mcp from v1 to v2](https://mastra.ai/reference/migrations/mcp-v2).
1715
1383
  - For more about the Model Context Protocol, see the [@modelcontextprotocol/sdk documentation](https://github.com/modelcontextprotocol/typescript-sdk).