cms-mcp
README.md
# cms-mcp
An MCP (Model Context Protocol) server that reproduces the CMS calls made by the
CoreNeo Eclipse plugin's *New Query / Service / Exception / Screen / ESB* actions —
**generating OIDs / exception ids** and **checking definition-name availability** —
implemented purely in Node.js.
It talks to the CMS the same way the plugin's `CmsHttpClient` does: **Spring
HttpInvoker** over HTTP with **HTTP Basic** auth. Because HttpInvoker uses Java's
native binary serialization, this project ships a small, focused implementation of
the Java Object Serialization Stream Protocol (`src/java-serialization.js`).
## Tools
| Tool | CMS method | Notes |
|------|-----------|-------|
| `generate_oid` | `generateOid(String platform)` | Returns the OID as a **string** (Java `long`). Used by Query/Service/Screen/ESB. |
| `get_next_exception_id` | `getNextExceptionId()` | Used by the New Exception action. |
| `generate_screen_name` | `generateComponentScreenName(CmsNeoComponent)` | New FBML/XAML Screen wizard. Returns `<module><number>` (e.g. `ANALYTICS20001`). Side-effecting: increments the module screen counter. |
| `list_components` | `getComponentList()` | Lists components; needed to pick a component for name checks / screen names. |
| `check_definition_name` | `existsQueryDefinition` / `existsServiceDefinition` / `existsRegionDefinition` / `existsPopupDefinition` / `existsDadDefinition` | The wizard's "Check Name" button. Supports `query/service/region/popup` and `esb`. |
### How `check_definition_name` works (capture-replay)
The `existsXxxDefinition` methods take a `CmsNeoComponent` object argument. Instead
of hand-modelling that entity's serialized form (fragile: field order + every
`serialVersionUID`), the client first calls `getComponentList()`, **captures** the
real serialized component objects the server sends, and **replays** the chosen one
as the argument — reusing the server's own class descriptor. So name checks stay
correct even if the entity changes shape, as long as `getComponentList` works.
### ESB name checks
`type: "esb"` maps to `existsDadDefinition(component, name, GLOBAL_SCOPE, code)` —
the same call `NewESBConfigurationPage` makes. Pass `configType`
(`Map`/`GroovyWsdl`/`GroovyRest`/`Xslt`/`Uddi`); its `code` (1–5) becomes the
definition type. Notes matching the plugin:
- **`Uddi`** has no uniqueness check → always reports `exists:false` with
`checkedOnServer:false` (no server call).
- ESB scope is always `GLOBAL_SCOPE` (name unique across all components).
- ESB names are **not** normalized (kept as-is), and the stored name **includes the
file extension** — e.g. check `EXT_SWIFT.cfg`, not `EXT_SWIFT`.
## Configuration
By default the server reads the **same files the Eclipse plugin uses** — no env
vars needed:
| File | Values read |
|------|-------------|
| `~/.symphony/cms_eclipse_config.xml` | `serviceUrl` (the `HttpUserService` HttpInvoker bean) |
| `~/.symphony/cms_eclipse.properties` | `cmsUserName`, `cmsPassword` (AES-decrypted), `cmsDBEnvironment` (default platform) |
`cmsPassword` is AES-128/ECB/PKCS5 encrypted with the fixed key from
`AESConverter` — the server decrypts it in memory exactly like the plugin does.
### Overrides (all optional)
Environment variables override the files when set:
| Var | Meaning |
|-----|---------|
| `CMS_URL` | Full remoting URL. |
| `CMS_HOST` / `CMS_PORT` / `CMS_CONTEXT` | Build a URL when no `CMS_URL`/xml URL. Defaults `8081` / `cms_server`. |
| `CMS_USERNAME` / `CMS_PASSWORD` | Plain (already-decrypted) credentials. |
| `CMS_PLATFORM` | Default platform for `generate_oid` (else `cmsDBEnvironment`, else `DEV`). |
| `CMS_TIMEOUT_MS` | Per-call timeout (default 60000). |
| `SYMPHONY_DIR` | `.symphony` directory (default `~/.symphony`). |
| `CMS_CONFIG_XML` / `CMS_PROPERTIES` | Point at specific config files (e.g. `cms_eclipse_config_kaspi.xml`). |
On startup the server logs (to stderr) the resolved target, user, platform, and
which file each value came from.
## Register in Claude Code
Nothing extra needed — it picks up `~/.symphony` automatically:
```bash
claude mcp add cms -- node C:/workspaces/cms/cms-mcp/src/index.js
```
To target a different environment file:
```bash
claude mcp add cms --env CMS_CONFIG_XML=C:/Users/you/.symphony/cms_eclipse_config_kaspi.xml \
-- node C:/workspaces/cms/cms-mcp/src/index.js
```
Or in your MCP client config (`.mcp.json` / client settings):
```json
{
"mcpServers": {
"cms": {
"command": "node",
"args": ["C:/workspaces/cms/cms-mcp/src/index.js"]
}
}
}
```
## Run / test
```bash
npm test # offline unit + wire + MCP-handshake tests (no live CMS needed)
npm start # start the stdio server (reads ~/.symphony by default)
```
### Test against the live CMS
`scripts/live-check.mjs` exercises the real calls directly (reads `~/.symphony`, no MCP
client needed):
```bash
node scripts/live-check.mjs # generate_oid + list_components
node scripts/live-check.mjs oid # generate_oid
node scripts/live-check.mjs exid # get_next_exception_id
node scripts/live-check.mjs screenname "<component>" # generate_screen_name
node scripts/live-check.mjs components # list_components
node scripts/live-check.mjs check query "<component>" "<NAME>" # check_definition_name
node scripts/live-check.mjs esbcheck Map "<component>" "<NAME.cfg>" # ESB name check
```
Debugging aids: `scripts/dump-response.mjs <method>` writes a raw serialized
response to `tmp/<method>.bin`; `scripts/parse-bin.mjs <file>` parses it and reports
where (if anywhere) deserialization derails.
## Status
Verified end-to-end against the live CMS: `generate_oid`, `get_next_exception_id`,
`list_components` (720 components), and `check_definition_name` for
query/service/region/popup **and esb** (correct `true`/`false` both ways).
The full `getComponentList` response also re-serializes **byte-for-byte identically**,
which proves the codec is faithful to the JVM for the real `CmsNeoComponent` graph.
### Notes / gotchas
- Component descriptors are **captured** from `getComponentList`, not constructed, so
they can't drift. Constructed descriptors that must stay correct live in
`src/http-invoker.js` (`RemoteInvocation`) and `src/java-serialization.js`
(`java.lang.String`, and the enum descriptors).
- Enum gotcha (already handled): the `java.lang.Enum` **super** descriptor must be
flagged `SC_ENUM` too (`0x12`), else the server throws
`InvalidClassException: cannot bind non-enum descriptor to an enum class`.
- Field-type signature strings in a class descriptor may be back-references, so the
reader resolves them via `readContent` (not a fresh string read).
## Layout
```
src/
java-serialization.js JOSP reader + writer (handles, classdesc, enum, array, block-data)
http-invoker.js RemoteInvocation build + POST (Basic auth) + RemoteInvocationResult parse
values.js node -> JS conversions; entity field / collection extraction
cms-client.js generateOid, getNextExceptionId, getComponentList, existsDefinition
symphony-config.js reads ~/.symphony config xml + properties; AES password decrypt
index.js MCP server + tool definitions
test/ serialization / wire / mcp-handshake tests
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues