From 6e9261776ceeb7855a19c0938719b8b497226d22 Mon Sep 17 00:00:00 2001 From: Thomas Kellermeier Date: Fri, 25 Sep 2026 15:24:22 +0200 Subject: [PATCH 1/3] fix(exo): allow viewport-free tool payloads [SPA-5371] --- .../exo/components/createComponent.test.ts | 11 ++++++++ .../tools/exo/components/createComponent.ts | 3 +- .../exo/components/upsertComponent.test.ts | 14 ++++++++++ .../tools/exo/components/upsertComponent.ts | 4 ++- .../createExperienceFragment.test.ts | 21 ++++++++++++++ .../createExperienceFragment.ts | 12 ++++++-- .../updateExperienceFragment.test.ts | 19 +++++++++++++ .../updateExperienceFragment.ts | 13 +++++++-- .../createExperienceTemplate.test.ts | 11 ++++++++ .../createExperienceTemplate.ts | 3 +- .../upsertExperienceTemplate.test.ts | 14 ++++++++++ .../upsertExperienceTemplate.ts | 4 ++- .../exo/experiences/createExperience.test.ts | 21 ++++++++++++++ .../tools/exo/experiences/createExperience.ts | 12 ++++++-- .../exo/experiences/upsertExperience.test.ts | 28 +++++++++++++++++-- .../tools/exo/experiences/upsertExperience.ts | 13 +++++++-- 16 files changed, 189 insertions(+), 14 deletions(-) diff --git a/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts b/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts index a088613c..25175b0b 100644 --- a/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts +++ b/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts @@ -89,6 +89,17 @@ describe('createComponent', () => { }); }); + it('omits viewports when creating a viewport-free component', async () => { + const { viewports: _, ...viewportFreeArgs } = args; + mockComponentCreate.mockResolvedValue(mockComponent); + + await createComponentTool(mockConfig)(viewportFreeArgs); + + expect(mockComponentCreate.mock.calls[0][1]).not.toHaveProperty( + 'viewports', + ); + }); + it('rejects writes to a protected environment', async () => { const protectedConfig = createMockConfig({ protectedEnvironments: ['test-environment'], diff --git a/packages/mcp-tools/src/tools/exo/components/createComponent.ts b/packages/mcp-tools/src/tools/exo/components/createComponent.ts index 81d77eb1..9e3e7969 100644 --- a/packages/mcp-tools/src/tools/exo/components/createComponent.ts +++ b/packages/mcp-tools/src/tools/exo/components/createComponent.ts @@ -29,6 +29,7 @@ export const CreateComponentToolParams = BaseToolSchema.extend({ description: z.string().describe('Description of the component'), viewports: z .array(ViewportSchema) + .optional() .describe('Viewport definitions for the component (may be empty)'), contentProperties: z .array(ContentPropertySchema) @@ -63,7 +64,7 @@ export function createComponentTool(config: ContentfulConfig) { const componentData = { name: args.name, description: args.description, - viewports: args.viewports, + ...(args.viewports !== undefined && { viewports: args.viewports }), contentProperties: args.contentProperties, designProperties: args.designProperties, ...(args.componentTree && { componentTree: args.componentTree }), diff --git a/packages/mcp-tools/src/tools/exo/components/upsertComponent.test.ts b/packages/mcp-tools/src/tools/exo/components/upsertComponent.test.ts index d4c7e480..7a267cbc 100644 --- a/packages/mcp-tools/src/tools/exo/components/upsertComponent.test.ts +++ b/packages/mcp-tools/src/tools/exo/components/upsertComponent.test.ts @@ -79,6 +79,20 @@ describe('upsertComponent', () => { expect(body.dataAssemblies).toEqual(dataAssemblies); }); + it('does not reintroduce viewports from a viewport-free component', async () => { + mockComponentGet.mockResolvedValue({ + ...mockComponent, + viewports: undefined, + }); + mockComponentUpsert.mockResolvedValue(mockComponent); + + await upsertComponentTool(mockConfig)({ ...mockArgs, version: 1 }); + + expect(mockComponentUpsert.mock.calls[0][1]).not.toHaveProperty( + 'viewports', + ); + }); + it('rejects a stale version', async () => { mockComponentGet.mockResolvedValue(mockComponent); // sys.version === 1 diff --git a/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts b/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts index 475eb196..52d4683d 100644 --- a/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts +++ b/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts @@ -94,7 +94,9 @@ export function upsertComponentTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - viewports: args.viewports ?? current.viewports, + ...((args.viewports ?? current.viewports) !== undefined && { + viewports: args.viewports ?? current.viewports, + }), contentProperties: args.contentProperties ?? current.contentProperties, designProperties: args.designProperties ?? current.designProperties, ...((args.componentTree ?? current.componentTree) diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts index 2925e27d..d722803f 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts @@ -47,6 +47,27 @@ describe('createExperienceFragment', () => { ); }); + it('creates a viewport-free experience fragment with flattened design properties', async () => { + const { viewports: _, ...viewportFreeArgs } = baseArgs; + mockExperienceFragmentCreate.mockResolvedValue(mockExperienceFragment); + + await createExperienceFragmentTool(mockConfig)({ + ...viewportFreeArgs, + designProperties: { + color: { type: 'ManualDesignValue', value: 'red' }, + }, + }); + + expect(mockExperienceFragmentCreate.mock.calls[0][1]).toMatchObject({ + designProperties: { + color: { type: 'ManualDesignValue', value: 'red' }, + }, + }); + expect(mockExperienceFragmentCreate.mock.calls[0][1]).not.toHaveProperty( + 'viewports', + ); + }); + it('rejects creates in a protected environment', async () => { const tool = createExperienceFragmentTool( createMockConfig({ protectedEnvironments: ['test-environment'] }), diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts index 15cb87ac..e778d740 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts @@ -11,6 +11,7 @@ import { import { ViewportSchema, ExperienceMetadataSchema, + DesignPropertyValueSchema, DimensionedDesignPropertyValueSchema, ExperienceContentBindingsSchema, ExperienceSlotNodeSchema, @@ -26,9 +27,16 @@ export const CreateExperienceFragmentToolParams = BaseToolSchema.extend({ ), viewports: z .array(ViewportSchema) + .optional() .describe('Viewport definitions (may be empty)'), designProperties: z - .record(z.string(), DimensionedDesignPropertyValueSchema) + .record( + z.string(), + z.union([ + DesignPropertyValueSchema, + DimensionedDesignPropertyValueSchema, + ]), + ) .describe( 'Design property values keyed by property ID (may be empty object)', ), @@ -61,7 +69,7 @@ export function createExperienceFragmentTool(config: ContentfulConfig) { name: args.name, description: args.description, component: args.component, - viewports: args.viewports, + ...(args.viewports !== undefined && { viewports: args.viewports }), designProperties: args.designProperties, ...(args.contentBindings && { contentBindings: args.contentBindings }), ...(args.slots && { slots: args.slots }), diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.test.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.test.ts index 65c5d1e2..22c2a10b 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.test.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.test.ts @@ -48,6 +48,25 @@ describe('updateExperienceFragment', () => { ); }); + it('preserves flattened design properties without reintroducing viewports', async () => { + mockExperienceFragmentGet.mockResolvedValue({ + ...mockExperienceFragment, + viewports: undefined, + designProperties: { + color: { type: 'ManualDesignValue', value: 'red' }, + }, + }); + mockExperienceFragmentUpsert.mockResolvedValue(mockExperienceFragment); + + await updateExperienceFragmentTool(mockConfig)({ ...mockArgs, version: 1 }); + + const [, body] = mockExperienceFragmentUpsert.mock.calls[0]; + expect(body).not.toHaveProperty('viewports'); + expect(body.designProperties).toEqual({ + color: { type: 'ManualDesignValue', value: 'red' }, + }); + }); + it('rejects a stale version', async () => { mockExperienceFragmentGet.mockResolvedValue(mockExperienceFragment); // sys.version === 1 diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts index 42ea62a2..2a0184e6 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts @@ -11,6 +11,7 @@ import { import { ViewportSchema, ExperienceMetadataSchema, + DesignPropertyValueSchema, DimensionedDesignPropertyValueSchema, ExperienceContentBindingsSchema, ExperienceSlotNodeSchema, @@ -39,7 +40,13 @@ export const UpdateExperienceFragmentToolParams = BaseToolSchema.extend({ .optional() .describe('Viewport definitions; replaces existing viewports if provided'), designProperties: z - .record(z.string(), DimensionedDesignPropertyValueSchema) + .record( + z.string(), + z.union([ + DesignPropertyValueSchema, + DimensionedDesignPropertyValueSchema, + ]), + ) .optional() .describe('Design property values; replaces existing if provided'), contentBindings: ExperienceContentBindingsSchema.optional().describe( @@ -97,7 +104,9 @@ export function updateExperienceFragmentTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - viewports: args.viewports ?? current.viewports, + ...((args.viewports ?? current.viewports) !== undefined && { + viewports: args.viewports ?? current.viewports, + }), designProperties: args.designProperties ?? current.designProperties, ...((args.contentBindings ?? current.contentBindings) ? { contentBindings: args.contentBindings ?? current.contentBindings } diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts index ae0693e0..400dbf12 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts @@ -35,6 +35,17 @@ describe('createExperienceTemplate', () => { ); }); + it('omits viewports when creating a viewport-free experience template', async () => { + const { viewports: _, ...viewportFreeArgs } = createArgs; + mockExperienceTemplateCreate.mockResolvedValue(mockExperienceTemplate); + + await createExperienceTemplateTool(mockConfig)(viewportFreeArgs); + + expect(mockExperienceTemplateCreate.mock.calls[0][1]).not.toHaveProperty( + 'viewports', + ); + }); + it('rejects writes to a protected environment', async () => { const tool = createExperienceTemplateTool( createMockConfig({ protectedEnvironments: ['test-environment'] }), diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts index 107e798f..a628c2e8 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts @@ -23,6 +23,7 @@ export const CreateExperienceTemplateToolParams = BaseToolSchema.extend({ description: z.string().describe('Description of the experience template'), viewports: z .array(ViewportSchema) + .optional() .describe( 'Viewport definitions for the experience template (may be empty)', ), @@ -61,7 +62,7 @@ export function createExperienceTemplateTool(config: ContentfulConfig) { { name: args.name, description: args.description, - viewports: args.viewports, + ...(args.viewports !== undefined && { viewports: args.viewports }), contentProperties: args.contentProperties, designProperties: args.designProperties, ...(args.componentTree && { componentTree: args.componentTree }), diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.test.ts b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.test.ts index 414f686d..b6c5199c 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.test.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.test.ts @@ -78,6 +78,20 @@ describe('upsertExperienceTemplate', () => { expect(body.dataAssemblies).toEqual(dataAssemblies); }); + it('does not reintroduce viewports from a viewport-free experience template', async () => { + mockExperienceTemplateGet.mockResolvedValue({ + ...mockExperienceTemplate, + viewports: undefined, + }); + mockExperienceTemplateUpsert.mockResolvedValue(mockExperienceTemplate); + + await upsertExperienceTemplateTool(mockConfig)({ ...mockArgs, version: 1 }); + + expect(mockExperienceTemplateUpsert.mock.calls[0][1]).not.toHaveProperty( + 'viewports', + ); + }); + it('rejects a stale version', async () => { mockExperienceTemplateGet.mockResolvedValue(mockExperienceTemplate); // sys.version === 1 diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts index 8d148e14..791759db 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts @@ -101,7 +101,9 @@ export function upsertExperienceTemplateTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - viewports: args.viewports ?? current.viewports, + ...((args.viewports ?? current.viewports) !== undefined && { + viewports: args.viewports ?? current.viewports, + }), contentProperties: args.contentProperties ?? current.contentProperties, designProperties: args.designProperties ?? current.designProperties, ...((args.componentTree ?? current.componentTree) diff --git a/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts b/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts index a23052de..129e6a8e 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts @@ -39,6 +39,27 @@ describe('createExperience', () => { expect(result.content[0].text).toContain('Experience created successfully'); }); + it('creates a viewport-free experience with flattened design properties', async () => { + const { viewports: _, ...viewportFreeArgs } = createArgs; + mockExperienceCreate.mockResolvedValue(mockExperience); + + await createExperienceTool(mockConfig)({ + ...viewportFreeArgs, + designProperties: { + color: { type: 'ManualDesignValue', value: 'red' }, + }, + }); + + expect(mockExperienceCreate.mock.calls[0][1]).toMatchObject({ + designProperties: { + color: { type: 'ManualDesignValue', value: 'red' }, + }, + }); + expect(mockExperienceCreate.mock.calls[0][1]).not.toHaveProperty( + 'viewports', + ); + }); + it('rejects writes to a protected environment', async () => { const tool = createExperienceTool( createMockConfig({ protectedEnvironments: ['test-environment'] }), diff --git a/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts b/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts index 3b0013a3..545373c0 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts @@ -11,6 +11,7 @@ import { import { ViewportSchema, ExperienceMetadataSchema, + DesignPropertyValueSchema, DimensionedDesignPropertyValueSchema, ExperienceContentBindingsSchema, ExperienceSlotNodeSchema, @@ -26,9 +27,16 @@ export const CreateExperienceToolParams = BaseToolSchema.extend({ ), viewports: z .array(ViewportSchema) + .optional() .describe('Viewport definitions for the experience (may be empty)'), designProperties: z - .record(z.string(), DimensionedDesignPropertyValueSchema) + .record( + z.string(), + z.union([ + DesignPropertyValueSchema, + DimensionedDesignPropertyValueSchema, + ]), + ) .describe( 'Design property values keyed by property ID. Each value is a dimensioned map ' + '(viewport ID → design value). May be an empty object.', @@ -64,7 +72,7 @@ export function createExperienceTool(config: ContentfulConfig) { name: args.name, description: args.description, experienceTemplate: args.experienceTemplate, - viewports: args.viewports, + ...(args.viewports !== undefined && { viewports: args.viewports }), designProperties: args.designProperties, ...(args.contentBindings && { contentBindings: args.contentBindings }), ...(args.slots && { slots: args.slots }), diff --git a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.test.ts b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.test.ts index d4c73d4c..034e4c3f 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.test.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.test.ts @@ -14,7 +14,10 @@ describe('upsertExperience', () => { it('reads the current experience before updating (read-before-write)', async () => { mockExperienceGet.mockResolvedValue(mockExperience); - mockExperienceUpsert.mockResolvedValue({ ...mockExperience, name: 'Renamed' }); + mockExperienceUpsert.mockResolvedValue({ + ...mockExperience, + name: 'Renamed', + }); const tool = upsertExperienceTool(mockConfig); const result = await tool({ ...mockArgs, version: 1, name: 'Renamed' }); @@ -43,7 +46,9 @@ describe('upsertExperience', () => { it('preserves unspecified fields from the existing experience', async () => { mockExperienceGet.mockResolvedValue({ ...mockExperience, - designProperties: { color: { _: { type: 'ManualDesignValue', value: 'red' } } }, + designProperties: { + color: { _: { type: 'ManualDesignValue', value: 'red' } }, + }, }); mockExperienceUpsert.mockResolvedValue(mockExperience); @@ -56,6 +61,25 @@ describe('upsertExperience', () => { }); }); + it('preserves flattened design properties without reintroducing viewports', async () => { + mockExperienceGet.mockResolvedValue({ + ...mockExperience, + viewports: undefined, + designProperties: { + color: { type: 'ManualDesignValue', value: 'red' }, + }, + }); + mockExperienceUpsert.mockResolvedValue(mockExperience); + + await upsertExperienceTool(mockConfig)({ ...mockArgs, version: 1 }); + + const [, body] = mockExperienceUpsert.mock.calls[0]; + expect(body).not.toHaveProperty('viewports'); + expect(body.designProperties).toEqual({ + color: { type: 'ManualDesignValue', value: 'red' }, + }); + }); + it('rejects a stale version', async () => { mockExperienceGet.mockResolvedValue(mockExperience); // sys.version === 1 diff --git a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts index 231fbf61..3beb3c7a 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts @@ -11,6 +11,7 @@ import { import { ViewportSchema, ExperienceMetadataSchema, + DesignPropertyValueSchema, DimensionedDesignPropertyValueSchema, ExperienceContentBindingsSchema, ExperienceSlotNodeSchema, @@ -34,7 +35,13 @@ export const UpsertExperienceToolParams = BaseToolSchema.extend({ .optional() .describe('Viewport definitions; replaces existing viewports if provided'), designProperties: z - .record(z.string(), DimensionedDesignPropertyValueSchema) + .record( + z.string(), + z.union([ + DesignPropertyValueSchema, + DimensionedDesignPropertyValueSchema, + ]), + ) .optional() .describe( 'Design property values keyed by property ID; replaces existing if provided', @@ -90,7 +97,9 @@ export function upsertExperienceTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - viewports: args.viewports ?? current.viewports, + ...((args.viewports ?? current.viewports) !== undefined && { + viewports: args.viewports ?? current.viewports, + }), designProperties: args.designProperties ?? current.designProperties, ...((args.contentBindings ?? current.contentBindings) ? { contentBindings: args.contentBindings ?? current.contentBindings } From 5020d7db05f827316ccbacc82ddfc4c3af9b470a Mon Sep 17 00:00:00 2001 From: Thomas Kellermeier Date: Fri, 25 Sep 2026 16:30:09 +0200 Subject: [PATCH 2/3] fix(exo): type viewport-free tool payloads [SPA-5371] --- .../exo/components/createComponent.test.ts | 2 +- .../tools/exo/components/createComponent.ts | 16 +++-- .../tools/exo/components/upsertComponent.ts | 15 ++++- .../createExperienceFragment.test.ts | 2 +- .../createExperienceFragment.ts | 30 ++++++---- .../updateExperienceFragment.ts | 54 ++++++++++------- .../createExperienceTemplate.test.ts | 2 +- .../createExperienceTemplate.ts | 28 +++++---- .../upsertExperienceTemplate.ts | 60 +++++++++++-------- .../exo/experiences/createExperience.test.ts | 2 +- .../tools/exo/experiences/createExperience.ts | 32 ++++++---- .../tools/exo/experiences/upsertExperience.ts | 15 ++++- .../src/types/cmaViewportCompatibility.ts | 52 ++++++++++++++++ 13 files changed, 218 insertions(+), 92 deletions(-) create mode 100644 packages/mcp-tools/src/types/cmaViewportCompatibility.ts diff --git a/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts b/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts index 25175b0b..fc00b907 100644 --- a/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts +++ b/packages/mcp-tools/src/tools/exo/components/createComponent.test.ts @@ -90,7 +90,7 @@ describe('createComponent', () => { }); it('omits viewports when creating a viewport-free component', async () => { - const { viewports: _, ...viewportFreeArgs } = args; + const viewportFreeArgs = { ...args, viewports: undefined }; mockComponentCreate.mockResolvedValue(mockComponent); await createComponentTool(mockConfig)(viewportFreeArgs); diff --git a/packages/mcp-tools/src/tools/exo/components/createComponent.ts b/packages/mcp-tools/src/tools/exo/components/createComponent.ts index 9e3e7969..d553c5fb 100644 --- a/packages/mcp-tools/src/tools/exo/components/createComponent.ts +++ b/packages/mcp-tools/src/tools/exo/components/createComponent.ts @@ -16,6 +16,10 @@ import { TreeNodeSchema, ExoMetadataSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayload, + type ViewportOptionalPayload, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const CreateComponentToolParams = BaseToolSchema.extend({ @@ -70,7 +74,9 @@ export function createComponentTool(config: ContentfulConfig) { ...(args.componentTree && { componentTree: args.componentTree }), ...(args.slots && { slots: args.slots }), ...(args.metadata && { metadata: args.metadata }), - }; + } satisfies ViewportOptionalPayload< + Parameters[1] + >; // Create the component with or without an explicit ID. Providing an ID // uses upsert (PUT) with no sys.version, which the CMA treats as a create. @@ -81,14 +87,16 @@ export function createComponentTool(config: ContentfulConfig) { environmentId: args.environmentId, componentId: args.componentId, }, - { + asViewportOptionalCmaPayload< + Parameters[1] + >({ sys: { id: args.componentId, type: 'Component' }, ...componentData, - }, + }), ) : await contentfulClient.component.create( { spaceId: args.spaceId, environmentId: args.environmentId }, - componentData, + asViewportOptionalCmaPayload(componentData), ); return createSuccessResponse('Component created successfully', { diff --git a/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts b/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts index 52d4683d..b1042d88 100644 --- a/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts +++ b/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts @@ -16,6 +16,10 @@ import { TreeNodeSchema, ExoMetadataSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayload, + type ViewportOptionalPayload, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const UpsertComponentToolParams = BaseToolSchema.extend({ @@ -86,7 +90,7 @@ export function upsertComponentTool(config: ContentfulConfig) { ); } - const component = await contentfulClient.component.upsert(params, { + const componentData = { sys: { id: current.sys.id, type: 'Component', @@ -111,7 +115,14 @@ export function upsertComponentTool(config: ContentfulConfig) { ...(current.dataAssemblies ? { dataAssemblies: current.dataAssemblies } : {}), - }); + } satisfies ViewportOptionalPayload< + Parameters[1] + >; + + const component = await contentfulClient.component.upsert( + params, + asViewportOptionalCmaPayload(componentData), + ); return createSuccessResponse('Component updated successfully', { component, diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts index d722803f..95aa62e0 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.test.ts @@ -48,7 +48,7 @@ describe('createExperienceFragment', () => { }); it('creates a viewport-free experience fragment with flattened design properties', async () => { - const { viewports: _, ...viewportFreeArgs } = baseArgs; + const viewportFreeArgs = { ...baseArgs, viewports: undefined }; mockExperienceFragmentCreate.mockResolvedValue(mockExperienceFragment); await createExperienceFragmentTool(mockConfig)({ diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts index e778d740..e55e740a 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts @@ -17,6 +17,10 @@ import { ExperienceSlotNodeSchema, ComponentResourceLinkSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayloadWithFlattenedDesignProperties, + type ViewportOptionalPayloadWithFlattenedDesignProperties, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const CreateExperienceFragmentToolParams = BaseToolSchema.extend({ @@ -63,18 +67,24 @@ export function createExperienceFragmentTool(config: ContentfulConfig) { const contentfulClient = createExoToolClient(config, args); + const experienceFragmentData = { + name: args.name, + description: args.description, + component: args.component, + ...(args.viewports !== undefined && { viewports: args.viewports }), + designProperties: args.designProperties, + ...(args.contentBindings && { contentBindings: args.contentBindings }), + ...(args.slots && { slots: args.slots }), + ...(args.metadata && { metadata: args.metadata }), + } satisfies ViewportOptionalPayloadWithFlattenedDesignProperties< + Parameters[1] + >; + const experienceFragment = await contentfulClient.experienceFragment.create( { spaceId: args.spaceId, environmentId: args.environmentId }, - { - name: args.name, - description: args.description, - component: args.component, - ...(args.viewports !== undefined && { viewports: args.viewports }), - designProperties: args.designProperties, - ...(args.contentBindings && { contentBindings: args.contentBindings }), - ...(args.slots && { slots: args.slots }), - ...(args.metadata && { metadata: args.metadata }), - }, + asViewportOptionalCmaPayloadWithFlattenedDesignProperties( + experienceFragmentData, + ), ); return createSuccessResponse('Experience fragment created successfully', { diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts index 2a0184e6..f8524b9c 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts @@ -16,6 +16,10 @@ import { ExperienceContentBindingsSchema, ExperienceSlotNodeSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayloadWithFlattenedDesignProperties, + type ViewportOptionalPayloadWithFlattenedDesignProperties, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const UpdateExperienceFragmentToolParams = BaseToolSchema.extend({ @@ -94,30 +98,36 @@ export function updateExperienceFragmentTool(config: ContentfulConfig) { // call is upsert(): the read-before-write guard above means this only ever updates // an existing fragment, and `update` is the verb the tool surface exposes for that. // Do not "fix" the tool name to match the SDK method. + const experienceFragmentData = { + sys: { + id: current.sys.id, + type: 'ExperienceFragment', + version: current.sys.version, + }, + name: args.name ?? current.name, + description: args.description ?? current.description, + ...((args.viewports ?? current.viewports) !== undefined && { + viewports: args.viewports ?? current.viewports, + }), + designProperties: args.designProperties ?? current.designProperties, + ...((args.contentBindings ?? current.contentBindings) + ? { contentBindings: args.contentBindings ?? current.contentBindings } + : {}), + ...((args.slots ?? current.slots) + ? { slots: args.slots ?? current.slots } + : {}), + ...((args.metadata ?? current.metadata) + ? { metadata: args.metadata ?? current.metadata } + : {}), + } satisfies ViewportOptionalPayloadWithFlattenedDesignProperties< + Parameters[1] + >; + const experienceFragment = await contentfulClient.experienceFragment.upsert( params, - { - sys: { - id: current.sys.id, - type: 'ExperienceFragment', - version: current.sys.version, - }, - name: args.name ?? current.name, - description: args.description ?? current.description, - ...((args.viewports ?? current.viewports) !== undefined && { - viewports: args.viewports ?? current.viewports, - }), - designProperties: args.designProperties ?? current.designProperties, - ...((args.contentBindings ?? current.contentBindings) - ? { contentBindings: args.contentBindings ?? current.contentBindings } - : {}), - ...((args.slots ?? current.slots) - ? { slots: args.slots ?? current.slots } - : {}), - ...((args.metadata ?? current.metadata) - ? { metadata: args.metadata ?? current.metadata } - : {}), - }, + asViewportOptionalCmaPayloadWithFlattenedDesignProperties( + experienceFragmentData, + ), ); return createSuccessResponse('Experience fragment updated successfully', { diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts index 400dbf12..f306b631 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.test.ts @@ -36,7 +36,7 @@ describe('createExperienceTemplate', () => { }); it('omits viewports when creating a viewport-free experience template', async () => { - const { viewports: _, ...viewportFreeArgs } = createArgs; + const viewportFreeArgs = { ...createArgs, viewports: undefined }; mockExperienceTemplateCreate.mockResolvedValue(mockExperienceTemplate); await createExperienceTemplateTool(mockConfig)(viewportFreeArgs); diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts index a628c2e8..65cf8764 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts @@ -16,6 +16,10 @@ import { TreeNodeSchema, ExoMetadataSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayload, + type ViewportOptionalPayload, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const CreateExperienceTemplateToolParams = BaseToolSchema.extend({ @@ -57,18 +61,22 @@ export function createExperienceTemplateTool(config: ContentfulConfig) { const contentfulClient = createExoToolClient(config, args); + const experienceTemplateData = { + name: args.name, + description: args.description, + ...(args.viewports !== undefined && { viewports: args.viewports }), + contentProperties: args.contentProperties, + designProperties: args.designProperties, + ...(args.componentTree && { componentTree: args.componentTree }), + ...(args.slots && { slots: args.slots }), + ...(args.metadata && { metadata: args.metadata }), + } satisfies ViewportOptionalPayload< + Parameters[1] + >; + const experienceTemplate = await contentfulClient.experienceTemplate.create( { spaceId: args.spaceId, environmentId: args.environmentId }, - { - name: args.name, - description: args.description, - ...(args.viewports !== undefined && { viewports: args.viewports }), - contentProperties: args.contentProperties, - designProperties: args.designProperties, - ...(args.componentTree && { componentTree: args.componentTree }), - ...(args.slots && { slots: args.slots }), - ...(args.metadata && { metadata: args.metadata }), - }, + asViewportOptionalCmaPayload(experienceTemplateData), ); return createSuccessResponse('Experience template created successfully', { diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts index 791759db..91bed202 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts @@ -16,6 +16,10 @@ import { TreeNodeSchema, ExoMetadataSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayload, + type ViewportOptionalPayload, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const UpsertExperienceTemplateToolParams = BaseToolSchema.extend({ @@ -91,34 +95,38 @@ export function upsertExperienceTemplateTool(config: ContentfulConfig) { ); } + const experienceTemplateData = { + sys: { + id: current.sys.id, + type: 'ExperienceTemplate', + version: current.sys.version, + }, + name: args.name ?? current.name, + description: args.description ?? current.description, + ...((args.viewports ?? current.viewports) !== undefined && { + viewports: args.viewports ?? current.viewports, + }), + contentProperties: args.contentProperties ?? current.contentProperties, + designProperties: args.designProperties ?? current.designProperties, + ...((args.componentTree ?? current.componentTree) + ? { componentTree: args.componentTree ?? current.componentTree } + : {}), + ...((args.slots ?? current.slots) + ? { slots: args.slots ?? current.slots } + : {}), + ...((args.metadata ?? current.metadata) + ? { metadata: args.metadata ?? current.metadata } + : {}), + ...(current.dataAssemblies + ? { dataAssemblies: current.dataAssemblies } + : {}), + } satisfies ViewportOptionalPayload< + Parameters[1] + >; + const experienceTemplate = await contentfulClient.experienceTemplate.upsert( params, - { - sys: { - id: current.sys.id, - type: 'ExperienceTemplate', - version: current.sys.version, - }, - name: args.name ?? current.name, - description: args.description ?? current.description, - ...((args.viewports ?? current.viewports) !== undefined && { - viewports: args.viewports ?? current.viewports, - }), - contentProperties: args.contentProperties ?? current.contentProperties, - designProperties: args.designProperties ?? current.designProperties, - ...((args.componentTree ?? current.componentTree) - ? { componentTree: args.componentTree ?? current.componentTree } - : {}), - ...((args.slots ?? current.slots) - ? { slots: args.slots ?? current.slots } - : {}), - ...((args.metadata ?? current.metadata) - ? { metadata: args.metadata ?? current.metadata } - : {}), - ...(current.dataAssemblies - ? { dataAssemblies: current.dataAssemblies } - : {}), - }, + asViewportOptionalCmaPayload(experienceTemplateData), ); return createSuccessResponse('Experience template updated successfully', { diff --git a/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts b/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts index 129e6a8e..3f0e14c8 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/createExperience.test.ts @@ -40,7 +40,7 @@ describe('createExperience', () => { }); it('creates a viewport-free experience with flattened design properties', async () => { - const { viewports: _, ...viewportFreeArgs } = createArgs; + const viewportFreeArgs = { ...createArgs, viewports: undefined }; mockExperienceCreate.mockResolvedValue(mockExperience); await createExperienceTool(mockConfig)({ diff --git a/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts b/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts index 545373c0..546ec323 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts @@ -17,6 +17,10 @@ import { ExperienceSlotNodeSchema, ExperienceTemplateResourceLinkSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayloadWithFlattenedDesignProperties, + type ViewportOptionalPayloadWithFlattenedDesignProperties, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const CreateExperienceToolParams = BaseToolSchema.extend({ @@ -38,8 +42,8 @@ export const CreateExperienceToolParams = BaseToolSchema.extend({ ]), ) .describe( - 'Design property values keyed by property ID. Each value is a dimensioned map ' + - '(viewport ID → design value). May be an empty object.', + 'Design property values keyed by property ID. When viewports are provided, each value is a dimensioned map ' + + '(viewport ID → design value); without viewports, use a direct design value. May be an empty object.', ), contentBindings: ExperienceContentBindingsSchema.optional().describe( 'Optional content bindings linking this experience to a data assembly', @@ -66,18 +70,22 @@ export function createExperienceTool(config: ContentfulConfig) { const contentfulClient = createExoToolClient(config, args); + const experienceData = { + name: args.name, + description: args.description, + experienceTemplate: args.experienceTemplate, + ...(args.viewports !== undefined && { viewports: args.viewports }), + designProperties: args.designProperties, + ...(args.contentBindings && { contentBindings: args.contentBindings }), + ...(args.slots && { slots: args.slots }), + ...(args.metadata && { metadata: args.metadata }), + } satisfies ViewportOptionalPayloadWithFlattenedDesignProperties< + Parameters[1] + >; + const experience = await contentfulClient.experience.create( { spaceId: args.spaceId, environmentId: args.environmentId }, - { - name: args.name, - description: args.description, - experienceTemplate: args.experienceTemplate, - ...(args.viewports !== undefined && { viewports: args.viewports }), - designProperties: args.designProperties, - ...(args.contentBindings && { contentBindings: args.contentBindings }), - ...(args.slots && { slots: args.slots }), - ...(args.metadata && { metadata: args.metadata }), - }, + asViewportOptionalCmaPayloadWithFlattenedDesignProperties(experienceData), ); return createSuccessResponse('Experience created successfully', { diff --git a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts index 3beb3c7a..8d1abbe9 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts @@ -16,6 +16,10 @@ import { ExperienceContentBindingsSchema, ExperienceSlotNodeSchema, } from '../../../types/exoSchemas.js'; +import { + asViewportOptionalCmaPayloadWithFlattenedDesignProperties, + type ViewportOptionalPayloadWithFlattenedDesignProperties, +} from '../../../types/cmaViewportCompatibility.js'; import type { ContentfulConfig } from '../../../config/types.js'; export const UpsertExperienceToolParams = BaseToolSchema.extend({ @@ -89,7 +93,7 @@ export function upsertExperienceTool(config: ContentfulConfig) { ); } - const experience = await contentfulClient.experience.upsert(params, { + const experienceData = { sys: { id: current.sys.id, type: 'Experience', @@ -110,7 +114,14 @@ export function upsertExperienceTool(config: ContentfulConfig) { ...((args.metadata ?? current.metadata) ? { metadata: args.metadata ?? current.metadata } : {}), - }); + } satisfies ViewportOptionalPayloadWithFlattenedDesignProperties< + Parameters[1] + >; + + const experience = await contentfulClient.experience.upsert( + params, + asViewportOptionalCmaPayloadWithFlattenedDesignProperties(experienceData), + ); return createSuccessResponse('Experience updated successfully', { experience, diff --git a/packages/mcp-tools/src/types/cmaViewportCompatibility.ts b/packages/mcp-tools/src/types/cmaViewportCompatibility.ts new file mode 100644 index 00000000..4032fcd6 --- /dev/null +++ b/packages/mcp-tools/src/types/cmaViewportCompatibility.ts @@ -0,0 +1,52 @@ +import type { ComponentTypeViewport } from 'contentful-management'; +import type { z } from 'zod'; +import type { + DesignPropertyValueSchema, + DimensionedDesignPropertyValueSchema, +} from './exoSchemas.js'; + +type DesignPropertyValue = z.infer; +type DimensionedDesignPropertyValue = z.infer< + typeof DimensionedDesignPropertyValueSchema +>; + +/** + * The CMA supports viewport-free ExO entities, but the currently consumed CMA + * SDK version still marks `viewports` as required. Keep the exception local to + * the SDK call boundary while callers validate the rest of the payload with + * `satisfies`. + */ +export type ViewportOptionalPayload = Omit & { + viewports?: ComponentTypeViewport[]; +}; + +/** + * Viewport-free Experiences and Experience Fragments use flattened design + * property values. The currently consumed CMA SDK only models their legacy, + * dimensioned shape. + */ +export type ViewportOptionalPayloadWithFlattenedDesignProperties = + Omit & { + viewports?: ComponentTypeViewport[]; + designProperties: Record< + string, + DesignPropertyValue | DimensionedDesignPropertyValue + >; + }; + +export function asViewportOptionalCmaPayload< + Payload extends { viewports?: ComponentTypeViewport[] }, +>(payload: ViewportOptionalPayload): Payload { + return payload as Payload; +} + +export function asViewportOptionalCmaPayloadWithFlattenedDesignProperties< + Payload extends { + viewports?: ComponentTypeViewport[]; + designProperties: Record; + }, +>( + payload: ViewportOptionalPayloadWithFlattenedDesignProperties, +): Payload { + return payload as Payload; +} From 7e31dd1f01e3d327d9d37757c551b9e963795185 Mon Sep 17 00:00:00 2001 From: Thomas Kellermeier Date: Fri, 25 Sep 2026 16:37:20 +0200 Subject: [PATCH 3/3] fix(exo): clarify viewport omission [SPA-5371] --- .../mcp-tools/src/tools/exo/components/createComponent.ts | 4 +++- .../mcp-tools/src/tools/exo/components/upsertComponent.ts | 6 +++--- .../exo/experience-fragments/createExperienceFragment.ts | 4 +++- .../exo/experience-fragments/updateExperienceFragment.ts | 6 +++--- .../exo/experience-templates/createExperienceTemplate.ts | 2 +- .../exo/experience-templates/upsertExperienceTemplate.ts | 6 +++--- .../mcp-tools/src/tools/exo/experiences/createExperience.ts | 4 +++- .../mcp-tools/src/tools/exo/experiences/upsertExperience.ts | 6 +++--- 8 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/mcp-tools/src/tools/exo/components/createComponent.ts b/packages/mcp-tools/src/tools/exo/components/createComponent.ts index d553c5fb..3aee6475 100644 --- a/packages/mcp-tools/src/tools/exo/components/createComponent.ts +++ b/packages/mcp-tools/src/tools/exo/components/createComponent.ts @@ -34,7 +34,9 @@ export const CreateComponentToolParams = BaseToolSchema.extend({ viewports: z .array(ViewportSchema) .optional() - .describe('Viewport definitions for the component (may be empty)'), + .describe( + 'Optional viewport definitions for the component. Omit for viewport-free components.', + ), contentProperties: z .array(ContentPropertySchema) .describe('Content property definitions (may be empty)'), diff --git a/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts b/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts index b1042d88..a828dcef 100644 --- a/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts +++ b/packages/mcp-tools/src/tools/exo/components/upsertComponent.ts @@ -90,6 +90,8 @@ export function upsertComponentTool(config: ContentfulConfig) { ); } + const viewports = args.viewports ?? current.viewports; + const componentData = { sys: { id: current.sys.id, @@ -98,9 +100,7 @@ export function upsertComponentTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - ...((args.viewports ?? current.viewports) !== undefined && { - viewports: args.viewports ?? current.viewports, - }), + ...(viewports !== undefined && { viewports }), contentProperties: args.contentProperties ?? current.contentProperties, designProperties: args.designProperties ?? current.designProperties, ...((args.componentTree ?? current.componentTree) diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts index e55e740a..00496a0a 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/createExperienceFragment.ts @@ -32,7 +32,9 @@ export const CreateExperienceFragmentToolParams = BaseToolSchema.extend({ viewports: z .array(ViewportSchema) .optional() - .describe('Viewport definitions (may be empty)'), + .describe( + 'Optional viewport definitions for the experience fragment. Omit for viewport-free experience fragments.', + ), designProperties: z .record( z.string(), diff --git a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts index f8524b9c..1df851a9 100644 --- a/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts +++ b/packages/mcp-tools/src/tools/exo/experience-fragments/updateExperienceFragment.ts @@ -98,6 +98,8 @@ export function updateExperienceFragmentTool(config: ContentfulConfig) { // call is upsert(): the read-before-write guard above means this only ever updates // an existing fragment, and `update` is the verb the tool surface exposes for that. // Do not "fix" the tool name to match the SDK method. + const viewports = args.viewports ?? current.viewports; + const experienceFragmentData = { sys: { id: current.sys.id, @@ -106,9 +108,7 @@ export function updateExperienceFragmentTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - ...((args.viewports ?? current.viewports) !== undefined && { - viewports: args.viewports ?? current.viewports, - }), + ...(viewports !== undefined && { viewports }), designProperties: args.designProperties ?? current.designProperties, ...((args.contentBindings ?? current.contentBindings) ? { contentBindings: args.contentBindings ?? current.contentBindings } diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts index 65cf8764..5af867d1 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/createExperienceTemplate.ts @@ -29,7 +29,7 @@ export const CreateExperienceTemplateToolParams = BaseToolSchema.extend({ .array(ViewportSchema) .optional() .describe( - 'Viewport definitions for the experience template (may be empty)', + 'Optional viewport definitions for the experience template. Omit for viewport-free experience templates.', ), contentProperties: z .array(ContentPropertySchema) diff --git a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts index 91bed202..ee9309aa 100644 --- a/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts +++ b/packages/mcp-tools/src/tools/exo/experience-templates/upsertExperienceTemplate.ts @@ -95,6 +95,8 @@ export function upsertExperienceTemplateTool(config: ContentfulConfig) { ); } + const viewports = args.viewports ?? current.viewports; + const experienceTemplateData = { sys: { id: current.sys.id, @@ -103,9 +105,7 @@ export function upsertExperienceTemplateTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - ...((args.viewports ?? current.viewports) !== undefined && { - viewports: args.viewports ?? current.viewports, - }), + ...(viewports !== undefined && { viewports }), contentProperties: args.contentProperties ?? current.contentProperties, designProperties: args.designProperties ?? current.designProperties, ...((args.componentTree ?? current.componentTree) diff --git a/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts b/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts index 546ec323..d103b692 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/createExperience.ts @@ -32,7 +32,9 @@ export const CreateExperienceToolParams = BaseToolSchema.extend({ viewports: z .array(ViewportSchema) .optional() - .describe('Viewport definitions for the experience (may be empty)'), + .describe( + 'Optional viewport definitions for the experience. Omit for viewport-free experiences.', + ), designProperties: z .record( z.string(), diff --git a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts index 8d1abbe9..6dd47c5c 100644 --- a/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts +++ b/packages/mcp-tools/src/tools/exo/experiences/upsertExperience.ts @@ -93,6 +93,8 @@ export function upsertExperienceTool(config: ContentfulConfig) { ); } + const viewports = args.viewports ?? current.viewports; + const experienceData = { sys: { id: current.sys.id, @@ -101,9 +103,7 @@ export function upsertExperienceTool(config: ContentfulConfig) { }, name: args.name ?? current.name, description: args.description ?? current.description, - ...((args.viewports ?? current.viewports) !== undefined && { - viewports: args.viewports ?? current.viewports, - }), + ...(viewports !== undefined && { viewports }), designProperties: args.designProperties ?? current.designProperties, ...((args.contentBindings ?? current.contentBindings) ? { contentBindings: args.contentBindings ?? current.contentBindings }