Skip to content

Commit 9800cc2

Browse files
committed
fix(auth): correct error propagation and authorization checks
1 parent ccb78f2 commit 9800cc2

21 files changed

Lines changed: 886 additions & 89 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ Next steps:
136136

137137
- Local SDK docs:
138138
- [docs/server.md](docs/server.md) – building MCP servers, transports, tools/resources/prompts, sampling, elicitation, tasks, and deployment patterns.
139+
- [docs/auth.md](docs/auth.md) – implementing authentication and authorization in MCP servers.
139140
- [docs/client.md](docs/client.md) – building MCP clients: connecting, tools, resources, prompts, server-initiated requests, and error handling
140141
- [docs/faq.md](docs/faq.md) – frequently asked questions and troubleshooting
141142
- External references:

docs/auth.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Authentication and Authorization
2+
3+
The MCP TypeScript SDK provides optional, opt-in support for authentication (AuthN) and authorization (AuthZ). This enables you to protect your MCP server resources, tools, and prompts using industry-standard schemes like OAuth 2.1 Bearer tokens.
4+
5+
## Key Concepts
6+
7+
- **Authenticator**: Responsible for extracting and validating authentication information from an incoming request.
8+
- **AuthInfo**: A structure containing information about the authenticated entity (e.g., user name, active scopes).
9+
- **Authorizer**: Used by the MCP server to verify if the authenticated entity has the required scopes to access a specific resource, tool, or prompt.
10+
- **Scopes**: Optional strings associated with registered items that define the required permissions.
11+
12+
## Implementing Authentication
13+
14+
To enable authentication, provide an `authenticator` in the `ServerOptions` when creating your server.
15+
16+
### Using Bearer Token Authentication
17+
18+
The SDK includes a `BearerTokenAuthenticator` for validating OAuth 2.1 Bearer tokens.
19+
20+
```typescript
21+
import { McpServer, BearerTokenAuthenticator } from "@modelcontextprotocol/server";
22+
23+
const server = new McpServer({
24+
name: "my-authenticated-server",
25+
version: "1.0.0",
26+
}, {
27+
authenticator: new BearerTokenAuthenticator({
28+
validate: async (token) => {
29+
// Validate the token (e.g., verify with an OAuth provider)
30+
if (token === "valid-token") {
31+
return {
32+
name: "john_doe",
33+
scopes: ["read:resources", "execute:tools"]
34+
};
35+
}
36+
return undefined; // Invalid token
37+
}
38+
})
39+
});
40+
```
41+
42+
## Implementing Authorization
43+
44+
Authorization is enforced using the `scopes` property when registering tools, resources, or prompts.
45+
46+
### Scoped Tools
47+
48+
```typescript
49+
server.tool(
50+
"secure_tool",
51+
{
52+
description: "A tool that requires specific scopes",
53+
scopes: ["execute:tools"]
54+
},
55+
async (args) => {
56+
return { content: [{ type: "text", text: "Success!" }] };
57+
}
58+
);
59+
```
60+
61+
### Scoped Resources
62+
63+
```typescript
64+
server.resource(
65+
"secure_resource",
66+
"secure://data",
67+
{ scopes: ["read:resources"] },
68+
async (uri) => {
69+
return { contents: [{ uri: uri.href, text: "Top secret data" }] };
70+
}
71+
);
72+
```
73+
74+
## Middleware Support
75+
76+
For framework-specific integrations, use the provided middleware to pre-authenticate requests.
77+
78+
### Express Middleware
79+
80+
```typescript
81+
import express from "express";
82+
import { auth } from "@modelcontextprotocol/express";
83+
84+
const app = express();
85+
app.use(auth({ authenticator }));
86+
87+
app.post("/mcp", (req, res) => {
88+
// req.auth is now populated
89+
transport.handleRequest(req, res);
90+
});
91+
```
92+
93+
### Hono Middleware
94+
95+
```typescript
96+
import { Hono } from "hono";
97+
import { auth } from "@modelcontextprotocol/hono";
98+
99+
const app = new Hono();
100+
app.use("/mcp/*", auth({ authenticator }));
101+
102+
app.all("/mcp", async (c) => {
103+
const authInfo = c.get("mcpAuthInfo");
104+
return transport.handleRequest(c.req.raw, { authInfo });
105+
});
106+
```
107+
108+
## Error Handling
109+
110+
- **401 Unauthorized**: Returned when authentication is required but missing or invalid. Includes `WWW-Authenticate: Bearer` header.
111+
- **403 Forbidden**: Returned when the authenticated entity lacks the required scopes.

