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 and gives the link to its page in the hub | 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, link
K->>B: shows the plan and the link
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",
"messageKey": "model-reused", "params": ["/crm/Customer", "9b1…"],
"message": "The model '/crm/Customer' already exists (9b1…) and is reused; …" }
],
"models": [ { "ref": "model.customer", "existingId": "9b1…" } ], …
}Every finding names its element and says in message, in English, what is wrong. That is what the AI client reads. messageKey and params are the same finding as a key, from which the change set page in the hub shows the text in your language. The warnings, the destructive changes and the detail of every planned change in the plan have the same shape.
You should know five 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.
Change sets in the hub
Every change set has its own page in the hub. There you can look at the plan calmly, instead of only reading it in the chat. And you can approve and apply it there.
The pages
There are two ways to get there:
- Through the link.
cdms_create_change_setanswers with alinkto the page of the new change set. The AI client passes it on to you. - Through the module. On the module’s page, the Change Sets button opens the list.
| Page | shows |
|---|---|
| List | all change sets of the CDMS, newest first, 25 per page. For each entry: when it was created, status, by whom, number of changes, how many of them are destructive, how many warnings there are, when and which way it was approved and applied |
| Detail | status and actions, the destructive changes (red), the warnings, the plan and the trail |
The detail page groups the plan in the order it is applied in: folders, enumerations, abstract models, data models, fields, relations, access filters, migrations. Each change carries its type: Neu (CREATE), Ergänzt (UPDATE), Entfernt (DELETE) or Unverändert (UNCHANGED).
The trail names every step with time and person, and for approving and applying also the way: per MCP (through MCP) or im Frontend (in the frontend). If applying failed, the cause is shown with it. People appear with their ID from the token.
Everyone who may see the CDMS sees the list. A CDMS you may not see does not exist here either, see Invisible means 404.
Approving and applying in the hub
| You created the operation | Operation still valid (30 min) | Status | What is offered |
|---|---|---|---|
| yes | yes | PENDING_APPROVAL | Freigeben (approve); for destructive changes only with the role for them |
| yes | yes | APPROVED | Anwenden (apply) |
| no | either | either | nothing, the page gives the reason |
| yes | no | either | nothing, the page gives the reason. Plan the change again. |
| either | either | APPLIED, FAILED, ROLLED_BACK | nothing, the change set is finished |
-
1User→Frontendclicks Freigeben (approve)
-
2FrontendDoes the plan contain destructive changes?Then you have to tick a box in the dialog saying you have seen them. Only then can you approve. The tick is the same as
confirmDestructive: truethrough MCP. -
3Frontend→Hubapproves the change set
-
4Hubchecks the same as for
cdms_approve_change_set: your operation, its validity, the status, and for destructive changes the confirmation and the role -
5Hub→Frontendstatus
APPROVED, way im Frontend -
6User→Frontendclicks Anwenden (apply) and confirms
-
7Frontend→Hubapplies the change set with an
idempotencyKeyIf the answer gets lost and you click again, the page sends the same key. It is still written only once. -
8Hub→Frontendstatus
APPLIED, result per element
You can mix the ways: the AI client creates the change set, you approve it in the hub, and the AI client applies it. Or the other way round.
If the hub refuses, the page says in words why, for example:
| Reason | Message |
|---|---|
| operation expired or not yours | “Die Operation ist abgelaufen oder gehört jemand anderem. Plane die Änderung neu.” (The operation has expired or belongs to someone else. Plan the change again.) |
| destructive change not confirmed | “Bestätige die destruktiven Änderungen, bevor du freigibst.” (Confirm the destructive changes before you approve.) |
| role for destructive changes missing | “Für destruktive Änderungen fehlt dir die Berechtigung.” (You lack the permission for destructive changes.) |
| another apply is running on the CDMS | “An diesem CDMS wird gerade eine andere Änderung angewendet. Versuche es gleich noch einmal.” (Another change is being applied to this CDMS. Try again in a moment.) |
| the schema changed since planning | “Das Datenmodell hat sich seit der Planung geändert. Plane die Änderung neu.” (The data model has changed since planning. Plan the change again.) |
| the status has changed in the meantime | “Das Change Set ist nicht mehr in dem Zustand, den die Seite zeigt. Lade sie neu.” (The change set is no longer in the state the page shows. Reload it.) |
Undoing works only through the AI client with cdms_rollback_change_set.
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.
The server keeps no session with the client. Every call stands on its own; the hub holds the context, the plan and the change set. If a connection drops, the client simply repeats the call, and the operationId stays valid. An interrupted apply is repeated with the same idempotencyKey: if it already ran, the client gets the result instead of a second write.
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 without a session on https://<host>/api/mcp: every call is a POST with exactly one JSON response, there is no Mcp-Session-Id and no GET channel (405). From a browser the server accepts requests only from its own origin. 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.