Skip to content

Import an MCP Toolbox configuration into a semantic model - #444

Draft
libei wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
libei:toolbox-import-rebased
Draft

libei wants to merge 1 commit into
GoogleCloudPlatform:mainfrom
libei:toolbox-import-rebased

Conversation

@libei

@libei libei commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Adds kcmd toolbox import <file|dir>: reads an MCP Toolbox for Databases
configuration and reconstructs the semantic model its tools imply.

Why it is not a one-to-one mapping

A Toolbox tools.yaml is a list of callable things. It has verbs and no
nouns — in the published quickstart the word hotels never appears as a thing,
only inside SQL, five times, as a table somebody happened to query. A semantic
model is a list of things that are true, with the callable surface derived
from them.

So the import cannot map verb to verb. It parses every statement and
reconstructs the nouns out of the SQL inside the verbs:

in the configuration in the model
a table named in any statement an entity, bound to that table
a column attributed to a table a field, expression = the column
a join equality in a SELECT a relationship, oriented by the foreign key
an INSERT/UPDATE/DELETE tool an action with a sql executor
the tool's parameters action parameters, rebound to @name, carrying description / required / default
the DML target, verb and columns the action's affects
the source's coordinates the entity source, and the model's deployment_target one level up
a SELECT tool nothing

The last row is the point: a lookup over an entity is generated from the model,
not stored in it. The AlloyDB quickstart's four tools import to two actions and
three lookups nobody wrote, and kcmd agent tools lists all five.

Notes on the mapping

affects is derived, not trusted. The Toolbox annotations say only
destructive; the statement says which table, which verb and which columns,
and id is excluded because the statement reads it to find the row rather than
changing it.

Positional placeholders are rebound by index, not by order of appearance —
the quickstart's SET checkin_date = CAST($2 ...), checkout_date = CAST($3 ...) WHERE id = $1 against parameters hotel_id, checkin_date, checkout_date would
otherwise bind the row's identity to a date.

Join direction is decided from evidence in the text: a column naming the other
table is the foreign-key side. Where nothing in the text decides it, the written
order is kept and a warning says so.

Parameter metadata rides on what #442 added, and the description is the string
that mattered — it is what the agent reads to decide what to put in the
argument, and it now reaches the derived tool listing verbatim. Toolbox's
required is nil-defaults-to-true, the opposite of how an absent flag
usually reads, so it is written out explicitly rather than passed on as an
ambiguity; a defaulted parameter states its default and not its necessity,
since push rejects required: true beside a default.

deployment_target is written from the source coordinates, which is what
separates a model that loads from a model that runs — kcmd asks for one before
it offers an agent any tool. A configuration naming two databases gets none and
a warning naming both.

What a configuration cannot say

Reported as warnings rather than guessed at, because each is a real difference
between the formats and not an unfinished corner:

  • no primary key — nothing states one; join columns are named as candidates
    and no key is written, since a wrong key is worse than an absent one
  • no value restriction on a parameterallowedValues is a rule about the
    argument, and a rule belongs in a constraint guarding the action, which the
    warning says; likewise a parameter filled from a verified claim imports as an
    ordinary one, which is a real loosening and is called out
  • no toolsets — no construct for a named subset of the surface
  • no templated statementstemplateParameters are interpolated before the
    statement is prepared, so the blast radius cannot be declared ahead of the
    call; those tools are skipped
  • nothing non-SQL — a tool wrapping an API has no statement to read an
    entity out of

A non-Google source (plain postgres, mysql) produces a model that loads,
validates and publishes to Knowledge Catalog but binds to no store. The import
says so at import time rather than leaving it to the first call.

Tests

Both published Toolbox quickstart configurations are the fixtures, unedited,
so the tests say what a real configuration yields rather than what a
purpose-written one does. 30 tests cover the converter and assert the output
loads via loadModels and passes validatePushRequirements; 1003 pass across
the semantic suite.

osi_schema.test.ts skips the new fixture directory — those are converter input
in a foreign format, the way the OWL inputs are Turtle.

A Toolbox `tools.yaml` is a list of callable things: it has verbs and no
nouns. The word `hotels` never appears in the quickstart config as a thing,
only inside SQL, five times, as a table somebody happened to query. So the
import cannot map verb to verb -- it reconstructs the nouns from the SQL
inside the verbs.

`kcmd toolbox import <file|dir>` parses every statement and reads out:

  a table named anywhere        -> an entity, bound to that table
  a column attributed to one    -> a field, expression = the column
  a join equality in a SELECT   -> a relationship, oriented by the FK
  an INSERT/UPDATE/DELETE tool  -> an action with a `sql` executor
  the tool's parameters         -> action parameters, rebound to @name,
                                   carrying description/required/default
  the DML target/verb/columns   -> the action's `affects`
  the source coordinates        -> the entity `source`, and the model's
                                   `deployment_target` one level up
  a SELECT tool                 -> nothing

That last one is the point: a lookup over an entity is generated from the
model, not stored in it. The AlloyDB quickstart's four tools import to two
actions and three lookups nobody wrote.

`affects` is derived rather than trusted -- the Toolbox annotations say only
"destructive", while the statement says which table, which verb and which
columns, and excludes the `id` it matched the row on.

Parameter metadata rides on what GoogleCloudPlatform#442 added. Toolbox's `required` is
nil-defaults-to-TRUE, the opposite of how an absent flag usually reads, so it
is written out explicitly rather than passed on as an ambiguity; a defaulted
parameter states its default and not its necessity, since push rejects
`required: true` beside a `default`.

Both published quickstart configs are the fixtures, unedited, so the tests
say what a real configuration yields. Both the flat form and the legacy
nested form parse to byte-identical output.

What a configuration cannot say is pinned as warnings rather than left to be
discovered: no primary key (join columns are named as candidates and nothing
is written), no value restrictions on a parameter (they belong in a constraint
guarding the action, which the warning says), no toolsets, no templated
statements (interpolated before the statement is prepared, so the blast radius
cannot be declared), and nothing for a tool that wraps an API instead of SQL.
A non-Google source binds to no store, which the import states at import time.

osi_schema.test.ts skips the new fixture directory: those are converter input
in a foreign format, the way the OWL inputs are Turtle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant