@softacus-software/elwis-mcp-core
v0.1.0
Published
Shared ELWIS domain logic: entity resolution, application scoping and access contracts, consumed by the ELWIS backend and the ELWIS MCP servers.
Keywords
Readme
@softacus-software/elwis-mcp-core
Shared ELWIS domain logic — entity resolution, application scoping, and the access contract — consumed by the ELWIS backend assistant and by the ELWIS MCP servers, so all three run the same code instead of three divergent copies.
This package contains no transport. It never opens a socket, reads an environment variable, or knows what MCP is. Everything it does goes through a port that the consumer implements.
The seam
Domain logic takes its data access as a parameter:
ResolveEntity(api, kind, applicationId, nameOrAlias)api satisfies ElwisReadPort. The MCP servers implement it over HTTP; the backend
implements it in-process against tenant services. Same logic, different adapter.
ElwisReadPort whoAmI · systemAttributes · list · getOne · listRecords
ElwisWritePort create · update · softDelete · createArtifact · updateArtifact ·
softDeleteArtifact · createLinks
ElwisApiPort bothOnly the read port is exercised by the current conformance suite. Write coverage lands with the write slice.
Install and build
npm install
npm run verify # build + tests + dual-module consumption checknpm run build emits dist/esm and dist/cjs, each stamped with its own type field.
TypeScript is pinned to ~5.9.3 on purpose. The ELWIS backend is on 5.9 and the Data MCP
on 6.0; declarations are emitted by the lower compiler so both consumers can read them.
main and types are set alongside exports on purpose. The backend compiles with
module: commonjs and no moduleResolution, which defaults to node10 — and node10 ignores
exports entirely. Removing the legacy fields silently breaks that consumer.
The conformance suite
The point of this package is that two adapters behave identically. RunReadPortConformance
is how you prove it — the same cases run against the HTTP adapter and the in-process adapter.
import { RunReadPortConformance } from '@softacus-software/elwis-mcp-core';
const results = await RunReadPortConformance(myAdapter, {
applicationId, applicationName, kind: 'data-type', knownEntityName, knownEntityId,
});
const failed = results.filter(r => !r.passed);It is framework-agnostic: no test runner is imported, cases return results rather than asserting, so consumers can drive it from jest, node:test, or a script.
Every case carries a why. The load-bearing one is excludes-soft-deleted: the HTTP adapter
gets soft-delete filtering for free because it injects deleted: null into every filter, while
an adapter reading collections directly does not. Without that case, an in-process adapter
would quietly surface deleted entities into resolution and into user-facing pickers.
The suite is itself tested: test/conformance.test.ts runs it against a deliberately broken
adapter and asserts that it fails.
Resolution behaviour
ResolveEntity and ResolveOneId are ported verbatim from elwis-data-mcp, including the
$or-to-name fallback for backends that reject $or with a 400. That fidelity is deliberate:
the extraction has to be provably behaviour-neutral before anything consumes it.
ResolveOneStrict is new and nothing calls it yet. It exists because the current assistant
answers a missed lookup with a raw validator message:
That answer won't work — Invalid string: must match pattern /^[a-f\d]{24}$/iResolveOneStrict instead throws NotFoundError carrying the entities that do exist, so a
caller can say "no data type called X here — did you mean one of these, or shall I create it?"
AmbiguityError covers the multiple-match case.
HTTP host (./host)
The ./host subpath (ServeMcpHttp, ElwisJwtVerifier) is the one exception to "no
transport" above: it is an actual node:http listener. elwis-docs-mcp and elwis-data-mcp
use it to serve MCP over HTTP with a bearer token instead of stdio.
It binds 127.0.0.1 unless the caller passes a different host; accepts only POST /mcp (a
GET is answered 405 Allow: POST — neither server sends server-initiated messages, so the
standing text/event-stream a GET would otherwise open carries nothing and exists only to
be held open); and caps both concurrent token verifications (maxConcurrentVerifications) and
concurrent live sessions (maxConcurrentSessions), so a caller with no valid credential at all
cannot force unbounded verification work, and a caller who does verify cannot hold an unbounded
number of built servers open. Neither cap, nor the bind address, is read from an environment
variable by this package itself — each server that embeds ServeMcpHttp wires its own env
vars for these and documents them in its own README's operator section.
This is a library entry point, not a runnable server on its own: nothing in this package listens by itself outside of a test.
Errors
ElwisApiError carries status, so shared logic can branch on an HTTP status without
importing a transport. IsElwisApiError also accepts the Data MCP's existing ElwisHttpError
by shape, so the standalone server does not have to change its error class to adopt this
package.
Status
Phase 1 of the capability-layer plan: extracted, tested, buildable, with no consumer yet.
Phase 2 switches elwis-data-mcp over and deletes its copies — until that lands, the logic
exists twice, which is the one thing this package is meant to prevent.
