This is page 3 of 3. Use http://codebase.md/ivo-toby/contentful-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .github
│ └── workflows
│ ├── pr-check.yml
│ └── release.yml
├── .gitignore
├── .npmrc
├── .prettierrc
├── .releaserc
├── bin
│ └── mcp-server.js
├── build.js
├── CLAUDE.md
├── codecompanion-workspace.json
├── Dockerfile
├── eslint.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│ ├── inspect-watch.js
│ └── inspect.js
├── smithery.yaml
├── src
│ ├── config
│ │ ├── ai-actions-client.ts
│ │ └── client.ts
│ ├── handlers
│ │ ├── ai-action-handlers.ts
│ │ ├── asset-handlers.ts
│ │ ├── bulk-action-handlers.ts
│ │ ├── comment-handlers.ts
│ │ ├── content-type-handlers.ts
│ │ ├── entry-handlers.ts
│ │ └── space-handlers.ts
│ ├── index.ts
│ ├── prompts
│ │ ├── ai-actions-invoke.ts
│ │ ├── ai-actions-overview.ts
│ │ ├── contentful-prompts.ts
│ │ ├── generateVariableTypeContent.ts
│ │ ├── handlePrompt.ts
│ │ ├── handlers.ts
│ │ └── promptHandlers
│ │ ├── aiActions.ts
│ │ └── contentful.ts
│ ├── transports
│ │ ├── sse.ts
│ │ └── streamable-http.ts
│ ├── types
│ │ ├── ai-actions.ts
│ │ └── tools.ts
│ └── utils
│ ├── ai-action-tool-generator.ts
│ ├── summarizer.ts
│ ├── to-camel-case.ts
│ └── validation.ts
├── test
│ ├── integration
│ │ ├── ai-action-handler.test.ts
│ │ ├── ai-actions-client.test.ts
│ │ ├── asset-handler.test.ts
│ │ ├── bulk-action-handler.test.ts
│ │ ├── client.test.ts
│ │ ├── comment-handler.test.ts
│ │ ├── content-type-handler.test.ts
│ │ ├── entry-handler.test.ts
│ │ ├── space-handler.test.ts
│ │ └── streamable-http.test.ts
│ ├── msw-setup.ts
│ ├── setup.ts
│ └── unit
│ ├── ai-action-header.test.ts
│ ├── ai-action-tool-generator.test.ts
│ ├── ai-action-tools.test.ts
│ ├── ai-actions.test.ts
│ ├── content-type-handler-merge.test.ts
│ ├── entry-handler-merge.test.ts
│ └── tools.test.ts
├── tsconfig.json
└── vitest.config.ts
```
# Files
--------------------------------------------------------------------------------
/test/msw-setup.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { setupServer } from "msw/node"
2 | import { http, HttpResponse } from "msw"
3 |
4 | // Mock data for bulk actions
5 | const mockBulkAction = {
6 | sys: {
7 | id: "test-bulk-action-id",
8 | status: "succeeded",
9 | version: 1,
10 | },
11 | succeeded: [
12 | { sys: { id: "test-entry-id", type: "Entry" } },
13 | { sys: { id: "test-asset-id", type: "Asset" } },
14 | ],
15 | }
16 |
17 | // Define handlers
18 | export const handlers = [
19 | // List spaces
20 | http.get("https://api.contentful.com/spaces", () => {
21 | return HttpResponse.json({
22 | items: [
23 | {
24 | sys: { id: "test-space-id" },
25 | name: "Test Space",
26 | },
27 | ],
28 | })
29 | }),
30 |
31 | // Get specific space
32 | http.get("https://api.contentful.com/spaces/:spaceId", ({ params }) => {
33 | const { spaceId } = params
34 | if (spaceId === "test-space-id") {
35 | return HttpResponse.json({
36 | sys: { id: "test-space-id" },
37 | name: "Test Space",
38 | })
39 | }
40 | return new HttpResponse(null, { status: 404 })
41 | }),
42 |
43 | // List environments
44 | http.get("https://api.contentful.com/spaces/:spaceId/environments", ({ params }) => {
45 | const { spaceId } = params
46 | if (spaceId === "test-space-id") {
47 | return HttpResponse.json({
48 | items: [
49 | {
50 | sys: { id: "master" },
51 | name: "master",
52 | },
53 | ],
54 | })
55 | }
56 | return new HttpResponse(null, { status: 404 })
57 | }),
58 |
59 | // Create environment
60 | http.post(
61 | "https://api.contentful.com/spaces/:spaceId/environments",
62 | async ({ params, request }) => {
63 | const { spaceId } = params
64 | if (spaceId === "test-space-id") {
65 | try {
66 | // Get data from request body
67 | const body = await request.json()
68 | console.log("Request body:", JSON.stringify(body))
69 |
70 | // In the real API implementation, the environmentId is taken
71 | // from the second argument to client.environment.create
72 | // We need to extract it from the name in our mock
73 | const environmentId = body?.name
74 |
75 | // Return correctly structured response with environment ID
76 | return HttpResponse.json({
77 | sys: { id: environmentId },
78 | name: environmentId,
79 | })
80 | } catch (error) {
81 | console.error("Error processing environment creation:", error)
82 | return new HttpResponse(null, { status: 500 })
83 | }
84 | }
85 | return new HttpResponse(null, { status: 404 })
86 | },
87 | ),
88 |
89 | // Delete environment
90 | http.delete("https://api.contentful.com/spaces/:spaceId/environments/:envId", ({ params }) => {
91 | const { spaceId, envId } = params
92 | if (spaceId === "test-space-id" && envId !== "non-existent-env") {
93 | return new HttpResponse(null, { status: 204 })
94 | }
95 | return new HttpResponse(null, { status: 404 })
96 | }),
97 | ]
98 |
99 | // Bulk action handlers
100 | const bulkActionHandlers = [
101 | // Create bulk publish action
102 | http.post(
103 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/bulk_actions/publish",
104 | async ({ params }) => {
105 | const { spaceId, environmentId } = params
106 | if (spaceId === "test-space-id") {
107 | return HttpResponse.json(
108 | {
109 | ...mockBulkAction,
110 | sys: {
111 | ...mockBulkAction.sys,
112 | id: "test-bulk-action-id",
113 | status: "created",
114 | },
115 | },
116 | { status: 201 },
117 | )
118 | }
119 | return new HttpResponse(null, { status: 404 })
120 | },
121 | ),
122 |
123 | // Create bulk unpublish action
124 | http.post(
125 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/bulk_actions/unpublish",
126 | async ({ params }) => {
127 | const { spaceId, environmentId } = params
128 | if (spaceId === "test-space-id") {
129 | return HttpResponse.json(
130 | {
131 | ...mockBulkAction,
132 | sys: {
133 | ...mockBulkAction.sys,
134 | id: "test-bulk-action-id",
135 | status: "created",
136 | },
137 | },
138 | { status: 201 },
139 | )
140 | }
141 | return new HttpResponse(null, { status: 404 })
142 | },
143 | ),
144 |
145 | // Create bulk validate action
146 | http.post(
147 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/bulk_actions/validate",
148 | async ({ params }) => {
149 | const { spaceId, environmentId } = params
150 | if (spaceId === "test-space-id") {
151 | return HttpResponse.json(
152 | {
153 | ...mockBulkAction,
154 | sys: {
155 | ...mockBulkAction.sys,
156 | id: "test-bulk-action-id",
157 | status: "created",
158 | },
159 | },
160 | { status: 201 },
161 | )
162 | }
163 | return new HttpResponse(null, { status: 404 })
164 | },
165 | ),
166 |
167 | // Get bulk action status
168 | http.get(
169 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/bulk_actions/:bulkActionId",
170 | ({ params }) => {
171 | const { spaceId, bulkActionId } = params
172 | if (spaceId === "test-space-id" && bulkActionId === "test-bulk-action-id") {
173 | return HttpResponse.json(mockBulkAction)
174 | }
175 | return new HttpResponse(null, { status: 404 })
176 | },
177 | ),
178 | ]
179 |
180 | const assetHandlers = [
181 | // Upload asset
182 | http.post(
183 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets",
184 | async ({ request, params }) => {
185 | console.log("MSW: Handling asset creation request")
186 | const { spaceId } = params
187 | if (spaceId === "test-space-id") {
188 | const body = (await request.json()) as {
189 | fields: {
190 | title: { "en-US": string }
191 | description?: { "en-US": string }
192 | file: {
193 | "en-US": {
194 | fileName: string
195 | contentType: string
196 | upload: string
197 | }
198 | }
199 | }
200 | }
201 |
202 | return HttpResponse.json({
203 | sys: {
204 | id: "test-asset-id",
205 | version: 1,
206 | type: "Asset",
207 | },
208 | fields: body.fields,
209 | })
210 | }
211 | return new HttpResponse(null, { status: 404 })
212 | },
213 | ),
214 | // Process asset
215 | http.put(
216 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets/:assetId/files/en-US/process",
217 | ({ params }) => {
218 | console.log("MSW: Handling asset processing request")
219 | const { spaceId, assetId } = params
220 | if (spaceId === "test-space-id" && assetId === "test-asset-id") {
221 | return HttpResponse.json({
222 | sys: {
223 | id: "test-asset-id",
224 | version: 2,
225 | publishedVersion: 1,
226 | },
227 | fields: {
228 | title: { "en-US": "Test Asset" },
229 | description: { "en-US": "Test Description" },
230 | file: {
231 | "en-US": {
232 | fileName: "test.jpg",
233 | contentType: "image/jpeg",
234 | url: "https://example.com/test.jpg",
235 | },
236 | },
237 | },
238 | })
239 | }
240 | return new HttpResponse(null, { status: 404 })
241 | },
242 | ),
243 |
244 | // Get asset
245 | http.get(
246 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets/:assetId",
247 | ({ params }) => {
248 | console.log("MSW: Handling get processed asset request")
249 | const { spaceId, assetId } = params
250 | if (spaceId === "test-space-id" && assetId === "test-asset-id") {
251 | return HttpResponse.json({
252 | sys: {
253 | id: "test-asset-id",
254 | version: 2,
255 | type: "Asset",
256 | publishedVersion: 1,
257 | },
258 | fields: {
259 | title: { "en-US": "Test Asset" },
260 | description: { "en-US": "Test Description" },
261 | file: {
262 | "en-US": {
263 | fileName: "test.jpg",
264 | contentType: "image/jpeg",
265 | url: "https://example.com/test.jpg",
266 | },
267 | },
268 | },
269 | })
270 | }
271 | return new HttpResponse(null, { status: 404 })
272 | },
273 | ),
274 |
275 | // Update asset
276 | http.put(
277 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets/:assetId",
278 | ({ params }) => {
279 | const { spaceId, assetId } = params
280 | if (spaceId === "test-space-id" && assetId === "test-asset-id") {
281 | return HttpResponse.json({
282 | sys: { id: "test-asset-id" },
283 | fields: {
284 | title: { "en-US": "Updated Asset" },
285 | description: { "en-US": "Updated Description" },
286 | },
287 | })
288 | }
289 | return new HttpResponse(null, { status: 404 })
290 | },
291 | ),
292 |
293 | // Delete asset
294 | http.delete(
295 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets/:assetId",
296 | ({ params }) => {
297 | const { spaceId, assetId } = params
298 | if (spaceId === "test-space-id" && assetId === "test-asset-id") {
299 | return new HttpResponse(null, { status: 204 })
300 | }
301 | return new HttpResponse(null, { status: 404 })
302 | },
303 | ),
304 |
305 | // Publish asset
306 | http.put(
307 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets/:assetId/published",
308 | ({ params }) => {
309 | const { spaceId, assetId } = params
310 | if (spaceId === "test-space-id" && assetId === "test-asset-id") {
311 | return HttpResponse.json({
312 | sys: {
313 | id: "test-asset-id",
314 | publishedVersion: 1,
315 | },
316 | })
317 | }
318 | return new HttpResponse(null, { status: 404 })
319 | },
320 | ),
321 |
322 | // Unpublish asset
323 | http.delete(
324 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/assets/:assetId/published",
325 | ({ params }) => {
326 | const { spaceId, assetId } = params
327 | if (spaceId === "test-space-id" && assetId === "test-asset-id") {
328 | return HttpResponse.json({
329 | sys: {
330 | id: "test-asset-id",
331 | },
332 | })
333 | }
334 | return new HttpResponse(null, { status: 404 })
335 | },
336 | ),
337 | ]
338 |
339 | const entryHandlers = [
340 | // Search entries
341 | http.get(
342 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries",
343 | ({ params, request }) => {
344 | const { spaceId } = params
345 | if (spaceId === "test-space-id") {
346 | return HttpResponse.json({
347 | items: [
348 | {
349 | sys: {
350 | id: "test-entry-id",
351 | contentType: { sys: { id: "test-content-type-id" } },
352 | },
353 | fields: {
354 | title: { "en-US": "Test Entry" },
355 | description: { "en-US": "Test Description" },
356 | },
357 | },
358 | ],
359 | })
360 | }
361 | return new HttpResponse(null, { status: 404 })
362 | },
363 | ),
364 |
365 | // Get specific entry
366 | http.get(
367 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries/:entryId",
368 | ({ params }) => {
369 | const { spaceId, entryId } = params
370 | if (spaceId === "test-space-id" && entryId === "test-entry-id") {
371 | return HttpResponse.json({
372 | sys: {
373 | id: "test-entry-id",
374 | contentType: { sys: { id: "test-content-type-id" } },
375 | },
376 | fields: {
377 | title: { "en-US": "Test Entry" },
378 | description: { "en-US": "Test Description" },
379 | },
380 | })
381 | }
382 | return new HttpResponse(null, { status: 404 })
383 | },
384 | ),
385 |
386 | // Create entry
387 | http.post(
388 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries",
389 | async ({ params, request }) => {
390 | const { spaceId } = params
391 | if (spaceId === "test-space-id") {
392 | const contentType = request.headers.get("X-Contentful-Content-Type")
393 | const body = (await request.json()) as {
394 | fields: Record<string, any>
395 | }
396 |
397 | return HttpResponse.json({
398 | sys: {
399 | id: "new-entry-id",
400 | type: "Entry",
401 | contentType: {
402 | sys: {
403 | type: "Link",
404 | linkType: "ContentType",
405 | id: contentType,
406 | },
407 | },
408 | },
409 | fields: body.fields,
410 | })
411 | }
412 | return new HttpResponse(null, { status: 404 })
413 | },
414 | ),
415 |
416 | // Update entry
417 | http.put(
418 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries/:entryId",
419 | async ({ params, request }) => {
420 | const { spaceId, entryId } = params
421 | if (spaceId === "test-space-id" && entryId === "test-entry-id") {
422 | const body = (await request.json()) as {
423 | fields: Record<string, any>
424 | }
425 | return HttpResponse.json({
426 | sys: {
427 | id: entryId,
428 | contentType: { sys: { id: "test-content-type-id" } },
429 | },
430 | fields: body.fields,
431 | })
432 | }
433 | return new HttpResponse(null, { status: 404 })
434 | },
435 | ),
436 |
437 | // Delete entry
438 | http.delete(
439 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries/:entryId",
440 | ({ params }) => {
441 | const { spaceId, entryId } = params
442 | if (spaceId === "test-space-id" && entryId === "test-entry-id") {
443 | return new HttpResponse(null, { status: 204 })
444 | }
445 | return new HttpResponse(null, { status: 404 })
446 | },
447 | ),
448 |
449 | // Publish entry
450 | http.put(
451 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries/:entryId/published",
452 | ({ params }) => {
453 | const { spaceId, entryId } = params
454 | if (spaceId === "test-space-id" && entryId === "test-entry-id") {
455 | return HttpResponse.json({
456 | sys: {
457 | id: entryId,
458 | publishedVersion: 1,
459 | },
460 | })
461 | }
462 | return new HttpResponse(null, { status: 404 })
463 | },
464 | ),
465 |
466 | // Unpublish entry
467 | http.delete(
468 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/entries/:entryId/published",
469 | ({ params }) => {
470 | const { spaceId, entryId } = params
471 | if (spaceId === "test-space-id" && entryId === "test-entry-id") {
472 | return HttpResponse.json({
473 | sys: { id: entryId },
474 | })
475 | }
476 | return new HttpResponse(null, { status: 404 })
477 | },
478 | ),
479 | ]
480 |
481 | const contentTypeHandlers = [
482 | // List content types
483 | http.get(
484 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types",
485 | ({ params }) => {
486 | const { spaceId } = params
487 | if (spaceId === "test-space-id") {
488 | return HttpResponse.json({
489 | items: [
490 | {
491 | sys: { id: "test-content-type-id" },
492 | name: "Test Content Type",
493 | fields: [
494 | {
495 | id: "title",
496 | name: "Title",
497 | type: "Text",
498 | required: true,
499 | },
500 | ],
501 | },
502 | ],
503 | })
504 | }
505 | return new HttpResponse(null, { status: 404 })
506 | },
507 | ),
508 |
509 | // Get specific content type
510 | http.get(
511 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types/:contentTypeId",
512 | ({ params }) => {
513 | const { spaceId, contentTypeId } = params
514 | if (spaceId === "test-space-id" && contentTypeId === "test-content-type-id") {
515 | return HttpResponse.json({
516 | sys: { id: "test-content-type-id" },
517 | name: "Test Content Type",
518 | fields: [
519 | {
520 | id: "title",
521 | name: "Title",
522 | type: "Text",
523 | required: true,
524 | },
525 | ],
526 | })
527 | }
528 | return new HttpResponse(null, { status: 404 })
529 | },
530 | ),
531 |
532 | // Create content type
533 | http.post(
534 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types",
535 | async ({ params, request }) => {
536 | const { spaceId } = params
537 | if (spaceId === "test-space-id") {
538 | const body = (await request.json()) as {
539 | name: string
540 | fields: Array<{
541 | id: string
542 | name: string
543 | type: string
544 | required?: boolean
545 | }>
546 | description?: string
547 | displayField?: string
548 | }
549 |
550 | return HttpResponse.json({
551 | sys: { id: "new-content-type-id" },
552 | name: body.name,
553 | fields: body.fields,
554 | description: body.description,
555 | displayField: body.displayField,
556 | })
557 | }
558 | return new HttpResponse(null, { status: 404 })
559 | },
560 | ),
561 |
562 | // create content type with ID
563 | http.put(
564 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types/:contentTypeId",
565 | async ({ params, request }) => {
566 | const { spaceId, contentTypeId } = params
567 | if (spaceId === "test-space-id") {
568 | const body = (await request.json()) as {
569 | name: string
570 | fields: Array<{
571 | id: string
572 | name: string
573 | type: string
574 | required?: boolean
575 | }>
576 | description?: string
577 | displayField?: string
578 | }
579 |
580 | return HttpResponse.json({
581 | sys: { id: contentTypeId },
582 | name: body.name,
583 | fields: body.fields,
584 | description: body.description,
585 | displayField: body.displayField,
586 | })
587 | }
588 | return new HttpResponse(null, { status: 404 })
589 | },
590 | ),
591 |
592 | // Publish content type
593 | http.put(
594 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types/:contentTypeId/published",
595 | ({ params }) => {
596 | const { spaceId, contentTypeId } = params
597 | if (spaceId === "test-space-id" && contentTypeId === "test-content-type-id") {
598 | return HttpResponse.json({
599 | sys: {
600 | id: contentTypeId,
601 | version: 1,
602 | publishedVersion: 1,
603 | },
604 | name: "Test Content Type",
605 | fields: [
606 | {
607 | id: "title",
608 | name: "Title",
609 | type: "Text",
610 | required: true,
611 | },
612 | ],
613 | })
614 | }
615 | return new HttpResponse(null, { status: 404 })
616 | },
617 | ),
618 |
619 | // Update content type
620 | http.put(
621 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types/:contentTypeId",
622 | async ({ params, request }) => {
623 | const { spaceId, contentTypeId } = params
624 | if (spaceId === "test-space-id" && contentTypeId === "test-content-type-id") {
625 | const body = (await request.json()) as {
626 | name: string
627 | fields: Array<{
628 | id: string
629 | name: string
630 | type: string
631 | required?: boolean
632 | }>
633 | description?: string
634 | displayField?: string
635 | }
636 |
637 | return HttpResponse.json({
638 | sys: { id: contentTypeId },
639 | name: body.name,
640 | fields: body.fields,
641 | description: body.description,
642 | displayField: body.displayField,
643 | })
644 | }
645 | return new HttpResponse(null, { status: 404 })
646 | },
647 | ),
648 |
649 | // Delete content type
650 | http.delete(
651 | "https://api.contentful.com/spaces/:spaceId/environments/:environmentId/content_types/:contentTypeId",
652 | ({ params }) => {
653 | const { spaceId, contentTypeId } = params
654 | if (spaceId === "test-space-id" && contentTypeId === "test-content-type-id") {
655 | return new HttpResponse(null, { status: 204 })
656 | }
657 | return new HttpResponse(null, { status: 404 })
658 | },
659 | ),
660 | ]
661 |
662 | // Setup MSW Server
663 | export const server = setupServer(
664 | ...handlers,
665 | ...assetHandlers,
666 | ...contentTypeHandlers,
667 | ...entryHandlers,
668 | ...bulkActionHandlers,
669 | )
670 |
```
--------------------------------------------------------------------------------
/test/integration/comment-handler.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /* eslint-disable @typescript-eslint/no-unused-expressions */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import { expect, vi } from "vitest"
4 | import { commentHandlers } from "../../src/handlers/comment-handlers.js"
5 | import { server } from "../msw-setup.js"
6 |
7 | // Mock comment data
8 | const mockComment = {
9 | sys: {
10 | id: "test-comment-id",
11 | version: 1,
12 | createdAt: "2023-01-01T00:00:00Z",
13 | updatedAt: "2023-01-01T00:00:00Z",
14 | createdBy: {
15 | sys: { id: "test-user-id", type: "Link", linkType: "User" },
16 | },
17 | updatedBy: {
18 | sys: { id: "test-user-id", type: "Link", linkType: "User" },
19 | },
20 | },
21 | body: "This is a test comment",
22 | status: "active",
23 | }
24 |
25 | const mockRichTextComment = {
26 | sys: {
27 | id: "test-rich-comment-id",
28 | version: 1,
29 | createdAt: "2023-01-01T00:00:00Z",
30 | updatedAt: "2023-01-01T00:00:00Z",
31 | createdBy: {
32 | sys: { id: "test-user-id", type: "Link", linkType: "User" },
33 | },
34 | updatedBy: {
35 | sys: { id: "test-user-id", type: "Link", linkType: "User" },
36 | },
37 | },
38 | body: {
39 | nodeType: "document",
40 | content: [
41 | {
42 | nodeType: "paragraph",
43 | content: [
44 | {
45 | nodeType: "text",
46 | value: "This is a rich text comment",
47 | marks: [],
48 | },
49 | ],
50 | },
51 | ],
52 | },
53 | status: "active",
54 | }
55 |
56 | const mockCommentsCollection = {
57 | sys: { type: "Array" },
58 | total: 2,
59 | skip: 0,
60 | limit: 100,
61 | items: [mockComment, mockRichTextComment],
62 | }
63 |
64 | // Mock Contentful client comment methods
65 | const mockCommentGetMany = vi.fn().mockResolvedValue(mockCommentsCollection)
66 | const mockCommentCreate = vi.fn().mockResolvedValue(mockComment)
67 | const mockCommentGet = vi.fn().mockResolvedValue(mockComment)
68 | const mockCommentDelete = vi.fn().mockResolvedValue(undefined)
69 | const mockCommentUpdate = vi.fn().mockResolvedValue(mockComment)
70 |
71 | // Mock the contentful client for testing comment operations
72 | vi.mock("../../src/config/client.js", async (importOriginal) => {
73 | const originalModule = (await importOriginal()) as any
74 |
75 | // Create a mock function that will be used for the content client
76 | const getContentfulClient = vi.fn()
77 |
78 | // Store the original function so we can call it if needed
79 | const originalGetClient = originalModule.getContentfulClient
80 |
81 | // Set up the mock function to return either the original or our mocked version
82 | getContentfulClient.mockImplementation(async () => {
83 | // Create our mock client
84 | const mockClient = {
85 | comment: {
86 | getMany: mockCommentGetMany,
87 | create: mockCommentCreate,
88 | get: mockCommentGet,
89 | delete: mockCommentDelete,
90 | update: mockCommentUpdate,
91 | },
92 | // Pass through other methods to the original client
93 | entry: {
94 | get: (...args: any[]) =>
95 | originalGetClient().then((client: any) => client.entry.get(...args)),
96 | getMany: (...args: any[]) =>
97 | originalGetClient().then((client: any) => client.entry.getMany(...args)),
98 | create: (...args: any[]) =>
99 | originalGetClient().then((client: any) => client.entry.create(...args)),
100 | update: (...args: any[]) =>
101 | originalGetClient().then((client: any) => client.entry.update(...args)),
102 | delete: (...args: any[]) =>
103 | originalGetClient().then((client: any) => client.entry.delete(...args)),
104 | publish: (...args: any[]) =>
105 | originalGetClient().then((client: any) => client.entry.publish(...args)),
106 | unpublish: (...args: any[]) =>
107 | originalGetClient().then((client: any) => client.entry.unpublish(...args)),
108 | },
109 | }
110 |
111 | return mockClient
112 | })
113 |
114 | return {
115 | ...originalModule,
116 | getContentfulClient,
117 | }
118 | })
119 |
120 | describe("Comment Handlers Integration Tests", () => {
121 | // Start MSW Server before tests
122 | beforeAll(() => {
123 | server.listen()
124 | // Ensure environment variables are not set
125 | delete process.env.SPACE_ID
126 | delete process.env.ENVIRONMENT_ID
127 | })
128 | afterEach(() => {
129 | server.resetHandlers()
130 | vi.clearAllMocks()
131 | })
132 | afterAll(() => server.close())
133 |
134 | const testSpaceId = "test-space-id"
135 | const testEnvironmentId = "master"
136 | const testEntryId = "test-entry-id"
137 | const testCommentId = "test-comment-id"
138 |
139 | describe("getComments", () => {
140 | it("should retrieve comments for an entry with default parameters", async () => {
141 | const result = await commentHandlers.getComments({
142 | spaceId: testSpaceId,
143 | environmentId: testEnvironmentId,
144 | entryId: testEntryId,
145 | })
146 |
147 | expect(mockCommentGetMany).toHaveBeenCalledWith({
148 | spaceId: testSpaceId,
149 | environmentId: testEnvironmentId,
150 | entryId: testEntryId,
151 | bodyFormat: "plain-text",
152 | query: { status: "active" },
153 | })
154 |
155 | expect(result).to.have.property("content").that.is.an("array")
156 | expect(result.content).to.have.lengthOf(1)
157 |
158 | const responseData = JSON.parse(result.content[0].text)
159 | expect(responseData.items).to.be.an("array")
160 | expect(responseData.total).to.equal(2)
161 | expect(responseData.showing).to.equal(2)
162 | expect(responseData.remaining).to.equal(0)
163 | })
164 |
165 | it("should retrieve comments with rich-text body format", async () => {
166 | const result = await commentHandlers.getComments({
167 | spaceId: testSpaceId,
168 | environmentId: testEnvironmentId,
169 | entryId: testEntryId,
170 | bodyFormat: "rich-text",
171 | })
172 |
173 | expect(mockCommentGetMany).toHaveBeenCalledWith({
174 | spaceId: testSpaceId,
175 | environmentId: testEnvironmentId,
176 | entryId: testEntryId,
177 | bodyFormat: "rich-text",
178 | query: { status: "active" },
179 | })
180 |
181 | expect(result).to.have.property("content")
182 |
183 | const responseData = JSON.parse(result.content[0].text)
184 | expect(responseData.items).to.be.an("array")
185 | })
186 |
187 | it("should retrieve comments with status filter", async () => {
188 | const result = await commentHandlers.getComments({
189 | spaceId: testSpaceId,
190 | environmentId: testEnvironmentId,
191 | entryId: testEntryId,
192 | status: "resolved",
193 | })
194 |
195 | expect(mockCommentGetMany).toHaveBeenCalledWith({
196 | spaceId: testSpaceId,
197 | environmentId: testEnvironmentId,
198 | entryId: testEntryId,
199 | bodyFormat: "plain-text",
200 | query: { status: "resolved" },
201 | })
202 |
203 | expect(result).to.have.property("content")
204 | })
205 |
206 | it("should retrieve all comments when status is 'all'", async () => {
207 | const result = await commentHandlers.getComments({
208 | spaceId: testSpaceId,
209 | environmentId: testEnvironmentId,
210 | entryId: testEntryId,
211 | status: "all",
212 | })
213 |
214 | expect(mockCommentGetMany).toHaveBeenCalledWith({
215 | spaceId: testSpaceId,
216 | environmentId: testEnvironmentId,
217 | entryId: testEntryId,
218 | bodyFormat: "plain-text",
219 | query: {},
220 | })
221 |
222 | expect(result).to.have.property("content")
223 | })
224 |
225 | it("should use environment variables when provided", async () => {
226 | // Set environment variables
227 | const originalSpaceId = process.env.SPACE_ID
228 | const originalEnvironmentId = process.env.ENVIRONMENT_ID
229 | process.env.SPACE_ID = "env-space-id"
230 | process.env.ENVIRONMENT_ID = "env-environment-id"
231 |
232 | const result = await commentHandlers.getComments({
233 | spaceId: testSpaceId,
234 | environmentId: testEnvironmentId,
235 | entryId: testEntryId,
236 | })
237 |
238 | expect(mockCommentGetMany).toHaveBeenCalledWith({
239 | spaceId: "env-space-id",
240 | environmentId: "env-environment-id",
241 | entryId: testEntryId,
242 | bodyFormat: "plain-text",
243 | query: { status: "active" },
244 | })
245 |
246 | // Restore environment variables
247 | process.env.SPACE_ID = originalSpaceId
248 | process.env.ENVIRONMENT_ID = originalEnvironmentId
249 |
250 | expect(result).to.have.property("content")
251 | })
252 |
253 | it("should handle pagination with limit parameter", async () => {
254 | const result = await commentHandlers.getComments({
255 | spaceId: testSpaceId,
256 | environmentId: testEnvironmentId,
257 | entryId: testEntryId,
258 | limit: 1,
259 | })
260 |
261 | const responseData = JSON.parse(result.content[0].text)
262 | expect(responseData.items).to.have.lengthOf(1)
263 | expect(responseData.total).to.equal(2)
264 | expect(responseData.showing).to.equal(1)
265 | expect(responseData.remaining).to.equal(1)
266 | expect(responseData.skip).to.equal(1)
267 | expect(responseData.message).to.include("skip parameter")
268 | })
269 |
270 | it("should handle pagination with skip parameter", async () => {
271 | const result = await commentHandlers.getComments({
272 | spaceId: testSpaceId,
273 | environmentId: testEnvironmentId,
274 | entryId: testEntryId,
275 | limit: 1,
276 | skip: 1,
277 | })
278 |
279 | const responseData = JSON.parse(result.content[0].text)
280 | expect(responseData.items).to.have.lengthOf(1)
281 | expect(responseData.total).to.equal(2)
282 | expect(responseData.showing).to.equal(1)
283 | expect(responseData.remaining).to.equal(0)
284 | expect(responseData.skip).to.be.undefined
285 | expect(responseData.message).to.be.undefined
286 | })
287 |
288 | it("should handle limit larger than available items", async () => {
289 | const result = await commentHandlers.getComments({
290 | spaceId: testSpaceId,
291 | environmentId: testEnvironmentId,
292 | entryId: testEntryId,
293 | limit: 10,
294 | })
295 |
296 | const responseData = JSON.parse(result.content[0].text)
297 | expect(responseData.items).to.have.lengthOf(2)
298 | expect(responseData.total).to.equal(2)
299 | expect(responseData.showing).to.equal(2)
300 | expect(responseData.remaining).to.equal(0)
301 | expect(responseData.skip).to.be.undefined
302 | expect(responseData.message).to.be.undefined
303 | })
304 | })
305 |
306 | describe("createComment", () => {
307 | it("should create a plain-text comment with default parameters", async () => {
308 | const testBody = "This is a test comment"
309 |
310 | const result = await commentHandlers.createComment({
311 | spaceId: testSpaceId,
312 | environmentId: testEnvironmentId,
313 | entryId: testEntryId,
314 | body: testBody,
315 | })
316 |
317 | expect(mockCommentCreate).toHaveBeenCalledWith(
318 | {
319 | spaceId: testSpaceId,
320 | environmentId: testEnvironmentId,
321 | entryId: testEntryId,
322 | },
323 | {
324 | body: testBody,
325 | status: "active",
326 | },
327 | )
328 |
329 | expect(result).to.have.property("content").that.is.an("array")
330 | expect(result.content).to.have.lengthOf(1)
331 |
332 | const responseData = JSON.parse(result.content[0].text)
333 | expect(responseData.sys.id).to.equal("test-comment-id")
334 | expect(responseData.body).to.equal("This is a test comment")
335 | expect(responseData.status).to.equal("active")
336 | })
337 |
338 | it("should create a rich-text comment", async () => {
339 | const testBody = "This is a rich text comment"
340 |
341 | mockCommentCreate.mockResolvedValueOnce(mockRichTextComment)
342 |
343 | const result = await commentHandlers.createComment({
344 | spaceId: testSpaceId,
345 | environmentId: testEnvironmentId,
346 | entryId: testEntryId,
347 | body: testBody,
348 | })
349 |
350 | expect(mockCommentCreate).toHaveBeenCalledWith(
351 | {
352 | spaceId: testSpaceId,
353 | environmentId: testEnvironmentId,
354 | entryId: testEntryId,
355 | },
356 | {
357 | body: testBody,
358 | status: "active",
359 | },
360 | )
361 |
362 | expect(result).to.have.property("content")
363 |
364 | const responseData = JSON.parse(result.content[0].text)
365 | expect(responseData.sys.id).to.equal("test-rich-comment-id")
366 | })
367 |
368 | it("should create a comment with custom status", async () => {
369 | const testBody = "This is a test comment"
370 |
371 | const result = await commentHandlers.createComment({
372 | spaceId: testSpaceId,
373 | environmentId: testEnvironmentId,
374 | entryId: testEntryId,
375 | body: testBody,
376 | status: "active",
377 | })
378 |
379 | expect(mockCommentCreate).toHaveBeenCalledWith(
380 | {
381 | spaceId: testSpaceId,
382 | environmentId: testEnvironmentId,
383 | entryId: testEntryId,
384 | },
385 | {
386 | body: testBody,
387 | status: "active",
388 | },
389 | )
390 |
391 | expect(result).to.have.property("content")
392 | })
393 |
394 | it("should use environment variables when provided", async () => {
395 | // Set environment variables
396 | const originalSpaceId = process.env.SPACE_ID
397 | const originalEnvironmentId = process.env.ENVIRONMENT_ID
398 | process.env.SPACE_ID = "env-space-id"
399 | process.env.ENVIRONMENT_ID = "env-environment-id"
400 |
401 | const testBody = "This is a test comment"
402 |
403 | const result = await commentHandlers.createComment({
404 | spaceId: testSpaceId,
405 | environmentId: testEnvironmentId,
406 | entryId: testEntryId,
407 | body: testBody,
408 | })
409 |
410 | expect(mockCommentCreate).toHaveBeenCalledWith(
411 | {
412 | spaceId: "env-space-id",
413 | environmentId: "env-environment-id",
414 | entryId: testEntryId,
415 | },
416 | {
417 | body: testBody,
418 | status: "active",
419 | },
420 | )
421 |
422 | // Restore environment variables
423 | process.env.SPACE_ID = originalSpaceId
424 | process.env.ENVIRONMENT_ID = originalEnvironmentId
425 |
426 | expect(result).to.have.property("content")
427 | })
428 | })
429 |
430 | describe("getSingleComment", () => {
431 | it("should retrieve a specific comment with default parameters", async () => {
432 | const result = await commentHandlers.getSingleComment({
433 | spaceId: testSpaceId,
434 | environmentId: testEnvironmentId,
435 | entryId: testEntryId,
436 | commentId: testCommentId,
437 | })
438 |
439 | expect(mockCommentGet).toHaveBeenCalledWith({
440 | spaceId: testSpaceId,
441 | environmentId: testEnvironmentId,
442 | entryId: testEntryId,
443 | commentId: testCommentId,
444 | bodyFormat: "plain-text",
445 | })
446 |
447 | expect(result).to.have.property("content").that.is.an("array")
448 | expect(result.content).to.have.lengthOf(1)
449 |
450 | const responseData = JSON.parse(result.content[0].text)
451 | expect(responseData.sys.id).to.equal("test-comment-id")
452 | expect(responseData.body).to.equal("This is a test comment")
453 | expect(responseData.status).to.equal("active")
454 | })
455 |
456 | it("should retrieve a specific comment with rich-text body format", async () => {
457 | mockCommentGet.mockResolvedValueOnce(mockRichTextComment)
458 |
459 | const result = await commentHandlers.getSingleComment({
460 | spaceId: testSpaceId,
461 | environmentId: testEnvironmentId,
462 | entryId: testEntryId,
463 | commentId: testCommentId,
464 | bodyFormat: "rich-text",
465 | })
466 |
467 | expect(mockCommentGet).toHaveBeenCalledWith({
468 | spaceId: testSpaceId,
469 | environmentId: testEnvironmentId,
470 | entryId: testEntryId,
471 | commentId: testCommentId,
472 | bodyFormat: "rich-text",
473 | })
474 |
475 | expect(result).to.have.property("content")
476 |
477 | const responseData = JSON.parse(result.content[0].text)
478 | expect(responseData.body).to.have.property("nodeType", "document")
479 | })
480 |
481 | it("should use environment variables when provided", async () => {
482 | // Set environment variables
483 | const originalSpaceId = process.env.SPACE_ID
484 | const originalEnvironmentId = process.env.ENVIRONMENT_ID
485 | process.env.SPACE_ID = "env-space-id"
486 | process.env.ENVIRONMENT_ID = "env-environment-id"
487 |
488 | const result = await commentHandlers.getSingleComment({
489 | spaceId: testSpaceId,
490 | environmentId: testEnvironmentId,
491 | entryId: testEntryId,
492 | commentId: testCommentId,
493 | })
494 |
495 | expect(mockCommentGet).toHaveBeenCalledWith({
496 | spaceId: "env-space-id",
497 | environmentId: "env-environment-id",
498 | entryId: testEntryId,
499 | commentId: testCommentId,
500 | bodyFormat: "plain-text",
501 | })
502 |
503 | // Restore environment variables
504 | process.env.SPACE_ID = originalSpaceId
505 | process.env.ENVIRONMENT_ID = originalEnvironmentId
506 |
507 | expect(result).to.have.property("content")
508 | })
509 |
510 | it("should handle errors gracefully", async () => {
511 | mockCommentGet.mockRejectedValueOnce(new Error("Comment not found"))
512 |
513 | try {
514 | await commentHandlers.getSingleComment({
515 | spaceId: testSpaceId,
516 | environmentId: testEnvironmentId,
517 | entryId: testEntryId,
518 | commentId: "invalid-comment-id",
519 | })
520 | expect.fail("Should have thrown an error")
521 | } catch (error: any) {
522 | expect(error).to.exist
523 | expect(error.message).to.equal("Comment not found")
524 | }
525 | })
526 | })
527 |
528 | describe("deleteComment", () => {
529 | it("should delete a specific comment", async () => {
530 | const result = await commentHandlers.deleteComment({
531 | spaceId: testSpaceId,
532 | environmentId: testEnvironmentId,
533 | entryId: testEntryId,
534 | commentId: testCommentId,
535 | })
536 |
537 | expect(mockCommentDelete).toHaveBeenCalledWith({
538 | spaceId: testSpaceId,
539 | environmentId: testEnvironmentId,
540 | entryId: testEntryId,
541 | commentId: testCommentId,
542 | version: 1,
543 | })
544 |
545 | expect(result).to.have.property("content").that.is.an("array")
546 | expect(result.content).to.have.lengthOf(1)
547 | expect(result.content[0].text).to.include(
548 | `Successfully deleted comment ${testCommentId} from entry ${testEntryId}`,
549 | )
550 | })
551 |
552 | it("should use environment variables when provided", async () => {
553 | // Set environment variables
554 | const originalSpaceId = process.env.SPACE_ID
555 | const originalEnvironmentId = process.env.ENVIRONMENT_ID
556 | process.env.SPACE_ID = "env-space-id"
557 | process.env.ENVIRONMENT_ID = "env-environment-id"
558 |
559 | const result = await commentHandlers.deleteComment({
560 | spaceId: testSpaceId,
561 | environmentId: testEnvironmentId,
562 | entryId: testEntryId,
563 | commentId: testCommentId,
564 | })
565 |
566 | expect(mockCommentDelete).toHaveBeenCalledWith({
567 | spaceId: "env-space-id",
568 | environmentId: "env-environment-id",
569 | entryId: testEntryId,
570 | commentId: testCommentId,
571 | version: 1,
572 | })
573 |
574 | // Restore environment variables
575 | process.env.SPACE_ID = originalSpaceId
576 | process.env.ENVIRONMENT_ID = originalEnvironmentId
577 |
578 | expect(result).to.have.property("content")
579 | })
580 |
581 | it("should handle errors gracefully", async () => {
582 | mockCommentDelete.mockRejectedValueOnce(new Error("Delete failed"))
583 |
584 | try {
585 | await commentHandlers.deleteComment({
586 | spaceId: testSpaceId,
587 | environmentId: testEnvironmentId,
588 | entryId: testEntryId,
589 | commentId: "invalid-comment-id",
590 | })
591 | expect.fail("Should have thrown an error")
592 | } catch (error) {
593 | expect(error).to.exist
594 | expect(error.message).to.equal("Delete failed")
595 | }
596 | })
597 | })
598 |
599 | describe("updateComment", () => {
600 | it("should update a comment with plain-text format", async () => {
601 | const testBody = "Updated comment body"
602 | const testStatus = "resolved"
603 |
604 | const result = await commentHandlers.updateComment({
605 | spaceId: testSpaceId,
606 | environmentId: testEnvironmentId,
607 | entryId: testEntryId,
608 | commentId: testCommentId,
609 | body: testBody,
610 | status: testStatus,
611 | })
612 |
613 | expect(mockCommentUpdate).toHaveBeenCalledWith(
614 | {
615 | spaceId: testSpaceId,
616 | environmentId: testEnvironmentId,
617 | entryId: testEntryId,
618 | commentId: testCommentId,
619 | },
620 | {
621 | body: testBody,
622 | status: testStatus,
623 | version: 1,
624 | },
625 | )
626 |
627 | expect(result).to.have.property("content").that.is.an("array")
628 | expect(result.content).to.have.lengthOf(1)
629 |
630 | const responseData = JSON.parse(result.content[0].text)
631 | expect(responseData.sys.id).to.equal("test-comment-id")
632 | })
633 |
634 | it("should update a comment with rich-text format", async () => {
635 | const testBody = "Updated rich text comment"
636 |
637 | mockCommentUpdate.mockResolvedValueOnce(mockRichTextComment)
638 |
639 | const result = await commentHandlers.updateComment({
640 | spaceId: testSpaceId,
641 | environmentId: testEnvironmentId,
642 | entryId: testEntryId,
643 | commentId: testCommentId,
644 | body: testBody,
645 | bodyFormat: "rich-text",
646 | })
647 |
648 | expect(mockCommentUpdate).toHaveBeenCalledWith(
649 | {
650 | spaceId: testSpaceId,
651 | environmentId: testEnvironmentId,
652 | entryId: testEntryId,
653 | commentId: testCommentId,
654 | },
655 | {
656 | body: testBody,
657 | version: 1,
658 | },
659 | )
660 |
661 | expect(result).to.have.property("content")
662 |
663 | const responseData = JSON.parse(result.content[0].text)
664 | expect(responseData.sys.id).to.equal("test-rich-comment-id")
665 | })
666 |
667 | it("should update only body when status is not provided", async () => {
668 | const testBody = "Updated comment body only"
669 |
670 | const result = await commentHandlers.updateComment({
671 | spaceId: testSpaceId,
672 | environmentId: testEnvironmentId,
673 | entryId: testEntryId,
674 | commentId: testCommentId,
675 | body: testBody,
676 | })
677 |
678 | expect(mockCommentUpdate).toHaveBeenCalledWith(
679 | {
680 | spaceId: testSpaceId,
681 | environmentId: testEnvironmentId,
682 | entryId: testEntryId,
683 | commentId: testCommentId,
684 | },
685 | {
686 | body: testBody,
687 | version: 1,
688 | },
689 | )
690 |
691 | expect(result).to.have.property("content")
692 | })
693 |
694 | it("should update only status when body is not provided", async () => {
695 | const testStatus = "resolved"
696 |
697 | const result = await commentHandlers.updateComment({
698 | spaceId: testSpaceId,
699 | environmentId: testEnvironmentId,
700 | entryId: testEntryId,
701 | commentId: testCommentId,
702 | status: testStatus,
703 | })
704 |
705 | expect(mockCommentUpdate).toHaveBeenCalledWith(
706 | {
707 | spaceId: testSpaceId,
708 | environmentId: testEnvironmentId,
709 | entryId: testEntryId,
710 | commentId: testCommentId,
711 | },
712 | {
713 | status: testStatus,
714 | version: 1,
715 | },
716 | )
717 |
718 | expect(result).to.have.property("content")
719 | })
720 |
721 | it("should use environment variables when provided", async () => {
722 | // Set environment variables
723 | const originalSpaceId = process.env.SPACE_ID
724 | const originalEnvironmentId = process.env.ENVIRONMENT_ID
725 | process.env.SPACE_ID = "env-space-id"
726 | process.env.ENVIRONMENT_ID = "env-environment-id"
727 |
728 | const testBody = "Updated comment"
729 |
730 | const result = await commentHandlers.updateComment({
731 | spaceId: testSpaceId,
732 | environmentId: testEnvironmentId,
733 | entryId: testEntryId,
734 | commentId: testCommentId,
735 | body: testBody,
736 | })
737 |
738 | expect(mockCommentUpdate).toHaveBeenCalledWith(
739 | {
740 | spaceId: "env-space-id",
741 | environmentId: "env-environment-id",
742 | entryId: testEntryId,
743 | commentId: testCommentId,
744 | },
745 | {
746 | body: testBody,
747 | version: 1,
748 | },
749 | )
750 |
751 | // Restore environment variables
752 | process.env.SPACE_ID = originalSpaceId
753 | process.env.ENVIRONMENT_ID = originalEnvironmentId
754 |
755 | expect(result).to.have.property("content")
756 | })
757 |
758 | it("should handle errors gracefully", async () => {
759 | mockCommentUpdate.mockRejectedValueOnce(new Error("Update failed"))
760 |
761 | try {
762 | await commentHandlers.updateComment({
763 | spaceId: testSpaceId,
764 | environmentId: testEnvironmentId,
765 | entryId: testEntryId,
766 | commentId: "invalid-comment-id",
767 | body: "Test body",
768 | })
769 | expect.fail("Should have thrown an error")
770 | } catch (error) {
771 | expect(error).to.exist
772 | expect(error.message).to.equal("Update failed")
773 | }
774 | })
775 | })
776 |
777 | describe("Error handling", () => {
778 | it("should handle getComments API errors", async () => {
779 | mockCommentGetMany.mockRejectedValueOnce(new Error("API Error"))
780 |
781 | try {
782 | await commentHandlers.getComments({
783 | spaceId: testSpaceId,
784 | environmentId: testEnvironmentId,
785 | entryId: testEntryId,
786 | })
787 | expect.fail("Should have thrown an error")
788 | } catch (error) {
789 | expect(error).to.exist
790 | expect(error.message).to.equal("API Error")
791 | }
792 | })
793 |
794 | it("should handle createComment API errors", async () => {
795 | mockCommentCreate.mockRejectedValueOnce(new Error("Create failed"))
796 |
797 | try {
798 | await commentHandlers.createComment({
799 | spaceId: testSpaceId,
800 | environmentId: testEnvironmentId,
801 | entryId: testEntryId,
802 | body: "Test comment",
803 | })
804 | expect.fail("Should have thrown an error")
805 | } catch (error) {
806 | expect(error).to.exist
807 | expect(error.message).to.equal("Create failed")
808 | }
809 | })
810 |
811 | it("should handle deleteComment API errors", async () => {
812 | mockCommentDelete.mockRejectedValueOnce(new Error("Delete failed"))
813 |
814 | try {
815 | await commentHandlers.deleteComment({
816 | spaceId: testSpaceId,
817 | environmentId: testEnvironmentId,
818 | entryId: testEntryId,
819 | commentId: testCommentId,
820 | })
821 | expect.fail("Should have thrown an error")
822 | } catch (error) {
823 | expect(error).to.exist
824 | expect(error.message).to.equal("Delete failed")
825 | }
826 | })
827 |
828 | it("should handle updateComment API errors", async () => {
829 | mockCommentUpdate.mockRejectedValueOnce(new Error("Update failed"))
830 |
831 | try {
832 | await commentHandlers.updateComment({
833 | spaceId: testSpaceId,
834 | environmentId: testEnvironmentId,
835 | entryId: testEntryId,
836 | commentId: testCommentId,
837 | body: "Test body",
838 | })
839 | expect.fail("Should have thrown an error")
840 | } catch (error) {
841 | expect(error).to.exist
842 | expect(error.message).to.equal("Update failed")
843 | }
844 | })
845 | })
846 | })
847 |
```
--------------------------------------------------------------------------------
/src/prompts/generateVariableTypeContent.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Generate detailed content for AI Action variable types
3 | * @param variableType Specific variable type to generate content for
4 | * @returns Detailed explanation of the variable type(s)
5 | */
6 | export function generateVariableTypeContent(variableType?: string): string {
7 | // If no specific type is requested, return an overview of all types
8 | if (!variableType) {
9 | return `AI Actions in Contentful use variables to make templates dynamic and adaptable. These variables serve as placeholders in your prompt templates that get replaced with actual values when an AI Action is invoked.
10 |
11 | ## Available Variable Types
12 |
13 | ### 1. StandardInput
14 |
15 | The primary input variable for text content. Ideal for the main content that needs processing.
16 |
17 | - **Use case**: Processing existing content, like rewriting or improving text
18 | - **Template usage**: {{input_text}}
19 | - **MCP parameter**: Usually exposed as "input_text"
20 | - **Best for**: Main content field being operated on
21 |
22 | ### 2. Text
23 |
24 | Simple text variables for additional information or context.
25 |
26 | - **Use case**: Adding supplementary information to guide the AI
27 | - **Template usage**: {{context}}, {{guidelines}}
28 | - **Configuration**: Can be strict (limit to specific values) or free-form
29 | - **Best for**: Providing context, instructions, or metadata
30 |
31 | ### 3. FreeFormInput
32 |
33 | Unstructured text input with minimal constraints.
34 |
35 | - **Use case**: Open-ended user input where flexibility is needed
36 | - **Template usage**: {{user_instructions}}
37 | - **Best for**: Custom requests or specific directions
38 |
39 | ### 4. StringOptionsList
40 |
41 | Predefined options presented as a dropdown menu.
42 |
43 | - **Use case**: Selecting from a fixed set of choices (tone, style, etc.)
44 | - **Template usage**: {{tone}}, {{style}}
45 | - **Configuration**: Requires defining the list of available options
46 | - **Best for**: Consistent, controlled parameters like tones or formatting options
47 |
48 | ### 5. Reference
49 |
50 | Links to other Contentful entries.
51 |
52 | - **Use case**: Using content from other entries as context
53 | - **Template usage**: Content accessed via helpers: "{{reference.fields.title}}"
54 | - **Configuration**: Can restrict to specific content types
55 | - **Special properties**: When using in MCP, requires "*_path" parameter to specify field path
56 | - **Best for**: Cross-referencing content for context or expansion
57 |
58 | ### 6. MediaReference
59 |
60 | Links to media assets like images, videos.
61 |
62 | - **Use case**: Generating descriptions from media, processing asset metadata
63 | - **Configuration**: Points to a specific asset in your space
64 | - **Special properties**: When using in MCP, requires "*_path" parameter to specify which part to process
65 | - **Best for**: Image description, alt text generation, media analysis
66 |
67 | ### 7. Locale
68 |
69 | Specifies the language or region for content.
70 |
71 | - **Use case**: Translation, localization, region-specific content
72 | - **Template usage**: {{locale}}
73 | - **Format**: Language codes like "en-US", "de-DE"
74 | - **Best for**: Multi-language operations or locale-specific formatting
75 |
76 | ## Best Practices for Variables
77 |
78 | 1. **Use descriptive names**: Make variable names intuitive ({{product_name}} not {{var1}})
79 | 2. **Provide clear descriptions**: Help users understand what each variable does
80 | 3. **Use appropriate types**: Match variable types to their purpose
81 | 4. **Set sensible defaults**: Pre-populate where possible to guide users
82 | 5. **Consider field paths**: For Reference and MediaReference variables, remember that users need to specify which field to access
83 |
84 | ## Template Integration
85 |
86 | Variables are referenced in templates using double curly braces: {{variable_id}}
87 |
88 | Example template with multiple variable types:
89 |
90 | \`\`\`
91 | You are a content specialist helping improve product descriptions.
92 |
93 | TONE: {{tone}}
94 |
95 | AUDIENCE: Customers interested in {{target_market}}
96 |
97 | ORIGINAL CONTENT:
98 | {{input_text}}
99 |
100 | INSTRUCTIONS:
101 | Rewrite the above product description to be more engaging, maintaining the key product details but optimizing for {{tone}} tone and the {{target_market}} market.
102 |
103 | IMPROVED DESCRIPTION:
104 | \`\`\`
105 |
106 | ## Working with References
107 |
108 | When working with References and MediaReferences, remember that the content must be accessed via the correct path. In the MCP integration, this is handled through separate parameters (e.g., "reference" and "reference_path").
109 |
110 | ## Variable Validation
111 |
112 | AI Actions validate variables at runtime to ensure they meet requirements. Configure validation rules appropriately to prevent errors during invocation.`;
113 | }
114 |
115 | // Generate content for specific variable types
116 | switch (variableType.toLowerCase()) {
117 | case "standardinput":
118 | case "standard input":
119 | return `## StandardInput Variable Type
120 |
121 | The StandardInput is the primary input variable for text content in AI Actions. It's designed to handle the main content that needs to be processed by the AI model.
122 |
123 | ### Purpose
124 |
125 | This variable type is typically used for:
126 | - The primary text to be transformed
127 | - Existing content that needs enhancement, rewriting, or analysis
128 | - The core content that the AI Action will operate on
129 |
130 | ### Configuration
131 |
132 | StandardInput has minimal configuration needs:
133 |
134 | - **ID**: A unique identifier (e.g., "main_content")
135 | - **Name**: User-friendly label (e.g., "Main Content")
136 | - **Description**: Clear explanation (e.g., "The content to be processed")
137 |
138 | No additional configuration properties are required for this type.
139 |
140 | ### In MCP Integration
141 |
142 | When working with the MCP integration, StandardInput variables are typically exposed with the parameter name "input_text" for consistency and clarity.
143 |
144 | ### Template Usage
145 |
146 | In your AI Action template, reference StandardInput variables using double curly braces:
147 |
148 | \`\`\`
149 | ORIGINAL CONTENT:
150 | {{input_text}}
151 |
152 | Please improve the above content by...
153 | \`\`\`
154 |
155 | ### Examples
156 |
157 | **Example 1: Content Enhancement**
158 |
159 | \`\`\`
160 | You are a content specialist.
161 |
162 | ORIGINAL CONTENT:
163 | {{input_text}}
164 |
165 | Enhance the above content by improving clarity, fixing grammar, and making it more engaging while preserving the key information.
166 |
167 | IMPROVED CONTENT:
168 | \`\`\`
169 |
170 | **Example 2: SEO Optimization**
171 |
172 | \`\`\`
173 | You are an SEO expert.
174 |
175 | ORIGINAL CONTENT:
176 | {{input_text}}
177 |
178 | KEYWORDS: {{keywords}}
179 |
180 | Rewrite the above content to optimize for SEO using the provided keywords. Maintain the core message but improve readability and keyword usage.
181 |
182 | SEO-OPTIMIZED CONTENT:
183 | \`\`\`
184 |
185 | ### Best Practices
186 |
187 | 1. **Clear instructions**: Always include clear directions about what to do with the input text
188 | 2. **Context setting**: Provide context about what the input represents (e.g., product description, blog post)
189 | 3. **Output expectations**: Clearly indicate what the expected output format should be
190 | 4. **Complementary variables**: Pair StandardInput with other variables that provide direction (tone, style, keywords)
191 |
192 | ### Implementation with MCP Tools
193 |
194 | When creating an AI Action with StandardInput using MCP tools:
195 |
196 | \`\`\`javascript
197 | create_ai_action({
198 | // other parameters...
199 | instruction: {
200 | template: "You are helping improve content...\\n\\nORIGINAL CONTENT:\\n{{input_text}}\\n\\nIMPROVED CONTENT:",
201 | variables: [
202 | {
203 | id: "input_text",
204 | type: "StandardInput",
205 | name: "Input Content",
206 | description: "The content to be improved"
207 | }
208 | // other variables...
209 | ]
210 | }
211 | });
212 | \`\`\``;
213 |
214 | case "text":
215 | return `## Text Variable Type
216 |
217 | The Text variable type provides a simple way to collect text input in AI Actions. It's more flexible than StringOptionsList but can include validation constraints if needed.
218 |
219 | ### Purpose
220 |
221 | Text variables are used for:
222 | - Supplementary information to guide the AI
223 | - Additional context that affects output
224 | - Simple inputs that don't require the full flexibility of FreeFormInput
225 |
226 | ### Configuration
227 |
228 | Text variables can be configured with these properties:
229 |
230 | - **ID**: Unique identifier (e.g., "brand_guidelines")
231 | - **Name**: User-friendly label (e.g., "Brand Guidelines")
232 | - **Description**: Explanation of what to input
233 | - **Configuration** (optional):
234 | - **strict**: Boolean indicating whether values are restricted
235 | - **in**: Array of allowed values if strict is true
236 |
237 | ### Template Usage
238 |
239 | Reference Text variables in templates using double curly braces:
240 |
241 | \`\`\`
242 | BRAND GUIDELINES: {{brand_guidelines}}
243 |
244 | CONTENT:
245 | {{input_text}}
246 |
247 | Please rewrite the above content following the brand guidelines provided.
248 | \`\`\`
249 |
250 | ### Examples
251 |
252 | **Example 1: Simple Text Variable**
253 |
254 | \`\`\`javascript
255 | {
256 | id: "customer_segment",
257 | type: "Text",
258 | name: "Customer Segment",
259 | description: "The target customer segment for this content"
260 | }
261 | \`\`\`
262 |
263 | **Example 2: Text Variable with Validation**
264 |
265 | \`\`\`javascript
266 | {
267 | id: "priority_level",
268 | type: "Text",
269 | name: "Priority Level",
270 | description: "The priority level for this task",
271 | configuration: {
272 | strict: true,
273 | in: ["High", "Medium", "Low"]
274 | }
275 | }
276 | \`\`\`
277 |
278 | ### Best Practices
279 |
280 | 1. **Clarify expectations**: Provide clear descriptions about what information is expected
281 | 2. **Use validation when appropriate**: If only certain values are valid, use the strict configuration
282 | 3. **Consider using StringOptionsList**: If you have a fixed set of options, StringOptionsList may be more appropriate
283 | 4. **Keep it focused**: Ask for specific information rather than general input
284 |
285 | ### Implementation with MCP Tools
286 |
287 | \`\`\`javascript
288 | create_ai_action({
289 | // other parameters...
290 | instruction: {
291 | template: "Create content with customer segment {{customer_segment}} in mind...",
292 | variables: [
293 | {
294 | id: "customer_segment",
295 | type: "Text",
296 | name: "Customer Segment",
297 | description: "The target customer segment for this content"
298 | }
299 | // other variables...
300 | ]
301 | }
302 | });
303 | \`\`\``;
304 |
305 | case "freeforminput":
306 | case "free form input":
307 | return `## FreeFormInput Variable Type
308 |
309 | The FreeFormInput variable type provides the most flexibility for collecting user input in AI Actions. It's designed for open-ended text entry with minimal constraints.
310 |
311 | ### Purpose
312 |
313 | FreeFormInput variables are ideal for:
314 | - Custom instructions from users
315 | - Specific guidance that can't be predetermined
316 | - Open-ended information that requires flexibility
317 |
318 | ### Configuration
319 |
320 | FreeFormInput has minimal configuration requirements:
321 |
322 | - **ID**: Unique identifier (e.g., "special_instructions")
323 | - **Name**: User-friendly label (e.g., "Special Instructions")
324 | - **Description**: Clear guidance on what kind of input is expected
325 |
326 | No additional configuration properties are typically needed.
327 |
328 | ### Template Usage
329 |
330 | Reference FreeFormInput variables in templates with double curly braces:
331 |
332 | \`\`\`
333 | CONTENT:
334 | {{input_text}}
335 |
336 | SPECIAL INSTRUCTIONS:
337 | {{special_instructions}}
338 |
339 | Please modify the content above according to the special instructions provided.
340 | \`\`\`
341 |
342 | ### Examples
343 |
344 | **Example: Content Creation Guidance**
345 |
346 | \`\`\`javascript
347 | {
348 | id: "author_preferences",
349 | type: "FreeFormInput",
350 | name: "Author Preferences",
351 | description: "Any specific preferences or requirements from the author that should be considered"
352 | }
353 | \`\`\`
354 |
355 | ### Best Practices
356 |
357 | 1. **Provide guidance**: Even though it's free-form, give users clear guidance about what kind of input is helpful
358 | 2. **Set expectations**: Explain how the input will be used in the AI Action
359 | 3. **Use sparingly**: Too many free-form inputs can make AI Actions confusing - use only where flexibility is needed
360 | 4. **Position appropriately**: Place FreeFormInput variables where they make most sense in your template flow
361 |
362 | ### When to Use FreeFormInput vs. Text
363 |
364 | - Use **FreeFormInput** when you need completely open-ended input without restrictions
365 | - Use **Text** when you want simple input that might benefit from validation
366 |
367 | ### Implementation with MCP Tools
368 |
369 | \`\`\`javascript
370 | create_ai_action({
371 | // other parameters...
372 | instruction: {
373 | template: "Generate content based on these specifications...\\n\\nSPECIAL REQUIREMENTS:\\n{{special_requirements}}",
374 | variables: [
375 | {
376 | id: "special_requirements",
377 | type: "FreeFormInput",
378 | name: "Special Requirements",
379 | description: "Any special requirements or preferences for the generated content"
380 | }
381 | // other variables...
382 | ]
383 | }
384 | });
385 | \`\`\``;
386 |
387 | case "stringoptionslist":
388 | case "string options list":
389 | return `## StringOptionsList Variable Type
390 |
391 | The StringOptionsList variable type provides a dropdown menu of predefined options. It's ideal for scenarios where users should select from a fixed set of choices.
392 |
393 | ### Purpose
394 |
395 | StringOptionsList variables are perfect for:
396 | - Tone selection (formal, casual, etc.)
397 | - Content categories or types
398 | - Predefined styles or formats
399 | - Any parameter with a limited set of valid options
400 |
401 | ### Configuration
402 |
403 | StringOptionsList requires these configuration properties:
404 |
405 | - **ID**: Unique identifier (e.g., "tone")
406 | - **Name**: User-friendly label (e.g., "Content Tone")
407 | - **Description**: Explanation of what the options represent
408 | - **Configuration** (required):
409 | - **values**: Array of string options to display
410 | - **allowFreeFormInput** (optional): Boolean indicating if custom values are allowed
411 |
412 | ### Template Usage
413 |
414 | Reference StringOptionsList variables in templates using double curly braces:
415 |
416 | \`\`\`
417 | TONE: {{tone}}
418 |
419 | CONTENT:
420 | {{input_text}}
421 |
422 | Please rewrite the above content using a {{tone}} tone.
423 | \`\`\`
424 |
425 | ### Examples
426 |
427 | **Example 1: Tone Selection**
428 |
429 | \`\`\`javascript
430 | {
431 | id: "tone",
432 | type: "StringOptionsList",
433 | name: "Content Tone",
434 | description: "The tone to use for the content",
435 | configuration: {
436 | values: ["Formal", "Professional", "Casual", "Friendly", "Humorous"],
437 | allowFreeFormInput: false
438 | }
439 | }
440 | \`\`\`
441 |
442 | **Example 2: Content Format with Custom Option**
443 |
444 | \`\`\`javascript
445 | {
446 | id: "format",
447 | type: "StringOptionsList",
448 | name: "Content Format",
449 | description: "The format for the generated content",
450 | configuration: {
451 | values: ["Blog Post", "Social Media", "Email", "Product Description", "Press Release"],
452 | allowFreeFormInput: true
453 | }
454 | }
455 | \`\`\`
456 |
457 | ### Best Practices
458 |
459 | 1. **Limit options**: Keep the list reasonably short (typically 3-7 options)
460 | 2. **Use clear labels**: Make option names self-explanatory
461 | 3. **Order logically**: Arrange options in a logical order (alphabetical, frequency, etc.)
462 | 4. **Consider defaults**: Place commonly used options earlier in the list
463 | 5. **Use allowFreeFormInput sparingly**: Only enable when custom options are truly needed
464 |
465 | ### In MCP Integration
466 |
467 | In the MCP implementation, StringOptionsList variables are presented as enum parameters with the predefined options as choices.
468 |
469 | ### Implementation with MCP Tools
470 |
471 | \`\`\`javascript
472 | create_ai_action({
473 | // other parameters...
474 | instruction: {
475 | template: "Generate a {{content_type}} about {{topic}}...",
476 | variables: [
477 | {
478 | id: "content_type",
479 | type: "StringOptionsList",
480 | name: "Content Type",
481 | description: "The type of content to generate",
482 | configuration: {
483 | values: ["Blog Post", "Social Media Post", "Newsletter", "Product Description"],
484 | allowFreeFormInput: false
485 | }
486 | },
487 | {
488 | id: "topic",
489 | type: "Text",
490 | name: "Topic",
491 | description: "The topic for the content"
492 | }
493 | // other variables...
494 | ]
495 | }
496 | });
497 | \`\`\``;
498 |
499 | case "reference":
500 | return `## Reference Variable Type
501 |
502 | The Reference variable type allows AI Actions to access content from other entries in your Contentful space, creating powerful content relationships and context-aware operations.
503 |
504 | ### Purpose
505 |
506 | Reference variables are used for:
507 | - Accessing content from related entries
508 | - Processing entry data for context or analysis
509 | - Creating content based on existing entries
510 | - Cross-referencing information across multiple content types
511 |
512 | ### Configuration
513 |
514 | Reference variables require these properties:
515 |
516 | - **ID**: Unique identifier (e.g., "product_entry")
517 | - **Name**: User-friendly label (e.g., "Product Entry")
518 | - **Description**: Explanation of what entry to reference
519 | - **Configuration** (optional):
520 | - **allowedEntities**: Array of entity types that can be referenced (typically ["Entry"])
521 |
522 | ### Field Path Specification
523 |
524 | When using References in MCP, you must provide both:
525 | 1. The entry ID (which entry to reference)
526 | 2. The field path (which field within that entry to use)
527 |
528 | This is handled through two parameters:
529 | - **reference**: The entry ID to reference
530 | - **reference_path**: The path to the field (e.g., "fields.description.en-US")
531 |
532 | ### Template Usage
533 |
534 | In templates, you can access referenced entry fields using helpers or direct field access:
535 |
536 | \`\`\`
537 | PRODUCT NAME: {{product_entry.fields.name}}
538 |
539 | CURRENT DESCRIPTION:
540 | {{product_entry.fields.description}}
541 |
542 | Please generate an improved product description that highlights the key features while maintaining brand voice.
543 | \`\`\`
544 |
545 | ### Examples
546 |
547 | **Example: Product Description Generator**
548 |
549 | \`\`\`javascript
550 | {
551 | id: "product_entry",
552 | type: "Reference",
553 | name: "Product Entry",
554 | description: "The product entry to generate content for",
555 | configuration: {
556 | allowedEntities: ["Entry"]
557 | }
558 | }
559 | \`\`\`
560 |
561 | ### Best Practices
562 |
563 | 1. **Clear field paths**: Always specify exactly which field to use from the referenced entry
564 | 2. **Provide context**: Explain which content type or entry type should be referenced
565 | 3. **Consider localization**: Remember that fields may be localized, so paths typically include locale code
566 | 4. **Check existence**: Handle cases where referenced fields might be empty
567 | 5. **Document requirements**: Clearly explain which entry types are valid for the reference
568 |
569 | ### MCP Implementation Notes
570 |
571 | When using Reference variables with the MCP server:
572 |
573 | 1. The dynamic tool will include two parameters for each Reference:
574 | - The reference ID parameter (e.g., "product_entry")
575 | - The path parameter (e.g., "product_entry_path")
576 |
577 | 2. Always specify both when invoking the AI Action:
578 | \`\`\`javascript
579 | invoke_ai_action_product_description({
580 | product_entry: "6tFnSQdgHuWYOk8eICA0w",
581 | product_entry_path: "fields.description.en-US"
582 | });
583 | \`\`\`
584 |
585 | ### Implementation with MCP Tools
586 |
587 | \`\`\`javascript
588 | create_ai_action({
589 | // other parameters...
590 | instruction: {
591 | template: "Generate SEO metadata for this product...\\n\\nPRODUCT: {{product.fields.title}}\\n\\nDESCRIPTION: {{product.fields.description}}",
592 | variables: [
593 | {
594 | id: "product",
595 | type: "Reference",
596 | name: "Product Entry",
597 | description: "The product entry to create metadata for",
598 | configuration: {
599 | allowedEntities: ["Entry"]
600 | }
601 | }
602 | // other variables...
603 | ]
604 | }
605 | });
606 | \`\`\`
607 |
608 | When invoking this AI Action via MCP, you would provide both the entry ID and the specific fields to process.`;
609 |
610 | case "mediareference":
611 | case "media reference":
612 | return `## MediaReference Variable Type
613 |
614 | The MediaReference variable type enables AI Actions to work with digital assets such as images, videos, documents, and other media files in your Contentful space.
615 |
616 | ### Purpose
617 |
618 | MediaReference variables are ideal for:
619 | - Generating descriptions for images
620 | - Creating alt text for accessibility
621 | - Analyzing media content
622 | - Processing metadata from assets
623 | - Working with document content
624 |
625 | ### Configuration
626 |
627 | MediaReference variables require these properties:
628 |
629 | - **ID**: Unique identifier (e.g., "product_image")
630 | - **Name**: User-friendly label (e.g., "Product Image")
631 | - **Description**: Explanation of what asset to reference
632 | - **Configuration**: Typically minimal, as it's restricted to assets
633 |
634 | ### Field Path Specification
635 |
636 | Similar to References, when using MediaReferences in MCP, you need to provide:
637 | 1. The asset ID (which media asset to reference)
638 | 2. The field path (which aspect of the asset to use)
639 |
640 | This is handled through two parameters:
641 | - **media**: The asset ID to reference
642 | - **media_path**: The path to the field (e.g., "fields.file.en-US.url" or "fields.title.en-US")
643 |
644 | ### Template Usage
645 |
646 | In templates, you can access asset properties:
647 |
648 | \`\`\`
649 | IMAGE URL: {{product_image.fields.file.url}}
650 | IMAGE TITLE: {{product_image.fields.title}}
651 |
652 | Please generate an SEO-friendly alt text description for this product image that highlights key visual elements.
653 | \`\`\`
654 |
655 | ### Examples
656 |
657 | **Example: Image Alt Text Generator**
658 |
659 | \`\`\`javascript
660 | {
661 | id: "product_image",
662 | type: "MediaReference",
663 | name: "Product Image",
664 | description: "The product image to generate alt text for"
665 | }
666 | \`\`\`
667 |
668 | ### Best Practices
669 |
670 | 1. **Specify asset type**: Clearly indicate what type of asset should be referenced (image, video, etc.)
671 | 2. **Include guidance**: Explain what aspect of the asset will be processed
672 | 3. **Consider asset metadata**: Remember that assets have both file data and metadata fields
673 | 4. **Handle different asset types**: If your AI Action supports multiple asset types, provide clear instructions
674 |
675 | ### MCP Implementation Notes
676 |
677 | When using MediaReference variables with the MCP server:
678 |
679 | 1. The dynamic tool will include two parameters for each MediaReference:
680 | - The media reference parameter (e.g., "product_image")
681 | - The path parameter (e.g., "product_image_path")
682 |
683 | 2. Always specify both when invoking the AI Action:
684 | \`\`\`javascript
685 | invoke_ai_action_alt_text_generator({
686 | product_image: "7tGnRQegIvWZPj9eICA1q",
687 | product_image_path: "fields.file.en-US"
688 | });
689 | \`\`\`
690 |
691 | ### Common Path Values
692 |
693 | - **fields.file.{locale}**: To access the file data
694 | - **fields.title.{locale}**: To access the asset title
695 | - **fields.description.{locale}**: To access the asset description
696 |
697 | ### Implementation with MCP Tools
698 |
699 | \`\`\`javascript
700 | create_ai_action({
701 | // other parameters...
702 | instruction: {
703 | template: "Generate an SEO-friendly alt text for this image...\\n\\nImage context: {{image_context}}\\n\\nProduct category: {{product_category}}",
704 | variables: [
705 | {
706 | id: "product_image",
707 | type: "MediaReference",
708 | name: "Product Image",
709 | description: "The product image to generate alt text for"
710 | },
711 | {
712 | id: "image_context",
713 | type: "Text",
714 | name: "Image Context",
715 | description: "Additional context about the image"
716 | },
717 | {
718 | id: "product_category",
719 | type: "StringOptionsList",
720 | name: "Product Category",
721 | description: "The category of the product",
722 | configuration: {
723 | values: ["Apparel", "Electronics", "Home", "Beauty", "Food"]
724 | }
725 | }
726 | ]
727 | }
728 | });
729 | \`\`\`
730 |
731 | When invoking this action via MCP, you would provide the asset ID and the specific field path to process.`;
732 |
733 | case "locale":
734 | return `## Locale Variable Type
735 |
736 | The Locale variable type allows AI Actions to work with specific languages and regions, enabling localization and translation workflows in your content operations.
737 |
738 | ### Purpose
739 |
740 | Locale variables are perfect for:
741 | - Translation operations
742 | - Region-specific content generation
743 | - Language-aware content processing
744 | - Multilingual content workflows
745 |
746 | ### Configuration
747 |
748 | Locale variables have straightforward configuration:
749 |
750 | - **ID**: Unique identifier (e.g., "target_language")
751 | - **Name**: User-friendly label (e.g., "Target Language")
752 | - **Description**: Explanation of how the locale will be used
753 |
754 | No additional configuration properties are typically required.
755 |
756 | ### Format
757 |
758 | Locale values follow the standard language-country format:
759 | - **Language code**: 2-letter ISO language code (e.g., "en", "de", "fr")
760 | - **Country/region code**: 2-letter country code (e.g., "US", "DE", "FR")
761 | - **Combined**: language-country (e.g., "en-US", "de-DE", "fr-FR")
762 |
763 | ### Template Usage
764 |
765 | Reference Locale variables in templates using double curly braces:
766 |
767 | \`\`\`
768 | ORIGINAL CONTENT (en-US):
769 | {{input_text}}
770 |
771 | Please translate the above content into {{target_locale}}.
772 |
773 | TRANSLATED CONTENT ({{target_locale}}):
774 | \`\`\`
775 |
776 | ### Examples
777 |
778 | **Example: Content Translation**
779 |
780 | \`\`\`javascript
781 | {
782 | id: "target_locale",
783 | type: "Locale",
784 | name: "Target Language",
785 | description: "The language to translate the content into"
786 | }
787 | \`\`\`
788 |
789 | ### Best Practices
790 |
791 | 1. **Clear descriptions**: Specify whether you're looking for target or source language
792 | 2. **Validate locale format**: Ensure users enter valid locale codes (typically managed by the UI)
793 | 3. **Consider language variants**: Be clear about regional differences (e.g., en-US vs. en-GB)
794 | 4. **Use with other variables**: Combine with StandardInput for the content to be localized
795 |
796 | ### MCP Implementation
797 |
798 | In the MCP integration, Locale variables are typically presented as string parameters with descriptions that guide users to enter valid locale codes.
799 |
800 | ### Implementation with MCP Tools
801 |
802 | \`\`\`javascript
803 | create_ai_action({
804 | // other parameters...
805 | instruction: {
806 | template: "Translate the following content into {{target_locale}}...\\n\\nORIGINAL CONTENT:\\n{{input_text}}\\n\\nTRANSLATED CONTENT:",
807 | variables: [
808 | {
809 | id: "target_locale",
810 | type: "Locale",
811 | name: "Target Language",
812 | description: "The language to translate the content into (e.g., fr-FR, de-DE, ja-JP)"
813 | },
814 | {
815 | id: "input_text",
816 | type: "StandardInput",
817 | name: "Content to Translate",
818 | description: "The content that needs to be translated"
819 | }
820 | ]
821 | }
822 | });
823 | \`\`\`
824 |
825 | When invoking this action via MCP:
826 |
827 | \`\`\`javascript
828 | invoke_ai_action_translator({
829 | target_locale: "de-DE",
830 | input_text: "Welcome to our store. We offer the best products at competitive prices."
831 | });
832 | \`\`\`
833 |
834 | ### Common Locale Codes
835 |
836 | - **English**: en-US, en-GB, en-AU, en-CA
837 | - **Spanish**: es-ES, es-MX, es-AR
838 | - **French**: fr-FR, fr-CA
839 | - **German**: de-DE, de-AT, de-CH
840 | - **Japanese**: ja-JP
841 | - **Chinese**: zh-CN, zh-TW
842 | - **Portuguese**: pt-PT, pt-BR
843 | - **Italian**: it-IT
844 | - **Dutch**: nl-NL
845 | - **Russian**: ru-RU`;
846 |
847 | default:
848 | return `# AI Action Variables: ${variableType}
849 |
850 | The variable type "${variableType}" doesn't match any of the standard Contentful AI Action variable types. The standard types are:
851 |
852 | 1. StandardInput
853 | 2. Text
854 | 3. FreeFormInput
855 | 4. StringOptionsList
856 | 5. Reference
857 | 6. MediaReference
858 | 7. Locale
859 |
860 | Please check the spelling or request information about one of these standard types for detailed guidance.`;
861 | }
862 | }
```
--------------------------------------------------------------------------------
/src/types/tools.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Define interface for config parameter
2 | interface ConfigSchema {
3 | type: string
4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
5 | properties: Record<string, any>
6 | required?: string[]
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | [key: string]: any
9 | }
10 |
11 | export const getSpaceEnvProperties = (config: ConfigSchema): ConfigSchema => {
12 | const spaceEnvProperties = {
13 | spaceId: {
14 | type: "string",
15 | description:
16 | "The ID of the Contentful space. This must be the space's ID, not its name, ask for this ID if it's unclear.",
17 | },
18 | environmentId: {
19 | type: "string",
20 | description:
21 | "The ID of the environment within the space, by default this will be called Master",
22 | default: "master",
23 | },
24 | }
25 |
26 | if (!process.env.SPACE_ID && !process.env.ENVIRONMENT_ID) {
27 | return {
28 | ...config,
29 | properties: {
30 | ...config.properties,
31 | ...spaceEnvProperties,
32 | },
33 | required: [...(config.required || []), "spaceId", "environmentId"],
34 | }
35 | }
36 |
37 | return config
38 | }
39 |
40 | // Tool definitions for Entry operations
41 | export const getEntryTools = () => {
42 | return {
43 | SEARCH_ENTRIES: {
44 | name: "search_entries",
45 | description:
46 | "Search for entries using query parameters. Returns a maximum of 3 items per request. Use skip parameter to paginate through results.",
47 | inputSchema: getSpaceEnvProperties({
48 | type: "object",
49 | properties: {
50 | query: {
51 | type: "object",
52 | description: "Query parameters for searching entries",
53 | properties: {
54 | content_type: { type: "string" },
55 | select: { type: "string" },
56 | limit: {
57 | type: "number",
58 | default: 3,
59 | maximum: 3,
60 | description: "Maximum number of items to return (max: 3)",
61 | },
62 | skip: {
63 | type: "number",
64 | default: 0,
65 | description: "Number of items to skip for pagination",
66 | },
67 | order: { type: "string" },
68 | query: { type: "string" },
69 | },
70 | required: ["limit", "skip"],
71 | },
72 | },
73 | required: ["query"],
74 | }),
75 | },
76 | CREATE_ENTRY: {
77 | name: "create_entry",
78 | description:
79 | "Create a new entry in Contentful. Before executing this function, you need to know the contentTypeId (not the content type NAME) and the fields of that contentType. You can get the fields definition by using the GET_CONTENT_TYPE tool. IMPORTANT: All field values MUST include a locale key (e.g., 'en-US') for each value, like: { title: { 'en-US': 'My Title' } }. Every field in Contentful requires a locale even for single-language content.",
80 | inputSchema: getSpaceEnvProperties({
81 | type: "object",
82 | properties: {
83 | contentTypeId: {
84 | type: "string",
85 | description: "The ID of the content type for the new entry",
86 | },
87 | fields: {
88 | type: "object",
89 | description:
90 | "The fields of the entry with localized values. Example: { title: { 'en-US': 'My Title' }, description: { 'en-US': 'My Description' } }",
91 | },
92 | },
93 | required: ["contentTypeId", "fields"],
94 | }),
95 | },
96 | GET_ENTRY: {
97 | name: "get_entry",
98 | description: "Retrieve an existing entry",
99 | inputSchema: getSpaceEnvProperties({
100 | type: "object",
101 | properties: {
102 | entryId: { type: "string" },
103 | },
104 | required: ["entryId"],
105 | }),
106 | },
107 | UPDATE_ENTRY: {
108 | name: "update_entry",
109 | description:
110 | "Update an existing entry. The handler will merge your field updates with the existing entry fields, so you only need to provide the fields and locales you want to change. IMPORTANT: All field values MUST include a locale key (e.g., 'en-US') for each value, like: { title: { 'en-US': 'My Updated Title' } }. Every field in Contentful requires a locale even for single-language content.",
111 | inputSchema: getSpaceEnvProperties({
112 | type: "object",
113 | properties: {
114 | entryId: { type: "string" },
115 | fields: {
116 | type: "object",
117 | description:
118 | "The fields to update with localized values. Example: { title: { 'en-US': 'My Updated Title' } }",
119 | },
120 | },
121 | required: ["entryId", "fields"],
122 | }),
123 | },
124 | DELETE_ENTRY: {
125 | name: "delete_entry",
126 | description: "Delete an entry",
127 | inputSchema: getSpaceEnvProperties({
128 | type: "object",
129 | properties: {
130 | entryId: { type: "string" },
131 | },
132 | required: ["entryId"],
133 | }),
134 | },
135 | PUBLISH_ENTRY: {
136 | name: "publish_entry",
137 | description:
138 | "Publish an entry or multiple entries. Accepts either a single entryId (string) or an array of entryIds (up to 100 entries). For a single entry, it uses the standard publish operation. For multiple entries, it automatically uses bulk publishing.",
139 | inputSchema: getSpaceEnvProperties({
140 | type: "object",
141 | properties: {
142 | entryId: {
143 | oneOf: [
144 | { type: "string" },
145 | {
146 | type: "array",
147 | items: { type: "string" },
148 | maxItems: 100,
149 | description: "Array of entry IDs to publish (max: 100)",
150 | },
151 | ],
152 | description: "ID of the entry to publish, or an array of entry IDs (max: 100)",
153 | },
154 | },
155 | required: ["entryId"],
156 | }),
157 | },
158 | UNPUBLISH_ENTRY: {
159 | name: "unpublish_entry",
160 | description:
161 | "Unpublish an entry or multiple entries. Accepts either a single entryId (string) or an array of entryIds (up to 100 entries). For a single entry, it uses the standard unpublish operation. For multiple entries, it automatically uses bulk unpublishing.",
162 | inputSchema: getSpaceEnvProperties({
163 | type: "object",
164 | properties: {
165 | entryId: {
166 | oneOf: [
167 | { type: "string" },
168 | {
169 | type: "array",
170 | items: { type: "string" },
171 | maxItems: 100,
172 | description: "Array of entry IDs to unpublish (max: 100)",
173 | },
174 | ],
175 | description: "ID of the entry to unpublish, or an array of entry IDs (max: 100)",
176 | },
177 | },
178 | required: ["entryId"],
179 | }),
180 | },
181 | }
182 | }
183 |
184 | // Tool definitions for Asset operations
185 | export const getAssetTools = () => {
186 | return {
187 | LIST_ASSETS: {
188 | name: "list_assets",
189 | description:
190 | "List assets in a space. Returns a maximum of 3 items per request. Use skip parameter to paginate through results.",
191 | inputSchema: getSpaceEnvProperties({
192 | type: "object",
193 | properties: {
194 | limit: {
195 | type: "number",
196 | default: 3,
197 | maximum: 3,
198 | description: "Maximum number of items to return (max: 3)",
199 | },
200 | skip: {
201 | type: "number",
202 | default: 0,
203 | description: "Number of items to skip for pagination",
204 | },
205 | },
206 | required: ["limit", "skip"],
207 | }),
208 | },
209 | UPLOAD_ASSET: {
210 | name: "upload_asset",
211 | description: "Upload a new asset",
212 | inputSchema: getSpaceEnvProperties({
213 | type: "object",
214 | properties: {
215 | title: { type: "string" },
216 | description: { type: "string" },
217 | file: {
218 | type: "object",
219 | properties: {
220 | upload: { type: "string" },
221 | fileName: { type: "string" },
222 | contentType: { type: "string" },
223 | },
224 | required: ["upload", "fileName", "contentType"],
225 | },
226 | },
227 | required: ["title", "file"],
228 | }),
229 | },
230 | GET_ASSET: {
231 | name: "get_asset",
232 | description: "Retrieve an asset",
233 | inputSchema: getSpaceEnvProperties({
234 | type: "object",
235 | properties: {
236 | assetId: { type: "string" },
237 | },
238 | required: ["assetId"],
239 | }),
240 | },
241 | UPDATE_ASSET: {
242 | name: "update_asset",
243 | description: "Update an asset",
244 | inputSchema: getSpaceEnvProperties({
245 | type: "object",
246 | properties: {
247 | assetId: { type: "string" },
248 | title: { type: "string" },
249 | description: { type: "string" },
250 | file: {
251 | type: "object",
252 | properties: {
253 | url: { type: "string" },
254 | fileName: { type: "string" },
255 | contentType: { type: "string" },
256 | },
257 | required: ["url", "fileName", "contentType"],
258 | },
259 | },
260 | required: ["assetId"],
261 | }),
262 | },
263 | DELETE_ASSET: {
264 | name: "delete_asset",
265 | description: "Delete an asset",
266 | inputSchema: getSpaceEnvProperties({
267 | type: "object",
268 | properties: {
269 | assetId: { type: "string" },
270 | },
271 | required: ["assetId"],
272 | }),
273 | },
274 | PUBLISH_ASSET: {
275 | name: "publish_asset",
276 | description: "Publish an asset",
277 | inputSchema: getSpaceEnvProperties({
278 | type: "object",
279 | properties: {
280 | assetId: { type: "string" },
281 | },
282 | required: ["assetId"],
283 | }),
284 | },
285 | UNPUBLISH_ASSET: {
286 | name: "unpublish_asset",
287 | description: "Unpublish an asset",
288 | inputSchema: getSpaceEnvProperties({
289 | type: "object",
290 | properties: {
291 | assetId: { type: "string" },
292 | },
293 | required: ["assetId"],
294 | }),
295 | },
296 | }
297 | }
298 |
299 | // Tool definitions for Content Type operations
300 | export const getContentTypeTools = () => {
301 | return {
302 | LIST_CONTENT_TYPES: {
303 | name: "list_content_types",
304 | description:
305 | "List content types in a space. Returns a maximum of 10 items per request. Use skip parameter to paginate through results.",
306 | inputSchema: getSpaceEnvProperties({
307 | type: "object",
308 | properties: {
309 | limit: {
310 | type: "number",
311 | default: 10,
312 | maximum: 20,
313 | description: "Maximum number of items to return (max: 3)",
314 | },
315 | skip: {
316 | type: "number",
317 | default: 0,
318 | description: "Number of items to skip for pagination",
319 | },
320 | },
321 | required: ["limit", "skip"],
322 | }),
323 | },
324 | GET_CONTENT_TYPE: {
325 | name: "get_content_type",
326 | description: "Get details of a specific content type",
327 | inputSchema: getSpaceEnvProperties({
328 | type: "object",
329 | properties: {
330 | contentTypeId: { type: "string" },
331 | },
332 | required: ["contentTypeId"],
333 | }),
334 | },
335 | CREATE_CONTENT_TYPE: {
336 | name: "create_content_type",
337 | description: "Create a new content type",
338 | inputSchema: getSpaceEnvProperties({
339 | type: "object",
340 | properties: {
341 | name: { type: "string" },
342 | fields: {
343 | type: "array",
344 | description: "Array of field definitions for the content type",
345 | items: {
346 | type: "object",
347 | properties: {
348 | id: {
349 | type: "string",
350 | description: "The ID of the field",
351 | },
352 | name: {
353 | type: "string",
354 | description: "Display name of the field",
355 | },
356 | type: {
357 | type: "string",
358 | description:
359 | "Type of the field (Text, Number, Date, Location, Media, Boolean, JSON, Link, Array, etc)",
360 | enum: [
361 | "Symbol",
362 | "Text",
363 | "Integer",
364 | "Number",
365 | "Date",
366 | "Location",
367 | "Object",
368 | "Boolean",
369 | "Link",
370 | "Array",
371 | ],
372 | },
373 | required: {
374 | type: "boolean",
375 | description: "Whether this field is required",
376 | default: false,
377 | },
378 | localized: {
379 | type: "boolean",
380 | description: "Whether this field can be localized",
381 | default: false,
382 | },
383 | linkType: {
384 | type: "string",
385 | description:
386 | "Required for Link fields. Specifies what type of resource this field links to",
387 | enum: ["Entry", "Asset"],
388 | },
389 | items: {
390 | type: "object",
391 | description:
392 | "Required for Array fields. Specifies the type of items in the array",
393 | properties: {
394 | type: {
395 | type: "string",
396 | enum: ["Symbol", "Link"],
397 | },
398 | linkType: {
399 | type: "string",
400 | enum: ["Entry", "Asset"],
401 | },
402 | validations: {
403 | type: "array",
404 | items: {
405 | type: "object",
406 | },
407 | },
408 | },
409 | },
410 | validations: {
411 | type: "array",
412 | description: "Array of validation rules for the field",
413 | items: {
414 | type: "object",
415 | },
416 | },
417 | },
418 | required: ["id", "name", "type"],
419 | },
420 | },
421 | description: { type: "string" },
422 | displayField: { type: "string" },
423 | },
424 | required: ["name", "fields"],
425 | }),
426 | },
427 | UPDATE_CONTENT_TYPE: {
428 | name: "update_content_type",
429 | description:
430 | "Update an existing content type. The handler will merge your field updates with existing content type data, so you only need to provide the fields and properties you want to change.",
431 | inputSchema: getSpaceEnvProperties({
432 | type: "object",
433 | properties: {
434 | contentTypeId: { type: "string" },
435 | name: { type: "string" },
436 | fields: {
437 | type: "array",
438 | items: { type: "object" },
439 | },
440 | description: { type: "string" },
441 | displayField: { type: "string" },
442 | },
443 | required: ["contentTypeId", "fields"],
444 | }),
445 | },
446 | DELETE_CONTENT_TYPE: {
447 | name: "delete_content_type",
448 | description: "Delete a content type",
449 | inputSchema: getSpaceEnvProperties({
450 | type: "object",
451 | properties: {
452 | contentTypeId: { type: "string" },
453 | },
454 | required: ["contentTypeId"],
455 | }),
456 | },
457 | PUBLISH_CONTENT_TYPE: {
458 | name: "publish_content_type",
459 | description: "Publish a content type",
460 | inputSchema: getSpaceEnvProperties({
461 | type: "object",
462 | properties: {
463 | contentTypeId: { type: "string" },
464 | },
465 | required: ["contentTypeId"],
466 | }),
467 | },
468 | }
469 | }
470 |
471 | // Tool definitions for Space & Environment operations
472 | export const getSpaceEnvTools = () => {
473 | if (process.env.SPACE_ID && process.env.ENVIRONMENT_ID) {
474 | return {}
475 | }
476 | return {
477 | LIST_SPACES: {
478 | name: "list_spaces",
479 | description: "List all available spaces",
480 | inputSchema: {
481 | type: "object",
482 | properties: {},
483 | },
484 | },
485 | GET_SPACE: {
486 | name: "get_space",
487 | description: "Get details of a space",
488 | inputSchema: {
489 | type: "object",
490 | properties: {
491 | spaceId: { type: "string" },
492 | },
493 | required: ["spaceId"],
494 | },
495 | },
496 | LIST_ENVIRONMENTS: {
497 | name: "list_environments",
498 | description: "List all environments in a space",
499 | inputSchema: {
500 | type: "object",
501 | properties: {
502 | spaceId: { type: "string" },
503 | },
504 | required: ["spaceId"],
505 | },
506 | },
507 | CREATE_ENVIRONMENT: {
508 | name: "create_environment",
509 | description: "Create a new environment",
510 | inputSchema: {
511 | type: "object",
512 | properties: {
513 | spaceId: { type: "string" },
514 | environmentId: { type: "string" },
515 | name: { type: "string" },
516 | },
517 | required: ["spaceId", "environmentId", "name"],
518 | },
519 | },
520 | DELETE_ENVIRONMENT: {
521 | name: "delete_environment",
522 | description: "Delete an environment",
523 | inputSchema: {
524 | type: "object",
525 | properties: {
526 | spaceId: { type: "string" },
527 | environmentId: { type: "string" },
528 | },
529 | required: ["spaceId", "environmentId"],
530 | },
531 | },
532 | }
533 | }
534 |
535 | // Tool definitions for Bulk Actions
536 | export const getBulkActionTools = () => {
537 | return {
538 | BULK_VALIDATE: {
539 | name: "bulk_validate",
540 | description: "Validate multiple entries at once",
541 | inputSchema: getSpaceEnvProperties({
542 | type: "object",
543 | properties: {
544 | entryIds: {
545 | type: "array",
546 | description: "Array of entry IDs to validate",
547 | items: {
548 | type: "string",
549 | },
550 | },
551 | },
552 | required: ["entryIds"],
553 | }),
554 | },
555 | }
556 | }
557 |
558 | // Tool definitions for AI Actions
559 | export const getAiActionTools = () => {
560 | return {
561 | LIST_AI_ACTIONS: {
562 | name: "list_ai_actions",
563 | description: "List all AI Actions in a space",
564 | inputSchema: getSpaceEnvProperties({
565 | type: "object",
566 | properties: {
567 | limit: {
568 | type: "number",
569 | default: 100,
570 | description: "Maximum number of AI Actions to return",
571 | },
572 | skip: {
573 | type: "number",
574 | default: 0,
575 | description: "Number of AI Actions to skip for pagination",
576 | },
577 | status: {
578 | type: "string",
579 | enum: ["all", "published"],
580 | description: "Filter AI Actions by status",
581 | },
582 | },
583 | required: [],
584 | }),
585 | },
586 | GET_AI_ACTION: {
587 | name: "get_ai_action",
588 | description: "Get a specific AI Action by ID",
589 | inputSchema: getSpaceEnvProperties({
590 | type: "object",
591 | properties: {
592 | aiActionId: {
593 | type: "string",
594 | description: "The ID of the AI Action to retrieve",
595 | },
596 | },
597 | required: ["aiActionId"],
598 | }),
599 | },
600 | CREATE_AI_ACTION: {
601 | name: "create_ai_action",
602 | description: "Create a new AI Action",
603 | inputSchema: getSpaceEnvProperties({
604 | type: "object",
605 | properties: {
606 | name: {
607 | type: "string",
608 | description: "The name of the AI Action",
609 | },
610 | description: {
611 | type: "string",
612 | description: "The description of the AI Action",
613 | },
614 | instruction: {
615 | type: "object",
616 | description: "The instruction object containing the template and variables",
617 | properties: {
618 | template: {
619 | type: "string",
620 | description: "The prompt template with variable placeholders",
621 | },
622 | variables: {
623 | type: "array",
624 | description: "Array of variable definitions",
625 | items: {
626 | type: "object",
627 | },
628 | },
629 | conditions: {
630 | type: "array",
631 | description: "Optional array of conditions for the template",
632 | items: {
633 | type: "object",
634 | },
635 | },
636 | },
637 | required: ["template", "variables"],
638 | },
639 | configuration: {
640 | type: "object",
641 | description: "The model configuration",
642 | properties: {
643 | modelType: {
644 | type: "string",
645 | description: "The type of model to use (e.g., gpt-4)",
646 | },
647 | modelTemperature: {
648 | type: "number",
649 | description: "The temperature setting for the model (0.0 to 1.0)",
650 | minimum: 0,
651 | maximum: 1,
652 | },
653 | },
654 | required: ["modelType", "modelTemperature"],
655 | },
656 | testCases: {
657 | type: "array",
658 | description: "Optional array of test cases for the AI Action",
659 | items: {
660 | type: "object",
661 | },
662 | },
663 | },
664 | required: ["name", "description", "instruction", "configuration"],
665 | }),
666 | },
667 | UPDATE_AI_ACTION: {
668 | name: "update_ai_action",
669 | description: "Update an existing AI Action",
670 | inputSchema: getSpaceEnvProperties({
671 | type: "object",
672 | properties: {
673 | aiActionId: {
674 | type: "string",
675 | description: "The ID of the AI Action to update",
676 | },
677 | name: {
678 | type: "string",
679 | description: "The name of the AI Action",
680 | },
681 | description: {
682 | type: "string",
683 | description: "The description of the AI Action",
684 | },
685 | instruction: {
686 | type: "object",
687 | description: "The instruction object containing the template and variables",
688 | properties: {
689 | template: {
690 | type: "string",
691 | description: "The prompt template with variable placeholders",
692 | },
693 | variables: {
694 | type: "array",
695 | description: "Array of variable definitions",
696 | items: {
697 | type: "object",
698 | },
699 | },
700 | conditions: {
701 | type: "array",
702 | description: "Optional array of conditions for the template",
703 | items: {
704 | type: "object",
705 | },
706 | },
707 | },
708 | required: ["template", "variables"],
709 | },
710 | configuration: {
711 | type: "object",
712 | description: "The model configuration",
713 | properties: {
714 | modelType: {
715 | type: "string",
716 | description: "The type of model to use (e.g., gpt-4)",
717 | },
718 | modelTemperature: {
719 | type: "number",
720 | description: "The temperature setting for the model (0.0 to 1.0)",
721 | minimum: 0,
722 | maximum: 1,
723 | },
724 | },
725 | required: ["modelType", "modelTemperature"],
726 | },
727 | testCases: {
728 | type: "array",
729 | description: "Optional array of test cases for the AI Action",
730 | items: {
731 | type: "object",
732 | },
733 | },
734 | },
735 | required: ["aiActionId", "name", "description", "instruction", "configuration"],
736 | }),
737 | },
738 | DELETE_AI_ACTION: {
739 | name: "delete_ai_action",
740 | description: "Delete an AI Action",
741 | inputSchema: getSpaceEnvProperties({
742 | type: "object",
743 | properties: {
744 | aiActionId: {
745 | type: "string",
746 | description: "The ID of the AI Action to delete",
747 | },
748 | },
749 | required: ["aiActionId"],
750 | }),
751 | },
752 | PUBLISH_AI_ACTION: {
753 | name: "publish_ai_action",
754 | description: "Publish an AI Action",
755 | inputSchema: getSpaceEnvProperties({
756 | type: "object",
757 | properties: {
758 | aiActionId: {
759 | type: "string",
760 | description: "The ID of the AI Action to publish",
761 | },
762 | },
763 | required: ["aiActionId"],
764 | }),
765 | },
766 | UNPUBLISH_AI_ACTION: {
767 | name: "unpublish_ai_action",
768 | description: "Unpublish an AI Action",
769 | inputSchema: getSpaceEnvProperties({
770 | type: "object",
771 | properties: {
772 | aiActionId: {
773 | type: "string",
774 | description: "The ID of the AI Action to unpublish",
775 | },
776 | },
777 | required: ["aiActionId"],
778 | }),
779 | },
780 | INVOKE_AI_ACTION: {
781 | name: "invoke_ai_action",
782 | description: "Invoke an AI Action with variables",
783 | inputSchema: getSpaceEnvProperties({
784 | type: "object",
785 | properties: {
786 | aiActionId: {
787 | type: "string",
788 | description: "The ID of the AI Action to invoke",
789 | },
790 | variables: {
791 | type: "object",
792 | description: "Key-value pairs of variable IDs and their values",
793 | additionalProperties: {
794 | type: "string",
795 | },
796 | },
797 | rawVariables: {
798 | type: "array",
799 | description:
800 | "Array of raw variable objects (for complex variable types like references)",
801 | items: {
802 | type: "object",
803 | },
804 | },
805 | outputFormat: {
806 | type: "string",
807 | enum: ["Markdown", "RichText", "PlainText"],
808 | default: "Markdown",
809 | description: "The format of the output content",
810 | },
811 | waitForCompletion: {
812 | type: "boolean",
813 | default: true,
814 | description: "Whether to wait for the AI Action to complete before returning",
815 | },
816 | },
817 | required: ["aiActionId"],
818 | }),
819 | },
820 | GET_AI_ACTION_INVOCATION: {
821 | name: "get_ai_action_invocation",
822 | description: "Get the result of a previous AI Action invocation",
823 | inputSchema: getSpaceEnvProperties({
824 | type: "object",
825 | properties: {
826 | aiActionId: {
827 | type: "string",
828 | description: "The ID of the AI Action",
829 | },
830 | invocationId: {
831 | type: "string",
832 | description: "The ID of the specific invocation to retrieve",
833 | },
834 | },
835 | required: ["aiActionId", "invocationId"],
836 | }),
837 | },
838 | }
839 | }
840 |
841 | // Tool definitions for Comment operations
842 | export const getCommentTools = () => {
843 | return {
844 | GET_COMMENTS: {
845 | name: "get_comments",
846 | description:
847 | "Retrieve comments for an entry with pagination support. Returns comments with their status and body content.",
848 | inputSchema: getSpaceEnvProperties({
849 | type: "object",
850 | properties: {
851 | entryId: {
852 | type: "string",
853 | description: "The unique identifier of the entry to get comments for",
854 | },
855 | bodyFormat: {
856 | type: "string",
857 | enum: ["plain-text", "rich-text"],
858 | default: "plain-text",
859 | description: "Format for the comment body content",
860 | },
861 | status: {
862 | type: "string",
863 | enum: ["active", "resolved", "all"],
864 | default: "active",
865 | description: "Filter comments by status",
866 | },
867 | limit: {
868 | type: "number",
869 | default: 10,
870 | minimum: 1,
871 | maximum: 100,
872 | description: "Maximum number of comments to return (1-100, default: 10)",
873 | },
874 | skip: {
875 | type: "number",
876 | default: 0,
877 | minimum: 0,
878 | description: "Number of comments to skip for pagination (default: 0)",
879 | },
880 | },
881 | required: ["entryId"],
882 | }),
883 | },
884 | CREATE_COMMENT: {
885 | name: "create_comment",
886 | description:
887 | "Create a new comment on an entry. The comment will be created with the specified body and status. To create a threaded conversation (reply to an existing comment), provide the parent comment ID. This allows you to work around the 512-character limit by creating threaded replies.",
888 | inputSchema: getSpaceEnvProperties({
889 | type: "object",
890 | properties: {
891 | entryId: {
892 | type: "string",
893 | description: "The unique identifier of the entry to comment on",
894 | },
895 | body: {
896 | type: "string",
897 | description: "The content of the comment (max 512 characters)",
898 | },
899 | status: {
900 | type: "string",
901 | enum: ["active"],
902 | default: "active",
903 | description: "The status of the comment",
904 | },
905 | parent: {
906 | type: "string",
907 | description:
908 | "Optional ID of the parent comment to reply to. Use this to create threaded conversations or to continue longer messages by replying to your own comments.",
909 | },
910 | },
911 | required: ["entryId", "body"],
912 | }),
913 | },
914 | GET_SINGLE_COMMENT: {
915 | name: "get_single_comment",
916 | description: "Retrieve a specific comment by its ID for an entry.",
917 | inputSchema: getSpaceEnvProperties({
918 | type: "object",
919 | properties: {
920 | entryId: {
921 | type: "string",
922 | description: "The unique identifier of the entry",
923 | },
924 | commentId: {
925 | type: "string",
926 | description: "The unique identifier of the comment to retrieve",
927 | },
928 | bodyFormat: {
929 | type: "string",
930 | enum: ["plain-text", "rich-text"],
931 | default: "plain-text",
932 | description: "Format for the comment body content",
933 | },
934 | },
935 | required: ["entryId", "commentId"],
936 | }),
937 | },
938 | DELETE_COMMENT: {
939 | name: "delete_comment",
940 | description: "Delete a specific comment from an entry.",
941 | inputSchema: getSpaceEnvProperties({
942 | type: "object",
943 | properties: {
944 | entryId: {
945 | type: "string",
946 | description: "The unique identifier of the entry",
947 | },
948 | commentId: {
949 | type: "string",
950 | description: "The unique identifier of the comment to delete",
951 | },
952 | },
953 | required: ["entryId", "commentId"],
954 | }),
955 | },
956 | UPDATE_COMMENT: {
957 | name: "update_comment",
958 | description:
959 | "Update an existing comment on an entry. The handler will merge your updates with the existing comment data.",
960 | inputSchema: getSpaceEnvProperties({
961 | type: "object",
962 | properties: {
963 | entryId: {
964 | type: "string",
965 | description: "The unique identifier of the entry",
966 | },
967 | commentId: {
968 | type: "string",
969 | description: "The unique identifier of the comment to update",
970 | },
971 | body: {
972 | type: "string",
973 | description: "The updated content of the comment",
974 | },
975 | status: {
976 | type: "string",
977 | enum: ["active", "resolved"],
978 | description: "The updated status of the comment",
979 | },
980 | bodyFormat: {
981 | type: "string",
982 | enum: ["plain-text", "rich-text"],
983 | default: "plain-text",
984 | description: "Format for the comment body content",
985 | },
986 | },
987 | required: ["entryId", "commentId"],
988 | }),
989 | },
990 | }
991 | }
992 |
993 | // Export combined tools
994 | export const getTools = () => {
995 | return {
996 | ...getEntryTools(),
997 | ...getAssetTools(),
998 | ...getContentTypeTools(),
999 | ...getSpaceEnvTools(),
1000 | ...getBulkActionTools(),
1001 | ...getAiActionTools(),
1002 | ...getCommentTools(),
1003 | }
1004 | }
1005 |
```