@channlize/mcp 0.1.1 → 0.4.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/CHANGELOG.md +49 -1
- package/README.md +117 -324
- package/dist/index.d.mts +83 -78
- package/dist/index.d.ts +83 -78
- package/dist/index.js +1091 -1599
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1090 -1600
- package/dist/index.mjs.map +1 -1
- package/dist/server.js +1191 -1706
- package/dist/server.js.map +1 -1
- package/package.json +12 -7
package/dist/server.js
CHANGED
|
@@ -1,1851 +1,1336 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/server.ts
|
|
27
|
+
var import_node_util = require("util");
|
|
4
28
|
|
|
5
29
|
// src/index.ts
|
|
6
|
-
var
|
|
7
|
-
var import_stdio = require("@modelcontextprotocol/
|
|
8
|
-
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
30
|
+
var import_server2 = require("@modelcontextprotocol/server");
|
|
31
|
+
var import_stdio = require("@modelcontextprotocol/server/stdio");
|
|
9
32
|
|
|
10
|
-
// src/
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
// src/api.ts
|
|
34
|
+
var import_core = require("@channlize/core");
|
|
35
|
+
var ToolError = class extends Error {
|
|
36
|
+
constructor(message, statusCode, code) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.statusCode = statusCode;
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.name = "ToolError";
|
|
41
|
+
}
|
|
42
|
+
statusCode;
|
|
43
|
+
code;
|
|
44
|
+
};
|
|
45
|
+
function hintFor(status, apiMessage) {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case 401:
|
|
48
|
+
return `${apiMessage}. The server API key is invalid, revoked or expired: create a new one under SDK & Integration \u2192 API keys and update CHANNLIZE_SERVER_API_KEY.`;
|
|
49
|
+
case 403:
|
|
50
|
+
return /permission/i.test(apiMessage) ? `${apiMessage}. This needs a server API key with the ${/write|notify/.test(apiMessage) ? "write" : "read"} scope: create one under SDK & Integration \u2192 API keys.` : apiMessage;
|
|
51
|
+
case 404:
|
|
52
|
+
return `${apiMessage}. Check the ID (and CHANNLIZE_PROJECT_ID): it must belong to the key's organization.`;
|
|
53
|
+
case 429:
|
|
54
|
+
return `${apiMessage}. Rate limited: wait a little before retrying.`;
|
|
55
|
+
default:
|
|
56
|
+
return apiMessage;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function toToolError(err) {
|
|
60
|
+
if (err instanceof ToolError) return err;
|
|
61
|
+
if (err instanceof import_core.ChannlizeError) {
|
|
62
|
+
return new ToolError(
|
|
63
|
+
err.statusCode ? hintFor(err.statusCode, err.message.replace(/\.$/, "")) : err.message,
|
|
64
|
+
err.statusCode,
|
|
65
|
+
err.code
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return new ToolError(err instanceof Error ? err.message : String(err));
|
|
69
|
+
}
|
|
70
|
+
var ChannlizeApi = class {
|
|
71
|
+
constructor(config) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
}
|
|
74
|
+
config;
|
|
75
|
+
projectIdPromise;
|
|
76
|
+
widgetClient;
|
|
77
|
+
get hasServerKey() {
|
|
78
|
+
return !!this.config.serverApiKey;
|
|
79
|
+
}
|
|
80
|
+
get hasWidgetKey() {
|
|
81
|
+
return !!this.config.widgetKey;
|
|
82
|
+
}
|
|
83
|
+
requireServerKey() {
|
|
84
|
+
if (!this.config.serverApiKey) {
|
|
85
|
+
throw new ToolError(
|
|
86
|
+
"This tool needs a server API key (ch_live_\u2026): set CHANNLIZE_SERVER_API_KEY or pass --server-api-key."
|
|
87
|
+
);
|
|
48
88
|
}
|
|
89
|
+
return this.config.serverApiKey;
|
|
49
90
|
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"properties": {
|
|
56
|
-
"id": {
|
|
57
|
-
"type": "string",
|
|
58
|
-
"description": "Unique session identifier"
|
|
59
|
-
},
|
|
60
|
-
"status": {
|
|
61
|
-
"type": "string",
|
|
62
|
-
"enum": ["active", "closed", "waiting"],
|
|
63
|
-
"description": "Current session status"
|
|
64
|
-
},
|
|
65
|
-
"messages": {
|
|
66
|
-
"type": "array",
|
|
67
|
-
"items": {
|
|
68
|
-
"$ref": "#/definitions/ChatMessage"
|
|
69
|
-
},
|
|
70
|
-
"description": "Array of messages in this session"
|
|
71
|
-
},
|
|
72
|
-
"metadata": {
|
|
73
|
-
"type": "object",
|
|
74
|
-
"description": "Custom metadata for the session"
|
|
75
|
-
},
|
|
76
|
-
"createdAt": {
|
|
77
|
-
"type": "string",
|
|
78
|
-
"format": "date-time",
|
|
79
|
-
"description": "Session creation timestamp"
|
|
80
|
-
},
|
|
81
|
-
"updatedAt": {
|
|
82
|
-
"type": "string",
|
|
83
|
-
"format": "date-time",
|
|
84
|
-
"description": "Last update timestamp"
|
|
91
|
+
async request(method, path, options = {}) {
|
|
92
|
+
const key = this.requireServerKey();
|
|
93
|
+
const url2 = new URL(`${this.config.baseUrl}${path}`);
|
|
94
|
+
for (const [k, v] of Object.entries(options.query ?? {})) {
|
|
95
|
+
if (v !== void 0 && v !== "") url2.searchParams.set(k, String(v));
|
|
85
96
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"authorData": {
|
|
96
|
-
"type": "object",
|
|
97
|
-
"properties": {
|
|
98
|
-
"name": { "type": "string" },
|
|
99
|
-
"email": { "type": "string" },
|
|
100
|
-
"avatarUrl": { "type": "string" }
|
|
101
|
-
}
|
|
97
|
+
let res;
|
|
98
|
+
try {
|
|
99
|
+
res = await fetch(url2, {
|
|
100
|
+
method,
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${key}`,
|
|
103
|
+
Accept: "application/json",
|
|
104
|
+
...options.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
105
|
+
...options.headers
|
|
102
106
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
// oxlint-disable-next-line unicorn/no-invalid-fetch-options -- `method` is dynamic; GET calls never pass a body.
|
|
108
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
109
|
+
signal: AbortSignal.timeout(this.config.timeout)
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const e = err;
|
|
113
|
+
if (e.name === "TimeoutError" || e.name === "AbortError") {
|
|
114
|
+
throw new ToolError(
|
|
115
|
+
`Channlize API timed out after ${this.config.timeout} ms (${method} ${url2.pathname}). Retry, or raise CHANNLIZE_TIMEOUT.`,
|
|
116
|
+
408,
|
|
117
|
+
"TIMEOUT"
|
|
118
|
+
);
|
|
107
119
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
throw new ToolError(
|
|
121
|
+
`Could not reach the Channlize API at ${this.config.baseUrl}: ${e.message}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
let data = void 0;
|
|
126
|
+
if (text) {
|
|
127
|
+
try {
|
|
128
|
+
data = JSON.parse(text);
|
|
129
|
+
} catch {
|
|
130
|
+
data = void 0;
|
|
117
131
|
}
|
|
118
132
|
}
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
const { message, code } = (0, import_core.parseApiError)(res.status, data, res.statusText);
|
|
135
|
+
throw new ToolError(
|
|
136
|
+
hintFor(res.status, message.replace(/\.$/, "")),
|
|
137
|
+
res.status,
|
|
138
|
+
code
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (data === void 0) {
|
|
142
|
+
throw new ToolError(
|
|
143
|
+
`Unexpected non-JSON response from ${method} ${url2.pathname}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return data;
|
|
119
147
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
148
|
+
/**
|
|
149
|
+
* `CHANNLIZE_PROJECT_ID`, or the first project of the key's organization
|
|
150
|
+
* (`GET /projects/me`), looked up once.
|
|
151
|
+
*/
|
|
152
|
+
projectId() {
|
|
153
|
+
if (this.config.projectId) return Promise.resolve(this.config.projectId);
|
|
154
|
+
this.projectIdPromise ??= this.request(
|
|
155
|
+
"GET",
|
|
156
|
+
"/projects/me"
|
|
157
|
+
).then((r) => {
|
|
158
|
+
if (!r.project?.id) {
|
|
159
|
+
throw new ToolError(
|
|
160
|
+
"Could not find a project for this key: set CHANNLIZE_PROJECT_ID."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return r.project.id;
|
|
164
|
+
}).catch((err) => {
|
|
165
|
+
this.projectIdPromise = void 0;
|
|
166
|
+
throw err;
|
|
167
|
+
});
|
|
168
|
+
return this.projectIdPromise;
|
|
169
|
+
}
|
|
170
|
+
/** `@channlize/core` client bound to the widget key (visitor tools). */
|
|
171
|
+
widget() {
|
|
172
|
+
if (!this.config.widgetKey) {
|
|
173
|
+
throw new ToolError(
|
|
174
|
+
"This tool needs the public widget key (wk_\u2026): set CHANNLIZE_WIDGET_KEY or pass --widget-key."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
this.widgetClient ??= new import_core.ChannlizeClient({
|
|
178
|
+
apiKey: this.config.widgetKey,
|
|
179
|
+
projectId: this.config.projectId ?? "",
|
|
180
|
+
baseUrl: this.config.baseUrl,
|
|
181
|
+
timeout: this.config.timeout
|
|
182
|
+
});
|
|
183
|
+
return this.widgetClient;
|
|
184
|
+
}
|
|
185
|
+
/** `@channlize/core` client for `notify` (server key + project). */
|
|
186
|
+
async notifier() {
|
|
187
|
+
const serverApiKey = this.requireServerKey();
|
|
188
|
+
return new import_core.ChannlizeClient({
|
|
189
|
+
// core requires a widget key; notify only uses serverApiKey.
|
|
190
|
+
apiKey: this.config.widgetKey ?? "unused",
|
|
191
|
+
serverApiKey,
|
|
192
|
+
projectId: await this.projectId(),
|
|
193
|
+
baseUrl: this.config.baseUrl,
|
|
194
|
+
timeout: this.config.timeout
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
164
198
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"customField": "value"
|
|
199
|
+
// src/config.ts
|
|
200
|
+
var import_core2 = require("@channlize/core");
|
|
201
|
+
var ConfigError = class extends Error {
|
|
202
|
+
constructor(message) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = "ConfigError";
|
|
172
205
|
}
|
|
206
|
+
};
|
|
207
|
+
function clean(value) {
|
|
208
|
+
const v = value?.trim();
|
|
209
|
+
return v ? v : void 0;
|
|
173
210
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
function resolveConfig(config) {
|
|
212
|
+
let serverApiKey = clean(config.serverApiKey);
|
|
213
|
+
let widgetKey = clean(config.widgetKey);
|
|
214
|
+
const apiKey = clean(config.apiKey);
|
|
215
|
+
if (serverApiKey && (0, import_core2.keyKind)(serverApiKey) === "widget") {
|
|
216
|
+
throw new ConfigError(
|
|
217
|
+
"CHANNLIZE_SERVER_API_KEY / --server-api-key holds a widget key (wk_\u2026). Widget keys are public and only work for the visitor tools: pass it as CHANNLIZE_WIDGET_KEY / --widget-key, and create a server API key (ch_live_\u2026) under SDK & Integration \u2192 API keys in the Channlize dashboard."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (widgetKey && (0, import_core2.keyKind)(widgetKey) === "server") {
|
|
221
|
+
throw new ConfigError(
|
|
222
|
+
"CHANNLIZE_WIDGET_KEY / --widget-key holds a server API key. Pass server keys (ch_live_\u2026) as CHANNLIZE_SERVER_API_KEY / --server-api-key; widget keys start with wk_."
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (apiKey) {
|
|
226
|
+
if ((0, import_core2.keyKind)(apiKey) === "widget") widgetKey ??= apiKey;
|
|
227
|
+
else serverApiKey ??= apiKey;
|
|
228
|
+
}
|
|
229
|
+
if (!serverApiKey && !widgetKey) {
|
|
230
|
+
throw new ConfigError(
|
|
231
|
+
"A Channlize key is required. Set CHANNLIZE_SERVER_API_KEY (or --server-api-key) to a server API key (ch_live_\u2026) from SDK & Integration \u2192 API keys in the Channlize dashboard. Use a read-scoped key for read-only access, or add the write scope to send events, logs and conversations."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const timeout = config.timeout ?? import_core2.DEFAULT_TIMEOUT;
|
|
235
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
236
|
+
throw new ConfigError(
|
|
237
|
+
`Invalid timeout "${config.timeout}": use milliseconds, e.g. 10000`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const baseUrl = (clean(config.baseUrl) ?? import_core2.DEFAULT_BASE_URL).replace(
|
|
241
|
+
/\/+$/,
|
|
242
|
+
""
|
|
243
|
+
);
|
|
244
|
+
try {
|
|
245
|
+
new URL(baseUrl);
|
|
246
|
+
} catch {
|
|
247
|
+
throw new ConfigError(
|
|
248
|
+
`Invalid base URL "${config.baseUrl}": use e.g. ${import_core2.DEFAULT_BASE_URL}`
|
|
249
|
+
);
|
|
187
250
|
}
|
|
251
|
+
return {
|
|
252
|
+
serverApiKey,
|
|
253
|
+
widgetKey,
|
|
254
|
+
projectId: clean(config.projectId),
|
|
255
|
+
baseUrl,
|
|
256
|
+
timeout
|
|
257
|
+
};
|
|
188
258
|
}
|
|
189
|
-
|
|
259
|
+
function configFromEnv(env = process.env) {
|
|
260
|
+
const { timeout, ...rest } = (0, import_core2.readChannlizeEnv)(env);
|
|
261
|
+
return { ...rest, timeout: timeout ? Number(timeout) : void 0 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/prompts.ts
|
|
265
|
+
var z = __toESM(require("zod/v4"));
|
|
266
|
+
|
|
267
|
+
// src/docs.ts
|
|
268
|
+
var QUICK_START = `# Channlize quick start
|
|
190
269
|
|
|
191
|
-
|
|
192
|
-
Send a message in a chat session.
|
|
270
|
+
Channlize routes website conversations and app events to Slack.
|
|
193
271
|
|
|
194
|
-
|
|
272
|
+
## Keys
|
|
273
|
+
- **Widget key** (\`wk_\u2026\`): public, safe in browser code. Used by the chat widget,
|
|
274
|
+
contact form and browser log tracking (\`apiKey\` in the SDK config).
|
|
275
|
+
- **Server API key** (\`ch_live_\u2026\`): secret, server only (\`serverApiKey\`). Scopes:
|
|
276
|
+
\`read\` (conversations, contacts, logs, Events) and \`write\` (notify, server logs,
|
|
277
|
+
create conversations/contacts). Create both under SDK & Integration \u2192 API keys.
|
|
278
|
+
- **Project ID**: shown next to each widget key.
|
|
195
279
|
|
|
196
|
-
|
|
197
|
-
|
|
280
|
+
## Install
|
|
281
|
+
\`\`\`bash
|
|
282
|
+
npm install @channlize/core # any JS runtime
|
|
283
|
+
npm install @channlize/react # React components and hooks
|
|
284
|
+
\`\`\`
|
|
198
285
|
|
|
199
|
-
|
|
200
|
-
|
|
286
|
+
## Browser
|
|
287
|
+
\`\`\`ts
|
|
288
|
+
import { ChannlizeClient } from '@channlize/core';
|
|
201
289
|
|
|
202
|
-
|
|
290
|
+
const client = new ChannlizeClient({ apiKey: 'wk_\u2026', projectId: '<project-id>' });
|
|
291
|
+
await client.submitContactForm({ name: 'Ada', email: 'ada@example.com', message: 'Hi!' });
|
|
292
|
+
await client.trackEvent('checkout.started', { properties: { cartId: 'c_1' } });
|
|
293
|
+
\`\`\`
|
|
203
294
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
- 429: Rate limit exceeded
|
|
208
|
-
- 500: Server error
|
|
295
|
+
## Server
|
|
296
|
+
\`\`\`ts
|
|
297
|
+
import { createNotifier } from '@channlize/core';
|
|
209
298
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
"error": "Error message",
|
|
214
|
-
"code": "ERROR_CODE",
|
|
215
|
-
"field": "fieldName" // for validation errors
|
|
216
|
-
}
|
|
299
|
+
// Reads CHANNLIZE_SERVER_API_KEY (or CHANNLIZE_API_KEY) and CHANNLIZE_PROJECT_ID.
|
|
300
|
+
const notify = createNotifier();
|
|
301
|
+
await notify('user.subscribed', { username: 'ada', plan: 'Pro' });
|
|
217
302
|
\`\`\`
|
|
218
|
-
`;
|
|
219
|
-
var QUICK_START = `# Channlize Quick Start
|
|
220
303
|
|
|
221
|
-
|
|
304
|
+
Errors are \`ChannlizeError\` (\`statusCode\`, \`code\`), with subclasses
|
|
305
|
+
\`ValidationError\` (400), \`AuthenticationError\` (401) and \`RateLimitError\` (429).
|
|
306
|
+
`;
|
|
307
|
+
var REACT_COMPONENTS = `# @channlize/react
|
|
222
308
|
|
|
223
309
|
\`\`\`bash
|
|
224
|
-
# For React applications
|
|
225
310
|
npm install @channlize/react
|
|
226
|
-
|
|
227
|
-
# For non-React applications
|
|
228
|
-
npm install @channlize/core
|
|
229
311
|
\`\`\`
|
|
230
312
|
|
|
231
|
-
##
|
|
232
|
-
|
|
233
|
-
1. Wrap your app with ChannlizeProvider:
|
|
313
|
+
## Floating chat widget (all-in-one)
|
|
314
|
+
\`ChannlizeChat\` includes the provider, launcher button and chat window.
|
|
234
315
|
|
|
235
316
|
\`\`\`tsx
|
|
236
|
-
|
|
317
|
+
'use client';
|
|
318
|
+
import { ChannlizeChat } from '@channlize/react';
|
|
237
319
|
|
|
238
|
-
function
|
|
320
|
+
export function SupportWidget() {
|
|
239
321
|
return (
|
|
240
|
-
<
|
|
322
|
+
<ChannlizeChat
|
|
241
323
|
config={{
|
|
242
|
-
apiKey:
|
|
243
|
-
projectId:
|
|
324
|
+
apiKey: process.env.NEXT_PUBLIC_CHANNLIZE_WIDGET_KEY!, // wk_\u2026, public
|
|
325
|
+
projectId: process.env.NEXT_PUBLIC_CHANNLIZE_PROJECT_ID!,
|
|
244
326
|
}}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
\`\`\`
|
|
251
|
-
|
|
252
|
-
2. Use the Form component:
|
|
253
|
-
|
|
254
|
-
\`\`\`tsx
|
|
255
|
-
import { Form } from '@channlize/react';
|
|
256
|
-
|
|
257
|
-
function ContactPage() {
|
|
258
|
-
return (
|
|
259
|
-
<Form
|
|
260
|
-
onSuccess={() => console.log('Form submitted!')}
|
|
261
|
-
submitText="Send Message"
|
|
327
|
+
position="bottom-right"
|
|
328
|
+
primaryColor="#2563eb"
|
|
329
|
+
theme="auto"
|
|
330
|
+
chatProps={{ title: 'Acme Support', greeting: 'Hi there!', welcomeMessage: 'Ask us anything.' }}
|
|
262
331
|
/>
|
|
263
332
|
);
|
|
264
333
|
}
|
|
265
334
|
\`\`\`
|
|
266
335
|
|
|
267
|
-
|
|
336
|
+
Key props: \`position\` ('bottom-right' | 'bottom-left' | 'top-right' | 'top-left'),
|
|
337
|
+
\`primaryColor\`, \`theme\` ('auto' | 'light' | 'dark'), \`defaultOpen\`, \`showOnMobile\`,
|
|
338
|
+
\`offset\`, \`zIndex\`, \`onOpen\`, \`onClose\`, and \`chatProps\` (SupportChat props).
|
|
268
339
|
|
|
340
|
+
## Provider + inline components
|
|
269
341
|
\`\`\`tsx
|
|
270
|
-
import {
|
|
342
|
+
import { ChannlizeProvider, SupportChat, ContactForm } from '@channlize/react';
|
|
271
343
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
title="Customer Support"
|
|
277
|
-
placeholder="How can we help?"
|
|
278
|
-
/>
|
|
279
|
-
);
|
|
280
|
-
}
|
|
344
|
+
<ChannlizeProvider config={{ apiKey: 'wk_\u2026', projectId: '<project-id>' }}>
|
|
345
|
+
<SupportChat title="Need help?" greeting="Hi!" suggestions={['Pricing', 'Billing']} />
|
|
346
|
+
<ContactForm submitText="Send" successMessage="Thanks! We'll reply by email." />
|
|
347
|
+
</ChannlizeProvider>
|
|
281
348
|
\`\`\`
|
|
282
349
|
|
|
283
|
-
|
|
350
|
+
SupportChat props include \`title\`, \`greeting\`, \`welcomeMessage\`, \`suggestions\`,
|
|
351
|
+
\`placeholder\`, \`allowAttachments\`, \`allowEndChat\`, \`theme\`, \`primaryColor\`,
|
|
352
|
+
\`locale\`, \`maxLength\`, \`labels\` and the \`useChatSession\` options
|
|
353
|
+
(\`autoConnect\`, \`metadata\`, \`persistSession\`, \`onMessage\`, \`onError\`).
|
|
354
|
+
|
|
355
|
+
ContactForm props: \`customFields\`, \`onSuccess\`, \`onError\`, \`className\`, \`styles\`,
|
|
356
|
+
\`submitText\`, \`successMessage\`.
|
|
357
|
+
|
|
358
|
+
## Hooks (inside ChannlizeProvider)
|
|
359
|
+
- \`useChatSession(options)\` \u2192 \`{ session, messages, isConnecting, isConnected, error,
|
|
360
|
+
sendMessage, startSession, endSession, reconnect }\`
|
|
361
|
+
- \`useContactForm({ onSuccess, onError })\` \u2192 \`{ submit, isSubmitting, isSuccess, error, reset }\`
|
|
362
|
+
- \`useChannlizeAnalytics()\` \u2192 \`{ track(name, { properties, severity }) }\` (log tracking)
|
|
363
|
+
- \`useChannlize()\` \u2192 \`{ client, config }\`
|
|
364
|
+
|
|
365
|
+
## Next.js App Router
|
|
366
|
+
Components use state and effects: render them from a \`'use client'\` file. The widget
|
|
367
|
+
key is public (\`NEXT_PUBLIC_\u2026\` is fine); never put the server API key in a
|
|
368
|
+
\`NEXT_PUBLIC_\` variable.
|
|
369
|
+
`;
|
|
370
|
+
var EVENTS = `# Events: notify Slack from code
|
|
284
371
|
|
|
285
|
-
|
|
372
|
+
Each event key (e.g. \`user.subscribed\`) has a Slack channel and a message template
|
|
373
|
+
with \`{{field}}\` placeholders, set under Events in the dashboard. A key that isn't set
|
|
374
|
+
up yet is saved as a disabled draft the first time it is sent (\`status: 'skipped'\`),
|
|
375
|
+
so you can ship code first and configure it after. Server only: needs a server API key
|
|
376
|
+
with the \`write\` scope.
|
|
286
377
|
|
|
287
|
-
\`\`\`
|
|
288
|
-
import {
|
|
378
|
+
\`\`\`ts
|
|
379
|
+
import { createNotifier } from '@channlize/core';
|
|
289
380
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
381
|
+
type AppEvents = {
|
|
382
|
+
'user.subscribed': { username: string; plan: string };
|
|
383
|
+
'invoice.paid': { amount: string; customer: string };
|
|
384
|
+
};
|
|
294
385
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
name: 'John Doe',
|
|
298
|
-
email: 'john@example.com',
|
|
299
|
-
message: 'I need help!',
|
|
300
|
-
});
|
|
386
|
+
// Env: CHANNLIZE_SERVER_API_KEY (or CHANNLIZE_API_KEY), CHANNLIZE_PROJECT_ID.
|
|
387
|
+
const notify = createNotifier<AppEvents>();
|
|
301
388
|
|
|
302
|
-
|
|
303
|
-
|
|
389
|
+
const res = await notify('user.subscribed', { username: 'ada', plan: 'Pro' });
|
|
390
|
+
// res.status: 'queued' | 'skipped' (see res.reason); res.duplicate on a repeated key
|
|
304
391
|
|
|
305
|
-
//
|
|
306
|
-
await
|
|
392
|
+
// Retry-safe: an idempotency key already used posts nothing.
|
|
393
|
+
await notify('invoice.paid', { amount: '$49', customer: 'ada@example.com' }, { idempotencyKey: 'inv_123' });
|
|
307
394
|
\`\`\`
|
|
308
395
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
## ChannlizeProvider
|
|
319
|
-
|
|
320
|
-
Wrap your app to provide Channlize configuration to all components.
|
|
396
|
+
To type keys app-wide instead, augment the interface:
|
|
397
|
+
\`\`\`ts
|
|
398
|
+
declare module '@channlize/core' {
|
|
399
|
+
interface ChannlizeEvents {
|
|
400
|
+
'user.subscribed': { username: string; plan: string };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
\`\`\`
|
|
321
404
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
405
|
+
Over HTTP:
|
|
406
|
+
\`\`\`bash
|
|
407
|
+
curl -X POST https://api.channlize.com/v1/notify \\
|
|
408
|
+
-H "Authorization: Bearer $CHANNLIZE_SERVER_API_KEY" \\
|
|
409
|
+
-H "Content-Type: application/json" \\
|
|
410
|
+
-H "Idempotency-Key: signup-42" \\
|
|
411
|
+
-d '{"projectId":"<project-id>","event":"user.subscribed","data":{"username":"ada"}}'
|
|
326
412
|
\`\`\`
|
|
327
413
|
|
|
328
|
-
|
|
414
|
+
Log tracking is separate: \`client.trackEvent\` (browser) / \`trackServerEvent\` (server)
|
|
415
|
+
record log lines shown under Log tracking.
|
|
416
|
+
`;
|
|
329
417
|
|
|
330
|
-
|
|
418
|
+
// src/prompts.ts
|
|
419
|
+
function user(text) {
|
|
420
|
+
return {
|
|
421
|
+
messages: [
|
|
422
|
+
{ role: "user", content: { type: "text", text } }
|
|
423
|
+
]
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function registerServerPrompts(server) {
|
|
427
|
+
server.registerPrompt(
|
|
428
|
+
"triage-conversations",
|
|
429
|
+
{
|
|
430
|
+
title: "Triage open conversations",
|
|
431
|
+
description: "Review open support conversations, rank them by urgency and draft a reply for each.",
|
|
432
|
+
argsSchema: z.object({
|
|
433
|
+
status: z.string().optional().describe(
|
|
434
|
+
"Status to triage: open (default), pending, resolved or closed"
|
|
435
|
+
),
|
|
436
|
+
limit: z.string().optional().describe("How many conversations (default 10)")
|
|
437
|
+
})
|
|
438
|
+
},
|
|
439
|
+
({ status, limit }) => user(`Triage my Channlize support inbox.
|
|
331
440
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
- \`className?: string\` - CSS class name
|
|
337
|
-
- \`styles?: FormStyles\` - Custom styles object
|
|
338
|
-
- \`submitText?: string\` - Submit button text
|
|
339
|
-
- \`successMessage?: string\` - Success message text
|
|
441
|
+
1. Call list_conversations with status "${status || "open"}" and limit ${Number(limit) || 10}.
|
|
442
|
+
2. For each conversation, call get_conversation to read the thread.
|
|
443
|
+
3. Return a table sorted by urgency: subject, customer, how long it has waited, current priority, a suggested priority (low / normal / high / urgent) and a one-line reason.
|
|
444
|
+
4. Under the table, draft a short, friendly reply for each conversation that needs one, and flag anything that looks like a bug, an outage or a billing problem.
|
|
340
445
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
446
|
+
Replies and status or priority changes are made by the team in the Slack thread or the Channlize dashboard, so present the drafts for me to send; don't invent facts about my product.`)
|
|
447
|
+
);
|
|
448
|
+
server.registerPrompt(
|
|
449
|
+
"summarize-conversation",
|
|
345
450
|
{
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
451
|
+
title: "Summarize a conversation",
|
|
452
|
+
description: "Summarize one conversation and suggest the next step.",
|
|
453
|
+
argsSchema: z.object({
|
|
454
|
+
conversationId: z.string().describe("Conversation ID")
|
|
455
|
+
})
|
|
351
456
|
},
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
457
|
+
({ conversationId }) => user(
|
|
458
|
+
`Call get_conversation with conversationId "${conversationId}". Summarize it in five bullet points or fewer: what the customer wants, what has been answered, what is still open, and the customer's mood. Then suggest the next step and draft a reply if one is needed.`
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
server.registerPrompt(
|
|
462
|
+
"check-events",
|
|
463
|
+
{
|
|
464
|
+
title: "Check Events delivery",
|
|
465
|
+
description: "Check that Events reach Slack: list them and explain failed or skipped deliveries.",
|
|
466
|
+
argsSchema: z.object({})
|
|
467
|
+
},
|
|
468
|
+
() => user(`Check my Channlize Events setup.
|
|
359
469
|
|
|
360
|
-
|
|
470
|
+
1. Call list_event_definitions and list the Events, flagging disabled drafts and Events without a Slack channel.
|
|
471
|
+
2. Call list_event_deliveries (limit 50) and group the deliveries by status.
|
|
472
|
+
3. For each failed or skipped delivery, explain the reason and how to fix it (enable the Event, pick a channel under Events in the dashboard, invite the Slack app to the channel, or connect Slack).`)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
function registerGuidePrompts(server) {
|
|
476
|
+
server.registerPrompt(
|
|
477
|
+
"setup-events",
|
|
478
|
+
{
|
|
479
|
+
title: "Add Events to my app",
|
|
480
|
+
description: "Add Channlize Events (notify Slack from server code) to the current codebase.",
|
|
481
|
+
argsSchema: z.object({
|
|
482
|
+
events: z.string().optional().describe('Events to add, e.g. "signup, subscription upgrade"')
|
|
483
|
+
})
|
|
484
|
+
},
|
|
485
|
+
({ events }) => user(`Add Channlize Events to this codebase${events ? ` for: ${events}` : ""}.
|
|
361
486
|
|
|
362
|
-
|
|
487
|
+
Find where these things happen on the server, then call notify there with a typed event map and an idempotency key derived from the record ID. Never call it from browser code. Read the server key from CHANNLIZE_SERVER_API_KEY and the project from CHANNLIZE_PROJECT_ID, and add both to the env example file. Don't let a notify failure break the request: catch and log it.
|
|
363
488
|
|
|
364
|
-
|
|
365
|
-
- \`autoStart?: boolean\` - Auto-start chat session
|
|
366
|
-
- \`metadata?: Record<string, unknown>\` - Session metadata
|
|
367
|
-
- \`onMessage?: (message: ChatMessage) => void\` - New message callback
|
|
368
|
-
- \`onError?: (error: Error) => void\` - Error callback
|
|
369
|
-
- \`className?: string\` - CSS class name
|
|
370
|
-
- \`styles?: ChatStyles\` - Custom styles object
|
|
371
|
-
- \`title?: string\` - Chat header title
|
|
372
|
-
- \`placeholder?: string\` - Input placeholder text
|
|
373
|
-
- \`maxHeight?: string\` - Maximum container height
|
|
489
|
+
Reference:
|
|
374
490
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
\`\`\`
|
|
491
|
+
${EVENTS}`)
|
|
492
|
+
);
|
|
493
|
+
server.registerPrompt(
|
|
494
|
+
"setup-live-chat",
|
|
495
|
+
{
|
|
496
|
+
title: "Add the chat widget",
|
|
497
|
+
description: "Add the Channlize live chat widget to a React or Next.js app.",
|
|
498
|
+
argsSchema: z.object({
|
|
499
|
+
framework: z.string().optional().describe("e.g. Next.js App Router, Vite, Remix")
|
|
500
|
+
})
|
|
501
|
+
},
|
|
502
|
+
({ framework }) => user(`Add the Channlize chat widget to this ${framework || "React"} app: install @channlize/react, render ChannlizeChat once in the root layout (from a 'use client' component on Next.js), read the widget key and project ID from public env variables, and match primaryColor to the app's brand color.
|
|
388
503
|
|
|
389
|
-
|
|
504
|
+
Reference:
|
|
390
505
|
|
|
391
|
-
|
|
392
|
-
|
|
506
|
+
${REACT_COMPONENTS}`)
|
|
507
|
+
);
|
|
508
|
+
server.registerPrompt(
|
|
509
|
+
"integrate-contact-form",
|
|
510
|
+
{
|
|
511
|
+
title: "Add a contact form",
|
|
512
|
+
description: "Add a Channlize contact form page to a React app.",
|
|
513
|
+
argsSchema: z.object({
|
|
514
|
+
styling: z.string().optional().describe("Styling approach, e.g. Tailwind or CSS modules")
|
|
515
|
+
})
|
|
516
|
+
},
|
|
517
|
+
({ styling }) => user(`Add a contact page to this app using Channlize. Use the ContactForm component (or useContactForm with a custom form${styling ? ` styled with ${styling}` : ""}) inside ChannlizeProvider, show a clear success state, and surface errors from onError.
|
|
393
518
|
|
|
394
|
-
|
|
395
|
-
const { submit, isSubmitting, isSuccess, error, reset } = useContactForm({
|
|
396
|
-
onSuccess: (response) => console.log('Submitted:', response),
|
|
397
|
-
onError: (error) => console.error('Failed:', error),
|
|
398
|
-
});
|
|
399
|
-
\`\`\`
|
|
519
|
+
Reference:
|
|
400
520
|
|
|
401
|
-
|
|
402
|
-
|
|
521
|
+
${REACT_COMPONENTS}`)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
403
524
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
messages,
|
|
408
|
-
isConnecting,
|
|
409
|
-
isConnected,
|
|
410
|
-
error,
|
|
411
|
-
sendMessage,
|
|
412
|
-
startSession,
|
|
413
|
-
endSession,
|
|
414
|
-
} = useChatSession({
|
|
415
|
-
autoConnect: false,
|
|
416
|
-
metadata: { page: 'support' },
|
|
417
|
-
onMessage: (message) => console.log('New message:', message),
|
|
525
|
+
// src/resources.ts
|
|
526
|
+
var markdown = (uri, text) => ({
|
|
527
|
+
contents: [{ uri, mimeType: "text/markdown", text }]
|
|
418
528
|
});
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
uri
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
529
|
+
function registerResources(server, api) {
|
|
530
|
+
server.registerResource(
|
|
531
|
+
"quick-start",
|
|
532
|
+
"channlize://docs/quick-start",
|
|
533
|
+
{
|
|
534
|
+
title: "Channlize quick start",
|
|
535
|
+
description: "Keys, install, and first calls from the browser and the server.",
|
|
536
|
+
mimeType: "text/markdown"
|
|
537
|
+
},
|
|
538
|
+
(uri) => markdown(uri.href, QUICK_START)
|
|
539
|
+
);
|
|
540
|
+
server.registerResource(
|
|
541
|
+
"react-components",
|
|
542
|
+
"channlize://docs/react-components",
|
|
543
|
+
{
|
|
544
|
+
title: "@channlize/react components and hooks",
|
|
545
|
+
description: "ChannlizeChat, SupportChat, ContactForm, hooks and Next.js notes.",
|
|
546
|
+
mimeType: "text/markdown"
|
|
547
|
+
},
|
|
548
|
+
(uri) => markdown(uri.href, REACT_COMPONENTS)
|
|
549
|
+
);
|
|
550
|
+
server.registerResource(
|
|
551
|
+
"events",
|
|
552
|
+
"channlize://docs/events",
|
|
553
|
+
{
|
|
554
|
+
title: "Events: notify Slack from code",
|
|
555
|
+
description: "createNotifier / notify with typed events, idempotency and curl.",
|
|
556
|
+
mimeType: "text/markdown"
|
|
557
|
+
},
|
|
558
|
+
(uri) => markdown(uri.href, EVENTS)
|
|
559
|
+
);
|
|
560
|
+
if (api.hasServerKey) {
|
|
561
|
+
server.registerResource(
|
|
562
|
+
"project",
|
|
563
|
+
"channlize://project",
|
|
564
|
+
{
|
|
565
|
+
title: "Current Channlize project",
|
|
566
|
+
description: "The key's project: ID, name, totals, permissions and Slack connection.",
|
|
567
|
+
mimeType: "application/json"
|
|
568
|
+
},
|
|
569
|
+
async (uri) => {
|
|
570
|
+
const r = await api.request(
|
|
571
|
+
"GET",
|
|
572
|
+
"/projects/me"
|
|
573
|
+
);
|
|
574
|
+
return {
|
|
575
|
+
contents: [
|
|
576
|
+
{
|
|
577
|
+
uri: uri.href,
|
|
578
|
+
mimeType: "application/json",
|
|
579
|
+
text: JSON.stringify(r.project, null, 2)
|
|
580
|
+
}
|
|
581
|
+
]
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
);
|
|
471
585
|
}
|
|
472
|
-
];
|
|
473
|
-
function getResource(uri) {
|
|
474
|
-
return resources.find((r) => r.uri === uri);
|
|
475
|
-
}
|
|
476
|
-
function listResources() {
|
|
477
|
-
return resources;
|
|
478
586
|
}
|
|
479
587
|
|
|
480
|
-
// src/tools
|
|
481
|
-
var
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
588
|
+
// src/tools.ts
|
|
589
|
+
var z2 = __toESM(require("zod/v4"));
|
|
590
|
+
var import_core3 = require("@channlize/core");
|
|
591
|
+
var STATUSES = ["open", "pending", "resolved", "closed"];
|
|
592
|
+
var PRIORITIES = ["low", "normal", "high", "urgent"];
|
|
593
|
+
function ok(data) {
|
|
594
|
+
return {
|
|
595
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
596
|
+
structuredContent: data
|
|
597
|
+
};
|
|
489
598
|
}
|
|
490
|
-
|
|
491
|
-
{
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
599
|
+
async function run(fn) {
|
|
600
|
+
try {
|
|
601
|
+
return ok(await fn());
|
|
602
|
+
} catch (err) {
|
|
603
|
+
const e = toToolError(err);
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: e.message }],
|
|
606
|
+
isError: true
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
var record2 = z2.record(z2.string(), z2.unknown());
|
|
611
|
+
var loose = z2.looseObject({});
|
|
612
|
+
var page = {
|
|
613
|
+
total: z2.number(),
|
|
614
|
+
limit: z2.number(),
|
|
615
|
+
offset: z2.number(),
|
|
616
|
+
hasMore: z2.boolean()
|
|
617
|
+
};
|
|
618
|
+
var conversationSummary = z2.looseObject({
|
|
619
|
+
id: z2.string(),
|
|
620
|
+
subject: z2.string().nullable().optional(),
|
|
621
|
+
status: z2.string(),
|
|
622
|
+
priority: z2.string().nullable().optional(),
|
|
623
|
+
customerEmail: z2.string().nullable().optional(),
|
|
624
|
+
customerName: z2.string().nullable().optional(),
|
|
625
|
+
tags: z2.array(z2.string()).nullable().optional(),
|
|
626
|
+
createdAt: z2.string().optional()
|
|
627
|
+
});
|
|
628
|
+
var READ = {
|
|
629
|
+
readOnlyHint: true,
|
|
630
|
+
destructiveHint: false,
|
|
631
|
+
idempotentHint: true,
|
|
632
|
+
openWorldHint: false
|
|
633
|
+
};
|
|
634
|
+
function registerServerTools(server, api) {
|
|
635
|
+
server.registerTool(
|
|
636
|
+
"get_project",
|
|
637
|
+
{
|
|
638
|
+
title: "Get project",
|
|
639
|
+
description: "Show the Channlize project behind the server API key: name, ID, totals, the key's permissions and whether Slack is connected. Start here to check the setup.",
|
|
640
|
+
inputSchema: z2.object({}),
|
|
641
|
+
outputSchema: z2.object({ project: loose }),
|
|
642
|
+
annotations: READ
|
|
528
643
|
},
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
})
|
|
644
|
+
() => run(async () => {
|
|
645
|
+
const r = await api.request("GET", "/projects/me");
|
|
646
|
+
return { project: r.project };
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
server.registerTool(
|
|
650
|
+
"get_analytics",
|
|
651
|
+
{
|
|
652
|
+
title: "Get analytics",
|
|
653
|
+
description: "Daily support activity for the last 30 days (UTC): conversations created and resolved, customer and agent messages.",
|
|
654
|
+
inputSchema: z2.object({}),
|
|
655
|
+
outputSchema: z2.object({ analytics: loose }),
|
|
656
|
+
annotations: READ
|
|
657
|
+
},
|
|
658
|
+
() => run(async () => {
|
|
659
|
+
const r = await api.request(
|
|
660
|
+
"GET",
|
|
661
|
+
"/projects/analytics"
|
|
662
|
+
);
|
|
663
|
+
return { analytics: r.analytics };
|
|
664
|
+
})
|
|
665
|
+
);
|
|
666
|
+
server.registerTool(
|
|
667
|
+
"list_conversations",
|
|
668
|
+
{
|
|
669
|
+
title: "List conversations",
|
|
670
|
+
description: "List the organization's most recent support conversations (newest first, from the latest 50), optionally filtered by status or priority. Use get_conversation for the messages.",
|
|
671
|
+
inputSchema: z2.object({
|
|
672
|
+
status: z2.enum(STATUSES).optional().describe("Only conversations with this status"),
|
|
673
|
+
priority: z2.enum(PRIORITIES).optional().describe("Only conversations with this priority"),
|
|
674
|
+
limit: z2.number().int().min(1).max(50).default(20).describe("How many to return (1-50, default 20)")
|
|
675
|
+
}),
|
|
676
|
+
outputSchema: z2.object({
|
|
677
|
+
conversations: z2.array(conversationSummary),
|
|
678
|
+
returned: z2.number(),
|
|
679
|
+
matched: z2.number(),
|
|
680
|
+
scanned: z2.number()
|
|
681
|
+
}),
|
|
682
|
+
annotations: READ
|
|
683
|
+
},
|
|
684
|
+
({ status, priority, limit }) => run(async () => {
|
|
685
|
+
const r = await api.request(
|
|
686
|
+
"GET",
|
|
687
|
+
"/conversations"
|
|
688
|
+
);
|
|
689
|
+
const all = r.conversations ?? [];
|
|
690
|
+
const matched = all.filter(
|
|
691
|
+
(c) => (!status || c.status === status) && (!priority || c.priority === priority)
|
|
692
|
+
);
|
|
693
|
+
const conversations = matched.slice(0, limit);
|
|
540
694
|
return {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
695
|
+
conversations,
|
|
696
|
+
returned: conversations.length,
|
|
697
|
+
matched: matched.length,
|
|
698
|
+
scanned: all.length
|
|
544
699
|
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
700
|
+
})
|
|
701
|
+
);
|
|
702
|
+
server.registerTool(
|
|
703
|
+
"get_conversation",
|
|
704
|
+
{
|
|
705
|
+
title: "Get conversation",
|
|
706
|
+
description: "Read one conversation with its full message thread (customer, agent and system messages), status, priority and tags.",
|
|
707
|
+
inputSchema: z2.object({
|
|
708
|
+
conversationId: z2.string().uuid().describe("Conversation ID (from list_conversations)")
|
|
709
|
+
}),
|
|
710
|
+
outputSchema: z2.object({ conversation: loose }),
|
|
711
|
+
annotations: READ
|
|
712
|
+
},
|
|
713
|
+
({ conversationId }) => run(async () => {
|
|
714
|
+
const conversation = await api.request(
|
|
715
|
+
"GET",
|
|
716
|
+
`/conversations/${encodeURIComponent(conversationId)}`
|
|
717
|
+
);
|
|
718
|
+
return { conversation };
|
|
719
|
+
})
|
|
720
|
+
);
|
|
721
|
+
server.registerTool(
|
|
722
|
+
"create_conversation",
|
|
723
|
+
{
|
|
724
|
+
title: "Create conversation",
|
|
725
|
+
description: "Open a new support conversation on behalf of a customer. It is posted to Slack (the given channel, else routing rules, else the default channel). Needs the write scope. Repeating the same customer, subject and message within 30 seconds returns the existing conversation.",
|
|
726
|
+
inputSchema: z2.object({
|
|
727
|
+
customerEmail: z2.email().describe("Customer's email address"),
|
|
728
|
+
customerName: z2.string().max(255).optional().describe("Customer's name"),
|
|
729
|
+
subject: z2.string().min(1).max(500).describe("Subject line"),
|
|
730
|
+
message: z2.string().min(1).describe("First message, from the customer"),
|
|
731
|
+
channel: z2.string().optional().describe("Slack channel ID to post to, overriding routing rules"),
|
|
732
|
+
tags: z2.array(z2.string()).max(20).optional().describe("Tags")
|
|
733
|
+
}),
|
|
734
|
+
outputSchema: z2.object({ conversation: loose }),
|
|
735
|
+
annotations: {
|
|
736
|
+
readOnlyHint: false,
|
|
737
|
+
destructiveHint: false,
|
|
738
|
+
idempotentHint: false,
|
|
739
|
+
openWorldHint: true
|
|
558
740
|
}
|
|
559
741
|
},
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
description: "Send a message in an existing chat session. The message will be delivered to the Slack thread and the user can see agent replies in real-time.",
|
|
579
|
-
inputSchema: {
|
|
580
|
-
type: "object",
|
|
581
|
-
properties: {
|
|
582
|
-
sessionId: {
|
|
583
|
-
type: "string",
|
|
584
|
-
description: "The chat session ID"
|
|
585
|
-
},
|
|
586
|
-
content: {
|
|
587
|
-
type: "string",
|
|
588
|
-
description: "The message content to send"
|
|
589
|
-
}
|
|
590
|
-
},
|
|
591
|
-
required: ["sessionId", "content"]
|
|
742
|
+
(args) => run(async () => {
|
|
743
|
+
const conversation = await api.request("POST", "/conversations", {
|
|
744
|
+
body: args
|
|
745
|
+
});
|
|
746
|
+
return { conversation };
|
|
747
|
+
})
|
|
748
|
+
);
|
|
749
|
+
server.registerTool(
|
|
750
|
+
"list_contacts",
|
|
751
|
+
{
|
|
752
|
+
title: "List contacts",
|
|
753
|
+
description: "List the customers who contacted the organization, with their conversation count. Paginate with limit/offset.",
|
|
754
|
+
inputSchema: z2.object({
|
|
755
|
+
limit: z2.number().int().min(1).max(100).default(50),
|
|
756
|
+
offset: z2.number().int().min(0).default(0)
|
|
757
|
+
}),
|
|
758
|
+
outputSchema: z2.object({ contacts: z2.array(loose), ...page }),
|
|
759
|
+
annotations: READ
|
|
592
760
|
},
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
761
|
+
({ limit, offset }) => run(async () => {
|
|
762
|
+
const r = await api.request("GET", "/contacts", {
|
|
763
|
+
query: { limit, offset }
|
|
764
|
+
});
|
|
596
765
|
return {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
author: response.message.author
|
|
603
|
-
},
|
|
604
|
-
sessionStatus: response.session.status,
|
|
605
|
-
deliveryMessage: "Message sent successfully"
|
|
766
|
+
contacts: r.contacts ?? [],
|
|
767
|
+
total: Number(r.total ?? 0),
|
|
768
|
+
limit: Number(r.limit ?? limit),
|
|
769
|
+
offset: Number(r.offset ?? offset),
|
|
770
|
+
hasMore: Boolean(r.hasMore)
|
|
606
771
|
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
limit: {
|
|
620
|
-
type: "number",
|
|
621
|
-
description: "Maximum number of messages to retrieve (default: 50)",
|
|
622
|
-
default: 50
|
|
623
|
-
},
|
|
624
|
-
offset: {
|
|
625
|
-
type: "number",
|
|
626
|
-
description: "Number of messages to skip (default: 0)",
|
|
627
|
-
default: 0
|
|
628
|
-
}
|
|
629
|
-
},
|
|
630
|
-
required: ["sessionId"]
|
|
772
|
+
})
|
|
773
|
+
);
|
|
774
|
+
server.registerTool(
|
|
775
|
+
"get_contact",
|
|
776
|
+
{
|
|
777
|
+
title: "Get contact",
|
|
778
|
+
description: "Read one contact (customer) by ID.",
|
|
779
|
+
inputSchema: z2.object({
|
|
780
|
+
contactId: z2.string().uuid().describe("Contact ID (from list_contacts)")
|
|
781
|
+
}),
|
|
782
|
+
outputSchema: z2.object({ contact: loose }),
|
|
783
|
+
annotations: READ
|
|
631
784
|
},
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
args2.limit || 50,
|
|
637
|
-
args2.offset || 0
|
|
785
|
+
({ contactId }) => run(async () => {
|
|
786
|
+
const contact = await api.request(
|
|
787
|
+
"GET",
|
|
788
|
+
`/contacts/${encodeURIComponent(contactId)}`
|
|
638
789
|
);
|
|
790
|
+
return { contact };
|
|
791
|
+
})
|
|
792
|
+
);
|
|
793
|
+
server.registerTool(
|
|
794
|
+
"create_contact",
|
|
795
|
+
{
|
|
796
|
+
title: "Create contact",
|
|
797
|
+
description: "Add a customer by email, or return the existing one with that email. Needs the write scope.",
|
|
798
|
+
inputSchema: z2.object({
|
|
799
|
+
email: z2.email(),
|
|
800
|
+
name: z2.string().max(255).optional(),
|
|
801
|
+
company: z2.string().optional(),
|
|
802
|
+
phone: z2.string().optional(),
|
|
803
|
+
tags: z2.array(z2.string()).max(20).optional()
|
|
804
|
+
}),
|
|
805
|
+
outputSchema: z2.object({ contact: loose }),
|
|
806
|
+
annotations: {
|
|
807
|
+
readOnlyHint: false,
|
|
808
|
+
destructiveHint: false,
|
|
809
|
+
idempotentHint: true,
|
|
810
|
+
openWorldHint: false
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
(args) => run(async () => {
|
|
814
|
+
const contact = await api.request("POST", "/contacts", {
|
|
815
|
+
body: args
|
|
816
|
+
});
|
|
817
|
+
return { contact };
|
|
818
|
+
})
|
|
819
|
+
);
|
|
820
|
+
server.registerTool(
|
|
821
|
+
"notify_event",
|
|
822
|
+
{
|
|
823
|
+
title: "Send an Event to Slack",
|
|
824
|
+
description: 'Post an Event to Slack: the event key (e.g. "user.subscribed") picks the channel and message template set under Events in the dashboard, and `data` fills its {{placeholders}}. An unknown key is saved as a disabled draft and answered with status "skipped". Needs the write scope. Pass an idempotencyKey so a retry never posts twice.',
|
|
825
|
+
inputSchema: z2.object({
|
|
826
|
+
event: z2.string().max(100).regex(
|
|
827
|
+
import_core3.EVENT_KEY_PATTERN,
|
|
828
|
+
"lowercase letters, digits, dots, underscores or dashes"
|
|
829
|
+
).describe("Event key, e.g. user.subscribed"),
|
|
830
|
+
data: record2.optional().describe(
|
|
831
|
+
'Template fields, e.g. { "username": "ada", "plan": "Pro" }'
|
|
832
|
+
),
|
|
833
|
+
idempotencyKey: z2.string().min(1).max(255).optional().describe("Send at most once per key (retry-safe)"),
|
|
834
|
+
channel: z2.string().optional().describe("Slack channel ID instead of the event's channel")
|
|
835
|
+
}),
|
|
836
|
+
outputSchema: z2.looseObject({
|
|
837
|
+
id: z2.string(),
|
|
838
|
+
event: z2.string(),
|
|
839
|
+
status: z2.string(),
|
|
840
|
+
reason: z2.string().optional(),
|
|
841
|
+
duplicate: z2.boolean().optional()
|
|
842
|
+
}),
|
|
843
|
+
annotations: {
|
|
844
|
+
readOnlyHint: false,
|
|
845
|
+
destructiveHint: false,
|
|
846
|
+
idempotentHint: false,
|
|
847
|
+
openWorldHint: true
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
({ event, data, idempotencyKey, channel }) => run(async () => {
|
|
851
|
+
const client = await api.notifier();
|
|
852
|
+
const r = await client.notify(event, data, { idempotencyKey, channel });
|
|
639
853
|
return {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
content: msg.content,
|
|
646
|
-
timestamp: msg.timestamp,
|
|
647
|
-
author: msg.author,
|
|
648
|
-
authorData: msg.authorData,
|
|
649
|
-
hasAttachments: (msg.attachments?.length || 0) > 0
|
|
650
|
-
}))
|
|
854
|
+
id: r.id,
|
|
855
|
+
event: r.event,
|
|
856
|
+
status: r.status,
|
|
857
|
+
...r.reason ? { reason: r.reason } : {},
|
|
858
|
+
...r.duplicate ? { duplicate: true } : {}
|
|
651
859
|
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
860
|
+
})
|
|
861
|
+
);
|
|
862
|
+
server.registerTool(
|
|
863
|
+
"list_event_definitions",
|
|
864
|
+
{
|
|
865
|
+
title: "List Events",
|
|
866
|
+
description: "List the Events configured on the project (key, name, Slack channel, message template, enabled), including disabled drafts created by unknown keys sent with notify_event.",
|
|
867
|
+
inputSchema: z2.object({}),
|
|
868
|
+
outputSchema: z2.object({
|
|
869
|
+
projectId: z2.string(),
|
|
870
|
+
definitions: z2.array(loose)
|
|
871
|
+
}),
|
|
872
|
+
annotations: READ
|
|
873
|
+
},
|
|
874
|
+
() => run(async () => {
|
|
875
|
+
const projectId = await api.projectId();
|
|
876
|
+
const r = await api.request(
|
|
877
|
+
"GET",
|
|
878
|
+
`/v1/projects/${encodeURIComponent(projectId)}/event-definitions`
|
|
879
|
+
);
|
|
880
|
+
return { projectId, definitions: r.definitions ?? [] };
|
|
881
|
+
})
|
|
882
|
+
);
|
|
883
|
+
server.registerTool(
|
|
884
|
+
"list_event_deliveries",
|
|
885
|
+
{
|
|
886
|
+
title: "List Event deliveries",
|
|
887
|
+
description: "Recent notify_event deliveries on the project, newest first, with their status (queued, sent, failed, skipped) and the reason when not sent. Use it to check why an Event did not reach Slack.",
|
|
888
|
+
inputSchema: z2.object({
|
|
889
|
+
definitionId: z2.string().uuid().optional().describe("Only this Event (ID from list_event_definitions)"),
|
|
890
|
+
limit: z2.number().int().min(1).max(100).default(20)
|
|
891
|
+
}),
|
|
892
|
+
outputSchema: z2.object({
|
|
893
|
+
projectId: z2.string(),
|
|
894
|
+
deliveries: z2.array(loose)
|
|
895
|
+
}),
|
|
896
|
+
annotations: READ
|
|
897
|
+
},
|
|
898
|
+
({ definitionId, limit }) => run(async () => {
|
|
899
|
+
const projectId = await api.projectId();
|
|
900
|
+
const r = await api.request(
|
|
901
|
+
"GET",
|
|
902
|
+
`/v1/projects/${encodeURIComponent(projectId)}/event-deliveries`,
|
|
903
|
+
{ query: { definitionId, limit } }
|
|
904
|
+
);
|
|
905
|
+
return { projectId, deliveries: r.deliveries ?? [] };
|
|
906
|
+
})
|
|
907
|
+
);
|
|
908
|
+
server.registerTool(
|
|
909
|
+
"query_logs",
|
|
910
|
+
{
|
|
911
|
+
title: "Query logs",
|
|
912
|
+
description: "Search log tracking (trackEvent / trackServerEvent / track_log), newest first. Filter by severity, source, text (matches the name and properties) and time range; paginate with limit/offset. Covers all projects unless projectId is given.",
|
|
913
|
+
inputSchema: z2.object({
|
|
914
|
+
severity: z2.enum(import_core3.EVENT_SEVERITIES).optional(),
|
|
915
|
+
source: z2.enum(["client_sdk", "server_api", "system"]).optional().describe(
|
|
916
|
+
"client_sdk = browser (widget key), server_api = server key"
|
|
917
|
+
),
|
|
918
|
+
search: z2.string().max(200).optional().describe("Text to look for"),
|
|
919
|
+
from: z2.iso.datetime({ offset: true }).optional().describe("ISO 8601 start"),
|
|
920
|
+
to: z2.iso.datetime({ offset: true }).optional().describe("ISO 8601 end"),
|
|
921
|
+
projectId: z2.string().uuid().optional(),
|
|
922
|
+
limit: z2.number().int().min(1).max(100).default(50),
|
|
923
|
+
offset: z2.number().int().min(0).default(0)
|
|
924
|
+
}),
|
|
925
|
+
outputSchema: z2.object({ events: z2.array(loose), ...page }),
|
|
926
|
+
annotations: READ
|
|
666
927
|
},
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
const session = await client.closeChatSession(args2.sessionId);
|
|
928
|
+
(args) => run(async () => {
|
|
929
|
+
const r = await api.request("GET", "/v1/events", { query: args });
|
|
670
930
|
return {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
931
|
+
events: r.events ?? [],
|
|
932
|
+
total: Number(r.total ?? 0),
|
|
933
|
+
limit: Number(r.limit ?? args.limit),
|
|
934
|
+
offset: Number(r.offset ?? args.offset),
|
|
935
|
+
hasMore: Boolean(r.hasMore)
|
|
676
936
|
};
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
];
|
|
680
|
-
function getTool(name) {
|
|
681
|
-
return tools.find((t) => t.name === name);
|
|
682
|
-
}
|
|
683
|
-
function listTools() {
|
|
684
|
-
return tools;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// src/prompts/index.ts
|
|
688
|
-
var prompts = [
|
|
689
|
-
{
|
|
690
|
-
name: "integrate-contact-form",
|
|
691
|
-
description: "Guide for adding a Channlize contact form to a React application",
|
|
692
|
-
arguments: [
|
|
693
|
-
{
|
|
694
|
-
name: "framework",
|
|
695
|
-
description: "The React framework being used (e.g., Next.js, Vite, CRA)",
|
|
696
|
-
required: false
|
|
697
|
-
},
|
|
698
|
-
{
|
|
699
|
-
name: "styling",
|
|
700
|
-
description: "The styling approach (e.g., Tailwind, CSS Modules, styled-components)",
|
|
701
|
-
required: false
|
|
702
|
-
}
|
|
703
|
-
],
|
|
704
|
-
getPrompt: (args2) => {
|
|
705
|
-
const framework = args2?.framework || "React";
|
|
706
|
-
const styling = args2?.styling || "CSS";
|
|
707
|
-
return `# Integrating Channlize Contact Form in ${framework}
|
|
708
|
-
|
|
709
|
-
## Step 1: Install Dependencies
|
|
710
|
-
|
|
711
|
-
\`\`\`bash
|
|
712
|
-
npm install @channlize/react
|
|
713
|
-
\`\`\`
|
|
714
|
-
|
|
715
|
-
## Step 2: Configure Provider
|
|
716
|
-
|
|
717
|
-
Wrap your application with the ChannlizeProvider at the root level:
|
|
718
|
-
|
|
719
|
-
\`\`\`tsx
|
|
720
|
-
import { ChannlizeProvider } from '@channlize/react';
|
|
721
|
-
|
|
722
|
-
function App() {
|
|
723
|
-
return (
|
|
724
|
-
<ChannlizeProvider
|
|
725
|
-
config={{
|
|
726
|
-
apiKey: process.env.NEXT_PUBLIC_CHANNLIZE_API_KEY, // Your widget key
|
|
727
|
-
projectId: process.env.NEXT_PUBLIC_CHANNLIZE_PROJECT_ID,
|
|
728
|
-
baseUrl: process.env.NEXT_PUBLIC_CHANNLIZE_API_URL, // Optional
|
|
729
|
-
}}
|
|
730
|
-
>
|
|
731
|
-
<YourAppContent />
|
|
732
|
-
</ChannlizeProvider>
|
|
937
|
+
})
|
|
733
938
|
);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
alert('Thank you! We will get back to you soon.');
|
|
751
|
-
}}
|
|
752
|
-
onError={(error) => {
|
|
753
|
-
console.error('Failed to submit:', error);
|
|
754
|
-
}}
|
|
755
|
-
customFields={[
|
|
756
|
-
{
|
|
757
|
-
name: 'department',
|
|
758
|
-
label: 'Department',
|
|
759
|
-
type: 'select',
|
|
760
|
-
options: ['Sales', 'Support', 'Billing'],
|
|
761
|
-
required: true,
|
|
762
|
-
},
|
|
763
|
-
]}
|
|
764
|
-
${styling === "Tailwind" ? `className="max-w-2xl"` : ""}
|
|
765
|
-
styles={{
|
|
766
|
-
form: { maxWidth: '600px', margin: '0 auto' },
|
|
767
|
-
button: {
|
|
768
|
-
background: '#007bff',
|
|
769
|
-
color: 'white',
|
|
770
|
-
padding: '0.75rem 1.5rem',
|
|
771
|
-
borderRadius: '0.5rem',
|
|
772
|
-
},
|
|
773
|
-
}}
|
|
774
|
-
submitText="Send Message"
|
|
775
|
-
successMessage="Thank you! We'll respond shortly."
|
|
776
|
-
/>
|
|
777
|
-
</div>
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
\`\`\`
|
|
781
|
-
|
|
782
|
-
## Step 4: Environment Variables
|
|
783
|
-
|
|
784
|
-
Add to your \`.env.local\` or \`.env\`:
|
|
785
|
-
|
|
786
|
-
\`\`\`
|
|
787
|
-
NEXT_PUBLIC_CHANNLIZE_API_KEY=your_widget_key_here
|
|
788
|
-
NEXT_PUBLIC_CHANNLIZE_PROJECT_ID=your_project_id_here
|
|
789
|
-
NEXT_PUBLIC_CHANNLIZE_API_URL=http://api.channlize.com # Optional
|
|
790
|
-
\`\`\`
|
|
791
|
-
|
|
792
|
-
## Step 5: Customize (Optional)
|
|
793
|
-
|
|
794
|
-
### Custom Fields
|
|
795
|
-
Add custom fields to gather specific information:
|
|
796
|
-
|
|
797
|
-
\`\`\`tsx
|
|
798
|
-
customFields={[
|
|
799
|
-
{
|
|
800
|
-
name: 'company',
|
|
801
|
-
label: 'Company Name',
|
|
802
|
-
type: 'text',
|
|
803
|
-
required: false,
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
name: 'priority',
|
|
807
|
-
label: 'Priority',
|
|
808
|
-
type: 'select',
|
|
809
|
-
options: ['Low', 'Medium', 'High'],
|
|
810
|
-
required: true,
|
|
811
|
-
},
|
|
812
|
-
]}
|
|
813
|
-
\`\`\`
|
|
814
|
-
|
|
815
|
-
### Custom Styling with ${styling}
|
|
816
|
-
${styling === "Tailwind" ? "Use Tailwind classes with the className prop or override styles." : "Apply custom styles using the styles prop or external CSS."}
|
|
817
|
-
|
|
818
|
-
## Tips
|
|
819
|
-
|
|
820
|
-
- Messages are sent to your configured Slack channel instantly
|
|
821
|
-
- Email fallback is automatic if Slack delivery fails
|
|
822
|
-
- Form includes built-in validation and error handling
|
|
823
|
-
- All fields support accessibility features (ARIA labels, keyboard navigation)
|
|
824
|
-
|
|
825
|
-
## Next Steps
|
|
826
|
-
|
|
827
|
-
- Set up Slack workspace integration in your Channlize dashboard
|
|
828
|
-
- Configure channel routing rules
|
|
829
|
-
- Add webhooks for custom workflows
|
|
830
|
-
`;
|
|
831
|
-
}
|
|
832
|
-
},
|
|
833
|
-
{
|
|
834
|
-
name: "setup-live-chat",
|
|
835
|
-
description: "Guide for implementing the Channlize live chat widget in a React application",
|
|
836
|
-
arguments: [
|
|
837
|
-
{
|
|
838
|
-
name: "placement",
|
|
839
|
-
description: 'Where to place the chat widget (e.g., "page", "popup", "sidebar")',
|
|
840
|
-
required: false
|
|
939
|
+
server.registerTool(
|
|
940
|
+
"track_log",
|
|
941
|
+
{
|
|
942
|
+
title: "Record a log",
|
|
943
|
+
description: "Record a log line under Log tracking from a trusted server context (source server_api). Needs the write scope. For Slack messages about product events use notify_event instead.",
|
|
944
|
+
inputSchema: z2.object({
|
|
945
|
+
name: z2.string().min(1).max(255).describe("Log name, e.g. payment.failed"),
|
|
946
|
+
severity: z2.enum(import_core3.EVENT_SEVERITIES).default("info"),
|
|
947
|
+
properties: record2.optional().describe("Extra fields")
|
|
948
|
+
}),
|
|
949
|
+
outputSchema: z2.object({ id: z2.string(), projectId: z2.string() }),
|
|
950
|
+
annotations: {
|
|
951
|
+
readOnlyHint: false,
|
|
952
|
+
destructiveHint: false,
|
|
953
|
+
idempotentHint: false,
|
|
954
|
+
openWorldHint: false
|
|
841
955
|
}
|
|
842
|
-
],
|
|
843
|
-
getPrompt: (args2) => {
|
|
844
|
-
const placement = args2?.placement || "page";
|
|
845
|
-
return `# Setting Up Channlize Live Chat Widget
|
|
846
|
-
|
|
847
|
-
## Step 1: Install Dependencies
|
|
848
|
-
|
|
849
|
-
\`\`\`bash
|
|
850
|
-
npm install @channlize/react
|
|
851
|
-
\`\`\`
|
|
852
|
-
|
|
853
|
-
## Step 2: Configure Provider
|
|
854
|
-
|
|
855
|
-
Ensure your app is wrapped with ChannlizeProvider:
|
|
856
|
-
|
|
857
|
-
\`\`\`tsx
|
|
858
|
-
import { ChannlizeProvider } from '@channlize/react';
|
|
859
|
-
|
|
860
|
-
function App() {
|
|
861
|
-
return (
|
|
862
|
-
<ChannlizeProvider
|
|
863
|
-
config={{
|
|
864
|
-
apiKey: process.env.NEXT_PUBLIC_CHANNLIZE_API_KEY,
|
|
865
|
-
projectId: process.env.NEXT_PUBLIC_CHANNLIZE_PROJECT_ID,
|
|
866
|
-
}}
|
|
867
|
-
>
|
|
868
|
-
<YourAppContent />
|
|
869
|
-
</ChannlizeProvider>
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
\`\`\`
|
|
873
|
-
|
|
874
|
-
## Step 3: Add Chat Component
|
|
875
|
-
|
|
876
|
-
${placement === "popup" ? `
|
|
877
|
-
### Popup Widget (Bottom-Right Corner)
|
|
878
|
-
|
|
879
|
-
\`\`\`tsx
|
|
880
|
-
import { useState } from 'react';
|
|
881
|
-
import { Chat } from '@channlize/react';
|
|
882
|
-
|
|
883
|
-
export function ChatWidget() {
|
|
884
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
885
|
-
|
|
886
|
-
return (
|
|
887
|
-
<>
|
|
888
|
-
{/* Floating Button */}
|
|
889
|
-
{!isOpen && (
|
|
890
|
-
<button
|
|
891
|
-
onClick={() => setIsOpen(true)}
|
|
892
|
-
style={{
|
|
893
|
-
position: 'fixed',
|
|
894
|
-
bottom: '20px',
|
|
895
|
-
right: '20px',
|
|
896
|
-
width: '60px',
|
|
897
|
-
height: '60px',
|
|
898
|
-
borderRadius: '50%',
|
|
899
|
-
background: '#007bff',
|
|
900
|
-
color: 'white',
|
|
901
|
-
border: 'none',
|
|
902
|
-
fontSize: '24px',
|
|
903
|
-
cursor: 'pointer',
|
|
904
|
-
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
905
|
-
zIndex: 1000,
|
|
906
|
-
}}
|
|
907
|
-
>
|
|
908
|
-
\u{1F4AC}
|
|
909
|
-
</button>
|
|
910
|
-
)}
|
|
911
|
-
|
|
912
|
-
{/* Chat Window */}
|
|
913
|
-
{isOpen && (
|
|
914
|
-
<div
|
|
915
|
-
style={{
|
|
916
|
-
position: 'fixed',
|
|
917
|
-
bottom: '20px',
|
|
918
|
-
right: '20px',
|
|
919
|
-
zIndex: 1000,
|
|
920
|
-
boxShadow: '0 4px 24px rgba(0,0,0,0.15)',
|
|
921
|
-
borderRadius: '12px',
|
|
922
|
-
overflow: 'hidden',
|
|
923
|
-
}}
|
|
924
|
-
>
|
|
925
|
-
<Chat
|
|
926
|
-
autoStart={true}
|
|
927
|
-
title="Customer Support"
|
|
928
|
-
placeholder="Type your message..."
|
|
929
|
-
onMessage={(message) => {
|
|
930
|
-
console.log('New message:', message);
|
|
931
|
-
}}
|
|
932
|
-
styles={{
|
|
933
|
-
container: {
|
|
934
|
-
width: '380px',
|
|
935
|
-
height: '600px',
|
|
936
|
-
display: 'flex',
|
|
937
|
-
flexDirection: 'column',
|
|
938
|
-
background: 'white',
|
|
939
|
-
},
|
|
940
|
-
header: {
|
|
941
|
-
background: '#007bff',
|
|
942
|
-
color: 'white',
|
|
943
|
-
padding: '1rem',
|
|
944
|
-
display: 'flex',
|
|
945
|
-
alignItems: 'center',
|
|
946
|
-
},
|
|
947
|
-
}}
|
|
948
|
-
/>
|
|
949
|
-
<button
|
|
950
|
-
onClick={() => setIsOpen(false)}
|
|
951
|
-
style={{
|
|
952
|
-
position: 'absolute',
|
|
953
|
-
top: '10px',
|
|
954
|
-
right: '10px',
|
|
955
|
-
background: 'rgba(255,255,255,0.2)',
|
|
956
|
-
border: 'none',
|
|
957
|
-
color: 'white',
|
|
958
|
-
cursor: 'pointer',
|
|
959
|
-
fontSize: '20px',
|
|
960
|
-
width: '30px',
|
|
961
|
-
height: '30px',
|
|
962
|
-
borderRadius: '50%',
|
|
963
|
-
}}
|
|
964
|
-
>
|
|
965
|
-
\xD7
|
|
966
|
-
</button>
|
|
967
|
-
</div>
|
|
968
|
-
)}
|
|
969
|
-
</>
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
\`\`\`
|
|
973
|
-
` : `
|
|
974
|
-
### Embedded Chat
|
|
975
|
-
|
|
976
|
-
\`\`\`tsx
|
|
977
|
-
import { Chat } from '@channlize/react';
|
|
978
|
-
|
|
979
|
-
export default function SupportPage() {
|
|
980
|
-
return (
|
|
981
|
-
<div className="container mx-auto py-8">
|
|
982
|
-
<h1>Customer Support</h1>
|
|
983
|
-
<Chat
|
|
984
|
-
autoStart={true}
|
|
985
|
-
title="Live Chat Support"
|
|
986
|
-
placeholder="How can we help you today?"
|
|
987
|
-
onMessage={(message) => {
|
|
988
|
-
// Handle incoming messages
|
|
989
|
-
console.log('New message:', message);
|
|
990
|
-
}}
|
|
991
|
-
onError={(error) => {
|
|
992
|
-
console.error('Chat error:', error);
|
|
993
|
-
}}
|
|
994
|
-
styles={{
|
|
995
|
-
container: {
|
|
996
|
-
width: '100%',
|
|
997
|
-
maxWidth: '800px',
|
|
998
|
-
height: '600px',
|
|
999
|
-
margin: '0 auto',
|
|
1000
|
-
border: '1px solid #e0e0e0',
|
|
1001
|
-
borderRadius: '12px',
|
|
1002
|
-
},
|
|
1003
|
-
header: {
|
|
1004
|
-
background: '#007bff',
|
|
1005
|
-
color: 'white',
|
|
1006
|
-
},
|
|
1007
|
-
}}
|
|
1008
|
-
maxHeight="600px"
|
|
1009
|
-
/>
|
|
1010
|
-
</div>
|
|
1011
|
-
);
|
|
1012
|
-
}
|
|
1013
|
-
\`\`\`
|
|
1014
|
-
`}
|
|
1015
|
-
|
|
1016
|
-
## Step 4: Advanced Features
|
|
1017
|
-
|
|
1018
|
-
### Custom Metadata
|
|
1019
|
-
Pass context about the user or page:
|
|
1020
|
-
|
|
1021
|
-
\`\`\`tsx
|
|
1022
|
-
<Chat
|
|
1023
|
-
metadata={{
|
|
1024
|
-
userId: user?.id,
|
|
1025
|
-
userName: user?.name,
|
|
1026
|
-
currentPage: window.location.pathname,
|
|
1027
|
-
plan: 'pro',
|
|
1028
|
-
}}
|
|
1029
|
-
/>
|
|
1030
|
-
\`\`\`
|
|
1031
|
-
|
|
1032
|
-
### Event Handlers
|
|
1033
|
-
React to chat events:
|
|
1034
|
-
|
|
1035
|
-
\`\`\`tsx
|
|
1036
|
-
<Chat
|
|
1037
|
-
onMessage={(message) => {
|
|
1038
|
-
// Track analytics
|
|
1039
|
-
analytics.track('chat_message_received', {
|
|
1040
|
-
author: message.author,
|
|
1041
|
-
timestamp: message.timestamp,
|
|
1042
|
-
});
|
|
1043
|
-
}}
|
|
1044
|
-
onError={(error) => {
|
|
1045
|
-
// Report errors
|
|
1046
|
-
errorReporting.log(error);
|
|
1047
|
-
}}
|
|
1048
|
-
/>
|
|
1049
|
-
\`\`\`
|
|
1050
|
-
|
|
1051
|
-
## Real-Time Features
|
|
1052
|
-
|
|
1053
|
-
- Messages are delivered instantly via Server-Sent Events (SSE)
|
|
1054
|
-
- Agent presence indicators show when team is active
|
|
1055
|
-
- File attachments supported (images, documents)
|
|
1056
|
-
- Automatic reconnection on connection loss
|
|
1057
|
-
- Message history persisted across sessions
|
|
1058
|
-
|
|
1059
|
-
## Tips
|
|
1060
|
-
|
|
1061
|
-
- Chat sessions are automatically created on first message
|
|
1062
|
-
- Sessions remain active until explicitly closed
|
|
1063
|
-
- Messages are threaded in Slack for team collaboration
|
|
1064
|
-
- Supports multiple concurrent chat sessions per project
|
|
1065
|
-
|
|
1066
|
-
## Next Steps
|
|
1067
|
-
|
|
1068
|
-
- Configure Slack notifications for new chats
|
|
1069
|
-
- Set up auto-responses for common questions
|
|
1070
|
-
- Integrate with your analytics platform
|
|
1071
|
-
- Add custom routing based on user context
|
|
1072
|
-
`;
|
|
1073
|
-
}
|
|
1074
|
-
},
|
|
1075
|
-
{
|
|
1076
|
-
name: "customize-styling",
|
|
1077
|
-
description: "Guide for theming and customizing Channlize components",
|
|
1078
|
-
getPrompt: () => `# Customizing Channlize Component Styling
|
|
1079
|
-
|
|
1080
|
-
Channlize components are designed to be highly customizable to match your brand.
|
|
1081
|
-
|
|
1082
|
-
## Styling Approaches
|
|
1083
|
-
|
|
1084
|
-
### 1. Inline Styles with \`styles\` Prop
|
|
1085
|
-
|
|
1086
|
-
The most direct way to customize appearance:
|
|
1087
|
-
|
|
1088
|
-
\`\`\`tsx
|
|
1089
|
-
<Form
|
|
1090
|
-
styles={{
|
|
1091
|
-
form: {
|
|
1092
|
-
maxWidth: '600px',
|
|
1093
|
-
margin: '0 auto',
|
|
1094
|
-
padding: '2rem',
|
|
1095
|
-
background: '#f9fafb',
|
|
1096
|
-
borderRadius: '8px',
|
|
1097
|
-
},
|
|
1098
|
-
field: {
|
|
1099
|
-
marginBottom: '1.5rem',
|
|
1100
|
-
},
|
|
1101
|
-
label: {
|
|
1102
|
-
display: 'block',
|
|
1103
|
-
marginBottom: '0.5rem',
|
|
1104
|
-
fontWeight: '600',
|
|
1105
|
-
color: '#374151',
|
|
1106
|
-
},
|
|
1107
|
-
input: {
|
|
1108
|
-
width: '100%',
|
|
1109
|
-
padding: '0.75rem',
|
|
1110
|
-
border: '1px solid #d1d5db',
|
|
1111
|
-
borderRadius: '6px',
|
|
1112
|
-
fontSize: '1rem',
|
|
1113
|
-
},
|
|
1114
|
-
textarea: {
|
|
1115
|
-
width: '100%',
|
|
1116
|
-
padding: '0.75rem',
|
|
1117
|
-
border: '1px solid #d1d5db',
|
|
1118
|
-
borderRadius: '6px',
|
|
1119
|
-
fontSize: '1rem',
|
|
1120
|
-
minHeight: '120px',
|
|
1121
|
-
},
|
|
1122
|
-
button: {
|
|
1123
|
-
background: '#3b82f6',
|
|
1124
|
-
color: 'white',
|
|
1125
|
-
padding: '0.75rem 2rem',
|
|
1126
|
-
border: 'none',
|
|
1127
|
-
borderRadius: '6px',
|
|
1128
|
-
fontSize: '1rem',
|
|
1129
|
-
fontWeight: '600',
|
|
1130
|
-
cursor: 'pointer',
|
|
1131
|
-
transition: 'background 0.2s',
|
|
1132
|
-
},
|
|
1133
|
-
error: {
|
|
1134
|
-
color: '#ef4444',
|
|
1135
|
-
fontSize: '0.875rem',
|
|
1136
|
-
marginTop: '0.5rem',
|
|
1137
|
-
},
|
|
1138
|
-
success: {
|
|
1139
|
-
background: '#ecfdf5',
|
|
1140
|
-
border: '1px solid #10b981',
|
|
1141
|
-
padding: '1.5rem',
|
|
1142
|
-
borderRadius: '6px',
|
|
1143
|
-
color: '#065f46',
|
|
1144
|
-
},
|
|
1145
|
-
}}
|
|
1146
|
-
/>
|
|
1147
|
-
\`\`\`
|
|
1148
|
-
|
|
1149
|
-
### 2. CSS Classes with \`className\` Prop
|
|
1150
|
-
|
|
1151
|
-
Use your existing CSS or CSS-in-JS solution:
|
|
1152
|
-
|
|
1153
|
-
\`\`\`tsx
|
|
1154
|
-
<Form className="custom-form" />
|
|
1155
|
-
\`\`\`
|
|
1156
|
-
|
|
1157
|
-
\`\`\`css
|
|
1158
|
-
/* styles.css */
|
|
1159
|
-
.channlize-form {
|
|
1160
|
-
max-width: 600px;
|
|
1161
|
-
margin: 0 auto;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
.channlize-field {
|
|
1165
|
-
margin-bottom: 1.5rem;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
.channlize-field label {
|
|
1169
|
-
font-weight: 600;
|
|
1170
|
-
color: var(--primary-text);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
.channlize-field input,
|
|
1174
|
-
.channlize-field textarea,
|
|
1175
|
-
.channlize-field select {
|
|
1176
|
-
border: 1px solid var(--border-color);
|
|
1177
|
-
border-radius: 6px;
|
|
1178
|
-
padding: 0.75rem;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
.channlize-form button[type="submit"] {
|
|
1182
|
-
background: var(--primary-color);
|
|
1183
|
-
color: white;
|
|
1184
|
-
}
|
|
1185
|
-
\`\`\`
|
|
1186
|
-
|
|
1187
|
-
### 3. Tailwind CSS
|
|
1188
|
-
|
|
1189
|
-
Combine with Tailwind utility classes:
|
|
1190
|
-
|
|
1191
|
-
\`\`\`tsx
|
|
1192
|
-
<Form
|
|
1193
|
-
className="max-w-2xl mx-auto p-8"
|
|
1194
|
-
styles={{
|
|
1195
|
-
input: {
|
|
1196
|
-
// Tailwind styles as objects don't work well
|
|
1197
|
-
// Use className on wrapper or global CSS
|
|
1198
956
|
},
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
.channlize-field textarea {
|
|
1211
|
-
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
.channlize-form button[type="submit"] {
|
|
1215
|
-
@apply bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-lg transition;
|
|
1216
|
-
}
|
|
957
|
+
({ name, severity, properties }) => run(async () => {
|
|
958
|
+
const projectId = await api.projectId();
|
|
959
|
+
const r = await api.request("POST", "/v1/events", {
|
|
960
|
+
body: { projectId, name, severity, properties }
|
|
961
|
+
});
|
|
962
|
+
return { id: r.id, projectId };
|
|
963
|
+
})
|
|
964
|
+
);
|
|
1217
965
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
966
|
+
var VISITOR = {
|
|
967
|
+
readOnlyHint: false,
|
|
968
|
+
destructiveHint: false,
|
|
969
|
+
idempotentHint: false,
|
|
970
|
+
openWorldHint: true
|
|
971
|
+
};
|
|
972
|
+
function registerWidgetTools(server, api) {
|
|
973
|
+
server.registerTool(
|
|
974
|
+
"submit_contact_form",
|
|
975
|
+
{
|
|
976
|
+
title: "Submit contact form",
|
|
977
|
+
description: "Submit the website contact form as a visitor (widget key). Creates a conversation that is posted to Slack. Useful to test the setup end to end.",
|
|
978
|
+
inputSchema: z2.object({
|
|
979
|
+
name: z2.string().min(1).describe("Visitor's name"),
|
|
980
|
+
email: z2.email().describe("Visitor's email"),
|
|
981
|
+
subject: z2.string().optional(),
|
|
982
|
+
message: z2.string().min(1),
|
|
983
|
+
pageUrl: z2.url().optional().describe("Page the form was sent from")
|
|
984
|
+
}),
|
|
985
|
+
outputSchema: z2.looseObject({
|
|
986
|
+
conversationId: z2.string().optional(),
|
|
987
|
+
message: z2.string().optional()
|
|
988
|
+
}),
|
|
989
|
+
annotations: VISITOR
|
|
1239
990
|
},
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
991
|
+
(args) => run(async () => {
|
|
992
|
+
const r = await api.widget().submitContactForm(args);
|
|
993
|
+
return { conversationId: r.conversationId, message: r.message };
|
|
994
|
+
})
|
|
995
|
+
);
|
|
996
|
+
server.registerTool(
|
|
997
|
+
"create_chat_session",
|
|
998
|
+
{
|
|
999
|
+
title: "Start chat session",
|
|
1000
|
+
description: "Start a live chat session as a website visitor (widget key). Returns the session ID for send_message.",
|
|
1001
|
+
inputSchema: z2.object({
|
|
1002
|
+
metadata: record2.optional().describe("Metadata to attach (e.g. user info)")
|
|
1003
|
+
}),
|
|
1004
|
+
outputSchema: z2.looseObject({
|
|
1005
|
+
sessionId: z2.string(),
|
|
1006
|
+
status: z2.string()
|
|
1007
|
+
}),
|
|
1008
|
+
annotations: VISITOR
|
|
1247
1009
|
},
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1010
|
+
({ metadata }) => run(async () => {
|
|
1011
|
+
const r = await api.widget().createChatSession(metadata);
|
|
1012
|
+
return {
|
|
1013
|
+
sessionId: r.session.id,
|
|
1014
|
+
status: r.session.status,
|
|
1015
|
+
createdAt: String(r.session.createdAt)
|
|
1016
|
+
};
|
|
1017
|
+
})
|
|
1018
|
+
);
|
|
1019
|
+
server.registerTool(
|
|
1020
|
+
"send_message",
|
|
1021
|
+
{
|
|
1022
|
+
title: "Send chat message",
|
|
1023
|
+
description: "Send a visitor message in a chat session (widget key); it is posted to the Slack thread.",
|
|
1024
|
+
inputSchema: z2.object({
|
|
1025
|
+
sessionId: z2.string().min(1).describe("From create_chat_session"),
|
|
1026
|
+
content: z2.string().min(1).max(5e3)
|
|
1027
|
+
}),
|
|
1028
|
+
outputSchema: z2.looseObject({
|
|
1029
|
+
messageId: z2.string(),
|
|
1030
|
+
sessionStatus: z2.string()
|
|
1031
|
+
}),
|
|
1032
|
+
annotations: VISITOR
|
|
1255
1033
|
},
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1034
|
+
({ sessionId, content }) => run(async () => {
|
|
1035
|
+
const r = await api.widget().sendMessage(sessionId, content);
|
|
1036
|
+
return { messageId: r.message.id, sessionStatus: r.session.status };
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
server.registerTool(
|
|
1040
|
+
"get_chat_messages",
|
|
1041
|
+
{
|
|
1042
|
+
title: "Read chat messages",
|
|
1043
|
+
description: "Read a chat session's messages, including agent replies from Slack (widget key).",
|
|
1044
|
+
inputSchema: z2.object({
|
|
1045
|
+
sessionId: z2.string().min(1),
|
|
1046
|
+
limit: z2.number().int().min(1).max(100).default(50),
|
|
1047
|
+
offset: z2.number().int().min(0).default(0)
|
|
1048
|
+
}),
|
|
1049
|
+
outputSchema: z2.object({
|
|
1050
|
+
sessionId: z2.string(),
|
|
1051
|
+
messages: z2.array(loose)
|
|
1052
|
+
}),
|
|
1053
|
+
annotations: { ...READ, openWorldHint: true }
|
|
1261
1054
|
},
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1055
|
+
({ sessionId, limit, offset }) => run(async () => {
|
|
1056
|
+
const messages = await api.widget().getChatMessages(sessionId, limit, offset);
|
|
1057
|
+
return {
|
|
1058
|
+
sessionId,
|
|
1059
|
+
messages: messages.map((m) => ({
|
|
1060
|
+
id: m.id,
|
|
1061
|
+
author: m.author,
|
|
1062
|
+
authorName: m.authorData?.name,
|
|
1063
|
+
content: m.content,
|
|
1064
|
+
timestamp: String(m.timestamp),
|
|
1065
|
+
attachments: m.attachments?.length ?? 0
|
|
1066
|
+
}))
|
|
1067
|
+
};
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
server.registerTool(
|
|
1071
|
+
"close_chat_session",
|
|
1072
|
+
{
|
|
1073
|
+
title: "End chat session",
|
|
1074
|
+
description: "End a chat session as the visitor (widget key).",
|
|
1075
|
+
inputSchema: z2.object({ sessionId: z2.string().min(1) }),
|
|
1076
|
+
outputSchema: z2.looseObject({
|
|
1077
|
+
sessionId: z2.string(),
|
|
1078
|
+
status: z2.string()
|
|
1079
|
+
}),
|
|
1080
|
+
annotations: { ...VISITOR, idempotentHint: true }
|
|
1269
1081
|
},
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
## CSS Class Reference
|
|
1275
|
-
|
|
1276
|
-
All Channlize components use semantic CSS classes:
|
|
1277
|
-
|
|
1278
|
-
**Form:**
|
|
1279
|
-
- \`.channlize-form\` - Form container
|
|
1280
|
-
- \`.channlize-form-success\` - Success message container
|
|
1281
|
-
- \`.channlize-field\` - Field wrapper
|
|
1282
|
-
- \`.channlize-error\` - Error message
|
|
1283
|
-
|
|
1284
|
-
**Chat:**
|
|
1285
|
-
- \`.channlize-chat-container\` - Main container
|
|
1286
|
-
- \`.channlize-chat-header\` - Header section
|
|
1287
|
-
- \`.channlize-chat-messages\` - Messages container
|
|
1288
|
-
- \`.channlize-message\` - Individual message
|
|
1289
|
-
- \`.channlize-message-user\` - User message
|
|
1290
|
-
- \`.channlize-message-agent\` - Agent message
|
|
1291
|
-
- \`.channlize-chat-input-container\` - Input area
|
|
1292
|
-
|
|
1293
|
-
## Dark Mode Example
|
|
1294
|
-
|
|
1295
|
-
\`\`\`tsx
|
|
1296
|
-
const darkStyles = {
|
|
1297
|
-
form: {
|
|
1298
|
-
background: '#1f2937',
|
|
1299
|
-
color: '#f9fafb',
|
|
1300
|
-
},
|
|
1301
|
-
input: {
|
|
1302
|
-
background: '#374151',
|
|
1303
|
-
color: '#f9fafb',
|
|
1304
|
-
border: '1px solid #4b5563',
|
|
1305
|
-
},
|
|
1306
|
-
button: {
|
|
1307
|
-
background: '#3b82f6',
|
|
1308
|
-
color: 'white',
|
|
1309
|
-
},
|
|
1310
|
-
};
|
|
1311
|
-
|
|
1312
|
-
<Form styles={darkStyles} />
|
|
1313
|
-
\`\`\`
|
|
1314
|
-
`
|
|
1315
|
-
},
|
|
1316
|
-
{
|
|
1317
|
-
name: "handle-webhooks",
|
|
1318
|
-
description: "Guide for integrating Channlize webhooks into custom workflows",
|
|
1319
|
-
getPrompt: () => `# Integrating Channlize Webhooks
|
|
1320
|
-
|
|
1321
|
-
Channlize can send webhook events to your backend for custom workflows and integrations.
|
|
1322
|
-
|
|
1323
|
-
## Available Webhook Events
|
|
1324
|
-
|
|
1325
|
-
- \`conversation.created\` - New contact form or chat session started
|
|
1326
|
-
- \`message.created\` - New message in a conversation
|
|
1327
|
-
- \`conversation.closed\` - Conversation marked as closed
|
|
1328
|
-
- \`agent.assigned\` - Agent assigned to conversation
|
|
1329
|
-
|
|
1330
|
-
## Setting Up Webhooks
|
|
1331
|
-
|
|
1332
|
-
### 1. Configure Webhook URL
|
|
1333
|
-
|
|
1334
|
-
In your Channlize dashboard:
|
|
1335
|
-
1. Navigate to Project Settings \u2192 Webhooks
|
|
1336
|
-
2. Add your webhook endpoint URL
|
|
1337
|
-
3. Select which events to receive
|
|
1338
|
-
4. Save your secret key for verification
|
|
1339
|
-
|
|
1340
|
-
### 2. Create Webhook Handler (Next.js API Route)
|
|
1341
|
-
|
|
1342
|
-
\`\`\`typescript
|
|
1343
|
-
// app/api/webhooks/channlize/route.ts
|
|
1344
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
1345
|
-
import crypto from 'crypto';
|
|
1346
|
-
|
|
1347
|
-
const WEBHOOK_SECRET = process.env.CHANNLIZE_WEBHOOK_SECRET!;
|
|
1348
|
-
|
|
1349
|
-
function verifySignature(payload: string, signature: string): boolean {
|
|
1350
|
-
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
|
|
1351
|
-
const digest = hmac.update(payload).digest('hex');
|
|
1352
|
-
return crypto.timingSafeEqual(
|
|
1353
|
-
Buffer.from(signature),
|
|
1354
|
-
Buffer.from(digest)
|
|
1082
|
+
({ sessionId }) => run(async () => {
|
|
1083
|
+
const s = await api.widget().closeChatSession(sessionId);
|
|
1084
|
+
return { sessionId: s.id ?? sessionId, status: s.status };
|
|
1085
|
+
})
|
|
1355
1086
|
);
|
|
1356
1087
|
}
|
|
1357
1088
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
);
|
|
1089
|
+
// src/http.ts
|
|
1090
|
+
var import_node_http = require("http");
|
|
1091
|
+
var import_server = require("@modelcontextprotocol/server");
|
|
1092
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
|
|
1093
|
+
async function toRequest(req, origin) {
|
|
1094
|
+
const headers = new Headers();
|
|
1095
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
1096
|
+
if (Array.isArray(v)) v.forEach((x) => headers.append(k, x));
|
|
1097
|
+
else if (v !== void 0) headers.set(k, v);
|
|
1367
1098
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
case 'conversation.created':
|
|
1374
|
-
await handleNewConversation(event.data);
|
|
1375
|
-
break;
|
|
1376
|
-
|
|
1377
|
-
case 'message.created':
|
|
1378
|
-
await handleNewMessage(event.data);
|
|
1379
|
-
break;
|
|
1380
|
-
|
|
1381
|
-
case 'conversation.closed':
|
|
1382
|
-
await handleConversationClosed(event.data);
|
|
1383
|
-
break;
|
|
1384
|
-
|
|
1385
|
-
default:
|
|
1386
|
-
console.log('Unhandled event type:', event.type);
|
|
1099
|
+
let body;
|
|
1100
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1101
|
+
const chunks = [];
|
|
1102
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1103
|
+
body = new Uint8Array(Buffer.concat(chunks));
|
|
1387
1104
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
async function handleNewConversation(data: any) {
|
|
1393
|
-
// Example: Log to analytics
|
|
1394
|
-
await analytics.track('support_conversation_started', {
|
|
1395
|
-
conversationId: data.conversation.id,
|
|
1396
|
-
type: data.conversation.type, // 'contact_form' or 'chat'
|
|
1397
|
-
customerEmail: data.conversation.email,
|
|
1105
|
+
const controller = new AbortController();
|
|
1106
|
+
req.on("close", () => {
|
|
1107
|
+
if (!req.complete) controller.abort();
|
|
1398
1108
|
});
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
customerEmail: data.conversation.email,
|
|
1405
|
-
message: data.conversation.initialMessage,
|
|
1406
|
-
});
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
async function handleNewMessage(data: any) {
|
|
1411
|
-
// Example: Auto-tag based on content
|
|
1412
|
-
const message = data.message.content.toLowerCase();
|
|
1413
|
-
|
|
1414
|
-
if (message.includes('bug') || message.includes('error')) {
|
|
1415
|
-
await tagConversation(data.conversation.id, 'bug');
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
if (message.includes('billing') || message.includes('payment')) {
|
|
1419
|
-
await tagConversation(data.conversation.id, 'billing');
|
|
1420
|
-
await assignToDepartment(data.conversation.id, 'billing');
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// Example: Sentiment analysis
|
|
1424
|
-
const sentiment = await analyzeSentiment(message);
|
|
1425
|
-
if (sentiment === 'negative') {
|
|
1426
|
-
await escalateConversation(data.conversation.id, 'high');
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
async function handleConversationClosed(data: any) {
|
|
1431
|
-
// Example: Send satisfaction survey
|
|
1432
|
-
await sendSurveyEmail({
|
|
1433
|
-
to: data.conversation.email,
|
|
1434
|
-
conversationId: data.conversation.id,
|
|
1435
|
-
resolutionTime: data.metrics.resolutionTime,
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
// Example: Update CRM
|
|
1439
|
-
await updateCRM({
|
|
1440
|
-
email: data.conversation.email,
|
|
1441
|
-
lastInteraction: new Date(),
|
|
1442
|
-
supportTicketId: data.conversation.id,
|
|
1443
|
-
resolved: true,
|
|
1109
|
+
return new Request(new URL(req.url ?? "/", origin), {
|
|
1110
|
+
method: req.method,
|
|
1111
|
+
headers,
|
|
1112
|
+
body,
|
|
1113
|
+
signal: controller.signal
|
|
1444
1114
|
});
|
|
1445
1115
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
});
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
## Webhook Event Payload Examples
|
|
1481
|
-
|
|
1482
|
-
### conversation.created
|
|
1483
|
-
|
|
1484
|
-
\`\`\`json
|
|
1485
|
-
{
|
|
1486
|
-
"type": "conversation.created",
|
|
1487
|
-
"timestamp": "2025-01-15T10:00:00Z",
|
|
1488
|
-
"data": {
|
|
1489
|
-
"conversation": {
|
|
1490
|
-
"id": "conv_123",
|
|
1491
|
-
"type": "contact_form",
|
|
1492
|
-
"status": "open",
|
|
1493
|
-
"email": "customer@example.com",
|
|
1494
|
-
"name": "John Doe",
|
|
1495
|
-
"initialMessage": "I need help with...",
|
|
1496
|
-
"metadata": {
|
|
1497
|
-
"pageUrl": "https://example.com/contact",
|
|
1498
|
-
"userAgent": "Mozilla/5.0..."
|
|
1499
|
-
},
|
|
1500
|
-
"createdAt": "2025-01-15T10:00:00Z"
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
\`\`\`
|
|
1505
|
-
|
|
1506
|
-
### message.created
|
|
1507
|
-
|
|
1508
|
-
\`\`\`json
|
|
1509
|
-
{
|
|
1510
|
-
"type": "message.created",
|
|
1511
|
-
"timestamp": "2025-01-15T10:05:00Z",
|
|
1512
|
-
"data": {
|
|
1513
|
-
"message": {
|
|
1514
|
-
"id": "msg_456",
|
|
1515
|
-
"conversationId": "conv_123",
|
|
1516
|
-
"content": "Thanks for reaching out!",
|
|
1517
|
-
"author": "agent",
|
|
1518
|
-
"authorData": {
|
|
1519
|
-
"name": "Sarah",
|
|
1520
|
-
"email": "sarah@example.com"
|
|
1521
|
-
},
|
|
1522
|
-
"timestamp": "2025-01-15T10:05:00Z"
|
|
1523
|
-
},
|
|
1524
|
-
"conversation": {
|
|
1525
|
-
"id": "conv_123",
|
|
1526
|
-
"status": "open"
|
|
1116
|
+
async function startHttpServer(factory, options = {}) {
|
|
1117
|
+
const host = options.host ?? "127.0.0.1";
|
|
1118
|
+
const port = options.port ?? 3333;
|
|
1119
|
+
const path = options.path ?? "/mcp";
|
|
1120
|
+
const local = LOCAL_HOSTS.has(host);
|
|
1121
|
+
const handler = (0, import_server.createMcpHandler)(() => factory(), {
|
|
1122
|
+
onerror: (err) => console.error("[channlize-mcp]", err.message)
|
|
1123
|
+
});
|
|
1124
|
+
const server = (0, import_node_http.createServer)(async (req, res) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const request = await toRequest(
|
|
1127
|
+
req,
|
|
1128
|
+
`http://${req.headers.host ?? `${host}:${port}`}`
|
|
1129
|
+
);
|
|
1130
|
+
const url2 = new URL(request.url);
|
|
1131
|
+
if (url2.pathname !== path) {
|
|
1132
|
+
res.writeHead(404, { "Content-Type": "text/plain" }).end(`Not found: use ${path}`);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const rejected = local ? (0, import_server.hostHeaderValidationResponse)(request, (0, import_server.localhostAllowedHostnames)()) ?? (0, import_server.originValidationResponse)(request, (0, import_server.localhostAllowedOrigins)()) : void 0;
|
|
1136
|
+
const response = rejected ?? await handler.fetch(request);
|
|
1137
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
1138
|
+
if (response.body) {
|
|
1139
|
+
for await (const chunk of response.body) {
|
|
1140
|
+
res.write(chunk);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
res.end();
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
console.error("[channlize-mcp]", err.message);
|
|
1146
|
+
if (!res.headersSent)
|
|
1147
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1148
|
+
res.end("Internal server error");
|
|
1527
1149
|
}
|
|
1528
|
-
}
|
|
1150
|
+
});
|
|
1151
|
+
await new Promise((resolve, reject) => {
|
|
1152
|
+
server.once("error", reject);
|
|
1153
|
+
server.listen(port, host, () => resolve());
|
|
1154
|
+
});
|
|
1155
|
+
const address = server.address();
|
|
1156
|
+
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
1157
|
+
const shownHost = host.includes(":") ? `[${host}]` : host;
|
|
1158
|
+
return {
|
|
1159
|
+
url: `http://${shownHost}:${actualPort}${path}`,
|
|
1160
|
+
server,
|
|
1161
|
+
close: () => new Promise((r) => server.close(() => r()))
|
|
1162
|
+
};
|
|
1529
1163
|
}
|
|
1530
|
-
\`\`\`
|
|
1531
|
-
|
|
1532
|
-
## Use Cases
|
|
1533
|
-
|
|
1534
|
-
1. **CRM Integration**: Sync conversations to Salesforce, HubSpot, etc.
|
|
1535
|
-
2. **Analytics**: Track support metrics in your data warehouse
|
|
1536
|
-
3. **Auto-routing**: Assign conversations based on content analysis
|
|
1537
|
-
4. **Notifications**: Alert teams via SMS, PagerDuty for urgent issues
|
|
1538
|
-
5. **AI Enhancement**: Feed messages to AI for sentiment/intent analysis
|
|
1539
|
-
6. **Billing**: Track support usage for metered billing
|
|
1540
1164
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
-
|
|
1546
|
-
-
|
|
1547
|
-
-
|
|
1548
|
-
|
|
1165
|
+
// src/index.ts
|
|
1166
|
+
var import_core4 = require("@channlize/core");
|
|
1167
|
+
var VERSION = true ? "0.4.0" : "0.0.0";
|
|
1168
|
+
var INSTRUCTIONS = `Channlize routes website conversations and app events to the team's Slack.
|
|
1169
|
+
- Inbox: list_conversations / get_conversation, then contacts. Replies and status changes happen in Slack or the dashboard.
|
|
1170
|
+
- Events (notify_event) post a Slack message per event key; list_event_definitions and list_event_deliveries explain what was sent or skipped.
|
|
1171
|
+
- Logs: query_logs searches log tracking; track_log records one.
|
|
1172
|
+
- Write tools need a server API key with the write scope; a 403 means the key is read-only.`;
|
|
1173
|
+
function buildServer(config, info = {}) {
|
|
1174
|
+
const api = new ChannlizeApi(resolveConfig(config));
|
|
1175
|
+
const server = new import_server2.McpServer(
|
|
1176
|
+
{ name: info.name ?? "channlize", version: info.version ?? VERSION },
|
|
1177
|
+
{ instructions: INSTRUCTIONS }
|
|
1178
|
+
);
|
|
1179
|
+
if (api.hasServerKey) {
|
|
1180
|
+
registerServerTools(server, api);
|
|
1181
|
+
registerServerPrompts(server);
|
|
1549
1182
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
function listPrompts() {
|
|
1555
|
-
return prompts;
|
|
1183
|
+
if (api.hasWidgetKey) registerWidgetTools(server, api);
|
|
1184
|
+
registerResources(server, api);
|
|
1185
|
+
registerGuidePrompts(server);
|
|
1186
|
+
return server;
|
|
1556
1187
|
}
|
|
1557
|
-
|
|
1558
|
-
// src/index.ts
|
|
1559
1188
|
var ChannlizeMCPServer = class {
|
|
1560
1189
|
server;
|
|
1561
1190
|
config;
|
|
1191
|
+
info;
|
|
1562
1192
|
constructor(options = {}) {
|
|
1563
|
-
this.config = options.config;
|
|
1564
|
-
this.
|
|
1565
|
-
|
|
1566
|
-
name: options.name || "channlize-mcp",
|
|
1567
|
-
version: options.version || "0.1.0"
|
|
1568
|
-
},
|
|
1569
|
-
{
|
|
1570
|
-
capabilities: {
|
|
1571
|
-
resources: {},
|
|
1572
|
-
tools: {},
|
|
1573
|
-
prompts: {}
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
);
|
|
1577
|
-
this.setupHandlers();
|
|
1578
|
-
}
|
|
1579
|
-
setupHandlers() {
|
|
1580
|
-
this.server.setRequestHandler(import_types.ListResourcesRequestSchema, async () => {
|
|
1581
|
-
const resources2 = listResources();
|
|
1582
|
-
return {
|
|
1583
|
-
resources: resources2.map((r) => ({
|
|
1584
|
-
uri: r.uri,
|
|
1585
|
-
name: r.name,
|
|
1586
|
-
description: r.description,
|
|
1587
|
-
mimeType: r.mimeType
|
|
1588
|
-
}))
|
|
1589
|
-
};
|
|
1590
|
-
});
|
|
1591
|
-
this.server.setRequestHandler(import_types.ReadResourceRequestSchema, async (request) => {
|
|
1592
|
-
const resource = getResource(request.params.uri);
|
|
1593
|
-
if (!resource) {
|
|
1594
|
-
throw new Error(`Resource not found: ${request.params.uri}`);
|
|
1595
|
-
}
|
|
1596
|
-
return {
|
|
1597
|
-
contents: [
|
|
1598
|
-
{
|
|
1599
|
-
uri: resource.uri,
|
|
1600
|
-
mimeType: resource.mimeType,
|
|
1601
|
-
text: resource.getText()
|
|
1602
|
-
}
|
|
1603
|
-
]
|
|
1604
|
-
};
|
|
1605
|
-
});
|
|
1606
|
-
this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => {
|
|
1607
|
-
const tools2 = listTools();
|
|
1608
|
-
return {
|
|
1609
|
-
tools: tools2.map((t) => ({
|
|
1610
|
-
name: t.name,
|
|
1611
|
-
description: t.description,
|
|
1612
|
-
inputSchema: t.inputSchema
|
|
1613
|
-
}))
|
|
1614
|
-
};
|
|
1615
|
-
});
|
|
1616
|
-
this.server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
1617
|
-
const tool = getTool(request.params.name);
|
|
1618
|
-
if (!tool) {
|
|
1619
|
-
throw new Error(`Tool not found: ${request.params.name}`);
|
|
1620
|
-
}
|
|
1621
|
-
if (!this.config?.apiKey) {
|
|
1622
|
-
throw new Error(
|
|
1623
|
-
"Channlize API key required. Please configure the MCP server with your API credentials."
|
|
1624
|
-
);
|
|
1625
|
-
}
|
|
1626
|
-
const context = {
|
|
1627
|
-
config: this.config
|
|
1628
|
-
};
|
|
1629
|
-
try {
|
|
1630
|
-
const result = await tool.handler(
|
|
1631
|
-
request.params.arguments || {},
|
|
1632
|
-
context
|
|
1633
|
-
);
|
|
1634
|
-
return {
|
|
1635
|
-
content: [
|
|
1636
|
-
{
|
|
1637
|
-
type: "text",
|
|
1638
|
-
text: JSON.stringify(result, null, 2)
|
|
1639
|
-
}
|
|
1640
|
-
]
|
|
1641
|
-
};
|
|
1642
|
-
} catch (error) {
|
|
1643
|
-
return {
|
|
1644
|
-
content: [
|
|
1645
|
-
{
|
|
1646
|
-
type: "text",
|
|
1647
|
-
text: JSON.stringify(
|
|
1648
|
-
{
|
|
1649
|
-
success: false,
|
|
1650
|
-
error: error.message || "Unknown error",
|
|
1651
|
-
code: error.code,
|
|
1652
|
-
statusCode: error.statusCode
|
|
1653
|
-
},
|
|
1654
|
-
null,
|
|
1655
|
-
2
|
|
1656
|
-
)
|
|
1657
|
-
}
|
|
1658
|
-
],
|
|
1659
|
-
isError: true
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
});
|
|
1663
|
-
this.server.setRequestHandler(import_types.ListPromptsRequestSchema, async () => {
|
|
1664
|
-
const prompts2 = listPrompts();
|
|
1665
|
-
return {
|
|
1666
|
-
prompts: prompts2.map((p) => ({
|
|
1667
|
-
name: p.name,
|
|
1668
|
-
description: p.description,
|
|
1669
|
-
arguments: p.arguments
|
|
1670
|
-
}))
|
|
1671
|
-
};
|
|
1672
|
-
});
|
|
1673
|
-
this.server.setRequestHandler(import_types.GetPromptRequestSchema, async (request) => {
|
|
1674
|
-
const prompt = getPrompt(request.params.name);
|
|
1675
|
-
if (!prompt) {
|
|
1676
|
-
throw new Error(`Prompt not found: ${request.params.name}`);
|
|
1677
|
-
}
|
|
1678
|
-
const args2 = request.params.arguments;
|
|
1679
|
-
const text = prompt.getPrompt(args2);
|
|
1680
|
-
return {
|
|
1681
|
-
description: prompt.description,
|
|
1682
|
-
messages: [
|
|
1683
|
-
{
|
|
1684
|
-
role: "user",
|
|
1685
|
-
content: {
|
|
1686
|
-
type: "text",
|
|
1687
|
-
text
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
]
|
|
1691
|
-
};
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
/**
|
|
1695
|
-
* Set or update the Channlize configuration
|
|
1696
|
-
*/
|
|
1697
|
-
setConfig(config2) {
|
|
1698
|
-
this.config = config2;
|
|
1193
|
+
this.config = resolveConfig(options.config ?? {});
|
|
1194
|
+
this.info = { name: options.name, version: options.version };
|
|
1195
|
+
this.server = buildServer(this.config, this.info);
|
|
1699
1196
|
}
|
|
1700
|
-
/**
|
|
1701
|
-
* Get the underlying MCP server instance
|
|
1702
|
-
*/
|
|
1197
|
+
/** The underlying `McpServer`. */
|
|
1703
1198
|
getServer() {
|
|
1704
1199
|
return this.server;
|
|
1705
1200
|
}
|
|
1706
|
-
/**
|
|
1707
|
-
* Start the server with stdio transport
|
|
1708
|
-
*/
|
|
1201
|
+
/** Serve over stdio (what desktop and IDE clients launch). */
|
|
1709
1202
|
async runStdio() {
|
|
1710
|
-
|
|
1711
|
-
await this.server.connect(transport);
|
|
1203
|
+
await this.server.connect(new import_stdio.StdioServerTransport());
|
|
1712
1204
|
}
|
|
1713
1205
|
/**
|
|
1714
|
-
*
|
|
1206
|
+
* Serve Streamable HTTP at `http://<host>:<port>/mcp` (default
|
|
1207
|
+
* 127.0.0.1:3333). Each request gets a fresh server; nothing is shared.
|
|
1715
1208
|
*/
|
|
1209
|
+
async runHttp(options = {}) {
|
|
1210
|
+
return startHttpServer(() => buildServer(this.config, this.info), options);
|
|
1211
|
+
}
|
|
1716
1212
|
async close() {
|
|
1717
1213
|
await this.server.close();
|
|
1718
1214
|
}
|
|
1719
1215
|
};
|
|
1720
1216
|
|
|
1721
1217
|
// src/server.ts
|
|
1722
|
-
var
|
|
1723
|
-
var getArg = (name) => {
|
|
1724
|
-
const index = args.findIndex(
|
|
1725
|
-
(arg) => arg === `--${name}` || arg === `-${name[0]}`
|
|
1726
|
-
);
|
|
1727
|
-
return index >= 0 && index + 1 < args.length ? args[index + 1] : void 0;
|
|
1728
|
-
};
|
|
1729
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
1730
|
-
console.log(`
|
|
1731
|
-
Channlize MCP Server
|
|
1218
|
+
var HELP = `Channlize MCP server ${VERSION}
|
|
1732
1219
|
|
|
1733
1220
|
Usage: channlize-mcp [options]
|
|
1734
1221
|
|
|
1222
|
+
Keys (flags override environment variables):
|
|
1223
|
+
--server-api-key <key> Server API key, ch_live_\u2026 CHANNLIZE_SERVER_API_KEY
|
|
1224
|
+
--widget-key <key> Public widget key, wk_\u2026 CHANNLIZE_WIDGET_KEY
|
|
1225
|
+
--api-key <key> Either kind, by prefix CHANNLIZE_API_KEY
|
|
1226
|
+
--project-id <id> Project (default: the key's first project)
|
|
1227
|
+
CHANNLIZE_PROJECT_ID
|
|
1735
1228
|
Options:
|
|
1736
|
-
--
|
|
1737
|
-
|
|
1738
|
-
--
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1229
|
+
--base-url <url> API URL (default https://api.channlize.com)
|
|
1230
|
+
CHANNLIZE_BASE_URL
|
|
1231
|
+
--timeout <ms> Request timeout (default 10000)
|
|
1232
|
+
CHANNLIZE_TIMEOUT
|
|
1233
|
+
--transport <t> stdio (default) or http CHANNLIZE_MCP_TRANSPORT
|
|
1234
|
+
--host <host> HTTP bind address (default 127.0.0.1)
|
|
1235
|
+
--port <port> HTTP port (default 3333) CHANNLIZE_MCP_PORT
|
|
1236
|
+
-v, --version Print the version
|
|
1237
|
+
-h, --help Show this help
|
|
1238
|
+
|
|
1239
|
+
The server API key enables the inbox, contact, Events and log tools (read
|
|
1240
|
+
scope for reads, write scope to send). The widget key adds visitor tools
|
|
1241
|
+
(contact form, chat). Create keys in the Channlize dashboard under
|
|
1242
|
+
SDK & Integration \u2192 API keys.
|
|
1243
|
+
|
|
1244
|
+
Claude Code:
|
|
1245
|
+
claude mcp add channlize --env CHANNLIZE_SERVER_API_KEY=ch_live_\u2026 -- npx -y @channlize/mcp
|
|
1246
|
+
|
|
1247
|
+
Docs: https://github.com/channlize/sdk/tree/main/packages/mcp
|
|
1248
|
+
`;
|
|
1249
|
+
function fail(message) {
|
|
1250
|
+
console.error(
|
|
1251
|
+
`channlize-mcp: ${message}
|
|
1756
1252
|
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1253
|
+
Run 'channlize-mcp --help' for usage.`
|
|
1254
|
+
);
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
async function main() {
|
|
1258
|
+
let values;
|
|
1259
|
+
try {
|
|
1260
|
+
({ values } = (0, import_node_util.parseArgs)({
|
|
1261
|
+
options: {
|
|
1262
|
+
"server-api-key": { type: "string" },
|
|
1263
|
+
"widget-key": { type: "string" },
|
|
1264
|
+
"api-key": { type: "string" },
|
|
1265
|
+
"project-id": { type: "string" },
|
|
1266
|
+
"base-url": { type: "string" },
|
|
1267
|
+
timeout: { type: "string" },
|
|
1268
|
+
transport: { type: "string" },
|
|
1269
|
+
host: { type: "string" },
|
|
1270
|
+
port: { type: "string" },
|
|
1271
|
+
help: { type: "boolean", short: "h" },
|
|
1272
|
+
version: { type: "boolean", short: "v" }
|
|
1273
|
+
},
|
|
1274
|
+
strict: true
|
|
1275
|
+
}));
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
fail(err.message);
|
|
1771
1278
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1279
|
+
if (values.help) {
|
|
1280
|
+
console.log(HELP);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (values.version) {
|
|
1284
|
+
console.log(VERSION);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const str = (k) => values[k];
|
|
1288
|
+
const env = configFromEnv();
|
|
1289
|
+
const timeout = str("timeout");
|
|
1290
|
+
const config = {
|
|
1291
|
+
serverApiKey: str("server-api-key") ?? env.serverApiKey,
|
|
1292
|
+
widgetKey: str("widget-key") ?? env.widgetKey,
|
|
1293
|
+
apiKey: str("api-key") ?? env.apiKey,
|
|
1294
|
+
projectId: str("project-id") ?? env.projectId,
|
|
1295
|
+
baseUrl: str("base-url") ?? env.baseUrl,
|
|
1296
|
+
timeout: timeout !== void 0 ? Number(timeout) : env.timeout
|
|
1297
|
+
};
|
|
1298
|
+
const transport = str("transport") ?? process.env.CHANNLIZE_MCP_TRANSPORT ?? "stdio";
|
|
1299
|
+
if (transport !== "stdio" && transport !== "http") {
|
|
1300
|
+
fail(`unknown transport "${transport}": use stdio or http`);
|
|
1301
|
+
}
|
|
1302
|
+
let server;
|
|
1303
|
+
try {
|
|
1304
|
+
server = new ChannlizeMCPServer({ config });
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
if (err instanceof ConfigError) fail(err.message);
|
|
1307
|
+
throw err;
|
|
1308
|
+
}
|
|
1309
|
+
if (transport === "http") {
|
|
1310
|
+
const port = Number(str("port") ?? process.env.CHANNLIZE_MCP_PORT ?? 3333);
|
|
1311
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1312
|
+
fail(`invalid port "${str("port") ?? process.env.CHANNLIZE_MCP_PORT}"`);
|
|
1784
1313
|
}
|
|
1314
|
+
const { url: url2, close } = await server.runHttp({ host: str("host"), port });
|
|
1315
|
+
console.error(`Channlize MCP server listening on ${url2}`);
|
|
1316
|
+
const stop2 = async () => {
|
|
1317
|
+
await close();
|
|
1318
|
+
process.exit(0);
|
|
1319
|
+
};
|
|
1320
|
+
process.on("SIGINT", stop2);
|
|
1321
|
+
process.on("SIGTERM", stop2);
|
|
1322
|
+
return;
|
|
1785
1323
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
Tools:
|
|
1795
|
-
submit_contact_form - Submit a contact form inquiry
|
|
1796
|
-
create_chat_session - Create a new chat session
|
|
1797
|
-
send_message - Send a message in a chat session
|
|
1798
|
-
get_chat_messages - Retrieve chat message history
|
|
1799
|
-
close_chat_session - Close an active chat session
|
|
1800
|
-
|
|
1801
|
-
Prompts:
|
|
1802
|
-
integrate-contact-form - Guide for adding contact form to React app
|
|
1803
|
-
setup-live-chat - Guide for implementing live chat widget
|
|
1804
|
-
customize-styling - Guide for theming components
|
|
1805
|
-
handle-webhooks - Guide for webhook integration
|
|
1806
|
-
|
|
1807
|
-
For more information, visit: https://github.com/channlize/sdk
|
|
1808
|
-
`);
|
|
1809
|
-
process.exit(0);
|
|
1810
|
-
}
|
|
1811
|
-
var config = {
|
|
1812
|
-
apiKey: getArg("api-key") || process.env.CHANNLIZE_API_KEY || "",
|
|
1813
|
-
projectId: getArg("project-id") || process.env.CHANNLIZE_PROJECT_ID,
|
|
1814
|
-
baseUrl: getArg("base-url") || process.env.CHANNLIZE_BASE_URL,
|
|
1815
|
-
timeout: parseInt(
|
|
1816
|
-
getArg("timeout") || process.env.CHANNLIZE_TIMEOUT || "10000",
|
|
1817
|
-
10
|
|
1818
|
-
)
|
|
1819
|
-
};
|
|
1820
|
-
if (!config.apiKey) {
|
|
1821
|
-
console.error(`
|
|
1822
|
-
Error: Channlize API key is required
|
|
1823
|
-
|
|
1824
|
-
Please provide your API key using either:
|
|
1825
|
-
--api-key <key>
|
|
1826
|
-
or set CHANNLIZE_API_KEY environment variable
|
|
1827
|
-
|
|
1828
|
-
Get your API key from your Channlize dashboard.
|
|
1829
|
-
|
|
1830
|
-
Run 'channlize-mcp --help' for more information.
|
|
1831
|
-
`);
|
|
1832
|
-
process.exit(1);
|
|
1324
|
+
const stop = async () => {
|
|
1325
|
+
await server.close();
|
|
1326
|
+
process.exit(0);
|
|
1327
|
+
};
|
|
1328
|
+
process.on("SIGINT", stop);
|
|
1329
|
+
process.on("SIGTERM", stop);
|
|
1330
|
+
await server.runStdio();
|
|
1833
1331
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
version: "0.1.0",
|
|
1837
|
-
config
|
|
1838
|
-
});
|
|
1839
|
-
process.on("SIGINT", async () => {
|
|
1840
|
-
await server.close();
|
|
1841
|
-
process.exit(0);
|
|
1842
|
-
});
|
|
1843
|
-
process.on("SIGTERM", async () => {
|
|
1844
|
-
await server.close();
|
|
1845
|
-
process.exit(0);
|
|
1846
|
-
});
|
|
1847
|
-
server.runStdio().catch((error) => {
|
|
1848
|
-
console.error("Failed to start Channlize MCP server:", error);
|
|
1332
|
+
main().catch((err) => {
|
|
1333
|
+
console.error("Failed to start the Channlize MCP server:", err.message);
|
|
1849
1334
|
process.exit(1);
|
|
1850
1335
|
});
|
|
1851
1336
|
//# sourceMappingURL=server.js.map
|