@sebastienrousseau/cloudcdn-mcp 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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Core control-plane MCP tools — zones, domains, edge rules, account stats.
3
+ * AccountKey auth on every call.
4
+ *
5
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/core
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import * as api from '../api-client.js';
10
+
11
+ /**
12
+ * Register the core tools (`statistics_summary`, `zone_*`, `domain_add`,
13
+ * `rules_*`) on the given MCP server.
14
+ *
15
+ * @param {{ tool: Function }} server - The MCP server instance.
16
+ * @returns {void}
17
+ */
18
+ export function registerCoreTools(server) {
19
+ server.tool(
20
+ 'statistics_summary',
21
+ 'Control-plane statistics: total zones, total files, total storage size, aggregate request volume, last sync timestamp. The control-plane counterpart to `insights_summary` (which is observability data); this answers "what does the account look like right now".',
22
+ {},
23
+ async () => {
24
+ const res = await api.get('/api/core/statistics', { auth: 'account' });
25
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
26
+ }
27
+ );
28
+
29
+ server.tool(
30
+ 'zone_list',
31
+ 'List all CDN zones (tenant namespaces) with file counts, storage usage, and categories.',
32
+ {},
33
+ async () => {
34
+ const res = await api.get('/api/core/zones', { auth: 'account' });
35
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
36
+ }
37
+ );
38
+
39
+ server.tool(
40
+ 'zone_get',
41
+ 'Get detailed information about a specific zone including all files, categories, formats, and storage usage.',
42
+ { zone_id: z.string().describe('Zone identifier (e.g., "akande", "bankingonai")') },
43
+ async ({ zone_id }) => {
44
+ const res = await api.get(`/api/core/zones/${zone_id}`, { auth: 'account' });
45
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
46
+ }
47
+ );
48
+
49
+ server.tool(
50
+ 'zone_create',
51
+ 'Create a new CDN zone with standard v1/ directory scaffolding (banners, github, icons, logos, titles). Committed via Git.',
52
+ {
53
+ name: z.string()
54
+ .min(2).max(64)
55
+ .regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/)
56
+ .describe('Zone name (2-64 chars, lowercase alphanumeric and hyphens)'),
57
+ },
58
+ async ({ name }) => {
59
+ const res = await api.post('/api/core/zones', { Name: name }, { auth: 'account' });
60
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
61
+ }
62
+ );
63
+
64
+ server.tool(
65
+ 'zone_delete',
66
+ 'Delete an entire zone and all its files. This is DESTRUCTIVE and cannot be undone. Initiates edge cache purge.',
67
+ { zone_id: z.string().describe('Zone identifier to delete') },
68
+ async ({ zone_id }) => {
69
+ const res = await api.del(`/api/core/zones/${zone_id}`, { auth: 'account' });
70
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
71
+ }
72
+ );
73
+
74
+ server.tool(
75
+ 'domain_add',
76
+ 'Add a custom domain to a zone. SSL/TLS certificate is provisioned automatically. Create a CNAME record pointing to cloudcdn-pro.pages.dev.',
77
+ {
78
+ zone_id: z.string().describe('Zone to attach the domain to'),
79
+ hostname: z.string().describe('Fully qualified domain (e.g., "cdn.example.com")'),
80
+ },
81
+ async ({ zone_id, hostname }) => {
82
+ const res = await api.post(`/api/core/zones/${zone_id}/domains`, { Hostname: hostname }, { auth: 'account' });
83
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
84
+ }
85
+ );
86
+
87
+ server.tool(
88
+ 'rules_get',
89
+ 'Read the current Cloudflare Pages edge configuration files (_headers and _redirects).',
90
+ {},
91
+ async () => {
92
+ const res = await api.get('/api/core/rules', { auth: 'account' });
93
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
94
+ }
95
+ );
96
+
97
+ server.tool(
98
+ 'rules_update',
99
+ 'Update the _headers or _redirects edge configuration file. Committed via Git, deployed in ~60-90 seconds.',
100
+ {
101
+ file: z.enum(['_headers', '_redirects']).describe('Which edge config file to update'),
102
+ content: z.string().max(100000).describe('Full file content'),
103
+ },
104
+ async ({ file, content }) => {
105
+ const res = await api.post('/api/core/rules', { File: file, Content: content }, { auth: 'account' });
106
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
107
+ }
108
+ );
109
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Edge-delivery MCP tools — transforms, cache purge, signed URLs, HLS
3
+ * playlists, and the asset pipeline.
4
+ *
5
+ * Auth is per-tool: `transform_image` and `stream_playlist` are public,
6
+ * `cache_purge` uses PurgeKey, `signed_url_generate` and `pipeline_ingest`
7
+ * use AccountKey.
8
+ *
9
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/delivery
10
+ */
11
+
12
+ import { z } from 'zod';
13
+ import * as api from '../api-client.js';
14
+
15
+ /**
16
+ * Register the delivery tools (`transform_image`, `cache_purge`,
17
+ * `signed_url_generate`, `stream_playlist`, `pipeline_ingest`) on the
18
+ * given MCP server.
19
+ *
20
+ * @param {{ tool: Function }} server - The MCP server instance.
21
+ * @returns {void}
22
+ */
23
+ export function registerDeliveryTools(server) {
24
+ server.tool(
25
+ 'transform_image',
26
+ 'Generate a transformed image URL with resize, format conversion, blur, and sharpen. Returns the CDN URL (does not download the image). Rate limited to 50,000/month.',
27
+ {
28
+ url: z.string().describe('Relative asset path (e.g., "/akande/v1/logos/akande.svg")'),
29
+ w: z.number().int().min(1).max(8192).optional().describe('Width in pixels'),
30
+ h: z.number().int().min(1).max(8192).optional().describe('Height in pixels'),
31
+ fit: z.enum(['cover', 'contain', 'fill', 'inside', 'outside']).optional(),
32
+ format: z.enum(['auto', 'webp', 'avif', 'png', 'jpeg']).optional(),
33
+ q: z.number().int().min(1).max(100).optional().describe('Quality (1-100)'),
34
+ blur: z.number().int().min(1).max(250).optional(),
35
+ sharpen: z.number().int().min(1).max(10).optional(),
36
+ gravity: z.enum([
37
+ 'center', 'north', 'south', 'east', 'west',
38
+ 'northeast', 'northwest', 'southeast', 'southwest',
39
+ 'face', 'auto',
40
+ ]).optional(),
41
+ },
42
+ async (params) => {
43
+ // Build the transform URL for the agent to use/reference
44
+ const queryParams = {};
45
+ for (const [k, v] of Object.entries(params)) {
46
+ if (v !== undefined) queryParams[k] = v;
47
+ }
48
+ const transformUrl = new URL('/api/transform', api.BASE_URL);
49
+ for (const [k, v] of Object.entries(queryParams)) {
50
+ transformUrl.searchParams.set(k, String(v));
51
+ }
52
+
53
+ // Also verify it works by making the request
54
+ const res = await api.get('/api/transform', { params: queryParams });
55
+ const result = {
56
+ transform_url: transformUrl.toString(),
57
+ status: res.status,
58
+ ok: res.ok,
59
+ content_type: res.data?.contentType || null,
60
+ };
61
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
62
+ }
63
+ );
64
+
65
+ server.tool(
66
+ 'cache_purge',
67
+ 'Purge the Cloudflare CDN cache by specific URLs, surrogate tags, or purge everything. Rate limited to 100/day. Exactly one of urls, tags, or purge_everything must be provided.',
68
+ {
69
+ urls: z.array(z.string()).max(30).optional().describe('Array of full URLs to purge (must start with https://cloudcdn.pro/)'),
70
+ tags: z.array(z.string()).max(30).optional().describe('Array of cache tags to purge (alphanumeric with hyphens)'),
71
+ purge_everything: z.boolean().optional().describe('Set true to purge ALL cached content'),
72
+ },
73
+ async ({ urls, tags, purge_everything }) => {
74
+ const body = {};
75
+ if (purge_everything) body.purge_everything = true;
76
+ else if (urls) body.urls = urls;
77
+ else if (tags) body.tags = tags;
78
+
79
+ const res = await api.post('/api/purge', body, { auth: 'purge' });
80
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
81
+ }
82
+ );
83
+
84
+ server.tool(
85
+ 'signed_url_generate',
86
+ 'Mint a time-limited HMAC-signed URL for a protected asset. Returns `{ url, expiresAt }` where `url` is ready to share; the signature is verified at the edge with constant-time HMAC-SHA256. Use for paywalled downloads, expiring sharable links, or any asset you want gated without baking auth into every request.',
87
+ {
88
+ path: z.string().min(1).max(1024).describe('Relative asset path (e.g. "/clients/acme/private/report.pdf")'),
89
+ expires_in: z.number().int().min(60).max(60 * 60 * 24 * 30).default(3600).optional()
90
+ .describe('TTL in seconds. Default 3600 (1 hour); max 30 days.'),
91
+ },
92
+ async ({ path, expires_in = 3600 }) => {
93
+ const expires = Math.floor(Date.now() / 1000) + expires_in;
94
+ const res = await api.post('/api/signed', { path, expires }, { auth: 'account' });
95
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
96
+ }
97
+ );
98
+
99
+ server.tool(
100
+ 'stream_playlist',
101
+ 'Build the HLS playlist URL for an adaptive-bitrate video. Returns `{ playlist_url }` ready for an HLS-compatible player. The edge serves an `.m3u8` index plus byte-range segments; bandwidth-adaptive selection happens client-side.',
102
+ {
103
+ asset_id: z.string().min(1).describe('Video asset ID or relative path (e.g. "/stocks/promo/launch.mp4")'),
104
+ },
105
+ async ({ asset_id }) => {
106
+ const res = await api.get('/api/stream', { params: { id: asset_id } });
107
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
108
+ }
109
+ );
110
+
111
+ server.tool(
112
+ 'pipeline_ingest',
113
+ 'Scaffold a full zone from a single SVG upload. Creates logos, icons, favicon, and directory structure in a single atomic commit.',
114
+ {
115
+ mode: z.enum(['client', 'stock']).describe('"client" creates a full zone scaffold, "stock" uploads to shared library'),
116
+ name: z.string().optional().describe('Zone name (required for client mode)'),
117
+ svg: z.string().describe('Base64-encoded SVG content'),
118
+ generateFavicon: z.boolean().default(true).optional(),
119
+ generateIcons: z.boolean().default(true).optional(),
120
+ generateBanners: z.boolean().default(true).optional(),
121
+ },
122
+ async (params) => {
123
+ const res = await api.post('/api/pipeline', params, { auth: 'account' });
124
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
125
+ }
126
+ );
127
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Insights / analytics MCP tools — summary, top assets, geography, errors,
3
+ * per-asset roll-ups, and the long-retention audit log.
4
+ *
5
+ * The first five are AccessKey-gated (observability data); `audit_logs`
6
+ * is AccountKey-gated (control-plane history).
7
+ *
8
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/insights
9
+ */
10
+
11
+ import { z } from 'zod';
12
+ import * as api from '../api-client.js';
13
+
14
+ const daysParam = z.number().int().min(1).max(90).default(7).optional().describe('Number of days to query (1-90)');
15
+
16
+ /**
17
+ * Register the insights tools (`insights_*`, `audit_logs`) on the given
18
+ * MCP server.
19
+ *
20
+ * @param {{ tool: Function }} server - The MCP server instance.
21
+ * @returns {void}
22
+ */
23
+ export function registerInsightsTools(server) {
24
+ server.tool(
25
+ 'insights_summary',
26
+ 'Get aggregated analytics: total requests, bandwidth, cache hit rate, and unique countries over a time window.',
27
+ {
28
+ days: daysParam,
29
+ zone: z.string().optional().describe('Optional zone filter'),
30
+ },
31
+ async (params) => {
32
+ const res = await api.get('/api/insights/summary', { auth: 'access', params });
33
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
34
+ }
35
+ );
36
+
37
+ server.tool(
38
+ 'insights_top_assets',
39
+ 'Get the most requested assets ranked by hit count.',
40
+ {
41
+ days: daysParam,
42
+ limit: z.number().int().min(1).max(100).default(20).optional().describe('Number of top assets to return'),
43
+ },
44
+ async (params) => {
45
+ const res = await api.get('/api/insights/top-assets', { auth: 'access', params });
46
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
47
+ }
48
+ );
49
+
50
+ server.tool(
51
+ 'insights_geography',
52
+ 'Get request distribution by country code.',
53
+ { days: daysParam },
54
+ async (params) => {
55
+ const res = await api.get('/api/insights/geography', { auth: 'access', params });
56
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
57
+ }
58
+ );
59
+
60
+ server.tool(
61
+ 'insights_errors',
62
+ 'Get error tracking data: breakdown by HTTP status code with the most affected paths.',
63
+ { days: daysParam },
64
+ async (params) => {
65
+ const res = await api.get('/api/insights/errors', { auth: 'access', params });
66
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
67
+ }
68
+ );
69
+
70
+ server.tool(
71
+ 'insights_asset',
72
+ 'Get per-asset analytics for a single path: request count, daily breakdown, and error roll-up by HTTP status. Path matching is tolerant of leading-slash differences.',
73
+ {
74
+ path: z.string().min(1).max(2048).describe('Asset path to query (e.g. "clients/akande/v1/logos/logo.svg" — leading slash optional)'),
75
+ days: daysParam,
76
+ },
77
+ async (params) => {
78
+ const res = await api.get('/api/insights/asset', { auth: 'access', params });
79
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
80
+ }
81
+ );
82
+
83
+ server.tool(
84
+ 'audit_logs',
85
+ 'Read the persistent control-plane audit trail: token creates/revokes, webhook registers/deletes, zone creates, successful purges. Records carry timestamp, action, client IP, user agent, request trace ID, and action-specific metadata. AccountKey-gated.',
86
+ {
87
+ days: z.number().int().min(1).max(90).default(7).optional().describe('Days to look back (1-90)'),
88
+ action: z.string().optional().describe('Filter to a single action (e.g. "token.create", "webhook.delete", "zone.create", "purge.execute")'),
89
+ limit: z.number().int().min(1).max(5000).default(500).optional().describe('Max entries to return (1-5000)'),
90
+ },
91
+ async (params) => {
92
+ const res = await api.get('/api/core/audit-logs', { auth: 'account', params });
93
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
94
+ }
95
+ );
96
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Operations / log-query MCP tools.
3
+ *
4
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/logs
5
+ */
6
+
7
+ import { z } from 'zod';
8
+ import * as api from '../api-client.js';
9
+
10
+ /**
11
+ * Register the log tools (`logs_query`) on the given MCP server.
12
+ *
13
+ * @param {{ tool: Function }} server - The MCP server instance.
14
+ * @returns {void}
15
+ */
16
+ export function registerLogTools(server) {
17
+ server.tool(
18
+ 'logs_query',
19
+ 'Query historical log entries with optional level filter. Returns structured JSON logs.',
20
+ {
21
+ days: z.number().int().min(1).max(7).default(1).optional().describe('Number of days to query'),
22
+ level: z.enum(['info', 'warn', 'error']).optional().describe('Filter by log level'),
23
+ limit: z.number().int().min(1).max(500).default(100).optional(),
24
+ },
25
+ async ({ days, level, limit }) => {
26
+ const params = {};
27
+ if (days) params.days = days;
28
+ if (level) params.level = level;
29
+ if (limit) params.limit = limit;
30
+ const res = await api.get('/api/logs', { auth: 'account', params });
31
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
32
+ }
33
+ );
34
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Storage-plane MCP tools — read/write the Bunny.net-compatible object store
3
+ * that backs CloudCDN. AccessKey auth on every call.
4
+ *
5
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/storage
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import * as api from '../api-client.js';
10
+
11
+ /**
12
+ * Register the storage tools (`storage_list`, `storage_upload`,
13
+ * `storage_delete`, `storage_batch_upload`) on the given MCP server.
14
+ *
15
+ * @param {{ tool: Function }} server - The MCP server instance.
16
+ * @returns {void}
17
+ */
18
+ export function registerStorageTools(server) {
19
+ server.tool(
20
+ 'storage_list',
21
+ 'List files and subdirectories at a storage path. Returns Bunny.net-compatible file entries with names, sizes, and timestamps.',
22
+ { path: z.string().describe('Directory path to list (e.g., "clients/akande/v1/logos/")') },
23
+ async ({ path }) => {
24
+ const res = await api.get(`/api/storage/${path}`, { auth: 'access' });
25
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
26
+ }
27
+ );
28
+
29
+ server.tool(
30
+ 'storage_upload',
31
+ 'Upload a file to the CDN. The file is committed via Git and deployed to 300+ edge locations in ~60-90 seconds.',
32
+ {
33
+ path: z.string().describe('Destination path (e.g., "clients/akande/v1/logos/logo.svg")'),
34
+ content_base64: z.string().describe('File content encoded as base64'),
35
+ checksum: z.string().optional().describe('Optional SHA-256 hex checksum for integrity verification'),
36
+ },
37
+ async ({ path, content_base64, checksum }) => {
38
+ const body = Uint8Array.from(atob(content_base64), c => c.charCodeAt(0));
39
+ const headers = {};
40
+ if (checksum) headers.Checksum = checksum;
41
+ const res = await api.put(`/api/storage/${path}`, body, { auth: 'access', headers });
42
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
43
+ }
44
+ );
45
+
46
+ server.tool(
47
+ 'storage_delete',
48
+ 'Delete a file from the CDN. Removes it from the repository and initiates an edge cache purge.',
49
+ { path: z.string().describe('File path to delete (e.g., "clients/akande/v1/logos/old.svg")') },
50
+ async ({ path }) => {
51
+ const res = await api.del(`/api/storage/${path}`, { auth: 'access' });
52
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
53
+ }
54
+ );
55
+
56
+ server.tool(
57
+ 'storage_batch_upload',
58
+ 'Upload up to 50 files in a single atomic Git commit. More efficient than individual uploads for multiple assets.',
59
+ {
60
+ files: z.array(z.object({
61
+ path: z.string().describe('Destination path for this file'),
62
+ content: z.string().describe('Base64-encoded file content'),
63
+ encoding: z.enum(['base64']).default('base64'),
64
+ })).max(50).describe('Array of file objects to upload'),
65
+ },
66
+ async ({ files }) => {
67
+ const res = await api.post('/api/storage/batch', { files }, { auth: 'access' });
68
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
69
+ }
70
+ );
71
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Scoped-API-token MCP tools — list, create, and revoke tokens. Secrets
3
+ * are SHA-256 hashed at rest and returned to the caller exactly once at
4
+ * creation time. AccountKey auth on every call.
5
+ *
6
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/tokens
7
+ */
8
+
9
+ import { z } from 'zod';
10
+ import * as api from '../api-client.js';
11
+
12
+ /**
13
+ * Register the token tools (`token_list`, `token_create`, `token_revoke`)
14
+ * on the given MCP server.
15
+ *
16
+ * @param {{ tool: Function }} server - The MCP server instance.
17
+ * @returns {void}
18
+ */
19
+ export function registerTokenTools(server) {
20
+ server.tool(
21
+ 'token_list',
22
+ 'List all API tokens (secrets redacted, only prefix shown).',
23
+ {},
24
+ async () => {
25
+ const res = await api.get('/api/tokens', { auth: 'account' });
26
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
27
+ }
28
+ );
29
+
30
+ server.tool(
31
+ 'token_create',
32
+ 'Create a new scoped API token. The secret is returned ONCE and cannot be retrieved again.',
33
+ {
34
+ name: z.string().max(100).describe('Human-readable token name (e.g., "CI deploy bot")'),
35
+ scopes: z.array(z.enum([
36
+ 'storage:read', 'storage:write', 'assets:read', 'insights:read',
37
+ 'zones:read', 'zones:write', 'purge:write', 'pipeline:write',
38
+ 'webhooks:read', 'webhooks:write',
39
+ ])).describe('Permission scopes for this token'),
40
+ expiresInDays: z.number().int().min(1).max(365).default(90).optional().describe('Token lifetime in days'),
41
+ },
42
+ async ({ name, scopes, expiresInDays }) => {
43
+ const res = await api.post('/api/tokens', { name, scopes, expiresInDays }, { auth: 'account' });
44
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
45
+ }
46
+ );
47
+
48
+ server.tool(
49
+ 'token_revoke',
50
+ 'Revoke an API token by its ID. The token will immediately stop working.',
51
+ { id: z.string().describe('Token ID to revoke') },
52
+ async ({ id }) => {
53
+ const res = await api.del('/api/tokens', { auth: 'account', params: { id } });
54
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
55
+ }
56
+ );
57
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Webhook MCP tools — list, create, and delete webhook subscriptions.
3
+ * Deliveries are HMAC-SHA256 signed; the consumer Worker takes care of
4
+ * the retry + DLQ flow. AccountKey auth on every call.
5
+ *
6
+ * @module @sebastienrousseau/cloudcdn-mcp/lib/tools/webhooks
7
+ */
8
+
9
+ import { z } from 'zod';
10
+ import * as api from '../api-client.js';
11
+
12
+ /**
13
+ * Register the webhook tools (`webhook_list`, `webhook_create`,
14
+ * `webhook_delete`) on the given MCP server.
15
+ *
16
+ * @param {{ tool: Function }} server - The MCP server instance.
17
+ * @returns {void}
18
+ */
19
+ export function registerWebhookTools(server) {
20
+ server.tool(
21
+ 'webhook_list',
22
+ 'List all registered webhooks.',
23
+ {},
24
+ async () => {
25
+ const res = await api.get('/api/webhooks', { auth: 'account' });
26
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
27
+ }
28
+ );
29
+
30
+ server.tool(
31
+ 'webhook_create',
32
+ 'Register a new webhook for CDN events (asset.created, asset.deleted, zone.created, etc.). Supports HMAC-SHA256 signed delivery.',
33
+ {
34
+ url: z.string().describe('Webhook delivery URL (must be https://)'),
35
+ events: z.array(z.enum([
36
+ 'asset.created', 'asset.deleted', 'asset.updated',
37
+ 'zone.created', 'zone.deleted',
38
+ 'purge.completed', 'pipeline.completed',
39
+ ])).describe('Events to subscribe to'),
40
+ secret: z.string().optional().describe('Optional HMAC secret for signature verification'),
41
+ },
42
+ async ({ url, events, secret }) => {
43
+ const body = { url, events };
44
+ if (secret) body.secret = secret;
45
+ const res = await api.post('/api/webhooks', body, { auth: 'account' });
46
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
47
+ }
48
+ );
49
+
50
+ server.tool(
51
+ 'webhook_delete',
52
+ 'Remove a registered webhook by its ID.',
53
+ { id: z.string().describe('Webhook ID to remove') },
54
+ async ({ id }) => {
55
+ const res = await api.del('/api/webhooks', { auth: 'account', params: { id } });
56
+ return { content: [{ type: 'text', text: JSON.stringify(res.data, null, 2) }] };
57
+ }
58
+ );
59
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@sebastienrousseau/cloudcdn-mcp",
3
+ "version": "0.1.1",
4
+ "description": "MCP server for CloudCDN \u2014 lets AI agents autonomously manage digital assets, zones, image transforms, cache, and edge analytics.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./server": "./server.js",
10
+ "./api-client": "./lib/api-client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "bin": {
14
+ "cloudcdn-mcp": "./index.js"
15
+ },
16
+ "files": [
17
+ "index.js",
18
+ "server.js",
19
+ "lib/",
20
+ "README.md",
21
+ "LICENSE-APACHE",
22
+ "LICENSE-MIT"
23
+ ],
24
+ "scripts": {
25
+ "start": "node index.js",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:coverage": "vitest run --coverage"
29
+ },
30
+ "keywords": [
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "cloudcdn",
34
+ "cdn",
35
+ "cloudflare",
36
+ "ai-agents",
37
+ "claude",
38
+ "cursor",
39
+ "windsurf",
40
+ "vectorize"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.12.0",
47
+ "zod": "4.6.7"
48
+ },
49
+ "devDependencies": {
50
+ "@vitest/coverage-v8": "4.1.12",
51
+ "vitest": "4.1.12"
52
+ },
53
+ "overrides": {
54
+ "hono": ">=4.13.11 <5.0.0",
55
+ "@hono/node-server": "^1.19.15",
56
+ "ip-address": ">=10.7.4 <11.0.0",
57
+ "vite": ">=8.1.6 <9.0.0",
58
+ "fast-uri": "3.1.8",
59
+ "nanoid": "^5.1.7",
60
+ "qs": "^6.16.0"
61
+ },
62
+ "homepage": "https://cloudcdn.pro",
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/sebastienrousseau/cloudcdn.pro.git",
66
+ "directory": "mcp"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/sebastienrousseau/cloudcdn.pro/issues"
70
+ },
71
+ "author": "Sebastien Rousseau",
72
+ "license": "Apache-2.0 OR MIT"
73
+ }