@dropspot/mcp 0.0.0-stage → 1.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Drop Spot Technologies
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 CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # @dropspot/mcp — Drop Spot MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@dropspot/mcp.svg)](https://www.npmjs.com/package/@dropspot/mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![MCP Standard](https://img.shields.io/badge/MCP-Standard-emerald.svg)](https://modelcontextprotocol.io)
6
+
7
+ Official **Model Context Protocol (MCP)** server for [Drop Spot](https://godropspot.com) — the passwordless client document intake and automated follow-up platform for accountants, law firms, and service professionals.
8
+
9
+ Connect Drop Spot directly to **Claude Desktop**, **Cursor AI**, or **Google Antigravity**. Instruct your AI in plain English to prepare client checklists, dispatch secure zero-password upload links, monitor completion status, and review incoming paperwork—with zero coding required.
10
+
11
+ ---
12
+
13
+ ## ⚡ Quickstart
14
+
15
+ ### 1. Get your Drop Spot API Key
16
+ 1. Sign in to your Drop Spot workspace at [godropspot.com](https://godropspot.com).
17
+ 2. Go to **Settings** → **Developer API & MCP**.
18
+ 3. Click **Generate New API Key**, select your desired scopes (or Full Access), and copy your token (`ds_live_...`).
19
+
20
+ ---
21
+
22
+ ### 2. Configure Your AI Client
23
+
24
+ #### Option A: Claude Desktop
25
+ Add the following to your `claude_desktop_config.json`:
26
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
27
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "dropspot": {
33
+ "command": "npx",
34
+ "args": ["-y", "@dropspot/mcp"],
35
+ "env": {
36
+ "DROPSPOT_API_KEY": "ds_live_YOUR_ACTUAL_KEY_HERE",
37
+ "DROPSPOT_API_BASE_URL": "https://godropspot.com/api/v1"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ #### Option B: Cursor AI
45
+ Add to `.cursor/mcp.json` in your project or global settings:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "dropspot": {
51
+ "command": "npx",
52
+ "args": ["-y", "@dropspot/mcp"],
53
+ "env": {
54
+ "DROPSPOT_API_KEY": "ds_live_YOUR_ACTUAL_KEY_HERE",
55
+ "DROPSPOT_API_BASE_URL": "https://godropspot.com/api/v1"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ #### Option C: Claude Code (CLI)
63
+ Add Drop Spot to Claude Code:
64
+
65
+ ```bash
66
+ claude mcp add dropspot npx -y @dropspot/mcp --env DROPSPOT_API_KEY=ds_live_YOUR_ACTUAL_KEY_HERE DROPSPOT_API_BASE_URL=https://godropspot.com/api/v1
67
+ ```
68
+
69
+ #### Option D: Windsurf / VS Code / Antigravity
70
+ Add to your client's MCP configuration (`mcpServers`):
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "dropspot": {
76
+ "command": "npx",
77
+ "args": ["-y", "@dropspot/mcp"],
78
+ "env": {
79
+ "DROPSPOT_API_KEY": "ds_live_YOUR_ACTUAL_KEY_HERE",
80
+ "DROPSPOT_API_BASE_URL": "https://godropspot.com/api/v1"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 🌐 Public Registries & Discovery
90
+
91
+ Drop Spot is an open, unproxied MCP server available across major AI registries:
92
+
93
+ - **Smithery.ai:** Direct registry listing indexed via `smithery.yaml` (Arcade.dev-powered, zero proxy latency).
94
+ - **Glama.ai:** Verified MCP listing claimed via `glama.json` ([glama.ai/mcp/servers](https://glama.ai/mcp/servers)).
95
+ - **PulseMCP:** Listed in the community catalog ([pulsemcp.com](https://pulsemcp.com)).
96
+ - **Official Model Context Protocol:** Cataloged on [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers).
97
+ - **npm:** Zero-install direct execution via `npx -y @dropspot/mcp`.
98
+
99
+ ---
100
+
101
+ ## 💬 Prompts Your AI Understands
102
+
103
+ Once connected, your AI gains native understanding of your Drop Spot workspace:
104
+
105
+ - *"Create an intake request for Sarah Jenkins using our 2026 Tax Return template."*
106
+ - *"Show me our client list and search for Acme Corp."*
107
+ - *"Update Sarah's phone number to (555) 234-5678 and add a note that she is a VIP client."*
108
+ - *"Check if Sarah uploaded her W-2 form yet."*
109
+ - *"Review Sarah's uploaded W-2 and approve it."*
110
+ - *"Reject item #2 with the note: 'Page 2 is blurry. Please re-upload a clear scan.' "*
111
+ - *"List all our active document requests with pending items."*
112
+ - *"Create a reusable checklist template called 'Quarterly Bookkeeping Close'."*
113
+
114
+ ---
115
+
116
+ ## 🛠️ Available MCP Tools (12 Tools)
117
+
118
+ | Tool Name | Description |
119
+ | :--- | :--- |
120
+ | `create_intake_request` | Create a new document intake dossier with an itemized checklist. Generates a passwordless magic upload link and optionally emails the client. |
121
+ | `list_intake_requests` | List and filter active document requests in the workspace by status or client email. |
122
+ | `get_intake_request` | Retrieve complete details of a specific request, including each checklist item, upload statuses, and file metadata. |
123
+ | `review_item` | Review an uploaded file: approve it or reject it with custom feedback to prompt the client for a re-upload. |
124
+ | `get_file_download_link` | Generate a short-lived (5-minute) secure presigned URL to inspect or download an uploaded client file. |
125
+ | `list_clients` | Search and browse your firm's client directory by name, email, company, entity type, status, or tag. |
126
+ | `get_client` | Fetch a client record with full accounting CRM metadata, spouses/linked contacts, tags, and intake history. |
127
+ | `create_or_update_client` | Add a new client or update contact & CRM profile (tax entity, FYE, cadence, state, tags, masked ID, pinned notes, linked contacts). |
128
+ | `list_templates` | View your reusable checklist templates (e.g. 1040 Tax Return, KYC, Mortgage Intake). |
129
+ | `get_template` | Inspect a specific checklist template and its default items. |
130
+ | `create_template` | Create a new reusable checklist template with predefined items and categories. |
131
+ | `update_template` | Edit an existing template's name, description, or checklist items. |
132
+
133
+ ---
134
+
135
+ ## 🔒 Security, Privacy & Compliance
136
+
137
+ - **Zero AI Training:** Client documents and personal data are **never** used to train public or proprietary AI models.
138
+ - **Short-Lived Signed URLs:** File downloads use ephemeral 5-minute pre-signed URLs; file binaries are never permanently exposed over the wire.
139
+ - **Scoped API Tokens:** Generate API keys restricted strictly to specific operations (`requests:read`, `requests:write`, `items:review`, `clients:write`, etc.).
140
+ - **Human-in-the-Loop:** AI tools execute actions on your behalf with scoped permissions, clear item status tracking, and 1-click token revocation.
141
+
142
+ ---
143
+
144
+ ## 📄 License
145
+ MIT © [Drop Spot Technologies](https://godropspot.com)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,871 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
+ const API_KEY = process.env.DROPSPOT_API_KEY || '';
6
+ const BASE_URL = (process.env.DROPSPOT_API_BASE_URL || process.env.DROPSPOT_BASE_URL || 'https://portal.godropspot.com').replace(/\/+$/, '');
7
+ async function apiRequest(endpoint, options = {}) {
8
+ // Support both full paths and /api/v1 paths
9
+ const cleanEndpoint = endpoint.startsWith('/api/v1')
10
+ ? (BASE_URL.endsWith('/api/v1') ? endpoint.replace('/api/v1', '') : endpoint)
11
+ : (BASE_URL.endsWith('/api/v1') ? endpoint : `/api/v1${endpoint}`);
12
+ const url = `${BASE_URL}${cleanEndpoint}`;
13
+ const headers = {
14
+ 'Content-Type': 'application/json',
15
+ Authorization: `Bearer ${API_KEY}`,
16
+ ...options.headers,
17
+ };
18
+ const response = await fetch(url, { ...options, headers });
19
+ const data = (await response.json());
20
+ if (!response.ok) {
21
+ throw new Error(data?.error || `API request failed with HTTP ${response.status}`);
22
+ }
23
+ return data;
24
+ }
25
+ const server = new Server({
26
+ name: 'dropspot-mcp',
27
+ version: '1.2.0',
28
+ }, {
29
+ capabilities: {
30
+ tools: {},
31
+ resources: {},
32
+ },
33
+ });
34
+ // ----------------------------------------------------------------------------
35
+ // 1. LIST TOOLS
36
+ // ----------------------------------------------------------------------------
37
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
38
+ return {
39
+ tools: [
40
+ {
41
+ name: 'create_intake_request',
42
+ description: 'Create a new Drop Spot client document intake request (dossier). Generates a zero-password magic upload link and optionally emails the client.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ clientName: {
47
+ type: 'string',
48
+ description: 'Full name of the client (e.g. "Jane Doe")',
49
+ },
50
+ clientEmail: {
51
+ type: 'string',
52
+ description: 'Email address of the client where the magic link will be sent',
53
+ },
54
+ clientPhone: {
55
+ type: 'string',
56
+ description: 'Optional phone number for SMS reminders',
57
+ },
58
+ title: {
59
+ type: 'string',
60
+ description: 'Title of the intake request (e.g. "2026 Tax Return Documentation")',
61
+ },
62
+ dueAt: {
63
+ type: 'string',
64
+ description: 'ISO 8601 deadline date (optional, defaults to 14 days from now)',
65
+ },
66
+ sendInvitationEmail: {
67
+ type: 'boolean',
68
+ description: 'Whether to automatically email the client the invitation link (defaults to true)',
69
+ },
70
+ items: {
71
+ type: 'array',
72
+ description: 'Checklist of items and documents required from the client',
73
+ items: {
74
+ type: 'object',
75
+ properties: {
76
+ title: { type: 'string', description: 'Name of the required document' },
77
+ description: { type: 'string', description: 'Helpful instructions for the client' },
78
+ isRequired: { type: 'boolean', description: 'Whether this item is mandatory (default: true)' },
79
+ allowMultiple: { type: 'boolean', description: 'Allow uploading multiple files for this item' },
80
+ category: { type: 'string', description: 'Grouping category (e.g. "Tax", "Identification")' },
81
+ },
82
+ required: ['title'],
83
+ },
84
+ },
85
+ },
86
+ required: ['clientName', 'clientEmail', 'title', 'items'],
87
+ },
88
+ },
89
+ {
90
+ name: 'list_intake_requests',
91
+ description: 'List client intake requests in the Drop Spot workspace. Filter by status or client email.',
92
+ inputSchema: {
93
+ type: 'object',
94
+ properties: {
95
+ status: {
96
+ type: 'string',
97
+ enum: ['in_process', 'completed', 'canceled'],
98
+ description: 'Filter by dossier status',
99
+ },
100
+ clientEmail: {
101
+ type: 'string',
102
+ description: 'Filter by specific client email',
103
+ },
104
+ limit: {
105
+ type: 'number',
106
+ description: 'Number of requests to return (default: 20, max: 100)',
107
+ },
108
+ },
109
+ },
110
+ },
111
+ {
112
+ name: 'get_intake_request',
113
+ description: 'Retrieve full details for an intake request by ID, including checklist items, upload progress, and file metadata.',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ requestId: {
118
+ type: 'string',
119
+ description: 'The UUID of the intake request',
120
+ },
121
+ },
122
+ required: ['requestId'],
123
+ },
124
+ },
125
+ {
126
+ name: 'review_item',
127
+ description: 'Approve or reject a document uploaded by a client. If rejected, sends automated feedback asking the client to re-upload.',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: {
131
+ itemId: {
132
+ type: 'string',
133
+ description: 'The UUID of the checklist item to review',
134
+ },
135
+ decision: {
136
+ type: 'string',
137
+ enum: ['approve', 'reject'],
138
+ description: 'Approval decision',
139
+ },
140
+ rejectionNote: {
141
+ type: 'string',
142
+ description: 'Reason for rejection to help client correct the issue (e.g. "Photo is blurry, please retake")',
143
+ },
144
+ },
145
+ required: ['itemId', 'decision'],
146
+ },
147
+ },
148
+ {
149
+ name: 'get_file_download_link',
150
+ description: 'Generate a secure, temporary presigned download URL for an uploaded file so an AI or operator can inspect the document contents.',
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ fileId: {
155
+ type: 'string',
156
+ description: 'The UUID of the uploaded file record',
157
+ },
158
+ expiresInSeconds: {
159
+ type: 'number',
160
+ description: 'Expiration in seconds (default: 300 = 5 minutes for privacy)',
161
+ },
162
+ },
163
+ required: ['fileId'],
164
+ },
165
+ },
166
+ // CLIENTS MANAGEMENT TOOLS
167
+ {
168
+ name: 'list_clients',
169
+ description: 'List and search clients in the Drop Spot directory. Filter by name, email, company, entity type, status, or tag.',
170
+ inputSchema: {
171
+ type: 'object',
172
+ properties: {
173
+ search: {
174
+ type: 'string',
175
+ description: 'Search string to filter clients by name, email, or company name',
176
+ },
177
+ entityType: {
178
+ type: 'string',
179
+ enum: ['individual', 's_corp', 'c_corp', 'partnership', 'llc', 'non_profit', 'trust_estate'],
180
+ description: 'Filter by tax entity classification',
181
+ },
182
+ status: {
183
+ type: 'string',
184
+ enum: ['active', 'lead', 'archived'],
185
+ description: 'Filter by client account status',
186
+ },
187
+ tag: {
188
+ type: 'string',
189
+ description: 'Filter by specific tag (e.g. "QBO", "Extension Needed")',
190
+ },
191
+ limit: {
192
+ type: 'number',
193
+ description: 'Max number of clients to return (default: 50, max: 100)',
194
+ },
195
+ },
196
+ },
197
+ },
198
+ {
199
+ name: 'get_client',
200
+ description: 'Retrieve detailed profile and CRM information for a specific client, including tax entity metadata, spouses/linked contacts, tags, pinned accountant note, and active/completed document requests.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ clientId: {
205
+ type: 'string',
206
+ description: 'The UUID of the client',
207
+ },
208
+ },
209
+ required: ['clientId'],
210
+ },
211
+ },
212
+ {
213
+ name: 'create_or_update_client',
214
+ description: 'Add a new client to the directory or update an existing client with accounting CRM metadata (entity type, filing cadence, FYE, state, tags, masked tax ID, pinned accountant notes, and linked contacts).',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ name: {
219
+ type: 'string',
220
+ description: 'Full name of the client (e.g. "Jane Doe")',
221
+ },
222
+ email: {
223
+ type: 'string',
224
+ description: 'Email address of the client',
225
+ },
226
+ phone: {
227
+ type: 'string',
228
+ description: 'Optional phone number',
229
+ },
230
+ company: {
231
+ type: 'string',
232
+ description: 'Company or business entity name',
233
+ },
234
+ notes: {
235
+ type: 'string',
236
+ description: 'General internal notes about this client',
237
+ },
238
+ entityType: {
239
+ type: 'string',
240
+ enum: ['individual', 's_corp', 'c_corp', 'partnership', 'llc', 'non_profit', 'trust_estate'],
241
+ description: 'Tax entity classification (e.g. "individual" for 1040, "s_corp" for 1120-S, "c_corp" for 1120, "partnership" for 1065)',
242
+ },
243
+ status: {
244
+ type: 'string',
245
+ enum: ['active', 'lead', 'archived'],
246
+ description: 'Account status (defaults to "active")',
247
+ },
248
+ fiscalYearEnd: {
249
+ type: 'string',
250
+ description: 'Fiscal Year End month/day (e.g. "12/31", "06/30")',
251
+ },
252
+ filingCadence: {
253
+ type: 'string',
254
+ enum: ['annual', 'quarterly', 'monthly'],
255
+ description: 'Tax filing frequency',
256
+ },
257
+ stateOfFiling: {
258
+ type: 'string',
259
+ description: 'US State abbreviation for tax filing (e.g. "FL", "CA", "NY")',
260
+ },
261
+ taxYear: {
262
+ type: 'string',
263
+ description: 'Current filing tax year (e.g. "2026")',
264
+ },
265
+ tags: {
266
+ type: 'array',
267
+ items: { type: 'string' },
268
+ description: 'Accounting tags (e.g. ["QBO", "Extension Needed", "VIP", "Monthly Books"])',
269
+ },
270
+ pinnedNote: {
271
+ type: 'string',
272
+ description: 'Sticky internal accountant note (communication quirks, deadline habits)',
273
+ },
274
+ qboId: {
275
+ type: 'string',
276
+ description: 'QuickBooks Online or Xero accounting customer ID',
277
+ },
278
+ taxSoftwareId: {
279
+ type: 'string',
280
+ description: 'Drake, UltraTax, or CCH Axcess client ID',
281
+ },
282
+ maskedId: {
283
+ type: 'string',
284
+ description: 'Masked tax ID / SSN / EIN (last 4 digits only, e.g. "1234" or "***-**-1234")',
285
+ },
286
+ linkedContacts: {
287
+ type: 'array',
288
+ items: {
289
+ type: 'object',
290
+ properties: {
291
+ name: { type: 'string', description: 'Full name of contact' },
292
+ role: { type: 'string', description: 'Role (e.g. "Spouse", "Bookkeeper", "Owner")' },
293
+ email: { type: 'string', description: 'Contact email' },
294
+ phone: { type: 'string', description: 'Contact phone' },
295
+ },
296
+ required: ['name', 'role'],
297
+ },
298
+ description: 'Associated contacts, spouses, bookkeepers, or family members',
299
+ },
300
+ },
301
+ required: ['name', 'email'],
302
+ },
303
+ },
304
+ // TEMPLATES MANAGEMENT TOOLS
305
+ {
306
+ name: 'list_templates',
307
+ description: 'List available checklist templates (including starter packages for tax returns, KYC, and mortgage underwriting).',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ includeStarters: {
312
+ type: 'boolean',
313
+ description: 'Whether to include built-in starter templates (default: true)',
314
+ },
315
+ },
316
+ },
317
+ },
318
+ {
319
+ name: 'get_template',
320
+ description: 'Get full details and checklist items for a specific reusable template.',
321
+ inputSchema: {
322
+ type: 'object',
323
+ properties: {
324
+ templateId: {
325
+ type: 'string',
326
+ description: 'The UUID or starter identifier of the template',
327
+ },
328
+ },
329
+ required: ['templateId'],
330
+ },
331
+ },
332
+ {
333
+ name: 'create_template',
334
+ description: 'Create a new reusable document checklist template with predefined items and categories.',
335
+ inputSchema: {
336
+ type: 'object',
337
+ properties: {
338
+ name: {
339
+ type: 'string',
340
+ description: 'Template title (e.g. "Q1 Bookkeeping Review")',
341
+ },
342
+ description: {
343
+ type: 'string',
344
+ description: 'Description of when to use this checklist',
345
+ },
346
+ isShared: {
347
+ type: 'boolean',
348
+ description: 'Whether this template is visible to the entire workspace team (default: true)',
349
+ },
350
+ items: {
351
+ type: 'array',
352
+ description: 'The list of checklist items',
353
+ items: {
354
+ type: 'object',
355
+ properties: {
356
+ title: { type: 'string', description: 'Name of the required document' },
357
+ description: { type: 'string', description: 'Instructions for the client' },
358
+ isRequired: { type: 'boolean', description: 'Whether item is mandatory (default: true)' },
359
+ allowMultiple: { type: 'boolean', description: 'Allow multiple files (default: false)' },
360
+ category: { type: 'string', description: 'Item category' },
361
+ },
362
+ required: ['title'],
363
+ },
364
+ },
365
+ },
366
+ required: ['name', 'items'],
367
+ },
368
+ },
369
+ {
370
+ name: 'update_template',
371
+ description: 'Edit an existing custom checklist template (update name, description, or checklist items).',
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ templateId: {
376
+ type: 'string',
377
+ description: 'The UUID of the template to edit',
378
+ },
379
+ name: {
380
+ type: 'string',
381
+ description: 'Updated template name',
382
+ },
383
+ description: {
384
+ type: 'string',
385
+ description: 'Updated description',
386
+ },
387
+ items: {
388
+ type: 'array',
389
+ description: 'Updated list of items',
390
+ items: {
391
+ type: 'object',
392
+ properties: {
393
+ title: { type: 'string' },
394
+ description: { type: 'string' },
395
+ isRequired: { type: 'boolean' },
396
+ allowMultiple: { type: 'boolean' },
397
+ category: { type: 'string' },
398
+ },
399
+ required: ['title'],
400
+ },
401
+ },
402
+ },
403
+ required: ['templateId'],
404
+ },
405
+ },
406
+ ],
407
+ };
408
+ });
409
+ // ----------------------------------------------------------------------------
410
+ // 2. CALL TOOL HANDLERS
411
+ // ----------------------------------------------------------------------------
412
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
413
+ const { name, arguments: args } = request.params;
414
+ if (!API_KEY) {
415
+ return {
416
+ isError: true,
417
+ content: [
418
+ {
419
+ type: 'text',
420
+ text: 'DROPSPOT_API_KEY environment variable is not configured. Please supply a valid Drop Spot API key.',
421
+ },
422
+ ],
423
+ };
424
+ }
425
+ try {
426
+ switch (name) {
427
+ case 'create_intake_request': {
428
+ const payload = {
429
+ clientName: args?.clientName,
430
+ clientEmail: args?.clientEmail,
431
+ clientPhone: args?.clientPhone,
432
+ title: args?.title,
433
+ dueAt: args?.dueAt,
434
+ sendInvitationEmail: args?.sendInvitationEmail ?? true,
435
+ items: args?.items,
436
+ };
437
+ const res = await apiRequest('/requests', {
438
+ method: 'POST',
439
+ body: JSON.stringify(payload),
440
+ });
441
+ return {
442
+ content: [
443
+ {
444
+ type: 'text',
445
+ text: `✅ Intake request created successfully!\n\n` +
446
+ `• ID: ${res.data.id}\n` +
447
+ `• Title: ${res.data.title}\n` +
448
+ `• Client: ${res.data.clientName} (${res.data.clientEmail})\n` +
449
+ `• Magic Portal URL: ${res.data.portalUrl}\n` +
450
+ `• Due Date: ${new Date(res.data.dueAt).toLocaleDateString()}\n` +
451
+ `• Items Requested: ${res.data.items?.length || 0}\n` +
452
+ `• Invitation Email Sent: ${res.data.invitationEmailSent ? 'Yes' : 'No'}`,
453
+ },
454
+ ],
455
+ };
456
+ }
457
+ case 'list_intake_requests': {
458
+ const params = new URLSearchParams();
459
+ if (args?.status)
460
+ params.set('status', String(args.status));
461
+ if (args?.clientEmail)
462
+ params.set('client_email', String(args.clientEmail));
463
+ if (args?.limit)
464
+ params.set('limit', String(args.limit));
465
+ const res = await apiRequest(`/requests?${params.toString()}`);
466
+ const requests = res.data || [];
467
+ if (requests.length === 0) {
468
+ return {
469
+ content: [{ type: 'text', text: 'No intake requests found matching the criteria.' }],
470
+ };
471
+ }
472
+ const formatted = requests
473
+ .map((r) => {
474
+ return `• [${r.status.toUpperCase()}] ${r.title} — ${r.clientName} (${r.clientEmail})\n` +
475
+ ` ID: ${r.id} | Progress: ${r.completedItemsCount}/${r.itemsCount} items approved | Due: ${new Date(r.dueAt).toLocaleDateString()}`;
476
+ })
477
+ .join('\n\n');
478
+ return {
479
+ content: [
480
+ {
481
+ type: 'text',
482
+ text: `Found ${requests.length} intake requests:\n\n${formatted}`,
483
+ },
484
+ ],
485
+ };
486
+ }
487
+ case 'get_intake_request': {
488
+ const requestId = String(args?.requestId);
489
+ const res = await apiRequest(`/requests/${requestId}`);
490
+ const d = res.data;
491
+ let itemsReport = (d.items || [])
492
+ .map((item, idx) => {
493
+ const statusIcon = item.status === 'approved' ? '✅' : item.status === 'rejected' ? '❌' : item.status === 'uploaded' ? '📥' : '⏳';
494
+ let fileDetails = '';
495
+ if (item.files && item.files.length > 0) {
496
+ fileDetails = item.files
497
+ .map((f) => `\n - File: ${f.filename} (${Math.round(f.sizeBytes / 1024)} KB) [File ID: ${f.id}]`)
498
+ .join('');
499
+ }
500
+ const rejection = item.rejectionNote ? `\n - Note: "${item.rejectionNote}"` : '';
501
+ return `${idx + 1}. ${statusIcon} ${item.title} [Status: ${item.status}]${rejection}${fileDetails}`;
502
+ })
503
+ .join('\n');
504
+ const summary = `📋 Intake Dossier: ${d.title}\n` +
505
+ `• ID: ${d.id}\n` +
506
+ `• Client: ${d.clientName} <${d.clientEmail}>\n` +
507
+ `• Status: ${d.status}\n` +
508
+ `• Progress: ${d.progress.approvedItems}/${d.progress.requiredItems} required items approved (${d.progress.percent}%)\n` +
509
+ `• Due Date: ${new Date(d.dueAt).toLocaleString()}\n\n` +
510
+ `Checklist Items:\n${itemsReport}`;
511
+ return {
512
+ content: [{ type: 'text', text: summary }],
513
+ };
514
+ }
515
+ case 'review_item': {
516
+ const itemId = String(args?.itemId);
517
+ const decision = String(args?.decision);
518
+ const rejectionNote = args?.rejectionNote ? String(args.rejectionNote) : undefined;
519
+ const res = await apiRequest(`/items/${itemId}/review`, {
520
+ method: 'POST',
521
+ body: JSON.stringify({ decision, rejectionNote }),
522
+ });
523
+ const icon = decision === 'approve' ? '✅' : '❌';
524
+ return {
525
+ content: [
526
+ {
527
+ type: 'text',
528
+ text: `${icon} Item "${res.data.item.title}" has been marked as ${decision.toUpperCase()}.\n` +
529
+ (decision === 'reject' ? `• Rejection feedback dispatched to client: "${res.data.item.rejectionNote}"\n` : '') +
530
+ `• Dossier Completed: ${res.data.request.completed ? 'YES (All required items approved!)' : 'No (Items remaining)'}`,
531
+ },
532
+ ],
533
+ };
534
+ }
535
+ case 'get_file_download_link': {
536
+ const fileId = String(args?.fileId);
537
+ const expiresIn = args?.expiresInSeconds ? Number(args.expiresInSeconds) : 300;
538
+ const res = await apiRequest(`/files/${fileId}/download?expires_in=${expiresIn}`);
539
+ const f = res.data;
540
+ return {
541
+ content: [
542
+ {
543
+ type: 'text',
544
+ text: `🔗 Secure download link generated (valid for ${Math.round(f.expiresInSeconds / 60)} minutes):\n\n` +
545
+ `• Filename: ${f.filename}\n` +
546
+ `• MIME Type: ${f.mimeType}\n` +
547
+ `• Size: ${(f.sizeBytes / 1024).toFixed(1)} KB\n` +
548
+ `• SHA-256: ${f.sha256Hash}\n` +
549
+ `• Direct URL: ${f.downloadUrl}`,
550
+ },
551
+ ],
552
+ };
553
+ }
554
+ // CLIENT HANDLERS
555
+ case 'list_clients': {
556
+ const params = new URLSearchParams();
557
+ if (args?.search)
558
+ params.set('search', String(args?.search));
559
+ if (args?.limit)
560
+ params.set('limit', String(args?.limit));
561
+ if (args?.entityType)
562
+ params.set('entityType', String(args?.entityType));
563
+ if (args?.status)
564
+ params.set('status', String(args?.status));
565
+ const res = await apiRequest(`/clients?${params.toString()}`);
566
+ let clients = res.data || [];
567
+ if (args?.tag) {
568
+ const searchTag = String(args.tag).toLowerCase();
569
+ clients = clients.filter((c) => Array.isArray(c.tags) && c.tags.some((t) => t.toLowerCase() === searchTag));
570
+ }
571
+ if (clients.length === 0) {
572
+ return {
573
+ content: [{ type: 'text', text: 'No clients found in the directory.' }],
574
+ };
575
+ }
576
+ const formatted = clients
577
+ .map((c) => {
578
+ const company = c.company ? ` (${c.company})` : '';
579
+ const phone = c.phone ? ` | Phone: ${c.phone}` : '';
580
+ const entity = c.entityType ? ` | Entity: ${c.entityType}` : '';
581
+ const tags = c.tags?.length ? ` | Tags: [${c.tags.join(', ')}]` : '';
582
+ return `• ${c.name} <${c.email}>${company}${phone}${entity}${tags}\n ID: ${c.id}`;
583
+ })
584
+ .join('\n\n');
585
+ return {
586
+ content: [
587
+ {
588
+ type: 'text',
589
+ text: `Found ${clients.length} clients:\n\n${formatted}`,
590
+ },
591
+ ],
592
+ };
593
+ }
594
+ case 'get_client': {
595
+ const clientId = String(args?.clientId);
596
+ const res = await apiRequest(`/clients/${clientId}`);
597
+ const c = res.data;
598
+ let requestsSummary = 'No document requests yet.';
599
+ if (c.requests && c.requests.length > 0) {
600
+ requestsSummary = c.requests
601
+ .map((r) => ` - [${r.status.toUpperCase()}] ${r.title} (ID: ${r.id})`)
602
+ .join('\n');
603
+ }
604
+ let contactsSummary = 'No linked contacts.';
605
+ if (c.linkedContacts && c.linkedContacts.length > 0) {
606
+ contactsSummary = c.linkedContacts
607
+ .map((lc) => ` - ${lc.name} (${lc.role}) ${lc.email ? `<${lc.email}>` : ''}`)
608
+ .join('\n');
609
+ }
610
+ const report = `👤 Client Profile: ${c.name}\n` +
611
+ `• ID: ${c.id}\n` +
612
+ `• Email: ${c.email}\n` +
613
+ `• Phone: ${c.phone || 'Not provided'}\n` +
614
+ `• Company: ${c.company || 'Not provided'}\n` +
615
+ `• Entity Type: ${c.entityType || 'individual'}\n` +
616
+ `• Status: ${c.status || 'active'}\n` +
617
+ `• Fiscal Year End: ${c.fiscalYearEnd || '12/31'}\n` +
618
+ `• Filing Cadence: ${c.filingCadence || 'annual'}\n` +
619
+ `• State: ${c.stateOfFiling || 'N/A'}\n` +
620
+ `• Tax Year: ${c.taxYear || '2026'}\n` +
621
+ `• Masked Tax ID: ${c.maskedId || 'None'}\n` +
622
+ `• Tags: ${c.tags?.length ? `[${c.tags.join(', ')}]` : 'None'}\n` +
623
+ `• Pinned Note: ${c.pinnedNote ? `"${c.pinnedNote}"` : 'None'}\n` +
624
+ `• Notes: ${c.notes || 'None'}\n\n` +
625
+ `Linked Contacts & Family:\n${contactsSummary}\n\n` +
626
+ `Document Intake Requests:\n${requestsSummary}`;
627
+ return {
628
+ content: [{ type: 'text', text: report }],
629
+ };
630
+ }
631
+ case 'create_or_update_client': {
632
+ const payload = {
633
+ name: args?.name,
634
+ email: args?.email,
635
+ phone: args?.phone,
636
+ company: args?.company,
637
+ notes: args?.notes,
638
+ entityType: args?.entityType,
639
+ status: args?.status,
640
+ fiscalYearEnd: args?.fiscalYearEnd,
641
+ filingCadence: args?.filingCadence,
642
+ stateOfFiling: args?.stateOfFiling,
643
+ taxYear: args?.taxYear,
644
+ tags: args?.tags,
645
+ pinnedNote: args?.pinnedNote,
646
+ qboId: args?.qboId,
647
+ taxSoftwareId: args?.taxSoftwareId,
648
+ maskedId: args?.maskedId,
649
+ linkedContacts: args?.linkedContacts,
650
+ };
651
+ const res = await apiRequest('/clients', {
652
+ method: 'POST',
653
+ body: JSON.stringify(payload),
654
+ });
655
+ const c = res.data;
656
+ const actionLabel = c.updatedExisting ? 'Updated existing client record' : 'Created new client record';
657
+ return {
658
+ content: [
659
+ {
660
+ type: 'text',
661
+ text: `✅ ${actionLabel}:\n\n` +
662
+ `• ID: ${c.id}\n` +
663
+ `• Name: ${c.name}\n` +
664
+ `• Email: ${c.email}\n` +
665
+ `• Company: ${c.company || 'N/A'}\n` +
666
+ `• Entity: ${c.entityType || 'individual'}\n` +
667
+ `• Status: ${c.status || 'active'}\n` +
668
+ `• Tags: ${c.tags?.length ? `[${c.tags.join(', ')}]` : 'None'}\n` +
669
+ `• FYE: ${c.fiscalYearEnd || '12/31'}\n` +
670
+ `• Pinned Note: ${c.pinnedNote ? `"${c.pinnedNote}"` : 'None'}`,
671
+ },
672
+ ],
673
+ };
674
+ }
675
+ // TEMPLATE HANDLERS
676
+ case 'list_templates': {
677
+ const params = new URLSearchParams();
678
+ if (args?.includeStarters !== undefined) {
679
+ params.set('includeStarters', String(args.includeStarters));
680
+ }
681
+ const res = await apiRequest(`/templates?${params.toString()}`);
682
+ const templates = res.data || [];
683
+ if (templates.length === 0) {
684
+ return {
685
+ content: [{ type: 'text', text: 'No checklist templates found in this workspace.' }],
686
+ };
687
+ }
688
+ const formatted = templates
689
+ .map((t) => {
690
+ const badge = t.isStarter ? ' [STARTER]' : '';
691
+ return `• ${t.name}${badge}\n` +
692
+ ` ID: ${t.id} | Items: ${t.items?.length || 0}\n` +
693
+ ` ${t.description || 'No description'}`;
694
+ })
695
+ .join('\n\n');
696
+ return {
697
+ content: [
698
+ {
699
+ type: 'text',
700
+ text: `Found ${templates.length} templates:\n\n${formatted}`,
701
+ },
702
+ ],
703
+ };
704
+ }
705
+ case 'get_template': {
706
+ const templateId = String(args?.templateId);
707
+ const res = await apiRequest(`/templates/${templateId}`);
708
+ const t = res.data;
709
+ const itemsList = (t.items || [])
710
+ .map((it, idx) => {
711
+ const req = it.isRequired ? '(Required)' : '(Optional)';
712
+ const multi = it.allowMultiple ? '[Multiple files]' : '';
713
+ return ` ${idx + 1}. ${it.title} ${req} ${multi}\n ${it.description || ''}`;
714
+ })
715
+ .join('\n');
716
+ const report = `📝 Template: ${t.name}\n` +
717
+ `• ID: ${t.id}\n` +
718
+ `• Description: ${t.description || 'None'}\n` +
719
+ `• Starter Template: ${t.isStarter ? 'Yes' : 'No'}\n\n` +
720
+ `Checklist Items (${t.items?.length || 0}):\n${itemsList}`;
721
+ return {
722
+ content: [{ type: 'text', text: report }],
723
+ };
724
+ }
725
+ case 'create_template': {
726
+ const payload = {
727
+ name: args?.name,
728
+ description: args?.description,
729
+ isShared: args?.isShared ?? true,
730
+ items: args?.items,
731
+ };
732
+ const res = await apiRequest('/templates', {
733
+ method: 'POST',
734
+ body: JSON.stringify(payload),
735
+ });
736
+ const t = res.data;
737
+ return {
738
+ content: [
739
+ {
740
+ type: 'text',
741
+ text: `✅ Template created successfully!\n\n` +
742
+ `• ID: ${t.id}\n` +
743
+ `• Name: ${t.name}\n` +
744
+ `• Items: ${t.items?.length || 0}\n` +
745
+ `• Description: ${t.description || 'None'}`,
746
+ },
747
+ ],
748
+ };
749
+ }
750
+ case 'update_template': {
751
+ const templateId = String(args?.templateId);
752
+ const payload = {};
753
+ if (args?.name !== undefined)
754
+ payload.name = args.name;
755
+ if (args?.description !== undefined)
756
+ payload.description = args.description;
757
+ if (args?.items !== undefined)
758
+ payload.items = args.items;
759
+ const res = await apiRequest(`/templates/${templateId}`, {
760
+ method: 'PATCH',
761
+ body: JSON.stringify(payload),
762
+ });
763
+ const t = res.data;
764
+ return {
765
+ content: [
766
+ {
767
+ type: 'text',
768
+ text: `✅ Template updated successfully!\n\n` +
769
+ `• ID: ${t.id}\n` +
770
+ `• Name: ${t.name}\n` +
771
+ `• Items: ${t.items?.length || 0}\n` +
772
+ `• Description: ${t.description || 'None'}`,
773
+ },
774
+ ],
775
+ };
776
+ }
777
+ default:
778
+ throw new Error(`Unknown tool: ${name}`);
779
+ }
780
+ }
781
+ catch (error) {
782
+ return {
783
+ isError: true,
784
+ content: [
785
+ {
786
+ type: 'text',
787
+ text: `❌ Drop Spot MCP Error: ${error.message || String(error)}`,
788
+ },
789
+ ],
790
+ };
791
+ }
792
+ });
793
+ // ----------------------------------------------------------------------------
794
+ // 3. RESOURCES: Dynamic Drop Spot Dossier Inspection
795
+ // ----------------------------------------------------------------------------
796
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
797
+ return {
798
+ resources: [
799
+ {
800
+ uri: 'dropspot://requests/active',
801
+ name: 'Active Client Intake Requests',
802
+ description: 'Real-time JSON dossier feed of all open document requests in the active workspace.',
803
+ mimeType: 'application/json',
804
+ },
805
+ {
806
+ uri: 'dropspot://clients/all',
807
+ name: 'Workspace Client Directory',
808
+ description: 'Complete client roster including names, emails, phone numbers, and company metadata.',
809
+ mimeType: 'application/json',
810
+ },
811
+ {
812
+ uri: 'dropspot://templates/all',
813
+ name: 'Checklist Templates',
814
+ description: 'All reusable document checklist templates available in the workspace.',
815
+ mimeType: 'application/json',
816
+ },
817
+ ],
818
+ };
819
+ });
820
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
821
+ const uri = request.params.uri;
822
+ if (uri === 'dropspot://requests/active') {
823
+ const res = await apiRequest('/requests?status=in_process');
824
+ return {
825
+ contents: [
826
+ {
827
+ uri,
828
+ mimeType: 'application/json',
829
+ text: JSON.stringify(res.data, null, 2),
830
+ },
831
+ ],
832
+ };
833
+ }
834
+ if (uri === 'dropspot://clients/all') {
835
+ const res = await apiRequest('/clients');
836
+ return {
837
+ contents: [
838
+ {
839
+ uri,
840
+ mimeType: 'application/json',
841
+ text: JSON.stringify(res.data, null, 2),
842
+ },
843
+ ],
844
+ };
845
+ }
846
+ if (uri === 'dropspot://templates/all') {
847
+ const res = await apiRequest('/templates');
848
+ return {
849
+ contents: [
850
+ {
851
+ uri,
852
+ mimeType: 'application/json',
853
+ text: JSON.stringify(res.data, null, 2),
854
+ },
855
+ ],
856
+ };
857
+ }
858
+ throw new Error(`Resource not found: ${uri}`);
859
+ });
860
+ // ----------------------------------------------------------------------------
861
+ // 4. SERVER INITIALIZATION & TRANSPORT
862
+ // ----------------------------------------------------------------------------
863
+ async function main() {
864
+ const transport = new StdioServerTransport();
865
+ await server.connect(transport);
866
+ console.error('Drop Spot MCP Server running on stdio');
867
+ }
868
+ main().catch((error) => {
869
+ console.error('Fatal error initializing Drop Spot MCP server:', error);
870
+ process.exit(1);
871
+ });
package/glama.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "name": "@dropspot/mcp",
4
+ "description": "Model Context Protocol (MCP) server for Drop Spot — automated client document collection, file intake, checklist management, and 1-click reviews.",
5
+ "maintainers": [
6
+ "officialdropspot",
7
+ "leoalfonso14"
8
+ ]
9
+ }
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@dropspot/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.2.0",
4
+ "description": "Model Context Protocol (MCP) server for Drop Spot — automated client document collection, file intake, checklist management, and 1-click reviews for Claude Desktop, Cursor, and AI agents.",
5
+ "type": "module",
6
+ "homepage": "https://godropspot.com",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/officialdropspot/dropspot-mcp.git"
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "model-context-protocol",
14
+ "claude-desktop",
15
+ "cursor",
16
+ "antigravity",
17
+ "file-sharing",
18
+ "document-collection",
19
+ "client-portal",
20
+ "client-intake",
21
+ "tax-documents",
22
+ "paperwork-automation",
23
+ "file-intake"
24
+ ],
25
+ "main": "dist/index.js",
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE",
30
+ "smithery.yaml",
31
+ "glama.json"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "license": "MIT",
37
+ "bin": {
38
+ "dropspot-mcp": "dist/index.js"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "start": "node dist/index.js",
43
+ "prepublishOnly": "npm run build"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.6.1",
47
+ "zod": "^3.24.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.10.1",
51
+ "typescript": "~5.7.2"
52
+ }
53
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,27 @@
1
+ # Smithery configuration file: https://smithery.ai/docs/config
2
+ startCommand:
3
+ type: stdio
4
+ configSchema:
5
+ type: object
6
+ required:
7
+ - dropspotApiKey
8
+ properties:
9
+ dropspotApiKey:
10
+ type: string
11
+ description: "Your Drop Spot API key (format: ds_live_... or ds_test_...)"
12
+ baseUrl:
13
+ type: string
14
+ description: "Drop Spot API base URL"
15
+ default: "https://portal.godropspot.com"
16
+ commandFunction: |
17
+ (config) => ({
18
+ command: 'npx',
19
+ args: ['-y', '@dropspot/mcp'],
20
+ env: {
21
+ DROPSPOT_API_KEY: config.dropspotApiKey,
22
+ DROPSPOT_API_BASE_URL: config.baseUrl || 'https://portal.godropspot.com'
23
+ }
24
+ })
25
+ exampleConfig:
26
+ dropspotApiKey: "ds_live_abcdef1234567890"
27
+ baseUrl: "https://portal.godropspot.com"