@netpad/mcp-server-remote 1.2.0 → 1.5.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.
- package/api/.well-known/oauth-authorization-server.ts +71 -0
- package/api/authorize.ts +538 -0
- package/api/index.ts +27 -4
- package/api/lib/metrics.ts +208 -0
- package/api/lib/oauth.ts +475 -0
- package/api/mcp.ts +199 -43
- package/api/oauth-metadata.ts +75 -0
- package/api/oauth-protected-resource.ts +40 -0
- package/api/token.ts +256 -0
- package/netpad-mcp-server-remote-1.3.0.tgz +0 -0
- package/netpad-mcp-server-remote-1.4.1.tgz +0 -0
- package/netpad-mcp-server-remote-1.4.2.tgz +0 -0
- package/netpad-mcp-server-remote-1.5.0.tgz +0 -0
- package/package.json +2 -2
- package/vercel.json +20 -0
package/api/mcp.ts
CHANGED
|
@@ -2,12 +2,20 @@ import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
|
2
2
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
3
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
4
|
import { createNetPadMcpServer } from '@netpad/mcp-server';
|
|
5
|
+
import { validateAccessToken } from './lib/oauth.js';
|
|
6
|
+
import {
|
|
7
|
+
recordMetric,
|
|
8
|
+
startMetricsContext,
|
|
9
|
+
getDuration,
|
|
10
|
+
extractToolFromBody,
|
|
11
|
+
flushMetrics,
|
|
12
|
+
} from './lib/metrics.js';
|
|
5
13
|
|
|
6
14
|
// Store transports by session ID for reconnection
|
|
7
15
|
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
8
16
|
|
|
9
17
|
// ============================================================================
|
|
10
|
-
// API
|
|
18
|
+
// AUTHENTICATION (Supports both OAuth tokens and API keys)
|
|
11
19
|
// ============================================================================
|
|
12
20
|
|
|
13
21
|
// Cache validated API keys for 5 minutes to reduce API calls
|
|
@@ -16,28 +24,46 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* Get the NetPad API base URL from environment or default
|
|
27
|
+
* Note: Using www.netpad.io because netpad.io redirects to www with 307
|
|
19
28
|
*/
|
|
20
29
|
function getNetPadApiUrl(): string {
|
|
21
|
-
return process.env.NETPAD_API_URL || 'https://netpad.io';
|
|
30
|
+
return process.env.NETPAD_API_URL || 'https://www.netpad.io';
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
* Authentication result
|
|
35
|
+
*/
|
|
36
|
+
interface AuthResult {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
userId?: string;
|
|
39
|
+
organizationId?: string;
|
|
40
|
+
scope?: string;
|
|
41
|
+
error?: {
|
|
42
|
+
status: number;
|
|
43
|
+
error: string;
|
|
44
|
+
code: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate the request using either OAuth token or API key
|
|
29
50
|
*
|
|
30
|
-
*
|
|
51
|
+
* Supports:
|
|
52
|
+
* 1. OAuth access tokens (JWT-like tokens from /token endpoint)
|
|
53
|
+
* 2. NetPad API keys (np_live_xxx or np_test_xxx)
|
|
31
54
|
*/
|
|
32
|
-
async function
|
|
55
|
+
async function validateRequest(req: VercelRequest): Promise<AuthResult> {
|
|
33
56
|
const authHeader = req.headers['authorization'];
|
|
34
57
|
|
|
35
58
|
// Check if Authorization header is present
|
|
36
59
|
if (!authHeader) {
|
|
37
60
|
return {
|
|
38
|
-
|
|
39
|
-
error:
|
|
40
|
-
|
|
61
|
+
valid: false,
|
|
62
|
+
error: {
|
|
63
|
+
status: 401,
|
|
64
|
+
error: 'Missing Authorization header. Use: Authorization: Bearer <token>',
|
|
65
|
+
code: 'MISSING_AUTH',
|
|
66
|
+
},
|
|
41
67
|
};
|
|
42
68
|
}
|
|
43
69
|
|
|
@@ -45,41 +71,94 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
|
|
|
45
71
|
const parts = authHeader.split(' ');
|
|
46
72
|
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
|
|
47
73
|
return {
|
|
48
|
-
|
|
49
|
-
error:
|
|
50
|
-
|
|
74
|
+
valid: false,
|
|
75
|
+
error: {
|
|
76
|
+
status: 401,
|
|
77
|
+
error: 'Invalid Authorization header format. Use: Authorization: Bearer <token>',
|
|
78
|
+
code: 'INVALID_AUTH_FORMAT',
|
|
79
|
+
},
|
|
51
80
|
};
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
const
|
|
83
|
+
const token = parts[1].trim();
|
|
55
84
|
|
|
56
|
-
if (!
|
|
85
|
+
if (!token) {
|
|
57
86
|
return {
|
|
58
|
-
|
|
59
|
-
error:
|
|
60
|
-
|
|
87
|
+
valid: false,
|
|
88
|
+
error: {
|
|
89
|
+
status: 401,
|
|
90
|
+
error: 'Token is empty',
|
|
91
|
+
code: 'EMPTY_TOKEN',
|
|
92
|
+
},
|
|
61
93
|
};
|
|
62
94
|
}
|
|
63
95
|
|
|
64
|
-
//
|
|
65
|
-
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Try OAuth token first (JWT-like format with dots)
|
|
98
|
+
// ============================================================================
|
|
99
|
+
if (token.includes('.')) {
|
|
100
|
+
console.log('[MCP] Attempting OAuth token validation...');
|
|
101
|
+
const payload = validateAccessToken(token);
|
|
102
|
+
if (payload) {
|
|
103
|
+
console.log('[MCP] OAuth token valid:', { userId: payload.sub, org: payload.org });
|
|
104
|
+
return {
|
|
105
|
+
valid: true,
|
|
106
|
+
userId: payload.sub,
|
|
107
|
+
organizationId: payload.org,
|
|
108
|
+
scope: payload.scope,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// If it looks like a JWT but failed validation, return error
|
|
112
|
+
// (don't fall through to API key validation)
|
|
113
|
+
console.log('[MCP] OAuth token validation failed');
|
|
66
114
|
return {
|
|
67
|
-
|
|
68
|
-
error:
|
|
69
|
-
|
|
115
|
+
valid: false,
|
|
116
|
+
error: {
|
|
117
|
+
status: 401,
|
|
118
|
+
error: 'Invalid or expired OAuth token',
|
|
119
|
+
code: 'INVALID_OAUTH_TOKEN',
|
|
120
|
+
},
|
|
70
121
|
};
|
|
71
122
|
}
|
|
72
123
|
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Try API key (np_live_xxx or np_test_xxx format)
|
|
126
|
+
// ============================================================================
|
|
127
|
+
if (token.startsWith('np_live_') || token.startsWith('np_test_')) {
|
|
128
|
+
return validateApiKey(token);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Unknown token format
|
|
132
|
+
return {
|
|
133
|
+
valid: false,
|
|
134
|
+
error: {
|
|
135
|
+
status: 401,
|
|
136
|
+
error: 'Invalid token format. Use an OAuth token or NetPad API key (np_live_xxx)',
|
|
137
|
+
code: 'INVALID_TOKEN_FORMAT',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate a NetPad API key
|
|
144
|
+
*/
|
|
145
|
+
async function validateApiKey(apiKey: string): Promise<AuthResult> {
|
|
73
146
|
// Check cache first
|
|
74
147
|
const cached = apiKeyCache.get(apiKey);
|
|
75
148
|
if (cached && cached.expiresAt > Date.now()) {
|
|
76
149
|
if (cached.valid) {
|
|
77
|
-
return
|
|
150
|
+
return {
|
|
151
|
+
valid: true,
|
|
152
|
+
organizationId: cached.organizationId,
|
|
153
|
+
};
|
|
78
154
|
}
|
|
79
155
|
return {
|
|
80
|
-
|
|
81
|
-
error:
|
|
82
|
-
|
|
156
|
+
valid: false,
|
|
157
|
+
error: {
|
|
158
|
+
status: 401,
|
|
159
|
+
error: 'Invalid or expired API key',
|
|
160
|
+
code: 'INVALID_API_KEY',
|
|
161
|
+
},
|
|
83
162
|
};
|
|
84
163
|
}
|
|
85
164
|
|
|
@@ -96,14 +175,18 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
|
|
|
96
175
|
});
|
|
97
176
|
|
|
98
177
|
if (response.ok) {
|
|
99
|
-
const data = await response.json();
|
|
178
|
+
const data = await response.json() as { userId?: string; organizationId?: string };
|
|
100
179
|
// Cache the valid result
|
|
101
180
|
apiKeyCache.set(apiKey, {
|
|
102
181
|
valid: true,
|
|
103
182
|
organizationId: data.organizationId,
|
|
104
183
|
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
105
184
|
});
|
|
106
|
-
return
|
|
185
|
+
return {
|
|
186
|
+
valid: true,
|
|
187
|
+
userId: data.userId,
|
|
188
|
+
organizationId: data.organizationId,
|
|
189
|
+
};
|
|
107
190
|
}
|
|
108
191
|
|
|
109
192
|
// Key is invalid - cache the negative result too (shorter TTL)
|
|
@@ -113,11 +196,14 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
|
|
|
113
196
|
});
|
|
114
197
|
|
|
115
198
|
// Parse error response
|
|
116
|
-
const errorData = await response.json().catch(() => ({}));
|
|
199
|
+
const errorData = await response.json().catch(() => ({})) as { error?: { message?: string; code?: string } };
|
|
117
200
|
return {
|
|
118
|
-
|
|
119
|
-
error:
|
|
120
|
-
|
|
201
|
+
valid: false,
|
|
202
|
+
error: {
|
|
203
|
+
status: response.status,
|
|
204
|
+
error: errorData.error?.message || 'Invalid or expired API key',
|
|
205
|
+
code: errorData.error?.code || 'INVALID_API_KEY',
|
|
206
|
+
},
|
|
121
207
|
};
|
|
122
208
|
} catch (error) {
|
|
123
209
|
console.error('Error validating API key against NetPad:', error);
|
|
@@ -126,12 +212,14 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
|
|
|
126
212
|
// This allows the MCP server to work even if NetPad API is temporarily unavailable
|
|
127
213
|
// but only accepts properly formatted keys
|
|
128
214
|
console.warn('NetPad API unavailable, accepting key based on format validation only');
|
|
129
|
-
return
|
|
215
|
+
return { valid: true };
|
|
130
216
|
}
|
|
131
217
|
}
|
|
132
218
|
|
|
133
219
|
// Main handler
|
|
134
220
|
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
221
|
+
const metricsContext = startMetricsContext();
|
|
222
|
+
|
|
135
223
|
// Handle CORS preflight
|
|
136
224
|
if (req.method === 'OPTIONS') {
|
|
137
225
|
res.status(200).end();
|
|
@@ -141,16 +229,41 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
141
229
|
// ============================================================================
|
|
142
230
|
// AUTHENTICATE REQUEST
|
|
143
231
|
// ============================================================================
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
232
|
+
const authResult = await validateRequest(req);
|
|
233
|
+
if (!authResult.valid) {
|
|
234
|
+
// Record failed auth metric
|
|
235
|
+
recordMetric({
|
|
236
|
+
method: req.method as 'GET' | 'POST',
|
|
237
|
+
authMethod: 'none',
|
|
238
|
+
authSuccess: false,
|
|
239
|
+
durationMs: getDuration(metricsContext),
|
|
240
|
+
statusCode: authResult.error?.status || 401,
|
|
241
|
+
errorCode: authResult.error?.code,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Flush metrics (serverless: memory is ephemeral)
|
|
245
|
+
flushMetrics().catch(() => {});
|
|
246
|
+
|
|
247
|
+
// Per MCP spec and RFC 9728, include WWW-Authenticate header with resource_metadata
|
|
248
|
+
// This tells OAuth clients where to find the authorization server
|
|
249
|
+
const resourceMetadataUrl = 'https://mcp.netpad.io/.well-known/oauth-protected-resource';
|
|
250
|
+
res.setHeader(
|
|
251
|
+
'WWW-Authenticate',
|
|
252
|
+
`Bearer resource_metadata="${resourceMetadataUrl}", scope="mcp read write"`
|
|
253
|
+
);
|
|
254
|
+
res.status(authResult.error?.status || 401).json({
|
|
255
|
+
error: authResult.error?.error || 'Authentication failed',
|
|
256
|
+
code: authResult.error?.code || 'AUTH_FAILED',
|
|
257
|
+
hint: 'Connect via Claude.ai Settings > Connectors, or use an API key from netpad.io/settings',
|
|
150
258
|
});
|
|
151
259
|
return;
|
|
152
260
|
}
|
|
153
261
|
|
|
262
|
+
// Determine auth method for metrics
|
|
263
|
+
const authHeader = req.headers['authorization'] || '';
|
|
264
|
+
const token = authHeader.split(' ')[1] || '';
|
|
265
|
+
const authMethod: 'oauth' | 'api_key' = token.includes('.') ? 'oauth' : 'api_key';
|
|
266
|
+
|
|
154
267
|
// Get session ID from headers
|
|
155
268
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
156
269
|
|
|
@@ -164,7 +277,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
164
277
|
// Create the full NetPad MCP server with all 80+ tools
|
|
165
278
|
const server = createNetPadMcpServer({
|
|
166
279
|
name: '@netpad/mcp-server-remote',
|
|
167
|
-
version: '1.
|
|
280
|
+
version: '1.2.0',
|
|
168
281
|
});
|
|
169
282
|
await server.connect(transport);
|
|
170
283
|
|
|
@@ -173,6 +286,21 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
173
286
|
transports.set(transport.sessionId, transport);
|
|
174
287
|
}
|
|
175
288
|
|
|
289
|
+
// Record successful SSE connection metric
|
|
290
|
+
recordMetric({
|
|
291
|
+
method: 'GET',
|
|
292
|
+
organizationId: authResult.organizationId,
|
|
293
|
+
userId: authResult.userId,
|
|
294
|
+
authMethod,
|
|
295
|
+
authSuccess: true,
|
|
296
|
+
durationMs: getDuration(metricsContext),
|
|
297
|
+
statusCode: 200,
|
|
298
|
+
sessionId: transport.sessionId,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Flush metrics (serverless: memory is ephemeral)
|
|
302
|
+
flushMetrics().catch(() => {});
|
|
303
|
+
|
|
176
304
|
await transport.handleRequest(
|
|
177
305
|
req as unknown as IncomingMessage,
|
|
178
306
|
res as unknown as ServerResponse
|
|
@@ -182,6 +310,9 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
182
310
|
|
|
183
311
|
// Handle POST requests
|
|
184
312
|
if (req.method === 'POST') {
|
|
313
|
+
// Extract tool name from request body for metrics
|
|
314
|
+
const tool = extractToolFromBody(req.body);
|
|
315
|
+
|
|
185
316
|
// Try to reuse existing transport for this session
|
|
186
317
|
let transport = sessionId ? transports.get(sessionId) : undefined;
|
|
187
318
|
|
|
@@ -194,7 +325,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
194
325
|
// Create the full NetPad MCP server with all 80+ tools
|
|
195
326
|
const server = createNetPadMcpServer({
|
|
196
327
|
name: '@netpad/mcp-server-remote',
|
|
197
|
-
version: '1.
|
|
328
|
+
version: '1.2.0',
|
|
198
329
|
});
|
|
199
330
|
await server.connect(transport);
|
|
200
331
|
|
|
@@ -208,9 +339,34 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
|
208
339
|
res as unknown as ServerResponse,
|
|
209
340
|
req.body
|
|
210
341
|
);
|
|
342
|
+
|
|
343
|
+
// Record POST request metric
|
|
344
|
+
recordMetric({
|
|
345
|
+
method: 'POST',
|
|
346
|
+
organizationId: authResult.organizationId,
|
|
347
|
+
userId: authResult.userId,
|
|
348
|
+
tool,
|
|
349
|
+
authMethod,
|
|
350
|
+
authSuccess: true,
|
|
351
|
+
durationMs: getDuration(metricsContext),
|
|
352
|
+
statusCode: 200,
|
|
353
|
+
sessionId: sessionId || transport.sessionId,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Opportunistically flush metrics (non-blocking)
|
|
357
|
+
flushMetrics().catch(() => {});
|
|
211
358
|
return;
|
|
212
359
|
}
|
|
213
360
|
|
|
214
361
|
// Method not allowed
|
|
362
|
+
recordMetric({
|
|
363
|
+
method: req.method as 'GET' | 'POST',
|
|
364
|
+
authMethod,
|
|
365
|
+
authSuccess: true,
|
|
366
|
+
durationMs: getDuration(metricsContext),
|
|
367
|
+
statusCode: 405,
|
|
368
|
+
errorCode: 'METHOD_NOT_ALLOWED',
|
|
369
|
+
});
|
|
370
|
+
flushMetrics().catch(() => {});
|
|
215
371
|
res.status(405).json({ error: 'Method not allowed' });
|
|
216
372
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Server Metadata
|
|
3
|
+
*
|
|
4
|
+
* GET /.well-known/oauth-authorization-server
|
|
5
|
+
*
|
|
6
|
+
* This endpoint returns metadata about the OAuth server according to RFC 8414.
|
|
7
|
+
* Claude.ai and other OAuth clients can use this to discover endpoints.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
11
|
+
import { OAUTH_CONFIG } from './lib/oauth.js';
|
|
12
|
+
|
|
13
|
+
export default function handler(req: VercelRequest, res: VercelResponse) {
|
|
14
|
+
if (req.method !== 'GET') {
|
|
15
|
+
res.status(405).json({ error: 'Method not allowed' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const issuer = OAUTH_CONFIG.issuer;
|
|
20
|
+
|
|
21
|
+
// OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
|
22
|
+
const metadata = {
|
|
23
|
+
// REQUIRED: Issuer identifier
|
|
24
|
+
issuer: issuer,
|
|
25
|
+
|
|
26
|
+
// REQUIRED: Authorization endpoint
|
|
27
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
28
|
+
|
|
29
|
+
// REQUIRED: Token endpoint
|
|
30
|
+
token_endpoint: `${issuer}/token`,
|
|
31
|
+
|
|
32
|
+
// OPTIONAL: Registration endpoint (not implemented)
|
|
33
|
+
// registration_endpoint: `${issuer}/register`,
|
|
34
|
+
|
|
35
|
+
// OPTIONAL: Scopes supported
|
|
36
|
+
scopes_supported: Object.keys(OAUTH_CONFIG.scopes),
|
|
37
|
+
|
|
38
|
+
// REQUIRED: Response types supported
|
|
39
|
+
response_types_supported: ['code'],
|
|
40
|
+
|
|
41
|
+
// OPTIONAL: Response modes supported
|
|
42
|
+
response_modes_supported: ['query'],
|
|
43
|
+
|
|
44
|
+
// OPTIONAL: Grant types supported
|
|
45
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
46
|
+
|
|
47
|
+
// OPTIONAL: Token endpoint authentication methods
|
|
48
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
49
|
+
|
|
50
|
+
// OPTIONAL: PKCE code challenge methods (RFC 7636)
|
|
51
|
+
code_challenge_methods_supported: ['S256', 'plain'],
|
|
52
|
+
|
|
53
|
+
// MCP REQUIRED: Support for Client ID Metadata Documents
|
|
54
|
+
// This allows Claude.ai to use its hosted client metadata
|
|
55
|
+
client_id_metadata_document_supported: true,
|
|
56
|
+
|
|
57
|
+
// OPTIONAL: Service documentation
|
|
58
|
+
service_documentation: 'https://docs.netpad.io/docs/developer/mcp-server',
|
|
59
|
+
|
|
60
|
+
// OPTIONAL: MCP-specific metadata
|
|
61
|
+
mcp_endpoint: `${issuer}/mcp`,
|
|
62
|
+
|
|
63
|
+
// Custom: NetPad-specific info
|
|
64
|
+
netpad: {
|
|
65
|
+
name: 'NetPad MCP Server',
|
|
66
|
+
version: '1.2.0',
|
|
67
|
+
tools_count: '80+',
|
|
68
|
+
api_key_url: 'https://netpad.io/settings',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
res.setHeader('Content-Type', 'application/json');
|
|
73
|
+
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
|
|
74
|
+
res.status(200).json(metadata);
|
|
75
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Protected Resource Metadata
|
|
3
|
+
*
|
|
4
|
+
* GET /.well-known/oauth-protected-resource
|
|
5
|
+
*
|
|
6
|
+
* This endpoint tells Claude.ai where to find the OAuth authorization server.
|
|
7
|
+
* Claude.ai queries this endpoint FIRST before initiating the OAuth flow.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
11
|
+
import { OAUTH_CONFIG } from './lib/oauth.js';
|
|
12
|
+
|
|
13
|
+
export default function handler(req: VercelRequest, res: VercelResponse) {
|
|
14
|
+
if (req.method !== 'GET') {
|
|
15
|
+
res.status(405).json({ error: 'Method not allowed' });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const issuer = OAUTH_CONFIG.issuer;
|
|
20
|
+
|
|
21
|
+
// OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
22
|
+
// This tells clients where to find the authorization server
|
|
23
|
+
const metadata = {
|
|
24
|
+
// The resource server (this MCP server)
|
|
25
|
+
resource: issuer,
|
|
26
|
+
|
|
27
|
+
// Where to find the authorization server(s)
|
|
28
|
+
authorization_servers: [issuer],
|
|
29
|
+
|
|
30
|
+
// Bearer token is the only supported method
|
|
31
|
+
bearer_methods_supported: ['header'],
|
|
32
|
+
|
|
33
|
+
// Scopes required to access this resource
|
|
34
|
+
scopes_supported: Object.keys(OAUTH_CONFIG.scopes),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
res.setHeader('Content-Type', 'application/json');
|
|
38
|
+
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
|
|
39
|
+
res.status(200).json(metadata);
|
|
40
|
+
}
|