@oneentry/mcp-platform-server
v0.1.14
Published
MCP server that lets an AI agent operate the OneEntry Admin API, grounded in the project's own rules
Maintainers
Readme
@oneentry/mcp-platform-server
MCP server that lets an AI agent operate the OneEntry Admin API — grounded in published operating rules rather than in guesswork.
Nothing is frozen into the package. Both halves are fetched at runtime and cached:
- Knowledge. Small markdown files from a public GitHub repository (ONEENTRY-PLATFORM/oneentry-platform-rules), chunked into searchable sections. Editing a doc there — through the GitHub UI, a PR, whatever — takes effect on the next restart of any server, with no npm release.
- Actuation. Every Admin API operation, built from the instance's own API document at
GET /api/admin/workflows/nodes/admin-api/swagger.json, with parameter and body schemas, the permission each one requires, and a risk classification — reachable through one generic invoke tool instead of one tool per endpoint. The catalog always describes the API that is actually running, not a snapshot of it.
The one thing still committed here is data/permissions.json: the permission an operation requires
is declared in platform code and never reaches the API document, so that map has to be recovered
separately.
Only the Admin API is exposed.
Install
npm i -g @oneentry/mcp-platform-server # or use npx, belowLocal mode (agent and instance on the same machine)
Add to .mcp.json in the repo you work from:
{
"mcpServers": {
"oneentry-mcp-platform": {
"command": "npx",
"args": ["-y", "@oneentry/mcp-platform-server"],
"env": {
"ONEENTRY_CMS_BASE_URL": "http://localhost:3003/api/admin",
"ONEENTRY_CMS_LOGIN": "your-admin-login",
"ONEENTRY_CMS_PASSWORD": "your-admin-password"
}
}
}
}Nothing has to be checked out locally. On first run the knowledge repository is downloaded once and cached; later runs re-use the cache and only ask GitHub whether the commit moved, at most once an hour.
The base URL must point at an instance that exposes the Admin API under /api/admin.
Remote mode (hosted, many agents, one instance)
oneentry-mcp-platform --http --port 8931 --audit /var/log/oneentry-mcp-platform-audit.jsonl \
--base-url https://your-instance.example/api/admin \
--allowed-origins https://agent.exampleEach MCP session carries its own identity, passed as connection headers — never as tool arguments, so a prompt-injected instruction cannot swap identities:
| header | meaning |
|---|---|
| x-cms-token | pre-issued Admin API access token (preferred) |
| x-cms-login + x-cms-password | credentials to log in with |
Sessions never share tokens or confirm tokens. GET /health reports liveness and session count.
The nine tools
| tool | what it does |
|---|---|
| cms_guide | read first: mode, policy, API map, the order to use the other tools |
| cms_docs_search | search the knowledge base, returns sections with snippets |
| cms_docs_read | read one section, with its sibling sections listed for paging |
| cms_api_search | find operations by keyword / tag / method / mutating |
| cms_api_describe | params, body schema, required permission, risk, confirm gating, and curatedBody where a shape has been verified on a live instance |
| cms_api_call | execute one operation; dryRun and confirm-gating for mutations |
| cms_upload_file | upload one file from this machine as multipart/form-data (local mode only) |
| cms_import_file_from_url | fetch one file over http(s) and upload it in a single step |
| cms_whoami | mode, base URL, admin + permissions, knowledge commit, catalog state and warnings |
Plus two MCP resources: oneentry://knowledge/mcp/operating-rules and oneentry://knowledge/index.
Uploading files
cms_api_call sends JSON only, and the upload endpoint wants multipart/form-data — which is why
the two upload tools exist rather than a note telling the agent to go around the server. They run
through the same gate as any other write: --allow=write, the local permission check, dryRun, and
the audit line.
Both bound their source, because a path or a URL arrives as a tool argument and a tool argument can be prompt-injected:
| flag | env | default | what it bounds |
|---|---|---|---|
| --upload-root | ONEENTRY_MCP_UPLOAD_ROOT | the process working directory | cms_upload_file reads nothing outside this directory, symlinks resolved first |
| --upload-max-bytes | ONEENTRY_MCP_UPLOAD_MAX_BYTES | 25 MiB | size of one file, checked on disk and on the wire |
| --upload-allowed-hosts | ONEENTRY_MCP_UPLOAD_ALLOWED_HOSTS | empty | hosts cms_import_file_from_url may fetch from |
cms_upload_file is refused in remote mode: there is no shared filesystem, and a session-supplied
path would read the host's files. cms_import_file_from_url refuses any address resolving to a
loopback, private, link-local or carrier-grade-NAT range, re-checks every redirect hop, and in remote
mode stays disabled until the operator sets an allowlist.
Pass template — the numeric id of a /template-previews record — on the first upload. Without
it the file is stored with no previewLink, nothing reports the omission, and the only repair is
uploading the file again.
Write safety
Read-only by default. --allow (or ONEENTRY_MCP_ALLOW) raises it:
| level | GET | POST/PUT/PATCH | DELETE |
|---|---|---|---|
| read (default) | ✅ | ❌ | ❌ |
| write | ✅ | ✅ | ❌ |
| destructive | ✅ | ✅ | ✅ confirm-gated |
- A refusal by level happens before authentication — no HTTP request is made at all.
- Deletes, and every path in the permanently gated list (
immutable-settings,admins,backups,modules,payments/webhook,settings-general,system/captcha-keys,auth/logout/all-users), require a confirm token: the first call returns the token and the current state of the target, fetched via the sibling GET, so a human can see what is about to change. The token is bound tohash(opId + arguments), single-use, valid 5 minutes. - Permissions are checked locally first: if the admin lacks the permission the operation declares, the call is refused without being sent.
- Every non-GET call is appended to a JSONL audit log (arguments are hashed, not stored). Required in remote mode.
Where the knowledge comes from
A public GitHub repository of small markdown files. Layout:
knowledge/
mcp/operating-rules.md -> docId "mcp/operating-rules"
mcp/docs/api/orders.md -> docId "mcp/docs/api/orders"
mcp/docs/server/cms-api-call.md -> docId "mcp/docs/server/cms-api-call"
mcp/docs/api/orders/index.md -> docId "mcp/docs/api/orders"The docId is the path under knowledge/ without .md; <name>/index.md collapses back to
<name>, which is what lets a long document be split into a directory of small files without
changing the id anything already links to. No file over 12 KB — the same cap cms_docs_read
applies to a section, so a section can never come back truncated.
At startup the server asks GitHub for the commit behind --knowledge-ref (one cheap request,
skipped entirely while the cache is younger than --knowledge-ttl, default 1 h), and downloads
codeload.github.com/<repo>/tar.gz/<sha> only when that commit has moved — one request for the
whole corpus, no API rate limit. The tree is unpacked into --cache-dir keyed by commit, so a
restart is instant and the previous commit is pruned.
| situation | what happens |
|---|---|
| cache fresh | no network at all |
| commit unchanged | one API request, no download |
| commit moved | one API request + one archive download |
| GitHub unreachable, cache present | the cached commit is used |
| GitHub unreachable, no cache | falls back to the bundled operating-rules.md, and says so in cms_guide and cms_whoami |
| --offline | cache and bundled rules only, never the network |
Which repository and which commit is a configuration choice, not a constant: --knowledge-repo
(ONEENTRY_MCP_KNOWLEDGE_REPO, default ONEENTRY-PLATFORM/oneentry-platform-rules) and
--knowledge-ref
(ONEENTRY_MCP_KNOWLEDGE_REF, default main) — a ref may be a branch, a tag or a commit sha, so an
instance can be frozen on a known-good corpus. ONEENTRY_GITHUB_TOKEN is optional and only raises
the API rate limit; the repo is public. cms_whoami reports the repo, ref and 12-character commit
the running server actually loaded.
Reading the docs from a local folder instead
For testing, or for editing docs without committing, point the server at a directory:
ONEENTRY_MCP_KNOWLEDGE_PATH=../oneentry-platform-rules oneentry-mcp-platform
# or: oneentry-mcp-platform --knowledge-path ../oneentry-platform-rulesEither the clone root or its knowledge/ directory works — the path is probed for a knowledge
subdirectory and used directly if there is none. While it is set, GitHub is never contacted and the
cache is not consulted, so edits take effect on the next restart with no commit and no push;
cms_whoami reports source: "local". Leave it unset for the default, which is GitHub.
A path that cannot be read is a startup error, not a fallback: unlike GitHub being unreachable, a bad path is a configuration mistake, and degrading silently to the bundled operating rules would look like an empty corpus.
Known limitation: retrieval is single-language
Search does not translate, and the corpus is English. A question asked in another language will not find an English-only document no matter how it is weighted. The fix is content, not code.
The corpus is hand-authored
The knowledge repository is written for a public audience, one file at a time. There is no import
script and one must not be added: bulk-copying internal documentation into it would publish source
paths, storage identifiers and tracker ids in a single command, and a repository that is public and
mirrored cannot be un-published. npm run publish:knowledge exists only to refuse.
At release time, copy knowledge/mcp/operating-rules.md from the rules repository over this
package's knowledge/operating-rules.md — the rules repository is the source, and the bundled seed
is never hand-edited.
Where the catalog comes from
Two sources, because neither alone is enough:
GET {baseUrl}/workflows/nodes/admin-api/swagger.json?langCode=en_US— paths, operation ids and schemas, straight from the instance you are pointed at. Cached per base URL, so an unreachable instance still yields the last known catalog.data/permissions.json— the permission each operation requires, which the API document does not express. Regenerate it when the platform changes its permissions:
npm run sync:permissions -- --back <path to a platform checkout>That pass walks base classes too, since the operation id is derived from the registered class while the handler often lives in a parent.
Inconsistencies are surfaced as catalog warnings (visible in cms_whoami) rather than swallowed:
paths outside /api/admin (dropped — this server exposes the Admin API only), operations with no
operationId, operations whose permission is known but which the instance does not expose, and
permissions that operations require but no admin can hold — such endpoints answer 403 forever.
If the instance does not serve its API document at all, the catalog is empty and says so loudly. Empty is safe — there is nothing to call — but silence would read as "the platform has no such endpoint".
The API document is not valid JSON Schema
Many type values in the source document are expressions in the platform's own type language
(Record<string, any>, CommonLocalizeInfos, AttributesSets, image, date). The build
normalizes what it can and marks the rest "x-loose": true with the original under x-source-type.
For loose fields the example is the contract, and client-side body validation is deliberately
advisory — the instance is the real validator, so a call is never blocked because a loose field
could not be checked.
Tests
npm test # unit + MCP protocol + remote-mode transport (no instance needed)
npm run test:integration # against a live instance; skips cleanly if unreachableThe integration suite needs ONEENTRY_CMS_BASE_URL pointing at an instance that exposes the Admin
API, plus ONEENTRY_CMS_LOGIN/ONEENTRY_CMS_PASSWORD. It walks search → describe → call, asserts a
read-only refusal sends nothing, and runs a full create → confirm-gated delete cycle on a throwaway
template while checking the audit log.
License
MIT — see LICENSE.
