@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/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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netpad/mcp-server-remote",
|
|
3
|
-
"version": "1.
|
|
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": "
|
|
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"
|