| name | ep-mule-onboarding |
| description | Automates generating a complete Anypoint Studio Mule 4 subscriber project directly from Solace Event Portal metadata via the EP MCP Server. Use this skill whenever a developer wants to onboard a new event subscriber, set up a MuleSoft project from Event Portal, generate Mule connector config from EP metadata, create a subscriber template for a Solace event domain, or pull topic/schema definitions from EP into Anypoint Studio. Invoke proactively when the user says things like "onboard [app] as subscriber", "set up Mule project for [app] from EP", "create subscriber template for [app]", "pull EP metadata into Studio", "generate Mule config from Event Portal", or any combination of Event Portal + MuleSoft/Anypoint/subscriber. This skill replaces the deprecated EPv1 connector integration with a modern MCP-powered workflow that is fully automated. |
EP → MuleSoft Subscriber Onboarding
This skill pulls event and schema metadata from Solace Event Portal via the EP MCP Server
and generates a complete, ready-to-run Anypoint Studio Mule 4 subscriber project — directly
into the developer's Studio workspace, without manual configuration steps.
It replaces the deprecated EPv1 Solace connector integration (removed in connector v1.8.0)
with a modern workflow: EP is the design-time source of truth, and Claude bridges it to Studio.
Known Pitfalls — Read Before Generating
These were discovered building the Roche MDM template. Avoid them from the start.
DataSense metadata wiring
- DO NOT write
catalog.xml with <flow-type-entry> — Studio ignores it entirely.
- DO NOT use
<types:json-type sourceType="JSON" schema="..."/> or isDefault="true" in application-types.xml — wrong format, Studio will overwrite it with an empty catalog on next open.
- The correct format uses
<types:type name="..." format="json"> + <types:shape format="json-schema" location="..."/> for definitions and <types:enrichment select="#[doc-id]"> for component wiring. See Step 6 for the exact XML.
- Write
application-types.xml only when the Studio project is closed. Studio regenerates this file from its internal workspace metadata on startup — if you write it while Studio is open, it gets overwritten.
Solace connector attributes
- DO NOT add
outputMimeType="application/json" to <solace:topic-listener> — it is not in the connector v1.8.x XSD and causes a build failure. The payload arrives as binary; parse it in DataWeave with read(payload, "application/json").
External DataWeave files
- Reference external
.dwl files with resource="filename.dwl" — no classpath: prefix. Mule resolves files in src/main/resources/ automatically.
Runtime version
- The
<app.runtime> value in pom.xml and minMuleVersion in mule-artifact.json must match the Mule Runtime version installed in Studio (visible in the Package Explorer under the project). A mismatch causes Netty classloader conflicts at startup.
Prerequisites
- EP MCP Server configured and connected (
mcp__solace-ep-designer__* tools available)
- Anypoint Studio project folder connected as a Cowork folder
- Application exists in EP with at least one subscribed event version
- Solace PubSub+ Connector v1.8.x in the project's
pom.xml
Workflow
Step 1 — Gather inputs
Use AskUserQuestion to collect anything not already known:
- Application name — exactly as it appears in EP (e.g., "KIAM")
- Application domain — EP domain name (e.g., "Roche MDM Customer Master Data")
- Studio project path — if not a connected Cowork folder, request via
mcp__cowork__request_cowork_directory
- Mule runtime version — ask which version is installed in Studio (e.g., 4.12.0) — needed for pom.xml and mule-artifact.json
- Broker credentials — host (tcps://... for TLS), VPN, username, password — only if not already in
config.properties
Step 2 — Query Event Portal
Use the EP MCP tools in this sequence:
getApplicationDomains → find domain ID by name
getApplications → find application by name (filter by domain ID)
getApplicationVersions → get latest version (sort by version, take highest)
→ from version.declaredConsumingEventVersionIds, for each subscribed event:
getEventVersion → topic address + schema reference (schemaVersionId)
getSchemaVersions(ids:[id]) → full JSON schema content field
From each event version extract:
- Topic pattern — from
deliveryDescriptor.address.addressLevels:
literal levels → static string (e.g., Roche, MDM, HCPProfile)
variable levels → {snake_case} placeholder (e.g., {region_code})
- If
deliveryDescriptor.address is null, reconstruct from event description or ask user
- Schema content — raw JSON schema string from
schemaVersion.content
- EP IDs — domain, app, app-version, event, event-version, schema-version (for traceability)
Step 3 — Derive the Mule subscription topic
Solace topic best practices:
- Solace topics are hierarchical, slash-delimited strings. Each level should represent a meaningful
business dimension. Never collapse multiple concepts into one level.
* matches exactly one level. > matches one or more remaining levels (must be last).
- Prefer
> at the end of a subscription to catch all sub-levels when the app doesn't need
to filter on trailing dynamic segments.
- Use
* per level only when you need to skip a specific level but still constrain later ones
(e.g., wildcard region but constrain country).
- Subscribe at the broadest level the application actually needs. Narrower subscriptions
reduce noise; too narrow a subscription misses events. Let the EP topic pattern guide you.
- Always include commented narrow examples in
config.properties so teams can restrict
without guessing the pattern.
Conversion rules:
- Remove
{variable} segments at the end → replace with >
- Replace
{variable} segments in the middle → replace with *
Example:
EP topic: Roche/MDM/ManageMasterDataGovernance/HCPProfile/Updated/v1/{region_code}/{country_code}/{uci}
Default sub: Roche/MDM/ManageMasterDataGovernance/HCPProfile/Updated/v1/> ← all regions/countries
Narrow EMEA: Roche/MDM/ManageMasterDataGovernance/HCPProfile/Updated/v1/EMEA/> ← EMEA only
Narrow UK: Roche/MDM/ManageMasterDataGovernance/HCPProfile/Updated/v1/*/UK/* ← UK only, any region
Step 4 — Generate UUIDs for DataSense wiring
python3 -c "import uuid; [print(str(uuid.uuid4())) for _ in range(2)]"
Assign: first UUID → solace:topic-listener (doc:id), second → ee:transform (doc:id).
Step 5 — Build the transformed output schema
The DataWeave transform output schema has three top-level objects:
metadata — topic routing info (eventType, sourceTopic, dynamic segment values, receivedAt)
[entityName] — normalised entity fields mapped from the raw payload
rawEvent — original unmodified payload (for auditability and downstream passthrough)
Optionally create this schema in EP via createSchema + createSchemaVersion to keep EP as
the source of truth for both input and output types.
Step 6 — Write all project files
Write all files to the Studio project folder. Close Studio first so application-types.xml
is not overwritten on startup.
pom.xml — key properties block (adapt to match the Mule runtime version installed):
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<app.runtime>[MULE_RUNTIME_VERSION]</app.runtime>
<solace.connector.version>1.8.1</solace.connector.version>
<mule.maven.plugin.version>4.10.0</mule.maven.plugin.version>
</properties>
mule-artifact.json
{
"name": "[app-name]",
"minMuleVersion": "[MULE_RUNTIME_VERSION]",
"javaSpecificationVersions": ["17"]
}
src/main/resources/config.properties
# =============================================================================
# [AppName] Subscriber — Connection & Subscription Configuration
# =============================================================================
# For production: supply credentials as environment variables or Anypoint Secrets Manager.
# Do NOT commit real credentials to source control.
# ── Broker Connection ─────────────────────────────────────────────────────────
solace.host=REPLACE_WITH_SMF_HOST
solace.vpn=REPLACE_WITH_VPN_NAME
solace.username=REPLACE_WITH_CLIENT_USERNAME
solace.password=REPLACE_WITH_CLIENT_PASSWORD
# ── Topic Subscription ────────────────────────────────────────────────────────
# Default: catches all [EventName] events across all dynamic segments
solace.topic.subscription=[DERIVED_WILDCARD_TOPIC]
# Narrow examples (uncomment and adjust):
# [REGION filter example using >]
# [COUNTRY filter example using */COUNTRY/*]
# ── Application Identity ───────────────────────────────────────────────────────
app.name=[APP_NAME]
app.version=1.0
# ── Solace Event Portal References ────────────────────────────────────────────
# EP Domain: [Domain Name] (id: [domain-id])
# EP Application: [App Name] (id: [app-id])
# EP App Version: [version] (id: [app-version-id])
# EP Event: [Event Name] (id: [event-id])
# EP Event Ver: [version] (id: [event-version-id])
# EP Schema: [Schema Name] v[ver] (id: [schema-version-id])
# EP Portal: https://console.solace.cloud/designer
# =============================================================================
src/main/resources/[event-name]-schema.json
Write the exact JSON schema content from EP. Add a description noting the EP origin:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Sourced from Solace Event Portal schema version [schema-version-id].",
...rest of EP schema content...
}
src/main/resources/[event-name]-transformed-schema.json
Write the enriched output schema matching the DataWeave transform output (metadata + entity + rawEvent).
src/main/resources/application-types.xml
Write this file only when Studio is closed. It defines types AND wires them to components.
Studio owns this file and will regenerate it — but only overwrites with its own state on open,
so writing it while closed is safe and will be picked up correctly.
<?xml version='1.0' encoding='UTF-8'?>
<types:mule xmlns:types="http://www.mulesoft.org/schema/mule/types">
<types:catalog>
<types:type name="[EventSchemaName]" format="json">
<types:shape format="json-schema" location="[event-name]-schema.json"/>
</types:type>
<types:type name="[TransformedSchemaName]" format="json">
<types:shape format="json-schema" location="[event-name]-transformed-schema.json"/>
</types:type>
</types:catalog>
<types:enrichment select="#[SOURCE_UUID]">
<types:processor-declaration>
<types:output-event>
<types:message>
<types:payload type="[EventSchemaName]"/>
</types:message>
</types:output-event>
</types:processor-declaration>
</types:enrichment>
<types:enrichment select="#[TRANSFORM_UUID]">
<types:processor-declaration>
<types:output-event>
<types:message>
<types:payload type="[TransformedSchemaName]"/>
</types:message>
</types:output-event>
</types:processor-declaration>
</types:enrichment>
</types:mule>
src/main/resources/[event-name]-transform.dwl
DataWeave best practices applied:
- Read payload directly with
read(payload, "application/json") — not read(payload as String, ...) (payload is already the right type)
- Access
event.field directly — avoid intermediate resolved* variables unless a field genuinely requires a topic-segment fallback
- Only create a variable when it is reused in multiple places or when the fallback logic is non-trivial
- Always include the segment index map comment — it is the most critical comment for future maintainers
- Use
write(payload, "application/json") in loggers to dump the full transformed structure
%dw 2.0
output application/json
// ── Topic parsing ──────────────────────────────────────────────────────────────
// Full pattern: [FULL_EP_TOPIC_PATTERN]
// Segment map: [0]=[seg0] [1]=[seg1] ... [N]={variable1} [N+1]={variable2}
var topic = attributes.destination default ""
var seg = topic splitBy "/"
// ── Parse payload (Solace delivers binary; read as JSON) ───────────────────────
var event = read(payload, "application/json")
---
// Output conforms to [TransformedSchemaName] (EP: [output-schema-version-id])
{
metadata: {
eventType: "[EventType]",
sourceTopic: topic,
[var1Name]: seg[[idx1]] default "UNKNOWN", // {variable1}
[var2Name]: seg[[idx2]] default "UNKNOWN", // {variable2}
receivedAt: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"}
},
[entityName]: {
// Map directly from event payload — no intermediate variables needed
// unless a field has a topic-segment fallback (use default for those)
[field1]: event.[field1],
[field2]: event.[field2] default "UNKNOWN", // required, fallback if missing
[field3]: event.[field3], // optional, passes null naturally
// ...one entry per schema property
},
rawEvent: event // preserved for auditability and downstream passthrough
}
src/main/mule/[app-name]-[event-name]-subscriber.xml
<?xml version="1.0" encoding="UTF-8"?>
<mule xmlns="http://www.mulesoft.org/schema/mule/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:solace="http://www.mulesoft.org/schema/mule/solace"
xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core"
xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
xsi:schemaLocation="
http://www.mulesoft.org/schema/mule/core
http://www.mulesoft.org/schema/mule/core/current/mule.xsd
http://www.mulesoft.org/schema/mule/solace
http://www.mulesoft.org/schema/mule/solace/current/mule-solace.xsd
http://www.mulesoft.org/schema/mule/ee/core
http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<configuration-properties file="config.properties" doc:name="Config Properties" />
<solace:config name="Solace_Config" doc:name="Solace Config">
<solace:connection
msgVPN="${solace.vpn}"
brokerHost="${solace.host}"
clientUserName="${solace.username}"
password="${solace.password}" />
</solace:config>
<flow name="[app-name]-[event-name]-subscriber"
doc:name="[AppName] [EventName] Subscriber">
<solace:topic-listener
config-ref="Solace_Config"
doc:name="Subscribe: [EventName]"
doc:id="[SOURCE_UUID]"
topics="${solace.topic.subscription}" />
<logger level="INFO" doc:name="Log: Raw Event"
message='#["[[APP]] Topic: " ++ (attributes.destination default "UNKNOWN") ++ " | Payload: " ++ payload]' />
<ee:transform doc:name="Transform: Parse [EventName] Payload"
doc:id="[TRANSFORM_UUID]">
<ee:message>
<ee:set-payload resource="[event-name]-transform.dwl"/>
</ee:message>
</ee:transform>
<logger level="INFO" doc:name="Log: Transformed [EventName]"
message='#["[[APP]] Transformed: " ++ write(payload, "application/json")]' />
<error-handler>
<on-error-continue enableNotifications="true" logException="true"
type="ANY" doc:name="Handle Any Error">
<logger level="ERROR" doc:name="Log: Error"
message='#["[[APP]] Error on topic: " ++ (attributes.destination default "UNKNOWN") ++ " | " ++ error.description]' />
</on-error-continue>
</error-handler>
</flow>
</mule>
Step 7 — Confirm and guide the user
After writing all files:
- List every file written with its path
- Reopen Studio → right-click project → Refresh
- Run the app — the DataSense metadata for both the topic-listener output and the transform output will be automatically wired from
application-types.xml
- To verify: click any component → Metadata tab → both input and output types should resolve without any manual steps
To test end-to-end:
- Fill in broker credentials in
config.properties
- Run the app
- Use the broker's Try Me! tab to publish a sample JSON payload to the full topic (e.g.,
Roche/MDM/.../v1/EMEA/UK/UCI-123)
- Confirm both log lines appear in the Studio console: raw topic log, then full transformed JSON
Quality standards
- Use real EP data — every field name and type must come from the actual schema. Never invent fields.
- Segment index map comment — always include it in the
.dwl file. It is the most important comment for future maintainers.
- EP reference IDs in every file — include domain, application, event, and schema version IDs as comments in every generated file. This creates a traceable audit trail back to the EP design-time spec.
- Sensible defaults — required fields default to
"UNKNOWN" or a topic segment; optional fields pass null naturally (no explicit default null needed in DataWeave unless required for schema compliance).
- No unnecessary variables — only create a DataWeave variable when the value is reused in multiple output fields, or when a non-trivial fallback expression would be repeated. Reading
event.field directly is cleaner than var x = event.field; ... x.
- External
.dwl files — always extract the DataWeave script to a separate file. Inline CDATA in the flow XML makes the transform invisible to version control diffs and harder to unit test.
- Full payload logging — always log the complete transformed payload with
write(payload, "application/json") after the transform. This is essential for integration testing and debugging.