cmskite-mcp 0.1.0 → 0.1.1

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
@@ -7,7 +7,29 @@ content surface behind one agent token.
7
7
 
8
8
  Create a token in the dashboard under **Settings → Agent tokens**. Tick only
9
9
  what the assistant needs; the defaults are read and write content. The token is
10
- shown once.
10
+ shown once, and that dialogue generates the block below for whichever client you
11
+ pick — **Set up a client** on the same screen shows it again afterwards.
12
+
13
+ Every client runs the same process. They disagree only about where it is written
14
+ and under what key, and a block copied from the wrong one pastes without
15
+ complaint and then does nothing.
16
+
17
+ ### Claude Code
18
+
19
+ ```sh
20
+ claude mcp add cmskite --scope user \
21
+ --env CMSKITE_API_URL=https://api.cmskite.com \
22
+ --env CMSKITE_AGENT_TOKEN=cka_live_… \
23
+ -- npx -y cmskite-mcp
24
+ ```
25
+
26
+ `--scope user` puts it in every project. Drop it to add it to one repository.
27
+
28
+ ### Claude Desktop, Cursor, Windsurf, and most others
29
+
30
+ `~/Library/Application Support/Claude/claude_desktop_config.json` ·
31
+ `%APPDATA%\Claude\claude_desktop_config.json` · `~/.cursor/mcp.json` ·
32
+ `~/.codeium/windsurf/mcp_config.json`
11
33
 
12
34
  ```jsonc
13
35
  {
@@ -24,6 +46,65 @@ shown once.
24
46
  }
25
47
  ```
26
48
 
49
+ Claude Desktop reads that file only at start-up, so quit it completely and open
50
+ it again.
51
+
52
+ ### Codex CLI
53
+
54
+ `~/.codex/config.toml` — TOML, and the only one here that does not take the
55
+ block above.
56
+
57
+ ```toml
58
+ [mcp_servers.cmskite]
59
+ command = "npx"
60
+ args = ["-y", "cmskite-mcp"]
61
+
62
+ [mcp_servers.cmskite.env]
63
+ CMSKITE_API_URL = "https://api.cmskite.com"
64
+ CMSKITE_AGENT_TOKEN = "cka_live_…"
65
+ ```
66
+
67
+ ### VS Code
68
+
69
+ `.vscode/mcp.json` — the key is `servers`, not `mcpServers`, and the transport
70
+ is named.
71
+
72
+ ```jsonc
73
+ {
74
+ "servers": {
75
+ "cmskite": {
76
+ "type": "stdio",
77
+ "command": "npx",
78
+ "args": ["-y", "cmskite-mcp"],
79
+ "env": {
80
+ "CMSKITE_API_URL": "https://api.cmskite.com",
81
+ "CMSKITE_AGENT_TOKEN": "cka_live_…"
82
+ }
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### Zed
89
+
90
+ Settings (`cmd-shift-p` → “open settings”) — context servers, marked custom.
91
+
92
+ ```jsonc
93
+ {
94
+ "context_servers": {
95
+ "cmskite": {
96
+ "source": "custom",
97
+ "command": "npx",
98
+ "args": ["-y", "cmskite-mcp"],
99
+ "env": {
100
+ "CMSKITE_API_URL": "https://api.cmskite.com",
101
+ "CMSKITE_AGENT_TOKEN": "cka_live_…"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
27
108
  | Variable | |
28
109
  |---|---|
29
110
  | `CMSKITE_AGENT_TOKEN` | Required. Starts `cka_`. A `csk_` value is a project API key: read-only, one project, and rejected here. |
@@ -41,9 +122,27 @@ shown once.
41
122
  **Content** — `list_posts`, `get_post`, `search_posts`, `create_post`,
42
123
  `update_post`, `delete_post`
43
124
 
125
+ **Keys** — `list_api_keys`, `create_api_key`
126
+
127
+ **Connecting a website** — `get_integration_guide`, `check_integration`,
128
+ `get_content_analytics`
129
+
44
130
  Start with `whoami`, then `list_projects` for a `prj_…` id. Every content tool
45
131
  needs one unless `CMSKITE_PROJECT_ID` is set.
46
132
 
133
+ ### Connecting a website
134
+
135
+ `get_integration_guide` returns the files to write for the project in front of
136
+ you. It answers for JavaScript with the `cmskite` package, and for PHP,
137
+ WordPress, Laravel, Python or a plain HTML site with the raw HTTP calls — those
138
+ need no dependency at all.
139
+
140
+ `check_integration` is the one to finish with, and the one to start with when
141
+ somebody says their views are zero. An integration has two halves: the server
142
+ fetches content, and the reader's browser reports the view. Building only the
143
+ first produces a site that works perfectly and counts nobody, with no error
144
+ anywhere to find. Nothing else can tell those two states apart.
145
+
47
146
  Each tool needs the matching grant, and a token has only what was ticked:
48
147
 
49
148
  | Tool | Grant |
@@ -54,10 +153,23 @@ Each tool needs the matching grant, and a token has only what was ticked:
54
153
  | `create_workspace` | `workspace.create` |
55
154
  | `list_*`, `get_post`, `search_posts` | `content.read` |
56
155
  | `create_*`, `update_*` | `content.write` |
156
+ | `update_post` with `status: "published"` | `content.publish`, on top of `content.write` |
57
157
  | `delete_*` | `content.delete` |
158
+ | `list_api_keys` | `apikey.read` |
159
+ | `create_api_key` | `apikey.write` |
160
+ | `check_integration` | `project.read` |
161
+ | `get_content_analytics` | `analytics.read` |
162
+ | `get_integration_guide` | none — it reads nothing and changes nothing |
58
163
 
59
164
  New posts are drafts. Publishing is `update_post` with `status: "published"` —
60
- a separate step, because publishing is a decision the person should make.
165
+ a separate step, because publishing is a decision the person should make, and a
166
+ separate grant, so an assistant can be allowed to draft without being allowed to
167
+ ship.
168
+
169
+ The member's own permissions are checked as well, fresh on every request. A
170
+ token ticked for `content.publish` held by somebody whose role does not include
171
+ it publishes nothing: the grant list is a ceiling on what its owner can already
172
+ do, never an addition to it.
61
173
 
62
174
  ## What it cannot do
63
175
 
@@ -65,8 +177,18 @@ This server adds no permissions. Every limit is enforced by the API on every
65
177
  request: the token's grant list, the member's current role, the one workspace
66
178
  the token is bound to, and the plan.
67
179
 
68
- No token can mint or revoke credentials, delete a project, or delete a
69
- workspace — not at any role and not with any configuration. See
180
+ No token can revoke credentials, invite or remove a member, change anybody's
181
+ permissions, delete a project, or delete a workspace — not at any role and not
182
+ with any configuration.
183
+
184
+ `create_api_key` is the one credential an agent can mint, and only with the
185
+ grant ticked. What it produces is strictly weaker than the token that made it:
186
+ one project, read-only, published content only, no ability to revoke anything.
187
+ It exists so that finishing a site does not require shipping the agent token —
188
+ which can write and delete — to production.
189
+
190
+ A token also cannot reach a project its owner was not admitted to. Project
191
+ access belongs to the person, and the token acts as them. See
70
192
  `docs/agents-and-mcp.md` in the API repository.
71
193
 
72
194
  `create_workspace` returns a **new** token for the workspace it created. The
@@ -0,0 +1,78 @@
1
+ import { z } from 'zod';
2
+ import { segment } from '../client.js';
3
+ import { defineTool } from './register.js';
4
+ /**
5
+ * Whether the work actually worked.
6
+ *
7
+ * Every other tool here changes something and reports that it changed it. None
8
+ * of them could tell anybody whether the result was a working website, and the
9
+ * failure this product has is specifically one that reports success: an
10
+ * assistant creates a project, writes eight posts, mints a key and wires up the
11
+ * content fetching, and every one of those steps succeeds. The tracker is a
12
+ * separate snippet, nothing asked for it, and so nobody added it. The site
13
+ * works. Every post reads zero views. There is no error anywhere to find.
14
+ *
15
+ * So `check_integration` exists to make that state something a tool can say out
16
+ * loud. It is the last call of any integration, and the first call of "why are
17
+ * my views zero".
18
+ */
19
+ export const diagnosticTools = [
20
+ defineTool({
21
+ name: 'check_integration',
22
+ title: 'Is this project actually working?',
23
+ description: 'Checks whether a project is really connected to a website: does it have a key, has a ' +
24
+ 'site ever used it, and are views arriving from the pages that render the content. ' +
25
+ 'Each check comes back with the one action that fixes it. ' +
26
+ 'ALWAYS call this after wiring up an integration, and always when somebody says views or ' +
27
+ 'analytics are zero — a site can fetch content perfectly and report nothing, and this is ' +
28
+ 'the only thing that tells the difference. Requires the project.read grant.',
29
+ input: { projectId: z.string().describe('The project id, as `prj_...`.') },
30
+ readOnly: true,
31
+ run: (client, args) => client.request(`/v1/projects/${segment(args.projectId)}/integration-health`),
32
+ }),
33
+ defineTool({
34
+ name: 'get_content_analytics',
35
+ title: 'Views, uniques and clicks',
36
+ description: 'What readers did with a project’s posts over a date range: totals, a daily series, and ' +
37
+ 'the posts that were actually read. Read from rollups, so the answer is the same size ' +
38
+ 'whatever the traffic was. If everything is zero, call check_integration — that is a ' +
39
+ 'tracking problem far more often than it is a traffic problem. Requires the ' +
40
+ 'analytics.read grant.',
41
+ input: {
42
+ projectId: z.string().describe('The project id, as `prj_...`.'),
43
+ from: z
44
+ .string()
45
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
46
+ .optional()
47
+ .describe('Inclusive UTC day. Defaults to 30 days ago.'),
48
+ to: z
49
+ .string()
50
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
51
+ .optional()
52
+ .describe('Inclusive UTC day. Defaults to today.'),
53
+ topPosts: z
54
+ .number()
55
+ .int()
56
+ .min(1)
57
+ .max(50)
58
+ .optional()
59
+ .describe('How many of the best-read posts to include. Defaults to 10.'),
60
+ },
61
+ readOnly: true,
62
+ run: async (client, args) => {
63
+ const query = { projectId: args.projectId, from: args.from, to: args.to };
64
+ // Two calls rather than one, because the summary and the leaderboard are
65
+ // different shapes and an assistant asking "how is the blog doing" wants
66
+ // both. They are independent reads and nothing here mutates, so they go
67
+ // together.
68
+ const [summary, top] = await Promise.all([
69
+ client.request('/v1/analytics/content/summary', { query }),
70
+ client.request('/v1/analytics/content/top-posts', {
71
+ query: { ...query, limit: args.topPosts ?? 10 },
72
+ }),
73
+ ]);
74
+ return { summary, topPosts: top };
75
+ },
76
+ }),
77
+ ];
78
+ //# sourceMappingURL=diagnose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnose.js","sourceRoot":"","sources":["../../src/tools/diagnose.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE1C;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,UAAU,CAAC;QACT,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,mCAAmC;QAC1C,WAAW,EACT,uFAAuF;YACvF,oFAAoF;YACpF,2DAA2D;YAC3D,0FAA0F;YAC1F,0FAA0F;YAC1F,4EAA4E;QAC9E,KAAK,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC,EAAE;QAC1E,QAAQ,EAAE,IAAI;QACd,GAAG,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CACpB,MAAM,CAAC,OAAO,CAAC,gBAAgB,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC;KAC/E,CAAC;IAEF,UAAU,CAAC;QACT,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,yFAAyF;YACzF,uFAAuF;YACvF,sFAAsF;YACtF,6EAA6E;YAC7E,uBAAuB;QACzB,KAAK,EAAE;YACL,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;YAC/D,IAAI,EAAE,CAAC;iBACJ,MAAM,EAAE;iBACR,KAAK,CAAC,qBAAqB,CAAC;iBAC5B,QAAQ,EAAE;iBACV,QAAQ,CAAC,6CAA6C,CAAC;YAC1D,EAAE,EAAE,CAAC;iBACF,MAAM,EAAE;iBACR,KAAK,CAAC,qBAAqB,CAAC;iBAC5B,QAAQ,EAAE;iBACV,QAAQ,CAAC,uCAAuC,CAAC;YACpD,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,GAAG,EAAE;iBACL,GAAG,CAAC,CAAC,CAAC;iBACN,GAAG,CAAC,EAAE,CAAC;iBACP,QAAQ,EAAE;iBACV,QAAQ,CAAC,6DAA6D,CAAC;SAC3E;QACD,QAAQ,EAAE,IAAI;QACd,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;YAC1B,MAAM,KAAK,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAA;YACzE,yEAAyE;YACzE,yEAAyE;YACzE,wEAAwE;YACxE,YAAY;YACZ,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACvC,MAAM,CAAC,OAAO,CAAU,+BAA+B,EAAE,EAAE,KAAK,EAAE,CAAC;gBACnE,MAAM,CAAC,OAAO,CAAU,iCAAiC,EAAE;oBACzD,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE;iBAChD,CAAC;aACH,CAAC,CAAA;YACF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAA;QACnC,CAAC;KACF,CAAC;CACM,CAAA"}
@@ -1,3 +1,6 @@
1
+ import { diagnosticTools } from './diagnose.js';
2
+ import { integrationTools } from './integrate.js';
3
+ import { keyTools } from './keys.js';
1
4
  import { postTools } from './posts.js';
2
5
  import { taxonomyTools } from './taxonomy.js';
3
6
  import { workspaceTools } from './workspace.js';
@@ -5,6 +8,16 @@ import { workspaceTools } from './workspace.js';
5
8
  * Every tool, in the order an assistant meeting a workspace for the first time
6
9
  * would want them: find out where you are, then what is here, then change it.
7
10
  *
11
+ * Integration is last and used first, which is the one exception: an assistant
12
+ * asked to "add a blog to this site" should call `get_integration_guide` before
13
+ * it writes a line, because the alternative is a hand-rolled fetch wrapper that
14
+ * works and counts no readers -- a customer gets a working site with no
15
+ * analytics and nothing indicating that anything is missing.
16
+ *
17
+ * Diagnostics come after integration for the same reason integration comes
18
+ * last: `check_integration` is what an assistant calls once it thinks it has
19
+ * finished, and what it calls first when somebody says the numbers are zero.
20
+ *
8
21
  * Media is deliberately absent. Uploading goes to object storage through a
9
22
  * presigned URL, so it is two calls and a byte stream, and an assistant that
10
23
  * cannot see the file has nothing useful to send. When it is added it belongs
@@ -14,6 +27,9 @@ export const allTools = [
14
27
  ...workspaceTools,
15
28
  ...taxonomyTools,
16
29
  ...postTools,
30
+ ...keyTools,
31
+ ...integrationTools,
32
+ ...diagnosticTools,
17
33
  ];
18
34
  export { registerTools } from './register.js';
19
35
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG,cAAc;IACjB,GAAG,aAAa;IAChB,GAAG,SAAS;CACS,CAAA;AAEvB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG,cAAc;IACjB,GAAG,aAAa;IAChB,GAAG,SAAS;IACZ,GAAG,QAAQ;IACX,GAAG,gBAAgB;IACnB,GAAG,eAAe;CACG,CAAA;AAEvB,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA"}