@cloudcdn/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,305 @@
1
+ <!-- SPDX-License-Identifier: MIT OR Apache-2.0 -->
2
+
3
+ <p align="center">
4
+ <img src="https://cloudcdn.pro/cloudcdn/v1/logos/cloudcdn.svg" alt="CloudCDN logo" width="128" />
5
+ </p>
6
+
7
+ <h1 align="center">@cloudcdn/mcp-server</h1>
8
+
9
+ <p align="center">
10
+ Model Context Protocol server for <a href="https://cloudcdn.pro">CloudCDN</a> —
11
+ lets AI agents autonomously manage static assets, zones, transforms,
12
+ analytics, and cache across 300+ edge locations.
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://www.npmjs.com/package/@cloudcdn/mcp-server"><img src="https://img.shields.io/npm/v/@cloudcdn/mcp-server?style=for-the-badge&logo=npm" alt="npm" /></a>
17
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/node-%E2%89%A518-339933?style=for-the-badge&logo=node.js" alt="Node >= 18" /></a>
18
+ <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-1.12%2B-6366f1?style=for-the-badge" alt="MCP SDK" /></a>
19
+ <a href="#testing"><img src="https://img.shields.io/badge/coverage-100%25-15803d?style=for-the-badge" alt="Coverage" /></a>
20
+ <a href="../LICENSE"><img src="https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue?style=for-the-badge" alt="License" /></a>
21
+ </p>
22
+
23
+ ---
24
+
25
+ ## Contents
26
+
27
+ - [Install](#install) — npm, npx, from source
28
+ - [Quick Start](#quick-start) — wire it into Claude Desktop in 30 seconds
29
+ - [Programmatic usage](#programmatic-usage) — embed the server in your own host
30
+ - [Tools (42)](#tools-42) — every callable, grouped by API plane
31
+ - [Resources (6)](#resources-6) — read-only context exposed to the agent
32
+ - [Configuration](#configuration) — environment variables
33
+ - [Host configs](#host-configs) — Claude Code, Claude Desktop, Cursor, VS Code, Windsurf
34
+ - [Examples](#examples) — runnable scripts under `examples/`
35
+ - [Local development](#local-development)
36
+ - [Testing](#testing)
37
+ - [License](#license)
38
+
39
+ ---
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ # Run directly (no install — recommended for MCP hosts)
45
+ npx @cloudcdn/mcp-server
46
+
47
+ # Or install globally to get a `cloudcdn-mcp` binary on $PATH
48
+ npm install -g @cloudcdn/mcp-server
49
+
50
+ # Or as a project dependency
51
+ npm install @cloudcdn/mcp-server
52
+ # yarn add @cloudcdn/mcp-server
53
+ # pnpm add @cloudcdn/mcp-server
54
+ ```
55
+
56
+ **Minimum runtime:** Node.js >= 18 (ESM-only).
57
+
58
+ ---
59
+
60
+ ## Quick Start
61
+
62
+ ```jsonc
63
+ // ~/Library/Application Support/Claude/claude_desktop_config.json
64
+ {
65
+ "mcpServers": {
66
+ "cloudcdn": {
67
+ "command": "npx",
68
+ "args": ["-y", "@cloudcdn/mcp-server"],
69
+ "env": {
70
+ "CLOUDCDN_ACCESS_KEY": "sk_live_...",
71
+ "CLOUDCDN_ACCOUNT_KEY": "ak_live_..."
72
+ }
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Restart Claude Desktop. The agent now has 42 tools and 6 read-only resources for driving your CloudCDN tenant. The server identifies itself to the host as `cloudcdn`.
79
+
80
+ ---
81
+
82
+ ## Programmatic usage
83
+
84
+ Embed the server in your own host — custom transport, hosted MCP gateway, integration tests:
85
+
86
+ ```js
87
+ // host.mjs
88
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
89
+ import { createServer } from '@cloudcdn/mcp-server/server';
90
+
91
+ // 1. Build the server with all 42 tools and 6 resources registered.
92
+ const server = createServer();
93
+
94
+ // 2. Attach a transport. Stdio is the default for desktop MCP hosts;
95
+ // swap in a Streamable HTTP or SSE transport for a hosted gateway.
96
+ const transport = new StdioServerTransport();
97
+ await server.connect(transport);
98
+
99
+ // 3. Tool handlers read CLOUDCDN_ACCESS_KEY / CLOUDCDN_ACCOUNT_KEY /
100
+ // CLOUDCDN_PURGE_KEY from process.env, so set those before connect().
101
+ ```
102
+
103
+ The thin HTTP client is also exported if you want to talk to the CloudCDN
104
+ REST API directly from outside an MCP context:
105
+
106
+ ```js
107
+ import { get, post } from '@cloudcdn/mcp-server/api-client';
108
+
109
+ // AccessKey-gated read.
110
+ const { ok, data } = await get('/api/assets', {
111
+ auth: 'access',
112
+ params: { project: 'akande', format: 'svg', per_page: 20 },
113
+ });
114
+
115
+ if (ok) console.log(`Found ${data.results.length} assets`);
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Tools (42)
121
+
122
+ | Tool | Plane | Auth | Description |
123
+ |---|---|---|---|
124
+ | `storage_list` | Storage | AccessKey | List files at a directory path |
125
+ | `storage_upload` | Storage | AccessKey | Upload a file (committed via Git, deployed in ~60–90 s) |
126
+ | `storage_delete` | Storage | AccessKey | Delete a file |
127
+ | `storage_batch_upload` | Storage | AccessKey | Upload up to 50 files in one atomic commit |
128
+ | `statistics_summary` | Core | AccountKey | Control-plane summary (zones, files, storage, last sync) |
129
+ | `zone_list` | Core | AccountKey | List all tenant zones |
130
+ | `zone_get` | Core | AccountKey | Get zone details with all files |
131
+ | `zone_create` | Core | AccountKey | Create a new zone with standard scaffolding |
132
+ | `zone_delete` | Core | AccountKey | Delete a zone (destructive) |
133
+ | `domain_add` | Core | AccountKey | Add a custom domain to a zone |
134
+ | `rules_get` | Core | AccountKey | Read edge rules (`_headers`, `_redirects`) |
135
+ | `rules_update` | Core | AccountKey | Update edge rules via Git |
136
+ | `assets_list` | Assets | AccessKey | Browse / filter / paginate the asset catalog |
137
+ | `asset_metadata_get` | Assets | AccessKey | Full metadata for a single asset (incl. AI-derived fields) |
138
+ | `assets_search` | Assets | AccessKey | Search assets by name / path |
139
+ | `insights_summary` | Insights | AccessKey | Analytics summary (requests, bandwidth, cache ratio) |
140
+ | `insights_top_assets` | Insights | AccessKey | Most-requested assets |
141
+ | `insights_geography` | Insights | AccessKey | Request distribution by country |
142
+ | `insights_errors` | Insights | AccessKey | Error breakdown (4xx / 5xx) |
143
+ | `insights_asset` | Insights | AccessKey | Per-asset daily request + error roll-ups |
144
+ | `audit_logs` | Insights | AccountKey | 90-day immutable audit log of every control-plane mutation |
145
+ | `transform_image` | Delivery | Public | Generate a transformed image URL (resize / format / blur / sharpen) |
146
+ | `cache_purge` | Delivery | PurgeKey | Purge cache by URL, surrogate tag, or everything |
147
+ | `signed_url_generate` | Delivery | AccountKey | Mint a time-limited HMAC-signed URL for protected assets |
148
+ | `stream_playlist` | Delivery | Public | Build an HLS `.m3u8` playlist URL for adaptive-bitrate video |
149
+ | `pipeline_ingest` | Delivery | AccountKey | Scaffold a zone from a single SVG |
150
+ | `semantic_search` | AI | Public | Natural-language asset search (Vectorize + fuzzy fallback) |
151
+ | `health_check` | AI | Public | Service health + binding status |
152
+ | `generate_alt_text` | AI | Public | AI-generated accessibility alt text for an image |
153
+ | `smart_crop` | AI | Public | Subject-aware crop `gravity` value for transform |
154
+ | `moderate_image` | AI | Public | Content safety classifier across 5 categories |
155
+ | `placeholder_lqip` | AI | Public | Low-quality image placeholder (data-URI) |
156
+ | `placeholder_blurhash` | AI | Public | BlurHash + data-URI pair for hash-deduped caching |
157
+ | `chat_ask` | AI | Public | RAG concierge — degrades to curated FAQ on AI quota |
158
+ | `remove_background` | AI | Public | Background removal (HTTP 501 until a matting model lands) |
159
+ | `webhook_list` | Webhooks | AccountKey | List registered webhooks |
160
+ | `webhook_create` | Webhooks | AccountKey | Subscribe an HTTPS URL to event types (HMAC-signed delivery) |
161
+ | `webhook_delete` | Webhooks | AccountKey | Revoke a webhook |
162
+ | `token_list` | Auth | AccountKey | List API tokens (redacted) |
163
+ | `token_create` | Auth | AccountKey | Mint a scoped API token |
164
+ | `token_revoke` | Auth | AccountKey | Revoke an API token by ID |
165
+ | `logs_query` | Operations | AccountKey | Stream or page operational logs |
166
+
167
+ ---
168
+
169
+ ## Resources (6)
170
+
171
+ | URI | Description |
172
+ |---|---|
173
+ | `cloudcdn://manifest` | Full asset manifest (names, paths, projects, sizes) |
174
+ | `cloudcdn://zones` | All zones with file counts and storage usage |
175
+ | `cloudcdn://rules` | Current `_headers` and `_redirects` edge configuration |
176
+ | `cloudcdn://health` | Live health snapshot — binding state + per-binding latency |
177
+ | `cloudcdn://openapi` | Full OpenAPI 3.1 spec — every path, schema, example |
178
+ | `cloudcdn://insights/today` | Today's analytics summary (requests, bandwidth, cache hit ratio) |
179
+
180
+ ---
181
+
182
+ ## Configuration
183
+
184
+ All settings are environment variables read at startup; tool handlers fail
185
+ with a structured MCP error if a key required for the call is unset.
186
+
187
+ | Variable | Required for | Description |
188
+ |---|---|---|
189
+ | `CLOUDCDN_ACCESS_KEY` | Storage, Assets, Insights | Tenant read/write key (`sk_live_…`) |
190
+ | `CLOUDCDN_ACCOUNT_KEY` | Core, Pipeline, Webhooks, Tokens, Logs, Audit | Account-admin key (`ak_live_…`) |
191
+ | `CLOUDCDN_PURGE_KEY` | `cache_purge` | Cache-purge key (`pk_live_…`) |
192
+ | `CLOUDCDN_ANALYTICS_KEY` | Analytics endpoints | Optional analytics key |
193
+ | `CLOUDCDN_BASE_URL` | All | Defaults to `https://cloudcdn.pro`. Point at `http://localhost:8788` for local Wrangler dev. |
194
+
195
+ ---
196
+
197
+ ## Host configs
198
+
199
+ <details>
200
+ <summary><strong>Claude Code</strong> — <code>~/.claude/settings.json</code></summary>
201
+
202
+ ```json
203
+ {
204
+ "mcpServers": {
205
+ "cloudcdn": {
206
+ "command": "npx",
207
+ "args": ["-y", "@cloudcdn/mcp-server"],
208
+ "env": {
209
+ "CLOUDCDN_ACCESS_KEY": "sk_live_...",
210
+ "CLOUDCDN_ACCOUNT_KEY": "ak_live_..."
211
+ }
212
+ }
213
+ }
214
+ }
215
+ ```
216
+ </details>
217
+
218
+ <details>
219
+ <summary><strong>Cursor</strong> — <code>.cursor/mcp.json</code> in your project root</summary>
220
+
221
+ ```json
222
+ {
223
+ "mcpServers": {
224
+ "cloudcdn": {
225
+ "command": "npx",
226
+ "args": ["-y", "@cloudcdn/mcp-server"],
227
+ "env": {
228
+ "CLOUDCDN_ACCESS_KEY": "sk_live_...",
229
+ "CLOUDCDN_ACCOUNT_KEY": "ak_live_..."
230
+ }
231
+ }
232
+ }
233
+ }
234
+ ```
235
+ </details>
236
+
237
+ <details>
238
+ <summary><strong>VS Code (GitHub Copilot Chat)</strong> — <code>.vscode/mcp.json</code></summary>
239
+
240
+ ```json
241
+ {
242
+ "servers": {
243
+ "cloudcdn": {
244
+ "command": "npx",
245
+ "args": ["-y", "@cloudcdn/mcp-server"],
246
+ "env": {
247
+ "CLOUDCDN_ACCESS_KEY": "sk_live_...",
248
+ "CLOUDCDN_ACCOUNT_KEY": "ak_live_..."
249
+ }
250
+ }
251
+ }
252
+ }
253
+ ```
254
+ </details>
255
+
256
+ ---
257
+
258
+ ## Examples
259
+
260
+ Runnable scripts live under [`examples/`](./examples/). Set the env vars
261
+ first, then `node examples/<name>.mjs`:
262
+
263
+ | Script | What it does |
264
+ |---|---|
265
+ | [`quickstart-stdio.mjs`](./examples/quickstart-stdio.mjs) | Boots the server on stdio — the same code path `npx` uses |
266
+ | [`programmatic-embed.mjs`](./examples/programmatic-embed.mjs) | Builds a `createServer()` instance with a custom transport |
267
+ | [`semantic-search.mjs`](./examples/semantic-search.mjs) | Calls the underlying `/api/search` endpoint via the API client |
268
+ | [`cache-purge.mjs`](./examples/cache-purge.mjs) | Purges a URL via `/api/purge` (requires `CLOUDCDN_PURGE_KEY`) |
269
+ | [`transform-image.mjs`](./examples/transform-image.mjs) | Builds a width-400 WebP transform URL |
270
+
271
+ ---
272
+
273
+ ## Local development
274
+
275
+ ```bash
276
+ git clone https://github.com/sebastienrousseau/cloudcdn.pro.git
277
+ cd cloudcdn.pro/mcp
278
+ npm ci
279
+
280
+ # Point the server at a local Wrangler dev instance of the CloudCDN API.
281
+ export CLOUDCDN_BASE_URL="http://localhost:8788"
282
+ export CLOUDCDN_ACCESS_KEY="sk_test_local"
283
+ export CLOUDCDN_ACCOUNT_KEY="ak_test_local"
284
+
285
+ node index.js
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Testing
291
+
292
+ ```bash
293
+ npm test # vitest run — all unit + regression tests
294
+ npm run test:watch # interactive
295
+ npm run test:coverage # v8 coverage report, 100% gate
296
+ ```
297
+
298
+ Coverage thresholds are pinned at **100% statements / branches / functions / lines**
299
+ in `vitest.config.js`. CI fails on any drop.
300
+
301
+ ---
302
+
303
+ ## License
304
+
305
+ Dual-licensed under [MIT](../LICENSE) and [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0), at your option.
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CloudCDN MCP Server — entry point.
5
+ *
6
+ * Boots the server and attaches it to a stdio transport. This is the code
7
+ * path that `npx @cloudcdn/mcp-server` (or the `cloudcdn-mcp` binary)
8
+ * executes when an MCP host (Claude Desktop, Cursor, Windsurf, etc.)
9
+ * launches it.
10
+ *
11
+ * For programmatic embedding — custom transport, hosted MCP gateway,
12
+ * integration tests — import `createServer` from `@cloudcdn/mcp-server/server`
13
+ * directly and skip this file.
14
+ *
15
+ * @module @cloudcdn/mcp-server
16
+ */
17
+
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19
+ import { createServer } from './server.js';
20
+
21
+ const server = createServer();
22
+ const transport = new StdioServerTransport();
23
+ await server.connect(transport);
@@ -0,0 +1,183 @@
1
+ /**
2
+ * CloudCDN API client — a small `fetch` wrapper used by every MCP tool.
3
+ *
4
+ * Reads configuration from environment variables (the MCP host populates
5
+ * them via the `env` block in its config file):
6
+ *
7
+ * CLOUDCDN_BASE_URL — API base URL (default: https://cloudcdn.pro)
8
+ * CLOUDCDN_ACCESS_KEY — AccessKey header (storage, assets, insights)
9
+ * CLOUDCDN_ACCOUNT_KEY — AccountKey header (core, pipeline, audit, …)
10
+ * CLOUDCDN_PURGE_KEY — x-api-key header (cache purge)
11
+ * CLOUDCDN_ANALYTICS_KEY — x-api-key header (analytics endpoints)
12
+ *
13
+ * Public surface:
14
+ *
15
+ * get(path, opts) — GET
16
+ * post(path, body, opts) — POST (JSON body if not a typed array)
17
+ * put(path, body, opts) — PUT
18
+ * del(path, opts) — DELETE
19
+ * head(path, opts) — HEAD
20
+ * BASE_URL — resolved base URL (read-only)
21
+ *
22
+ * Each helper returns `{ ok: boolean, status: number, data: unknown }`.
23
+ * For non-JSON responses, `data` is `{ contentType, contentLength, url }`
24
+ * (no body parsing — useful for binary endpoints like /api/transform).
25
+ *
26
+ * @module @cloudcdn/mcp-server/api-client
27
+ */
28
+
29
+ const BASE_URL = process.env.CLOUDCDN_BASE_URL || 'https://cloudcdn.pro';
30
+
31
+ /**
32
+ * Resolves the auth headers for a request, based on which auth name the
33
+ * caller picked. Each entry is a thunk so the env vars are read at call
34
+ * time (not at module load) — keeps tests reliable.
35
+ *
36
+ * @type {Record<'access'|'account'|'purge'|'analytics'|'none', () => Record<string, string>>}
37
+ */
38
+ const AUTH_HEADERS = {
39
+ access: () => {
40
+ const key = process.env.CLOUDCDN_ACCESS_KEY;
41
+ return key ? { AccessKey: key } : {};
42
+ },
43
+ account: () => {
44
+ const key = process.env.CLOUDCDN_ACCOUNT_KEY;
45
+ return key ? { AccountKey: key } : {};
46
+ },
47
+ purge: () => {
48
+ const key = process.env.CLOUDCDN_PURGE_KEY;
49
+ return key ? { 'x-api-key': key } : {};
50
+ },
51
+ analytics: () => {
52
+ const key = process.env.CLOUDCDN_ANALYTICS_KEY;
53
+ return key ? { 'x-api-key': key } : {};
54
+ },
55
+ none: () => ({}),
56
+ };
57
+
58
+ /**
59
+ * Builds an absolute URL with serialized query params.
60
+ *
61
+ * Undefined and null param values are silently dropped — that lets call
62
+ * sites pass `{ project, format }` even when one of them is unset.
63
+ *
64
+ * @param {string} path - Relative API path (e.g. `/api/assets`).
65
+ * @param {Record<string, unknown>} [params] - Query string parameters.
66
+ * @returns {string} Fully qualified URL.
67
+ */
68
+ function buildUrl(path, params = {}) {
69
+ const url = new URL(path, BASE_URL);
70
+ for (const [k, v] of Object.entries(params)) {
71
+ if (v !== undefined && v !== null) {
72
+ url.searchParams.set(k, String(v));
73
+ }
74
+ }
75
+ return url.toString();
76
+ }
77
+
78
+ /**
79
+ * Issues a request and returns the structured response shape used by every
80
+ * MCP tool handler. Internal — call {@link get}/{@link post}/etc. instead.
81
+ *
82
+ * @param {string} method - HTTP verb.
83
+ * @param {string} path - Relative API path.
84
+ * @param {object} [opts]
85
+ * @param {'access'|'account'|'purge'|'analytics'|'none'} [opts.auth='none'] - Auth profile.
86
+ * @param {Record<string, unknown>} [opts.params] - Query string parameters.
87
+ * @param {unknown} [opts.body] - Request body (JSON-serialized unless ArrayBuffer/Uint8Array).
88
+ * @param {Record<string, string>} [opts.headers] - Extra request headers.
89
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
90
+ */
91
+ async function request(method, path, { auth = 'none', params, body, headers: extra } = {}) {
92
+ const url = buildUrl(path, params);
93
+ const authHeaders = AUTH_HEADERS[auth]?.() || {};
94
+
95
+ const headers = {
96
+ ...authHeaders,
97
+ ...extra,
98
+ };
99
+
100
+ const opts = { method, headers };
101
+
102
+ if (body !== undefined) {
103
+ if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
104
+ opts.body = body;
105
+ } else {
106
+ headers['Content-Type'] = 'application/json';
107
+ opts.body = JSON.stringify(body);
108
+ }
109
+ }
110
+
111
+ const res = await fetch(url, opts);
112
+ const contentType = res.headers.get('content-type') || '';
113
+
114
+ if (contentType.includes('application/json')) {
115
+ const data = await res.json();
116
+ return { ok: res.ok, status: res.status, data };
117
+ }
118
+
119
+ // Non-JSON — return metadata only (binary responses are not buffered).
120
+ return {
121
+ ok: res.ok,
122
+ status: res.status,
123
+ data: {
124
+ contentType,
125
+ contentLength: res.headers.get('content-length'),
126
+ url,
127
+ },
128
+ };
129
+ }
130
+
131
+ /**
132
+ * GET — no request body.
133
+ * @param {string} path
134
+ * @param {Parameters<typeof request>[2]} [opts]
135
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
136
+ */
137
+ export function get(path, opts) {
138
+ return request('GET', path, opts);
139
+ }
140
+
141
+ /**
142
+ * POST — JSON-serialised body unless a typed array is given.
143
+ * @param {string} path
144
+ * @param {unknown} body
145
+ * @param {Parameters<typeof request>[2]} [opts]
146
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
147
+ */
148
+ export function post(path, body, opts = {}) {
149
+ return request('POST', path, { ...opts, body });
150
+ }
151
+
152
+ /**
153
+ * PUT — JSON body by default, raw bytes if `body` is an ArrayBuffer / Uint8Array.
154
+ * @param {string} path
155
+ * @param {unknown} body
156
+ * @param {Parameters<typeof request>[2]} [opts]
157
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
158
+ */
159
+ export function put(path, body, opts = {}) {
160
+ return request('PUT', path, { ...opts, body });
161
+ }
162
+
163
+ /**
164
+ * DELETE — no request body.
165
+ * @param {string} path
166
+ * @param {Parameters<typeof request>[2]} [opts]
167
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
168
+ */
169
+ export function del(path, opts) {
170
+ return request('DELETE', path, opts);
171
+ }
172
+
173
+ /**
174
+ * HEAD — no request body, no response body.
175
+ * @param {string} path
176
+ * @param {Parameters<typeof request>[2]} [opts]
177
+ * @returns {Promise<{ ok: boolean, status: number, data: unknown }>}
178
+ */
179
+ export function head(path, opts) {
180
+ return request('HEAD', path, opts);
181
+ }
182
+
183
+ export { BASE_URL };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Read-only MCP resources exposed under the `cloudcdn://` scheme.
3
+ *
4
+ * Resources are passive — agents read them to populate context — and so
5
+ * each handler just forwards to a REST endpoint and serialises the JSON
6
+ * body back. All six refresh on every read.
7
+ *
8
+ * @module @cloudcdn/mcp-server/lib/resources
9
+ */
10
+
11
+ import * as api from '../api-client.js';
12
+
13
+ /**
14
+ * Register the six MCP resources (`manifest`, `zones`, `rules`, `health`,
15
+ * `openapi`, `insights-today`) on the given MCP server.
16
+ *
17
+ * @param {{ resource: Function }} server - The MCP server instance.
18
+ * @returns {void}
19
+ */
20
+ export function registerResources(server) {
21
+ server.resource(
22
+ 'manifest',
23
+ 'cloudcdn://manifest',
24
+ {
25
+ description: 'Complete JSON manifest of all CDN assets with names, paths, projects, categories, formats, and sizes.',
26
+ mimeType: 'application/json',
27
+ },
28
+ async () => {
29
+ const res = await api.get('/manifest.json');
30
+ return {
31
+ contents: [{
32
+ uri: 'cloudcdn://manifest',
33
+ mimeType: 'application/json',
34
+ text: JSON.stringify(res.data, null, 2),
35
+ }],
36
+ };
37
+ }
38
+ );
39
+
40
+ server.resource(
41
+ 'zones',
42
+ 'cloudcdn://zones',
43
+ {
44
+ description: 'List of all CDN zones with file counts and storage usage.',
45
+ mimeType: 'application/json',
46
+ },
47
+ async () => {
48
+ const res = await api.get('/api/core/zones', { auth: 'account' });
49
+ return {
50
+ contents: [{
51
+ uri: 'cloudcdn://zones',
52
+ mimeType: 'application/json',
53
+ text: JSON.stringify(res.data, null, 2),
54
+ }],
55
+ };
56
+ }
57
+ );
58
+
59
+ server.resource(
60
+ 'rules',
61
+ 'cloudcdn://rules',
62
+ {
63
+ description: 'Current _headers and _redirects edge configuration files.',
64
+ mimeType: 'application/json',
65
+ },
66
+ async () => {
67
+ const res = await api.get('/api/core/rules', { auth: 'account' });
68
+ return {
69
+ contents: [{
70
+ uri: 'cloudcdn://rules',
71
+ mimeType: 'application/json',
72
+ text: JSON.stringify(res.data, null, 2),
73
+ }],
74
+ };
75
+ }
76
+ );
77
+
78
+ server.resource(
79
+ 'health',
80
+ 'cloudcdn://health',
81
+ {
82
+ description: 'Live service-health snapshot — binding presence (KV / AI / Vectorize / Durable Object / Queue), per-binding latency, healthy/degraded state. Refresh on every read.',
83
+ mimeType: 'application/json',
84
+ },
85
+ async () => {
86
+ const res = await api.get('/api/health', { params: { deep: 1 } });
87
+ return {
88
+ contents: [{
89
+ uri: 'cloudcdn://health',
90
+ mimeType: 'application/json',
91
+ text: JSON.stringify(res.data, null, 2),
92
+ }],
93
+ };
94
+ }
95
+ );
96
+
97
+ server.resource(
98
+ 'openapi',
99
+ 'cloudcdn://openapi',
100
+ {
101
+ description: 'Full OpenAPI 3.1 specification for the CloudCDN REST API — every path, schema, tag group, security scheme, and example. Agents can read this as the source-of-truth contract when wiring direct HTTP calls.',
102
+ mimeType: 'application/json',
103
+ },
104
+ async () => {
105
+ const res = await api.get('/api-reference/openapi.json');
106
+ return {
107
+ contents: [{
108
+ uri: 'cloudcdn://openapi',
109
+ mimeType: 'application/json',
110
+ text: JSON.stringify(res.data, null, 2),
111
+ }],
112
+ };
113
+ }
114
+ );
115
+
116
+ server.resource(
117
+ 'insights-today',
118
+ 'cloudcdn://insights/today',
119
+ {
120
+ description: 'Current-day insights summary — request volume, bandwidth served, cache hit ratio, top assets, error rate. Pulls from /api/insights/summary; refreshes on every read.',
121
+ mimeType: 'application/json',
122
+ },
123
+ async () => {
124
+ const res = await api.get('/api/insights/summary', { auth: 'access' });
125
+ return {
126
+ contents: [{
127
+ uri: 'cloudcdn://insights/today',
128
+ mimeType: 'application/json',
129
+ text: JSON.stringify(res.data, null, 2),
130
+ }],
131
+ };
132
+ }
133
+ );
134
+ }