@kerabie/mcp 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,56 @@
1
- # Temporary Holding Version
1
+ # @kerabie/mcp
2
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.
3
+ An [MCP](https://modelcontextprotocol.io) server for [Kerabie](https://kerabie.com).
4
+
5
+ - **Build with Kerabie, no API key needed:** search the docs, list REST endpoints, read the TypeScript and React Native SDK types, and get integration guides (`typescript`, `react-native`, `wordpress`, `web-widget`, `rest-api`). Answers come from docs bundled in the package, so they match the SDK version you install.
6
+ - **Work with your account, with `KERABIE_API_KEY`:** conversations, tickets, contacts, campaigns, the Knowledge Base, analytics, AI tools and the MCP servers your chatbot uses. A thin layer over `@kerabie/sdk` and the `/v1` API, so it can do exactly what your key can.
7
+
8
+ ## Hosted (no install)
9
+
10
+ ```
11
+ POST https://mcp.kerabie.com/mcp
12
+ Authorization: Bearer ker_live_...
13
+ ```
14
+
15
+ Stateless Streamable HTTP. The caller's key is used for every call and is never stored; with no key only the developer tools are offered; `?readOnly=true` leaves out tools that change data. To host it yourself run `kerabie-mcp-http` (`PORT`, optional `KERABIE_BASE_URL`; `GET /health` for probes) behind TLS.
16
+
17
+ ## Use locally (stdio)
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "kerabie": {
23
+ "command": "npx",
24
+ "args": ["-y", "@kerabie/mcp"],
25
+ "env": { "KERABIE_API_KEY": "ker_live_..." }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ | Variable | |
32
+ |---|---|
33
+ | `KERABIE_API_KEY` | Optional. API key from Settings > API. Without it only the developer tools are offered. |
34
+ | `KERABIE_ORG_ID` | Optional. Only if the key covers several organisations. |
35
+ | `KERABIE_BASE_URL` | Optional. Defaults to `https://api.kerabie.com`. |
36
+ | `KERABIE_MCP_READ_ONLY` | `true` (or `--read-only`) offers only tools that read. |
37
+
38
+ Tools that change data carry the MCP `readOnlyHint: false` annotation, and deleting ones `destructiveHint: true`, so clients can ask before running them. Start read-only.
39
+
40
+ Full tool list and docs: https://kerabie.com/docs/sdks/mcp
41
+
42
+ ## Deploy (Cloudflare Workers)
43
+
44
+ `src/worker.ts` is the hosted endpoint; `wrangler.jsonc` serves it at `mcp.kerabie.com`. Every push to `main` runs the tests and deploys through `.github/workflows/deploy-worker.yml`, which needs two repository secrets: `CLOUDFLARE_API_TOKEN` (the "Edit Cloudflare Workers" template) and `CLOUDFLARE_ACCOUNT_ID`.
45
+
46
+ The build installs `@kerabie/sdk` from npm and uses the docs knowledge committed in `src/generated/`. After changing the docs or the SDK types, run `npm run sync` and commit the result.
47
+
48
+ ## Develop
49
+
50
+ ```bash
51
+ npm install
52
+ npm run sync # rebuild src/generated/knowledge.json from the docs, readmes and SDK types
53
+ npm run dev:worker # run the Worker locally with wrangler
54
+ npm test # in-process MCP client against the server, SDK faked
55
+ npm run build # dist/cli.js is the `kerabie-mcp` binary
56
+ ```
@@ -0,0 +1,86 @@
1
+ import {
2
+ createServer
3
+ } from "./chunk-72BI6YMN.js";
4
+
5
+ // src/common.ts
6
+ var MAX_BODY_BYTES = 1e6;
7
+ function bearerKey(header) {
8
+ const m = /^Bearer\s+(\S+)$/i.exec(header ?? "");
9
+ return m ? m[1] : null;
10
+ }
11
+ var CORS = {
12
+ "Access-Control-Allow-Origin": "*",
13
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
14
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, Mcp-Protocol-Version",
15
+ "Access-Control-Max-Age": "86400"
16
+ };
17
+
18
+ // src/http.ts
19
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20
+ import { KerAbieClient } from "@kerabie/sdk";
21
+ function send(res, status, body, headers = {}) {
22
+ res.writeHead(status, { "Content-Type": "application/json", ...CORS, ...headers });
23
+ res.end(JSON.stringify(body));
24
+ }
25
+ function readBody(req) {
26
+ return new Promise((resolve, reject) => {
27
+ let size = 0;
28
+ const chunks = [];
29
+ req.on("data", (c) => {
30
+ size += c.length;
31
+ if (size > MAX_BODY_BYTES) {
32
+ reject(Object.assign(new Error("Request body too large"), { status: 413 }));
33
+ req.destroy();
34
+ return;
35
+ }
36
+ chunks.push(c);
37
+ });
38
+ req.on("end", () => {
39
+ try {
40
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
41
+ } catch {
42
+ reject(Object.assign(new Error("Body must be JSON"), { status: 400 }));
43
+ }
44
+ });
45
+ req.on("error", reject);
46
+ });
47
+ }
48
+ function createHttpHandler(opts = {}) {
49
+ return async (req, res) => {
50
+ const url = new URL(req.url ?? "/", "http://localhost");
51
+ if (url.pathname === "/health") return send(res, 200, { ok: true });
52
+ if (url.pathname !== "/mcp") return send(res, 404, { error: "Not found. The MCP endpoint is POST /mcp." });
53
+ if (req.method === "OPTIONS") {
54
+ res.writeHead(204, CORS);
55
+ res.end();
56
+ return;
57
+ }
58
+ if (req.method !== "POST") return send(res, 405, { jsonrpc: "2.0", error: { code: -32e3, message: "Use POST." }, id: null }, { Allow: "POST, OPTIONS" });
59
+ let body;
60
+ try {
61
+ body = await readBody(req);
62
+ } catch (e) {
63
+ return send(res, e?.status ?? 400, { jsonrpc: "2.0", error: { code: -32700, message: e?.message ?? "Bad request" }, id: null });
64
+ }
65
+ const key = bearerKey(req.headers.authorization);
66
+ const client = key ? new KerAbieClient({ apiKey: key, baseUrl: opts.baseUrl }) : null;
67
+ const server = createServer(client, { readOnly: url.searchParams.get("readOnly") === "true", version: opts.version });
68
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0, enableJsonResponse: true });
69
+ res.on("close", () => {
70
+ void transport.close();
71
+ void server.close();
72
+ });
73
+ try {
74
+ await server.connect(transport);
75
+ for (const [k, v] of Object.entries(CORS)) res.setHeader(k, v);
76
+ await transport.handleRequest(req, res, body);
77
+ } catch {
78
+ if (!res.headersSent) send(res, 500, { jsonrpc: "2.0", error: { code: -32603, message: "Internal error" }, id: null });
79
+ }
80
+ };
81
+ }
82
+
83
+ export {
84
+ bearerKey,
85
+ createHttpHandler
86
+ };