Skip to content

Commit 66456ec

Browse files
committed
feat(oauth): Integrate OAuth support into the MCP server
- Added new environment variables for OAuth configuration in the environment schema. - Enhanced CLI options to include `--enable-oauth` for enabling OAuth endpoints. - Updated TransportManager to handle OAuth configuration and registration of OAuth router. - Integrated ProxyOAuthServerProvider for managing OAuth token validation and client retrieval. - Refactored HTTP server to register OAuth router when enabled, improving security and flexibility in transport management. - Updated TypeScript configuration to include new types for OAuth support.
1 parent 77d3562 commit 66456ec

File tree

12 files changed

+635
-15
lines changed

12 files changed

+635
-15
lines changed

package-lock.json

Lines changed: 464 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@
6060
"@commander-js/extra-typings": "^14.0.0",
6161
"@confluentinc/kafka-javascript": "^1.3.1",
6262
"@confluentinc/schemaregistry": "^1.3.1",
63+
"@fastify/express": "^4.0.2",
6364
"@fastify/swagger": "^9.5.1",
6465
"@fastify/swagger-ui": "^5.2.2",
6566
"@modelcontextprotocol/sdk": "^1.12.0",
67+
"@types/express": "^5.0.2",
6668
"commander": "^14.0.0",
6769
"dotenv": "^16.5.0",
6870
"fastify": "^5.3.3",

src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface CLIOptions {
1717
listTools?: boolean;
1818
disableConfluentCloudTools?: boolean;
1919
kafkaConfig: KeyValuePairObject;
20+
enableOAuth?: boolean;
2021
}
2122

