@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 +305 -0
- package/index.js +23 -0
- package/lib/api-client.js +183 -0
- package/lib/resources/index.js +134 -0
- package/lib/tools/ai.js +148 -0
- package/lib/tools/assets.js +66 -0
- package/lib/tools/core.js +109 -0
- package/lib/tools/delivery.js +127 -0
- package/lib/tools/insights.js +96 -0
- package/lib/tools/logs.js +34 -0
- package/lib/tools/storage.js +71 -0
- package/lib/tools/tokens.js +57 -0
- package/lib/tools/webhooks.js +59 -0
- package/package.json +70 -0
- package/server.js +80 -0
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
|
+
}
|