@caelo-cms/mcp-server 0.1.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 ADDED
@@ -0,0 +1,61 @@
1
+ # `@caelo-cms/mcp-server`
2
+
3
+ MCP server exposing your Caelo CMS install's chat-runner as a single
4
+ `caelo_chat` tool. Lets Claude Code (or any MCP-aware client) drive
5
+ your install — read pages, propose edits, queue Owner-approval
6
+ proposals, summarise plugin data — without opening the browser.
7
+
8
+ This package is a thin shim. Every `caelo_chat` invocation becomes one
9
+ HTTPS POST against your admin install's `/api/mcp/chat` endpoint, which
10
+ dispatches into the same chat-runner that powers the live-edit overlay.
11
+
12
+ ## Install
13
+
14
+ In your Caelo install:
15
+
16
+ 1. Visit `/security/mcp` as an Owner.
17
+ 2. Click **New token**, give it a name (`claude-code`, `laptop`, `ci`,
18
+ etc.), optionally set an AI-spend cap (microcents), and copy the
19
+ bearer that's shown ONCE.
20
+ 3. The page renders the exact `claude mcp add` snippet — copy + run it.
21
+
22
+ Manual setup if your client isn't Claude Code:
23
+
24
+ ```bash
25
+ # stdio server invoked by your MCP-aware client
26
+ bunx @caelo-cms/mcp-server
27
+ ```
28
+
29
+ with these env vars set:
30
+
31
+ | Variable | Required | Notes |
32
+ |---|---|---|
33
+ | `CAELO_ADMIN_URL` | yes | `https://admin.example.com` — point at your install. |
34
+ | `CAELO_MCP_TOKEN` | yes | Bearer minted at `/security/mcp`. |
35
+
36
+ ## The `caelo_chat` tool
37
+
38
+ | Field | Type | Notes |
39
+ |---|---|---|
40
+ | `message` | string, required | What you want to say to the Caelo agent. |
41
+ | `chatSessionId` | uuid, optional | Continue an existing chat session. |
42
+ | `pageId` | uuid, optional | Bind a NEW chat to one page so the agent's page-context block populates. |
43
+
44
+ Output: assistant reply text + a JSON block with `chatSessionId` (for
45
+ the next call), `requestId` (click through to
46
+ `/security/audit/<requestId>` for the full audit trail), the structured
47
+ tool-call summaries the agent dispatched, the per-turn cost in
48
+ microcents, and a `pendingProposals` count so the agent can surface
49
+ "you have N things waiting for Owner approval".
50
+
51
+ ## Why a single tool?
52
+
53
+ Browse / publish / propose actions happen *through* the chat — "which
54
+ pages exist?" → the Caelo agent calls `pages.list` internally → answers
55
+ in text. Same auth surface, same RLS scoping, same audit trail, same
56
+ propose-execute Owner gate as the browser. The remote agent talks to a
57
+ human-equivalent agent, not to a programmatic API.
58
+
59
+ ## License
60
+
61
+ MPL-2.0.
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@caelo-cms/mcp-server",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "license": "MPL-2.0",
6
+ "description": "MCP server exposing the Caelo CMS chat-runner as a `caelo_chat` tool. Lets Claude Code (or any MCP-aware client) drive a Caelo install from outside the browser.",
7
+ "type": "module",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "bin": {
11
+ "caelo-mcp-server": "./src/index.ts"
12
+ },
13
+ "exports": {
14
+ ".": "./src/index.ts"
15
+ },
16
+ "scripts": {
17
+ "typecheck": "tsc -b"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "1.29.0",
21
+ "zod": "4.3.6"
22
+ }
23
+ }
@@ -0,0 +1,73 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * HTTP bridge — translates a `caelo_chat` MCP call into a single POST
5
+ * against the admin install's `/api/mcp/chat` endpoint. The endpoint
6
+ * dispatches `mcp.send_chat` (system-only op) which resolves the
7
+ * bearer to a Caelo actor and drives `runChatTurn`.
8
+ *
9
+ * 30-second client-side timeout — chat-runner turns longer than that
10
+ * are likely stuck (the runner has its own provider-call timeouts).
11
+ * MCP clients see a clean error instead of a hung connection.
12
+ */
13
+
14
+ export interface SendChatOpts {
15
+ readonly adminUrl: string;
16
+ readonly token: string;
17
+ readonly message: string;
18
+ readonly chatSessionId?: string;
19
+ readonly pageId?: string;
20
+ }
21
+
22
+ export interface SendChatResult {
23
+ readonly chatSessionId: string;
24
+ readonly requestId: string;
25
+ readonly assistant: string;
26
+ readonly toolCalls: ReadonlyArray<{ name: string; summary: string; succeeded: boolean }>;
27
+ readonly pendingProposals: number;
28
+ readonly costMicrocents: number;
29
+ }
30
+
31
+ const TIMEOUT_MS = 30_000;
32
+
33
+ export async function sendChat(opts: SendChatOpts): Promise<SendChatResult> {
34
+ const controller = new AbortController();
35
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
36
+ try {
37
+ const url = `${opts.adminUrl.replace(/\/+$/, "")}/api/mcp/chat`;
38
+ const res = await fetch(url, {
39
+ method: "POST",
40
+ headers: {
41
+ "content-type": "application/json",
42
+ "x-caelo-mcp-token": opts.token,
43
+ },
44
+ body: JSON.stringify({
45
+ message: opts.message,
46
+ chatSessionId: opts.chatSessionId,
47
+ pageId: opts.pageId,
48
+ }),
49
+ signal: controller.signal,
50
+ });
51
+ if (!res.ok) {
52
+ const body = await safeText(res);
53
+ throw new Error(`HTTP ${res.status}: ${body || res.statusText}`);
54
+ }
55
+ const json = (await res.json()) as SendChatResult;
56
+ return json;
57
+ } catch (e) {
58
+ if (e instanceof Error && e.name === "AbortError") {
59
+ throw new Error(`timeout after ${TIMEOUT_MS / 1000}s`);
60
+ }
61
+ throw e;
62
+ } finally {
63
+ clearTimeout(timer);
64
+ }
65
+ }
66
+
67
+ async function safeText(res: Response): Promise<string> {
68
+ try {
69
+ return (await res.text()).slice(0, 500);
70
+ } catch {
71
+ return "";
72
+ }
73
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bun
2
+ // SPDX-License-Identifier: MPL-2.0
3
+
4
+ /**
5
+ * @caelo-cms/mcp-server — entrypoint.
6
+ *
7
+ * Reads `CAELO_ADMIN_URL` + `CAELO_MCP_TOKEN` from env, builds an MCP
8
+ * server exposing exactly one tool (`caelo_chat`), and listens over
9
+ * stdio (Claude Code's default invocation pattern).
10
+ *
11
+ * Bridge model: this process is a thin shim. Every `caelo_chat` call
12
+ * becomes a single HTTP POST against the admin install's
13
+ * `/api/mcp/chat` endpoint, which dispatches into the chat-runner with
14
+ * the resolved actor identity. The MCP server NEVER touches the
15
+ * database directly — it just translates between MCP semantics and
16
+ * the admin's HTTP surface.
17
+ */
18
+
19
+ import { startMcpServer } from "./server.js";
20
+
21
+ const adminUrl = process.env["CAELO_ADMIN_URL"];
22
+ const token = process.env["CAELO_MCP_TOKEN"];
23
+
24
+ if (!adminUrl) {
25
+ console.error("CAELO_ADMIN_URL not set — point me at your admin install (e.g. https://admin.example.com)");
26
+ process.exit(2);
27
+ }
28
+ if (!token) {
29
+ console.error("CAELO_MCP_TOKEN not set — mint one at /security/mcp on your admin install");
30
+ process.exit(2);
31
+ }
32
+
33
+ await startMcpServer({ adminUrl, token });
package/src/server.ts ADDED
@@ -0,0 +1,135 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * MCP server construction. Registers exactly one tool — `caelo_chat` —
5
+ * and binds it to a stdio transport. The single-tool design is
6
+ * deliberate: the remote agent talks to Caelo's chat-runner the same
7
+ * way a human in the browser does. Browse / publish / propose actions
8
+ * happen through the chat ("which pages exist?" → agent calls
9
+ * pages.list internally → text response). Same auth surface, same
10
+ * actor scope, same audit trail.
11
+ */
12
+
13
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import {
16
+ CallToolRequestSchema,
17
+ ListToolsRequestSchema,
18
+ } from "@modelcontextprotocol/sdk/types.js";
19
+ import { z } from "zod";
20
+ import { sendChat } from "./chat-bridge.js";
21
+
22
+ export interface StartOpts {
23
+ readonly adminUrl: string;
24
+ readonly token: string;
25
+ }
26
+
27
+ const caeloChatInputSchema = z
28
+ .object({
29
+ message: z.string().min(1).max(50_000),
30
+ chatSessionId: z.string().uuid().optional(),
31
+ pageId: z.string().uuid().optional(),
32
+ })
33
+ .strict();
34
+
35
+ export async function startMcpServer(opts: StartOpts): Promise<void> {
36
+ const server = new Server(
37
+ {
38
+ name: "caelo-mcp-server",
39
+ version: "0.1.0",
40
+ },
41
+ {
42
+ capabilities: { tools: {} },
43
+ },
44
+ );
45
+
46
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
47
+ tools: [
48
+ {
49
+ name: "caelo_chat",
50
+ description:
51
+ "Talk to your Caelo CMS install's AI agent. Same agent that powers the live-edit chat overlay — it can read pages, propose edits, queue Owner-approval proposals, summarise plugin data. Continues an existing chat session (when chatSessionId is supplied) or starts a fresh one. Returns the agent's reply text plus structured tool-call summaries.",
52
+ inputSchema: {
53
+ type: "object",
54
+ required: ["message"],
55
+ properties: {
56
+ message: {
57
+ type: "string",
58
+ description: "What you want to say to the Caelo agent.",
59
+ },
60
+ chatSessionId: {
61
+ type: "string",
62
+ description:
63
+ "Optional. Chat session UUID to continue. Omit to start a fresh page-unbound chat.",
64
+ },
65
+ pageId: {
66
+ type: "string",
67
+ description:
68
+ "Optional. Bind a NEW chat to one page so the agent's page-context block is populated. Same shape as /edit?page=<id>.",
69
+ },
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ }));
75
+
76
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
77
+ if (req.params.name !== "caelo_chat") {
78
+ return {
79
+ isError: true,
80
+ content: [
81
+ { type: "text", text: `unknown tool: ${req.params.name}` },
82
+ ],
83
+ };
84
+ }
85
+ const parsed = caeloChatInputSchema.safeParse(req.params.arguments ?? {});
86
+ if (!parsed.success) {
87
+ return {
88
+ isError: true,
89
+ content: [
90
+ { type: "text", text: `invalid arguments: ${parsed.error.message}` },
91
+ ],
92
+ };
93
+ }
94
+ try {
95
+ const result = await sendChat({
96
+ adminUrl: opts.adminUrl,
97
+ token: opts.token,
98
+ message: parsed.data.message,
99
+ ...(parsed.data.chatSessionId ? { chatSessionId: parsed.data.chatSessionId } : {}),
100
+ ...(parsed.data.pageId ? { pageId: parsed.data.pageId } : {}),
101
+ });
102
+ // Two content blocks: human-readable assistant text + a JSON
103
+ // structured block the calling agent can parse for the
104
+ // requestId, tool calls, cost, pending proposals.
105
+ return {
106
+ content: [
107
+ { type: "text", text: result.assistant },
108
+ {
109
+ type: "text",
110
+ text: JSON.stringify(
111
+ {
112
+ chatSessionId: result.chatSessionId,
113
+ requestId: result.requestId,
114
+ toolCalls: result.toolCalls,
115
+ pendingProposals: result.pendingProposals,
116
+ costMicrocents: result.costMicrocents,
117
+ },
118
+ null,
119
+ 2,
120
+ ),
121
+ },
122
+ ],
123
+ };
124
+ } catch (e) {
125
+ const msg = e instanceof Error ? e.message : String(e);
126
+ return {
127
+ isError: true,
128
+ content: [{ type: "text", text: `caelo_chat failed: ${msg}` }],
129
+ };
130
+ }
131
+ });
132
+
133
+ const transport = new StdioServerTransport();
134
+ await server.connect(transport);
135
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+ "noEmit": false
8
+ },
9
+ "include": ["src/**/*"],
10
+ "exclude": ["**/*.test.ts", "**/dist", "**/node_modules"]
11
+ }