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 |
|
Source |
|
Catalogue
| Extension | Aggregator | Role |
|---|---|---|
|
|
Wires Chappe as HTTP/1.1, H2, H3 transport. Configures bind via |
|
|
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 |
|
|
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). |
|
|
Flyway-based schema migrations. |
|
|
Liquibase-based schema migrations. Liquibase’s usage analytics stay off unless you configure them (Liquibase analytics stay off NEW). |
|
|
Hosts a langchain4j-cdi MCP server: one dependency brings the server, its CDI 4.1 invoker, Jakarta REST and the server’s bean list; |
|
|
Wires Cassini for Jakarta REST 4.0. Static routing generated at build (companion |
|
|
JDBC connection pooling via Mansart Pool (companion |
|
|
Jakarta Data 1.0 via Mansart — scans |
|
|
Handles |
|
|
https://microprofile.io/specifications/microprofile-config/ via Ravel — external sources, profiles, typed conversion. Companion |
|
|
https://microprofile.io/specifications/microprofile-health/ via Knock — |
|
|
https://microprofile.io/specifications/microprofile-metrics/ via Dirac — Prometheus endpoint. Its |
|
|
https://microprofile.io/specifications/microprofile-open-api/ via Grimm — document generated at compile time ( |
|
|
MicroProfile JWT Auth via Cervantes. |
|
|
MicroProfile REST Client via Cyrano. Companion |
|
|
MicroProfile Telemetry via Humboldt — traces, metrics, logs, OTLP/HTTP export. The extension brings the Jakarta REST API NEW, which |
|
|
MicroProfile Fault Tolerance via Heisenberg — |
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 |
|---|---|---|---|
|
|
100 |
installs |
|
Contributors (REST, Servlet) |
500–9999 |
call |
|
|
10000 |
starts a |
|
|
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 |
|---|---|
|
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. |
|
Log the |
|
How long stopping the server waits for the requests in flight. |
|
Called on the boot thread once the server listens, with the address it bound: the real port when the declared one was |
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.
vidocq-runtime-it-langchain4j-cdi-mcp, -Dvidocq.startup.report=detailedrest 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 |
|---|---|---|
|
counter |
The requests the mount answered since the boot, whatever their status. Plotted as requests per second. |
|
counter |
The same, by status class. The Responses by status chart plots |
|
gauge |
The requests handed to Cassini and not answered yet. |
|
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. |
|
duration |
The mean and the longest time to answer one request since the boot; absent, |
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,baseandvendor: its counters, timers, histograms and gauges, such as1 counter, 1 timer, 0 histograms, 1 gauge; -
a list
<registry> metricsfor 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 ascheckout.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 |
|---|---|---|
|
gauge |
How many metrics of that kind the registry holds. |
|
counter |
Its count. The page shows how much it grew since the previous poll. |
|
counter |
How many times it was timed. |
|
duration |
Its mean: the time it recorded, added up, divided by its count; absent, |
|
counter |
How many values it recorded. |
|
text |
How many metrics of the card did not fit, such as |
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 readsgetCount()andgetElapsedTime()only. A histogram shows its count for the same reason. -
More than 64 values per card. The four counts and
omittedleave 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 — andomittedsays 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
Healthlink toGET /health, at the path therestsection declared for Knock’s resource:/api/healthwhen the application mounts its resources under/api,/healthby 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,readinessandstartup: each check by the name the panel shows it under, its simple class name, without the_ClientProxysuffix 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 |
|---|---|---|
|
text |
|
|
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. |
|
gauge |
1 when the check last answered UP, 0 when DOWN; absent, |
|
text |
When it last answered, in the server’s time zone: |
|
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, |
|
text |
How many checks of the card have no |
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
/healthserves 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 |
|---|---|
|
In a |
|
The user name, in a |
|
|
|
|
|
The validation mode and timeout, and leak detection: |
|
The |
|
The dev service that provided the pool, and where it listens: |
|
|
|
|
|
The keys of the driver properties, never their values. |
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 |
|---|---|---|
|
gauge, max |
The connections lent, and those ready to lend. |
|
gauge |
The borrowers waiting for a connection, an estimate. |
|
counter |
The borrows so far, and those that gave up after |
|
counter |
The connections held beyond |
|
duration |
The mean borrow time over the pool’s whole life, waiting and opening a connection included; |
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 - idleis 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 + idlecan 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 says12 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 thetable.columna 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,EXPLAINorTABLE, 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 says3 rows in 12 ms, orfirst 100 rows in 40 mswhen 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, anUPDATE … RETURNINGincluded, or how many it changed:3 rows · rolled back. PostgreSQL rolls aCREATE TABLEback; 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 |
|---|---|
|
The method name. |
|
|
|
The |
|
|
|
The generic return type in simple names: |
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 |
|---|---|
|
The attribute, its column, the simple name of its Java type. |
|
|
|
|
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:
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 |
|---|---|
|
A string; one character for a |
|
An integer; a fraction, or a value out of the type’s range, is refused. |
|
A number; a |
|
|
An |
The name of one of its constants. |
|
ISO text: |
|
Its text. |
An entity of a repository |
An object, one property per column. Its schema NEW says |
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 projectionSELECT a, b FROM …, aSELECT COUNT(this) FROM …or an aggregate such asSELECT MAX(price) FROM …, without asking first. AnUPDATEor aDELETEis 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 aDELETE 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 offersrollback, the default, thencommit, 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’sTransactionManager, rolled back unlesscommitis asked, and always rolled back when it fails. Without aTransactionManager,commitonly.
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.
-
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). -
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,
trueorfalse, aUUIDas 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. -
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 aslong, 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 auser:password@masked. Without aTransactionManager,commitonly, 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\nwhen written, by\r\nor\nwhen 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; -
nullis 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.50whatever the separator (a decimal comma is refused:line 2, price: not a number),trueorfalse, an enum by its name, a date or a time in ISO, aUUIDas its text, a reference as the referenced entity’s id: the text an import reads back. An attribute of another type is exported as itstoString(), 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 |
<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, |
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 |
|
|
|
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.pathdirectory, next to the application. The JVM stops beforemainwithFindException: Module dev.langchain4j.cdi.mcp.server not found, required by io.vidocq.runtime.extensions.essentials.langchain4jcdi.mcp.vidocq:packagenever builds this layout: dependencies go tolib/. -
-Dvidocq.app.modulesnaming adev.langchain4j.cdi.mcp.*module. langchain4j-cdi is then loaded a second time, in the application layer. The MCP calls still work, themcpsection readsloaded twiceand VIDOCQ-MCP-003 — the server is loaded twice NEW is reported.
Configuration NEW
| Key | Default | Meaning |
|---|---|---|
|
|
The server name advertised to clients ( |
|
|
The server version advertised to clients. |
|
(empty) |
Comma-separated |
|
|
How a tool’s client interactions (elicitation, sampling, roots) are served to |
|
(random per JVM) |
The secret the |
|
|
How long a request state stays valid, ISO-8601. |
|
|
How long a |
|
|
The |
|
|
The |
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.
-Dvidocq.mcp.serverName=time-server -Dvidocq.mcp.serverVersion=1.2.3mcp 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 |
|---|---|---|
|
gauge, count |
The MCP sessions the server holds, from |
|
gauge, count |
The SSE streams connected to the notification broadcaster. Absent |
|
gauge, count |
The open |
|
gauge, count |
The requests the server has made of a client — an elicitation, a sampling, a |
|
gauge, count |
The MCP methods the CDI 4.1 invoker provider has an entry for. |
|
counter, count |
The MCP methods that resolved to a container invoker. |
|
counter, count |
The MCP methods that did not, and that the server invokes with |
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.
|
|
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 |
|---|---|---|
|
absent, |
gauge |
|
gauge |
gauge |
|
gauge |
gauge |
|
absent, |
absent, |
|
absent, |
gauge |
|
absent, |
counter |
|
absent, |
counter |
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 |
|---|---|---|
|
|
|
|
the one backend |
|
|
the backend’s |
The locations of the |
|
— |
Enrols the named datasource of |
|
|
|
|
|
|
|
|
The same for the named datasource |
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 (theapp/of a distribution). A script keeps the name Flyway’s own scanner gives it, so a schema history written by a flat launch, or throughfilesystem:, stays valid. Flyway instantiates a Java migration itself, so its package must stay open to it, as theopens db.migration;thatvidocq:check-module-inforequires. -
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: |
|
In the detailed report: |
|
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 |
|---|---|---|
|
text |
The version the schema is at after the last run: |
|
text |
What ran last and how many migrations it applied: |
|
text |
|
|
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 |
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’scleanor Liquibase’sdropAll, and migrates it from scratch:default: schema cleaned, 3 migrations applied, schema at version 3. It is refused unless the datasource’scleanDisabledkey isfalse, 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