Skip to main content
Glama
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
```