vidocq-runtime-extensions is the set of extensions shipped out of the box, grouped by domain under five aggregators (essentials, jakartaee-core, jakartaee-web, microprofile, module-repackaged). Each leaf is an independent Maven artefact, activated by the user simply adding it as a dependency.

Coordinates

Parent artefact

io.vidocq.runtime.extensions:vidocq-runtime-extensions:0.4.0-SNAPSHOT

Source

vidocq-runtime-extensions/

Catalogue

Extension Aggregator Role

vidocq-runtime-chappe-webserver-extension

essentials

Wires Chappe as HTTP/1.1, H2, H3 transport. Configures bind via vidocq.http.host / vidocq.http.port.

vidocq-runtime-devconsole-extension NEW

essentials

The dev console: a read-only page, on a loopback listener of its own, with the startup report of the running boot and the live values of the extensions. On in a dev launch only. Its own panels show the configuration NEW (the config panel), the beans of the Vauban container NEW (the cdi panel), the last log records and the log levels, in a dev launch NEW (the logs panel), the last test run under vidocq:dev with continuous testing NEW (the tests panel), and the JVM.

vidocq-runtime-migration-extension

essentials

Schema-migration facade — runs the configured migration engine before the datasource opens, and says what it did in the startup report NEW (Schema migration NEW).

vidocq-runtime-flyway-migration-extension

essentials

Flyway-based schema migrations.

vidocq-runtime-liquibase-migration-extension

essentials

Liquibase-based schema migrations. Liquibase’s usage analytics stay off unless you configure them (Liquibase analytics stay off NEW).

vidocq-runtime-langchain4j-cdi-mcp-extension NEW

essentials

Hosts a langchain4j-cdi MCP server: one dependency brings the server, its CDI 4.1 invoker, Jakarta REST and the server’s bean list; vidocq.mcp.* keys configure it. Its section of the startup report is a live dev console panel NEW (The mcp panel in the dev console NEW). Not on Maven Central yet. See langchain4j-cdi MCP server NEW.

vidocq-runtime-cassini-rest-extension

jakartaee-core

Wires Cassini for Jakarta REST 4.0. Static routing generated at build (companion -codegen artefact), CDI bridged via Vauban. Its rest section of the startup report lists every route, and is a live dev console panel NEW (The rest section of the startup report NEW, The rest panel in the dev console NEW).

vidocq-runtime-mansart-pool-extension

jakartaee-web

JDBC connection pooling via Mansart Pool (companion -datasources-codegen artefact for multi-datasource holders). Its section of the startup report is a live dev console panel NEW (Mansart pools in the startup report and the dev console NEW).

vidocq-runtime-mansart-data-extension

jakartaee-web

Jakarta Data 1.0 via Mansart — scans @Repository, implementations generated via APT (companion -codegen artefact). Its section of the startup report is the application’s entities and repositories, a dev console panel NEW (The Mansart Data catalogue in the startup report and the dev console NEW).

vidocq-runtime-mansart-transactions-extension NEW

jakartaee-web

Handles @Transactional via mansart-transactions (companion -codegen artefact). One dependency is enough: it also brings the JDBC-to-JTA bridge, so Mansart Data writes actually join the transaction (Mansart Data writes join the transaction NEW).

vidocq-runtime-ravel-config-extension

microprofile

https://microprofile.io/specifications/microprofile-config/ via Ravel — external sources, profiles, typed conversion. Companion -codegen artefact NEW: it runs Ravel’s extension in the compiler, so @Inject @ConfigProperty points are satisfied when Vauban validates the application at build time; the values themselves are checked when the application starts.

vidocq-runtime-knock-health-extension

microprofile

https://microprofile.io/specifications/microprofile-health/ via Knock — /health, /health/live, /health/ready, /health/started endpoints. Its health section of the startup report, the checks by probe and the last answer of each, is a live dev console panel NEW (Knock health checks in the startup report and the dev console NEW).

vidocq-runtime-dirac-metrics-extension

microprofile

https://microprofile.io/specifications/microprofile-metrics/ via Dirac — Prometheus endpoint. Its metrics section of the startup report, what the registries hold, is a live dev console panel NEW (Dirac metrics in the startup report and the dev console NEW).

vidocq-runtime-grimm-openapi-extension

microprofile

https://microprofile.io/specifications/microprofile-open-api/ via Grimm — document generated at compile time (-ui companion serves the viewer). Its openapi section counts the operations and offers to open the document and the Swagger UI NEW; the rest panel of the dev console shows the same buttons. The extension requires Grimm’s named module, io.vidocq.grimm.cdi.vauban NEW: with the automatic module it used to require, a packaged application whose cassini:generate sealed its modules lost every Grimm bean, and /openapi answered 404 (Vidocq/grimm#15). The extension brings the Jakarta REST API NEW, which grimm-cdi-vauban leaves to the runtime, so an application with OpenAPI and no Cassini starts.

vidocq-runtime-cervantes-jwt-extension

microprofile

MicroProfile JWT Auth via Cervantes.

vidocq-runtime-cyrano-rest-client-extension

microprofile

