0nmcp 3.2.2 → 4.5.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 +94 -21
- package/README.md +9 -8
- package/crm/addons.js +319 -0
- package/crm/agent-builder.js +223 -0
- package/crm/billing.js +173 -0
- package/crm/conversations.js +14 -0
- package/crm/course-generator.js +161 -0
- package/crm/email-campaigns.js +250 -0
- package/crm/helpers.js +9 -0
- package/crm/index.js +48 -2
- package/crm/marketplace-billing.js +162 -0
- package/crm/media.js +167 -0
- package/crm/oauth-store.js +262 -0
- package/crm/phone-system.js +88 -0
- package/crm/saas-management.js +72 -0
- package/crm/sdk.js +60 -0
- package/crm/supabase-session-storage.js +54 -0
- package/crm/surveys-forms.js +96 -0
- package/crm/user-context.js +103 -0
- package/lib/stats.json +1 -1
- package/package.json +4 -3
package/crm/media.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — CRM Media Library Tools (SDK-powered)
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Upload, organize, browse, folder management.
|
|
5
|
+
// SDK: medias.fetchMediaContent, uploadMediaContent,
|
|
6
|
+
// deleteMediaContent, updateMediaObject, createMediaFolder,
|
|
7
|
+
// bulkUpdateMediaObjects, bulkDeleteMediaObjects
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
import getSDK from "./sdk.js";
|
|
11
|
+
|
|
12
|
+
export function registerMediaTools(server, z) {
|
|
13
|
+
|
|
14
|
+
server.tool(
|
|
15
|
+
"crm_media_list",
|
|
16
|
+
"List files and folders from the CRM media library. Supports pagination, search, and folder browsing.",
|
|
17
|
+
{
|
|
18
|
+
location_id: z.string().describe("CRM location ID"),
|
|
19
|
+
parent_id: z.string().optional().describe("Folder ID to browse into (omit for root)"),
|
|
20
|
+
type: z.string().optional().describe("Filter: file, folder, or all (default: all)"),
|
|
21
|
+
query: z.string().optional().describe("Search query"),
|
|
22
|
+
limit: z.number().optional().describe("Max results (default 50)"),
|
|
23
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
24
|
+
sort_by: z.string().optional().describe("Sort field (default: createdAt)"),
|
|
25
|
+
sort_order: z.string().optional().describe("asc or desc (default: desc)"),
|
|
26
|
+
},
|
|
27
|
+
async ({ location_id, parent_id, type, query, limit, offset, sort_by, sort_order }) => {
|
|
28
|
+
try {
|
|
29
|
+
const sdk = await getSDK();
|
|
30
|
+
const result = await sdk.medias.fetchMediaContent({
|
|
31
|
+
altType: "location",
|
|
32
|
+
altId: location_id,
|
|
33
|
+
type: type || "all",
|
|
34
|
+
sortBy: sort_by || "createdAt",
|
|
35
|
+
sortOrder: sort_order || "desc",
|
|
36
|
+
limit: String(limit || 50),
|
|
37
|
+
offset: String(offset || 0),
|
|
38
|
+
query: query || undefined,
|
|
39
|
+
parentId: parent_id || undefined,
|
|
40
|
+
});
|
|
41
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
server.tool(
|
|
49
|
+
"crm_media_upload",
|
|
50
|
+
"Upload a file to the CRM media library. Provide a hosted URL or file data. Max 25MB.",
|
|
51
|
+
{
|
|
52
|
+
location_id: z.string().describe("CRM location ID"),
|
|
53
|
+
file_url: z.string().describe("URL of the file to upload"),
|
|
54
|
+
name: z.string().describe("File name"),
|
|
55
|
+
folder_id: z.string().optional().describe("Parent folder ID"),
|
|
56
|
+
},
|
|
57
|
+
async ({ location_id, file_url, name, folder_id }) => {
|
|
58
|
+
try {
|
|
59
|
+
const sdk = await getSDK();
|
|
60
|
+
const body = {
|
|
61
|
+
hosted: true,
|
|
62
|
+
fileUrl: file_url,
|
|
63
|
+
name,
|
|
64
|
+
altType: "location",
|
|
65
|
+
altId: location_id,
|
|
66
|
+
parentId: folder_id || undefined,
|
|
67
|
+
};
|
|
68
|
+
const result = await sdk.medias.uploadMediaContent(body);
|
|
69
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
server.tool(
|
|
77
|
+
"crm_media_create_folder",
|
|
78
|
+
"Create a new folder in the CRM media library.",
|
|
79
|
+
{
|
|
80
|
+
location_id: z.string().describe("CRM location ID"),
|
|
81
|
+
name: z.string().describe("Folder name"),
|
|
82
|
+
parent_id: z.string().optional().describe("Parent folder ID (omit for root)"),
|
|
83
|
+
},
|
|
84
|
+
async ({ location_id, name, parent_id }) => {
|
|
85
|
+
try {
|
|
86
|
+
const sdk = await getSDK();
|
|
87
|
+
const result = await sdk.medias.createMediaFolder({
|
|
88
|
+
altType: "location",
|
|
89
|
+
altId: location_id,
|
|
90
|
+
name,
|
|
91
|
+
parentId: parent_id || undefined,
|
|
92
|
+
});
|
|
93
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
server.tool(
|
|
101
|
+
"crm_media_delete",
|
|
102
|
+
"Delete a file or folder from the CRM media library.",
|
|
103
|
+
{
|
|
104
|
+
location_id: z.string().describe("CRM location ID"),
|
|
105
|
+
id: z.string().describe("File or folder ID to delete"),
|
|
106
|
+
},
|
|
107
|
+
async ({ location_id, id }) => {
|
|
108
|
+
try {
|
|
109
|
+
const sdk = await getSDK();
|
|
110
|
+
const result = await sdk.medias.deleteMediaContent({
|
|
111
|
+
id,
|
|
112
|
+
altType: "location",
|
|
113
|
+
altId: location_id,
|
|
114
|
+
});
|
|
115
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted: true, data: result }, null, 2) }] };
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.tool(
|
|
123
|
+
"crm_media_rename",
|
|
124
|
+
"Rename a file or folder in the CRM media library.",
|
|
125
|
+
{
|
|
126
|
+
location_id: z.string().describe("CRM location ID"),
|
|
127
|
+
id: z.string().describe("File or folder ID"),
|
|
128
|
+
name: z.string().describe("New name"),
|
|
129
|
+
},
|
|
130
|
+
async ({ location_id, id, name }) => {
|
|
131
|
+
try {
|
|
132
|
+
const sdk = await getSDK();
|
|
133
|
+
const result = await sdk.medias.updateMediaObject(
|
|
134
|
+
{ id },
|
|
135
|
+
{ name, altType: "location", altId: location_id }
|
|
136
|
+
);
|
|
137
|
+
return { content: [{ type: "text", text: JSON.stringify({ renamed: true, data: result }, null, 2) }] };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
server.tool(
|
|
145
|
+
"crm_media_bulk_delete",
|
|
146
|
+
"Bulk delete or trash multiple files and folders.",
|
|
147
|
+
{
|
|
148
|
+
location_id: z.string().describe("CRM location ID"),
|
|
149
|
+
ids: z.array(z.string()).describe("Array of file/folder IDs to delete"),
|
|
150
|
+
trash: z.boolean().optional().describe("Soft delete (trash) instead of permanent (default: true)"),
|
|
151
|
+
},
|
|
152
|
+
async ({ location_id, ids, trash }) => {
|
|
153
|
+
try {
|
|
154
|
+
const sdk = await getSDK();
|
|
155
|
+
const result = await sdk.medias.bulkDeleteMediaObjects({
|
|
156
|
+
altType: "location",
|
|
157
|
+
altId: location_id,
|
|
158
|
+
filesToBeDeleted: ids.map(id => ({ _id: id })),
|
|
159
|
+
status: trash !== false ? "trashed" : "deleted",
|
|
160
|
+
});
|
|
161
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted: ids.length, data: result }, null, 2) }] };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — CRM OAuth Token Store
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Stores and auto-refreshes OAuth tokens per CRM location.
|
|
5
|
+
// Tokens stored in ~/.0n/connections/crm-oauth-{locationId}.0n
|
|
6
|
+
// Falls back to PIT from crm.0n if no OAuth token exists.
|
|
7
|
+
//
|
|
8
|
+
// Usage in tool factory:
|
|
9
|
+
// const token = await oauthStore.resolveToken(locationId)
|
|
10
|
+
// // token is always a valid access_token — OAuth or PIT
|
|
11
|
+
// ============================================================
|
|
12
|
+
|
|
13
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
|
|
18
|
+
const CONNECTIONS_DIR = join(homedir(), ".0n", "connections");
|
|
19
|
+
const TOKEN_URL = "https://services.leadconnectorhq.com/oauth/token";
|
|
20
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1000; // refresh 5 min before expiry
|
|
21
|
+
|
|
22
|
+
// Default marketplace app credentials (0nCORE app — 140+ scopes)
|
|
23
|
+
const DEFAULT_CLIENT_ID = "69c762225a31e1cd2f28dd4c-mnu5pazi";
|
|
24
|
+
const DEFAULT_CLIENT_SECRET = "92cb8fc3-2b23-40bf-adce-6b6afe9b8445";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} StoredToken
|
|
28
|
+
* @property {string} access_token
|
|
29
|
+
* @property {string} refresh_token
|
|
30
|
+
* @property {number} expires_at - Unix timestamp in ms
|
|
31
|
+
* @property {string} location_id
|
|
32
|
+
* @property {string} [company_id]
|
|
33
|
+
* @property {string} [client_id]
|
|
34
|
+
* @property {string} [client_secret]
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
function tokenPath(locationId) {
|
|
38
|
+
return join(CONNECTIONS_DIR, `crm-oauth-${locationId}.0n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Load stored OAuth token for a location.
|
|
43
|
+
* @param {string} locationId
|
|
44
|
+
* @returns {Promise<StoredToken|null>}
|
|
45
|
+
*/
|
|
46
|
+
export async function loadToken(locationId) {
|
|
47
|
+
const path = tokenPath(locationId);
|
|
48
|
+
if (!existsSync(path)) return null;
|
|
49
|
+
try {
|
|
50
|
+
const raw = await readFile(path, "utf-8");
|
|
51
|
+
const data = JSON.parse(raw);
|
|
52
|
+
return data.oauth || null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save OAuth token for a location.
|
|
60
|
+
* @param {string} locationId
|
|
61
|
+
* @param {Object} tokens - { access_token, refresh_token, expires_in, companyId }
|
|
62
|
+
* @param {string} [clientId]
|
|
63
|
+
* @param {string} [clientSecret]
|
|
64
|
+
*/
|
|
65
|
+
export async function saveToken(locationId, tokens, clientId, clientSecret) {
|
|
66
|
+
await mkdir(CONNECTIONS_DIR, { recursive: true });
|
|
67
|
+
const path = tokenPath(locationId);
|
|
68
|
+
const data = {
|
|
69
|
+
"$0n": {
|
|
70
|
+
type: "connection",
|
|
71
|
+
name: `CRM OAuth — ${locationId}`,
|
|
72
|
+
version: "1.0.0",
|
|
73
|
+
created: new Date().toISOString(),
|
|
74
|
+
updated: new Date().toISOString(),
|
|
75
|
+
},
|
|
76
|
+
service: "crm",
|
|
77
|
+
auth: { type: "oauth" },
|
|
78
|
+
oauth: {
|
|
79
|
+
access_token: tokens.access_token,
|
|
80
|
+
refresh_token: tokens.refresh_token,
|
|
81
|
+
expires_at: Date.now() + (tokens.expires_in || 86400) * 1000,
|
|
82
|
+
location_id: locationId,
|
|
83
|
+
company_id: tokens.companyId || tokens.company_id || null,
|
|
84
|
+
client_id: clientId || DEFAULT_CLIENT_ID,
|
|
85
|
+
client_secret: clientSecret || DEFAULT_CLIENT_SECRET,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
await writeFile(path, JSON.stringify(data, null, 2));
|
|
89
|
+
console.error(`[oauth-store] Saved token for location ${locationId}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Refresh an expired token.
|
|
94
|
+
* @param {StoredToken} stored
|
|
95
|
+
* @returns {Promise<StoredToken>}
|
|
96
|
+
*/
|
|
97
|
+
async function refreshToken(stored) {
|
|
98
|
+
const clientId = stored.client_id || DEFAULT_CLIENT_ID;
|
|
99
|
+
const clientSecret = stored.client_secret || DEFAULT_CLIENT_SECRET;
|
|
100
|
+
|
|
101
|
+
console.error(`[oauth-store] Refreshing token for ${stored.location_id}...`);
|
|
102
|
+
|
|
103
|
+
const res = await fetch(TOKEN_URL, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
106
|
+
body: new URLSearchParams({
|
|
107
|
+
client_id: clientId,
|
|
108
|
+
client_secret: clientSecret,
|
|
109
|
+
grant_type: "refresh_token",
|
|
110
|
+
refresh_token: stored.refresh_token,
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const err = await res.text();
|
|
116
|
+
console.error(`[oauth-store] Refresh failed for ${stored.location_id}:`, err);
|
|
117
|
+
throw new Error(`OAuth refresh failed: ${res.status}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
const updated = {
|
|
122
|
+
access_token: data.access_token,
|
|
123
|
+
refresh_token: data.refresh_token || stored.refresh_token,
|
|
124
|
+
expires_at: Date.now() + (data.expires_in || 86400) * 1000,
|
|
125
|
+
location_id: stored.location_id,
|
|
126
|
+
company_id: data.companyId || stored.company_id,
|
|
127
|
+
client_id: clientId,
|
|
128
|
+
client_secret: clientSecret,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await saveToken(stored.location_id, {
|
|
132
|
+
access_token: updated.access_token,
|
|
133
|
+
refresh_token: updated.refresh_token,
|
|
134
|
+
expires_in: data.expires_in || 86400,
|
|
135
|
+
companyId: updated.company_id,
|
|
136
|
+
}, clientId, clientSecret);
|
|
137
|
+
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a valid access token for a location.
|
|
143
|
+
* Auto-refreshes if expired. Falls back to PIT if no OAuth token.
|
|
144
|
+
* @param {string} locationId
|
|
145
|
+
* @returns {Promise<string>} - Valid access_token
|
|
146
|
+
*/
|
|
147
|
+
export async function resolveToken(locationId) {
|
|
148
|
+
let stored = await loadToken(locationId);
|
|
149
|
+
|
|
150
|
+
if (stored) {
|
|
151
|
+
if (Date.now() >= stored.expires_at - REFRESH_BUFFER_MS) {
|
|
152
|
+
stored = await refreshToken(stored);
|
|
153
|
+
}
|
|
154
|
+
return stored.access_token;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fall back to PIT from crm.0n
|
|
158
|
+
const pitPath = join(CONNECTIONS_DIR, "crm.0n");
|
|
159
|
+
if (existsSync(pitPath)) {
|
|
160
|
+
try {
|
|
161
|
+
const raw = await readFile(pitPath, "utf-8");
|
|
162
|
+
const data = JSON.parse(raw);
|
|
163
|
+
const pit = data.auth?.credentials?.access_token;
|
|
164
|
+
if (pit) {
|
|
165
|
+
console.error(`[oauth-store] No OAuth for ${locationId}, falling back to PIT`);
|
|
166
|
+
return pit;
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new Error(`No OAuth or PIT token available for location ${locationId}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* List all locations with stored OAuth tokens.
|
|
176
|
+
* @returns {Promise<Array<{location_id: string, expires_at: number, expired: boolean}>>}
|
|
177
|
+
*/
|
|
178
|
+
export async function listConnectedLocations() {
|
|
179
|
+
if (!existsSync(CONNECTIONS_DIR)) return [];
|
|
180
|
+
const { readdir } = await import("fs/promises");
|
|
181
|
+
const files = await readdir(CONNECTIONS_DIR);
|
|
182
|
+
const locations = [];
|
|
183
|
+
for (const f of files) {
|
|
184
|
+
if (f.startsWith("crm-oauth-") && f.endsWith(".0n")) {
|
|
185
|
+
try {
|
|
186
|
+
const raw = await readFile(join(CONNECTIONS_DIR, f), "utf-8");
|
|
187
|
+
const data = JSON.parse(raw);
|
|
188
|
+
if (data.oauth) {
|
|
189
|
+
locations.push({
|
|
190
|
+
location_id: data.oauth.location_id,
|
|
191
|
+
company_id: data.oauth.company_id,
|
|
192
|
+
expires_at: data.oauth.expires_at,
|
|
193
|
+
expired: Date.now() >= data.oauth.expires_at,
|
|
194
|
+
file: f,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return locations;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Register OAuth store MCP tools.
|
|
205
|
+
*/
|
|
206
|
+
export function registerOAuthStoreTools(server, z) {
|
|
207
|
+
server.tool(
|
|
208
|
+
"crm_oauth_connect",
|
|
209
|
+
"Store OAuth tokens for a CRM location after OAuth callback. This enables auto-auth for all CRM tools on that location.",
|
|
210
|
+
{
|
|
211
|
+
location_id: z.string().describe("CRM location ID"),
|
|
212
|
+
access_token: z.string().describe("OAuth access token"),
|
|
213
|
+
refresh_token: z.string().describe("OAuth refresh token"),
|
|
214
|
+
expires_in: z.number().optional().describe("Token TTL in seconds (default 86400)"),
|
|
215
|
+
company_id: z.string().optional().describe("Company ID"),
|
|
216
|
+
client_id: z.string().optional().describe("Marketplace app client ID (default: 0nCORE app)"),
|
|
217
|
+
client_secret: z.string().optional().describe("Marketplace app client secret (default: 0nCORE app)"),
|
|
218
|
+
},
|
|
219
|
+
async ({ location_id, access_token, refresh_token, expires_in, company_id, client_id, client_secret }) => {
|
|
220
|
+
await saveToken(location_id, { access_token, refresh_token, expires_in: expires_in || 86400, companyId: company_id }, client_id, client_secret);
|
|
221
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "connected", location_id, expires_in: expires_in || 86400 }) }] };
|
|
222
|
+
}
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
server.tool(
|
|
226
|
+
"crm_oauth_status",
|
|
227
|
+
"Show OAuth connection status for all CRM locations. Shows which have OAuth tokens and which are using PIT fallback.",
|
|
228
|
+
{},
|
|
229
|
+
async () => {
|
|
230
|
+
const locations = await listConnectedLocations();
|
|
231
|
+
return { content: [{ type: "text", text: JSON.stringify({ connected_locations: locations, count: locations.length }, null, 2) }] };
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
server.tool(
|
|
236
|
+
"crm_oauth_resolve",
|
|
237
|
+
"Resolve a working access token for a location. Auto-refreshes if expired, falls back to PIT if no OAuth. Use this to test auth before making API calls.",
|
|
238
|
+
{
|
|
239
|
+
location_id: z.string().describe("CRM location ID"),
|
|
240
|
+
},
|
|
241
|
+
async ({ location_id }) => {
|
|
242
|
+
try {
|
|
243
|
+
const token = await resolveToken(location_id);
|
|
244
|
+
const stored = await loadToken(location_id);
|
|
245
|
+
return {
|
|
246
|
+
content: [{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: JSON.stringify({
|
|
249
|
+
status: "ok",
|
|
250
|
+
location_id,
|
|
251
|
+
auth_type: stored ? "oauth" : "pit",
|
|
252
|
+
token_preview: token.slice(0, 20) + "...",
|
|
253
|
+
expires_at: stored?.expires_at ? new Date(stored.expires_at).toISOString() : null,
|
|
254
|
+
}, null, 2),
|
|
255
|
+
}],
|
|
256
|
+
};
|
|
257
|
+
} catch (err) {
|
|
258
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "error", message: err.message }) }] };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — CRM Phone System API Tool Definitions
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Phone number purchasing, management, and number pools.
|
|
5
|
+
// Scopes: phonenumbers.read, phonenumbers.write, numberpools.read
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
{
|
|
10
|
+
name: "crm_purchase_phone_number",
|
|
11
|
+
description: "Purchase a phone number for a CRM location. Specify country, capabilities (voice/sms), and optionally an area code or contains pattern.",
|
|
12
|
+
method: "POST",
|
|
13
|
+
path: "/phone-number/purchase",
|
|
14
|
+
params: {
|
|
15
|
+
locationId: { type: "string", description: "Location ID", required: true, in: "body" },
|
|
16
|
+
number: { type: "string", description: "Specific phone number to purchase (E.164 format, e.g. +14155551234)", required: false, in: "body" },
|
|
17
|
+
country: { type: "string", description: "Country code (US, CA, GB, etc.)", required: false, in: "body" },
|
|
18
|
+
areaCode: { type: "string", description: "Desired area code (e.g. 412, 724)", required: false, in: "body" },
|
|
19
|
+
contains: { type: "string", description: "Pattern the number should contain", required: false, in: "body" },
|
|
20
|
+
capabilities: { type: "array", description: "Capabilities: ['voice', 'sms', 'mms']", required: false, in: "body" },
|
|
21
|
+
},
|
|
22
|
+
body: ["locationId", "number", "country", "areaCode", "contains", "capabilities"],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
name: "crm_search_phone_numbers",
|
|
27
|
+
description: "Search available phone numbers to purchase by area code, country, or pattern.",
|
|
28
|
+
method: "GET",
|
|
29
|
+
path: "/phone-number/search",
|
|
30
|
+
params: {
|
|
31
|
+
locationId: { type: "string", description: "Location ID", required: true, in: "query" },
|
|
32
|
+
country: { type: "string", description: "Country code (US, CA, GB, etc.)", required: false, in: "query" },
|
|
33
|
+
areaCode: { type: "string", description: "Area code to search (e.g. 412)", required: false, in: "query" },
|
|
34
|
+
contains: { type: "string", description: "Pattern to search for", required: false, in: "query" },
|
|
35
|
+
type: { type: "string", description: "Number type: local, tollfree, mobile", required: false, in: "query" },
|
|
36
|
+
limit: { type: "number", description: "Max results", required: false, in: "query" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
name: "crm_list_active_numbers",
|
|
42
|
+
description: "List all active phone numbers for a location with pagination.",
|
|
43
|
+
method: "GET",
|
|
44
|
+
path: "/phone-number/active",
|
|
45
|
+
params: {
|
|
46
|
+
locationId: { type: "string", description: "Location ID", required: true, in: "query" },
|
|
47
|
+
page: { type: "number", description: "Page number", required: false, in: "query" },
|
|
48
|
+
pageSize: { type: "number", description: "Results per page", required: false, in: "query" },
|
|
49
|
+
searchFilter: { type: "string", description: "Filter by number or label", required: false, in: "query" },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
name: "crm_release_phone_number",
|
|
55
|
+
description: "Release (cancel) a phone number from a location.",
|
|
56
|
+
method: "DELETE",
|
|
57
|
+
path: "/phone-number/:phoneNumberId",
|
|
58
|
+
params: {
|
|
59
|
+
phoneNumberId: { type: "string", description: "Phone number ID to release", required: true, in: "path" },
|
|
60
|
+
locationId: { type: "string", description: "Location ID", required: true, in: "query" },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
name: "crm_update_phone_number",
|
|
66
|
+
description: "Update phone number settings — label, forwarding, voicemail, call recording.",
|
|
67
|
+
method: "PUT",
|
|
68
|
+
path: "/phone-number/:phoneNumberId",
|
|
69
|
+
params: {
|
|
70
|
+
phoneNumberId: { type: "string", description: "Phone number ID", required: true, in: "path" },
|
|
71
|
+
locationId: { type: "string", description: "Location ID", required: true, in: "body" },
|
|
72
|
+
name: { type: "string", description: "Label/name for the number", required: false, in: "body" },
|
|
73
|
+
forwardingNumber: { type: "string", description: "Forward calls to this number", required: false, in: "body" },
|
|
74
|
+
callRecording: { type: "boolean", description: "Enable call recording", required: false, in: "body" },
|
|
75
|
+
},
|
|
76
|
+
body: ["locationId", "name", "forwardingNumber", "callRecording"],
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
name: "crm_list_number_pools",
|
|
81
|
+
description: "List number pools for a location (used for call tracking).",
|
|
82
|
+
method: "GET",
|
|
83
|
+
path: "/phone-number/number-pool",
|
|
84
|
+
params: {
|
|
85
|
+
locationId: { type: "string", description: "Location ID", required: true, in: "query" },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — CRM SaaS Management Tools (SDK-powered)
|
|
3
|
+
// ============================================================
|
|
4
|
+
// SDK-only tools that complement the data-driven saas.js module.
|
|
5
|
+
// These use the SDK directly for methods not in the REST catalog.
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
import getSDK from "./sdk.js";
|
|
9
|
+
|
|
10
|
+
export function registerSaasManagementTools(server, z) {
|
|
11
|
+
|
|
12
|
+
server.tool(
|
|
13
|
+
"crm_saas_generate_payment_link",
|
|
14
|
+
"Generate a payment link for a sub-account to pay for their SaaS subscription.",
|
|
15
|
+
{
|
|
16
|
+
company_id: z.string().describe("Agency company ID"),
|
|
17
|
+
location_id: z.string().describe("Location ID"),
|
|
18
|
+
},
|
|
19
|
+
async ({ company_id, location_id }) => {
|
|
20
|
+
try {
|
|
21
|
+
const sdk = await getSDK();
|
|
22
|
+
const result = await sdk.saasApi.generatePaymentLink({
|
|
23
|
+
companyId: company_id,
|
|
24
|
+
locationId: location_id,
|
|
25
|
+
});
|
|
26
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
server.tool(
|
|
34
|
+
"crm_saas_get_company_info",
|
|
35
|
+
"Get agency company details — name, settings, billing configuration.",
|
|
36
|
+
{
|
|
37
|
+
company_id: z.string().describe("Agency company ID"),
|
|
38
|
+
},
|
|
39
|
+
async ({ company_id }) => {
|
|
40
|
+
try {
|
|
41
|
+
const sdk = await getSDK();
|
|
42
|
+
const result = await sdk.companies.getCompany({ companyId: company_id });
|
|
43
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.tool(
|
|
51
|
+
"crm_saas_update_location_rebilling",
|
|
52
|
+
"Update rebilling/pricing configuration for a specific sub-account location via SDK.",
|
|
53
|
+
{
|
|
54
|
+
company_id: z.string().describe("Agency company ID"),
|
|
55
|
+
location_id: z.string().describe("Location ID"),
|
|
56
|
+
config: z.record(z.any()).describe("Rebilling configuration object"),
|
|
57
|
+
},
|
|
58
|
+
async ({ company_id, location_id, config }) => {
|
|
59
|
+
try {
|
|
60
|
+
const sdk = await getSDK();
|
|
61
|
+
const result = await sdk.saasApi.updateRebilling({
|
|
62
|
+
companyId: company_id,
|
|
63
|
+
locationId: location_id,
|
|
64
|
+
...config,
|
|
65
|
+
});
|
|
66
|
+
return { content: [{ type: "text", text: JSON.stringify({ updated: true, data: result }, null, 2) }] };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
package/crm/sdk.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — HighLevel SDK Singleton
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Initialized once with 0nCORE marketplace app credentials.
|
|
5
|
+
// Supabase-backed session storage for persistent OAuth tokens.
|
|
6
|
+
// Every module available: courses, voiceAi, marketplace, etc.
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
import { SupabaseSessionStorage } from "./supabase-session-storage.js";
|
|
10
|
+
|
|
11
|
+
let _sdk = null;
|
|
12
|
+
let _initPromise = null;
|
|
13
|
+
|
|
14
|
+
const CLIENT_ID = process.env.CRM_MARKETPLACE_CLIENT_ID || "69c762225a31e1cd2f28dd4c-mnu5pazi";
|
|
15
|
+
const CLIENT_SECRET = process.env.CRM_MARKETPLACE_CLIENT_SECRET || "92cb8fc3-2b23-40bf-adce-6b6afe9b8445";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the HighLevel SDK singleton.
|
|
19
|
+
* Lazy-loads and initializes with Supabase session storage.
|
|
20
|
+
*/
|
|
21
|
+
export async function getSDK() {
|
|
22
|
+
if (_sdk) return _sdk;
|
|
23
|
+
if (_initPromise) return _initPromise;
|
|
24
|
+
|
|
25
|
+
_initPromise = (async () => {
|
|
26
|
+
const { HighLevel } = await import("@gohighlevel/api-client");
|
|
27
|
+
|
|
28
|
+
_sdk = new HighLevel({
|
|
29
|
+
clientId: CLIENT_ID,
|
|
30
|
+
clientSecret: CLIENT_SECRET,
|
|
31
|
+
sessionStorage: new SupabaseSessionStorage(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.error("[0nMCP] HighLevel SDK initialized with Supabase session storage");
|
|
35
|
+
return _sdk;
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
return _initPromise;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Store an OAuth token in the SDK's session storage.
|
|
43
|
+
* Call this after OAuth callback to persist tokens.
|
|
44
|
+
*/
|
|
45
|
+
export async function storeOAuthToken(locationId, tokenData) {
|
|
46
|
+
const sdk = await getSDK();
|
|
47
|
+
const key = `location:${locationId}`;
|
|
48
|
+
await sdk.config.sessionStorage.set(key, {
|
|
49
|
+
access_token: tokenData.access_token,
|
|
50
|
+
refresh_token: tokenData.refresh_token,
|
|
51
|
+
expires_at: Date.now() + (tokenData.expires_in || 86400) * 1000,
|
|
52
|
+
location_id: locationId,
|
|
53
|
+
company_id: tokenData.companyId || null,
|
|
54
|
+
token_type: tokenData.token_type || "Bearer",
|
|
55
|
+
scope: tokenData.scope || "",
|
|
56
|
+
});
|
|
57
|
+
console.error(`[0nMCP] OAuth token stored for location ${locationId}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default getSDK;
|