docs/server.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Building a server takes three steps:
1111
1. Create an {@linkcode @modelcontextprotocol/server!server/mcp.McpServer | McpServer} and register your [tools, resources, and prompts](#tools-resources-and-prompts).
1212
2. Create a transport — [Streamable HTTP](#streamable-http) for remote servers or [stdio](#stdio) for local, process‑spawned integrations.
1313
3. Wire the transport into your HTTP framework (or use stdio directly) and call `server.connect(transport)`.
14+
1. (Optional) Configure [authentication and authorization](#authentication-and-authorization) to protect your server.
1415

1516
The sections below cover each of these. For a feature‑rich starting point, see [`simpleStreamableHttp.ts`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/src/simpleStreamableHttp.ts) — remove what you don't need and register your own tools, resources, and prompts. For stateless or JSON‑response‑mode alternatives, see the examples linked in [Transports](#transports) below.
1617

@@ -444,6 +445,27 @@ Task-based execution enables "call-now, fetch-later" patterns for long-running o
444445
> [!WARNING]
445446
> The tasks API is experimental and may change without notice.
446447
448+
## Authentication and Authorization
449+
450+
The MCP TypeScript SDK provides optional, opt-in support for authentication (AuthN) and authorization (AuthZ). For a comprehensive guide, see the [Authentication and Authorization guide](./auth.md).
451+
452+
Quick example:
453+
454+
```ts
455+
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, {
456+
authenticator: new BearerTokenAuthenticator({
457+
validate: async (token) => {
458+
if (token === 'secret') return { name: 'admin', scopes: ['all'] };
459+
return undefined;
460+
}
461+
})
462+
});
463+
464+
server.tool('secure-tool', { scopes: ['all'] }, async (args) => {
465+
return { content: [{ type: 'text', text: 'Success' }] };
466+
});
467+
```
468+
447469
## Deployment
448470

449471
### DNS rebinding protection

packages/core/src/shared/protocol.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -704,16 +704,24 @@ export abstract class Protocol<ContextT extends BaseContext> {
704704
};
705705

706706
const _onmessage = this._transport?.onmessage;
707-
this._transport.onmessage = (message, extra) => {
708-
_onmessage?.(message, extra);
709-
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
710-
this._onresponse(message);
711-
} else if (isJSONRPCRequest(message)) {
712-
this._onrequest(message, extra);
713-
} else if (isJSONRPCNotification(message)) {
714-
this._onnotification(message);
715-
} else {
716-
this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`));
707+
this._transport.onmessage = async (message, extra) => {
708+
try {
709+
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
710+
await _onmessage?.(message, extra);
711+
this._onresponse(message);
712+
} else if (isJSONRPCRequest(message)) {
713+
await this._onrequest(message, extra);
714+
} else if (isJSONRPCNotification(message)) {
715+
await this._onnotification(message);
716+
} else {
717+
await _onmessage?.(message, extra);
718+
this._onerror(new Error(`Unknown message type: ${JSON.stringify(message)}`));
719+
}
720+
} catch (error) {
721+
if (error instanceof ProtocolError && (error.message.includes('Unauthorized') || error.message.includes('Forbidden'))) {
722+
throw error;
723+
}
724+
this._onerror(error instanceof Error ? error : new Error(String(error)));
717725
}
718726
};
719727

@@ -758,7 +766,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
758766
.catch(error => this._onerror(new Error(`Uncaught error in notification handler: ${error}`)));
759767
}
760768

761-
private _onrequest(request: JSONRPCRequest, extra?: MessageExtraInfo): void {
769+
protected async _onrequest(request: JSONRPCRequest, extra?: MessageExtraInfo): Promise<void> {
762770
const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
763771

764772
// Capture the current transport at request time to ensure responses go to the correct client
@@ -838,7 +846,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
838846
const ctx = this.buildContext(baseCtx, extra);
839847

840848
// Starting with Promise.resolve() puts any synchronous errors into the monad as well.
841-
Promise.resolve()
849+
return Promise.resolve()
842850
.then(() => {
843851
// If this request asked for task creation, check capability first
844852
if (taskCreationParams) {
@@ -879,6 +887,10 @@ export abstract class Protocol<ContextT extends BaseContext> {
879887
return;
880888
}
881889

890+
if (error instanceof ProtocolError && (error.message.includes('Unauthorized') || error.message.includes('Forbidden'))) {
891+
throw error;
892+
}
893+
882894
const errorResponse: JSONRPCErrorResponse = {
883895
jsonrpc: '2.0',
884896
id: request.id,
@@ -903,7 +915,13 @@ export abstract class Protocol<ContextT extends BaseContext> {
903915
: capturedTransport?.send(errorResponse));
904916
}
905917
)
906-
.catch(error => this._onerror(new Error(`Failed to send response: ${error}`)))
918+
.catch(error => {
919+
if (error instanceof ProtocolError && (error.message.includes('Unauthorized') || error.message.includes('Forbidden'))) {
920+
throw error;
921+
}
922+
// Do not report as protocol error if it's already an auth error we're escaping
923+
this._onerror(new Error(`Failed to send response: ${error}`));
924+
})
907925
.finally(() => {
908926
this._requestHandlerAbortControllers.delete(request.id);
909927
});

packages/core/src/shared/transport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export interface Transport {
114114
*
115115
* The {@linkcode MessageExtraInfo.requestInfo | requestInfo} can be used to get the original request information (headers, etc.)
116116
*/
117-
onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void;
117+
onmessage?: <T extends JSONRPCMessage>(message: T, extra?: MessageExtraInfo) => void | Promise<void>;
118118

119119
/**
120120
* The session ID generated for this connection.

packages/middleware/express/src/express.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Express } from 'express';
22
import express from 'express';
33

44
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
5+
export { auth } from './middleware/auth.js';
6+
export type { AuthMiddlewareOptions } from './middleware/auth.js';
57

68
/**
79
* Options for creating an MCP Express application.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './express.js';
2+
export { auth } from './middleware/auth.js';
23
export * from './middleware/hostHeaderValidation.js';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { Authenticator, AuthInfo } from '@modelcontextprotocol/server';
3+
4+
/**
5+
* Options for the MCP Express authentication middleware.
6+
*/
7+
export interface AuthMiddlewareOptions {
8+
/**
9+
* The authenticator to use for validating requests.
10+
*/
11+
authenticator: Authenticator;
12+
}
13+
14+
/**
15+
* Creates an Express middleware for MCP authentication.
16+
*
17+
* This middleware extracts authentication information from the request using the provided authenticator
18+
* and attaches it to the request object as `req.auth`. The MCP Express transport will then
19+
* pick up this information automatically.
20+
*
21+
* @param options - Middleware options
22+
* @returns An Express middleware function
23+
*
24+
* @example
25+
* ```ts
26+
* const authenticator = new BearerTokenAuthenticator({
27+
* validate: async (token) => ({ name: 'user', scopes: ['read'] })
28+
* });
29+
* app.use(auth({ authenticator }));
30+
* ```
31+
*/
32+
export function auth(options: AuthMiddlewareOptions) {
33+
return async (req: Request & { auth?: AuthInfo }, res: Response, next: NextFunction) => {
34+
try {
35+
const headers: Record<string, string> = {};
36+
for (const [key, value] of Object.entries(req.headers)) {
37+
if (typeof value === 'string') {
38+
headers[key] = value;
39+
} else if (Array.isArray(value)) {
40+
headers[key] = value.join(', ');
41+
}
42+
}
43+
44+
const authInfo = await options.authenticator.authenticate({
45+
method: req.method,
46+
headers,
47+
});
48+
if (authInfo) {
49+
req.auth = authInfo;
50+
}
51+
next();
52+
} catch (error) {
53+
// If authentication fails, we let the MCP server handle it later,
54+
// or the developer can choose to reject here.
55+
// By default, we just proceed to allow the MCP server to decide (e.g., if auth is optional).
56+
next();
57+
}
58+
};
59+
}

packages/middleware/hono/src/hono.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Context } from 'hono';
22
import { Hono } from 'hono';
33

44
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
5+
export { auth } from './middleware/auth.js';
6+
export type { AuthMiddlewareOptions } from './middleware/auth.js';
57

68
/**
79
* Options for creating an MCP Hono application.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './hono.js';
2+
export { auth } from './middleware/auth.js';
23
export * from './middleware/hostHeaderValidation.js';

0 commit comments

Comments
 (0)