@esignlaunchpad/mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -0
  3. package/package.json +56 -0
  4. package/src/index.js +308 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eSign Launchpad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # eSign Launchpad MCP Server
2
+
3
+ MCP (Model Context Protocol) server that enables AI agents to interact with the eSign Launchpad e-signature platform. Create, send, and manage signing packages directly from Claude, Cursor, or any MCP-compatible AI client.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install
9
+ npm install @esignlaunchpad/mcp-server
10
+
11
+ # Run (requires API key)
12
+ ESIGN_API_KEY=eslp_live_your_key_here npx @esignlaunchpad/mcp-server
13
+ ```
14
+
15
+ ## Claude Desktop Configuration
16
+
17
+ Add to your `claude_desktop_config.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "esign-launchpad": {
23
+ "command": "npx",
24
+ "args": ["@esignlaunchpad/mcp-server"],
25
+ "env": {
26
+ "ESIGN_API_KEY": "eslp_live_your_key_here"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ## What Can AI Agents Do?
34
+
35
+ With this MCP server, AI agents can:
36
+
37
+ - **Create signing packages** with documents, signers, and annotation fields
38
+ - **Upload documents** (PDF, DOCX) via chunked upload
39
+ - **Add signers** with authentication requirements (email, SMS, KBA, ID verification)
40
+ - **Place signature fields** on documents with precise positioning
41
+ - **Send packages** for signing with custom email messages
42
+ - **Track status** of packages and individual signers
43
+ - **Download completed documents** and certificates of completion
44
+ - **Manage templates** for reusable signing workflows
45
+ - **Configure webhooks** for real-time event notifications
46
+ - **Manage signing groups** for team-based signing
47
+
48
+ ## Environment Variables
49
+
50
+ | Variable | Required | Default | Description |
51
+ |----------|----------|---------|-------------|
52
+ | `ESIGN_API_KEY` | Yes | - | Your eSign Launchpad API key (`eslp_live_...`) |
53
+ | `ESIGN_API_URL` | No | `https://api.esignlaunchpad.com` | API base URL |
54
+ | `ESIGN_SPEC_URL` | No | `{API_URL}/openapi/tenant-api.json` | OpenAPI spec URL |
55
+
56
+ ## API Key Setup
57
+
58
+ 1. Log in to [eSign Launchpad](https://app.esignlaunchpad.com)
59
+ 2. Go to **Settings > API**
60
+ 3. Click **Generate API Key**
61
+ 4. Select the permissions your agent needs
62
+ 5. Copy the key (shown only once)
63
+
64
+ ## Available Permissions
65
+
66
+ API keys are scoped. Select only what your agent needs:
67
+
68
+ | Scope | Operations |
69
+ |-------|-----------|
70
+ | `packages:read` | List, view, download packages |
71
+ | `packages:write` | Create, send, void, archive packages |
72
+ | `templates:read` | List and view templates |
73
+ | `templates:write` | Create and modify templates |
74
+ | `webhooks:manage` | Create and manage webhook subscriptions |
75
+ | `signing-groups:read` | View signing groups |
76
+ | `signing-groups:manage` | Create and manage signing groups |
77
+
78
+ ## How It Works
79
+
80
+ This MCP server reads the eSign Launchpad OpenAPI specification at startup and dynamically generates MCP tools for each API endpoint. When the API is updated with new features, simply restart the server to pick up new tools automatically.
81
+
82
+ No code generation needed. No manual tool definitions. The OpenAPI spec is the single source of truth.
83
+
84
+ ## License
85
+
86
+ MIT
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@esignlaunchpad/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP (Model Context Protocol) server for eSign Launchpad - enables AI agents to create, send, and manage e-sign packages.",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "esign-mcp": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "esign",
17
+ "e-signature",
18
+ "electronic-signature",
19
+ "ai-agent",
20
+ "claude",
21
+ "cursor",
22
+ "signing",
23
+ "documents",
24
+ "api"
25
+ ],
26
+ "license": "MIT",
27
+ "author": {
28
+ "name": "eSign Launchpad",
29
+ "email": "preflight@notarylaunchpad.com",
30
+ "url": "https://www.esignlaunchpad.com"
31
+ },
32
+ "homepage": "https://www.esignlaunchpad.com/Home/Developers",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/esignlaunchpad/mcp-server"
36
+ },
37
+ "bugs": {
38
+ "url": "https://www.esignlaunchpad.com/Home/Support",
39
+ "email": "preflight@notarylaunchpad.com"
40
+ },
41
+ "funding": {
42
+ "type": "commercial",
43
+ "url": "https://www.esignlaunchpad.com/Home/Pricing"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.12.1"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ },
51
+ "files": [
52
+ "src/",
53
+ "README.md",
54
+ "LICENSE"
55
+ ]
56
+ }
package/src/index.js ADDED
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * eSign Launchpad MCP Server
5
+ *
6
+ * Exposes the eSign Launchpad REST API as MCP tools for AI agents.
7
+ * Reads the OpenAPI spec at startup and dynamically generates tools
8
+ * for each API operation. When the API changes, restart the server
9
+ * to pick up new endpoints automatically.
10
+ *
11
+ * Usage:
12
+ * npx @esignlaunchpad/mcp-server
13
+ *
14
+ * Environment variables:
15
+ * ESIGN_API_KEY - Your eSign Launchpad API key (eslp_live_...)
16
+ * ESIGN_API_URL - API base URL (default: https://api.esignlaunchpad.com)
17
+ * ESIGN_SPEC_URL - OpenAPI spec URL (default: {ESIGN_API_URL}/openapi/tenant-api.json)
18
+ */
19
+
20
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { z } from "zod";
23
+
24
+ const API_KEY = process.env.ESIGN_API_KEY;
25
+ const API_URL = (process.env.ESIGN_API_URL || "https://api.esignlaunchpad.com").replace(/\/$/, "");
26
+ const SPEC_URL = process.env.ESIGN_SPEC_URL || `${API_URL}/openapi/tenant-api.json`;
27
+
28
+ if (!API_KEY) {
29
+ console.error("Error: ESIGN_API_KEY environment variable is required.");
30
+ console.error("Generate an API key at https://app.esignlaunchpad.com/Settings/Api");
31
+ process.exit(1);
32
+ }
33
+
34
+ // ── Fetch and parse OpenAPI spec ──
35
+
36
+ async function fetchSpec() {
37
+ const res = await fetch(SPEC_URL);
38
+ if (!res.ok) throw new Error(`Failed to fetch OpenAPI spec from ${SPEC_URL}: ${res.status}`);
39
+ return res.json();
40
+ }
41
+
42
+ // ── Convert OpenAPI schema to Zod schema ──
43
+
44
+ function openApiSchemaToZod(schema, required = false) {
45
+ if (!schema) return required ? z.unknown() : z.unknown().optional();
46
+
47
+ if (schema.enum) {
48
+ return required ? z.enum(schema.enum) : z.enum(schema.enum).optional();
49
+ }
50
+
51
+ switch (schema.type) {
52
+ case "string":
53
+ return required ? z.string().describe(schema.description || "") : z.string().optional().describe(schema.description || "");
54
+ case "integer":
55
+ case "number":
56
+ return required ? z.number().describe(schema.description || "") : z.number().optional().describe(schema.description || "");
57
+ case "boolean":
58
+ return required ? z.boolean().describe(schema.description || "") : z.boolean().optional().describe(schema.description || "");
59
+ case "array":
60
+ const itemSchema = openApiSchemaToZod(schema.items, true);
61
+ return required ? z.array(itemSchema).describe(schema.description || "") : z.array(itemSchema).optional().describe(schema.description || "");
62
+ case "object":
63
+ if (schema.properties) {
64
+ const shape = {};
65
+ const reqFields = new Set(schema.required || []);
66
+ for (const [key, prop] of Object.entries(schema.properties)) {
67
+ shape[key] = openApiSchemaToZod(prop, reqFields.has(key));
68
+ }
69
+ return required ? z.object(shape).describe(schema.description || "") : z.object(shape).optional().describe(schema.description || "");
70
+ }
71
+ return required ? z.record(z.unknown()) : z.record(z.unknown()).optional();
72
+ default:
73
+ return required ? z.unknown() : z.unknown().optional();
74
+ }
75
+ }
76
+
77
+ // ── Resolve $ref in OpenAPI spec ──
78
+
79
+ function resolveRef(spec, ref) {
80
+ if (!ref || !ref.startsWith("#/")) return ref;
81
+ const parts = ref.substring(2).split("/");
82
+ let current = spec;
83
+ for (const part of parts) {
84
+ current = current?.[part];
85
+ if (current === undefined) return null;
86
+ }
87
+ return current;
88
+ }
89
+
90
+ function resolveSchema(spec, schema) {
91
+ if (!schema) return null;
92
+ if (schema.$ref) return resolveRef(spec, schema.$ref);
93
+ return schema;
94
+ }
95
+
96
+ // ── Build tool name from operationId or path ──
97
+
98
+ function buildToolName(method, path, operation) {
99
+ if (operation.operationId) {
100
+ // Convert PascalCase to snake_case
101
+ return operation.operationId
102
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
103
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
104
+ .toLowerCase()
105
+ .replace(/[^a-z0-9_]/g, "_")
106
+ .replace(/_+/g, "_")
107
+ .substring(0, 64);
108
+ }
109
+ // Fallback: method + path
110
+ return `${method}_${path.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_")}`.toLowerCase().substring(0, 64);
111
+ }
112
+
113
+ // ── Build tool description ──
114
+
115
+ function buildDescription(operation, method, path) {
116
+ const parts = [];
117
+ if (operation.summary) parts.push(operation.summary);
118
+ if (operation.description && operation.description !== operation.summary) {
119
+ parts.push(operation.description);
120
+ }
121
+ parts.push(`[${method.toUpperCase()} ${path}]`);
122
+ return parts.join("\n\n");
123
+ }
124
+
125
+ // ── Extract parameters and request body into a single Zod schema ──
126
+
127
+ function buildInputSchema(spec, operation, pathParams) {
128
+ const shape = {};
129
+
130
+ // Path and query parameters
131
+ const allParams = [...(pathParams || []), ...(operation.parameters || [])];
132
+ for (const param of allParams) {
133
+ const resolved = param.$ref ? resolveRef(spec, param.$ref) : param;
134
+ if (!resolved) continue;
135
+ const paramSchema = resolveSchema(spec, resolved.schema) || { type: "string" };
136
+ shape[resolved.name] = openApiSchemaToZod(paramSchema, resolved.required);
137
+ if (resolved.description && shape[resolved.name]) {
138
+ shape[resolved.name] = shape[resolved.name].describe(resolved.description);
139
+ }
140
+ }
141
+
142
+ // Request body (JSON)
143
+ const body = operation.requestBody;
144
+ if (body) {
145
+ const resolved = body.$ref ? resolveRef(spec, body.$ref) : body;
146
+ const content = resolved?.content?.["application/json"];
147
+ if (content?.schema) {
148
+ const bodySchema = resolveSchema(spec, content.schema);
149
+ if (bodySchema?.properties) {
150
+ const reqFields = new Set(bodySchema.required || []);
151
+ for (const [key, prop] of Object.entries(bodySchema.properties)) {
152
+ const resolvedProp = resolveSchema(spec, prop) || prop;
153
+ shape[key] = openApiSchemaToZod(resolvedProp, reqFields.has(key));
154
+ }
155
+ } else {
156
+ // Opaque body — accept as JSON string
157
+ shape["_body"] = z.string().optional().describe("JSON request body");
158
+ }
159
+ }
160
+ }
161
+
162
+ return Object.keys(shape).length > 0 ? z.object(shape) : z.object({});
163
+ }
164
+
165
+ // ── Make API call ──
166
+
167
+ async function callApi(method, pathTemplate, params, spec) {
168
+ // Substitute path parameters
169
+ let path = pathTemplate;
170
+ const queryParams = {};
171
+ const bodyParams = {};
172
+
173
+ // Identify which params are path, query, or body
174
+ const pathParamNames = new Set(
175
+ (pathTemplate.match(/\{([^}]+)\}/g) || []).map((m) => m.slice(1, -1))
176
+ );
177
+
178
+ // Collect all declared parameters
179
+ const operation = getOperation(spec, method, pathTemplate);
180
+ const declaredParams = new Set();
181
+ for (const p of [...(spec.paths[pathTemplate]?.parameters || []), ...(operation?.parameters || [])]) {
182
+ const resolved = p.$ref ? resolveRef(spec, p.$ref) : p;
183
+ if (resolved) declaredParams.add(resolved.name);
184
+ }
185
+
186
+ for (const [key, value] of Object.entries(params || {})) {
187
+ if (value === undefined || value === null) continue;
188
+ if (key === "_body") continue;
189
+ if (pathParamNames.has(key)) {
190
+ path = path.replace(`{${key}}`, encodeURIComponent(String(value)));
191
+ } else if (declaredParams.has(key)) {
192
+ // It's a declared query param
193
+ queryParams[key] = value;
194
+ } else {
195
+ // Everything else goes into body
196
+ bodyParams[key] = value;
197
+ }
198
+ }
199
+
200
+ // Build URL with query string
201
+ const url = new URL(`${API_URL}${path}`);
202
+ for (const [k, v] of Object.entries(queryParams)) {
203
+ url.searchParams.set(k, String(v));
204
+ }
205
+
206
+ // Build request
207
+ const fetchOptions = {
208
+ method: method.toUpperCase(),
209
+ headers: {
210
+ "X-API-KEY": API_KEY,
211
+ "Accept": "application/json",
212
+ },
213
+ };
214
+
215
+ // Add body for non-GET methods
216
+ const hasBody = Object.keys(bodyParams).length > 0 || params?._body;
217
+ if (hasBody && method !== "get") {
218
+ fetchOptions.headers["Content-Type"] = "application/json";
219
+ fetchOptions.body = params?._body || JSON.stringify(bodyParams);
220
+ }
221
+
222
+ const res = await fetch(url.toString(), fetchOptions);
223
+ const contentType = res.headers.get("content-type") || "";
224
+
225
+ if (contentType.includes("application/json")) {
226
+ const json = await res.json();
227
+ return { status: res.status, data: json };
228
+ } else {
229
+ const text = await res.text();
230
+ return { status: res.status, data: text.substring(0, 5000) };
231
+ }
232
+ }
233
+
234
+ function getOperation(spec, method, path) {
235
+ return spec.paths?.[path]?.[method];
236
+ }
237
+
238
+ // ── Main ──
239
+
240
+ async function main() {
241
+ console.error(`eSign Launchpad MCP Server starting...`);
242
+ console.error(`API: ${API_URL}`);
243
+ console.error(`Spec: ${SPEC_URL}`);
244
+
245
+ const spec = await fetchSpec();
246
+ console.error(`Loaded OpenAPI spec: ${spec.info?.title} ${spec.info?.version}`);
247
+
248
+ const server = new McpServer({
249
+ name: "esign-launchpad",
250
+ version: "1.0.0",
251
+ });
252
+
253
+ // Register a tool for each API operation
254
+ let toolCount = 0;
255
+ for (const [path, pathItem] of Object.entries(spec.paths || {})) {
256
+ const pathParams = pathItem.parameters || [];
257
+
258
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
259
+ const operation = pathItem[method];
260
+ if (!operation) continue;
261
+
262
+ // Skip operations not in the tenant-api group
263
+ const tags = operation.tags || [];
264
+
265
+ const toolName = buildToolName(method, path, operation);
266
+ const description = buildDescription(operation, method, path);
267
+ const inputSchema = buildInputSchema(spec, operation, pathParams);
268
+
269
+ server.tool(toolName, description, inputSchema.shape, async (params) => {
270
+ try {
271
+ const result = await callApi(method, path, params, spec);
272
+ return {
273
+ content: [
274
+ {
275
+ type: "text",
276
+ text: JSON.stringify(result, null, 2),
277
+ },
278
+ ],
279
+ };
280
+ } catch (err) {
281
+ return {
282
+ content: [
283
+ {
284
+ type: "text",
285
+ text: JSON.stringify({ error: err.message }, null, 2),
286
+ },
287
+ ],
288
+ isError: true,
289
+ };
290
+ }
291
+ });
292
+
293
+ toolCount++;
294
+ }
295
+ }
296
+
297
+ console.error(`Registered ${toolCount} tools from OpenAPI spec`);
298
+
299
+ // Connect via stdio transport
300
+ const transport = new StdioServerTransport();
301
+ await server.connect(transport);
302
+ console.error("MCP server connected via stdio");
303
+ }
304
+
305
+ main().catch((err) => {
306
+ console.error("Fatal error:", err);
307
+ process.exit(1);
308
+ });