Skip to content

Commit 80e1484

Browse files
Merge pull request #151 from modelcontextprotocol/justin/server-auth
Server implementation of MCP auth
2 parents 506ae06 + f0777d2 commit 80e1484

29 files changed

+4038
-404
lines changed

Diff for: package-lock.json

+707-315
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+9-3
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,30 @@
4747
},
4848
"dependencies": {
4949
"content-type": "^1.0.5",
50+
"cors": "^2.8.5",
5051
"eventsource": "^3.0.2",
52+
"express": "^5.0.1",
53+
"express-rate-limit": "^7.5.0",
5154
"pkce-challenge": "^4.1.0",
5255
"raw-body": "^3.0.0",
5356
"zod": "^3.23.8",
5457
"zod-to-json-schema": "^3.24.1"
5558
},
5659
"devDependencies": {
5760
"@eslint/js": "^9.8.0",
61+
"@jest-mock/express": "^3.0.0",
5862
"@types/content-type": "^1.1.8",
63+
"@types/cors": "^2.8.17",
5964
"@types/eslint__js": "^8.42.3",
6065
"@types/eventsource": "^1.1.15",
61-
"@types/express": "^4.17.21",
66+
"@types/express": "^5.0.0",
6267
"@types/jest": "^29.5.12",
6368
"@types/node": "^22.0.2",
69+
"@types/supertest": "^6.0.2",
6470
"@types/ws": "^8.5.12",
6571
"eslint": "^9.8.0",
66-
"express": "^4.19.2",
6772
"jest": "^29.7.0",
73+
"supertest": "^7.0.0",
6874
"ts-jest": "^29.2.4",
6975
"tsx": "^4.16.5",
7076
"typescript": "^5.5.4",
@@ -74,4 +80,4 @@
7480
"resolutions": {
7581
"strip-ansi": "6.0.1"
7682
}
77-
}
83+
}

Diff for: src/client/auth.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
registerClient,
77
} from "./auth.js";
88

9-
109
// Mock fetch globally
1110
const mockFetch = jest.fn();
1211
global.fetch = mockFetch;

Diff for: src/client/auth.ts

+8-84
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,7 @@
11
import pkceChallenge from "pkce-challenge";
2-
import { z } from "zod";
32
import { LATEST_PROTOCOL_VERSION } from "../types.js";
4-
5-
export const OAuthMetadataSchema = z
6-
.object({
7-
issuer: z.string(),
8-
authorization_endpoint: z.string(),
9-
token_endpoint: z.string(),
10-
registration_endpoint: z.string().optional(),
11-
scopes_supported: z.array(z.string()).optional(),
12-
response_types_supported: z.array(z.string()),
13-
response_modes_supported: z.array(z.string()).optional(),
14-
grant_types_supported: z.array(z.string()).optional(),
15-
token_endpoint_auth_methods_supported: z.array(z.string()).optional(),
16-
token_endpoint_auth_signing_alg_values_supported: z
17-
.array(z.string())
18-
.optional(),
19-
service_documentation: z.string().optional(),
20-
revocation_endpoint: z.string().optional(),
21-
revocation_endpoint_auth_methods_supported: z.array(z.string()).optional(),
22-
revocation_endpoint_auth_signing_alg_values_supported: z
23-
.array(z.string())
24-
.optional(),
25-
introspection_endpoint: z.string().optional(),
26-
introspection_endpoint_auth_methods_supported: z
27-
.array(z.string())
28-
.optional(),
29-
introspection_endpoint_auth_signing_alg_values_supported: z
30-
.array(z.string())
31-
.optional(),
32-
code_challenge_methods_supported: z.array(z.string()).optional(),
33-
})
34-
.passthrough();
35-
36-
export const OAuthTokensSchema = z
37-
.object({
38-
access_token: z.string(),
39-
token_type: z.string(),
40-
expires_in: z.number().optional(),
41-
scope: z.string().optional(),
42-
refresh_token: z.string().optional(),
43-
})
44-
.strip();
45-
46-
/**
47-
* Client metadata schema according to RFC 7591 OAuth 2.0 Dynamic Client Registration
48-
*/
49-
export const OAuthClientMetadataSchema = z.object({
50-
redirect_uris: z.array(z.string()),
51-
token_endpoint_auth_method: z.string().optional(),
52-
grant_types: z.array(z.string()).optional(),
53-
response_types: z.array(z.string()).optional(),
54-
client_name: z.string().optional(),
55-
client_uri: z.string().optional(),
56-
logo_uri: z.string().optional(),
57-
scope: z.string().optional(),
58-
contacts: z.array(z.string()).optional(),
59-
tos_uri: z.string().optional(),
60-
policy_uri: z.string().optional(),
61-
jwks_uri: z.string().optional(),
62-
jwks: z.any().optional(),
63-
software_id: z.string().optional(),
64-
software_version: z.string().optional(),
65-
}).passthrough();
66-
67-
/**
68-
* Client information response schema according to RFC 7591
69-
*/
70-
export const OAuthClientInformationSchema = z.object({
71-
client_id: z.string(),
72-
client_secret: z.string().optional(),
73-
client_id_issued_at: z.number().optional(),
74-
client_secret_expires_at: z.number().optional(),
75-
}).passthrough();
76-
77-
export type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>;
78-
export type OAuthTokens = z.infer<typeof OAuthTokensSchema>;
79-
80-
export type OAuthClientMetadata = z.infer<typeof OAuthClientMetadataSchema>;
81-
export type OAuthClientInformation = z.infer<typeof OAuthClientInformationSchema>;
3+
import type { OAuthClientMetadata, OAuthClientInformation, OAuthTokens, OAuthMetadata, OAuthClientInformationFull } from "../shared/auth.js";
4+
import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthTokensSchema } from "../shared/auth.js";
825

836
/**
847
* Implements an end-to-end OAuth client to be used with one MCP server.
@@ -113,7 +36,7 @@ export interface OAuthClientProvider {
11336
* This method is not required to be implemented if client information is
11437
* statically known (e.g., pre-registered).
11538
*/
116-
saveClientInformation?(clientInformation: OAuthClientInformation): void | Promise<void>;
39+
saveClientInformation?(clientInformation: OAuthClientInformationFull): void | Promise<void>;
11740

11841
/**
11942
* Loads any existing OAuth tokens for the current session, or returns
@@ -175,12 +98,13 @@ export async function auth(
17598
throw new Error("OAuth client information must be saveable for dynamic registration");
17699
}
177100

178-
clientInformation = await registerClient(serverUrl, {
101+
const fullInformation = await registerClient(serverUrl, {
179102
metadata,
180103
clientMetadata: provider.clientMetadata,
181104
});
182105

183-
await provider.saveClientInformation(clientInformation);
106+
await provider.saveClientInformation(fullInformation);
107+
clientInformation = fullInformation;
184108
}
185109

186110
// Exchange authorization code for tokens
@@ -448,7 +372,7 @@ export async function registerClient(
448372
metadata?: OAuthMetadata;
449373
clientMetadata: OAuthClientMetadata;
450374
},
451-
): Promise<OAuthClientInformation> {
375+
): Promise<OAuthClientInformationFull> {
452376
let registrationUrl: URL;
453377

454378
if (metadata) {
@@ -473,5 +397,5 @@ export async function registerClient(
473397
throw new Error(`Dynamic client registration failed: HTTP ${response.status}`);
474398
}
475399

476-
return OAuthClientInformationSchema.parse(await response.json());
400+
return OAuthClientInformationFullSchema.parse(await response.json());
477401
}

Diff for: src/client/sse.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { createServer, type IncomingMessage, type Server } from "http";
22
import { AddressInfo } from "net";
33
import { JSONRPCMessage } from "../types.js";
44
import { SSEClientTransport } from "./sse.js";
5-
import { OAuthClientProvider, OAuthTokens, UnauthorizedError } from "./auth.js";
5+
import { OAuthClientProvider, UnauthorizedError } from "./auth.js";
6+
import { OAuthTokens } from "../shared/auth.js";
67

78
describe("SSEClientTransport", () => {
89
let server: Server;

Diff for: src/server/auth/clients.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { OAuthClientInformationFull } from "../../shared/auth.js";
2+
3+
/**
4+
* Stores information about registered OAuth clients for this server.
5+
*/
6+
export interface OAuthRegisteredClientsStore {
7+
/**
8+
* Returns information about a registered client, based on its ID.
9+
*/
10+
getClient(clientId: string): OAuthClientInformationFull | undefined | Promise<OAuthClientInformationFull | undefined>;
11+
12+
/**
13+
* Registers a new client with the server. The client ID and secret will be automatically generated by the library. A modified version of the client information can be returned to reflect specific values enforced by the server.
14+
*
15+
* NOTE: Implementations should NOT delete expired client secrets in-place. Auth middleware provided by this library will automatically check the `client_secret_expires_at` field and reject requests with expired secrets. Any custom logic for authenticating clients should check the `client_secret_expires_at` field as well.
16+
*
17+
* If unimplemented, dynamic client registration is unsupported.
18+
*/
19+
registerClient?(client: OAuthClientInformationFull): OAuthClientInformationFull | Promise<OAuthClientInformationFull>;
20+
}

0 commit comments

Comments
 (0)