@netpad/mcp-server-remote 1.0.1 → 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/mcp.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import type { VercelRequest, VercelResponse } from '@vercel/node';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
2
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
3
  import { IncomingMessage, ServerResponse } from 'node:http';
5
- import { z } from 'zod';
4
+ import { createNetPadMcpServer } from '@netpad/mcp-server';
5
+ import { validateAccessToken } from './lib/oauth.js';
6
6
 
7
7
  // Store transports by session ID for reconnection
8
8
  const transports = new Map<string, StreamableHTTPServerTransport>();
9
9
 
10
10
  // ============================================================================
11
- // API KEY AUTHENTICATION
11
+ // AUTHENTICATION (Supports both OAuth tokens and API keys)
12
12
  // ============================================================================
13
13
 
14
14
  // Cache validated API keys for 5 minutes to reduce API calls
@@ -17,28 +17,46 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
17
17
 
18
18
  /**
19
19
  * Get the NetPad API base URL from environment or default
20
+ * Note: Using www.netpad.io because netpad.io redirects to www with 307
20
21
  */
21
22
  function getNetPadApiUrl(): string {
22
- return process.env.NETPAD_API_URL || 'https://netpad.io';
23
+ return process.env.NETPAD_API_URL || 'https://www.netpad.io';
23
24
  }
24
25
 
