An intelligent Application Portfolio Management (APM) hub — a central registry for tracking applications across their lifecycle, investment posture, technology stack, and governance metadata, formalised by the Nexus Insight APM Ontology.
- Runtime — Node.js with Express 5
- Database — MongoDB via Mongoose 9
- Validation — Joi
- Identity — Keycloak (OAuth2/OIDC); the API verifies Keycloak-issued JWTs, it does not store credentials itself
- API contract — OpenAPI 3.0, served as interactive docs (Swagger UI) and enforced at runtime for every request/response under
/api - AI access — Model Context Protocol server exposing read-only tools over the same service layer as the REST API, so an LLM client can answer portfolio questions in plain English
- Testing — Jest + Supertest
- Evaluation — an agentic-equivalence harness that compares an MCP-tool-using Claude agent's answers against SPARQL ground truth — see Evaluating agentic equivalence
- Node.js 18+
- Docker (for MongoDB and Keycloak)
npm installnpm run dev:freshRuns scripts/fresh-start.js to bring up a fully working local environment in one step: starts MongoDB and Keycloak via Docker Compose, waits for the nexus-insight realm to be imported, starts the dev server if one isn't already running on http://localhost:3000, resets and reseeds the sample data (npm run seed:sample-data -- --reset), and registers the Nexus MCP server with the Claude Code CLI (npm run connect:mcp. A warning is printed if the claude CLI isn't on PATH). If it started the server itself, it keeps it running in the foreground until you press Ctrl+C.
Use this for manual testing and demos; use the steps below when you want to start/control each piece individually.
npm run mongonpm run keycloakStarts Keycloak at http://localhost:8080 and auto-imports a realm with a client and three test users — see Authentication below.
# Development
npm run env:dev
# Test environment
npm run env:test
# Production
npm run env:prodThe server starts on http://localhost:3000 by default (configurable via PORT in the relevant .env file).
docker build -t nexus-insight .
docker run --rm -p 3000:3000 \
-e PORT=3000 \
-e DB_CONNECTOR="mongodb://host.docker.internal:27017/nexus" \
-e KEYCLOAK_URL="http://host.docker.internal:8080" \
-e KEYCLOAK_REALM="nexus-insight" \
-e KEYCLOAK_CLIENT_ID="nexus-api" \
nexus-insightConfiguration is supplied entirely through environment variables at docker run time (see Environment Variables below), and the process runs as the non-root node user. MongoDB and Keycloak still run separately (npm run mongo / npm run keycloak, or your own instances) — point DB_CONNECTOR/KEYCLOAK_URL at them; host.docker.internal reaches services running on the host from inside the container on Docker Desktop (Windows/Mac).
| Variable | Description |
|---|---|
PORT |
HTTP port (default: 3000) |
DB_CONNECTOR |
MongoDB connection string |
KEYCLOAK_URL |
Base URL of the Keycloak server, e.g. http://localhost:8080 |
KEYCLOAK_REALM |
Keycloak realm name |
KEYCLOAK_CLIENT_ID |
Public client id tokens are issued to |
Environment files: .env.dev, .env.test, .env.prod.
Nexus Insight delegates identity to Keycloak — the API never stores or checks a password itself, it only verifies bearer JWTs Keycloak issued (signature + issuer, against Keycloak's JWKS endpoint).
npm run keycloak auto-imports keycloak/realm-export.json: a nexus-insight realm, a public nexus-api client, three realm roles, and three test users. Admin console: http://localhost:8080 (admin / admin).
| Username | Password | Realm role | Access |
|---|---|---|---|
alice.admin |
Passw0rd! |
admin |
Full access — create, update, delete, manage governance data |
bob.manager |
Passw0rd! |
portfolio-manager |
Create/update, no delete |
carol.viewer |
Passw0rd! |
viewer |
Read-only |
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/login |
Exchange a username/password for a token pair |
POST |
/api/auth/refresh |
Exchange a refresh token for a new token pair |
POST |
/api/auth/logout |
Revoke a refresh token and its session |
GET |
/api/auth/me |
Return the caller's identity and roles (requires a Bearer token) |
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice.admin","password":"Passw0rd!"}'/api/auth/login proxies Keycloak's Direct Access Grant flow for convenience during local development and testing. It is not a production login pattern for user-facing clients — those should use Authorization Code + PKCE against Keycloak directly.
const { authenticate, authorize } = require('../../middleware/auth');
router.delete('/:id', authenticate, authorize('admin'), controller.remove);authenticate verifies the token and attaches req.user = { id, username, email, roles }. authorize(...roles) rejects the request with a 403 unless req.user.roles includes at least one of the given Keycloak realm roles.
Every concrete class in the Nexus Insight APM Ontology has a matching backend module — 34 resources in total, each following the same layered pattern (routes → controller → validations → service → repository → schema.
GET /api/<resource> # list, with resource-specific filter query params
POST /api/<resource> # create (admin, portfolio-manager)
GET /api/<resource>/:id # get one
PUT /api/<resource>/:id # update (admin, portfolio-manager)
DELETE /api/<resource>/:id # delete (admin only)
All routes require a Bearer token (authenticate); write/delete access is additionally gated by role (authorize) as shown above. Full request/response schemas and per-resource filters are in the Swagger UI (see API Documentation) — this table is just the map of what exists, grouped by the ontology's layers:
| Layer | Resources |
|---|---|
| Business | actors, roles, organization-units, locations, business-functions, business-processes, business-services, business-capabilities, portfolios |
| Application | applications, application-contacts, application-dependencies, logical-application-components, physical-application-components |
| Data | data-entities, logical-data-components, physical-data-components |
| Technology | technology-services, logical-technology-components, physical-technology-components, technology-dependencies |
| Governance & Risk | controls, findings, suppliers, cost-records, performance-assessments, service-level-agreements, sla-metrics |
| Software Asset Management (ISO/IEC 19770) | software-products, software-entitlements, metrics, resource-utilization-records |
| Reference documents | documents, code-repositories |
The two abstract ontology superclasses, apm:Dependency and apm:LinkedResource, are never instantiated directly (ApplicationDependency/TechnologyDependency and Document/CodeRepository are their concrete subclasses) and so have no module of their own — every other class does. All 14 SPARQL competency questions in apm-competency-queries.sparql are answerable through this API.
src/modules/dependency-intelligence/ is the one module that isn't a 1:1 ontology-class CRUD resource — it's a read-only graph-analysis layer over the ApplicationDependency/TechnologyDependency edges the modules above already store, answering "what would be affected if this went down?" questions:
| Method | Path | Description |
|---|---|---|
GET |
/api/dependency-intelligence/applications/:id/blast-radius |
Every application transitively downstream of the given one, via BFS over ApplicationDependency |
GET |
/api/dependency-intelligence/technology-components/:id/blast-radius |
Every physical technology component transitively downstream of the given one, via BFS over TechnologyDependency |
Both accept an optional ?maxDepth=<n> query param to bound the traversal, and both also run a DFS cycle check over the reachable subgraph, returning hasCycle (and the cycle itself, if found).
POST /mcp exposes a Model Context Protocol server (src/modules/mcp/) so an LLM client can query the portfolio in plain English instead of writing SPARQL or calling REST endpoints directly. It runs in-process inside the same Express app (Streamable HTTP transport, stateless — no session store), requires the same Bearer token and roles as the REST API (authenticate + authorize('admin', 'portfolio-manager', 'viewer')), and every call is recorded to the audit log alongside REST calls.
Each of the 34 ontology-mapped domain modules above contributes a list_<resource>/get_<resource> tool pair, and the Dependency Intelligence Engine contributes two more (get_application_blast_radius, get_technology_blast_radius) — 71 tools in total, registered in src/modules/mcp/tools/registry.js and including one extra beyond the list/get pairs (get_application_stats). Every tool is a thin, read-only wrapper around the same *.service.js the REST controller calls — no separate business logic, no create/update/delete tools yet. list_* tools accept the same filters as their REST GET list endpoint;
To point an MCP-aware client (e.g. Claude Code) at it, configure a Streamable HTTP server entry for http://localhost:3000/mcp with an Authorization: Bearer <token> header, using a token from /api/auth/login. To exercise it directly:
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_application_stats","arguments":{}}}'Interactive Swagger UI is served at http://localhost:3000/docs once the server is running, generated from src/openapi/openapi.yaml. Every request and response under /api is validated against this spec at runtime — the spec is an enforced contract, not just documentation.
Every call under /api is recorded to an AuditLog MongoDB collection by middleware/logger.js: method, path, status code, duration, caller IP, and — when the route ran authenticate beforehand — the identity that made the call (userId, username, roles; recorded as "anonymous" otherwise).
Request/response bodies and headers are deliberately never recorded — that would otherwise put the password from /api/auth/login or bearer tokens straight into the audit trail.
There is currently no endpoint to read these entries back through the API — querying them today means going straight to MongoDB.
The ontology itself lives in ontology/:
| File | Purpose |
|---|---|
apm-ontology.ttl |
The Nexus Insight APM Ontology (OWL/Turtle) |
apm-shapes.ttl |
SHACL shapes constraining the ontology's classes/properties |
apm-instances-sample.ttl |
A sample instance dataset used to exercise the ontology |
apm-competency-queries.sparql |
SPARQL competency questions answered against the sample dataset |
seed-sample-data.js |
Loads apm-instances-sample.ttl's scenario into MongoDB — see Seeding sample data |
evaluate-agentic-equivalence.js |
Compares an MCP-tool-using agent against SPARQL ground truth — see Evaluating agentic equivalence |
Every concrete class the ontology defines has a backend module and SHACL shapes constrain the classes where cardinality is unambiguous.
npm run validate:ontologyRuns ontology/validate-ttl.js against every .ttl file in ontology/ (or specific files passed as arguments), using N3.js to parse each one and report a line-numbered error for anything that isn't valid Turtle. This checks syntax only — not OWL/SHACL semantics — and exits non-zero on failure, so it's usable as a CI or pre-commit gate.
npm run validate:competency-queriesRuns ontology/validate-competency-queries.js, which parses every SPARQL query straight out of apm-competency-queries.sparql (so the .sparql file stays the single source of truth for the queries themselves), executes each one against apm-ontology.ttl + apm-instances-sample.ttl via Comunica, and asserts the actual results against hand-verified expectations. This is a regression test for the ontology + sample dataset pairing — if an edit to either file changes what a competency question returns, this catches it and exits non-zero.
The expectations are based on running each query and checking its real output, not on blindly trusting the .sparql file's "Expected result" comments.
npm run seed:sample-data # seed once; no-op if already seeded
npm run seed:sample-data -- --reset # delete the previous seed, then reseedRuns ontology/seed-sample-data.js, which loads apm-instances-sample.ttl's scenario into MongoDB through the real REST API (not direct Mongoose inserts), so the seeded data is guaranteed to pass the same Joi/OpenAPI/Mongoose validation the live system enforces. This gives a reproducible dataset whose results should agree with the SPARQL queries run directly against the ontology — the shared basis for both the competency-question regression tests and the agentic-equivalence evaluation below. Requires MongoDB and the dev server running (npm run mongo, npm run env:dev).
npm run evaluate:agentic-equivalence
npm run evaluate:agentic-equivalence -- --only=CQ-1,CQ-14Runs ontology/evaluate-agentic-equivalence.js: for each of the 14 competency questions, a Claude agent equipped with the same MCP tool surface the REST API's controllers use (src/modules/mcp) is given the question as a natural-language prompt, explores the portfolio via real tool calls against the live (seeded) MongoDB data, and submits its final answer through a harness-only submit_answer tool. That structured answer is compared against the truth produced by running the equivalent SPARQL query over apm-ontology.ttl + apm-instances-sample.ttl, and results are reported per-trial and as an overall agreement rate.
This is the project's central evaluation: it tests whether an LLM agent using the MCP tool surface is equivalent to a deterministic SPARQL query over the same ontology, not just whether the tools work in isolation. Requires MongoDB seeded to match apm-instances-sample.ttl (see Seeding sample data above) and ANTHROPIC_API_KEY set (in .env.dev or the shell environment; optionally ANTHROPIC_EVAL_MODEL, default claude-sonnet-5).
src/
├── app.js # Express app setup
├── server.js # Entry point — loads env and starts server
├── config/ # App, auth (Keycloak/OIDC), and DB configuration
├── middleware/
│ ├── auth.js # authenticate / authorize (Keycloak JWT verification)
│ ├── error.js # Central error-handling middleware
│ └── logger.js # Audit logging (see Audit Logging above)
├── common/
│ ├── errors/ # Shared error classes (NotFound, Unauthorized, Forbidden, Conflict, Validation)
│ └── vocabularies/ # Controlled-vocabulary lists mirrored from ontology/apm-ontology.ttl
├── openapi/ # OpenAPI spec + Swagger UI / validator wiring
├── routes/
│ └── index.js # Root router — mounts all module routes
└── modules/
├── applications/ # apm:Application, and sibling directories —
├── .../ # one per ontology class
├── auth/ # Login/refresh/logout/me, backed by Keycloak
├── audit/ # AuditLog Mongoose schema
├── dependency-intelligence/ # BFS blast-radius + DFS cycle detection
└── mcp/
├── server.js # McpServer factory
├── mcp.router.js # Express router — Streamable HTTP transport, mounted at /mcp
└── tools/ # One list_x/get_x tools file per domain module, plus
# dependencyIntelligence.tools.js + registry.js
ontology/
├── apm-ontology.ttl # The Nexus Insight APM Ontology (OWL/Turtle)
├── apm-shapes.ttl # SHACL shapes
├── apm-instances-sample.ttl # Sample instance dataset
├── apm-competency-queries.sparql # SPARQL competency questions
├── validate-ttl.js # `npm run validate:ontology`
├── validate-competency-queries.js # `npm run validate:competency-queries`
├── seed-sample-data.js # `npm run seed:sample-data`
└── evaluate-agentic-equivalence.js # `npm run evaluate:agentic-equivalence`
keycloak/
├── docker-compose.yml # `npm run keycloak`
└── realm-export.json # Realm, client, roles, and test users
mongo/
└── docker-compose.yml # `npm run mongo`
scripts/
├── fresh-start.js # `npm run dev:fresh` — one-shot local environment setup
└── connect-mcp.js # `npm run connect:mcp` — registers the MCP server with the Claude Code CLI