2223
/**
@@ -139,6 +140,7 @@ export function parseCliArgs(): CLIOptions {
139140
"--disable-confluent-cloud-tools",
140141
"Disable all tools that require Confluent Cloud REST APIs (cloud-only tools).",
141142
)
143+
.option("--enable-oauth", "Enable OAuth endpoints for HTTP/SSE transports")
142144
.allowExcessArguments(false)
143145
.exitOverride();
144146

@@ -174,6 +176,7 @@ export function parseCliArgs(): CLIOptions {
174176
listTools: !!opts.listTools,
175177
disableConfluentCloudTools: !!opts.disableConfluentCloudTools,
176178
kafkaConfig: kafkaConfig,
179+
enableOAuth: !!opts.enableOauth,
177180
};
178181
} catch (error: unknown) {
179182
if (

src/confluent/tools/handlers/environments/read-environment-handler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import {
44
BaseToolHandler,
55
ToolConfig,
66
} from "@src/confluent/tools/base-tools.js";
7+
import {
8+
Environment,
9+
environmentSchema,
10+
} from "@src/confluent/tools/handlers/environments/list-environments-handler.js";
711
import { ToolName } from "@src/confluent/tools/tool-name.js";
812
import { EnvVar } from "@src/env-schema.js";
913
import env from "@src/env.js";
1014
import { logger } from "@src/logger.js";
1115
import { wrapAsPathBasedClient } from "openapi-fetch";
1216
import { z } from "zod";
13-
import { Environment, environmentSchema } from "./list-environments-handler.js";
1417

1518
const readEnvironmentArguments = z.object({
1619
baseUrl: z

src/env-schema.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,36 @@ const envSchema = z.object({
8383
.trim()
8484
.min(1)
8585
.optional(),
86+
OAUTH_AUTHORIZATION_URL: z
87+
.string()
88+
.url()
89+
.describe("OAuth2 authorization endpoint URL"),
90+
OAUTH_TOKEN_URL: z.string().url().describe("OAuth2 token endpoint URL"),
91+
OAUTH_REVOCATION_URL: z
92+
.string()
93+
.url()
94+
.describe("OAuth2 revocation endpoint URL"),
95+
OAUTH_ISSUER_URL: z.string().url().describe("OAuth2 issuer URL"),
96+
OAUTH_BASE_URL: z.string().url().describe("OAuth2 base URL for this service"),
97+
OAUTH_DOCS_URL: z.string().url().describe("OAuth2 service documentation URL"),
98+
OAUTH_REDIRECT_URI: z
99+
.string()
100+
.url()
101+
.describe("OAuth2 redirect URI for client registration"),
102+
OAUTH_CLIENT_ID: z
103+
.string()
104+
.min(1)
105+
.describe("OAuth2 client ID for token validation"),
106+
OAUTH_SCOPES: z
107+
.string()
108+
.min(1)
109+
.transform((val) =>
110+
val
111+
.split(",")
112+
.map((s) => s.trim())
113+
.filter(Boolean),
114+
)
115+
.describe("Comma-separated OAuth2 scopes for token validation"),
86116
});
87117

88118
// Environment variables that are optional for tools / could be provided at runtime

src/index.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env node
22

33
import { GlobalConfig } from "@confluentinc/kafka-javascript";
4+
import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
5+
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
46
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
57
import {
68
getFilteredToolNames,
@@ -139,7 +141,54 @@ async function main() {
139141
);
140142
});
141143

142-
const transportManager = new TransportManager(server);
144+
const transportManager = new TransportManager(server, {
145+
enableOAuth: cliOptions.enableOAuth,
146+
oauthConfig: {
147+
OAUTH_AUTHORIZATION_URL: env.OAUTH_AUTHORIZATION_URL,
148+
OAUTH_TOKEN_URL: env.OAUTH_TOKEN_URL,
149+
OAUTH_REVOCATION_URL: env.OAUTH_REVOCATION_URL,
150+
OAUTH_ISSUER_URL: env.OAUTH_ISSUER_URL,
151+
OAUTH_BASE_URL: env.OAUTH_BASE_URL,
152+
OAUTH_DOCS_URL: env.OAUTH_DOCS_URL,
153+
OAUTH_REDIRECT_URI: env.OAUTH_REDIRECT_URI,
154+
OAUTH_CLIENT_ID: env.OAUTH_CLIENT_ID,
155+
OAUTH_SCOPES: env.OAUTH_SCOPES,
156+
},
157+
});
158+
159+
const proxyProvider = new ProxyOAuthServerProvider({
160+
endpoints: {
161+
authorizationUrl: env.OAUTH_AUTHORIZATION_URL,
162+
tokenUrl: env.OAUTH_TOKEN_URL,
163+
revocationUrl: env.OAUTH_REVOCATION_URL,
164+
},
165+
verifyAccessToken: async (token) => {
166+
return {
167+
token,
168+
clientId: env.OAUTH_CLIENT_ID,
169+
scopes: env.OAUTH_SCOPES,
170+
};
171+
},
172+
getClient: async (client_id) => {
173+
return {
174+
client_id,
175+
redirect_uris: [env.OAUTH_REDIRECT_URI],
176+
};
177+
},
178+
});
179+
180+
const oauthRouter = mcpAuthRouter({
181+
provider: proxyProvider,
182+
issuerUrl: new URL(env.OAUTH_ISSUER_URL),
183+
baseUrl: new URL(env.OAUTH_BASE_URL),
184+
serviceDocumentationUrl: new URL(env.OAUTH_DOCS_URL),
185+
});
186+
187+
// Register the OAuth router on the Fastify server (if HTTP server is used)
188+
const httpServer = transportManager.getHttpServer();
189+
if (httpServer) {
190+
await httpServer.registerOAuthRouter(oauthRouter);
191+
}
143192

144193
// Start all transports with a single call
145194
logger.info(`Starting transports: ${cliOptions.transports.join(", ")}`);

src/mcp/transports/http.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
33
import { logger } from "@src/logger.js";
4+
import { HttpServer } from "@src/mcp/transports/server.js";
5+
import { Transport } from "@src/mcp/transports/types.js";
46
import { randomUUID } from "crypto";
57
import { FastifyReply, FastifyRequest } from "fastify";
6-
import { HttpServer } from "./server.js";
7-
import { Transport } from "./types.js";
88

99
interface McpRequestHeaders {
1010
"mcp-session-id"?: string;

src/mcp/transports/manager.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,41 @@
1+
import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
2+
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
13
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
import { logger } from "@src/logger.js";
3-
import { HttpTransport } from "./http.js";
4-
import { HttpServer } from "./server.js";
5-
import { SseTransport } from "./sse.js";
6-
import { StdioTransport } from "./stdio.js";
7-
import { Transport, TransportType } from "./types.js";
5+
import { HttpTransport } from "@src/mcp/transports/http.js";
6+
import { HttpServer } from "@src/mcp/transports/server.js";
7+
import { SseTransport } from "@src/mcp/transports/sse.js";
8+
import { StdioTransport } from "@src/mcp/transports/stdio.js";
9+
import { Transport, TransportType } from "@src/mcp/transports/types.js";
10+
11+
export type OAuthEnv = {
12+
OAUTH_AUTHORIZATION_URL: string;
13+
OAUTH_TOKEN_URL: string;
14+
OAUTH_REVOCATION_URL: string;
15+
OAUTH_ISSUER_URL: string;
16+
OAUTH_BASE_URL: string;
17+
OAUTH_DOCS_URL: string;
18+
OAUTH_REDIRECT_URI: string;
19+
OAUTH_CLIENT_ID: string;
20+
OAUTH_SCOPES: string[];
21+
};
22+
23+
type TransportManagerOptions = {
24+
enableOAuth?: boolean;
25+
oauthConfig?: OAuthEnv;
26+
};
827

928
export class TransportManager {
1029
private transports: Map<TransportType, Transport> = new Map();
1130
private httpServer: HttpServer | null = null;
31+
private options: TransportManagerOptions;
1232

13-
constructor(private server: McpServer) {}
33+
constructor(
34+
private server: McpServer,
35+
options: TransportManagerOptions = {},
36+
) {
37+
this.options = options;
38+
}
1439

1540
async start(
1641
types: TransportType[],
@@ -22,10 +47,42 @@ export class TransportManager {
2247
(type) => type === TransportType.HTTP || type === TransportType.SSE,
2348
);
2449

25-
// Initialize and prepare HTTP server if needed
2650
if (needsHttpServer) {
2751
this.httpServer = new HttpServer();
2852
await this.httpServer.prepare();
53+
54+
// Register OAuth router if enabled
55+
if (this.options.enableOAuth) {
56+
if (!this.options.oauthConfig) {
57+
throw new Error("OAuth is enabled but OAuth config is missing.");
58+
}
59+
const env = this.options.oauthConfig;
60+
const proxyProvider = new ProxyOAuthServerProvider({
61+
endpoints: {
62+
authorizationUrl: env.OAUTH_AUTHORIZATION_URL,
63+
tokenUrl: env.OAUTH_TOKEN_URL,
64+
revocationUrl: env.OAUTH_REVOCATION_URL,
65+
},
66+
verifyAccessToken: async (token) => ({
67+
token,
68+
clientId: env.OAUTH_CLIENT_ID,
69+
scopes: env.OAUTH_SCOPES,
70+
}),
71+
getClient: async (client_id) => ({
72+
client_id,
73+
redirect_uris: [env.OAUTH_REDIRECT_URI],
74+
}),
75+
});
76+
77+
const oauthRouter = mcpAuthRouter({
78+
provider: proxyProvider,
79+
issuerUrl: new URL(env.OAUTH_ISSUER_URL),
80+
baseUrl: new URL(env.OAUTH_BASE_URL),
81+
serviceDocumentationUrl: new URL(env.OAUTH_DOCS_URL),
82+
});
83+
84+
await this.httpServer.registerOAuthRouter(oauthRouter);
85+
}
2986
}
3087

3188
// Create and connect all transports
@@ -101,4 +158,8 @@ export class TransportManager {
101158
throw new Error(`Unsupported transport type: ${type}`);
102159
}
103160
}
161+
162+
public getHttpServer(): HttpServer | null {
163+
return this.httpServer;
164+
}
104165
}

src/mcp/transports/server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import fastifyExpress from "@fastify/express";
12
import fastifySwagger from "@fastify/swagger";
23
import fastifySwaggerUi from "@fastify/swagger-ui";
34
import { logger } from "@src/logger.js";
45
import { ServerConfig } from "@src/mcp/transports/types.js";
6+
import { RequestHandler } from "express";
57
import {
68
default as Fastify,
79
FastifyBaseLogger,
@@ -17,6 +19,7 @@ export class HttpServer {
1719
// Useful for when ongoing http/sse connections are present
1820
forceCloseConnections: true,
1921
});
22+
this.fastify.register(fastifyExpress);
2023
}
2124

2225
async prepare(): Promise<void> {
@@ -95,4 +98,8 @@ export class HttpServer {
9598
throw error;
9699
}
97100
}
101+
102+
async registerOAuthRouter(oauthRouter: RequestHandler) {
103+
this.fastify.use(oauthRouter);
104+
}
98105
}

src/mcp/transports/sse.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
33
import { logger } from "@src/logger.js";
4+
import { HttpServer } from "@src/mcp/transports/server.js";
5+
import { Transport } from "@src/mcp/transports/types.js";
46
import { FastifyReply, FastifyRequest } from "fastify";
5-
import { HttpServer } from "./server.js";
6-
import { Transport } from "./types.js";
77

88
interface SseMessageRequest {
99
Querystring: {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy