Skip to content

refactor: make searching guidance entity-agnostic for ExO envs - #432

Merged
Tyler Pina (tylerpina) merged 1 commit into
feat/exofrom
chore/entity-agnostic-search-guidance
Jul 8, 2026
Merged

refactor: make searching guidance entity-agnostic for ExO envs#432
Tyler Pina (tylerpina) merged 1 commit into
feat/exofrom
chore/entity-agnostic-search-guidance

Conversation

@tylerpina

Copy link
Copy Markdown
Contributor

Summary

  • Reframe SEARCHING_GUIDANCE in the shared MCP context corpus from classic-specific wording (list_content_types / get_content_type / search_entries, "referenced entry (the author)") to schema- and entity-agnostic phrasing.
  • The section now explicitly names both primitive families — classic content types & entries and Experience Orchestration component types & experiences — so the guidance doesn't mislead an agent operating in an ExO-only environment.
  • Mirrors the same change made on the remote MCP server (contentful/remote-mcp-server#87), keeping the two servers' searching guidance aligned.

Why

get_initial_context returns this corpus verbatim on every session. In an ExO environment the classic-only framing ("call search_entries", "content-type first") points the agent at the wrong primitives. Making it entity-agnostic keeps the guidance correct regardless of whether the environment uses classic content modeling or ExO.

Test plan

  • packages/mcp-tools instructions.test.ts passes (3/3) — corpus guard still green; the fiction/load-bearing-fact assertions live in CORE_INVARIANTS, unaffected by the searching-verb changes
  • Reviewer sanity-check the reworded section reads clearly for both classic and ExO users

Note: based off feat/exo (where the split instruction constants live — main still has the monolithic MCP_INSTRUCTIONS), so this targets feat/exo rather than main.

Generated with Claude Code

@tylerpina
Tyler Pina (tylerpina) requested a review from a team as a code owner July 8, 2026 16:38
@bito-code-review

bito-code-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bito Automatic Review Skipped - Branch Excluded

Bito didn't auto-review because the source or target branch is excluded from automatic reviews.
No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.
You can change the branch exclusion settings here, or contact your Bito workspace admin at michael.pearce@contentful.com.

@tylerpina
Tyler Pina (tylerpina) merged commit 497ae54 into feat/exo Jul 8, 2026
9 checks passed
@tylerpina
Tyler Pina (tylerpina) deleted the chore/entity-agnostic-search-guidance branch July 8, 2026 16:39
Tyler Pina (tylerpina) added a commit that referenced this pull request Jul 28, 2026
* feat: add ComponentType CRUD MCP tools [AIS-73] (#419)

* chore(exo): pin contentful-management to 12.6.0-dev.3 for ExO entities [AIS-73]

* feat(exo): support componentType in delete confirmation tokens [AIS-73]

* test(exo): add shared componentType mock client [AIS-73]

* feat(exo): add get_component_type tool [AIS-73]

* feat(exo): add list_component_types tool [AIS-73]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(exo): add create_component_type tool [AIS-73]

* feat(exo): add upsert_component_type tool with read-before-write [AIS-73]

* feat(exo): add publish/unpublish_component_type tools with read-before-write [AIS-73]

* feat(exo): add delete_component_type tool with two-phase confirmation [AIS-73]

* feat(exo): assemble componentType tool collection [AIS-73]

* feat(exo): expose getComponentTypeTools on ContentfulMcpTools [AIS-73]

* feat(exo): register componentType tools in the MCP server [AIS-73]

* style(exo): apply prettier formatting to componentType tools [AIS-73]

* fix(exo): preserve dataAssemblies on componentType upsert + add pagePrev test [AIS-73]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(exo): enforce read-before-write via required version param on componentType upsert/publish/unpublish [AIS-73]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: gitignore superpowers planning scaffolding [AIS-73]

* feat: schema imported from cma

* chore: forgot to add

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(exo): implement Fragment CRUD MCP tools [AIS-76] (#420)

* feat(exo): implement Fragment CRUD MCP tools [AIS-76]

Add get, list, create, update, delete, publish, and unpublish tools for
ExO Fragment entities, following the component-type patterns established
in AIS-73. Includes two-phase delete confirmation, read-before-write
version enforcement on all mutating ops, and full test coverage (50 tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: nx

* fix(test): add fragment tools to register.test.ts expected count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add Template CRUD MCP tools [AIS-77] (#421)

Implements 7 MCP tools for the ExO Template entity (get, list, create,
upsert, delete, publish, unpublish) following the established ComponentType
pattern. Includes read-before-write for upsert/publish/unpublish, two-phase
confirmation for delete, and full test coverage.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(exo): implement Experience CRUD MCP tools [AIS-74]

Adds 7 MCP tools for ExO Experience management (get, list, create, upsert,
delete, publish, unpublish), following the same patterns established by
ComponentType, Fragment, and Template tools. All write tools enforce
read-before-write with version conflict rejection; delete uses the
two-phase confirm/confirmToken flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(exo): implement DataAssembly CRUD MCP tools [AIS-75] (#422)

* feat(exo): implement DataAssembly CRUD MCP tools [AIS-75]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(exo): rename sys.id[in] schema key to sysIdIn in data assembly list tools

Anthropic API rejects tool schemas with property keys containing brackets —
the pattern ^[a-zA-Z0-9_.-]{1,64}$ disallows [ and ]. Rename the input schema
key to sysIdIn while preserving the API query mapping to sys.id[in].

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(exo): use z.record for DataAssembly return field to prevent string serialization

The return field was typed as z.unknown() which caused the MCP client to
serialize object values as JSON strings, resulting in API validation errors.
Changed to z.record(z.unknown()) to match parameters/resolvers and ensure
correct object serialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(exo): update listDataAssemblies test to use sysIdIn param name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(exo): add getDataAssemblyTools to register.test.ts standard collections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(exo): pin ExO tool schemas to canonical CMA types (#425)

The Zod schemas for the 5 ExO entity groups (component-types, templates,
experiences, fragments, data-assemblies) were hand-written and had drifted
from contentful-management's actual types: componentTree nodes used a
bogus "View" node instead of CMA's "Fragment" arm, componentType was a bare
string ID instead of a ResourceLink, design properties were a flat shape
instead of CMA's discriminated union, and several create/upsert call sites
papered over the mismatch with `as Parameters<...>[1]` casts.

contentful-management doesn't re-export the individual ExO Props types by
name from its package root, so each schema type is derived structurally
off PlainClientAPI's method signatures instead. Derived types are wrapped
in a Distribute<T> helper (a distributive mapped type) to keep them
nominal for tsup's declaration-emit step — combining two indexed-access
types derived from the same CMA entity in one z.object()/extend() call
otherwise fails DTS build with TS2742.

Data assemblies previously had no real validation (parameters/resolvers/
return were all z.unknown()); new dataAssemblySchemas.ts models CMA's
PointerExpressionValue and resolver union (GraphQL | nested data assembly)
precisely.

Schemas stay zod v3-compatible — the two spots that would have used
z.templateLiteral() (a v4-only API) either had no real usage or are
replaced with z.custom() plus a regex predicate, so no zod version bump
is needed for this change.

Removes every `as Parameters<...>[1]` cast in the touched create/upsert
tool files; the payloads now typecheck against CMA's real method
signatures without one.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(exo): use type-literal discriminant for DataAssembly type aliases

CanonicalDataAssemblyDataTypeField and LegacyDataAssemblyDataTypeField
previously used optional-field filters ({ source?, ref? }) in their
Extract/Exclude calls. Because absence satisfies an optional property in
TS structural typing, both aliases collapsed to the full union / never.

Fix: discriminate by the narrow type literals from DataTypeDefinition
(String, Number, Integer, etc.) instead — the canonical arm has those
literals, the legacy arm has a wide `type: string`, so the partition
is unambiguous.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: deliver corrected MCP context via get_initial_context (#423)

* refactor: split MCP guidance into corrected tiered corpus

* feat: add get_guidance tool for on-demand context

* refactor: return tiered index from get_initial_context

* test: anchor get_initial_context test to legacy-envelope removal

* refactor: fold get_guidance sections into get_initial_context

Collapse the two-tool tiered design into a single get_initial_context
call. Searching and conventions guidance are now returned inline;
editing guidance is dropped since its one load-bearing rule
(get_entry -> sys.version -> update_entry) already lives in
CORE_INVARIANTS. Removes the now-unused get_guidance tool and its tests.

* feat: include today's date as a session fact in get_initial_context

Restores the current date that the legacy MCP_INSTRUCTIONS envelope
exposed via <todaysDate>. Dropping it in the corpus rewrite was an
unintended regression — content work (scheduling, "recent", publish
timing) needs the model to know the current date. Emitted as a plain
session fact (weekday, month, day, year) rather than the old tag format.

* fix: add URN examples to ExO resource link schema descriptions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: gate ExO tool registration behind ENABLE_EXO_TOOLS opt-in (#428)

* feat: add EXO_DISPOSITION instruction nudge to mcp-tools

* feat: add exoToolsRegistered flag to ContentfulConfig

* feat: add detectExoDisposition shared classification helper

* feat: append EXO_DISPOSITION to get_initial_context when ExO tools registered

* feat: gate ExO tool registration on space detection (local server)

Makes registerAllTools async and probes the configured space's ExO
disposition once via detectExoDisposition before registering the
component-type, data-assembly, experience, template, and fragment
tool collections. Fails closed (classic-only) on a classic space or a
probe error, and skips the probe entirely when no SPACE_ID is set.
Sets exoToolsRegistered on the tools config so getInitialContextTool
can surface the EXO_DISPOSITION nudge.

* fix: log ExO detection failures instead of silently swallowing

* feat: replace ExO space-detection with ENABLE_EXO_TOOLS opt-in

Registering ExO tool collections behind a runtime space probe was too
heavy for the local server: it made registerAllTools async, added CMA
calls at startup, and inferred user intent from Component Type presence.

Replace it with an explicit opt-in env var. ENABLE_EXO_TOOLS=true
registers the five ExO collections and appends the (reworded) ExO nudge
to get_initial_context; anything else registers classic-only with no
nudge and byte-for-byte-identical context output.

- add ENABLE_EXO_TOOLS to env schema (transforms to boolean; only 'true')
- registerAllTools is synchronous again; drop the detection probe
- delete detectExoDisposition helper + test and its mcp-tools exports
  (added only for deferred remote work; nothing consumes them now)
- reword EXO_DISPOSITION to drop detection-derived 'empty or already
  uses ExO' framing
- rewrite register.test.ts gating cases as flag-on / flag-off

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: export shared instruction constants from mcp-tools

Re-export CORE_INVARIANTS, SEARCHING_GUIDANCE, CONVENTIONS_GUIDANCE, and
EXO_DISPOSITION from the package entrypoint so downstream consumers (the
remote MCP server) can import the shared instruction corpus. Completes the
export step the ExO local plan deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: document ENABLE_EXO_TOOLS env var in mcp-server README

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: make searching guidance entity-agnostic for ExO envs (#432)

* refactor(mcp-tools): make core invariants entity-agnostic, strengthen ExO disposition (#434)

* refactor(mcp-tools): make core invariants entity-agnostic, strengthen ExO disposition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(mcp-tools): drop tool-name assertions from instructions.test after entity-agnostic rewrite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(exo): rebase ExO type aliases onto dev.4 named Props exports (#435)

contentful-management 12.6.0-dev.4 now re-exports the top-level ExO
entity Props types (ComponentTypeProps, ExperienceProps, ...) from the
package root. Derive the schema type aliases by indexing directly into
those named exports instead of reaching through PlainClientAPI
method-return signatures.

The nested constituent types (ComponentTypeViewport, TreeNode, ...) are
still not root-exported, so the Distribute<T> wrapper stays to keep
declaration emit portable (TS2742 otherwise surfaces in the tool
consumers, not the schema file itself). Comment updated to reflect this.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat: require exoM1 entitlement to register ExO tools [AIS-191] (#437)

ExO tool collections now require BOTH gates: the ENABLE_EXO_TOOLS opt-in
env var AND the org's exoM1 entitlement. Adds a hasExoM1Entitlement helper
to @contentful/mcp-tools that reads the public CMA
/organizations/{orgId}/organization_entitlement_set endpoint and fails
closed (offline/error/no-org-id → no ExO). registerAllTools is now async;
the entitlement call is skipped entirely when the env var is off.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore: trim verbose PR comments to concise one-liners

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: bump contentful-management to 12.10.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove as-unknown cast in listComponentTypes query params

CursorPaginationParams is a discriminated union with mutually-exclusive
pageNext/pagePrev arms. Build the cursor object via a ternary so TypeScript
picks the correct union member without a cast.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: import ExoMetadataProps and ExperienceMetadataProps directly from CMA

Both types are exported from common-types.d.ts which is fully re-exported
via the CMA barrel, so they don't need Distribute<T> wrappers. Entity-file
types (ComponentTypeViewport, TreeNode, etc.) still require Distribute<T>
as they're not reachable from the package root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: import ExO nested types directly from cma.js 12.11.0

Bump contentful-management to 12.11.0, which now barrel-exports the
nested ExO entity types. Replace the local Distribute<...> indexed-access
derivations with direct imports:

- component-type: ComponentTypeViewport, ComponentTypeContentProperty,
  ComponentTypeDesignProperty, ComponentTypeSlotDefinition, TreeNode,
  ComponentNode, FragmentNode, SlotNode
- experience: ExperienceContentBindings, InlineFragmentNode

ExperienceSlotNode is still not barrel-exported, so it remains derived
from ExperienceProps via Distribute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant