@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.
@@ -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
+ }
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.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": "file:../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"