@solumflow-app/crm-mcp-server 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ascensie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { createClient } from "@solumflow-app/crm-client";
7
+
8
+ // src/catalog-tools.ts
9
+ import { z } from "zod";
10
+
11
+ // src/tool-answers.ts
12
+ import { CrmApiError } from "@solumflow-app/crm-client";
13
+ function answer(text, data) {
14
+ return {
15
+ content: [{ type: "text", text }],
16
+ structuredContent: { result: data }
17
+ };
18
+ }
19
+ function refusal(error) {
20
+ if (!(error instanceof CrmApiError)) {
21
+ throw error;
22
+ }
23
+ const lines = [`${error.code}: ${error.message}`];
24
+ const next = NEXT_STEP[error.code];
25
+ if (next) {
26
+ lines.push(next);
27
+ }
28
+ for (const field of error.fields) {
29
+ lines.push(`- ${field.path}: ${field.message}`);
30
+ }
31
+ return {
32
+ isError: true,
33
+ content: [{ type: "text", text: lines.join("\n") }]
34
+ };
35
+ }
36
+ var NEXT_STEP = {
37
+ unauthorized: "The key is not recognised or was revoked. Ask the account owner for a new secret key.",
38
+ forbidden: 'The key may not do this. It needs to be a secret key with the scope "website:manage".',
39
+ feature_unavailable: "The module this needs is switched off for the account. Ask somebody to switch it on in the app.",
40
+ rate_limited: "Too many requests. Wait a moment and try again.",
41
+ internal_error: "Something went wrong in the app. Trying again is safe."
42
+ };
43
+ function codeBlock(snippet) {
44
+ return `\`\`\`html
45
+ ${snippet}
46
+ \`\`\``;
47
+ }
48
+
49
+ // src/catalog-tools.ts
50
+ var CONTENT_BLOCKS = 'Besides plain values (text, number, choice), a product can carry content blocks, three field types: "content_text" is { html, links? } with already-sanitised html; "cta" is { heading, body, primary: {label,url}, secondary? }, a call to action; "repeater" is { items: [{ id, ...one value per item field }] } and covers lists, accordions (FAQ), downloads and testimonials. A repeater\u2019s field says how to draw it: "display" is "list" or "accordion", "preset" names where it started (list, faq, downloads, testimonials), "itemFields" is the structure of one item (key, label, type: text, richtext, url {label,url}, icon {set,name}, image or file {url,name,size,contentType}) and "titleKey" is the item field that names an item, such as the question of a FAQ. A block that is public but empty is null; a block that is not public is not there at all.';
51
+ var slugOrId = z.string().min(1).max(200).describe("The address (slug) or the id of the product.");
52
+ function registerCatalogTools(server, client) {
53
+ server.registerTool(
54
+ "list_catalog_fields",
55
+ {
56
+ title: "List the public fields of the catalogue",
57
+ description: "The custom fields the catalogue publishes on its products, with their type, choices and, for a content block, how it is built. Read this once before building a product page: it says which blocks exist and what each one looks like. " + CONTENT_BLOCKS,
58
+ inputSchema: {},
59
+ annotations: { readOnlyHint: true, idempotentHint: true }
60
+ },
61
+ async () => {
62
+ try {
63
+ const fields = await client.getFields();
64
+ return answer(
65
+ fields.length === 0 ? "The catalogue publishes no custom fields." : fields.map(describeField).join("\n"),
66
+ fields
67
+ );
68
+ } catch (error) {
69
+ return refusal(error);
70
+ }
71
+ }
72
+ );
73
+ server.registerTool(
74
+ "get_product",
75
+ {
76
+ title: "Read one product",
77
+ description: "One product as its page needs it: name, prices, pictures, categories, variants and `fields`, keyed by field slug. May be a minute old. Answers that it is not found for an unknown product. " + CONTENT_BLOCKS,
78
+ inputSchema: { slugOrId },
79
+ annotations: { readOnlyHint: true, idempotentHint: true }
80
+ },
81
+ async ({ slugOrId: wanted }) => {
82
+ try {
83
+ const product = await client.getProduct(wanted);
84
+ if (!product) {
85
+ return answer(`No product "${wanted}" on this account.`, null);
86
+ }
87
+ const slugs = Object.keys(product.fields);
88
+ return answer(
89
+ `${product.name} (${product.slug}), ${slugs.length} public fields: ${slugs.join(", ") || "none"}.`,
90
+ product
91
+ );
92
+ } catch (error) {
93
+ return refusal(error);
94
+ }
95
+ }
96
+ );
97
+ }
98
+ function describeField(field) {
99
+ if (field.type !== "repeater") {
100
+ return `- ${field.slug}: ${field.label}, ${field.type}`;
101
+ }
102
+ const items = (field.itemFields ?? []).map(({ key, type }) => `${key}:${type}`).join(", ");
103
+ const preset = field.preset ? `, ${field.preset}` : "";
104
+ const title = field.titleKey ? `; titled by ${field.titleKey}` : "";
105
+ return `- ${field.slug}: ${field.label}, repeater (${field.display ?? "list"}${preset}; items: ${items}${title})`;
106
+ }
107
+
108
+ // src/placement-tools.ts
109
+ import { z as z2 } from "zod";
110
+ var REACT_HINT = "On a React or Next.js site, prefer the components in `@solumflow-app/crm-client/react` (CrmForm, CrmBooking, CrmChatWidget, CrmSiteTracking) over pasting HTML.";
111
+ var page = {
112
+ cursor: z2.string().optional().describe("The nextCursor of the previous page; leave out for the first."),
113
+ limit: z2.number().int().min(1).max(100).optional()
114
+ };
115
+ var origin = z2.string().regex(/^https?:\/\/[a-zA-Z0-9.-]+(:[0-9]{1,5})?$/).describe("Scheme, host and optional port; no path: https://www.example.com");
116
+ function registerPlacementTools(server, client) {
117
+ server.registerTool(
118
+ "list_forms",
119
+ {
120
+ title: "List published forms",
121
+ description: "The published forms of the account, each with its own link and the HTML snippet that places it on a page. Forms are made and published by a person in the app\u2019s form builder; this cannot create one. If the form you need is missing, ask the user to build and publish it first. " + REACT_HINT,
122
+ inputSchema: page,
123
+ annotations: { readOnlyHint: true, idempotentHint: true }
124
+ },
125
+ async (input) => {
126
+ try {
127
+ const list = await client.getForms(input);
128
+ return answer(
129
+ list.data.length === 0 ? "No published forms. Ask the user to build and publish one in the app." : [...list.data.map(describeForm), more(list.nextCursor)].join(
130
+ "\n\n"
131
+ ),
132
+ list
133
+ );
134
+ } catch (error) {
135
+ return refusal(error);
136
+ }
137
+ }
138
+ );
139
+ server.registerTool(
140
+ "list_booking_pages",
141
+ {
142
+ title: "List booking pages",
143
+ description: "Every page a visitor can book on: the services overview, each category and service, and each appointment calendar. Each has a link; services, categories and the overview also have an embed snippet (an iframe plus a sizing script). A calendar has no embed: link to it. " + REACT_HINT,
144
+ inputSchema: {},
145
+ annotations: { readOnlyHint: true, idempotentHint: true }
146
+ },
147
+ async () => {
148
+ try {
149
+ const pages = await client.getBookingPages();
150
+ return answer(
151
+ pages.length === 0 ? "Nothing can be booked online on this account yet." : pages.map(describeBookingPage).join("\n\n"),
152
+ pages
153
+ );
154
+ } catch (error) {
155
+ return refusal(error);
156
+ }
157
+ }
158
+ );
159
+ server.registerTool(
160
+ "list_chat_widgets",
161
+ {
162
+ title: "List chat widgets",
163
+ description: "The chat widgets of the account, with the sites each may run on and, for a published one, the script tag that places it. An empty list of allowed origins means any site. " + REACT_HINT,
164
+ inputSchema: page,
165
+ annotations: { readOnlyHint: true, idempotentHint: true }
166
+ },
167
+ async (input) => {
168
+ try {
169
+ const list = await client.getChatWidgets(input);
170
+ return answer(
171
+ list.data.length === 0 ? "No chat widgets yet. upsert_chat_widget can make one." : [
172
+ ...list.data.map(describeChatWidget),
173
+ more(list.nextCursor)
174
+ ].join("\n\n"),
175
+ list
176
+ );
177
+ } catch (error) {
178
+ return refusal(error);
179
+ }
180
+ }
181
+ );
182
+ server.registerTool(
183
+ "upsert_chat_widget",
184
+ {
185
+ title: "Make or change a chat widget",
186
+ description: "Makes the chat widget at this address when there is none, or changes it. Sets its name, whether it is live, and which sites it may run on. How it looks and what it says is done in the app\u2019s builder. Sites are added and removed, never replaced. Careful: an empty list means ANY site, so adding the first origin narrows the widget to that site only, and removing an origin takes the chat off a site that may be live.",
187
+ inputSchema: {
188
+ slug: z2.string().regex(/^[a-z][a-z0-9-]*$/).max(100).describe(
189
+ 'The widget\u2019s address on this account, such as "website-chat".'
190
+ ),
191
+ name: z2.string().min(1).max(200).optional().describe("Required when the widget does not exist yet."),
192
+ status: z2.enum(["draft", "published"]).optional().describe("Only a published widget answers on a site."),
193
+ addAllowedOrigins: z2.array(origin).max(20).optional(),
194
+ removeAllowedOrigins: z2.array(origin).max(20).optional()
195
+ },
196
+ annotations: {
197
+ readOnlyHint: false,
198
+ destructiveHint: false,
199
+ idempotentHint: true
200
+ }
201
+ },
202
+ async ({ slug, ...input }) => {
203
+ try {
204
+ const widget = await client.upsertChatWidget(slug, input);
205
+ return answer(
206
+ `${widget.created ? "Made" : "Updated"} the widget.
207
+
208
+ ${describeChatWidget(widget)}`,
209
+ widget
210
+ );
211
+ } catch (error) {
212
+ return refusal(error);
213
+ }
214
+ }
215
+ );
216
+ server.registerTool(
217
+ "get_site_tracking",
218
+ {
219
+ title: "Get the tracking script",
220
+ description: "The website tracking script of the account: its tag, the hosts it counts visits from, and the consent it waits for. Answers that tracking is not set up when it never was; update_site_tracking sets it up. " + REACT_HINT,
221
+ inputSchema: {},
222
+ annotations: { readOnlyHint: true, idempotentHint: true }
223
+ },
224
+ async () => {
225
+ try {
226
+ const site = await client.getSiteTracking();
227
+ return answer(
228
+ site ? describeSiteTracking(site) : "Website tracking is not set up yet. update_site_tracking with the site\u2019s host sets it up.",
229
+ site
230
+ );
231
+ } catch (error) {
232
+ return refusal(error);
233
+ }
234
+ }
235
+ );
236
+ server.registerTool(
237
+ "update_site_tracking",
238
+ {
239
+ title: "Set up or change the tracking script",
240
+ description: "Sets website tracking up when it is not there, adds or removes the hosts it counts visits from, and changes the consent it waits for. An empty list of hosts counts nothing, so add the host of every site the script is placed on. Hosts are added and removed, never replaced.",
241
+ inputSchema: {
242
+ addAllowedHosts: z2.array(z2.string()).max(20).optional().describe('Hosts such as "www.example.com" or "*.example.com".'),
243
+ removeAllowedHosts: z2.array(z2.string()).max(20).optional(),
244
+ consentSignal: z2.enum([
245
+ "ad_storage",
246
+ "analytics_storage",
247
+ "personalization_storage",
248
+ "api_only"
249
+ ]).optional().describe(
250
+ "The consent the script waits for. api_only: it waits until the page tells it."
251
+ )
252
+ },
253
+ annotations: {
254
+ readOnlyHint: false,
255
+ destructiveHint: false,
256
+ idempotentHint: true
257
+ }
258
+ },
259
+ async (input) => {
260
+ try {
261
+ const site = await client.updateSiteTracking(input);
262
+ return answer(
263
+ `${site.created ? "Set tracking up." : "Updated tracking."}
264
+
265
+ ${describeSiteTracking(site)}`,
266
+ site
267
+ );
268
+ } catch (error) {
269
+ return refusal(error);
270
+ }
271
+ }
272
+ );
273
+ }
274
+ function describeForm(form) {
275
+ return [
276
+ `**${form.name}** (id ${form.id})`,
277
+ `Link: ${form.url}`,
278
+ codeBlock(form.embed)
279
+ ].join("\n");
280
+ }
281
+ function describeBookingPage(page2) {
282
+ return [
283
+ `**${page2.name}** (${page2.kind}${page2.id ? `, id ${page2.id}` : ""})`,
284
+ `Link: ${page2.url}`,
285
+ page2.embed ? codeBlock(page2.embed) : "No embed: link to it."
286
+ ].join("\n");
287
+ }
288
+ function describeChatWidget(widget) {
289
+ return [
290
+ `**${widget.name}** (${widget.slug}, ${widget.status}, id ${widget.id})`,
291
+ `Allowed sites: ${widget.allowedOrigins.length > 0 ? widget.allowedOrigins.join(", ") : "any site"}`,
292
+ widget.embed ? codeBlock(widget.embed) : "No snippet until the widget is published."
293
+ ].join("\n");
294
+ }
295
+ function describeSiteTracking(site) {
296
+ return [
297
+ `Hosts: ${site.allowedHosts.length > 0 ? site.allowedHosts.join(", ") : "none \u2014 no visit is counted until a host is added"}`,
298
+ `Waits for consent: ${site.consentSignal}`,
299
+ `Last visit received: ${site.lastReceivedAt ?? "never"}`,
300
+ codeBlock(site.embed)
301
+ ].join("\n");
302
+ }
303
+ function more(nextCursor) {
304
+ return nextCursor ? `More on the next page: cursor ${nextCursor}` : "";
305
+ }
306
+
307
+ // src/server-config.ts
308
+ function readServerConfig(env) {
309
+ const apiKey = env.SOLUMFLOW_API_KEY?.trim() ?? "";
310
+ const baseUrl = env.SOLUMFLOW_API_URL?.trim().replace(/\/+$/, "") ?? "";
311
+ if (!apiKey) {
312
+ return {
313
+ ok: false,
314
+ error: 'SOLUMFLOW_API_KEY is not set. Make a secret key with the scope "website:manage" in the app (Settings \u2192 API keys) and pass it in the environment.'
315
+ };
316
+ }
317
+ if (!apiKey.startsWith("crms_")) {
318
+ return {
319
+ ok: false,
320
+ error: 'SOLUMFLOW_API_KEY is not a secret key. Placing things on a website needs a `crms_` key with the scope "website:manage"; a `crmp_` key can only read the catalogue.'
321
+ };
322
+ }
323
+ if (!/^https?:\/\/[^/]+$/.test(baseUrl)) {
324
+ return {
325
+ ok: false,
326
+ error: 'SOLUMFLOW_API_URL is not set to the app\u2019s address, such as "https://app.example.com" (an origin, no path).'
327
+ };
328
+ }
329
+ return { ok: true, apiKey, baseUrl };
330
+ }
331
+
332
+ // src/index.ts
333
+ async function main() {
334
+ const config = readServerConfig(process.env);
335
+ if (!config.ok) {
336
+ console.error(`crm-mcp-server: ${config.error}`);
337
+ process.exit(1);
338
+ }
339
+ const server = new McpServer({
340
+ name: "crm-mcp-server",
341
+ version: "0.2.0"
342
+ });
343
+ const client = createClient({
344
+ apiKey: config.apiKey,
345
+ baseUrl: config.baseUrl
346
+ });
347
+ registerPlacementTools(server, client);
348
+ registerCatalogTools(server, client);
349
+ await server.connect(new StdioServerTransport());
350
+ }
351
+ main().catch((error) => {
352
+ console.error("crm-mcp-server:", error);
353
+ process.exit(1);
354
+ });
355
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/catalog-tools.ts","../src/tool-answers.ts","../src/placement-tools.ts","../src/server-config.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { createClient } from '@solumflow-app/crm-client';\n\nimport { registerCatalogTools } from './catalog-tools';\nimport { registerPlacementTools } from './placement-tools';\nimport { readServerConfig } from './server-config';\n\n/**\n * An MCP server over stdio that lets an agent place an account's forms,\n * booking pages, chat and tracking on a website, and read the catalogue's\n * fields and products to build their pages. It holds no rules of its own:\n * every tool is one call of the public API through the published client.\n *\n * Errors go to stderr; stdout belongs to the protocol.\n */\nasync function main() {\n const config = readServerConfig(process.env);\n\n if (!config.ok) {\n console.error(`crm-mcp-server: ${config.error}`);\n process.exit(1);\n }\n\n const server = new McpServer({\n name: 'crm-mcp-server',\n version: '0.2.0',\n });\n\n const client = createClient({\n apiKey: config.apiKey,\n baseUrl: config.baseUrl,\n });\n\n registerPlacementTools(server, client);\n registerCatalogTools(server, client);\n\n await server.connect(new StdioServerTransport());\n}\n\nmain().catch((error: unknown) => {\n console.error('crm-mcp-server:', error);\n process.exit(1);\n});\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CatalogField, CrmClient } from '@solumflow-app/crm-client';\nimport { z } from 'zod';\n\nimport { answer, refusal } from './tool-answers';\n\n/** The part of the client these tools call; a test hands in a fake of exactly this. */\nexport type CatalogClient = Pick<CrmClient, 'getFields' | 'getProduct'>;\n\n/**\n * What an agent needs to know about the content blocks of a product, without\n * reading documentation. Said once, in both tool descriptions: the field list\n * tells how each block is built, the product carries what is in it.\n */\nconst CONTENT_BLOCKS =\n 'Besides plain values (text, number, choice), a product can carry content ' +\n 'blocks, three field types: \"content_text\" is { html, links? } with ' +\n 'already-sanitised html; \"cta\" is { heading, body, primary: {label,url}, ' +\n 'secondary? }, a call to action; \"repeater\" is { items: [{ id, ...one ' +\n 'value per item field }] } and covers lists, accordions (FAQ), downloads ' +\n 'and testimonials. A repeater’s field says how to draw it: \"display\" is ' +\n '\"list\" or \"accordion\", \"preset\" names where it started (list, faq, ' +\n 'downloads, testimonials), \"itemFields\" is the structure of one item ' +\n '(key, label, type: text, richtext, url {label,url}, icon {set,name}, ' +\n 'image or file {url,name,size,contentType}) and \"titleKey\" is the item ' +\n 'field that names an item, such as the question of a FAQ. A block that is ' +\n 'public but empty is null; a block that is not public is not there at all.';\n\nconst slugOrId = z\n .string()\n .min(1)\n .max(200)\n .describe('The address (slug) or the id of the product.');\n\nexport function registerCatalogTools(server: McpServer, client: CatalogClient) {\n server.registerTool(\n 'list_catalog_fields',\n {\n title: 'List the public fields of the catalogue',\n description:\n 'The custom fields the catalogue publishes on its products, with their ' +\n 'type, choices and, for a content block, how it is built. Read this ' +\n 'once before building a product page: it says which blocks exist and ' +\n 'what each one looks like. ' +\n CONTENT_BLOCKS,\n inputSchema: {},\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async () => {\n try {\n const fields = await client.getFields();\n\n return answer(\n fields.length === 0\n ? 'The catalogue publishes no custom fields.'\n : fields.map(describeField).join('\\n'),\n fields,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n\n server.registerTool(\n 'get_product',\n {\n title: 'Read one product',\n description:\n 'One product as its page needs it: name, prices, pictures, ' +\n 'categories, variants and `fields`, keyed by field slug. May be a ' +\n 'minute old. Answers that it is not found for an unknown product. ' +\n CONTENT_BLOCKS,\n inputSchema: { slugOrId },\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async ({ slugOrId: wanted }) => {\n try {\n const product = await client.getProduct(wanted);\n\n if (!product) {\n return answer(`No product \"${wanted}\" on this account.`, null);\n }\n\n const slugs = Object.keys(product.fields);\n\n return answer(\n `${product.name} (${product.slug}), ${slugs.length} public fields: ${slugs.join(', ') || 'none'}.`,\n product,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n}\n\nfunction describeField(field: CatalogField) {\n if (field.type !== 'repeater') {\n return `- ${field.slug}: ${field.label}, ${field.type}`;\n }\n\n const items = (field.itemFields ?? [])\n .map(({ key, type }) => `${key}:${type}`)\n .join(', ');\n const preset = field.preset ? `, ${field.preset}` : '';\n const title = field.titleKey ? `; titled by ${field.titleKey}` : '';\n\n return `- ${field.slug}: ${field.label}, repeater (${field.display ?? 'list'}${preset}; items: ${items}${title})`;\n}\n","import { CrmApiError } from '@solumflow-app/crm-client';\n\n/**\n * A tool's answer: a short text for the model to read, and the API's own JSON\n * beside it for a client that wants the fields.\n */\nexport function answer(text: string, data: unknown) {\n return {\n content: [{ type: 'text' as const, text }],\n structuredContent: { result: data },\n };\n}\n\n/**\n * A refusal from the API, said in a way an agent can act on. The code is\n * kept, so the agent can tell \"the module is off\" from \"the key cannot do\n * this\"; the fields name what in the input was wrong.\n */\nexport function refusal(error: unknown) {\n if (!(error instanceof CrmApiError)) {\n throw error;\n }\n\n const lines = [`${error.code}: ${error.message}`];\n const next = NEXT_STEP[error.code];\n\n if (next) {\n lines.push(next);\n }\n\n for (const field of error.fields) {\n lines.push(`- ${field.path}: ${field.message}`);\n }\n\n return {\n isError: true,\n content: [{ type: 'text' as const, text: lines.join('\\n') }],\n };\n}\n\nconst NEXT_STEP: Partial<Record<CrmApiError['code'], string>> = {\n unauthorized:\n 'The key is not recognised or was revoked. Ask the account owner for a new secret key.',\n forbidden:\n 'The key may not do this. It needs to be a secret key with the scope \"website:manage\".',\n feature_unavailable:\n 'The module this needs is switched off for the account. Ask somebody to switch it on in the app.',\n rate_limited: 'Too many requests. Wait a moment and try again.',\n internal_error: 'Something went wrong in the app. Trying again is safe.',\n};\n\n/** A snippet as a fenced block, so a model copies it whole. */\nexport function codeBlock(snippet: string) {\n return `\\`\\`\\`html\\n${snippet}\\n\\`\\`\\``;\n}\n","import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type {\n CrmClient,\n PlacementBookingPage,\n PlacementChatWidget,\n PlacementForm,\n PlacementSiteTracking,\n} from '@solumflow-app/crm-client';\nimport { z } from 'zod';\n\nimport { answer, codeBlock, refusal } from './tool-answers';\n\n/** The part of the client these tools call; a test hands in a fake of exactly this. */\nexport type PlacementClient = Pick<\n CrmClient,\n | 'getForms'\n | 'getChatWidgets'\n | 'upsertChatWidget'\n | 'getSiteTracking'\n | 'updateSiteTracking'\n | 'getBookingPages'\n>;\n\nconst REACT_HINT =\n 'On a React or Next.js site, prefer the components in ' +\n '`@solumflow-app/crm-client/react` (CrmForm, CrmBooking, CrmChatWidget, ' +\n 'CrmSiteTracking) over pasting HTML.';\n\nconst page = {\n cursor: z\n .string()\n .optional()\n .describe('The nextCursor of the previous page; leave out for the first.'),\n limit: z.number().int().min(1).max(100).optional(),\n};\n\nconst origin = z\n .string()\n .regex(/^https?:\\/\\/[a-zA-Z0-9.-]+(:[0-9]{1,5})?$/)\n .describe('Scheme, host and optional port; no path: https://www.example.com');\n\n/**\n * One tool per route of the public API, nothing in between: every rule about\n * what may be read or changed is the API's, and this only says it in a way an\n * agent can follow. The annotations describe; the key's scope is what\n * enforces.\n */\nexport function registerPlacementTools(\n server: McpServer,\n client: PlacementClient,\n) {\n server.registerTool(\n 'list_forms',\n {\n title: 'List published forms',\n description:\n 'The published forms of the account, each with its own link and the ' +\n 'HTML snippet that places it on a page. Forms are made and published ' +\n 'by a person in the app’s form builder; this cannot create one. If the ' +\n 'form you need is missing, ask the user to build and publish it first. ' +\n REACT_HINT,\n inputSchema: page,\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async (input) => {\n try {\n const list = await client.getForms(input);\n\n return answer(\n list.data.length === 0\n ? 'No published forms. Ask the user to build and publish one in the app.'\n : [...list.data.map(describeForm), more(list.nextCursor)].join(\n '\\n\\n',\n ),\n list,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n\n server.registerTool(\n 'list_booking_pages',\n {\n title: 'List booking pages',\n description:\n 'Every page a visitor can book on: the services overview, each ' +\n 'category and service, and each appointment calendar. Each has a link; ' +\n 'services, categories and the overview also have an embed snippet ' +\n '(an iframe plus a sizing script). A calendar has no embed: link to it. ' +\n REACT_HINT,\n inputSchema: {},\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async () => {\n try {\n const pages = await client.getBookingPages();\n\n return answer(\n pages.length === 0\n ? 'Nothing can be booked online on this account yet.'\n : pages.map(describeBookingPage).join('\\n\\n'),\n pages,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n\n server.registerTool(\n 'list_chat_widgets',\n {\n title: 'List chat widgets',\n description:\n 'The chat widgets of the account, with the sites each may run on and, ' +\n 'for a published one, the script tag that places it. An empty list of ' +\n 'allowed origins means any site. ' +\n REACT_HINT,\n inputSchema: page,\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async (input) => {\n try {\n const list = await client.getChatWidgets(input);\n\n return answer(\n list.data.length === 0\n ? 'No chat widgets yet. upsert_chat_widget can make one.'\n : [\n ...list.data.map(describeChatWidget),\n more(list.nextCursor),\n ].join('\\n\\n'),\n list,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n\n server.registerTool(\n 'upsert_chat_widget',\n {\n title: 'Make or change a chat widget',\n description:\n 'Makes the chat widget at this address when there is none, or changes ' +\n 'it. Sets its name, whether it is live, and which sites it may run on. ' +\n 'How it looks and what it says is done in the app’s builder. Sites are ' +\n 'added and removed, never replaced. Careful: an empty list means ANY ' +\n 'site, so adding the first origin narrows the widget to that site only, ' +\n 'and removing an origin takes the chat off a site that may be live.',\n inputSchema: {\n slug: z\n .string()\n .regex(/^[a-z][a-z0-9-]*$/)\n .max(100)\n .describe(\n 'The widget’s address on this account, such as \"website-chat\".',\n ),\n name: z\n .string()\n .min(1)\n .max(200)\n .optional()\n .describe('Required when the widget does not exist yet.'),\n status: z\n .enum(['draft', 'published'])\n .optional()\n .describe('Only a published widget answers on a site.'),\n addAllowedOrigins: z.array(origin).max(20).optional(),\n removeAllowedOrigins: z.array(origin).max(20).optional(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n },\n },\n async ({ slug, ...input }) => {\n try {\n const widget = await client.upsertChatWidget(slug, input);\n\n return answer(\n `${widget.created ? 'Made' : 'Updated'} the widget.\\n\\n${describeChatWidget(widget)}`,\n widget,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n\n server.registerTool(\n 'get_site_tracking',\n {\n title: 'Get the tracking script',\n description:\n 'The website tracking script of the account: its tag, the hosts it ' +\n 'counts visits from, and the consent it waits for. Answers that ' +\n 'tracking is not set up when it never was; update_site_tracking sets ' +\n 'it up. ' +\n REACT_HINT,\n inputSchema: {},\n annotations: { readOnlyHint: true, idempotentHint: true },\n },\n async () => {\n try {\n const site = await client.getSiteTracking();\n\n return answer(\n site\n ? describeSiteTracking(site)\n : 'Website tracking is not set up yet. update_site_tracking with the site’s host sets it up.',\n site,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n\n server.registerTool(\n 'update_site_tracking',\n {\n title: 'Set up or change the tracking script',\n description:\n 'Sets website tracking up when it is not there, adds or removes the ' +\n 'hosts it counts visits from, and changes the consent it waits for. An ' +\n 'empty list of hosts counts nothing, so add the host of every site the ' +\n 'script is placed on. Hosts are added and removed, never replaced.',\n inputSchema: {\n addAllowedHosts: z\n .array(z.string())\n .max(20)\n .optional()\n .describe('Hosts such as \"www.example.com\" or \"*.example.com\".'),\n removeAllowedHosts: z.array(z.string()).max(20).optional(),\n consentSignal: z\n .enum([\n 'ad_storage',\n 'analytics_storage',\n 'personalization_storage',\n 'api_only',\n ])\n .optional()\n .describe(\n 'The consent the script waits for. api_only: it waits until the page tells it.',\n ),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n },\n },\n async (input) => {\n try {\n const site = await client.updateSiteTracking(input);\n\n return answer(\n `${site.created ? 'Set tracking up.' : 'Updated tracking.'}\\n\\n${describeSiteTracking(site)}`,\n site,\n );\n } catch (error) {\n return refusal(error);\n }\n },\n );\n}\n\nfunction describeForm(form: PlacementForm) {\n return [\n `**${form.name}** (id ${form.id})`,\n `Link: ${form.url}`,\n codeBlock(form.embed),\n ].join('\\n');\n}\n\nfunction describeBookingPage(page: PlacementBookingPage) {\n return [\n `**${page.name}** (${page.kind}${page.id ? `, id ${page.id}` : ''})`,\n `Link: ${page.url}`,\n page.embed ? codeBlock(page.embed) : 'No embed: link to it.',\n ].join('\\n');\n}\n\nfunction describeChatWidget(widget: PlacementChatWidget) {\n return [\n `**${widget.name}** (${widget.slug}, ${widget.status}, id ${widget.id})`,\n `Allowed sites: ${\n widget.allowedOrigins.length > 0\n ? widget.allowedOrigins.join(', ')\n : 'any site'\n }`,\n widget.embed\n ? codeBlock(widget.embed)\n : 'No snippet until the widget is published.',\n ].join('\\n');\n}\n\nfunction describeSiteTracking(site: PlacementSiteTracking) {\n return [\n `Hosts: ${\n site.allowedHosts.length > 0\n ? site.allowedHosts.join(', ')\n : 'none — no visit is counted until a host is added'\n }`,\n `Waits for consent: ${site.consentSignal}`,\n `Last visit received: ${site.lastReceivedAt ?? 'never'}`,\n codeBlock(site.embed),\n ].join('\\n');\n}\n\nfunction more(nextCursor: string | null) {\n return nextCursor ? `More on the next page: cursor ${nextCursor}` : '';\n}\n","/**\n * Where the server finds its account: a secret key and the app's address,\n * both from the environment. Every tool needs `website:manage`, which only a\n * secret (`crms_`) key can hold, so a publishable key is refused here rather\n * than on the first call.\n *\n * The address is required, not defaulted. The same server talks to a\n * production app, a reseller's own domain or a local one, and guessing the\n * wrong one sends the key somewhere it was never meant to go.\n */\nexport function readServerConfig(env: Record<string, string | undefined>) {\n const apiKey = env.SOLUMFLOW_API_KEY?.trim() ?? '';\n const baseUrl = env.SOLUMFLOW_API_URL?.trim().replace(/\\/+$/, '') ?? '';\n\n if (!apiKey) {\n return {\n ok: false as const,\n error:\n 'SOLUMFLOW_API_KEY is not set. Make a secret key with the scope ' +\n '\"website:manage\" in the app (Settings → API keys) and pass it in the ' +\n 'environment.',\n };\n }\n\n if (!apiKey.startsWith('crms_')) {\n return {\n ok: false as const,\n error:\n 'SOLUMFLOW_API_KEY is not a secret key. Placing things on a website ' +\n 'needs a `crms_` key with the scope \"website:manage\"; a `crmp_` key ' +\n 'can only read the catalogue.',\n };\n }\n\n if (!/^https?:\\/\\/[^/]+$/.test(baseUrl)) {\n return {\n ok: false as const,\n error:\n 'SOLUMFLOW_API_URL is not set to the app’s address, such as ' +\n '\"https://app.example.com\" (an origin, no path).',\n };\n }\n\n return { ok: true as const, apiKey, baseUrl };\n}\n"],"mappings":"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","names":["z","z","page"]}
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@solumflow-app/crm-mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Let an AI agent place an account's forms, booking pages, chat and tracking on a website and read its catalogue, through the public API.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "crm-mcp-server": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "type": "module",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "1.30.0",
18
+ "@solumflow-app/crm-client": "0.3.0",
19
+ "zod": "4.5.4"
20
+ },
21
+ "devDependencies": {
22
+ "@kit/tsconfig": "0.1.0",
23
+ "@types/node": "26.4.1",
24
+ "tsup": "8.5.1",
25
+ "typescript": "^7.0.2",
26
+ "vitest": "^4.1.11"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "scripts": {
32
+ "clean": "git clean -xdf .turbo node_modules dist",
33
+ "build": "tsup",
34
+ "typecheck": "tsc --noEmit",
35
+ "test:unit": "vitest run"
36
+ }
6
37
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.