MCP Server Configuration
1. Introduction
The Universal MCP Server is a thin JSON‑RPC wrapper that exposes Conscia’s orchestrated capabilities (built in the DX Engine) as MCP‑compliant tools. This allows any LLM‑based agent (ChatGPT, Perplexity, Gemini, etc.) or your own web chat client to invoke business capabilities such as product discovery or checkout via a single, standard gateway.
Why a wrapper? 95 % of the heavy lifting—data stitching, transformation, rule evaluation, caching—resides in the DX Engine. The MCP Server simply converts the DX Engine’s JSON payloads into the standard MCP response envelope so that agents can consume them.
2. Prerequisites
- An active Conscia DX Engine tenant with at least one customer & environment.
- API URL and token with permission to read the Flows you will expose.
- Relevant Flows (orchestrations) already authored and tested in DX Engine.
3. MCP Configuration JSON
This section describes how to author an MCP Configuration — the JSON file that defines a single MCP server: how it authenticates callers, which users/tokens may use which tools, the tools it exposes, and how each tool calls the DX Engine.
3.1. How a configuration is located and addressed
Public URL the MCP client connects to:
https://mcp.conscia.ai/{customerCode}/{environmentCode}/{mcpConfigurationCode}
e.g. https://mcp.conscia.ai/ayana-corp/mcp/discover
→ customerCode=ayana-corp, environmentCode=mcp, mcpConfigurationCode=discover.
Each configuration is a JSON file identified by three codes:
| Code | Source |
|---|---|
customerCode | the first path segment |
environmentCode | the second path segment |
mcpConfigurationCode | the thircd path segment |
3.2. Top-level structure
{
"mcpConfigurationCode": "discover", // identity / metadata
"version": "1.0.0",
"name": "Ayana's MCP Server", // shown to the MCP client
"description": "This is Ayana's MCP Server",
"environmentCode": "mcp",
"authentication": { /* §3.3 — how callers prove who they are */ },
"authorization": { /* §3.4 — which caller may use which tools */ },
"dxengine": { /* §3.6 — default DX Engine connection for all tools */ },
"tools": { "mapping": [ /* §5 — the tools exposed */ ] },
"discovery": { /* §3.7 — OAuth protected-resource metadata (oauth only) */ },
"resources": {}, // reserved
"prompts": {} // reserved
}
| Field | Required | Notes |
|---|---|---|
mcpConfigurationCode, version, name | yes | metadata; name/version/description are reported to the client on initialize |
description, environmentCode | no | informational |
authentication | no | defaults to { "type": "oauth" } (see §3) |
authorization | no | per-tool / server gating (see §4) |
dxengine | yes (if any tool calls DXE) | default connection for tool calls (see §6) |
tools.mapping | yes | array of tool definitions (see §5) |
discovery | only for oauth | RFC 9728 metadata (see §7) |
3.3. Authentication options (authentication)
authentication.type selects the auth model. If the whole authentication block is omitted, the
default is oauth.
"authentication": { "type": "public" | "oauth" | "bearerToken", ... }
3.3.1 public
No authentication. Every caller is anonymous and all tools are available. The server returns a normal
200 to an unauthenticated initialize, so an MCP client connects without any OAuth prompt.
"authentication": { "type": "public" }
A
publicserver still calls the DX Engine using the tokens in itsdxengineblock, so anyone who knows the URL can invoke its tools. Scope the DX Engine token accordingly.
3.3.2 oauth (default)
Validates a Keycloak-issued JWT (cryptographically verified against the realm's JWKS and
issuer). Pairs with the authorization block (§4) to gate access by JWT claims. This is the
model used for Claude custom connectors via OAuth.
"authentication": { "type": "oauth" }
- A missing/invalid token →
401with aWWW-Authenticateheader pointing at the protected-resource metadata (this is what triggers the client's OAuth login). - Requires a
discoveryblock (§7) so the client can find the authorization server.
3.3.3 bearerToken
Treats the Authorization: Bearer <token> value as an opaque, static shared secret (it is not
verified as a JWT). The token is resolved to a set of permission tags (or full access), which are
then matched against authorization.tools (§4) to decide which tools are exposed.
The block may contain a tokenPermissions array and/or a dxengine block:
"authentication": {
"type": "bearerToken",
"tokenPermissions": [ // optional; checked first
{ "token": "abc-123-xyz", "permissions": ["tag-one"] },
{ "token": "def-456-uvw", "permissions": ["tag-two", "tag-one"] },
{ "token": "ghi-789-mno", "fullAccess": true }
],
"dxengine": { // optional; fallback lookup
"templateCode": "get-permissions",
"responseTransform": "`response.components['get-permissions'].response`" // optional
}
}
Resolution order for the presented token:
tokenPermissions(precedence, terminal). Find the entry whosetokenexactly equals the presented token. If found, that entry decides the outcome — the DX Engine fallback is not consulted:"fullAccess": true→ all tools."permissions": [ ... ](non-empty) → those permission tags.- neither → denied (matched, but grants nothing).
dxenginefallback (only when notokenPermissionsentry matched). Calls the DX Engine withtemplateCodeand a context of exactly{ "token": "<token>" }. The response is unwrapped withresponseTransform(defaults to`response.components['<templateCode>'].response`— the same wrapping tool calls use) and must be one of:or{ "fullAccess": true }{ "permissions": ["tag-one", "tag-three"] }- Denied (HTTP
403, with a descriptive server log) when: no token (→401), notokenPermissionsmatch and nodxengineblock, the DX Engine response is non-conforming, or the resolved access is empty (fullAccess:false/permissions:[]).
At least one of
tokenPermissions/dxengineshould be present. Raw tokens are never logged — only a short SHA-256 hash.
3.4. Authorization options (authorization)
The authorization block maps a caller's values to the tools they may use. The source of those
values depends on the authentication type:
| auth type | values come from | authorization.claim used? |
|---|---|---|
oauth | a JWT claim (authorization.claim) | yes |
bearerToken | the token's resolved permissions tags | no (ignored) |
public | n/a — authorization is not applied | no |
"authorization": {
"claim": "realm_access.roles", // oauth only: dot-path into the JWT payload
"allowedValues": ["mcp-access"], // any-of → access to the server + all tools w/o an override
"tools": { // optional per-tool any-of overrides
"listCollections": ["tag-one"],
"getCollectionByCode": ["tag-two"]
}
}
Fields
claim(oauth only) — dot-path into the JWT payload, e.g.realm_access.roles(Keycloak),groups, orscope. The resolved value is normalized to a set of strings (arrays → items; a string → split on whitespace, which also covers space-delimitedscope). This makes the model IdP-agnostic — pointclaimat whatever your IdP emits.allowedValues— the default set of values. A caller holding any of them gets server access and access to every tool that does not have its own override.tools— per-tool overrides.tools[<toolName>]is the any-of list of values required for that specific tool (it replacesallowedValuesfor that tool).
Semantics (all matching is any-of / OR, case-sensitive)
- Per-tool requirement =
tools[name]if present, elseallowedValues, else none (open). - Tool access = caller's value-set intersects the tool's requirement (or the requirement is none).
- Server access = the caller can access at least one tool; otherwise the whole connection is
rejected with
403. fullAccess(from abearerTokenresolution) bypasses all per-tool checks → every tool.- If the
authorizationblock is omitted, no per-tool gating is applied — every authenticated caller sees every tool.
Example: who sees what
With the tools overrides above:
| Caller values | listCollections | getCollectionByCode |
|---|---|---|
["tag-one"] | ✅ | ❌ |
["tag-two"] | ❌ | ✅ |
["tag-one","tag-two"] | ✅ | ✅ |
fullAccess | ✅ | ✅ |
| none / unrelated | server access denied (403) | — |
3.5. Tool definitions (tools.mapping[])
Each entry defines one tool exposed to the MCP client.
{
"name": "getCollectionByCode", // tool id the client/LLM calls
"description": "Get a collection's details by code. ...", // tell the LLM when/how to use it
"schema": { // JSON Schema of the tool's arguments
"type": "object",
"properties": {
"collectionCode": { "type": "string", "description": "The code of the collection to get." }
},
"required": ["collectionCode"]
},
"dxengine": { // how this tool calls the DX Engine (see §6)
"templateCode": "get-collection-by-code",
"responseTransform": "`response.components['get-collection-by-code'].response`"
}
}
| Field | Required | Notes |
|---|---|---|
name | yes | unique tool identifier |
description | yes | written for the LLM — describe purpose, inputs, and output thoroughly |
schema | yes | a JSON Schema object for the arguments; converted to a Zod schema at runtime. Use title/description on each property and a required array |
dxengine | yes (to call DXE) | per-tool DX Engine settings; missing fields fall back to the top-level dxengine block (§6) |
The tool name is matched against authorization.tools for per-tool gating (§4).
3.6. DX Engine calls (dxengine)
There are two dxengine blocks:
- Top-level
dxengine— the default connection (URL + token + customer/environment) used by all tools and by thebearerTokenpermissions lookup. - Per-tool
dxengine(inside atools.mapping[]entry) — the template to run, plus optional overrides of the connection fields.
"dxengine": {
"url": "https://query.conscia.io/api",
"token": "eyJhbGci...", // DX Engine auth token (Bearer)
"customerCode": "ayana-corp",
"environmentCode": "mcp"
}
What a tool call sends
When a tool is invoked, the service issues:
POST {url}/experience/template/_query
Authorization: Bearer {token}
x-customer-code: {customerCode}
x-environment-code: {environmentCode}
{ "templateCode": "<tool.dxengine.templateCode>", "context": <the tool's arguments> }
Field resolution (per-tool value wins, then top-level, then the request's codes):
| Sent value | Resolution order |
|---|---|
url, token | tool.dxengine.* → mcpConfig.dxengine.* |
x-customer-code | tool.dxengine.customerCode → mcpConfig.tools.customerCode → mcpConfig.dxengine.customerCode → request customerCode |
x-environment-code | tool.dxengine.environmentCode → mcpConfig.tools.environmentCode → mcpConfig.dxengine.environmentCode → request environmentCode |
Per-tool dxengine fields
| Field | Notes |
|---|---|
templateCode | required — the DX Engine experience/template to run |
url, token, customerCode, environmentCode | optional overrides of the top-level connection |
responseTransform | optional expression to shape the response (see below) |
contextTransform | present in the schema but not currently applied — the tool's raw arguments are sent as context as-is |
responseTransform and the expression language
responseTransform is evaluated against the DX Engine response, with the raw parsed body bound to a
variable named response. DX Engine wraps template output under
components['<templateCode>'].response, so the common form is:
"responseTransform": "`response.components['get-collection-by-code'].response`"
Expression syntax (handled by src/utils/evaluate-object.js):
`…`(backticks) → a JavaScript/ES6 expression, e.g.`response.components['x'].response`.#…#→ a JSONata expression.
If responseTransform is omitted, the full HTTP response object is returned. The tool result is
returned to the client as JSON text.
3.7. Discovery block (discovery) — oauth only
For oauth configs, this provides the RFC 9728 protected-resource metadata the MCP client fetches
after a 401 to discover the authorization server.
"discovery": {
"resource": "http://localhost:5030", // overridden at runtime with the live request URL
"authorization_servers": ["https://keycloak.conscia.io/realms/conscia-dev"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["openid", "profile", "email"], // advisory only — not enforced
"resource_name": "Test MCP Server"
}
| Field | Notes |
|---|---|
resource | the canonical resource URL; automatically overridden at runtime to the live https://{host}/{env}/{config} |
authorization_servers | issuer URL(s) of the IdP (Keycloak realm) |
bearer_methods_supported | how the token is sent (header) |
scopes_supported | advisory — tells the client which scopes it may request; enforcement is via authorization (§4), not this list |
resource_name | human label |
public and bearerToken configs do not need a discovery block (they never emit the OAuth 401).
3.8. Complete examples
3.8.1 OAuth + role-gated tools
{
"mcpConfigurationCode": "discover",
"version": "1.0.0",
"name": "DX Graph MCP Server",
"environmentCode": "mcp",
"authentication": { "type": "oauth" },
"authorization": {
"claim": "realm_access.roles",
"allowedValues": ["mcp-access"],
"tools": { "getCollectionByCode": ["mcp-admin"] }
},
"discovery": {
"authorization_servers": ["https://keycloak.conscia.ai/realms/conscia-dev"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["openid", "profile", "email"],
"resource_name": "Test MCP Server"
},
"dxengine": {
"url": "https://query.conscia.io/api",
"token": "eyJ...",
"customerCode": "ayana-corp",
"environmentCode": "mcp"
},
"tools": {
"mapping": [
{
"name": "listCollections",
"description": "List all collections",
"schema": { "type": "object", "properties": { "customerCode": { "type": "string" } }, "required": ["customerCode"] },
"dxengine": { "templateCode": "list-collections", "responseTransform": "`response.components['list-collections'].response`" }
},
{
"name": "getCollectionByCode",
"description": "Get a collection's details by code.",
"schema": { "type": "object", "properties": { "collectionCode": { "type": "string" } }, "required": ["collectionCode"] },
"dxengine": { "templateCode": "get-collection-by-code", "responseTransform": "`response.components['get-collection-by-code'].response`" }
}
]
}
}
A user needs the mcp-access realm role to connect and use listCollections, and the mcp-admin
role to additionally use getCollectionByCode.
8.2 Static bearer tokens
{
"mcpConfigurationCode": "bearerdemo",
"version": "1.0.0",
"name": "DX Graph MCP Server (Bearer Token)",
"environmentCode": "mcp",
"authentication": {
"type": "bearerToken",
"tokenPermissions": [
{ "token": "abc-123-xyz", "permissions": ["tag-one"] },
{ "token": "ghi-789-mno", "fullAccess": true }
],
"dxengine": { "templateCode": "get-permissions" }
},
"authorization": {
"tools": { "listCollections": ["tag-one"], "getCollectionByCode": ["tag-two"] }
},
"dxengine": { "url": "https://query.conscia.io/api", "token": "eyJ...", "customerCode": "ayana-corp", "environmentCode": "mcp" },
"tools": { "mapping": [ /* listCollections, getCollectionByCode */ ] }
}
abc-123-xyz sees only listCollections; ghi-789-mno (fullAccess) sees both; any other token is
resolved via the get-permissions DX Engine template, or denied.
8.3 Public (no auth)
{
"mcpConfigurationCode": "publicdemo",
"version": "1.0.0",
"name": "DX Graph MCP Server (Public)",
"environmentCode": "mcp",
"authentication": { "type": "public" },
"dxengine": { "url": "https://query.conscia.io/api", "token": "eyJ...", "customerCode": "ayana-corp", "environmentCode": "mcp" },
"tools": { "mapping": [ /* tools available to everyone */ ] }
}
4. Quick reference
{
"mcpConfigurationCode": "string", // required
"version": "string", // required
"name": "string", // required
"description": "string",
"environmentCode": "string",
"authentication": {
"type": "public" | "oauth" | "bearerToken", // default "oauth"
// bearerToken only:
"tokenPermissions": [ { "token": "string", "permissions": ["string"] | "fullAccess": true } ],
"dxengine": { "templateCode": "string", "responseTransform": "string?" }
},
"authorization": {
"claim": "string", // oauth only (dot-path into JWT)
"allowedValues": ["string"],
"tools": { "<toolName>": ["string"] }
},
"dxengine": { "url": "string", "token": "string", "customerCode": "string", "environmentCode": "string" },
"tools": {
"mapping": [
{
"name": "string",
"description": "string",
"schema": { /* JSON Schema */ },
"dxengine": {
"templateCode": "string",
"url": "string?", "token": "string?", "customerCode": "string?", "environmentCode": "string?",
"responseTransform": "string?"
}
}
]
},
"discovery": { // oauth only
"authorization_servers": ["string"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["string"],
"resource_name": "string"
}
}
5. API Reference (DX Engine & MCP Server)
Below is a concise reference for the endpoints used by the MCP Server when talking to Conscia’s DX Engine. Replace the variables in {{CAPS}} with real values at runtime.
| # | Method & Path | Purpose |
|---|---|---|
| 1 | POST /mcp | Create (or upsert) an MCP configuration. |
| 2 | PATCH /mcp/{mcpConfigurationCode} | Update an existing MCP configuration. |
| 3 | GET /mcp/{mcpConfigurationCode} | Retrieve a single MCP configuration. |
| 4 | GET /mcp | List all MCP configurations for a customer/environment. |
| 5 | DELETE /mcp/{mcpConfigurationCode} | Delete an MCP configuration. |
5.1 Common Headers
Content-Type: application/json
Authorization: Bearer {{DX_ENGINE_SYSTEM_TOKEN}}
X-Customer-Code: {{customerCode}}
X-Environment-Code: {{environmentCode}}
Authorizationmust carry a System‑level PAT (scope: System). PATs scoped to Query will not be able to create or update MCP configurations.
9.2 Create or Upsert an MCP Configuration
POST {{DX_ENGINE_URL}}/mcp
# body snippet
{
"mcpConfiguration": {
"mcpConfigurationCode": "bestbuy",
"version": "1.0.0",
"name": "My First MCP Configuration",
...
"tools": { "mapping": [ /* see §5 */ ] },
"resources": {},
"prompts": {}
}
}
9.3 Update an MCP Configuration (Partial)
PATCH {{DX_ENGINE_URL}}/mcp/bestbuy
# body identical to POST
9.4 Retrieve an MCP Configuration
GET {{DX_ENGINE_URL}}/mcp/bestbuy
Response is the stored JSON object.
9.5 List All MCP Configurations
GET {{DX_ENGINE_URL}}/mcp
9.6 Delete an MCP Configuration
DELETE {{DX_ENGINE_URL}}/mcp/bestbuy
10. Appendix: Useful DX Engine Links & Resources
- DX Engine Docs → Components, Flows, Templates
- Recipes → Multi‑Brand Conversational Commerce, Checkout Orchestration