What this is about
You can maintain models in the hub by hand. But you can also let an AI client change them, for example Claude. For this, the hub has an MCP server. MCP (Model Context Protocol) is a standard that lets an AI client call the tools of a server.
The AI client does not simply start writing. Every change goes through the same gate: describe, check, plan, get your approval, apply once.
Three terms come up again and again on this page:
- binds all following calls to one CDMS
- belongs to the user who creates it
- is valid for 30 minutes
- describes what should exist
- symbolic
refs instead of IDs - only adds; change and delete only through
migrations
- the frozen plan
- is approved and then applied
The tools
The MCP server offers 15 tools. Most of them only read.
| Tool | does | changes anything? |
|---|---|---|
cdms_list_targets | lists the CDMS systems you are allowed to see | no |
cdms_get_target | metadata of a CDMS | no |
cdms_get_capabilities | allowed field, relation, rule, model and endpoint types, naming rules, system fields | no |
cdms_get_structure | folders with IDs and paths | no |
cdms_get_schema | the complete current schema | no |
cdms_get_element | a single element with its blueprint key | no |
cdms_create_operation_context | opens a change to a CDMS | no, only opens the bracket |
cdms_get_operation_context | status and target of a change | no |
cdms_validate_blueprint | checks a blueprint | no |
cdms_plan_changes | checks and calculates what would change | no |
cdms_create_change_set | freezes the plan as a change set | no |
cdms_approve_change_set | approves the change set | only the status |
cdms_apply_change_set | writes the change to the hub | yes |
cdms_get_change_set_status | status and results | no |
cdms_rollback_change_set | undoes an applied change set | yes |
In the hub, a CDMS is a module. Its ID is the cdmsId that all tools work with. The same information is also available as readable MCP resources under cdms://targets/{cdmsId}/….
The path of a change
-
AI clientReadWhich CDMS exist, what is allowed, what does the schema look like today?
-
MCP serverOpen the bracketIs the user allowed to see this CDMS?↳ no
CDMS_TARGET_NOT_FOUND -
MCP serverValidateIs the blueprint valid, do target and bracket match?↳ no List of all errors and warnings, fix the blueprint
-
MCP serverPlanWhat is created, what stays, what is destructive?↳ no
BLUEPRINT_INVALID -
UserApproveHave you seen the plan and explicitly agreed? For destructive changes: have you confirmed them separately, and do you hold the role for them?↳ no Change set stays in
PENDING_APPROVAL, for destructive changes withDESTRUCTIVE_CHANGE_BLOCKED -
MCP serverApplyIs it approved, unchanged, the only one under way on this CDMS right now, and is the schema still the same as when it was planned?↳ no
CHANGE_SET_NOT_APPROVED,BLUEPRINT_INVALID,DESTRUCTIVE_CHANGE_BLOCKEDorSCHEMA_VERSION_CONFLICT, nothing written - The model in the hub is changed
This is what it looks like as a conversation between the parties:
sequenceDiagram
actor B as User
participant K as AI client
participant M as MCP server (hub)
participant DB as Hub database
K->>M: cdms_list_targets, cdms_get_capabilities, cdms_get_schema
M-->>K: targets, rules, current schema
K->>M: cdms_create_operation_context(cdmsId)
M-->>K: operationId (valid for 30 min)
K->>M: cdms_validate_blueprint(operationId, blueprint)
M-->>K: valid, errors, warnings
K->>M: cdms_plan_changes(operationId, blueprint)
M-->>K: planId, summary, changes, destructiveChanges
K->>M: cdms_create_change_set(operationId, planId)
M-->>K: changeSetId, PENDING_APPROVAL
K->>B: shows the plan
B-->>K: "Yes, apply"
K->>M: cdms_approve_change_set(operationId, changeSetId, confirmDestructive)
M-->>K: APPROVED
K->>M: cdms_apply_change_set(operationId, changeSetId, idempotencyKey)
M->>DB: read the current schema again and compare
M->>DB: all changes in one transaction
M-->>K: APPLIED + result per element
The blueprint
The blueprint is a JSON document. It describes folders, enums, models, fields, relations and access filters the way they should exist after the change.
{
"operationId": "4c2b…",
"blueprint": {
"blueprintVersion": "1",
"target": { "cdmsId": "a7f5…", "expectedCdmsName": "CVC Backend" },
"folders": [
{ "ref": "folder.crm", "name": "CRM",
"existingFolderId": "2a7f…" } ← existing folder
],
"enums": [
{ "ref": "enum.status", "folderRef": "folder.crm",
"name": "CUSTOMER_STATUS",
"values": [ { "key": "ACTIVE" }, { "key": "BLOCKED" } ] }
],
"models": [
{ "ref": "model.customer", "folderRef": "folder.crm",
"name": "Customer", ← already exists
"fields": [
{ "ref": "field.customer.status", "name": "status",
"type": "ENUM", "enumRef": "enum.status",
"required": true }
] }
]
}
}{
"valid": true,
"errors": [],
"warnings": [
{ "code": "BLUEPRINT_INVALID", "element": "model.customer",
"message": "Das Modell '/crm/Customer' existiert bereits … wird wiederverwendet …" }
],
"models": [ { "ref": "model.customer", "existingId": "9b1…" } ], …
}You should know four rules:
- Symbolic names instead of IDs. Elements refer to each other through their
ref, for exampleenumRef: "enum.status". The hub assigns the IDs. - Add, never delete silently. The hub recognizes what already exists by its path (
/crm/Customer) and reuses it. What is missing from the blueprint stays unchanged. Changing or deleting existing things only works explicitly through themigrationssection. - What you use, you declare too. If you want to reference an existing model or enum, it must be in the blueprint, otherwise you get
BLUEPRINT_REFERENCE_NOT_FOUND. For existing folders you setexistingFolderId. - A relation is a pair of fields. Both sides are in the blueprint as a
RELATIONfield and point to each other throughinverseFieldRef, see Both sides of a relation. - Do not name an environment. A CDMS has no environment: its model is the same in development, test and production, and the environments differ only in their data.
target.expectedEnvironmentis therefore an error.
The hub checks strictly: an unknown JSON key is already an error. But it collects all content findings, so the AI client can fix everything in one pass.
| Result | Examples |
|---|---|
Error (valid: false) | name breaks the naming rule or is reserved (id, _createdOn …), duplicate ref, field already exists in the base model, other side of a relation is missing, unknown field or rule type, cycle in the inheritance, target.expectedEnvironment set |
Warning (valid: true) | element already exists and is reused, a different value on an existing element is not taken over, details such as displayName are ignored, empty blueprint |
Explicit changes: migrations
Everything that changes or removes existing things is its own entry under migrations:
| Kind | Examples |
|---|---|
| rebuild | RENAME, MOVE, SET_OPTIONS, SET_FIELD_TYPE, SET_RULE |
| remove | REMOVE_RULE, REMOVE_RECURSION, REMOVE_ENDPOINT, REMOVE_ROLE, REMOVE_ENUM_VALUE |
| delete | DELETE_FIELD, DELETE_MODEL, DELETE_ENUM, DELETE_FOLDER |
Every migration counts as destructive in the plan. A cascading delete through a relation, a removed access filter and a model that is audited are destructive as well. You approve a change set with destructive changes only with a dedicated role and only if you confirm those changes separately, see “Approving destructive changes” further down.
The plan
cdms_plan_changes validates once more and then compares the blueprint with the current schema. The result is a plan:
{ "operationId": "4c2b…", "blueprint": { … as above … } }{
"planId": "af72…",
"summary": { "createCount": 2, "updateCount": 0, "deleteCount": 0,
"unchangedCount": 2, "destructiveChangeCount": 0 },
"changes": [
{ "type": "UNCHANGED", "elementType": "folder", "path": "/crm" },
{ "type": "CREATE", "elementType": "enum", "path": "/crm/CUSTOMER_STATUS" },
{ "type": "UNCHANGED", "elementType": "model", "path": "/crm/Customer" },
{ "type": "CREATE", "elementType": "field", "path": "/crm/Customer#status" }
],
"destructiveChanges": [],
"blueprintHash": "269a…"
}Every row in changes has a type: CREATE, UPDATE, DELETE or UNCHANGED. destructiveChanges lists in plain text all changes that can destroy something. This exact plan is what the AI client shows you before you approve. A plan lives for 30 minutes.
The change set and its states
cdms_create_change_set takes only a planId, no new blueprint. So what gets approved is exactly what was planned.
stateDiagram-v2
[*] --> PENDING_APPROVAL: cdms_create_change_set
PENDING_APPROVAL --> APPROVED: cdms_approve_change_set
APPROVED --> APPLIED: cdms_apply_change_set, success
APPROVED --> FAILED: cdms_apply_change_set, error
APPLIED --> ROLLED_BACK: cdms_rollback_change_set
FAILED --> [*]
ROLLED_BACK --> [*]
APPLIED --> [*]
| State | means | what next |
|---|---|---|
PENDING_APPROVAL | planned, waiting for you | approve, or just leave it |
APPROVED | approved | apply |
APPLIED | written to the hub | done, or undo |
FAILED | applying failed, nothing was written | plan again |
ROLLED_BACK | undone | plan again |
FAILED and ROLLED_BACK are final states. A change set is never tried a second time. There is no tool to discard one: a change set that you do not approve stays in PENDING_APPROVAL and is never applied.
Approving destructive changes
If the plan lists entries under destructiveChanges, a plain “yes” is not enough:
| Role for destructive changes | confirmDestructive | What happens |
|---|---|---|
| yes | true | APPROVED |
| yes | missing or false | DESTRUCTIVE_CHANGE_BLOCKED (destructive-change-not-confirmed), stays PENDING_APPROVAL |
| no | either | DESTRUCTIVE_CHANGE_BLOCKED (destructive-change-requires-role), stays PENDING_APPROVAL |
The AI client shows you the destructive changes one by one and asks about them separately. Only if you confirm exactly that part does it send confirmDestructive: true. Your administrator grants the role; which role it is, the operator of the hub decides. On apply the server checks the role once more. If it has been taken from you in the meantime, nothing is written.
A change set without destructive changes needs neither the role nor the confirmation.
Apply exactly once
-
1AI client→MCP servercalls
cdms_apply_change_setwithoperationId,changeSetIdand anidempotencyKey -
2MCP serverIs the change set
APPROVED? -
3MCP serveralready
APPLIEDorROLLED_BACK→CHANGE_SET_ALREADY_APPLIED; not yet approved →CHANGE_SET_NOT_APPROVED -
4MCP serverWas the same
idempotencyKeyalready used for this change set?Then the earlier result comes back. Nothing is written twice. This only works for you: whoever repeats someone else's key getsOPERATION_CONTEXT_NOT_FOUND. -
5MCP serverIs another apply or undo running on this CDMS right now?
-
6MCP serveryes →
SCHEMA_VERSION_CONFLICT(cdms-change-in-progress), nothing written. Try again later. -
7MCP serverDoes the frozen blueprint still match its
blueprintHash? If the change set has destructive changes, do you still hold the role for them? -
8MCP serverno →
BLUEPRINT_INVALIDorDESTRUCTIVE_CHANGE_BLOCKED, nothing written -
9MCP server→Databasereads the current schema again and compares it with the state at planning time
-
10MCP serverbase has changed →
SCHEMA_VERSION_CONFLICT, nothing written -
11MCP server→Databasewrites all changes in the order of the plan, in one transactionWriting goes through the normal system layers of the hub, so with permission checks and hooks like any other write access.
-
12MCP serverchecks the result: every element exists exactly once, every
refis resolved -
13MCP serverany error → everything is discarded, status
FAILED -
14MCP server→AI clientcommit, status
APPLIED, result per element (CREATED,REUSED, …)
When the base has changed in the meantime
Between planning and applying, someone else may have changed the model in the hub. That is why the server reads the schema again before writing:
| What has changed since planning? | What happens |
|---|---|
| nothing | is applied |
| a planned new element now exists | SCHEMA_VERSION_CONFLICT, plan again |
| a reused element has disappeared | SCHEMA_VERSION_CONFLICT, plan again |
| an old value no longer matches (length, default, auditing …) | SCHEMA_VERSION_CONFLICT, plan again |
| the target of a migration has changed or is gone | SCHEMA_VERSION_CONFLICT, plan again |
Undo
cdms_rollback_change_set only works from APPLIED. The rollback undoes, in reverse order, what the change set created or changed. Elements that were only reused (REUSED) stay untouched. The rollback also runs in one transaction, and as with apply, only one runs on a CDMS at a time. It does not need the role for destructive changes: it only takes back what the same change set did.
A change set that deleted a model (DELETE_MODEL) or an enum (DELETE_ENUM) cannot be undone. In that case the server refuses the rollback right away.
The Operation Context: one target, half an hour
-
MCP serverContext knownDoes the
operationIdexist, is it younger than 30 minutes, and did you create it?↳ noOPERATION_CONTEXT_NOT_FOUND -
MCP serverSame targetDoes the call, including
target.cdmsIdin the blueprint, point to the same CDMS?↳ noCDMS_TARGET_MISMATCH -
MCP serverSame bracketDo plan and change set belong to this
operationId?↳ no Error, nothing happens - The call affects exactly the CDMS that was chosen when it was opened
The operationId is not a key you can pass on. If someone else knows it, for example from a shared chat history, they cannot approve, apply or undo anything with it: for them, this context does not exist.
The context is valid for 30 minutes from when it is created. Approve, apply and undo need it. So the whole chain from opening to applying must run within this time. When the time is up, you open a new bracket and plan again.
When opening, the server accepts only the UUID of the CDMS. The reading tools also accept the exact module name. Several matches result in CDMS_TARGET_AMBIGUOUS. There is no fuzzy search.
Connecting the AI client
Your access
You get the access data from us when you register or sign the contract:
| Item | Meaning |
|---|---|
| Base URL | where the AI client connects, e.g. https://<host>/api/mcp |
| Client ID | the identifier of your access. It is public and may go into the repository |
| Callback port | the local port on which the AI client receives the answer after login |
There is no client secret. Your access is a public OAuth client secured with PKCE: for every login the AI client creates a one-time key that only it knows. A tool on your machine could not keep a secret secret anyway.
What you may see and change is decided by your user account, not by the access. A CDMS you have no rights on does not show up in cdms_list_targets.
Claude Code
claude mcp add --transport http --client-id <your-client-id> --callback-port <port> \
codamai-cdms https://<host>/api/mcp
--transport httpgoes before the name and the URL. Without it, Claude Code creates a local stdio server.--client-idand--callback-portare required. Without them Claude Code tries to register itself as a client, and the hub rejects that.- No
--client-secret, see above. - With
--scope projectthe entry goes into the project’s.mcp.jsonand applies to the whole team; with--scope userit applies to all your projects.
Then you log in inside a Claude Code session:
-
1Developer→AI clienttypes
/mcpand picks Authenticate forcodamai-cdms -
2AI client→Browseropens the login page
-
3User→Keycloaklogs in with their account
-
4Keycloak→AI clientsends the code to
http://localhost:<port>/callback -
5AI client→Keycloakexchanges the code for a token and stores it locally
-
6AI client→MCP serverserver is
connected, the tools are ready
In the same /mcp menu you see the server’s tools and can log out again, for example when you switch users or your permissions have changed. You can also manage the entry from the command line:
claude mcp list # all servers with connection status
claude mcp get codamai-cdms # details for one server
claude mcp remove codamai-cdms # remove a server
Instead of claude mcp add you can also write the server directly into the project’s .mcp.json:
{
"mcpServers": {
"codamai-cdms": {
"type": "http",
"url": "https://<host>/api/mcp",
"oauth": { "clientId": "<your-client-id>", "callbackPort": <port> }
}
}
}
Do not add an Authorization header. The client gets the token itself through the login, and a fixed header would be a committed access key.
Fewer prompts, but not for writing
Claude Code asks before every tool call. You can allow the reading tools permanently in .claude/settings.json. The names follow the scheme mcp__<servername>__<toolname>:
{
"permissions": {
"allow": [
"mcp__codamai-cdms__cdms_list_targets",
"mcp__codamai-cdms__cdms_get_target",
"mcp__codamai-cdms__cdms_get_capabilities",
"mcp__codamai-cdms__cdms_get_structure",
"mcp__codamai-cdms__cdms_get_schema"
]
}
}
cdms_approve_change_set, cdms_apply_change_set and cdms_rollback_change_set never go into this list. The prompt before these calls is your approval.
Other AI clients
When: recommended
claude mcp add as above, with client ID and callback port.
Result: Login via /mcp
When: the client supports Streamable HTTP and OAuth
Transport Streamable HTTP on https://<host>/api/mcp. The client finds the login by itself: to the first call without a token the server answers 401 and names the discovery document (/.well-known/oauth-protected-resource/api/mcp) in the WWW-Authenticate header. Enter your client ID as client_id and http://localhost:<port>/callback with your callback port as the redirect URI.
When: the client can only register itself
Some clients, such as custom connectors in Claude Desktop, only log in through dynamic client registration. The hub does not offer that. In such cases use Claude Code or a client that lets you pass the client ID and callback port.
Permissions
Everything the MCP server reads and writes goes through the same permission checks as the REST API. A CDMS you may not see does not exist for you: the answer is CDMS_TARGET_NOT_FOUND, see Invisible means 404.
And then?
Applying only changes the models in the hub. Your application only notices it at the next build:
-
1MCP server→HubChange set
APPLIED: model in the hub changed -
2Build→Hubfetches the metadata again at the next build
-
3Generatorcreates the code from the new model
-
4CDMS→Databaseadjusts the database schema at startup, depending on the migration modeResult: The application knows the new field
How the build fetches the metadata is described in Code generation in the build.