@se-studio/contentful-cms 3.0.2 → 3.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/CHANGELOG.md +12 -0
- package/dist/commands/asset.d.ts.map +1 -1
- package/dist/commands/asset.js +40 -7
- package/dist/commands/asset.js.map +1 -1
- package/dist/commands/host-scaffold.d.ts.map +1 -1
- package/dist/commands/host-scaffold.js +2 -0
- package/dist/commands/host-scaffold.js.map +1 -1
- package/dist/generated/hosted-guide.d.ts +1 -1
- package/dist/generated/hosted-guide.d.ts.map +1 -1
- package/dist/generated/hosted-guide.js +1 -1
- package/dist/generated/hosted-guide.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/hosted/index.d.ts +3 -0
- package/dist/hosted/index.d.ts.map +1 -1
- package/dist/hosted/index.js +3 -0
- package/dist/hosted/index.js.map +1 -1
- package/dist/hosted/next/redis.d.ts.map +1 -1
- package/dist/hosted/next/redis.js +4 -0
- package/dist/hosted/next/redis.js.map +1 -1
- package/dist/hosted/next/routes/mcp.d.ts.map +1 -1
- package/dist/hosted/next/routes/mcp.js +2 -0
- package/dist/hosted/next/routes/mcp.js.map +1 -1
- package/dist/hosted/next/routes/staged-upload.d.ts +2 -0
- package/dist/hosted/next/routes/staged-upload.d.ts.map +1 -0
- package/dist/hosted/next/routes/staged-upload.js +83 -0
- package/dist/hosted/next/routes/staged-upload.js.map +1 -0
- package/dist/mcp/http-handler.d.ts.map +1 -1
- package/dist/mcp/http-handler.js +19 -19
- package/dist/mcp/http-handler.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/cms-edit.d.ts +1 -1
- package/dist/mcp/tools/cms-edit.d.ts.map +1 -1
- package/dist/mcp/tools/cms-edit.js +3 -1
- package/dist/mcp/tools/cms-edit.js.map +1 -1
- package/dist/mcp/tools/staged-upload.d.ts +4 -0
- package/dist/mcp/tools/staged-upload.d.ts.map +1 -0
- package/dist/mcp/tools/staged-upload.js +77 -0
- package/dist/mcp/tools/staged-upload.js.map +1 -0
- package/dist/runtime/context.d.ts +3 -0
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js.map +1 -1
- package/dist/session/session-store.d.ts +2 -0
- package/dist/session/session-store.d.ts.map +1 -1
- package/dist/session/session-store.js.map +1 -1
- package/dist/staged-upload/hmac.d.ts +6 -0
- package/dist/staged-upload/hmac.d.ts.map +1 -0
- package/dist/staged-upload/hmac.js +43 -0
- package/dist/staged-upload/hmac.js.map +1 -0
- package/dist/staged-upload/limits.d.ts +6 -0
- package/dist/staged-upload/limits.d.ts.map +1 -0
- package/dist/staged-upload/limits.js +9 -0
- package/dist/staged-upload/limits.js.map +1 -0
- package/dist/staged-upload/mime.d.ts +2 -0
- package/dist/staged-upload/mime.d.ts.map +1 -0
- package/dist/staged-upload/mime.js +10 -0
- package/dist/staged-upload/mime.js.map +1 -0
- package/dist/staged-upload/request.d.ts +11 -0
- package/dist/staged-upload/request.d.ts.map +1 -0
- package/dist/staged-upload/request.js +40 -0
- package/dist/staged-upload/request.js.map +1 -0
- package/dist/staged-upload/store.d.ts +19 -0
- package/dist/staged-upload/store.d.ts.map +1 -0
- package/dist/staged-upload/store.js +124 -0
- package/dist/staged-upload/store.js.map +1 -0
- package/dist/staged-upload/types.d.ts +27 -0
- package/dist/staged-upload/types.d.ts.map +1 -0
- package/dist/staged-upload/types.js +2 -0
- package/dist/staged-upload/types.js.map +1 -0
- package/docs/editor-tasks/media-reuse-and-upload.md +58 -5
- package/docs/mcp/hosted-guide.md +33 -3
- package/package.json +1 -1
- package/skills/core/SKILL.md +4 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RedisClient } from '../session/session-store.js';
|
|
2
|
+
import type { StagedUploadConsumeResult, StagedUploadRecord } from './types.js';
|
|
3
|
+
export declare class StagedUploadStore {
|
|
4
|
+
private readonly redis;
|
|
5
|
+
private readonly keyPrefix;
|
|
6
|
+
constructor(redis: RedisClient, keyPrefix?: string);
|
|
7
|
+
private fullKey;
|
|
8
|
+
assertRateLimit(userId: string): Promise<void>;
|
|
9
|
+
createPending(params: {
|
|
10
|
+
userId: string;
|
|
11
|
+
fileName: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
byteLength?: number;
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
storeFile(userId: string, uploadId: string, buffer: Buffer, mimeType: string): Promise<StagedUploadRecord>;
|
|
16
|
+
consume(userId: string, uploadId: string): Promise<StagedUploadConsumeResult>;
|
|
17
|
+
getRecord(userId: string, uploadId: string): Promise<StagedUploadRecord | null>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/staged-upload/store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAO/D,OAAO,KAAK,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA0BhF,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,KAAK,EAAE,WAAW,EAClB,SAAS,SAAa;IAGzC,OAAO,CAAC,OAAO;IAIT,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc9C,aAAa,CAAC,MAAM,EAAE;QAC1B,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqCb,SAAS,CACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC;IAgCxB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAuB7E,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;CAGtF"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { assertUploadBufferSize } from '../contentful/upload-limits.js';
|
|
3
|
+
import { hostedStagedMaxBytes, STAGED_UPLOAD_RATE_LIMIT_PER_HOUR, STAGED_UPLOAD_TTL_SECONDS, } from './limits.js';
|
|
4
|
+
import { isAllowedStagedMimeType } from './mime.js';
|
|
5
|
+
const KEY_PREFIX = 'cms-edit:staged:';
|
|
6
|
+
const RATE_PREFIX = 'cms-edit:staged-rate:';
|
|
7
|
+
function rateKey(userId, hourBucket) {
|
|
8
|
+
return `${RATE_PREFIX}${userId}:${String(hourBucket)}`;
|
|
9
|
+
}
|
|
10
|
+
function parseRecord(raw) {
|
|
11
|
+
if (!raw) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (typeof raw === 'string') {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (typeof raw === 'object') {
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export class StagedUploadStore {
|
|
28
|
+
redis;
|
|
29
|
+
keyPrefix;
|
|
30
|
+
constructor(redis, keyPrefix = KEY_PREFIX) {
|
|
31
|
+
this.redis = redis;
|
|
32
|
+
this.keyPrefix = keyPrefix;
|
|
33
|
+
}
|
|
34
|
+
fullKey(userId, uploadId) {
|
|
35
|
+
return `${this.keyPrefix}${userId}:${uploadId}`;
|
|
36
|
+
}
|
|
37
|
+
async assertRateLimit(userId) {
|
|
38
|
+
const hourBucket = Math.floor(Date.now() / (60 * 60 * 1000));
|
|
39
|
+
const key = rateKey(userId, hourBucket);
|
|
40
|
+
const count = await this.redis.incr(key);
|
|
41
|
+
if (count === 1) {
|
|
42
|
+
await this.redis.expire(key, 60 * 60);
|
|
43
|
+
}
|
|
44
|
+
if (count > STAGED_UPLOAD_RATE_LIMIT_PER_HOUR) {
|
|
45
|
+
throw new Error(`Staged upload rate limit exceeded (${String(STAGED_UPLOAD_RATE_LIMIT_PER_HOUR)} per hour).`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async createPending(params) {
|
|
49
|
+
if (!isAllowedStagedMimeType(params.mimeType)) {
|
|
50
|
+
throw new Error(`MIME type not allowed for staged upload: ${params.mimeType}`);
|
|
51
|
+
}
|
|
52
|
+
if (params.byteLength !== undefined && params.byteLength > hostedStagedMaxBytes()) {
|
|
53
|
+
throw new Error(`Declared byteLength exceeds hosted staged upload limit (${String(hostedStagedMaxBytes())} bytes).`);
|
|
54
|
+
}
|
|
55
|
+
await this.assertRateLimit(params.userId);
|
|
56
|
+
const uploadId = crypto.randomUUID().replace(/-/g, '');
|
|
57
|
+
const record = {
|
|
58
|
+
uploadId,
|
|
59
|
+
userId: params.userId,
|
|
60
|
+
fileName: params.fileName,
|
|
61
|
+
mimeType: params.mimeType,
|
|
62
|
+
byteLength: params.byteLength,
|
|
63
|
+
status: 'pending',
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
const set = await this.redis.set(this.fullKey(params.userId, uploadId), JSON.stringify(record), {
|
|
67
|
+
ex: STAGED_UPLOAD_TTL_SECONDS,
|
|
68
|
+
nx: true,
|
|
69
|
+
});
|
|
70
|
+
if (set !== true) {
|
|
71
|
+
throw new Error('Failed to reserve staged upload slot.');
|
|
72
|
+
}
|
|
73
|
+
return uploadId;
|
|
74
|
+
}
|
|
75
|
+
async storeFile(userId, uploadId, buffer, mimeType) {
|
|
76
|
+
assertUploadBufferSize(buffer);
|
|
77
|
+
if (buffer.length > hostedStagedMaxBytes()) {
|
|
78
|
+
throw new Error(`File exceeds hosted staged upload limit (${String(hostedStagedMaxBytes())} bytes).`);
|
|
79
|
+
}
|
|
80
|
+
if (!isAllowedStagedMimeType(mimeType)) {
|
|
81
|
+
throw new Error(`MIME type not allowed for staged upload: ${mimeType}`);
|
|
82
|
+
}
|
|
83
|
+
const key = this.fullKey(userId, uploadId);
|
|
84
|
+
const existing = parseRecord(await this.redis.get(key));
|
|
85
|
+
if (!existing || existing.userId !== userId || existing.uploadId !== uploadId) {
|
|
86
|
+
throw new Error('Staged upload slot not found or expired.');
|
|
87
|
+
}
|
|
88
|
+
if (existing.status === 'ready') {
|
|
89
|
+
throw new Error('Staged upload already received.');
|
|
90
|
+
}
|
|
91
|
+
const record = {
|
|
92
|
+
...existing,
|
|
93
|
+
status: 'ready',
|
|
94
|
+
mimeType,
|
|
95
|
+
byteLength: buffer.length,
|
|
96
|
+
dataBase64: buffer.toString('base64'),
|
|
97
|
+
};
|
|
98
|
+
await this.redis.set(key, JSON.stringify(record), { ex: STAGED_UPLOAD_TTL_SECONDS });
|
|
99
|
+
return record;
|
|
100
|
+
}
|
|
101
|
+
async consume(userId, uploadId) {
|
|
102
|
+
const key = this.fullKey(userId, uploadId);
|
|
103
|
+
const existing = parseRecord(await this.redis.get(key));
|
|
104
|
+
if (!existing || existing.userId !== userId || existing.uploadId !== uploadId) {
|
|
105
|
+
throw new Error('Staged upload not found or expired.');
|
|
106
|
+
}
|
|
107
|
+
if (existing.status !== 'ready' || !existing.dataBase64) {
|
|
108
|
+
throw new Error('Staged upload has no file data yet. POST the file to uploadUrl first.');
|
|
109
|
+
}
|
|
110
|
+
await this.redis.del(key);
|
|
111
|
+
const buffer = Buffer.from(existing.dataBase64, 'base64');
|
|
112
|
+
assertUploadBufferSize(buffer);
|
|
113
|
+
return {
|
|
114
|
+
buffer,
|
|
115
|
+
fileName: existing.fileName,
|
|
116
|
+
mimeType: existing.mimeType,
|
|
117
|
+
byteLength: buffer.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async getRecord(userId, uploadId) {
|
|
121
|
+
return parseRecord(await this.redis.get(this.fullKey(userId, uploadId)));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/staged-upload/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type StagedUploadStatus = 'pending' | 'ready';
|
|
2
|
+
export interface StagedUploadRecord {
|
|
3
|
+
uploadId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
fileName: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
byteLength?: number;
|
|
8
|
+
status: StagedUploadStatus;
|
|
9
|
+
dataBase64?: string;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface StagedUploadRequestResult {
|
|
13
|
+
uploadId: string;
|
|
14
|
+
uploadUrl: string;
|
|
15
|
+
expiresIn: number;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
maxBytes: number;
|
|
18
|
+
curlCommand: string;
|
|
19
|
+
consumeArgs: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface StagedUploadConsumeResult {
|
|
22
|
+
buffer: Buffer;
|
|
23
|
+
fileName: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
byteLength: number;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/staged-upload/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/staged-upload/types.ts"],"names":[],"mappings":""}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- upload image
|
|
6
6
|
- find existing asset
|
|
7
7
|
- add photo to page or article
|
|
8
|
+
- update person headshot / portrait
|
|
8
9
|
|
|
9
10
|
## Requires capabilities
|
|
10
11
|
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
## Prerequisites
|
|
14
15
|
|
|
15
16
|
- **Always run `index sync` first** if the catalog may be stale
|
|
17
|
+
- On **hosted MCP**, the server has **no filesystem** — use **staged upload**, `--base64`, or `--url` (never a local file path in `args`)
|
|
16
18
|
|
|
17
19
|
## Reuse-first workflow (default)
|
|
18
20
|
|
|
@@ -25,27 +27,78 @@
|
|
|
25
27
|
|
|
26
28
|
## Upload only when no match
|
|
27
29
|
|
|
30
|
+
### Hosted MCP (default)
|
|
31
|
+
|
|
32
|
+
**Prefer staged upload** for files larger than ~500 KB or when base64 in `cms_edit` args fails (agent harness message limits):
|
|
33
|
+
|
|
34
|
+
1. `cms_edit_request_staged_upload` with `fileName`, `mimeType`, optional `byteLength`, optional `title`
|
|
35
|
+
2. Run the returned `curlCommand` in a shell (POST file to `uploadUrl`)
|
|
36
|
+
3. `cms_edit` with the returned `consumeArgs` (e.g. `["asset","upload","--staged","<uploadId>",…]`)
|
|
37
|
+
|
|
38
|
+
Rules:
|
|
39
|
+
|
|
40
|
+
- Staged upload keeps binary **out of MCP args** — only a short `uploadId` is passed to Contentful
|
|
41
|
+
- Hosted limit: **4 MB** per file; TTL **15 minutes**; single-use
|
|
42
|
+
- **PNG** when the source has transparency (portraits on brand-colour cards); JPEG flattens alpha
|
|
43
|
+
- Upload alone does **not** attach the asset — follow with **link + save** (below)
|
|
44
|
+
|
|
45
|
+
**Small files:** `--base64` with actual encoded bytes in `args` when the harness accepts the payload:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
cms_edit ["asset", "upload", "--base64", "<encoded-bytes>", "--mime", "image/png", "--file-name", "icon.png", "--title", "…"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Pass **raw base64 text** — not a filesystem path, not `file:///…`.
|
|
52
|
+
|
|
53
|
+
**Last resort `--url`** only when staged upload and base64 are impractical:
|
|
54
|
+
|
|
28
55
|
```
|
|
29
|
-
cms_edit ["asset", "upload", "
|
|
56
|
+
cms_edit ["asset", "upload", "--url", "https://<public-https-url>/file.png", "--mime", "image/png", "--file-name", "headshot-name-1200.png", "--title", "…"]
|
|
30
57
|
```
|
|
31
58
|
|
|
32
|
-
|
|
59
|
+
The URL must be fetchable by the hosted MCP server (public HTTPS). Do not use local-only URLs or tunnel hacks unless the user explicitly approves.
|
|
33
60
|
|
|
34
|
-
Optional Media wrapper:
|
|
61
|
+
Optional Media wrapper (RTF embeds, not person `media`):
|
|
35
62
|
|
|
36
63
|
```
|
|
37
|
-
cms_edit ["asset", "upload", "
|
|
64
|
+
cms_edit ["asset", "upload", "--base64", "<encoded>", "--mime", "image/png", "--file-name", "figure.png", "--with-media", "--media-name", "Figure 1"]
|
|
38
65
|
```
|
|
39
66
|
|
|
67
|
+
Use `--if-exists-by-filename` to avoid duplicate assets when re-running.
|
|
68
|
+
|
|
69
|
+
### Local CLI (only when user approves)
|
|
70
|
+
|
|
71
|
+
Local `cms-edit` may accept `--file` in some versions; hosted agents must not use local CLI without permission (see repo `Agents.md`).
|
|
72
|
+
|
|
73
|
+
## Link uploaded asset to an entry
|
|
74
|
+
|
|
75
|
+
Upload returns an **asset ID**. Attach it, then save:
|
|
76
|
+
|
|
77
|
+
**Person portrait (`media` is an Asset link):**
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
cms_edit ["open", "--id", "<person-entry-id>"]
|
|
81
|
+
cms_edit ["asset", "set", "@root", "media", "<new-asset-id>"]
|
|
82
|
+
cms_edit ["diff"]
|
|
83
|
+
cms_edit ["save"]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Component visual / article featured image:** same pattern — `asset set <ref> <field> <asset-id>` (or `set … --asset`).
|
|
87
|
+
|
|
88
|
+
Always `diff` → `save`. Verify with `preview urls` on the public path.
|
|
89
|
+
|
|
40
90
|
## Confirmation gates
|
|
41
91
|
|
|
42
92
|
1. Report search results before uploading
|
|
43
93
|
2. Confirm upload filename/title with user if ambiguous
|
|
94
|
+
3. After upload, confirm `asset info <id>` shows expected width/height/size before linking
|
|
44
95
|
|
|
45
96
|
## Out of scope
|
|
46
97
|
|
|
47
98
|
- Publish assets (draft upload; human publishes in Contentful UI if needed)
|
|
99
|
+
- Deleting orphan assets from failed uploads (human cleanup in Contentful)
|
|
48
100
|
|
|
49
101
|
## Related resources
|
|
50
102
|
|
|
51
|
-
- `cms-edit://customer/defaults` (featured image rules)
|
|
103
|
+
- `cms-edit://customer/defaults` (featured image rules)
|
|
104
|
+
- `cms-edit://customer/people` (person `media` field, when present)
|
package/docs/mcp/hosted-guide.md
CHANGED
|
@@ -20,6 +20,7 @@ All `save` operations create **drafts only**. A human must review and publish in
|
|
|
20
20
|
| Tool | Purpose |
|
|
21
21
|
|------|---------|
|
|
22
22
|
| `cms_edit` | Run any cms-edit command via an `args` array |
|
|
23
|
+
| `cms_edit_request_staged_upload` | Get a presigned upload URL for large files (then `curl` + `asset upload --staged`) |
|
|
23
24
|
| `cms_edit_version` | Check the installed cms-edit version (no session required) |
|
|
24
25
|
| `cms_edit_help` | Read this guide by topic (`overview`, `workflow`, `commands`, `troubleshooting`, `hosted`) |
|
|
25
26
|
|
|
@@ -125,13 +126,42 @@ Always run `diff` before `save`. To abandon changes: `["discard"]`.
|
|
|
125
126
|
|
|
126
127
|
| Task | Where |
|
|
127
128
|
|------|-------|
|
|
128
|
-
| Edit copy, add sections, create pages/articles | **
|
|
129
|
+
| Edit copy, add sections, create pages/articles | **Agent + cms-edit** (drafts) |
|
|
130
|
+
| Upload new images and files | **Agent + cms-edit** (`asset upload`) — search/reuse first |
|
|
129
131
|
| Publish approved drafts | **Contentful UI** |
|
|
130
|
-
|
|
|
131
|
-
| Edit copy directly in Contentful | **Avoid** — use Claude instead |
|
|
132
|
+
| Edit copy directly in Contentful | **Avoid** — use cms-edit instead |
|
|
132
133
|
|
|
133
134
|
cms-edit saves **drafts only**. A colleague must **publish in Contentful** for changes to go live.
|
|
134
135
|
|
|
136
|
+
## Asset upload (hosted MCP)
|
|
137
|
+
|
|
138
|
+
Hosted connectors have **no server filesystem**. Use one source per upload:
|
|
139
|
+
|
|
140
|
+
| Source | When | Example |
|
|
141
|
+
|--------|------|---------|
|
|
142
|
+
| **Staged upload** (preferred for files >~500 KB) | Agent harness cannot pass large base64 in `cms_edit` args | See **Staged upload** below |
|
|
143
|
+
| **`--base64`** | Small files when inline base64 works | `["asset", "upload", "--base64", "<bytes>", "--mime", "image/png", "--file-name", "photo.png"]` |
|
|
144
|
+
| **`--url`** (last resort) | Public HTTPS URL fetchable by the host | `["asset", "upload", "--url", "https://…/photo.png", "--mime", "image/png", "--file-name", "photo.png"]` |
|
|
145
|
+
|
|
146
|
+
**Required with `--base64`:** `--mime`, `--file-name`. Pass **encoded bytes** in `args` — never a local path (`./file.png`, `file:///tmp/…`) or placeholder string.
|
|
147
|
+
|
|
148
|
+
**After upload:** note the returned asset `id`, attach to the entry (`asset set @root media <id>` for person portraits), then `diff` → `save`. Upload alone does not update entries.
|
|
149
|
+
|
|
150
|
+
**PNG** when transparency matters.
|
|
151
|
+
|
|
152
|
+
Full workflow: `cms-edit://customer/task-media-reuse-and-upload`.
|
|
153
|
+
|
|
154
|
+
### Staged upload (large files)
|
|
155
|
+
|
|
156
|
+
Keeps bulk binary **out of MCP tool args**. The agent uploads via `curl`, then passes only a short `uploadId`.
|
|
157
|
+
|
|
158
|
+
1. `cms_edit_request_staged_upload` with `fileName`, `mimeType`, optional `byteLength`, optional `title`
|
|
159
|
+
2. Run the returned `curlCommand` in a shell (POST multipart to `uploadUrl`)
|
|
160
|
+
3. `cms_edit` with the returned `consumeArgs` (includes `asset upload --staged <uploadId>`)
|
|
161
|
+
4. Link and save as usual
|
|
162
|
+
|
|
163
|
+
Hosted limit: **4 MB** per staged file (Vercel body limit). TTL: **15 minutes**, single-use.
|
|
164
|
+
|
|
135
165
|
## Featured images (articles)
|
|
136
166
|
|
|
137
167
|
Read `cms-edit://customer/defaults` when present. Otherwise:
|
package/package.json
CHANGED
package/skills/core/SKILL.md
CHANGED
|
@@ -369,11 +369,14 @@ cms-edit asset search "hero background"
|
|
|
369
369
|
cms-edit asset search --filename istockphoto-123.jpg
|
|
370
370
|
cms-edit asset search --filename-match 'istockphoto-.*'
|
|
371
371
|
|
|
372
|
-
# Upload from local file, URL, or
|
|
372
|
+
# Upload from local file, URL, base64, or staged ref (hosted MCP)
|
|
373
373
|
cms-edit asset upload ./poster.jpg --title "Poster"
|
|
374
374
|
cms-edit asset upload --url https://example.com/image.jpg --if-exists-by-filename
|
|
375
375
|
cms-edit asset upload --base64 "$B64" --mime image/png --file-name poster.png
|
|
376
376
|
|
|
377
|
+
# Hosted MCP — large files: cms_edit_request_staged_upload → curl uploadUrl → consumeArgs
|
|
378
|
+
# cms_edit ["asset", "upload", "--staged", "<uploadId>", "--mime", "image/png", "--file-name", "photo.png"]
|
|
379
|
+
|
|
377
380
|
# Upload and create a Media wrapper in one step
|
|
378
381
|
cms-edit asset upload ./figure.png --with-media --media-name "Figure 1" --media-position Middle
|
|
379
382
|
|