@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/token.ts ADDED
@@ -0,0 +1,256 @@
1
+ /**
2
+ * OAuth 2.0 Token Endpoint
3
+ *
4
+ * POST /token - Exchange authorization code for tokens
5
+ *
6
+ * This endpoint:
7
+ * 1. Validates the authorization code
8
+ * 2. Verifies the PKCE code_verifier
9
+ * 3. Issues access_token and refresh_token
10
+ */
11
+
12
+ import type { VercelRequest, VercelResponse } from '@vercel/node';
13
+ import {
14
+ OAUTH_CONFIG,
15
+ consumeAuthorizationCode,
16
+ verifyPkceChallenge,
17
+ generateAccessToken,
18
+ generateRefreshToken,
19
+ exchangeRefreshToken,
20
+ } from './lib/oauth.js';
21
+ import { recordMetric, startMetricsContext, getDuration } from './lib/metrics.js';
22
+
23
+ export default async function handler(req: VercelRequest, res: VercelResponse) {
24
+ const metricsContext = startMetricsContext();
25
+
26
+ // Set CORS headers for token endpoint
27
+ res.setHeader('Access-Control-Allow-Origin', '*');
28
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
29
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
30
+ res.setHeader('Cache-Control', 'no-store');
31
+ res.setHeader('Pragma', 'no-cache');
32
+
33
+ // Handle OPTIONS preflight
34
+ if (req.method === 'OPTIONS') {
35
+ res.status(200).end();
36
+ return;
37
+ }
38
+
39
+ // Only POST is allowed
40
+ if (req.method !== 'POST') {
41
+ recordMetric({
42
+ method: req.method as 'GET' | 'POST',
43
+ tool: 'oauth/token',
44
+ authMethod: 'none',
45
+ authSuccess: false,
46
+ durationMs: getDuration(metricsContext),
47
+ statusCode: 405,
48
+ errorCode: 'METHOD_NOT_ALLOWED',
49
+ });
50
+ res.status(405).json({ error: 'method_not_allowed' });
51
+ return;
52
+ }
53
+
54
+ // Parse body (support both JSON and form-urlencoded)
55
+ let body = req.body;
56
+ if (typeof body === 'string') {
57
+ try {
58
+ body = JSON.parse(body);
59
+ } catch {
60
+ // Try parsing as form-urlencoded
61
+ body = Object.fromEntries(new URLSearchParams(body));
62
+ }
63
+ }
64
+
65
+ const { grant_type, code, redirect_uri, code_verifier, refresh_token, client_id } = body;
66
+
67
+ console.log('[Token] Request received:', {
68
+ grant_type,
69
+ code: code ? `${code.substring(0, 30)}...` : undefined,
70
+ redirect_uri,
71
+ code_verifier: code_verifier ? `${code_verifier.substring(0, 20)}...` : undefined,
72
+ client_id,
73
+ });
74
+
75
+ // ============================================================================
76
+ // Authorization Code Grant
77
+ // ============================================================================
78
+ if (grant_type === 'authorization_code') {
79
+ // Validate required parameters
80
+ if (!code) {
81
+ res.status(400).json({
82
+ error: 'invalid_request',
83
+ error_description: 'code is required',
84
+ });
85
+ return;
86
+ }
87
+
88
+ if (!redirect_uri) {
89
+ res.status(400).json({
90
+ error: 'invalid_request',
91
+ error_description: 'redirect_uri is required',
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (!code_verifier) {
97
+ res.status(400).json({
98
+ error: 'invalid_request',
99
+ error_description: 'code_verifier is required (PKCE)',
100
+ });
101
+ return;
102
+ }
103
+
104
+ // Consume the authorization code (one-time use)
105
+ console.log('[Token] Attempting to consume auth code...');
106
+ const authCode = consumeAuthorizationCode(code);
107
+
108
+ if (!authCode) {
109
+ console.log('[Token] Auth code validation failed');
110
+ res.status(400).json({
111
+ error: 'invalid_grant',
112
+ error_description: 'Authorization code is invalid, expired, or already used',
113
+ });
114
+ return;
115
+ }
116
+
117
+ console.log('[Token] Auth code valid:', {
118
+ clientId: authCode.clientId,
119
+ userId: authCode.userId,
120
+ scope: authCode.scope,
121
+ });
122
+
123
+ // Validate redirect_uri matches
124
+ if (authCode.redirectUri !== redirect_uri) {
125
+ console.log('[Token] redirect_uri mismatch:', {
126
+ expected: authCode.redirectUri,
127
+ received: redirect_uri,
128
+ });
129
+ res.status(400).json({
130
+ error: 'invalid_grant',
131
+ error_description: 'redirect_uri does not match',
132
+ });
133
+ return;
134
+ }
135
+
136
+ console.log('[Token] redirect_uri validated OK');
137
+
138
+ // Verify PKCE
139
+ console.log('[Token] Verifying PKCE...');
140
+ const pkceValid = verifyPkceChallenge(
141
+ code_verifier,
142
+ authCode.codeChallenge,
143
+ authCode.codeChallengeMethod
144
+ );
145
+
146
+ if (!pkceValid) {
147
+ console.log('[Token] PKCE verification failed:', {
148
+ verifier: code_verifier?.substring(0, 20) + '...',
149
+ challenge: authCode.codeChallenge?.substring(0, 20) + '...',
150
+ method: authCode.codeChallengeMethod,
151
+ });
152
+ res.status(400).json({
153
+ error: 'invalid_grant',
154
+ error_description: 'PKCE verification failed',
155
+ });
156
+ return;
157
+ }
158
+
159
+ console.log('[Token] PKCE verified OK, generating tokens...');
160
+
161
+ // Generate tokens
162
+ const accessToken = generateAccessToken({
163
+ userId: authCode.userId,
164
+ organizationId: authCode.organizationId,
165
+ scope: authCode.scope,
166
+ });
167
+
168
+ const refreshToken = generateRefreshToken({
169
+ userId: authCode.userId,
170
+ organizationId: authCode.organizationId,
171
+ scope: authCode.scope,
172
+ });
173
+
174
+ // Record successful token exchange
175
+ recordMetric({
176
+ method: 'POST',
177
+ organizationId: authCode.organizationId,
178
+ userId: authCode.userId,
179
+ tool: 'oauth/token/authorization_code',
180
+ authMethod: 'oauth',
181
+ authSuccess: true,
182
+ durationMs: getDuration(metricsContext),
183
+ statusCode: 200,
184
+ });
185
+
186
+ // Return token response
187
+ console.log('[Token] SUCCESS - returning tokens');
188
+ res.status(200).json({
189
+ access_token: accessToken,
190
+ token_type: 'Bearer',
191
+ expires_in: OAUTH_CONFIG.accessTokenTtlSeconds,
192
+ refresh_token: refreshToken,
193
+ scope: authCode.scope,
194
+ });
195
+ return;
196
+ }
197
+
198
+ // ============================================================================
199
+ // Refresh Token Grant
200
+ // ============================================================================
201
+ if (grant_type === 'refresh_token') {
202
+ if (!refresh_token) {
203
+ res.status(400).json({
204
+ error: 'invalid_request',
205
+ error_description: 'refresh_token is required',
206
+ });
207
+ return;
208
+ }
209
+
210
+ const newTokens = exchangeRefreshToken(refresh_token);
211
+
212
+ if (!newTokens) {
213
+ res.status(400).json({
214
+ error: 'invalid_grant',
215
+ error_description: 'Refresh token is invalid or expired',
216
+ });
217
+ return;
218
+ }
219
+
220
+ // Record successful refresh token exchange
221
+ recordMetric({
222
+ method: 'POST',
223
+ tool: 'oauth/token/refresh',
224
+ authMethod: 'oauth',
225
+ authSuccess: true,
226
+ durationMs: getDuration(metricsContext),
227
+ statusCode: 200,
228
+ });
229
+
230
+ res.status(200).json({
231
+ access_token: newTokens.accessToken,
232
+ token_type: 'Bearer',
233
+ expires_in: newTokens.expiresIn,
234
+ refresh_token: newTokens.refreshToken,
235
+ scope: newTokens.scope,
236
+ });
237
+ return;
238
+ }
239
+
240
+ // ============================================================================
241
+ // Unsupported Grant Type
242
+ // ============================================================================
243
+ recordMetric({
244
+ method: 'POST',
245
+ tool: 'oauth/token',
246
+ authMethod: 'none',
247
+ authSuccess: false,
248
+ durationMs: getDuration(metricsContext),
249
+ statusCode: 400,
250
+ errorCode: 'UNSUPPORTED_GRANT_TYPE',
251
+ });
252
+ res.status(400).json({
253
+ error: 'unsupported_grant_type',
254
+ error_description: 'Only authorization_code and refresh_token grants are supported',
255
+ });
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netpad/mcp-server-remote",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "Remote MCP server for NetPad - deployable to Vercel for Claude custom connectors. Includes all 80+ tools from @netpad/mcp-server.",
5
5
  "author": "Michael Lynn",
6
6
  "license": "Apache-2.0",
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@modelcontextprotocol/sdk": "^1.0.0",
15
- "@netpad/mcp-server": "file:../mcp-server",
15
+ "@netpad/mcp-server": "^2.4.0",
16
16
  "zod": "^3.23.0"
17
17
  },
18
18
  "devDependencies": {
package/vercel.json CHANGED
@@ -9,6 +9,26 @@
9
9
  "source": "/health",
10
10
  "destination": "/api/health"
11
11
  },
12
+ {
13
+ "source": "/authorize",
14
+ "destination": "/api/authorize"
15
+ },
16
+ {
17
+ "source": "/token",
18
+ "destination": "/api/token"
19
+ },
20
+ {
21
+ "source": "/.well-known/oauth-authorization-server",
22
+ "destination": "/api/oauth-metadata"
23
+ },
24
+ {
25
+ "source": "/.well-known/oauth-protected-resource",
26
+ "destination": "/api/oauth-protected-resource"
27
+ },
28
+ {
29
+ "source": "/oauth-metadata",
30
+ "destination": "/api/oauth-metadata"
31
+ },
12
32
  {
13
33
  "source": "/",
14
34
  "destination": "/api/index"