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