daeda-mcp 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.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/db/config.d.ts +12 -0
- package/dist/db/config.js +53 -0
- package/dist/db/keychain.d.ts +2 -0
- package/dist/db/keychain.js +11 -0
- package/dist/db/schema.d.ts +10 -0
- package/dist/db/schema.js +65 -0
- package/dist/db/sqlite.d.ts +43 -0
- package/dist/db/sqlite.js +280 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -0
- package/dist/sync/csv-loader.d.ts +15 -0
- package/dist/sync/csv-loader.js +450 -0
- package/dist/sync/csv-worker.d.ts +21 -0
- package/dist/sync/csv-worker.js +60 -0
- package/dist/sync/export-api.d.ts +57 -0
- package/dist/sync/export-api.js +355 -0
- package/dist/sync/init-manager.d.ts +5 -0
- package/dist/sync/init-manager.js +274 -0
- package/dist/sync/init-state.d.ts +31 -0
- package/dist/sync/init-state.js +64 -0
- package/dist/sync/seeder.d.ts +1 -0
- package/dist/sync/seeder.js +176 -0
- package/dist/tools/auth.d.ts +26 -0
- package/dist/tools/auth.js +127 -0
- package/dist/tools/query.d.ts +6 -0
- package/dist/tools/query.js +109 -0
- package/package.json +34 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { createClient } from "@libsql/client";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { mkdir, access, unlink } from "fs/promises";
|
|
5
|
+
import { getDbEncryptionKey } from "./keychain.js";
|
|
6
|
+
import { SCHEMA_SQL } from "./schema.js";
|
|
7
|
+
import { resetInitState } from "../sync/init-state.js";
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const DATA_DIR = join(__dirname, "..", "..", "data");
|
|
10
|
+
const DB_PATH = join(DATA_DIR, "hubspot.db");
|
|
11
|
+
let dbClient = null;
|
|
12
|
+
export function getDbPath() {
|
|
13
|
+
return DB_PATH;
|
|
14
|
+
}
|
|
15
|
+
export async function dbExists() {
|
|
16
|
+
try {
|
|
17
|
+
await access(DB_PATH);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function isDbHealthy() {
|
|
25
|
+
try {
|
|
26
|
+
const db = await getDb();
|
|
27
|
+
const result = await db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='contacts'");
|
|
28
|
+
return result.rows.length > 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function getDb() {
|
|
35
|
+
if (dbClient)
|
|
36
|
+
return dbClient;
|
|
37
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
38
|
+
const encryptionKey = await getDbEncryptionKey();
|
|
39
|
+
dbClient = createClient({
|
|
40
|
+
url: `file:${DB_PATH}`,
|
|
41
|
+
encryptionKey,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
await initializeSchema();
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const isKeyMismatch = err instanceof Error &&
|
|
48
|
+
(err.message.includes("SQLITE_NOTADB") ||
|
|
49
|
+
err.message.includes("file is not a database"));
|
|
50
|
+
if (isKeyMismatch) {
|
|
51
|
+
console.error("[sqlite] Encryption key mismatch detected, resetting all sync state...");
|
|
52
|
+
dbClient.close();
|
|
53
|
+
dbClient = null;
|
|
54
|
+
await unlink(DB_PATH).catch(() => { });
|
|
55
|
+
resetInitState();
|
|
56
|
+
dbClient = createClient({
|
|
57
|
+
url: `file:${DB_PATH}`,
|
|
58
|
+
encryptionKey,
|
|
59
|
+
});
|
|
60
|
+
await initializeSchema();
|
|
61
|
+
console.error("[sqlite] Database and sync state reset with current encryption key");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return dbClient;
|
|
68
|
+
}
|
|
69
|
+
async function initializeSchema() {
|
|
70
|
+
if (!dbClient)
|
|
71
|
+
throw new Error("Database not initialized");
|
|
72
|
+
const statements = SCHEMA_SQL.split(";")
|
|
73
|
+
.map((s) => s.trim())
|
|
74
|
+
.filter((s) => s.length > 0);
|
|
75
|
+
for (const statement of statements) {
|
|
76
|
+
await dbClient.execute(statement);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function closeDb() {
|
|
80
|
+
if (dbClient) {
|
|
81
|
+
dbClient.close();
|
|
82
|
+
dbClient = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function clearTable(table) {
|
|
86
|
+
const db = await getDb();
|
|
87
|
+
await db.execute(`DELETE FROM ${table}`);
|
|
88
|
+
}
|
|
89
|
+
export async function insertContact(id, email, properties) {
|
|
90
|
+
const db = await getDb();
|
|
91
|
+
await db.execute({
|
|
92
|
+
sql: `INSERT OR REPLACE INTO contacts (id, email, last_synced, properties) VALUES (?, ?, ?, ?)`,
|
|
93
|
+
args: [id, email, new Date().toISOString(), JSON.stringify(properties)],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export async function insertCompany(id, domain, properties) {
|
|
97
|
+
const db = await getDb();
|
|
98
|
+
await db.execute({
|
|
99
|
+
sql: `INSERT OR REPLACE INTO companies (id, domain, last_synced, properties) VALUES (?, ?, ?, ?)`,
|
|
100
|
+
args: [id, domain, new Date().toISOString(), JSON.stringify(properties)],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function insertDeal(id, dealname, properties) {
|
|
104
|
+
const db = await getDb();
|
|
105
|
+
await db.execute({
|
|
106
|
+
sql: `INSERT OR REPLACE INTO deals (id, dealname, last_synced, properties) VALUES (?, ?, ?, ?)`,
|
|
107
|
+
args: [id, dealname, new Date().toISOString(), JSON.stringify(properties)],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export async function insertAssociation(associationType, fromId, toId) {
|
|
111
|
+
const db = await getDb();
|
|
112
|
+
let sql;
|
|
113
|
+
switch (associationType) {
|
|
114
|
+
case "contact_company":
|
|
115
|
+
sql = `INSERT OR IGNORE INTO contact_company (contact_id, company_id) VALUES (?, ?)`;
|
|
116
|
+
break;
|
|
117
|
+
case "deal_contact":
|
|
118
|
+
sql = `INSERT OR IGNORE INTO deal_contact (deal_id, contact_id) VALUES (?, ?)`;
|
|
119
|
+
break;
|
|
120
|
+
case "deal_company":
|
|
121
|
+
sql = `INSERT OR IGNORE INTO deal_company (deal_id, company_id) VALUES (?, ?)`;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
await db.execute({ sql, args: [fromId, toId] });
|
|
125
|
+
}
|
|
126
|
+
export async function setMetadata(key, value) {
|
|
127
|
+
const db = await getDb();
|
|
128
|
+
await db.execute({
|
|
129
|
+
sql: `INSERT OR REPLACE INTO sync_metadata (key, value) VALUES (?, ?)`,
|
|
130
|
+
args: [key, value],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const BATCH_SIZE = 500;
|
|
134
|
+
export async function batchInsertContacts(rows) {
|
|
135
|
+
if (rows.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
const db = await getDb();
|
|
138
|
+
const now = new Date().toISOString();
|
|
139
|
+
await db.execute("BEGIN TRANSACTION");
|
|
140
|
+
try {
|
|
141
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
142
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
143
|
+
const placeholders = batch.map(() => "(?, ?, ?, ?)").join(", ");
|
|
144
|
+
const args = batch.flatMap((row) => [
|
|
145
|
+
row.id,
|
|
146
|
+
row.email,
|
|
147
|
+
now,
|
|
148
|
+
JSON.stringify(row.properties),
|
|
149
|
+
]);
|
|
150
|
+
await db.execute({
|
|
151
|
+
sql: `INSERT OR REPLACE INTO contacts (id, email, last_synced, properties) VALUES ${placeholders}`,
|
|
152
|
+
args,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
await db.execute("COMMIT");
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
await db.execute("ROLLBACK");
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function batchInsertCompanies(rows) {
|
|
163
|
+
if (rows.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
const db = await getDb();
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
await db.execute("BEGIN TRANSACTION");
|
|
168
|
+
try {
|
|
169
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
170
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
171
|
+
const placeholders = batch.map(() => "(?, ?, ?, ?)").join(", ");
|
|
172
|
+
const args = batch.flatMap((row) => [
|
|
173
|
+
row.id,
|
|
174
|
+
row.domain,
|
|
175
|
+
now,
|
|
176
|
+
JSON.stringify(row.properties),
|
|
177
|
+
]);
|
|
178
|
+
await db.execute({
|
|
179
|
+
sql: `INSERT OR REPLACE INTO companies (id, domain, last_synced, properties) VALUES ${placeholders}`,
|
|
180
|
+
args,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
await db.execute("COMMIT");
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
await db.execute("ROLLBACK");
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export async function batchInsertDeals(rows) {
|
|
191
|
+
if (rows.length === 0)
|
|
192
|
+
return;
|
|
193
|
+
const db = await getDb();
|
|
194
|
+
const now = new Date().toISOString();
|
|
195
|
+
await db.execute("BEGIN TRANSACTION");
|
|
196
|
+
try {
|
|
197
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
198
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
199
|
+
const placeholders = batch.map(() => "(?, ?, ?, ?)").join(", ");
|
|
200
|
+
const args = batch.flatMap((row) => [
|
|
201
|
+
row.id,
|
|
202
|
+
row.dealname,
|
|
203
|
+
now,
|
|
204
|
+
JSON.stringify(row.properties),
|
|
205
|
+
]);
|
|
206
|
+
await db.execute({
|
|
207
|
+
sql: `INSERT OR REPLACE INTO deals (id, dealname, last_synced, properties) VALUES ${placeholders}`,
|
|
208
|
+
args,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
await db.execute("COMMIT");
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
await db.execute("ROLLBACK");
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export async function batchInsertAssociations(associationType, rows) {
|
|
219
|
+
if (rows.length === 0)
|
|
220
|
+
return;
|
|
221
|
+
const db = await getDb();
|
|
222
|
+
const tableConfig = {
|
|
223
|
+
contact_company: { table: "contact_company", cols: "contact_id, company_id" },
|
|
224
|
+
deal_contact: { table: "deal_contact", cols: "deal_id, contact_id" },
|
|
225
|
+
deal_company: { table: "deal_company", cols: "deal_id, company_id" },
|
|
226
|
+
};
|
|
227
|
+
const { table, cols } = tableConfig[associationType];
|
|
228
|
+
await db.execute("BEGIN TRANSACTION");
|
|
229
|
+
try {
|
|
230
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
231
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
232
|
+
const placeholders = batch.map(() => "(?, ?)").join(", ");
|
|
233
|
+
const args = batch.flatMap((row) => [row.fromId, row.toId]);
|
|
234
|
+
await db.execute({
|
|
235
|
+
sql: `INSERT OR IGNORE INTO ${table} (${cols}) VALUES ${placeholders}`,
|
|
236
|
+
args,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
await db.execute("COMMIT");
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
await db.execute("ROLLBACK");
|
|
243
|
+
throw e;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export async function getMetadata(key) {
|
|
247
|
+
const db = await getDb();
|
|
248
|
+
const result = await db.execute({
|
|
249
|
+
sql: `SELECT value FROM sync_metadata WHERE key = ?`,
|
|
250
|
+
args: [key],
|
|
251
|
+
});
|
|
252
|
+
return result.rows[0]?.value ?? null;
|
|
253
|
+
}
|
|
254
|
+
export async function getAllMetadata() {
|
|
255
|
+
const db = await getDb();
|
|
256
|
+
const result = await db.execute(`SELECT key, value FROM sync_metadata`);
|
|
257
|
+
const metadata = {};
|
|
258
|
+
for (const row of result.rows) {
|
|
259
|
+
metadata[row.key] = row.value;
|
|
260
|
+
}
|
|
261
|
+
return metadata;
|
|
262
|
+
}
|
|
263
|
+
export async function getRecordCount(objectType) {
|
|
264
|
+
const db = await getDb();
|
|
265
|
+
const result = await db.execute(`SELECT COUNT(*) as count FROM ${objectType}`);
|
|
266
|
+
return result.rows[0]?.count ?? 0;
|
|
267
|
+
}
|
|
268
|
+
export async function executeQuery(sql) {
|
|
269
|
+
const db = await getDb();
|
|
270
|
+
const result = await db.execute(sql);
|
|
271
|
+
const columns = result.columns;
|
|
272
|
+
const rows = result.rows.map((row) => {
|
|
273
|
+
const obj = {};
|
|
274
|
+
columns.forEach((col, idx) => {
|
|
275
|
+
obj[col] = row[idx];
|
|
276
|
+
});
|
|
277
|
+
return obj;
|
|
278
|
+
});
|
|
279
|
+
return { columns, rows };
|
|
280
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { dbStatus } from "./tools/auth.js";
|
|
7
|
+
import { resumeIfNeeded } from "./sync/init-manager.js";
|
|
8
|
+
import { getRawSql, rawSqlSchema } from "./tools/query.js";
|
|
9
|
+
import { readInitState } from "./sync/init-state.js";
|
|
10
|
+
function openUrl(url) {
|
|
11
|
+
const platform = process.platform;
|
|
12
|
+
let command;
|
|
13
|
+
let args;
|
|
14
|
+
switch (platform) {
|
|
15
|
+
case "darwin":
|
|
16
|
+
command = "open";
|
|
17
|
+
args = [url];
|
|
18
|
+
break;
|
|
19
|
+
case "win32":
|
|
20
|
+
command = "cmd";
|
|
21
|
+
args = ["/c", "start", "", url];
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
command = "xdg-open";
|
|
25
|
+
args = [url];
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
spawn(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
29
|
+
}
|
|
30
|
+
const DATABASE_SCHEMA = {
|
|
31
|
+
tables: {
|
|
32
|
+
contacts: {
|
|
33
|
+
description: "HubSpot contacts synced to local database",
|
|
34
|
+
columns: {
|
|
35
|
+
id: { type: "TEXT", description: "HubSpot contact ID (primary key)" },
|
|
36
|
+
email: { type: "TEXT", description: "Contact email address (indexed)" },
|
|
37
|
+
last_synced: { type: "TEXT", description: "ISO timestamp of last sync" },
|
|
38
|
+
properties: {
|
|
39
|
+
type: "TEXT (JSON)",
|
|
40
|
+
description: "All HubSpot contact properties stored as JSON. Use json_extract(properties, '$.fieldname') to query specific fields.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
companies: {
|
|
45
|
+
description: "HubSpot companies synced to local database",
|
|
46
|
+
columns: {
|
|
47
|
+
id: { type: "TEXT", description: "HubSpot company ID (primary key)" },
|
|
48
|
+
domain: { type: "TEXT", description: "Company domain (indexed)" },
|
|
49
|
+
last_synced: { type: "TEXT", description: "ISO timestamp of last sync" },
|
|
50
|
+
properties: {
|
|
51
|
+
type: "TEXT (JSON)",
|
|
52
|
+
description: "All HubSpot company properties stored as JSON. Use json_extract(properties, '$.fieldname') to query specific fields.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
deals: {
|
|
57
|
+
description: "HubSpot deals synced to local database",
|
|
58
|
+
columns: {
|
|
59
|
+
id: { type: "TEXT", description: "HubSpot deal ID (primary key)" },
|
|
60
|
+
dealname: { type: "TEXT", description: "Deal name (indexed)" },
|
|
61
|
+
last_synced: { type: "TEXT", description: "ISO timestamp of last sync" },
|
|
62
|
+
properties: {
|
|
63
|
+
type: "TEXT (JSON)",
|
|
64
|
+
description: "All HubSpot deal properties stored as JSON. Use json_extract(properties, '$.fieldname') to query specific fields.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
associations: {
|
|
70
|
+
contact_company: {
|
|
71
|
+
description: "Links contacts to companies (many-to-many)",
|
|
72
|
+
columns: {
|
|
73
|
+
contact_id: { type: "TEXT", description: "Foreign key to contacts.id" },
|
|
74
|
+
company_id: { type: "TEXT", description: "Foreign key to companies.id" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
deal_contact: {
|
|
78
|
+
description: "Links deals to contacts (many-to-many)",
|
|
79
|
+
columns: {
|
|
80
|
+
deal_id: { type: "TEXT", description: "Foreign key to deals.id" },
|
|
81
|
+
contact_id: { type: "TEXT", description: "Foreign key to contacts.id" },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
deal_company: {
|
|
85
|
+
description: "Links deals to companies (many-to-many)",
|
|
86
|
+
columns: {
|
|
87
|
+
deal_id: { type: "TEXT", description: "Foreign key to deals.id" },
|
|
88
|
+
company_id: { type: "TEXT", description: "Foreign key to companies.id" },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
queryExamples: [
|
|
93
|
+
{
|
|
94
|
+
description: "Get all contacts with their email",
|
|
95
|
+
sql: "SELECT id, email, json_extract(properties, '$.firstname') as firstname FROM contacts LIMIT 10",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
description: "Find contacts at a specific company",
|
|
99
|
+
sql: "SELECT c.* FROM contacts c JOIN contact_company cc ON c.id = cc.contact_id JOIN companies co ON cc.company_id = co.id WHERE co.domain = 'example.com'",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
description: "Get deals with their associated company names",
|
|
103
|
+
sql: "SELECT d.dealname, json_extract(co.properties, '$.name') as company_name FROM deals d JOIN deal_company dc ON d.id = dc.deal_id JOIN companies co ON dc.company_id = co.id",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
const server = new McpServer({
|
|
108
|
+
name: "daeda-mcp",
|
|
109
|
+
version: "1.0.0",
|
|
110
|
+
});
|
|
111
|
+
server.registerTool("db_status", {
|
|
112
|
+
description: "Check database initialization status and sync progress. The database auto-initializes on server startup. " +
|
|
113
|
+
"Returns status: 'not_started' | 'sending_requests' | 'seeding' | 'syncing' | 'ready' | 'error'. " +
|
|
114
|
+
"During 'seeding', a quick preview of ~1000 recent deals is loaded for immediate use while full sync continues. " +
|
|
115
|
+
"When syncing, shows progress as 'X/6 exports synced'. " +
|
|
116
|
+
"Use forceReinit=true to restart initialization from scratch.",
|
|
117
|
+
inputSchema: z.object({
|
|
118
|
+
forceReinit: z
|
|
119
|
+
.boolean()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("Force re-initialization from scratch, discarding current progress"),
|
|
122
|
+
}),
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
const { forceReinit = false } = args;
|
|
125
|
+
const result = await dbStatus(forceReinit);
|
|
126
|
+
return { content: [{ type: "text", text: result }] };
|
|
127
|
+
});
|
|
128
|
+
server.registerTool("get_raw_sql", {
|
|
129
|
+
description: "Execute a custom SELECT query against the local CRM database. Only SELECT queries are allowed for security.",
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
sql: z.string().describe("SQL query to execute (SELECT only)"),
|
|
132
|
+
}),
|
|
133
|
+
}, async (args) => {
|
|
134
|
+
const result = await getRawSql(rawSqlSchema.parse(args));
|
|
135
|
+
return { content: [{ type: "text", text: result }] };
|
|
136
|
+
});
|
|
137
|
+
server.registerResource("database-schema", "hubspot://schema", {
|
|
138
|
+
title: "HubSpot Database Schema",
|
|
139
|
+
description: "Complete schema for the local HubSpot CRM database including tables, columns, associations, and example queries. " +
|
|
140
|
+
"All HubSpot properties are stored as JSON in the 'properties' column - use json_extract() to query them.",
|
|
141
|
+
mimeType: "application/json",
|
|
142
|
+
}, async (uri) => ({
|
|
143
|
+
contents: [
|
|
144
|
+
{
|
|
145
|
+
uri: uri.href,
|
|
146
|
+
text: JSON.stringify(DATABASE_SCHEMA, null, 2),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
}));
|
|
150
|
+
// Start server
|
|
151
|
+
async function main() {
|
|
152
|
+
const transport = new StdioServerTransport();
|
|
153
|
+
await server.connect(transport);
|
|
154
|
+
console.error("Daeda MCP server started");
|
|
155
|
+
const state = readInitState();
|
|
156
|
+
if (state.status === "idle") {
|
|
157
|
+
openUrl("https://www.daeda.tech/daeda-mcp-waitlist");
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await resumeIfNeeded();
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error("Failed to start/resume initialization:", err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
main().catch((error) => {
|
|
167
|
+
console.error("Fatal error:", error);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ObjectType, type AssociationType } from "../db/schema.js";
|
|
2
|
+
export declare function loadContactsCsvFromFile(filePath: string, onProgress?: (loaded: number) => void): Promise<number>;
|
|
3
|
+
export declare function loadCompaniesCsvFromFile(filePath: string, onProgress?: (loaded: number) => void): Promise<number>;
|
|
4
|
+
export declare function loadDealsCsvFromFile(filePath: string, onProgress?: (loaded: number) => void): Promise<number>;
|
|
5
|
+
export declare function loadAssociationsCsvFromFile(filePath: string, associationType: AssociationType, onProgress?: (loaded: number) => void): Promise<number>;
|
|
6
|
+
export declare function loadContactsCsv(csvData: string, onProgress?: (loaded: number) => void): Promise<number>;
|
|
7
|
+
export declare function loadCompaniesCsv(csvData: string, onProgress?: (loaded: number) => void): Promise<number>;
|
|
8
|
+
export declare function loadDealsCsv(csvData: string, onProgress?: (loaded: number) => void): Promise<number>;
|
|
9
|
+
export declare function loadAssociationsCsv(csvData: string, associationType: AssociationType, onProgress?: (loaded: number) => void): Promise<number>;
|
|
10
|
+
export declare function loadAllData(objects: Record<ObjectType, string>, associations: Record<AssociationType, string>, onProgress?: (message: string) => void): Promise<{
|
|
11
|
+
contacts: number;
|
|
12
|
+
companies: number;
|
|
13
|
+
deals: number;
|
|
14
|
+
associations: Record<AssociationType, number>;
|
|
15
|
+
}>;
|