MicroProfile REST Client via Cyrano. Companion -codegen artefact NEW: it runs Cyrano’s extension in the compiler, so the @RestClient bean of each @RegisterRestClient interface exists when Vauban validates the application, and generates each client’s proxy, so the application opens nothing to Cyrano. Without it, @Inject @RestClient is unsatisfied; vidocq:checkpom asks for it. An application with a Rest Client now links with vidocq:jlink: org.reactivestreams, an automatic module, is no longer required (Vidocq/cyrano#30). vidocq-runtime-cyrano-rest-client-example calls one of its own resources through an injected client. The extension brings the Jakarta REST API and Champollion’s JSON-B implementation NEW: Cyrano depends on the APIs only, so an application with the Rest Client and no REST server needs nothing else to read and write JSON bodies.

vidocq-runtime-humboldt-telemetry-extension

microprofile

MicroProfile Telemetry via Humboldt — traces, metrics, logs, OTLP/HTTP export. The extension brings the Jakarta REST API NEW, which humboldt-rest leaves to the runtime, so an application with Telemetry and no REST endpoint starts.

vidocq-runtime-heisenberg-fault-tolerance-extension

microprofile

MicroProfile Fault Tolerance via Heisenberg — @Retry, @Timeout, @CircuitBreaker, @Bulkhead, @Fallback interceptors (pure CDI, no HTTP endpoint; Dirac metrics and Humboldt telemetry stay optional).

CDI (Vauban) and JSON (Champollion) are not standalone extensions: Vauban is the engine built into vidocq-runtime-core, and Champollion ships transitively with the REST extension.

Planned: a Jakarta Servlet 6.1 extension wiring Foy on top of Chappe.

Typical lifecycle — Chappe example

The vidocq-runtime-chappe-webserver-extension extension illustrates the pattern. Four hook points:

Phase Actor Priority Action

configure

ChappeEngineExtension

100

installs ChappeMountPoint

onStart

Contributors (REST, Servlet)

500–9999

call mount(…​)

onStart

ChappeServerBootstrap

10000

starts a Server per listener

onStop

ChappeServerBootstrap

10000

shuts servers down cleanly

Every listener logs the address it actually bound NEW, not the one it was given: with port=0, the line shows the real port.

Chappe listener 'default' started on http://localhost:43127/

A wildcard host reads localhost and an IPv6 address is bracketed, http://[::1]:8080/.

The startup report gets the same addresses in its http section NEW, one per listener of the configuration. The sections that declare routes, such as rest, then print them as absolute URLs, and the mcp section finds the URL of /mcp there:

http          HTTP (Chappe) | 0 ms
  default http://127.0.0.1:18090/
rest          REST (Cassini) | 1 ms
  1 resource class, 4 routes, 4 providers at /
  POST    http://127.0.0.1:18090/mcp          McpEndpoint#handlePost

A listener an extension declares for itself is left to that extension’s section: the dev console’s is in the devconsole section. An extension reads the URL a listener really listens on with ChappeMountPoint.instance().boundUrl(name), from its contribute onwards.

A listener of an extension’s own NEW

An extension that serves something apart from the application, as the dev console does, declares its own listener from its onStart, between priority 100 and 10000, then mounts on it by name like on any other:

ChappeMountPoint mp = ChappeMountPoint.instance();
mp.declareListener(ChappeListener.http("dev", "127.0.0.1", 8888),
        new ListenerOptions(true, true, Duration.ofSeconds(1), this::bound));
mp.router("dev").get("/api/snapshot", snapshot);
ListenerOptions Effect

anyPortWhenTaken

When the port is in use, listen on a free port instead of failing the boot, and log a WARNING naming both ports. Chappe first retries the bind with a backoff, as for any listener, so falling back costs about one second.

quiet

Log the Chappe listener '…' started on … line at DEBUG, for an extension that prints its own.

shutdownGracePeriod

How long stopping the server waits for the requests in flight. null keeps Chappe’s 30 seconds.

onBound

Called on the boot thread once the server listens, with the address it bound: the real port when the declared one was 0 or taken. Nothing can be mounted any more, and an exception it throws is logged as a WARNING, never fatal.

ListenerOptions.DEFAULTS starts the listener like one of the configuration. ChappeListener.httpUrl(address) turns the address onBound receives into the URL the log line shows.

A listener name has one owner. The extensions' listeners start first, then those of vidocq.chappe.listeners, which must not list a name an extension declared: the boot fails with listener 'dev' is declared by an extension (DevConsoleExtension); remove it from vidocq.chappe.listeners. The listener named default is always the application’s.

Other extensions follow comparable scheduling logic with their own priorities.

The rest section of the startup report NEW

vidocq-runtime-cassini-rest-extension writes the rest section: what the Jakarta REST application exposes, as Cassini resolved it. It covers every Cassini stack mounted during the boot — the automatic mount configured by vidocq.rest.*, reported as vidocq.rest, and each declarative vidocq.http.mount.<name>.type=restful mount, reported by its name.

The summary line counts the resource classes, the routes and the providers, and says where they are mounted. The detailed report then lists the routes in match order — when two routes match a request, the one listed first wins — each with the resource class and method that handles it; then each mount, the resource classes, and the @Provider classes grouped by the contract they implement. A provider that is both a filter and an exception mapper is listed under both.

The MCP server of vidocq-runtime-it-langchain4j-cdi-mcp, -Dvidocq.startup.report=detailed
rest          REST (Cassini) | 0 ms
  1 resource class, 4 routes, 4 providers at /
  POST    /mcp/_listen  McpEndpoint#handleListen
  POST    /mcp          McpEndpoint#handlePost
  GET     /mcp          McpEndpoint#handleGet
  DELETE  /mcp          McpEndpoint#handleDelete
  mount vidocq.rest  listener default, prefix /, 4 routes
  resources          dev.langchain4j.cdi.mcp.server.transport.McpEndpoint
  request filters    dev.langchain4j.cdi.mcp.server.transport.McpListenRoutingFilter,
                     io.vidocq.cassini.cdi.vauban.VaubanRequestScopeFilter
  response filters   dev.langchain4j.cdi.mcp.server.transport.McpSseStreamHeadersFilter,
                     io.vidocq.cassini.cdi.vauban.VaubanRequestScopeFilter
  exception mappers  dev.langchain4j.cdi.mcp.server.transport.McpExceptionMapper

A route’s path is the one a client requests on the listener: the mount prefix, then the path Cassini matches. A mount declared with strip-prefix=false hands Cassini the full path, so its routes are printed as Cassini matches them, and its row says (not stripped). A sub-resource locator whose target class is only known when a request reaches it is printed with * as its method and names the locator itself.

The route table names handler classes, as every routing section of the report does; it is printed in the detailed report only, the summary keeps its one line. Every route is declared to the report at every level, so a later section finds the URL of its own resource: the mcp section’s endpoint row reads its URL there. The routes are printed as absolute URLs, joined with the listener the http section declares.

The section reads what Cassini computed when each stack was built, through CassiniStack.routes() (Reading the route table): it creates no resource or provider instance, and it keeps names only, never a class of the application, so a dev reload releases the previous application.

The rest panel in the dev console NEW

The dev console shows the section as the rest panel, with the boot facts above, and one card per mount below it, sampled about once a second from the counters each Cassini stack keeps (Reading live request figures):

Key Kind What it is

requests

counter

The requests the mount answered since the boot, whatever their status. Plotted as requests per second.

status.1xx to status.5xx

counter

The same, by status class. The Responses by status chart plots 2xx to 5xx per second.

in-flight

gauge

The requests handed to Cassini and not answered yet.

handler-time

counter, nanoseconds

The time spent answering, added up. Its rate is the share of wall time spent in handlers, added up over concurrent requests: 200 % reads as two requests being served at every instant, on average.

mean, max

duration

The mean and the longest time to answer one request since the boot; absent, no request served yet, before the first.

A request is timed from the moment Chappe hands it to Cassini to the moment Cassini has written its response: filters, the resource method, exception mappers and serialisation, not the network. A request no resource matches counts as the 404 it gets. The figures are totals for the whole mount: there is no breakdown per route.

Sampling sums a few counters and creates nothing. The counters are on in every stack Vidocq builds; they cost about 25 ns per request and allocate nothing, as Cassini’s BENCH.md measures. A stack built without them shows requests absent, counters off, never a zero.

Dirac metrics in the startup report and the dev console NEW

vidocq-runtime-dirac-metrics-extension writes the metrics section, titled Metrics (Dirac): what the three MicroProfile Metrics registries of Dirac hold. The dev console shows it as the metrics panel. Dirac itself needs no Vidocq code; the extension’s one class, DiracMetricsExtension, only reads what Dirac publishes.

Boot facts NEW

The summary line counts the metrics of each registry, gauges included: 3 application metrics, 13 base, 0 vendor. The base registry holds the gauges Dirac reads from the JVM. The detailed report then writes:

  • one row per registry, application, base and vendor: its counters, timers, histograms and gauges, such as 1 counter, 1 timer, 0 histograms, 1 gauge;

  • a list <registry> metrics for each registry that holds a counter, a timer or a histogram: the key each one has in the panel, its kind, then its name and tags, such as checkout.shop-eu, checkout.shop-eu.mean = timer checkout{shop=eu}. This is where a key the panel shortened or numbered reads back as the metric it stands for;

  • a list <registry> gauges: each gauge by its name and tags.

A metric name is not a valid key of the console, so the panel derives one, always the same for the same registry: the name, then each tag in the order of the tag names as .name-value; lowercased, every character other than a letter, a digit, a dot or a hyphen replaced with a hyphen, and m- in front when it does not start with a letter; cut to 40 characters, less 5 for a timer, whose mean needs .mean; and when two metrics end up with the same key, the second gets -2, the third -3. com.acme.Shop.placeOrder with the tag shop=EU is com.acme.shop.placeorder.shop-eu.

The metrics panel in the dev console NEW

One card per registry, application first, then base and vendor, sampled about once a second:

Key Kind What it is

counters, timers, histograms, gauges

gauge

How many metrics of that kind the registry holds.

<key> of a counter

counter

Its count. The page shows how much it grew since the previous poll.

<key> of a timer

counter

How many times it was timed.

<key>.mean of a timer

duration

Its mean: the time it recorded, added up, divided by its count; absent, no call yet, before the first.

<key> of a histogram

counter

How many values it recorded.

omitted

text

How many metrics of the card did not fit, such as 11 metrics left out: the console shows 64 values per group. Written only when some did not.

What is never shown:

  • A gauge’s value. A gauge’s getValue() runs application code, which may do I/O or wait on a lock. A gauge is counted and named in the boot facts, and never called, by the report or by the panel.

  • A timer’s longest time, or any percentile. Dirac’s Timer.getSnapshot() copies and sorts every value its reservoir holds, too much for a poll that should take well under a millisecond. The panel reads getCount() and getElapsedTime() only. A histogram shows its count for the same reason.

  • More than 64 values per card. The four counts and omitted leave 59 values for the metrics: counters first, then timers, two values each, then histograms, each kind in the order of its name and tags. Past them, the rest of the card is left out — a timer whole or not at all — and omitted says how many metrics were. The application’s metrics have a card of their own, so the JVM gauges of the base registry never push them out.

The section and the panel read the registries of the MetricRegistryProducerBean instance the application already has: the bean is resolved once when the extension starts, and each sample asks its context for the existing instance, which never creates one. Before anything asked Dirac for a registry or a metric, the section’s summary is registries not created yet and the panel shows a single registries value, absent, not created yet. A container with no Dirac producer, or with a second copy of Dirac loaded by the application layer, which the extension cannot read, shows no Dirac registry in this container.

Knock health checks in the startup report and the dev console NEW

vidocq-runtime-knock-health-extension writes the health section, titled Health (Knock): the health checks Knock registered, by probe, and the last answer of each. The dev console shows it as the health panel. Knock itself needs no Vidocq code; the extension’s one class, KnockHealthExtension, only reads what Knock keeps in memory.

A health check is application code, and may do I/O: neither the section nor the panel ever calls one. Knock remembers the last answer of each check as a probe request ran it, GET /health, /health/live, /health/ready or /health/started, and the panel shows that answer. A check that turns DOWN shows DOWN once the next probe request has run it, not before.

Boot facts NEW

The summary line counts the checks of each probe: 3 checks: 1 liveness, 2 readiness, 0 startup. Then:

  • a Health link to GET /health, at the path the rest section declared for Knock’s resource: /api/health when the application mounts its resources under /api, /health by default. The console shows it as a button that opens the endpoint, which runs every check;

  • in the detailed report, a list per probe that has checks, liveness, readiness and startup: each check by the name the panel shows it under, its simple class name, without the _ClientProxy suffix of a scoped bean’s proxy: AppLivenessCheck.

The health panel in the dev console NEW

One card per probe that has at least one check, liveness, then readiness and startup, sampled about once a second. A check with two probe qualifiers is in both cards: Knock runs it, and records its answer, for each.

Key Kind What it is

status

text

UP when every check of the probe that answered last answered UP, DOWN when one did; absent, never called, until a probe request runs one.

up, down

gauge

How many checks of the probe last answered UP and DOWN, out of its checks. The Checks UP / DOWN chart plots them over time. Written once a check has answered.

<key> of a check

gauge

1 when the check last answered UP, 0 when DOWN; absent, never called, until a probe request runs it.

<key>.at of a check

text

When it last answered, in the server’s time zone: 2026-09-23 17:22:35.

checks

table

Every check of the probe: the name it is shown under, the name its response carries, its last status, when it answered, and its data, pool=8, ok=true.

omitted

text

How many checks of the card have no <key> value, such as 11 checks omitted: the console shows 64 values per card, which leaves room for 29 checks. The counts and the table still cover every check. Written only when some did not fit.

The key of a check is derived from its name, always the same for the same checks: its words hyphenated and lowercased, AppLivenessCheck is app-liveness-check; every character other than a letter, a digit, a dot or a hyphen replaced with a hyphen, and c- in front when it does not start with a letter; cut to 37 characters, to leave room for .at; and when two checks end up with the same key, the second gets -2, the third -3.

What is never shown:

  • A check called by the console. Sampling reads Knock’s last results, in memory: a check that no probe request has run is never called, and a check that changed since the last request still shows its last answer.

  • An exception’s message. A check that throws answers DOWN under the simple name of the exception’s class, as /health serves it; the message, which may carry a secret, is never kept.

The section and the panel read Knock’s HealthCheckRegistry bean, resolved once when the extension starts: each sample asks its context for the existing instance, which never creates one. Knock creates the registry when it registers the first check; a container whose registry was never created has no check, and the section’s summary is no health check (registry not created yet), the panel a single checks value, absent, registry not created yet. A container without Knock shows no Knock registry in this container.

Mansart pools in the startup report and the dev console NEW

vidocq-runtime-mansart-pool-extension writes the section mansart-pool, Mansart pools, of the startup report, and the dev console shows it live, one card per pool. The pools open in beforeStart, before the container, so that the extensions after this one find them.

The unnamed pool, the one of vidocq.pool.url injected as @Default, is labelled @Default; named pools, vidocq.pool.<name>.url, follow under their name, in name order. A pool named default is never mistaken for the unnamed one.

Boot facts NEW

The summary names the pools and their total size: 2 pools (@Default, audit), 12 connections max, or idle: no vidocq.pool[.<name>].url when no pool is configured. The detailed report, which the console always has, adds for each pool:

Row Value

<label>

In a dev launch, the JDBC URL without its credentials; otherwise, the database kind only: h2 mem, postgresql.

<label> user

The user name, in a dev launch only.

<label> size

min idle 0 (boot only), max 8: minIdle is only honoured when the pool opens.

<label> timeouts

acquire PT5S, idle PT10M, lifetime PT30M.

<label> checks

The validation mode and timeout, and leak detection: validation PERIODIC PT1S, leaks off.

<label> xa

The XADataSource class, when XA is on.

<label> source

The dev service that provided the pool, and where it listens: dev service postgres, localhost:54213. In a dev launch only, when vidocq:dev marked the pool’s URL with vidocq.dev.provided.<key>=<provider id>.

<label> password

configured or not configured, never the password.

<label> url credentials

configured, when the URL itself carried credentials, which the URL row no longer shows.

<label> driver properties

The keys of the driver properties, never their values.

The H2 example launched in dev (the second pool shortened)
mansart-pool  Mansart pools | 1 ms
  2 pools (@Default, audit), 12 connections max
  @Default           jdbc:h2:mem:mansart-vidocq-demo;DB_CLOSE_DELAY=-1
  @Default user      sa
  @Default size      min idle 0 (boot only), max 8
  @Default timeouts  acquire PT5S, idle PT10M, lifetime PT30M
  @Default checks    validation PERIODIC PT1S, leaks off
  @Default xa        org.h2.jdbcx.JdbcDataSource
  @Default password  not configured
  audit              jdbc:h2:mem:mansart-vidocq-audit;DB_CLOSE_DELAY=-1
  ...

A URL loses its user info (//user:password@, Oracle’s user/password@) and every parameter or setting whose key holds password, passwd, pwd, secret, token or credential: jdbc:postgresql://audit:s3cret@db.example:5432/audit?sslpassword=k3y&ssl=true shows as jdbc:postgresql://db.example:5432/audit?ssl=true. The user info runs to the last @ before the first ? or ;, whatever the password holds, a /, a //, a `, an `=` or an `@`: `jdbc:oracle:thin:scott/Xy7//Qp4==@db:1521:orcl shows as jdbc:oracle:thin:@db:1521:orcl. A URL that cannot be read shows as jdbc:<sub-protocol>:…, and so does one whose password holds a ? or a ; — with one known exception: when what follows that ? or ; reads as a parameter whose key names a user, a mail or a secret (pa;pwd=x@host), the redactor takes it for a parameter and the first part of the password stays visible. Failing closed there would also hide ordinary URLs such as ;password=P@ss, so it is left as it is and documented. The error of xa=true for a URL whose XADataSource cannot be guessed quotes the URL the same way.

Live values NEW

One group per pool, read from the pool’s own counters, without a lock and without I/O:

Key Kind Value

active, idle

gauge, max maxSize

The connections lent, and those ready to lend.

waiting

gauge

The borrowers waiting for a connection, an estimate.

borrows, timeouts

counter

The borrows so far, and those that gave up after acquireTimeout.

leaks

counter

The connections held beyond leakDetectionThreshold; leak detection off while the threshold is PT0S, the default.

mean-borrow

duration

The mean borrow time over the pool’s whole life, waiting and opening a connection included; no borrow yet before the first. Shown as a number, never plotted.

Two charts per pool: Connections — active, idle stacked on it, waiting, under the dashed maxSize — and Throughput — borrows and timeouts per second.

Two readings of the gauges would be wrong:

  • maxSize - active - idle is not free capacity: a borrower that is still opening its connection counts in none of them.

  • The figures are not read at one instant: each is read on its own, so active + idle can dip, or briefly exceed what the pool holds, while a connection moves from one to the other.

Tables and a SQL editor NEW

Under mvn vidocq:dev, each pool also has a tab of the Mansart pools panel, named after it — @Default first, then the named pools in name order — whose list offers five actions. They answer in the dev console’s table of rows.

  • Tables lists the tables and views of the pool’s catalog and its schemas, the system ones (information_schema, pg_catalog…) left out, in schema then name order: schema, name, kind (TABLE, BASE TABLE, VIEW…) and number of columns, with a Describe and a Preview button that fill those forms with the table. The line says 12 tables, 2 views.

  • Describe takes one of the tables listed at boot: each column’s name, SQL type with its size (varchar(200), numeric(10,2)), whether it is nullable, its default, its place in the primary key, and the table.column a foreign key refers to; the line names its indexes, 9 columns · indexes: tasks_pkey (id), tasks_status_due_idx (status, due_date).

  • Preview shows the first rows of one, 100 unless you say from 1 to 1000: SELECT * FROM "public"."tasks", the table quoted as the database quotes a name, read in a transaction rolled back.

  • Query runs a statement that only reads — its first word, comments skipped, is SELECT, WITH, VALUES, SHOW, EXPLAIN or TABLE, anything else refused before it runs: Query only reads: use Execute — on a read-only connection, in a transaction always rolled back. It shows 100 rows at most; the line says 3 rows in 12 ms, or first 100 rows in 40 ms when there are more.

  • Execute runs any one statement, asked first (Runs this SQL on @Default. A DDL statement (CREATE, ALTER, DROP, TRUNCATE…) may be committed by the database itself whatever is chosen.), in a transaction that its Transaction list says to roll back, the default, or to commit; it is always rolled back when the statement fails. It answers the rows the statement returns, an UPDATE … RETURNING included, or how many it changed: 3 rows · rolled back. PostgreSQL rolls a CREATE TABLE back; most databases commit a DDL statement whatever is asked.

The form has two fields: sql, the statement, in the dev console’s query editor with the pool’s language (SQL), and params, its named parameters as a JSON object, whose schema follows the statement. A :name outside a string, a quoted name and a comment is bound with the member name of params, as setObject: a string, a number (an integer as a long), a boolean or null, and a list as a SQL array where the driver makes one (WHERE id = ANY(:ids)); a cast such as due::text is no parameter. On PostgreSQL a string is bound untyped, so that the database types it from where it is: {"day": "2026-10-01"} is a date in due_date < :day, and a time with its offset, a UUID or an enum constant work the same way; a parameter PostgreSQL cannot type, such as :p in :p IS NULL, makes the statement run again with its strings and its nulls bound as text. A … or E'…' string is read as a string, as '…' is. A parameter with no member is refused before anything runs, missing parameter id, and so is a text of two statements, one statement at a time; a last ; is fine.

SELECT t.title, p.name FROM tasks t JOIN projects p ON p.id = t.project_id WHERE t.status = :status
                                                                            params: {"status": "OPEN"}
UPDATE tasks SET priority = 'HIGH' WHERE id = :id RETURNING id, priority      params: {"id": 3}

Values. A number is shown as a number, but a numeric and an integer past 2^53 as their text, so that nothing is rounded; a date, a time or a timestamp as ISO text, with its offset when it has a zone (2026-10-01T12:00+02:00); binary as 0x and the hex of its first 64 bytes, … after when there are more; an array as the JSON of its elements; a text past 10 000 characters cut with …; anything else, a json, an interval, as the driver writes it.

Failures. A statement runs at most 30 seconds, then is cancelled: the statement ran past 30 s and was cancelled. A failing statement shows the database’s message, on one line, with the pool’s URL, its password and its user after the word user masked; Exchange holds the statement, its parameters, the transaction, the SQLState and the vendor code. A pool with no connection to lend answers once its acquireTimeout has passed. Every call takes a connection from the pool and gives it back as it was, its auto-commit and read-only flag restored, whatever happened.

The sql language. What the editor knows of the pool is read once per boot from DatabaseMetaData and offered as the language sql-<pool> — sql-default for @Default, a named pool’s name in lower case: SQL-92’s keywords and the product’s (getSQLKeywords()), the common functions, the database’s identifier quote and how it stores a name written without quotes; each table and view, bare in the current schema and schema.name in another, with its columns, their JSON type and <SQL type> · column, and a column of a single-column foreign key leading to the table it refers to. Past 1 MiB it is written without the details, then without the types; past it still, the pool has no language, which a WARNING says, and the editor colours the keywords it knows alone. A pool whose tables cannot be read at boot gets no language, no Describe and no Preview, which a WARNING says without its URL; Tables, Query and Execute still work. A table created later is listed by Tables but known to the editor, Describe and Preview after the next dev reload only.

MANSART-POOL-001 — pre-fill opened nothing NEW

A pool whose minIdle asks for connections at boot opened none of them. The pool gives up its pre-fill at the first failure, without a word, and opens connections on demand only, so the first requests pay for them, and fail if the cause is still there:

[MANSART-POOL-001] Pool 'ghost' opened none of the 1 connection its minIdle asks for at boot: the pool gave up at the first failure and opens connections on demand only Check vidocq.pool.ghost.url, the credentials and the JDBC driver on the module path

Check the URL, the credentials and the JDBC driver of that pool.

MANSART-POOL-002 — named pool without its bean NEW

A named pool is open, but no @Named("<name>") DataSource bean serves it: the bean is generated at compile time by vidocq-runtime-mansart-pool-datasources-codegen, which the application’s annotation processor paths lack. An injection or a repository routed to that pool fails on its first use:

[MANSART-POOL-002] Named pool 'ghost' is open, but no @Named("ghost") DataSource bean serves it: an injection or a repository routed to it fails on first use Add vidocq-runtime-mansart-pool-datasources-codegen to the annotationProcessorPaths of the application

When the container cannot say which beans it has, no pool is flagged: the report never guesses.

The Mansart Data catalogue in the startup report and the dev console NEW

vidocq-runtime-mansart-data-extension writes the section mansart-data, Mansart Data, of the startup report: what Mansart Data knows about the application, read once per boot from the CDI container and by reflection. Under mvn vidocq:dev, its companion vidocq-runtime-mansart-data-extension-dev shows it in the dev console as the Mansart Data tab, one card per entity with its repositories inside. Nothing here opens a connection or runs a query.

Repositories. Every @Repository interface a bean implements, once: the implementation mansart-data-processor generates and its interface count as one. The primary entity and the id type come from the type arguments of BasicRepository, CrudRepository or DataRepository, however deep in the super-interfaces; a repository without one is listed under Other repositories. Its declared methods, in name order:

Column Value

method

The method name.

kind

JDQL for @Query; @Find, @Insert, @Update, @Delete or @Save; derived for a name that starts with find, count, exists or delete and holds By; other otherwise.

query

The @Query text as written, cut after 1,000 characters; empty for the other kinds. The console shows the first 200.

parameters

name: Type, comma-separated. The name comes from @Param, else from the class file when the application is compiled with -parameters, else it is arg0, arg1…

returns

The generic return type in simple names: List<Task>, Optional<Task>, long, void.

The methods inherited from Jakarta Data are a value next to the table, task-repository.inherits: BasicRepository: delete, deleteAll, deleteById, findAll, findById, save, saveAll.

Entities. The primary entities of the repositories, each with the model Mansart itself uses, EntityModels.of: the generated _Entity.$MODEL, or the one Mansart builds at run time. The card shows its table, schema.table when it has a schema, and its columns, the id first, then the version, then the others in model order:

Column Value

field, column, type

The attribute, its column, the simple name of its Java type.

key

id, or id, generated; version; enum; → <Entity> for a reference; joined for an attribute reached through a relation; empty otherwise.

nullable, unique

yes, or empty.

The summary counts the entities, the repositories and the methods they declare: 2 entities, 2 repositories, 7 methods. The detailed report, which the console always has, adds a row per entity, its table and column count, then a row per repository, its entity and method count, the repositories without a primary entity last:

A tasks application (shape of the section)
mansart-data  Mansart Data
  2 entities, 2 repositories, 7 methods
  Task                 tasks, 10 columns
  TaskEvent            task_events, 5 columns
  TaskEventRepository  TaskEvent, 1 method
  TaskRepository       Task, 6 methods

Two entities, or two repositories, with the same simple name are shown under their full names. The catalogue lists at most 200 entities, 200 repositories and 200 methods per repository; the rest is counted, not listed: more entities and 3 more. The console keeps 64 cards and 100 rows per table.

The panel does not show the SQL of a derived method, which Mansart does not keep at run time, nor any statistic, nor Mansart persistence.

MANSART-DATA-001 — the model of an entity cannot be read NEW

Mansart could not build the model of a repository’s primary entity: its generated metamodel failed, or the entity has no id field, no no-arg constructor, or a package Mansart cannot open. The entity keeps its place in the catalogue, without its columns, and its card says unavailable: with the class of the exception. The exception’s message is never shown. The boot goes on, but the repositories of that entity will likely fail on their first call:

[MANSART-DATA-001] The model of entity com.acme.Invoice could not be read (io.vidocq.mansart.data.core.MansartDataException) Check its mapping annotations; Mansart could not build its model, so its repositories may fail too.

Check the entity’s jakarta.persistence annotations, its id and its constructor, and that its package is open to io.vidocq.mansart.data.core.

Running a repository method from the dev console NEW

Under mvn vidocq:dev, the Mansart Data tab of the dev console also runs the application’s repository methods against its database: a query to see what it returns, a write to try it, then roll it back or keep it. The tab keeps the catalogue (The Mansart Data catalogue in the startup report and the dev console NEW) under Monitoring, then shows one tab per repository, titled with its name, whose list offers its methods by name: every method it declares, plus the findById, findAll, save, deleteById and delete it inherits from Jakarta Data.

Arguments. A method’s arguments are one JSON object, one property per parameter, named as the catalogue names it (@Param, else the real name when the application is compiled with -parameters, else arg0, arg1…), all required. The page shows a form, an entity as a group of fields in it NEW: one per column, a column that cannot be null starred as required, the generated id and the version marked (generated), each field’s tooltip naming its column (The generated form). Its JSON switch shows the same arguments in the JSON editor, which completes an entity’s attributes and checks their types.

Java type JSON

String, char, Character

A string; one character for a char.

int, long, short, byte, their boxes, BigInteger

An integer; a fraction, or a value out of the type’s range, is refused.

double, float, their boxes, BigDecimal

A number; a BigDecimal keeps the digits as written.

boolean, Boolean

true or false.

An enum

The name of one of its constants.

LocalDate; LocalDateTime, Instant, OffsetDateTime, ZonedDateTime; LocalTime

ISO text: 2026-09-28; 2026-09-28T10:15:30 for a LocalDateTime, which has no zone, and 2026-09-28T10:15:30Z or 2026-09-28T10:15:30+02:00 for the three others; 10:15.

UUID

Its text.

An entity of a repository

An object, one property per column. Its schema NEW says required for a column that cannot be null and whose field is not a primitive, readOnly for the generated id and the version, and names each column in description (id of Gizmo, column gizmo_id for a reference); the console does not enforce it: an absent property keeps what the entity’s no-arg constructor sets, so that an absent generated id is generated. A reference to another entity takes that entity’s id. The entity is built through Mansart’s own model, as Mansart builds it from a row.

A boxed type takes null, a primitive does not. A value that does not convert fails the call before anything runs, with the parameter and why: dueDate: not an ISO date. A method with any other parameter type, such as PageRequest, Sort, Limit or a List, is listed under Monitoring in not-runnable, with the reason, and is not offered.

Writes. save, saveAll, a method whose name starts with insert, update or delete, one annotated @Insert, @Update, @Delete or @Save, and a @Query that starts with UPDATE or DELETE are writes. The page asks before running one (Runs TaskRepository.save against the database.), and its Transaction list offers rollback, the default, then commit. The console runs the call in a transaction of the application’s jakarta.transaction.TransactionManager, then rolls it back or commits it; an exception from the method always rolls it back. A rollback undoes only what was done on a connection enlisted in that transaction, which vidocq-runtime-mansart-transactions-extension sets up. An application without a TransactionManager bean gets commit only, and the call then runs as it would outside a transaction. Reads run as they are.

Results. The answer is JSON, shown by the page’s JSON viewer: an entity as an object, one property per column (a referenced entity as its id, a joined attribute left out), a List, Collection, Stream or array as an array of at most 100 elements, an Optional as its value or null. The line under the form says what came back and, for a write, what became of the transaction: 3 rows in 12 ms, first 100 rows in 40 ms, 1 row · rolled back, 42 · committed, done. Exchange holds the method, each argument with its type and value, and the transaction asked. A method that throws shows the class of its exception and its message, cut after 500 characters, with a user:password@ in it masked.

History. Each repository tab lists its last 20 calls of the boot, newest first, each with Replay, which fills the form again; nothing is sent until you run it. A dev reload starts a new history, and builds the tabs again from the new boot.

Limits. The console keeps 128 actions per panel: past them, Monitoring says and N more methods. A call longer than the console’s 60 seconds keeps running, and its outcome shows on a later poll. The repository’s package must be open to Vidocq — a Vidocq application is an open module by convention, or opens the package; otherwise its methods are listed as package <p> not open to Vidocq.

A JDQL console for Mansart Data NEW

Under mvn vidocq:dev, the Mansart Data tab of the dev console ends with a JDQL tab, which runs a JDQL statement you type against the application’s database: a query to look at data, an UPDATE or a DELETE to try a change, then roll it back or keep it. Its list offers two actions.

  • Query runs a FROM …, a projection SELECT a, b FROM …, a SELECT COUNT(this) FROM … or an aggregate such as SELECT MAX(price) FROM …, without asking first. An UPDATE or a DELETE is refused there: an UPDATE or DELETE: use Update / Delete; and when the application has a transaction manager, a query runs in a transaction that is always rolled back, so that nothing it could hold is kept.

  • Update / Delete runs an UPDATE … SET … or a DELETE FROM …, and nothing else (not an UPDATE or DELETE: use Query). The page asks first (Runs this JDQL statement against the database.), and its Transaction list offers rollback, the default, then commit, as a repository method’s write does (Running a repository method from the dev console NEW): the statement runs in a transaction of the application’s TransactionManager, rolled back unless commit is asked, and always rolled back when it fails. Without a TransactionManager, commit only.

The form has two fields of several lines: query, the statement, and params, its named parameters as a JSON object. The statement is typed in the dev console’s query editor NEW, which colours it, completes the entities after FROM and their attributes elsewhere, and underlines an unknown entity or attribute before anything runs (Query editor); params in a JSON editor whose schema follows the statement NEW: after WHERE status = :status, it offers status with the values of its enum, and flags it while it is missing (Its parameters).

FROM Task WHERE status = :status ORDER BY dueDate           params: {"status": "OPEN"}
SELECT title, priority FROM Task WHERE project = :project   params: {"project": "vidocq"}
SELECT COUNT(this) FROM Task WHERE dueDate < :day           params: {"day": "2026-10-01"}
UPDATE Task SET priority = :priority WHERE project = :p     params: {"priority": "HIGH", "p": "vidocq"}
DELETE FROM TaskEvent WHERE type = 'REOPENED'

The entity. A statement names its entity after FROM, UPDATE or DELETE FROM, by its simple name, or by its full class name when two entities share a simple name, as the catalogue then shows them. A name the catalogue does not know is refused, with the names it knows: unknown entity Nope; entities: Task, TaskEvent.

The jdql language NEW. What the query editor knows comes from the panel, built once per boot from the catalogue and Mansart’s models (A query language for a panel): the words of Mansart’s JDQL, then one entity per entity of the catalogue whose model could be read, under its simple name, with its table, and every attribute of its model but the joined ones, with the JSON type of its Java type and a detail such as Long · id, generated, String · column title or → Project · column project_id; a reference leads to its entity, so that project. completes a project’s attributes. An entity whose simple name another shares, which the tab refuses as ambiguous, is left out. Export CSV's statement is typed in the same editor.

Parameters. A named parameter :name takes the member name of params; a parameter with no value, or a member no parameter uses, is refused (:status: no value given), and so is a positional ?1. A value compared to an attribute — a parameter, a literal such as 'OPEN' or '2026-09-29', a value of SET — is converted to the attribute’s Java type: an enum by the name of a constant, a date or a time as ISO text, a number exactly (2.5 for an int is refused), a Boolean from true or false, a UUID from its text. A JSON array feeds an IN :names. A value that does not convert fails the statement before it runs: :status: not a TaskStatus.

Results. The entities as rows of a table NEW, one column per attribute as a repository method returns them (Running a repository method from the dev console NEW), each typed from its attribute (integer, string, date…); a projection’s columns, a column of entities typed object and each written as its JSON; at most 100 rows, the line saying 3 rows in 12 ms or first 100 rows in 40 ms (A table of rows, with JSON and Copy as CSV). A count is its number, 42; an aggregate its value, null when there is no row; a write the rows it changed, 4 rows · rolled back. Exchange holds the entity, the statement, its parameters and the transaction asked. A statement that does not parse, or fails, shows the class of the exception and its message, cut after 500 characters and with a user:password@ masked.

History. The JDQL tab lists its last 20 statements, Query or Update / Delete, newest first, each with Replay, which fills the form again without sending it.

Limits. JDQL has no INSERT: insert with a repository’s save, or an @Insert method (Running a repository method from the dev console NEW). A statement runs on the default data store, the RepositoryRuntime bean of the @Default datasource: a repository routed elsewhere with @Repository(dataStore = …) is not reached. Paths such as author.name go as far as Mansart’s JDQL does. The whole result is read before the first 100 rows are shown: narrow a large table with a WHERE. The console runs Mansart’s own JdqlExecutor.run(jdql, parameters, model, runtime), which a tool of yours may call too.

CSV export and import for Mansart Data NEW

The JDQL tab of the Mansart Data panel (A JDQL console for Mansart Data NEW) has two more actions, which move rows between the application’s database and a CSV file in your browser: Export CSV downloads the result of a query, Import CSV saves the rows of a file you paste or pick. Nothing is read from, or written to, the project’s directory.

Export CSV runs a JDQL query as Query does and answers its whole result as CSV, shown as text with a Download button, which saves it as jdql.export-20260929-143012.csv. FROM Task exports every attribute of every task; SELECT title, priority FROM Task WHERE project = :p some columns of some. An UPDATE or a DELETE is refused (an UPDATE or DELETE: export a query), and the query runs in a transaction that is always rolled back. The header is the entity’s attributes in model order, a joined attribute left out, or the columns of a projection; a count is one column count, an aggregate one column value. The line says 42 rows · 3.1 KiB in 12 ms. Every row is exported, but the file is at most 256 KiB in UTF-8: a larger one is refused, larger than 256 KiB: narrow the query, never cut. No more rows are read from the database than could fit, a SQL LIMIT, so a large table is never loaded whole to be refused; Query likewise reads one row more than the 100 it shows.

Import CSV takes an entity, picked in the list of the catalogue’s, and the CSV text: paste it, or Choose file to load a file of at most 60 KiB, read as UTF-8 or, when it is not, as Windows-1252 (the line under the input says so), its line ends kept while you leave it unedited. The page asks first (Saves these CSV rows against the database.). Its Transaction list offers rollback, the default, then commit: with rollback an import is a dry run, every row saved then the transaction rolled back, so that what goes through rollback goes through commit.

  1. The file is read completely before anything is saved. Its first line is the header: each name an attribute of the entity, once. A joined attribute, or a name the entity does not have, is refused with the attributes it has (header: unknown attribute colour; attributes: id, title, priority, dueDate), and so is an attribute of a type the console does not convert (header: attachment cannot be imported; …). Then at least one row, at most 5000 (blank lines at the end of the file do not count), each with as many fields as the header (line 9: 3 fields, the header has 4).

  2. Each row becomes an entity: its no-arg constructor, then each column set, its text converted by the attribute’s type as a repository method’s argument is (Running a repository method from the dev console NEW) — an enum by its constant’s name, a date or a time as ISO text, a number exactly, true or false, a UUID as its text, a reference by the referenced entity’s id. An attribute without a column keeps the constructor’s value; an empty field for a primitive is refused. The first row that does not convert stops the import, nothing saved: line 7, priority: no constant URGENT in Priority, the line counted in the text, the header being line 1.

  3. Every entity is then saved with Mansart’s RepositoryRuntime.save, in order, in one transaction. A row whose id is empty is inserted, its id generated; a row with an id updates that row, or inserts one with that id. An id of a primitive type, such as long, is never generated: the header must then name it (header: id is a long, which is never generated: give each row its id). Re-importing an edited export therefore updates the rows it holds; empty its ids to add copies. A save that fails rolls everything back: line 12: io.vidocq.mansart.data.core.MansartDataException: …, the message cut and a user:password@ masked. Without a TransactionManager, commit only, each row committed as it is saved: a failing row leaves the rows before it, and the line says so (· 11 rows before it stay committed).

The result says 42 rows saved · rolled back or · committed; Exchange holds the entity, the number of rows, the separator and the transaction, never the file.

The format is RFC 4180, written and read by the console itself:

  • fields separated by ,, or by ; as a spreadsheet of a decimal-comma locale expects: the separator of both actions; records ended by \r\n when written, by \r\n or \n when read, the last one with or without; a UTF-8 byte order mark at the start ignored;

  • a field holding the separator, a ", a line end, or nothing at all is quoted, a " doubled;

  • null is an empty field, a,,b, and the empty text "", a,"",b: an export and an import keep them apart;

  • a number as Java writes it, 2.50 whatever the separator (a decimal comma is refused: line 2, price: not a number), true or false, an enum by its name, a date or a time in ISO, a UUID as its text, a reference as the referenced entity’s id: the text an import reads back. An attribute of another type is exported as its toString(), and refused at import.

History. Both actions join the JDQL tab’s history, Export CSV and Import CSV; an export’s Replay fills its form again, an import’s is empty as soon as its call passes 4 KiB.

Limits. A file picked with Choose file is at most 60 KiB, and the whole request at most the console’s 64 KiB, which a CSV fills a little more than its size once sent as JSON: split a larger file. At most 5000 rows per import, 256 KiB per export: export a large table in slices, FROM Task WHERE id ⇐ 2000 ORDER BY id. The default data store only, and no joined attribute, as in the JDQL console.

Mansart Data writes join the transaction NEW

vidocq-runtime-mansart-transactions-extension now brings mansart-transactions-jdbc itself, as a compile dependency and a requires of its module descriptor, alongside mansart-transactions-cdi. Adding the extension is enough for @Transactional to cover Mansart Data writes — no second dependency to remember.

Mansart Data’s JDBC-to-JTA bridge (io.vidocq.mansart.transactions.jdbc.ConnectionXAResource) only enlists a repository’s JDBC connection in the active JTA transaction when that class can be loaded; mansart-data-cdi declares it an optional dependency. Before this fix, an application that depended on this extension alone did not have it on the module path: every repository call still took its own autocommit connection, so a @Transactional method began and rolled back a real JTA transaction, but no connection was ever enlisted in it — a write made before a thrown exception stayed committed, silently, with no anomaly in the startup report (Vidocq/vidocq#97).

<dependency>
    <groupId>io.vidocq.runtime.extensions.jakartaee.web</groupId>
    <artifactId>vidocq-runtime-mansart-transactions-extension</artifactId>
    <version>0.4.0-SNAPSHOT</version>
</dependency>

langchain4j-cdi MCP server NEW

vidocq-runtime-langchain4j-cdi-mcp-extension hosts a langchain4j-cdi MCP server on Vidocq: the @Tool, @Prompt, @Resource and @ResourceTemplate methods of the application’s CDI beans are served over the Model Context Protocol at /mcp, in its 2026-07-28 and 2025-03-26 revisions. One dependency brings everything the server needs.

The extension is not on Maven Central yet. It depends on langchain4j-cdi 1.4.0-SNAPSHOT, the version that carries the MCP 2026-07-28 work and declares provides org.mcpjava.server.spi.McpServerSPI in its module descriptor. Until langchain4j-cdi releases it, the extension is built in the Vidocq reactor and left out of Vidocq releases: build Vidocq with mvn install to use it.

<dependency>
    <groupId>io.vidocq.runtime.extensions.essentials</groupId>
    <artifactId>vidocq-runtime-langchain4j-cdi-mcp-extension</artifactId>
    <version>0.4.0-SNAPSHOT</version>
</dependency>

The application’s module-info.java still reads the MCP server, whose annotations and types it compiles against; mcp.server.api comes with it. It never requires the extension, which the runtime finds as a service.

module com.example.time {
    requires io.vidocq.runtime.core;
    requires dev.langchain4j.cdi.mcp.server;
}

Compile the application with -parameters, or name every argument with @ToolArg(name = …​), @PromptArg(name = …​) or @ResourceTemplateArg(name = …​): an argument without a name is advertised as arg0 and never bound (VIDOCQ-MCP-005 — arguments without a name NEW).

What the dependency brings NEW

Brought Replaces, in the application

langchain4j-cdi’s MCP server and its CDI 4.1 invoker, dev.langchain4j.cdi.mcp:langchain4j-cdi-mcp-server and langchain4j-cdi-mcp-invoker-cdi41

The two langchain4j-cdi dependencies.

The Cassini REST extension, with Chappe and the Champollion JSON-P and JSON-B providers, and the Jakarta REST 4.0, JSON-P 2.1 and JSON-B 3.0 APIs

parsson, yasson, and the Jakarta API versions the application pinned.

META-INF/vauban-beans.list, generated from the langchain4j-cdi jar when the extension is built

scanDependencies on vidocq:generate: the extension’s manifest declares the two jars, so the application’s own vidocq:generate scans them without configuration (see below), and an IDE launch without a Maven step still finds /mcp, its beans then created by reflection.

Where the server lives NEW

The extension’s module, io.vidocq.runtime.extensions.essentials.langchain4jcdi.mcp, is kept in the boot layer like every io.vidocq.runtime.extensions module, and it reads dev.langchain4j.cdi.mcp.server and the invoker. A module kept in the boot layer keeps what it reads, so the MCP server lives in the boot layer in every launch shape: vidocq:dev, vidocq:run, the generated launcher and a @VidocqMain trampoline started from the IDE. The application layer holds the application’s modules only, and the report’s layer section shows it.

The extension’s manifest asks the application’s vidocq:generate to scan the langchain4j-cdi jars it brings (Vidocq-Scan-Dependencies: dev.langchain4j.cdi.mcp:*) NEW: the server’s beans run generated code — created, injected and called by a _VaubanComponents in their own packages, behind build-time client proxies — instead of reflection, from an enriched copy of the jars that every launch uses. The JAX-RS providers among them carry @Provider alone and are beans only through Cassini’s build-compatible extension; vidocq:generate runs it too, so they are covered like the rest. See the vidocq:generate goal.

Two hand-made layouts break this:

  • The langchain4j-cdi jars in the -Dvidocq.app.path directory, next to the application. The JVM stops before main with FindException: Module dev.langchain4j.cdi.mcp.server not found, required by io.vidocq.runtime.extensions.essentials.langchain4jcdi.mcp. vidocq:package never builds this layout: dependencies go to lib/.

  • -Dvidocq.app.modules naming a dev.langchain4j.cdi.mcp.* module. langchain4j-cdi is then loaded a second time, in the application layer. The MCP calls still work, the mcp section reads loaded twice and VIDOCQ-MCP-003 — the server is loaded twice NEW is reported.

Configuration NEW

Key Default Meaning

vidocq.mcp.serverName

langchain4j-cdi

The server name advertised to clients (serverInfo of initialize).

vidocq.mcp.serverVersion

unknown

The server version advertised to clients.

vidocq.mcp.allowedOrigins

(empty)

Comma-separated Origin values the DNS rebinding protection accepts, or * for all. Empty accepts a present Origin only when its host and the Host header’s host are both loopback.

vidocq.mcp.mrtrMode

REPLAY

How a tool’s client interactions (elicitation, sampling, roots) are served to 2026-07-28 clients: REPLAY re-executes the call with the answers carried in an encrypted request state, CONTINUATION parks it until they arrive and needs sticky routing.

vidocq.mcp.requestStateSecret

(random per JVM)

The secret the REPLAY request state is encrypted and authenticated with (AES-256-GCM, key derived from it) NEW, at least 32 bytes in UTF-8. A client, a proxy or an access log can neither read the answers the request state carries nor alter them; the tool name and arguments travel in clear beside it. Set the same value on every instance that serves the same clients, use a random one (openssl rand -base64 32), not a passphrase, and rotate it from time to time. A secret: never printed.

vidocq.mcp.requestStateTtl

PT10M

How long a request state stays valid, ISO-8601.

vidocq.mcp.continuationTimeout

PT5M

How long a CONTINUATION waits for its answers, ISO-8601.

vidocq.mcp.cacheTtl

PT0S

The ttlMs caching hint of the cacheable 2026-07-28 results (the lists and resources/read), ISO-8601; PT0S means stale at once.

vidocq.mcp.cacheScope

public

The cacheScope hint of the same results: public (no user-specific data) or private.

With no key set, the extension adds nothing and the server keeps its defaults, or the application’s own @Named("mcp-server") McpServerConfig producer. With one key or more, the extension publishes the whole configuration as that bean; when the application produces one as well, the keys win (VIDOCQ-MCP-004 — keys and an application producer NEW). A value a key does not accept stops the boot with an error that names the key and what it accepts, never the value. A misspelled key under vidocq.mcp. is reported as VIDOCQ-CFG-003.

The mcp section of the startup report NEW

The summary line counts what the server serves, and the detailed report describes it. Without an McpEndpoint bean the summary reads not deployed; with langchain4j-cdi loaded twice, loaded twice and nothing else.

A trampoline launch with -Dvidocq.mcp.serverName=time-server -Dvidocq.mcp.serverVersion=1.2.3
mcp           MCP server (langchain4j-cdi) | 3 ms
  2 tools, 1 prompt, 0 resources, 1 resource template at http://localhost:8081/mcp
  endpoint            http://localhost:8081/mcp
  protocols           2026-07-28 (modern), 2025-03-26 (legacy)
  server              time-server 1.2.3 (vidocq.mcp.*)
  tools               convert_time, current_time
  prompts             plan_meeting
  resource templates  time://zone/{zone}
  invoker             CDI 4.1 invoker registered (built at the first call)
  mrtr                REPLAY, request state TTL PT10M
  requestStateSecret  not configured
  request state key   random per-JVM key (single instance only)
  allowed origins     loopback only
  cache hints         ttl PT0S, public

server says where the configuration comes from: vidocq.mcp.*, application bean or defaults. The request-state secret is only ever printed as configured or not configured; session ids, tool descriptions, schemas and headers are never printed. The section reads what the server holds in memory and creates none of its session beans; at the detailed level it also reads the configuration resolver.

The mcp panel in the dev console NEW

The mcp section is also a panel of the dev console: its boot facts stay, and seven values are read again on every poll, about once a second, from the counters the MCP server already keeps in memory — no lock, no I/O, nothing logged, and no bean created.

Key Kind Value, and what its absence says

sessions

gauge, count

The MCP sessions the server holds, from McpSessionManager. Absent no request served yet while that bean has no instance: the server creates it when it first answers.

streams

gauge, count

The SSE streams connected to the notification broadcaster. Absent not created yet before the server needs it.

listens

gauge, count

The open subscriptions/listen streams of 2026-07-28 clients. Absent not created yet before the server needs it.

pending

gauge, count

The requests the server has made of a client — an elicitation, a sampling, a roots/list — and is still waiting the answer of. Absent not created yet while a tool has asked the client for nothing, which is the usual reading of this panel.

invoker.methods

gauge, count

The MCP methods the CDI 4.1 invoker provider has an entry for.

invoker.matches

counter, count

The MCP methods that resolved to a container invoker.

invoker.misses

counter, count

The MCP methods that did not, and that the server invokes with Method.invoke.

The three invoker.* values come from one bean, so they are absent together, with the same reason: reflection when no invoker provider bean exists at all — the optional CDI 4.1 invoker module registered none, and the whole server invokes by reflection — and built at the first call when the bean exists but the container has not built it yet. When the MCP server is not in this container, every value is absent with no MCP server in this container, as the summary reads not deployed (VIDOCQ-MCP-002 — the server is not deployed NEW). A twin server (VIDOCQ-MCP-003 — the server is loaded twice NEW) leaves the panel inert: it samples nothing at all, as its section shows nothing.

Two charts: Connections — sessions as an area, streams and listens as lines — and Server-to-client requests — pending as a line. The three invoker values get no chart on purpose: they grow once per method and then stop, so a curve of them would read as traffic.

invoker.matches and invoker.misses count distinct methods, not calls. The provider counts one match the first time a given MCP method resolves to an invoker, so both totals stop growing once every method has been called once: a server whose four methods have each been called reads 4 after four calls and still 4 after four hundred. They answer "does this server invoke without reflection?", never "how busy is it?".

Here is a real server read through api/snapshot, once before it had served anything and once after six calls over four MCP methods:

Key Before the first request After six calls

sessions

absent, no request served yet

gauge 1

streams

gauge 0

gauge 0

listens

gauge 0

gauge 0

pending

absent, not created yet

absent, not created yet

invoker.methods

absent, built at the first call

gauge 4

invoker.matches

absent, built at the first call

counter 4

invoker.misses

absent, built at the first call

counter 0

Two traits of this panel are worth knowing, because both read as bugs until they are explained.

It never creates a bean. sample() holds the Bean metadata resolved once at onStart, and asks the bean’s Context for the instance it already has, with the single-argument Context.get, which returns null rather than creating one. Holding a client proxy obtained at boot and calling it on every poll would create the bean on the first poll of the page: for McpSessionManager that means starting its mcp-session-cleanup thread, once a second, on an application that has served no request and asked for nothing. A panel measures; it does not change what it measures.

A missing instance is absent with a reason, never a zero. A bean with no instance has no number to give, and a zero would claim a measure the panel does not have — "no session open" and "nobody has ever asked" are not the same fact. The reasons above are what the page shows in place of the number, greyed.

What reaches the page is a key, a kind, a number and a unit: no session id, no tool name, no header, and never the request-state secret, which stays what the report writes as configured or not configured.

The anomalies below are each one WARNING record on io.vidocq.startup.anomaly, logged at every level of the report (Anomalies). They cover what the extension makes possible or alone can see; langchain4j-cdi’s own failures stay in its logs.

The MCP inspector NEW

The mcp panel is also where the dev console calls the server’s own tools, prompts and resources: it publishes the URL of /mcp the startup report printed, the way its own mcp section does, so the panel can find it after a dev reload too, without a listener of its own. The panel builds nothing else and creates no bean beyond what its live values already read. See MCP inspector for what it shows and how a call is made.

VIDOCQ-MCP-001 — the MCP server module cannot serve from the boot layer NEW

The descriptor of dev.langchain4j.cdi.mcp.server does not declare provides org.mcpjava.server.spi.McpServerSPI, or the module is not open. In the boot layer the JDK ignores META-INF/services, so every tool or prompt that builds a ToolResponse, a PromptResponse or a TextContent fails:

[VIDOCQ-MCP-001] Module dev.langchain4j.cdi.mcp.server does not provide org.mcpjava.server.spi.McpServerSPI: tools and prompts that build a ToolResponse, a PromptResponse or a TextContent fail with "No McpServerSPI implementation found". Use a langchain4j-cdi version whose dev.langchain4j.cdi.mcp.server module is open and provides org.mcpjava.server.spi.McpServerSPI.

Use a langchain4j-cdi version that declares both.

VIDOCQ-MCP-002 — the server is not deployed NEW

The extension is present but no McpEndpoint bean exists, so /mcp answers 404. The bean list names the classes of the langchain4j-cdi version the extension was built with: an application that forces another langchain4j-cdi version can lose them. The section reads not deployed:

[VIDOCQ-MCP-002] The langchain4j-cdi MCP server extension is present but no McpEndpoint bean exists: /mcp is not served. Align the langchain4j-cdi version with the one this extension is built with: its bean list names the classes of that version.

VIDOCQ-MCP-003 — the server is loaded twice NEW

-Dvidocq.app.modules put a dev.langchain4j.cdi.mcp.* module in the application layer, so langchain4j-cdi is loaded twice: its beans come from the application layer while the extension reads the boot-layer copy. The calls still work; the section reads loaded twice and shows nothing else:

[VIDOCQ-MCP-003] langchain4j-cdi's MCP server is loaded twice: its beans come from the application layer while this extension links to the boot-layer copy, so the mcp section is left empty. Do not list dev.langchain4j.cdi.mcp.* modules in -Dvidocq.app.modules.

VIDOCQ-MCP-004 — keys and an application producer NEW

vidocq.mcp.* keys are set and the application also produces a @Named("mcp-server") McpServerConfig. The keys win:

[VIDOCQ-MCP-004] vidocq.mcp.* keys are set and the application also produces a @Named("mcp-server") McpServerConfig: the keys win and the application's producer is not used. Keep either the vidocq.mcp.* keys or the application's @Named("mcp-server") producer.

Keep one of the two.

VIDOCQ-MCP-005 — arguments without a name NEW

A tool, prompt or resource template argument has no explicit name and its class was compiled without -parameters: clients see it as arg0, and its value is never bound. One record names every method concerned:

[VIDOCQ-MCP-005] 2 MCP method(s) have parameters without a name (TimeTools#convertTime, TimeTools#currentTime): clients see them as arg0, arg1, ... and their values are never bound. Compile the application with -parameters, or name each argument with @ToolArg(name = ...), @PromptArg(name = ...) or @ResourceTemplateArg(name = ...).

Schema migration NEW

vidocq-runtime-migration-extension migrates each datasource at boot, in beforeStart, before the pools open, with the one backend on the path: vidocq-runtime-flyway-migration-extension or vidocq-runtime-liquibase-migration-extension. It migrates the @Default datasource whenever vidocq.pool.url is set, and a named datasource only when vidocq.migration.<name>.locations is set. A migration that fails stops the boot.

Configuration NEW

Key Default Description

vidocq.migration.enabled

true

false skips every migration.

vidocq.migration.engine

the one backend

flyway or liquibase, required only when both backends are on the path.

vidocq.migration.locations NEW

the backend’s

The locations of the @Default datasource, comma-separated: Flyway locations, classpath:db/migration by default, or the Liquibase changelog, db/changelog/db.changelog-master.xml by default. Read from every configuration source: set in vidocq.properties or as a -D, it used to stop the boot with a StringIndexOutOfBoundsException, and only the environment variable VIDOCQ_MIGRATION_LOCATIONS worked.

vidocq.migration.<name>.locations

—

Enrols the named datasource of vidocq.pool.<name>.url, with its locations.

vidocq.migration.failOnMissingLocations NEW

false

true stops the boot when a Flyway location holds no script (Unable to resolve location classpath:db/migration.), instead of migrating nothing. Off by default, so that an application can add the extension before its first script. Liquibase always stops on a missing changelog.

vidocq.migration.cleanDisabled NEW

true

false lets the dev console’s clean-and-migrate action drop every object in the schema of the @Default datasource (The migration panel and its actions NEW). Any other value keeps it refused. It is Flyway’s own cleanDisabled, passed on, and the switch Liquibase’s dropAll lacks. Nothing but that action ever cleans a schema.

vidocq.migration.<name>.cleanDisabled NEW

true

The same for the named datasource <name>.

The extension declares these keys, so a mistyped one, such as vidocq.migration.location, is reported as read by nothing NEW (VIDOCQ-CFG-003).

Scripts in the application layer NEW

vidocq:dev, vidocq:run and the launcher vidocq:package writes by default boot the application in a module layer of its own. Its class loader serves a script by name, but lists no directory. The backends used to scan classpath:db/migration with a loader, and found nothing there: Flyway logged No migrations found. Are your locations set up correctly?, the boot went on with an empty schema, and the first query failed with Table "…" not found. Liquibase stopped the boot, the changelog not found.

The backends now list classpath: locations from the application’s own modules, through ApplicationLayer (Listing the application’s files), and read each file by name:

  • Flyway: SQL scripts, SQL callbacks and Java migrations such as V3__Backfill.class, from a directory (vidocq:dev) or a jar (the app/ of a distribution). A script keeps the name Flyway’s own scanner gives it, so a schema history written by a flat launch, or through filesystem:, stays valid. Flyway instantiates a Java migration itself, so its package must stay open to it, as the opens db.migration; that vidocq:check-module-info requires.

  • Liquibase: the changelog, every file it includes, and the directories of includeAll.

A @VidocqMain trampoline re-layers the application in Vidocq.run(), so its IDE and flat launches take this path too. A launch that leaves the application module in the JVM’s boot layer keeps the backend’s own scanning: -Dvidocq.dev.layer=false or vidocq.package.layer=false for an application without a trampoline, a class-path launch, and the tests. Flyway locations that mix classpath: with filesystem: are left to Flyway’s scanner, which cannot list the classpath: ones in the application layer. The migration logs a warning saying so: keep the locations all classpath: or all filesystem:.

Liquibase analytics stay off NEW

Since 4.30, Liquibase sends usage data to Liquibase by default for open-source users (liquibase.analytics.enabled, with its settings fetched from config.liquibase.com). The extension runs every migration, listing and clean with that option off, unless a value is configured where Liquibase reads its configuration: -Dliquibase.analytics.enabled=true, LIQUIBASE_ANALYTICS_ENABLED, or a defaults file. A configured value, true or false, is kept as it is.

In the startup report NEW

The section migration, Schema migration, says what each datasource’s migration did:

Line Value

summary

The engine, then each datasource: flyway: default 2 applied, version 2, or default not migrated when nothing was found. With no datasource, idle: no vidocq.pool[.<name>].url; turned off, disabled: vidocq.migration.enabled=false.

<datasource>

In the detailed report: 0 applied, version 2. The version is the one the schema is at. On a restart with nothing left to run it is the current one NEW, and the log line says Migration done: datasource=default applied=0 version=2 where it used to say version=(none).

<datasource> locations

In the detailed report: where the datasource was migrated from, the backend’s default when no location is configured.

VIDOCQ-MIG-001 — no migration found NEW

A datasource’s migration found no migration at its locations, and its schema history records none: the schema was not migrated at all, and the first query on it will fail. It is a warning in every launch mode:

[VIDOCQ-MIG-001] datasource default: no migration found in [classpath:db/migration] and none in its schema history; the schema was not migrated Check vidocq.migration.locations and that the scripts are in the application

Check the key the hint names, and that the scripts are packaged with the application under that path. In a launch that leaves the application module in the boot layer (Scripts in the application layer NEW), also check that module-info.java opens their package (opens db.migration;). A schema whose history holds a migration, a repeatable one included, is never flagged, so an up-to-date restart is not an anomaly. vidocq.migration.failOnMissingLocations=true turns an empty Flyway location into a boot failure instead.

The migration panel and its actions NEW

The dev console shows the section as the migration panel, with the boot facts above and one card per migrated datasource below:

Key Kind What it is

version

text

The version the schema is at after the last run: 2, (none), or (liquibase), which numbers no version.

last-run

text

What ran last and how many migrations it applied: boot: 2 applied, migrate: 1 applied, clean-and-migrate: 3 applied, or migrate: failed (FlywayValidateException), by the exception’s class only, when an action failed.

clean

text

allowed, or disabled: vidocq.migration.cleanDisabled=false allows it, naming the key of that datasource.

applied, pending

table

The migrations the schema history records, the latest 100, and those still to run, the first 100: version, description, type, installed on and state, as Flyway’s info gives them. Liquibase numbers no version: a changeset’s id stands in the version column, its author in the description. Absent, listed in a dev launch only, outside a dev launch.

The migrations are listed once after the boot’s migration, in a dev launch only, and again after each action: listing opens a connection, and the page’s polls never do.

In a dev launch the panel offers two actions (the console runs them for its own page only), each on one migrated datasource, picked from a list:

  • Migrate now, migrate, applies the migrations added since the boot, as the boot does, without a restart: default: 1 migration applied, schema at version 3. Its outcome replaces the one the panel shows.

  • Clean and migrate, clean-and-migrate, asks first: Drop every object in the schema of the chosen datasource, then migrate it again? This cannot be undone. It then drops every table, view and sequence of the schema, its history included, with Flyway’s clean or Liquibase’s dropAll, and migrates it from scratch: default: schema cleaned, 3 migrations applied, schema at version 3. It is refused unless the datasource’s cleanDisabled key is false, and then drops nothing: default: clean refused, nothing dropped; set vidocq.migration.cleanDisabled=false to allow it. Flyway itself refuses too, since the extension passes it that same setting.

Actions on one datasource run one after the other. An action runs while the application runs: a query on the datasource at the same time may fail, which the developer who clicked accepts. The answer and the log line name the datasource and counts, never its URL or password. A backend written for an older SPI, without info or clean, shows its tables absent, flyway cannot list the migrations for instance, and answers clean-and-migrate with default: flyway cannot clean a schema, nothing dropped.

Activation by dependency

An extension activates as soon as it is on the module-path. No @EnableX annotation is required — the APT index detects the Java Modules provides VidocqExtension declaration.

<dependency>
    <groupId>io.vidocq.runtime.extensions.jakartaee.core</groupId>
    <artifactId>vidocq-runtime-cassini-rest-extension</artifactId>
    <version>0.4.0-SNAPSHOT</version>
</dependency>

Next steps

  • SPI — how to write a new extension

  • TCK status — coverage by extension

  • Concepts — extension vocabulary