
TLDR; Adobe’s CJA MCP is powerful and knows your full component configuration; schema paths, usage patterns, attribution settings, and more. What it cannot know is the organizational context your team carries: what a metric really means to the business, how it should be combined, what the caveats are, and who is accountable for it.. This post walks through building a semantic sidecar MCP on AppBuilder Runtime that adds business context, implementation notes, and governance rules. Tested against the LUMA e-commerce demo org with four real use cases showing the before and after. The short version: system prompt × context × governance makes CJA MCP actually trustworthy.
Adobe just launched something genuinely useful: a native MCP server for Customer Journey Analytics. Point Claude Desktop at it, ask “show me orders by marketing channel this month” and it works. No API wrappers, no custom scripts, no middleware. Just natural language talking directly to your CJA data.
I spent time testing it against the LUMA demo store, a fictional e-commerce organization Adobe uses for training and certification. The reporting side impressed me. The component discovery side exposed a problem I already knew existed but hadn’t fully confronted.
⚠️ Privacy notice: Before connecting CJA to any external LLM, review your LLM provider’s data handling policies and check with your legal team. Adobe recommends the same.
The problem: MCP is a mirror
Ask the CJA MCP “what is the Orders metric?” and it looks up the component configuration in your data view. Component ID, schema path, data type, persistence settings, and the description field.
That description field is where things fall apart.
At most organizations I’ve spoken to, the description field is either empty or contains whatever an analyst or developer typed during implementation two years ago. Something like “order count” or, optimistically, nothing at all. The CJA MCP faithfully returns this emptiness. It has no way to know that this metric includes cancelled and refunded orders, that it overstates actual business performance by 8 to 12 percent, or that finance always uses the Net Orders calculated metric instead. Using the wrong one in an executive dashboard would be an embarrassing mistake.
MCP is a mirror. It reflects exactly what you put into your metadata layer. If that layer is thin, MCP gives thin answers. This isn’t a criticism of Adobe’s implementation. It’s correct by design. The question is what you do about it.
The idea: A semantic sidecar
The Model Context Protocol supports running multiple MCP servers simultaneously. Claude sees all available tools and decides which ones to call based on the question. This is the key insight.
If the native CJA MCP handles the technical layer (component IDs, schema paths, data view structure, report execution) then a custom MCP server can handle the semantic layer: business context, implementation notes, industry-specific meaning, known caveats. Claude synthesizes both. You get answers that combine technical accuracy with organizational knowledge.
The custom MCP tool is simple. One endpoint, one job:
get_component_context(component_id, dataview_id)
→ returns enriched metadata from your semantic definitions file
Claude calls the Adobe MCP for the technical facts. It calls your custom MCP for the context that lives in nobody’s head except your team’s. The combined answer is qualitatively different from either source alone.
The first idea that didn’t work
My initial instinct was different: upload a semantic dataset directly into AEP. The logic made sense on paper. AEP is your governed data layer. Everything else already lives there: events, profiles, lookups. If you could store component definitions as a dataset in AEP, services like CJA could theoretically consume from it. One source of truth, native to the ecosystem, no additional infrastructure.
The problem is where CJA components actually live. Dimensions and metrics are not data. They are configuration. They exist in the Data View configuration layer, which sits on top of your AEP Connection and datasets. There is no mechanism to link an AEP lookup dataset to a Data View component. You cannot JOIN on a component ID the way you would join on a product ID or a campaign code, because component IDs are metadata, not fields in your event stream.

AEP lookup datasets work by enriching records at query time, matching on a field that exists in your event or profile data. Component IDs never appear in your event data. They are an abstraction layer above it. So the join has no left side to attach to.
The AEP dataset idea is architecturally clean but technically a dead end for this use case. Components live in the configuration layer, and that layer has no native extensibility for semantic enrichment. Which is what led to the custom MCP server approach.
Where it runs: AppBuilder Runtime
The custom MCP server needs to be hosted somewhere accessible, not just running on your laptop. For anyone on the Adobe stack, the answer is already in your license: App Builder Runtime.
Adobe published an official template for this exact use case: generator-app-remote-mcp-server-generic. You scaffold a working MCP server skeleton with one command:
aio app init my-cja-semantic-mcp
# select generator-app-remote-mcp-server-generic
aio app deploy
You get a public HTTPS endpoint at https://<namespace>.adobeioruntime.net/api/v1/web/semantic-mcp-server/mcp-server. Adobe IMS auth is handled by the Runtime layer. Your colleagues authenticate with their existing Adobe ID.
The semantic definitions JSON lives in AppBuilder Cloud File Storage, a blob store that is part of your AppBuilder pack and separate from your action code. This matters because you can update definitions without redeploying. Upload a new JSON file and changes are live on the next tool call.

The cost math is almost embarrassingly favorable. A semantic lookup action uses roughly 0.05 GB-seconds per invocation at 256MB memory and approximately 200ms execution time. A standard AppBuilder pack includes 6 million GB-seconds per year. For internal tooling used by a handful of analysts, you will consume a fraction of a percent of your quota. This runs on infrastructure you already own.
Claude Desktop Configuration
Once deployed, connecting Claude Desktop is two lines of config:
{
"mcpServers": {
"cja": {
"url": "https://mcp-gateway.adobe.io/cja/mcp",
"type": "streamable-http"
},
"cja-semantic-layer": {
"url": "https://<namespace>.adobeioruntime.net/api/v1/web/semantic-mcp-server/mcp-server",
"type": "streamable-http"
}
}
}
Both MCPs run alongside each other. Claude decides which tools to call. The analyst asks a question in natural language and gets an answer that combines Adobe’s technical metadata with your organization’s semantic layer.
What goes in the semantic layer
A flat description field does not solve anything. It is the same problem you have in CJA today. You need structured context split by audience.
After thinking through what actually makes an analyst’s question answerable, I landed on three context layers plus supporting metadata.
- Business context is what this component means to a business user. Written for someone who knows the KPIs but not the implementation. “Total number of orders placed including all order statuses. Use Net Orders for any revenue performance reporting since this metric includes cancellations and refunds.”
- Implementation context is how it is actually built. Written for a developer or analyst who needs to know the technical reality. “Count of commerce.purchases.value events. Captures every order submission event regardless of downstream status. No cancellation or refund logic applied at collection time.”
- Industry context is the domain-specific layer that makes your definitions meaningful. “In e-commerce, gross order counts are useful for operational capacity planning. They are not suitable for financial reporting or conversion rate benchmarking where cancelled and returned orders distort performance figures.”

Beyond the three context layers, the supporting fields are what turn a definition into something operationally useful.
known_caveatscarries the tribal knowledge that never gets written down: data quality history, migration gotchas, edge cases. This is the highest-value field in the whole schema.example_valuesgives concrete examples of what values look like in practice.related_componentslinks to component IDs that belong in the same analysis.allowed_metricsandallowed_dimensionsdefine valid combinations.forbidden_combinationsnames combinations that are technically possible but analytically wrong, with the reason why.-
requires_filterencodes filters that must always be applied. source_systemrecords where the raw data originates.ownernames who is accountable for correctness.governance_levelcontrols who is allowed to use this component in reports.last_updatedtracks definition freshness and signals the maintenance backlog.
The full JSON schema entry for a single component looks like this, using the LUMA Cart Additions metric as an example:
context.json
{
"component_id": "commerce.productListAdds.value",
"friendly_name": "Cart Additions",
"dataview_id": "dv_653281907fd53c76c30410f0",
"dataview_name": "Adobestore (Analytics Champs)",
"business_context": "Number of times a product was added to the cart. Primary metric for upper-funnel engagement and product interest. A high cart addition rate with low checkout rate indicates checkout friction.",
"implementation_context": "Sum of commerce.productListAdds.value XDM field, set via Web SDK on add-to-cart events. Populated client-side via Adobe Launch data layer push.",
"industry_context": "Cart addition rate (cart additions divided by product views) industry average is 8 to 10 percent. Add-to-cart to checkout conversion of 40 to 60 percent is typical. Significant drops in either ratio signal UX or pricing issues.",
"known_caveats": "Automated testing and bot traffic inflate cart addition counts significantly. This metric must always be used with the Exclude Bots segment. Without it, counts are inflated by approximately 15 to 20 percent. A data collection outage on 2026-05-12 caused undercounting for approximately 6 hours. Data for that day is not reliable for trend analysis.",
"example_values": "1, 2, 3 (units added per event)",
"related_components": "orders, net_orders, device.type",
"allowed_metrics": "orders, net_orders",
"allowed_dimensions": "device.type, 66212a8e01cd0b29af76fc18_3",
"forbidden_combinations": "",
"requires_filter": "Exclude Bots — automated testing inflates cart addition counts by 15 to 20 percent without this filter.",
"source_system": "Web SDK / Adobe Launch",
"owner": "E-Commerce Analytics Team",
"governance_level": "open",
"last_updated": "2026-05-28"
}
Governance is not optional
Adding context fields is straightforward. Adding governance fields is where this architecture earns its place in a real organization.
allowed_metrics and allowed_dimensions define what can be combined with a component. If an analyst asks Claude to build a report using a combination not on the allowed list, Claude flags it before touching the data. Not after the report runs. Before.
forbidden_combinations goes further. It explicitly names combinations that are technically possible but analytically wrong. A legacy derived field dimension and a device dimension from the same data view will produce a result. That result will look plausible. It will be misleading. This field is where you document that, and the system prompt ensures Claude checks it before every query.

requires_filter encodes the filters that should always accompany a component but never appear in the component definition itself in CJA. Bot exclusion, internal traffic exclusion, date range constraints. Things every analyst knows should be applied but that get forgotten under deadline pressure. When this field is populated, Claude applies the filter automatically and informs the analyst it did so.
governance_level is the access control layer. Values like open, analyst, management only, or finance only tell Claude who is allowed to use this component. When a component is marked management only and an analyst asks for it, Claude does not return results. It returns a clear message:
“This component is restricted to management level access. If you need this analysis, please contact your analytics team lead.”
This is not enforced at the infrastructure level. There is no authentication check happening inside the MCP tool. The enforcement is Claude respecting the governance rule in the system prompt. That distinction matters: it works as an organizational guardrail for well-intentioned use, not as a security boundary for adversarial access. For most internal analytics governance scenarios, that is exactly the right level of control.
Seeing it in practice: Four use cases
To validate the concept I tested against the LUMA Adobestore data view, a realistic e-commerce implementation with product, campaign, order, and engagement data. Each use case was run twice: once with only the native CJA MCP connected, and once with the semantic sidecar added alongside it.
Use Case 1 — The metric that looks simple but isn’t
Prompt: “How many orders did we get this year?”
Without the sidecar, Claude runs the report using the Orders metric. Returns a clean breakdown by month. No warnings, some context. The numbers look right.

With the sidecar, Claude calls get_component_context on the Orders metric before running anything. The sidecar returns the business context flagging that Orders includes cancellations and refunds, and that Net Orders is the correct metric for performance reporting. Claude surfaces this before executing, asks which metric to proceed with, and notes the numbers could be overstated by 8 to 12 percent.

Without the sidecar, an analyst presenting this report to leadership would be showing gross orders as performance. A number finance would immediately question.
Use Case 2 — The forbidden combination
Prompt: “Show me conversion rate by device type and marketing channel.”
Without the sidecar, Claude builds the report combining Device Type with the legacy derived field Marketing Channel. Returns a table. Mobile shows significantly lower conversion rates than desktop.
With the sidecar, Claude calls get_component_context on both components. The sidecar returns the forbidden_combinations flag on the legacy Marketing Channel dimension: combining it with Device Type inflates mobile conversion rates artificially due to cross-device last-touch attribution. Claude stops, explains the conflict in one sentence, and suggests using the right Marketing Channel dimension instead before proceeding.

The first report would have led to incorrect conclusions about mobile performance. The second gets the analyst to the right answer before a single misleading number appears.
Use Case 3 — The required filter nobody remembers
Prompt: “What are our top products by cart additions this week?”
Without the sidecar, Claude runs the report. Returns a product ranking by cart additions. Looks reasonable (unfortunately Claude made the products not that readable).

With the sidecar, Claude calls get_component_context on both Cart Additions and Product Name. Both carry a requires_filter for Exclude Bots. Claude informs the analyst that the filter is being applied automatically because cart addition counts are inflated by 15 to 20 percent without it, then runs the filtered report. The product ranking is different from the unfiltered version.

The unfiltered report would have surfaced products inflated by automated testing. The sidecar applied the right filter silently and told the analyst it did so.
Use Case 4 — The caveat that changes everything
Prompt: “Our cart additions dropped significantly on May 12th. Can you investigate what caused the drop?”
Without the sidecar, Claude investigates. Looks at traffic sources, campaign data, device breakdowns. Surfaces possible causes: campaign issues, traffic anomalies, potential UX problems. A reasonable analysis of what is actually a data artifact.

With the sidecar, Claude calls get_component_context on Cart Additions first. The sidecar immediately returns the known caveat: a data collection outage on 2026-05-12 caused undercounting for approximately 6 hours. Claude flags this before any analysis runs, tells the analyst the data for that day is not reliable, and recommends comparing May 11th and May 13th instead.

Without the sidecar, an analyst could spend an hour investigating a drop that was never real. With it, the answer is immediate.
Keeping It Alive
The hardest part of this project is not the code. It is the JSON file.
A semantic layer that nobody maintains becomes stale in three months. The implementation context you write today will be wrong after the next data migration. The known caveat you document now will be forgotten by whoever reads it in 2027.
A few practices that help. Start with coverage, not completeness. Do not try to fill in all fields for all components before going live. Pick your top 20 to 30 most-queried dimensions and metrics, get those right, and use the rest as a backlog. Treat missing definitions as a signal: when Claude returns “no semantic definition found for component X”, that is a prompt to add one. Let actual usage drive prioritization rather than trying to anticipate everything upfront. Make known_caveats a living document. Every time someone discovers a data quality issue, a migration artifact, or an edge case, it goes in the caveats field. This is where institutional memory lives. And take last_updated seriously. A definition that has not been reviewed in 18 months should be treated with suspicion. Build a quarterly review into your analytics governance cycle.
The architecture that makes it work
Once the semantic sidecar and governance fields are in place, the real insight becomes clear. This is not just a lookup tool. It is three layers working together, each doing exactly one thing.
System Prompt
Defines behaviour and workflow. Instructs Claude to always
check the semantic layer before executing any report.
Specifies how to handle forbidden combinations, required
filters, and governance restrictions.
Semantic Sidecar MCP (yours)
Provides knowledge. Business context, implementation notes,
industry specifics, known caveats, combination rules,
governance levels. Lives in a JSON file you control and
can update without redeploying anything.
CJA MCP (Adobe's)
Executes against data. Fetches component lists, runs reports,
queries your data views. Does not know your business rules.
Does not know your caveats. That is not its job.
The combination system prompt × context × governance produces something qualitatively different from any single layer alone. The system prompt without the sidecar is just instructions with no knowledge to act on. The sidecar without the system prompt provides context that Claude may or may not check before acting. The CJA MCP alone is a powerful but ungoverned reporting tool. Together they form a governed analytical assistant that knows your data, knows your rules, and applies both before touching a query.
Each layer is independently maintainable. The system prompt is owned by whoever sets up the Claude Desktop configuration. The semantic JSON is owned by your analytics or data governance team: no code, no deploy, just a file. The CJA MCP is owned by Adobe. Nobody steps on anyone else.
What this changes
Adobe gives you a data layer, a reporting layer, and now an access layer. What was always missing was the knowledge layer. The organizational context, the rules, the caveats, the governance. That lived in people’s heads and Slack messages that disappear. This architecture makes it operational.

The four use cases above are not edge cases. They are the everyday reality of working with CJA data at any organization that has been running for more than a year. Metrics that include things they should not. Combinations that produce plausible but wrong results. Filters everyone knows are required but nobody applies consistently. Outages that get forgotten three months later when someone tries to explain an anomaly.
The semantic sidecar does not require a big project, a new license, or a migration. It is a JSON file, a lightweight AppBuilder action, and a system prompt. Infrastructure you already own. Knowledge your team already has, finally structured and retrievable at query time.
The MCP server Adobe built is a good foundation. The semantic sidecar is what turns it into infrastructure your organization can actually trust.
The AppBuilder project scaffold, full JSON schema for the LUMA use cases, system prompt, and Claude Code prompt used for this build are available on request. If you are working on something similar or have questions about the implementation, reach out.