@netpad/mcp-server-remote 1.0.1
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/.env.example +11 -0
- package/README.md +210 -0
- package/api/health.ts +9 -0
- package/api/index.ts +14 -0
- package/api/mcp.ts +745 -0
- package/netpad-mcp-server-remote-1.0.1.tgz +0 -0
- package/package.json +26 -0
- package/tsconfig.json +19 -0
- package/vercel.json +36 -0
package/.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# NetPad Remote MCP Server - Environment Variables
|
|
2
|
+
# Copy this file to .env.local for local development
|
|
3
|
+
|
|
4
|
+
# Optional: NetPad API URL for key validation
|
|
5
|
+
# Default: https://netpad.io
|
|
6
|
+
# Set this if you're running NetPad locally or self-hosted
|
|
7
|
+
# NETPAD_API_URL=http://localhost:3000
|
|
8
|
+
|
|
9
|
+
# Note: API keys are NOT stored here!
|
|
10
|
+
# Users provide their NetPad API key (np_live_xxx) in the Authorization header.
|
|
11
|
+
# The server validates keys against the NetPad API at NETPAD_API_URL.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @netpad/mcp-server-remote
|
|
2
|
+
|
|
3
|
+
Remote MCP server for NetPad, deployable to Vercel for use with Claude Custom Connectors.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides a remote HTTP-based MCP server that can be used as a **Claude Custom Connector**. Unlike the stdio-based `@netpad/mcp-server` (for Claude Desktop), this server runs as a web service that Claude can connect to over the internet.
|
|
8
|
+
|
|
9
|
+
**Authentication:** Uses your existing NetPad API keys (`np_live_xxx` or `np_test_xxx`).
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Get Your NetPad API Key
|
|
14
|
+
|
|
15
|
+
1. Go to [netpad.io/settings](https://netpad.io/settings)
|
|
16
|
+
2. Click the **API Keys** tab
|
|
17
|
+
3. Click **Create API Key**
|
|
18
|
+
4. Give it a name like "Claude MCP Connector"
|
|
19
|
+
5. Select permissions (recommended: `forms:read`, `submissions:read`)
|
|
20
|
+
6. Copy the generated key (starts with `np_live_` or `np_test_`)
|
|
21
|
+
|
|
22
|
+
> **Important:** The full key is only shown once. Save it securely!
|
|
23
|
+
|
|
24
|
+
### 2. Deploy to Vercel
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd packages/mcp-server-remote
|
|
28
|
+
npm install
|
|
29
|
+
vercel --prod
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or deploy via the Vercel dashboard by importing this directory.
|
|
33
|
+
|
|
34
|
+
### 3. Configure Environment Variables (Optional)
|
|
35
|
+
|
|
36
|
+
By default, the server validates API keys against the NetPad API at `https://netpad.io`.
|
|
37
|
+
|
|
38
|
+
For self-hosted NetPad or development:
|
|
39
|
+
|
|
40
|
+
| Variable | Required | Description |
|
|
41
|
+
|----------|----------|-------------|
|
|
42
|
+
| `NETPAD_API_URL` | No | NetPad API base URL (default: `https://netpad.io`) |
|
|
43
|
+
|
|
44
|
+
### 4. Configure in Claude
|
|
45
|
+
|
|
46
|
+
1. Go to **Claude Settings > Connectors**
|
|
47
|
+
2. Click **"Add custom connector"**
|
|
48
|
+
3. Enter your connector name: `NetPad`
|
|
49
|
+
4. Enter your Vercel deployment URL:
|
|
50
|
+
```
|
|
51
|
+
https://your-app.vercel.app/mcp
|
|
52
|
+
```
|
|
53
|
+
5. Click **"Advanced settings"**
|
|
54
|
+
6. In the **OAuth Client Secret** field, enter your NetPad API key: `np_live_xxxxx`
|
|
55
|
+
|
|
56
|
+
> **Note:** Claude's custom connector UI uses OAuth fields, but we use them for Bearer token auth.
|
|
57
|
+
|
|
58
|
+
7. Click **Add**
|
|
59
|
+
|
|
60
|
+
### 5. Use in Conversations
|
|
61
|
+
|
|
62
|
+
In any Claude conversation, enable the NetPad connector via the "+" menu > Connectors.
|
|
63
|
+
|
|
64
|
+
## Authentication
|
|
65
|
+
|
|
66
|
+
All requests to the MCP server require a valid NetPad API key in the `Authorization` header:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Authorization: Bearer np_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### How It Works
|
|
73
|
+
|
|
74
|
+
1. You send a request with your NetPad API key
|
|
75
|
+
2. The MCP server validates the key against the NetPad API (`/api/v1/auth/validate`)
|
|
76
|
+
3. Valid keys are cached for 5 minutes to reduce API calls
|
|
77
|
+
4. The request proceeds if the key is valid
|
|
78
|
+
|
|
79
|
+
### Error Responses
|
|
80
|
+
|
|
81
|
+
| Status | Code | Description |
|
|
82
|
+
|--------|------|-------------|
|
|
83
|
+
| 401 | `MISSING_API_KEY` | No Authorization header provided |
|
|
84
|
+
| 401 | `INVALID_AUTH_FORMAT` | Header doesn't use Bearer format |
|
|
85
|
+
| 401 | `INVALID_API_KEY_FORMAT` | Key doesn't start with `np_live_` or `np_test_` |
|
|
86
|
+
| 401 | `INVALID_API_KEY` | Key is invalid, expired, or revoked |
|
|
87
|
+
| 403 | `INSUFFICIENT_PERMISSIONS` | Key lacks required permissions |
|
|
88
|
+
| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |
|
|
89
|
+
|
|
90
|
+
### Key Management
|
|
91
|
+
|
|
92
|
+
Manage your API keys at [netpad.io/settings](https://netpad.io/settings):
|
|
93
|
+
|
|
94
|
+
- **Create** new keys with specific permissions
|
|
95
|
+
- **Revoke** compromised keys (takes effect immediately)
|
|
96
|
+
- **Monitor** usage statistics
|
|
97
|
+
- **Set expiration** dates for temporary access
|
|
98
|
+
|
|
99
|
+
## Available Tools
|
|
100
|
+
|
|
101
|
+
The remote server includes a subset of the full NetPad MCP tools:
|
|
102
|
+
|
|
103
|
+
### Form Building
|
|
104
|
+
- `generate_form` - Generate form configurations from natural language
|
|
105
|
+
- `list_field_types` - List all 23+ supported field types
|
|
106
|
+
- `list_form_templates` - Browse pre-built form templates
|
|
107
|
+
- `create_form_from_template` - Create forms from templates
|
|
108
|
+
|
|
109
|
+
### Workflow Automation
|
|
110
|
+
- `list_workflow_templates` - Browse workflow templates
|
|
111
|
+
- `list_workflow_node_types` - List available workflow nodes
|
|
112
|
+
|
|
113
|
+
### Data
|
|
114
|
+
- `generate_mongodb_query` - Generate MongoDB queries
|
|
115
|
+
|
|
116
|
+
## Local Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# Install dependencies
|
|
120
|
+
npm install
|
|
121
|
+
|
|
122
|
+
# Create .env.local
|
|
123
|
+
cp .env.example .env.local
|
|
124
|
+
|
|
125
|
+
# Set your local NetPad URL (if running NetPad locally)
|
|
126
|
+
# NETPAD_API_URL=http://localhost:3000
|
|
127
|
+
|
|
128
|
+
# Run locally with Vercel CLI
|
|
129
|
+
npm run dev
|
|
130
|
+
|
|
131
|
+
# The server will be available at http://localhost:3000/mcp
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Testing Authentication Locally
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Test with your NetPad API key
|
|
138
|
+
curl -X POST http://localhost:3000/mcp \
|
|
139
|
+
-H "Authorization: Bearer np_live_your_key_here" \
|
|
140
|
+
-H "Content-Type: application/json" \
|
|
141
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
142
|
+
|
|
143
|
+
# Test without API key (should return 401)
|
|
144
|
+
curl -X POST http://localhost:3000/mcp \
|
|
145
|
+
-H "Content-Type: application/json" \
|
|
146
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## API Endpoints
|
|
150
|
+
|
|
151
|
+
| Endpoint | Auth Required | Description |
|
|
152
|
+
|----------|---------------|-------------|
|
|
153
|
+
| `GET /` | No | Server info and documentation |
|
|
154
|
+
| `GET /health` | No | Health check endpoint |
|
|
155
|
+
| `GET /mcp` | **Yes** | SSE stream for MCP communication |
|
|
156
|
+
| `POST /mcp` | **Yes** | JSON-RPC message endpoint |
|
|
157
|
+
|
|
158
|
+
## Architecture
|
|
159
|
+
|
|
160
|
+
This server uses the MCP SDK's `StreamableHTTPServerTransport` which implements the MCP Streamable HTTP transport specification. It supports:
|
|
161
|
+
|
|
162
|
+
- Server-Sent Events (SSE) for server-to-client messages
|
|
163
|
+
- HTTP POST for client-to-server messages
|
|
164
|
+
- Session management via `Mcp-Session-Id` header
|
|
165
|
+
- Bearer token authentication via NetPad API
|
|
166
|
+
|
|
167
|
+
### Authentication Flow
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
┌─────────┐ ┌───────────────────┐ ┌────────────┐
|
|
171
|
+
│ Claude │────▶│ MCP Server Remote │────▶│ NetPad │
|
|
172
|
+
│ │ │ (Vercel) │ │ API │
|
|
173
|
+
└─────────┘ └───────────────────┘ └────────────┘
|
|
174
|
+
│ │ │
|
|
175
|
+
│ 1. Request with │ │
|
|
176
|
+
│ API key │ │
|
|
177
|
+
│──────────────────▶│ │
|
|
178
|
+
│ │ 2. Validate key │
|
|
179
|
+
│ │─────────────────────▶│
|
|
180
|
+
│ │ │
|
|
181
|
+
│ │ 3. Key info │
|
|
182
|
+
│ │◀─────────────────────│
|
|
183
|
+
│ │ │
|
|
184
|
+
│ 4. MCP Response │ (cached 5 min) │
|
|
185
|
+
│◀──────────────────│ │
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Environment Variables
|
|
189
|
+
|
|
190
|
+
| Variable | Required | Description |
|
|
191
|
+
|----------|----------|-------------|
|
|
192
|
+
| `NETPAD_API_URL` | No | NetPad API base URL (default: `https://netpad.io`) |
|
|
193
|
+
|
|
194
|
+
## Security Considerations
|
|
195
|
+
|
|
196
|
+
- **Centralized Key Management:** Keys are managed in NetPad, not in environment variables
|
|
197
|
+
- **Instant Revocation:** Revoked keys stop working within 5 minutes (cache TTL)
|
|
198
|
+
- **Usage Tracking:** All key usage is logged in NetPad
|
|
199
|
+
- **Rate Limiting:** Respects NetPad's per-key rate limits
|
|
200
|
+
- **HTTPS:** Always use HTTPS in production (Vercel provides this automatically)
|
|
201
|
+
- **Permissions:** Keys can be scoped to specific permissions
|
|
202
|
+
|
|
203
|
+
## Related Packages
|
|
204
|
+
|
|
205
|
+
- [@netpad/mcp-server](../mcp-server) - Local stdio MCP server for Claude Desktop (no auth required)
|
|
206
|
+
- [NetPad Platform](https://netpad.io) - Full form builder platform
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
Apache-2.0
|
package/api/health.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
2
|
+
|
|
3
|
+
export default function handler(req: VercelRequest, res: VercelResponse) {
|
|
4
|
+
res.status(200).json({
|
|
5
|
+
status: 'healthy',
|
|
6
|
+
timestamp: new Date().toISOString(),
|
|
7
|
+
service: '@netpad/mcp-server-remote',
|
|
8
|
+
});
|
|
9
|
+
}
|
package/api/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
2
|
+
|
|
3
|
+
export default function handler(req: VercelRequest, res: VercelResponse) {
|
|
4
|
+
res.status(200).json({
|
|
5
|
+
name: '@netpad/mcp-server-remote',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
description: 'NetPad MCP Server - Remote API for Claude Custom Connectors',
|
|
8
|
+
endpoints: {
|
|
9
|
+
mcp: '/mcp',
|
|
10
|
+
health: '/health',
|
|
11
|
+
},
|
|
12
|
+
documentation: 'https://github.com/mrlynn/netpad-v3/tree/main/packages/mcp-server-remote',
|
|
13
|
+
});
|
|
14
|
+
}
|
package/api/mcp.ts
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// Store transports by session ID for reconnection
|
|
8
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// API KEY AUTHENTICATION
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
// Cache validated API keys for 5 minutes to reduce API calls
|
|
15
|
+
const apiKeyCache = new Map<string, { valid: boolean; organizationId?: string; expiresAt: number }>();
|
|
16
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the NetPad API base URL from environment or default
|
|
20
|
+
*/
|
|
21
|
+
function getNetPadApiUrl(): string {
|
|
22
|
+
return process.env.NETPAD_API_URL || 'https://netpad.io';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
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
|
|
30
|
+
*
|
|
31
|
+
* @returns null if valid, or an error response object if invalid
|
|
32
|
+
*/
|
|
33
|
+
async function validateApiKey(req: VercelRequest): Promise<{ status: number; error: string; code: string } | null> {
|
|
34
|
+
const authHeader = req.headers['authorization'];
|
|
35
|
+
|
|
36
|
+
// Check if Authorization header is present
|
|
37
|
+
if (!authHeader) {
|
|
38
|
+
return {
|
|
39
|
+
status: 401,
|
|
40
|
+
error: 'Missing Authorization header. Use: Authorization: Bearer np_live_xxx',
|
|
41
|
+
code: 'MISSING_API_KEY',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if it's a Bearer token
|
|
46
|
+
const parts = authHeader.split(' ');
|
|
47
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
|
|
48
|
+
return {
|
|
49
|
+
status: 401,
|
|
50
|
+
error: 'Invalid Authorization header format. Use: Authorization: Bearer np_live_xxx',
|
|
51
|
+
code: 'INVALID_AUTH_FORMAT',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const apiKey = parts[1].trim();
|
|
56
|
+
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
return {
|
|
59
|
+
status: 401,
|
|
60
|
+
error: 'API key is empty',
|
|
61
|
+
code: 'EMPTY_API_KEY',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validate key format (must start with np_live_ or np_test_)
|
|
66
|
+
if (!apiKey.startsWith('np_live_') && !apiKey.startsWith('np_test_')) {
|
|
67
|
+
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',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check cache first
|
|
75
|
+
const cached = apiKeyCache.get(apiKey);
|
|
76
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
77
|
+
if (cached.valid) {
|
|
78
|
+
return null; // Valid key from cache
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
status: 401,
|
|
82
|
+
error: 'Invalid or expired API key',
|
|
83
|
+
code: 'INVALID_API_KEY',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate against NetPad API
|
|
88
|
+
try {
|
|
89
|
+
const netpadUrl = getNetPadApiUrl();
|
|
90
|
+
const response = await fetch(`${netpadUrl}/api/v1/auth/validate`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({ source: 'mcp-server-remote' }),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (response.ok) {
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
// Cache the valid result
|
|
102
|
+
apiKeyCache.set(apiKey, {
|
|
103
|
+
valid: true,
|
|
104
|
+
organizationId: data.organizationId,
|
|
105
|
+
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
106
|
+
});
|
|
107
|
+
return null; // Valid key
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Key is invalid - cache the negative result too (shorter TTL)
|
|
111
|
+
apiKeyCache.set(apiKey, {
|
|
112
|
+
valid: false,
|
|
113
|
+
expiresAt: Date.now() + 60 * 1000, // Cache invalid keys for 1 minute
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Parse error response
|
|
117
|
+
const errorData = await response.json().catch(() => ({}));
|
|
118
|
+
return {
|
|
119
|
+
status: response.status,
|
|
120
|
+
error: errorData.error?.message || 'Invalid or expired API key',
|
|
121
|
+
code: errorData.error?.code || 'INVALID_API_KEY',
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error validating API key against NetPad:', error);
|
|
125
|
+
|
|
126
|
+
// On network error, fall back to format validation only
|
|
127
|
+
// This allows the MCP server to work even if NetPad API is temporarily unavailable
|
|
128
|
+
// but only accepts properly formatted keys
|
|
129
|
+
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' });
|
|
650
|
+
}
|
|
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
|
+
}
|
|
669
|
+
|
|
670
|
+
// Main handler
|
|
671
|
+
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
672
|
+
// Handle CORS preflight
|
|
673
|
+
if (req.method === 'OPTIONS') {
|
|
674
|
+
res.status(200).end();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ============================================================================
|
|
679
|
+
// AUTHENTICATE REQUEST
|
|
680
|
+
// ============================================================================
|
|
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".',
|
|
687
|
+
});
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Get session ID from headers
|
|
692
|
+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
693
|
+
|
|
694
|
+
// Handle GET requests for SSE streams
|
|
695
|
+
if (req.method === 'GET') {
|
|
696
|
+
// For initialization or reconnection
|
|
697
|
+
const transport = new StreamableHTTPServerTransport({
|
|
698
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const server = createNetPadServer();
|
|
702
|
+
await server.connect(transport);
|
|
703
|
+
|
|
704
|
+
// Store transport for future requests
|
|
705
|
+
if (transport.sessionId) {
|
|
706
|
+
transports.set(transport.sessionId, transport);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
await transport.handleRequest(
|
|
710
|
+
req as unknown as IncomingMessage,
|
|
711
|
+
res as unknown as ServerResponse
|
|
712
|
+
);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Handle POST requests
|
|
717
|
+
if (req.method === 'POST') {
|
|
718
|
+
// Try to reuse existing transport for this session
|
|
719
|
+
let transport = sessionId ? transports.get(sessionId) : undefined;
|
|
720
|
+
|
|
721
|
+
if (!transport) {
|
|
722
|
+
// Create new transport for this request
|
|
723
|
+
transport = new StreamableHTTPServerTransport({
|
|
724
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
const server = createNetPadServer();
|
|
728
|
+
await server.connect(transport);
|
|
729
|
+
|
|
730
|
+
if (transport.sessionId) {
|
|
731
|
+
transports.set(transport.sessionId, transport);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
await transport.handleRequest(
|
|
736
|
+
req as unknown as IncomingMessage,
|
|
737
|
+
res as unknown as ServerResponse,
|
|
738
|
+
req.body
|
|
739
|
+
);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Method not allowed
|
|
744
|
+
res.status(405).json({ error: 'Method not allowed' });
|
|
745
|
+
}
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@netpad/mcp-server-remote",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Remote MCP server for NetPad - deployable to Vercel for Claude custom connectors",
|
|
5
|
+
"author": "MongoDB",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vercel dev",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"deploy": "vercel --prod"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
15
|
+
"zod": "^3.23.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20.0.0",
|
|
19
|
+
"@vercel/node": "^3.0.0",
|
|
20
|
+
"typescript": "^5.4.0",
|
|
21
|
+
"vercel": "^37.0.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "."
|
|
16
|
+
},
|
|
17
|
+
"include": ["api/**/*", "lib/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
package/vercel.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"rewrites": [
|
|
4
|
+
{
|
|
5
|
+
"source": "/mcp",
|
|
6
|
+
"destination": "/api/mcp"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"source": "/health",
|
|
10
|
+
"destination": "/api/health"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"source": "/",
|
|
14
|
+
"destination": "/api/index"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"headers": [
|
|
18
|
+
{
|
|
19
|
+
"source": "/(.*)",
|
|
20
|
+
"headers": [
|
|
21
|
+
{
|
|
22
|
+
"key": "Access-Control-Allow-Origin",
|
|
23
|
+
"value": "*"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"key": "Access-Control-Allow-Methods",
|
|
27
|
+
"value": "GET, POST, OPTIONS"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"key": "Access-Control-Allow-Headers",
|
|
31
|
+
"value": "Content-Type, Authorization, X-Session-Id, Mcp-Session-Id"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|