@netpad/mcp-server-remote 1.2.0 → 1.4.2
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/oauth.ts +411 -0
- package/api/mcp.ts +120 -41
- package/api/oauth-metadata.ts +71 -0
- package/api/oauth-protected-resource.ts +40 -0
- package/api/token.ts +213 -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/package.json +2 -2
- package/vercel.json +20 -0
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
// OPTIONAL: Service documentation
|
|
54
|
+
service_documentation: 'https://docs.netpad.io/docs/developer/mcp-server',
|
|
55
|
+
|
|
56
|
+
// OPTIONAL: MCP-specific metadata
|
|
57
|
+
mcp_endpoint: `${issuer}/mcp`,
|
|
58
|
+
|
|
59
|
+
// Custom: NetPad-specific info
|
|
60
|
+
netpad: {
|
|
61
|
+
name: 'NetPad MCP Server',
|
|
62
|
+
version: '1.2.0',
|
|
63
|
+
tools_count: '80+',
|
|
64
|
+
api_key_url: 'https://netpad.io/settings',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
res.setHeader('Content-Type', 'application/json');
|
|
69
|
+
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
|
|
70
|
+
res.status(200).json(metadata);
|
|
71
|
+
}
|
|
@@ -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
|
+
}
|
package/api/token.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
|
|
22
|
+
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
23
|
+
// Set CORS headers for token endpoint
|
|
24
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
25
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
26
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
27
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
28
|
+
res.setHeader('Pragma', 'no-cache');
|
|
29
|
+
|
|
30
|
+
// Handle OPTIONS preflight
|
|
31
|
+
if (req.method === 'OPTIONS') {
|
|
32
|
+
res.status(200).end();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Only POST is allowed
|
|
37
|
+
if (req.method !== 'POST') {
|
|
38
|
+
res.status(405).json({ error: 'method_not_allowed' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse body (support both JSON and form-urlencoded)
|
|
43
|
+
let body = req.body;
|
|
44
|
+
if (typeof body === 'string') {
|
|
45
|
+
try {
|
|
46
|
+
body = JSON.parse(body);
|
|
47
|
+
} catch {
|
|
48
|
+
// Try parsing as form-urlencoded
|
|
49
|
+
body = Object.fromEntries(new URLSearchParams(body));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { grant_type, code, redirect_uri, code_verifier, refresh_token, client_id } = body;
|
|
54
|
+
|
|
55
|
+
console.log('[Token] Request received:', {
|
|
56
|
+
grant_type,
|
|
57
|
+
code: code ? `${code.substring(0, 30)}...` : undefined,
|
|
58
|
+
redirect_uri,
|
|
59
|
+
code_verifier: code_verifier ? `${code_verifier.substring(0, 20)}...` : undefined,
|
|
60
|
+
client_id,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Authorization Code Grant
|
|
65
|
+
// ============================================================================
|
|
66
|
+
if (grant_type === 'authorization_code') {
|
|
67
|
+
// Validate required parameters
|
|
68
|
+
if (!code) {
|
|
69
|
+
res.status(400).json({
|
|
70
|
+
error: 'invalid_request',
|
|
71
|
+
error_description: 'code is required',
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!redirect_uri) {
|
|
77
|
+
res.status(400).json({
|
|
78
|
+
error: 'invalid_request',
|
|
79
|
+
error_description: 'redirect_uri is required',
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!code_verifier) {
|
|
85
|
+
res.status(400).json({
|
|
86
|
+
error: 'invalid_request',
|
|
87
|
+
error_description: 'code_verifier is required (PKCE)',
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Consume the authorization code (one-time use)
|
|
93
|
+
console.log('[Token] Attempting to consume auth code...');
|
|
94
|
+
const authCode = consumeAuthorizationCode(code);
|
|
95
|
+
|
|
96
|
+
if (!authCode) {
|
|
97
|
+
console.log('[Token] Auth code validation failed');
|
|
98
|
+
res.status(400).json({
|
|
99
|
+
error: 'invalid_grant',
|
|
100
|
+
error_description: 'Authorization code is invalid, expired, or already used',
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('[Token] Auth code valid:', {
|
|
106
|
+
clientId: authCode.clientId,
|
|
107
|
+
userId: authCode.userId,
|
|
108
|
+
scope: authCode.scope,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Validate redirect_uri matches
|
|
112
|
+
if (authCode.redirectUri !== redirect_uri) {
|
|
113
|
+
console.log('[Token] redirect_uri mismatch:', {
|
|
114
|
+
expected: authCode.redirectUri,
|
|
115
|
+
received: redirect_uri,
|
|
116
|
+
});
|
|
117
|
+
res.status(400).json({
|
|
118
|
+
error: 'invalid_grant',
|
|
119
|
+
error_description: 'redirect_uri does not match',
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('[Token] redirect_uri validated OK');
|
|
125
|
+
|
|
126
|
+
// Verify PKCE
|
|
127
|
+
console.log('[Token] Verifying PKCE...');
|
|
128
|
+
const pkceValid = verifyPkceChallenge(
|
|
129
|
+
code_verifier,
|
|
130
|
+
authCode.codeChallenge,
|
|
131
|
+
authCode.codeChallengeMethod
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!pkceValid) {
|
|
135
|
+
console.log('[Token] PKCE verification failed:', {
|
|
136
|
+
verifier: code_verifier?.substring(0, 20) + '...',
|
|
137
|
+
challenge: authCode.codeChallenge?.substring(0, 20) + '...',
|
|
138
|
+
method: authCode.codeChallengeMethod,
|
|
139
|
+
});
|
|
140
|
+
res.status(400).json({
|
|
141
|
+
error: 'invalid_grant',
|
|
142
|
+
error_description: 'PKCE verification failed',
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('[Token] PKCE verified OK, generating tokens...');
|
|
148
|
+
|
|
149
|
+
// Generate tokens
|
|
150
|
+
const accessToken = generateAccessToken({
|
|
151
|
+
userId: authCode.userId,
|
|
152
|
+
organizationId: authCode.organizationId,
|
|
153
|
+
scope: authCode.scope,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const refreshToken = generateRefreshToken({
|
|
157
|
+
userId: authCode.userId,
|
|
158
|
+
organizationId: authCode.organizationId,
|
|
159
|
+
scope: authCode.scope,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Return token response
|
|
163
|
+
console.log('[Token] SUCCESS - returning tokens');
|
|
164
|
+
res.status(200).json({
|
|
165
|
+
access_token: accessToken,
|
|
166
|
+
token_type: 'Bearer',
|
|
167
|
+
expires_in: OAUTH_CONFIG.accessTokenTtlSeconds,
|
|
168
|
+
refresh_token: refreshToken,
|
|
169
|
+
scope: authCode.scope,
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Refresh Token Grant
|
|
176
|
+
// ============================================================================
|
|
177
|
+
if (grant_type === 'refresh_token') {
|
|
178
|
+
if (!refresh_token) {
|
|
179
|
+
res.status(400).json({
|
|
180
|
+
error: 'invalid_request',
|
|
181
|
+
error_description: 'refresh_token is required',
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const newTokens = exchangeRefreshToken(refresh_token);
|
|
187
|
+
|
|
188
|
+
if (!newTokens) {
|
|
189
|
+
res.status(400).json({
|
|
190
|
+
error: 'invalid_grant',
|
|
191
|
+
error_description: 'Refresh token is invalid or expired',
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
res.status(200).json({
|
|
197
|
+
access_token: newTokens.accessToken,
|
|
198
|
+
token_type: 'Bearer',
|
|
199
|
+
expires_in: newTokens.expiresIn,
|
|
200
|
+
refresh_token: newTokens.refreshToken,
|
|
201
|
+
scope: newTokens.scope,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Unsupported Grant Type
|
|
208
|
+
// ============================================================================
|
|
209
|
+
res.status(400).json({
|
|
210
|
+
error: 'unsupported_grant_type',
|
|
211
|
+
error_description: 'Only authorization_code and refresh_token grants are supported',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
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.2
|
|
3
|
+
"version": "1.4.2",
|
|
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.3.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"
|