25
26
  /**
26
- * Validate the API key by calling the NetPad API.
27
- * Uses the existing NetPad API key validation system.
28
- *
29
- * API keys should be in the format: np_live_xxx or np_test_xxx
27
+ * Authentication result
28
+ */
29
+ interface AuthResult {
30
+ valid: boolean;
31
+ userId?: string;
32
+ organizationId?: string;
33
+ scope?: string;
34
+ error?: {
35
+ status: number;
36
+ error: string;
37
+ code: string;
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Validate the request using either OAuth token or API key
30
43
  *
31
- * @returns null if valid, or an error response object if invalid
44
+ * Supports:
45
+ * 1. OAuth access tokens (JWT-like tokens from /token endpoint)
46
+ * 2. NetPad API keys (np_live_xxx or np_test_xxx)
32
47
  */
33
- async function validateApiKey(req: VercelRequest): Promise<{ status: number; error: string; code: string } | null> {
48
+ async function validateRequest(req: VercelRequest): Promise<AuthResult> {
34
49
  const authHeader = req.headers['authorization'];
35
50
 
36
51
  // Check if Authorization header is present
37
52
  if (!authHeader) {
38
53
  return {
39
- status: 401,
40
- error: 'Missing Authorization header. Use: Authorization: Bearer np_live_xxx',
41
- code: 'MISSING_API_KEY',
54
+ valid: false,
55
+ error: {
56
+ status: 401,
57
+ error: 'Missing Authorization header. Use: Authorization: Bearer <token>',
58
+ code: 'MISSING_AUTH',
59
+ },
42
60
  };
43
61
  }
44
62
 
@@ -46,41 +64,94 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
46
64
  const parts = authHeader.split(' ');
47
65
  if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
48
66
  return {
49
- status: 401,
50
- error: 'Invalid Authorization header format. Use: Authorization: Bearer np_live_xxx',
51
- code: 'INVALID_AUTH_FORMAT',
67
+ valid: false,
68
+ error: {
69
+ status: 401,
70
+ error: 'Invalid Authorization header format. Use: Authorization: Bearer <token>',
71
+ code: 'INVALID_AUTH_FORMAT',
72
+ },
52
73
  };
53
74
  }
54
75
 
55
- const apiKey = parts[1].trim();
76
+ const token = parts[1].trim();
56
77
 
57
- if (!apiKey) {
78
+ if (!token) {
58
79
  return {
59
- status: 401,
60
- error: 'API key is empty',
61
- code: 'EMPTY_API_KEY',
80
+ valid: false,
81
+ error: {
82
+ status: 401,
83
+ error: 'Token is empty',
84
+ code: 'EMPTY_TOKEN',
85
+ },
62
86
  };
63
87
  }
64
88
 
65
- // Validate key format (must start with np_live_ or np_test_)
66
- if (!apiKey.startsWith('np_live_') && !apiKey.startsWith('np_test_')) {
89
+ // ============================================================================
90
+ // Try OAuth token first (JWT-like format with dots)
91
+ // ============================================================================
92
+ if (token.includes('.')) {
93
+ console.log('[MCP] Attempting OAuth token validation...');
94
+ const payload = validateAccessToken(token);
95
+ if (payload) {
96
+ console.log('[MCP] OAuth token valid:', { userId: payload.sub, org: payload.org });
97
+ return {
98
+ valid: true,
99
+ userId: payload.sub,
100
+ organizationId: payload.org,
101
+ scope: payload.scope,
102
+ };
103
+ }
104
+ // If it looks like a JWT but failed validation, return error
105
+ // (don't fall through to API key validation)
106
+ console.log('[MCP] OAuth token validation failed');
67
107
  return {
68
- status: 401,
69
- error: 'Invalid API key format. Keys should start with np_live_ or np_test_. Generate one at netpad.io/settings',
70
- code: 'INVALID_API_KEY_FORMAT',
108
+ valid: false,
109
+ error: {
110
+ status: 401,
111
+ error: 'Invalid or expired OAuth token',
112
+ code: 'INVALID_OAUTH_TOKEN',
113
+ },
71
114
  };
72
115
  }
73
116
 
117
+ // ============================================================================
118
+ // Try API key (np_live_xxx or np_test_xxx format)
119
+ // ============================================================================
120
+ if (token.startsWith('np_live_') || token.startsWith('np_test_')) {
121
+ return validateApiKey(token);
122
+ }
123
+
124
+ // Unknown token format
125
+ return {
126
+ valid: false,
127
+ error: {
128
+ status: 401,
129
+ error: 'Invalid token format. Use an OAuth token or NetPad API key (np_live_xxx)',
130
+ code: 'INVALID_TOKEN_FORMAT',
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Validate a NetPad API key
137
+ */
138
+ async function validateApiKey(apiKey: string): Promise<AuthResult> {
74
139
  // Check cache first
75
140
  const cached = apiKeyCache.get(apiKey);
76
141
  if (cached && cached.expiresAt > Date.now()) {
77
142
  if (cached.valid) {
78
- return null; // Valid key from cache
143
+ return {
144
+ valid: true,
145
+ organizationId: cached.organizationId,
146
+ };
79
147
  }
80
148
  return {
81
- status: 401,
82
- error: 'Invalid or expired API key',
83
- code: 'INVALID_API_KEY',
149
+ valid: false,
150
+ error: {
151
+ status: 401,
152
+ error: 'Invalid or expired API key',
153
+ code: 'INVALID_API_KEY',
154
+ },
84
155
  };
85
156
  }
86
157
 
@@ -104,7 +175,11 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
104
175
  organizationId: data.organizationId,
105
176
  expiresAt: Date.now() + CACHE_TTL_MS,
106
177
  });
107
- return null; // Valid key
178
+ return {
179
+ valid: true,
180
+ userId: data.userId,
181
+ organizationId: data.organizationId,
182
+ };
108
183
  }
109
184
 
110
185
  // Key is invalid - cache the negative result too (shorter TTL)
@@ -116,9 +191,12 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
116
191
  // Parse error response
117
192
  const errorData = await response.json().catch(() => ({}));
118
193
  return {
119
- status: response.status,
120
- error: errorData.error?.message || 'Invalid or expired API key',
121
- code: errorData.error?.code || 'INVALID_API_KEY',
194
+ valid: false,
195
+ error: {
196
+ status: response.status,
197
+ error: errorData.error?.message || 'Invalid or expired API key',
198
+ code: errorData.error?.code || 'INVALID_API_KEY',
199
+ },
122
200
  };
123
201
  } catch (error) {
124
202
  console.error('Error validating API key against NetPad:', error);
@@ -127,544 +205,8 @@ async function validateApiKey(req: VercelRequest): Promise<{ status: number; err
127
205
  // This allows the MCP server to work even if NetPad API is temporarily unavailable
128
206
  // but only accepts properly formatted keys
129
207
  console.warn('NetPad API unavailable, accepting key based on format validation only');
130
- return null;
131
- }
132
- }
133
-
134
- // Create and configure the MCP server with NetPad tools
135
- function createNetPadServer(): McpServer {
136
- const server = new McpServer({
137
- name: '@netpad/mcp-server-remote',
138
- version: '1.0.0',
139
- });
140
-
141
- // ============================================================================
142
- // RESOURCES - Documentation
143
- // ============================================================================
144
-
145
- server.resource(
146
- 'netpad-docs',
147
- 'netpad://docs/readme',
148
- async () => ({
149
- contents: [
150
- {
151
- uri: 'netpad://docs/readme',
152
- mimeType: 'text/markdown',
153
- text: `# NetPad MCP Server
154
-
155
- NetPad is a form builder and workflow automation platform.
156
-
157
- ## Quick Start
158
-
159
- Use the available tools to:
160
- - Generate forms from natural language descriptions
161
- - Create workflow automations
162
- - Browse and query MongoDB data
163
- - Search the marketplace for pre-built applications
164
-
165
- ## Available Tools
166
-
167
- - \`generate_form\` - Generate a complete form configuration
168
- - \`list_field_types\` - List all supported field types
169
- - \`list_form_templates\` - Browse pre-built form templates
170
- - \`create_form_from_template\` - Create a form from a template
171
- - \`list_workflow_templates\` - Browse workflow automation templates
172
-
173
- ## Extensions System
174
-
175
- NetPad supports extensions that add custom functionality:
176
-
177
- **Built-in Extensions:**
178
- - **@netpad/cloud-features**: Billing, Atlas provisioning, premium AI features (cloud only)
179
- - **@netpad/collaborate**: Community gallery and collaboration features
180
- - **@netpad/demo-node**: Example extension showing how to create custom workflow nodes (use as template)
181
-
182
- **Extension Capabilities:**
183
- - Custom API routes under /api/ext/{extension-name}/
184
- - Custom workflow node types (see @netpad/demo-node for example)
185
- - Shared services (billing, provisioning, analytics)
186
- - Request/response middleware
187
- - React UI components
188
- - Feature flags
189
-
190
- **Creating Custom Extensions:**
191
- Use @netpad/demo-node as a template:
192
- 1. Copy the demo-node package
193
- 2. Update package.json with your extension name
194
- 3. Modify the node definition and handler in src/index.ts
195
- 4. Export your extension as default
196
- 5. Install and enable via NETPAD_EXTENSIONS
197
-
198
- **Enabling Extensions:**
199
- Set NETPAD_EXTENSIONS environment variable:
200
- \`NETPAD_EXTENSIONS=@netpad/collaborate,@netpad/demo-node,@myorg/custom-extension\`
201
-
202
- **Example: Demo Node Extension**
203
- The @netpad/demo-node extension provides a "Log Message" workflow node that:
204
- - Logs messages with configurable levels (info/warn/error)
205
- - Supports {{variable}} syntax for dynamic values
206
- - Can pass through input data to downstream nodes
207
- - Demonstrates complete extension structure (metadata, node definition, handler, lifecycle hooks)
208
-
209
- For more information, visit https://netpad.io and https://docs.netpad.io/docs/extensions/overview
210
- `,
211
- },
212
- ],
213
- })
214
- );
215
-
216
- // ============================================================================
217
- // TOOLS - Form Building
218
- // ============================================================================
219
-
220
- server.tool(
221
- 'generate_form',
222
- 'Generate a complete NetPad form configuration from a natural language description',
223
- {
224
- description: z.string().describe('Description of the form to generate'),
225
- formName: z.string().describe('Name of the form'),
226
- includeMultiPage: z.boolean().optional().describe('Organize fields into multiple pages'),
227
- },
228
- async ({ description, formName, includeMultiPage }) => {
229
- const slug = formName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
230
-
231
- // Parse description to generate appropriate fields
232
- const fields = parseDescriptionToFields(description);
233
-
234
- const config = {
235
- name: formName,
236
- slug,
237
- description,
238
- fieldConfigs: fields,
239
- multiPage: includeMultiPage ? {
240
- enabled: true,
241
- pages: [{ id: 'page-1', title: 'Form', fields: fields.map(f => f.path) }],
242
- } : undefined,
243
- };
244
-
245
- return {
246
- content: [
247
- {
248
- type: 'text',
249
- text: JSON.stringify(config, null, 2),
250
- },
251
- ],
252
- };
253
- }
254
- );
255
-
256
- server.tool(
257
- 'list_field_types',
258
- 'List all supported field types in NetPad forms',
259
- {},
260
- async () => {
261
- const fieldTypes = [
262
- { id: 'short_text', name: 'Short Text', description: 'Single line text input' },
263
- { id: 'long_text', name: 'Long Text', description: 'Multi-line textarea' },
264
- { id: 'email', name: 'Email', description: 'Email input with validation' },
265
- { id: 'phone', name: 'Phone', description: 'Phone number input' },
266
- { id: 'number', name: 'Number', description: 'Numeric input' },
267
- { id: 'date', name: 'Date', description: 'Date picker' },
268
- { id: 'time', name: 'Time', description: 'Time picker' },
269
- { id: 'datetime', name: 'Date & Time', description: 'Combined date and time picker' },
270
- { id: 'dropdown', name: 'Dropdown', description: 'Single select dropdown' },
271
- { id: 'multiple_choice', name: 'Multiple Choice', description: 'Radio button selection' },
272
- { id: 'checkboxes', name: 'Checkboxes', description: 'Multi-select checkboxes' },
273
- { id: 'yes_no', name: 'Yes/No', description: 'Boolean toggle' },
274
- { id: 'rating', name: 'Rating', description: 'Star rating input' },
275
- { id: 'slider', name: 'Slider', description: 'Range slider' },
276
- { id: 'file', name: 'File Upload', description: 'File attachment' },
277
- { id: 'image', name: 'Image Upload', description: 'Image upload with preview' },
278
- { id: 'signature', name: 'Signature', description: 'Signature capture' },
279
- { id: 'url', name: 'URL', description: 'URL input with validation' },
280
- { id: 'currency', name: 'Currency', description: 'Monetary value input' },
281
- { id: 'address', name: 'Address', description: 'Address autocomplete' },
282
- { id: 'section_header', name: 'Section Header', description: 'Section divider with title' },
283
- { id: 'paragraph', name: 'Paragraph', description: 'Static text/instructions' },
284
- { id: 'hidden', name: 'Hidden', description: 'Hidden field for data' },
285
- ];
286
-
287
- return {
288
- content: [
289
- {
290
- type: 'text',
291
- text: JSON.stringify(fieldTypes, null, 2),
292
- },
293
- ],
294
- };
295
- }
296
- );
297
-
298
- server.tool(
299
- 'list_form_templates',
300
- 'List available pre-built form templates',
301
- {
302
- category: z.string().optional().describe('Filter by category'),
303
- },
304
- async ({ category }) => {
305
- const templates = [
306
- { id: 'contact-form', name: 'Contact Form', category: 'Business', description: 'Basic contact form' },
307
- { id: 'lead-capture', name: 'Lead Capture', category: 'Business', description: 'Sales lead collection' },
308
- { id: 'newsletter', name: 'Newsletter Signup', category: 'Business', description: 'Email subscription' },
309
- { id: 'event-registration', name: 'Event Registration', category: 'Events', description: 'Event signup form' },
310
- { id: 'rsvp', name: 'RSVP', category: 'Events', description: 'Event RSVP form' },
311
- { id: 'customer-feedback', name: 'Customer Feedback', category: 'Feedback', description: 'Customer satisfaction survey' },
312
- { id: 'nps-survey', name: 'NPS Survey', category: 'Feedback', description: 'Net Promoter Score survey' },
313
- { id: 'support-ticket', name: 'Support Ticket', category: 'Support', description: 'Help desk ticket form' },
314
- { id: 'job-application', name: 'Job Application', category: 'HR', description: 'Employment application' },
315
- { id: 'patient-intake', name: 'Patient Intake', category: 'Healthcare', description: 'Medical intake form' },
316
- { id: 'order-form', name: 'Order Form', category: 'E-commerce', description: 'Product order form' },
317
- { id: 'expense-report', name: 'Expense Report', category: 'Finance', description: 'Expense submission form' },
318
- ];
319
-
320
- const filtered = category
321
- ? templates.filter(t => t.category.toLowerCase() === category.toLowerCase())
322
- : templates;
323
-
324
- return {
325
- content: [
326
- {
327
- type: 'text',
328
- text: JSON.stringify(filtered, null, 2),
329
- },
330
- ],
331
- };
332
- }
333
- );
334
-
335
- server.tool(
336
- 'create_form_from_template',
337
- 'Create a form configuration from a pre-built template',
338
- {
339
- templateId: z.string().describe('Template ID (e.g., "contact-form", "lead-capture")'),
340
- formName: z.string().describe('Name for the new form'),
341
- customizations: z.object({
342
- additionalFields: z.array(z.string()).optional(),
343
- removeFields: z.array(z.string()).optional(),
344
- }).optional().describe('Optional customizations'),
345
- },
346
- async ({ templateId, formName, customizations }) => {
347
- const templateConfigs: Record<string, { fields: Array<{ path: string; label: string; type: string; required?: boolean; options?: Array<{ label: string; value: string }> }> }> = {
348
- 'contact-form': {
349
- fields: [
350
- { path: 'name', label: 'Name', type: 'short_text', required: true },
351
- { path: 'email', label: 'Email', type: 'email', required: true },
352
- { path: 'phone', label: 'Phone', type: 'phone' },
353
- { path: 'message', label: 'Message', type: 'long_text', required: true },
354
- ],
355
- },
356
- 'lead-capture': {
357
- fields: [
358
- { path: 'firstName', label: 'First Name', type: 'short_text', required: true },
359
- { path: 'lastName', label: 'Last Name', type: 'short_text', required: true },
360
- { path: 'email', label: 'Work Email', type: 'email', required: true },
361
- { path: 'company', label: 'Company', type: 'short_text', required: true },
362
- { path: 'jobTitle', label: 'Job Title', type: 'short_text' },
363
- { path: 'phone', label: 'Phone', type: 'phone' },
364
- { path: 'interest', label: 'Interest', type: 'dropdown', options: [
365
- { label: 'Product Demo', value: 'demo' },
366
- { label: 'Pricing', value: 'pricing' },
367
- { label: 'Partnership', value: 'partnership' },
368
- { label: 'Other', value: 'other' },
369
- ]},
370
- ],
371
- },
372
- 'customer-feedback': {
373
- fields: [
374
- { path: 'satisfaction', label: 'Overall Satisfaction', type: 'rating', required: true },
375
- { path: 'recommend', label: 'How likely are you to recommend us?', type: 'slider' },
376
- { path: 'liked', label: 'What did you like?', type: 'checkboxes', options: [
377
- { label: 'Product Quality', value: 'quality' },
378
- { label: 'Customer Service', value: 'service' },
379
- { label: 'Pricing', value: 'pricing' },
380
- { label: 'Ease of Use', value: 'ease' },
381
- ]},
382
- { path: 'improvements', label: 'What could we improve?', type: 'long_text' },
383
- { path: 'email', label: 'Email (optional)', type: 'email' },
384
- ],
385
- },
386
- 'support-ticket': {
387
- fields: [
388
- { path: 'name', label: 'Name', type: 'short_text', required: true },
389
- { path: 'email', label: 'Email', type: 'email', required: true },
390
- { path: 'category', label: 'Category', type: 'dropdown', required: true, options: [
391
- { label: 'Technical Issue', value: 'technical' },
392
- { label: 'Billing', value: 'billing' },
393
- { label: 'Feature Request', value: 'feature' },
394
- { label: 'General Inquiry', value: 'general' },
395
- ]},
396
- { path: 'priority', label: 'Priority', type: 'multiple_choice', options: [
397
- { label: 'Low', value: 'low' },
398
- { label: 'Medium', value: 'medium' },
399
- { label: 'High', value: 'high' },
400
- { label: 'Critical', value: 'critical' },
401
- ]},
402
- { path: 'subject', label: 'Subject', type: 'short_text', required: true },
403
- { path: 'description', label: 'Description', type: 'long_text', required: true },
404
- { path: 'attachment', label: 'Attachment', type: 'file' },
405
- ],
406
- },
407
- };
408
-
409
- const template = templateConfigs[templateId];
410
- if (!template) {
411
- return {
412
- content: [
413
- {
414
- type: 'text',
415
- text: `Template "${templateId}" not found. Available templates: ${Object.keys(templateConfigs).join(', ')}`,
416
- },
417
- ],
418
- };
419
- }
420
-
421
- let fields = [...template.fields];
422
-
423
- if (customizations?.removeFields) {
424
- fields = fields.filter(f => !customizations.removeFields!.includes(f.path));
425
- }
426
-
427
- const slug = formName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
428
-
429
- return {
430
- content: [
431
- {
432
- type: 'text',
433
- text: JSON.stringify({
434
- name: formName,
435
- slug,
436
- templateId,
437
- fieldConfigs: fields,
438
- }, null, 2),
439
- },
440
- ],
441
- };
442
- }
443
- );
444
-
445
- // ============================================================================
446
- // TOOLS - Workflow Templates
447
- // ============================================================================
448
-
449
- server.tool(
450
- 'list_workflow_templates',
451
- 'List available workflow automation templates',
452
- {},
453
- async () => {
454
- const templates = [
455
- {
456
- id: 'form-to-email',
457
- name: 'Form to Email',
458
- description: 'Send email notification on form submission',
459
- nodes: ['form-trigger', 'email-send'],
460
- },
461
- {
462
- id: 'form-to-database',
463
- name: 'Form to Database',
464
- description: 'Save form submissions to MongoDB',
465
- nodes: ['form-trigger', 'mongodb-insert'],
466
- },
467
- {
468
- id: 'lead-qualification',
469
- name: 'Lead Qualification',
470
- description: 'Score and route leads based on criteria',
471
- nodes: ['form-trigger', 'condition', 'email-send', 'slack-message'],
472
- },
473
- {
474
- id: 'scheduled-report',
475
- name: 'Scheduled Report',
476
- description: 'Generate and email periodic reports',
477
- nodes: ['schedule-trigger', 'mongodb-query', 'transform', 'email-send'],
478
- },
479
- {
480
- id: 'webhook-processor',
481
- name: 'Webhook Processor',
482
- description: 'Process incoming webhooks and store data',
483
- nodes: ['webhook-trigger', 'transform', 'mongodb-insert'],
484
- },
485
- ];
486
-
487
- return {
488
- content: [
489
- {
490
- type: 'text',
491
- text: JSON.stringify(templates, null, 2),
492
- },
493
- ],
494
- };
495
- }
496
- );
497
-
498
- server.tool(
499
- 'list_workflow_node_types',
500
- 'List available workflow node types for building automations',
501
- {
502
- category: z.string().optional().describe('Filter by category'),
503
- },
504
- async ({ category }) => {
505
- const nodeTypes = [
506
- // Triggers
507
- { id: 'form-trigger', name: 'Form Submission', category: 'Triggers', description: 'Trigger on form submission' },
508
- { id: 'webhook-trigger', name: 'Webhook', category: 'Triggers', description: 'Trigger on webhook call' },
509
- { id: 'schedule-trigger', name: 'Schedule', category: 'Triggers', description: 'Trigger on schedule (cron)' },
510
- { id: 'manual-trigger', name: 'Manual', category: 'Triggers', description: 'Trigger manually' },
511
-
512
- // Logic
513
- { id: 'condition', name: 'Condition', category: 'Logic', description: 'Branch based on conditions' },
514
- { id: 'switch', name: 'Switch', category: 'Logic', description: 'Multi-way branching' },
515
- { id: 'loop', name: 'Loop', category: 'Logic', description: 'Iterate over items' },
516
- { id: 'delay', name: 'Delay', category: 'Logic', description: 'Wait for duration' },
517
-
518
- // Data
519
- { id: 'transform', name: 'Transform', category: 'Data', description: 'Transform data with expressions' },
520
- { id: 'code', name: 'Code', category: 'Data', description: 'Run custom JavaScript' },
521
- { id: 'set-variable', name: 'Set Variable', category: 'Data', description: 'Set workflow variable' },
522
-
523
- // Database
524
- { id: 'mongodb-query', name: 'MongoDB Query', category: 'Database', description: 'Query MongoDB collection' },
525
- { id: 'mongodb-insert', name: 'MongoDB Insert', category: 'Database', description: 'Insert document' },
526
- { id: 'mongodb-update', name: 'MongoDB Update', category: 'Database', description: 'Update documents' },
527
- { id: 'mongodb-delete', name: 'MongoDB Delete', category: 'Database', description: 'Delete documents' },
528
-
529
- // Communication
530
- { id: 'email-send', name: 'Send Email', category: 'Communication', description: 'Send email via SMTP' },
531
- { id: 'slack-message', name: 'Slack Message', category: 'Communication', description: 'Send Slack message' },
532
- { id: 'http-request', name: 'HTTP Request', category: 'Communication', description: 'Make HTTP API call' },
533
-
534
- // AI
535
- { id: 'ai-generate', name: 'AI Generate', category: 'AI', description: 'Generate text with AI' },
536
- { id: 'ai-classify', name: 'AI Classify', category: 'AI', description: 'Classify with AI' },
537
- { id: 'ai-extract', name: 'AI Extract', category: 'AI', description: 'Extract structured data' },
538
- ];
539
-
540
- const filtered = category
541
- ? nodeTypes.filter(n => n.category.toLowerCase() === category.toLowerCase())
542
- : nodeTypes;
543
-
544
- return {
545
- content: [
546
- {
547
- type: 'text',
548
- text: JSON.stringify(filtered, null, 2),
549
- },
550
- ],
551
- };
552
- }
553
- );
554
-
555
- // ============================================================================
556
- // TOOLS - MongoDB Data Browser
557
- // ============================================================================
558
-
559
- server.tool(
560
- 'generate_mongodb_query',
561
- 'Generate a MongoDB query based on natural language description',
562
- {
563
- description: z.string().describe('Description of what to query'),
564
- collection: z.string().describe('Collection name'),
565
- operation: z.enum(['find', 'aggregate', 'count', 'distinct']).optional().describe('Query operation'),
566
- },
567
- async ({ description, collection, operation = 'find' }) => {
568
- // Simple query generation based on description
569
- const descLower = description.toLowerCase();
570
-
571
- let filter: Record<string, unknown> = {};
572
- let sort: Record<string, number> | undefined;
573
- let limit: number | undefined;
574
-
575
- if (descLower.includes('recent') || descLower.includes('latest')) {
576
- sort = { createdAt: -1 };
577
- limit = 10;
578
- }
579
-
580
- if (descLower.includes('active')) {
581
- filter['status'] = 'active';
582
- }
583
-
584
- if (descLower.includes('today')) {
585
- const today = new Date();
586
- today.setHours(0, 0, 0, 0);
587
- filter['createdAt'] = { $gte: today.toISOString() };
588
- }
589
-
590
- const query = {
591
- collection,
592
- operation,
593
- filter,
594
- sort,
595
- limit,
596
- code: operation === 'find'
597
- ? `db.collection('${collection}').find(${JSON.stringify(filter)})${sort ? `.sort(${JSON.stringify(sort)})` : ''}${limit ? `.limit(${limit})` : ''}.toArray()`
598
- : `db.collection('${collection}').${operation}(${JSON.stringify(filter)})`,
599
- };
600
-
601
- return {
602
- content: [
603
- {
604
- type: 'text',
605
- text: JSON.stringify(query, null, 2),
606
- },
607
- ],
608
- };
609
- }
610
- );
611
-
612
- return server;
613
- }
614
-
615
- // Helper function to parse natural language description into form fields
616
- function parseDescriptionToFields(description: string): Array<{
617
- path: string;
618
- label: string;
619
- type: string;
620
- required?: boolean;
621
- options?: Array<{ label: string; value: string }>;
622
- }> {
623
- const fields: Array<{
624
- path: string;
625
- label: string;
626
- type: string;
627
- required?: boolean;
628
- options?: Array<{ label: string; value: string }>;
629
- }> = [];
630
- const descLower = description.toLowerCase();
631
-
632
- // Common field patterns
633
- if (descLower.includes('name') || descLower.includes('contact')) {
634
- fields.push({ path: 'name', label: 'Name', type: 'short_text', required: true });
635
- }
636
- if (descLower.includes('email')) {
637
- fields.push({ path: 'email', label: 'Email', type: 'email', required: true });
638
- }
639
- if (descLower.includes('phone') || descLower.includes('contact')) {
640
- fields.push({ path: 'phone', label: 'Phone', type: 'phone' });
641
- }
642
- if (descLower.includes('message') || descLower.includes('comment') || descLower.includes('feedback')) {
643
- fields.push({ path: 'message', label: 'Message', type: 'long_text' });
644
- }
645
- if (descLower.includes('date') || descLower.includes('when')) {
646
- fields.push({ path: 'date', label: 'Date', type: 'date' });
647
- }
648
- if (descLower.includes('address') || descLower.includes('location')) {
649
- fields.push({ path: 'address', label: 'Address', type: 'address' });
208
+ return { valid: true };
650
209
  }
651
- if (descLower.includes('rating') || descLower.includes('score')) {
652
- fields.push({ path: 'rating', label: 'Rating', type: 'rating' });
653
- }
654
- if (descLower.includes('file') || descLower.includes('upload') || descLower.includes('attachment')) {
655
- fields.push({ path: 'file', label: 'File Upload', type: 'file' });
656
- }
657
-
658
- // If no fields detected, create basic contact fields
659
- if (fields.length === 0) {
660
- fields.push(
661
- { path: 'name', label: 'Name', type: 'short_text', required: true },
662
- { path: 'email', label: 'Email', type: 'email', required: true },
663
- { path: 'message', label: 'Message', type: 'long_text' }
664
- );
665
- }
666
-
667
- return fields;
668
210
  }
669
211
 
670
212
  // Main handler
@@ -678,12 +220,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
678
220
  // ============================================================================
679
221
  // AUTHENTICATE REQUEST
680
222
  // ============================================================================
681
- const authError = await validateApiKey(req);
682
- if (authError) {
683
- res.status(authError.status).json({
684
- error: authError.error,
685
- code: authError.code,
686
- hint: 'Generate an API key at netpad.io/settings and add it to Claude\'s connector settings under "Advanced settings".',
223
+ const authResult = await validateRequest(req);
224
+ if (!authResult.valid) {
225
+ res.status(authResult.error?.status || 401).json({
226
+ error: authResult.error?.error || 'Authentication failed',
227
+ code: authResult.error?.code || 'AUTH_FAILED',
228
+ hint: 'Connect via Claude.ai Settings > Connectors, or use an API key from netpad.io/settings',
687
229
  });
688
230
  return;
689
231
  }
@@ -698,7 +240,11 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
698
240
  sessionIdGenerator: () => crypto.randomUUID(),
699
241
  });
700
242
 
701
- const server = createNetPadServer();
243
+ // Create the full NetPad MCP server with all 80+ tools
244
+ const server = createNetPadMcpServer({
245
+ name: '@netpad/mcp-server-remote',
246
+ version: '1.2.0',
247
+ });
702
248
  await server.connect(transport);
703
249
 
704
250
  // Store transport for future requests
@@ -724,7 +270,11 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
724
270
  sessionIdGenerator: () => crypto.randomUUID(),
725
271
  });
726
272
 
727
- const server = createNetPadServer();
273
+ // Create the full NetPad MCP server with all 80+ tools
274
+ const server = createNetPadMcpServer({
275
+ name: '@netpad/mcp-server-remote',
276
+ version: '1.2.0',
277
+ });
728
278
  await server.connect(transport);
729
279
 
730
280
  if (transport.sessionId) {