Repository navigation
v2.7.0
Added
- The truncation boundary at
max_tools_per_serveris now pinned by tests at
limit - 1,limitandlimit + 1. Mutating the guard from>=to>—
which lets a server put one more tool in the catalog than the bound allows —
previously survived the whole oftests/test_client_manager.py. (#175)
Changed
- A policy file found by auto-discovery that parses but is not a valid policy
now terminates startup instead of being discarded. This can stop a gateway
that starts today, and that is the point: the discarded policy was replaced by
the defaultGatewayPolicy(), and that default is allow-all — every field
is adefault_factory, so a policy with one mistake in it did not degrade to a
partial policy, it degraded to no policy. Allow/deny lists, limits and
redaction all silently reverted to permissive behind a single warning line that
did not say so. The condition is exact: the file exists,yaml.safe_load/
json.loadsreturned without raising, and the result is not a valid
GatewayPolicy— which includes a list root, a scalar root and an empty YAML
file, all of which parse cleanly and fail only the object schema. A file the
parser rejects, or one that cannot be read at all, still warns and continues
as before: it could be a half-written file, an unrelated.jsonat the repo
root, or a merge conflict, and that fallback is deliberate. The surviving
warning now states plainly that no policy is in effect. Explicit--policy/
PMCP_POLICYis unchanged — it was already fatal for every mode. (#202) - The default policy search paths now follow the working directory. The two
project-local entries inDEFAULT_POLICY_PATHSwere joined withPath.cwd()
at module import, freezing the directory as of first import; a gateway that
changed directory before constructing itsPolicyManagerlooked for a policy
somewhere else, found none, and ran unrestricted — by that road with no warning
at all, since "no policy file" is legitimately silent. They are resolved at
construction now. The module attribute remains patchable for tests, and
absolute entries pass through unchanged. (#202) - A downstream tool that declares no
inputSchemais now skipped instead of
indexed. This is a behaviour change, and the only one in this set. The
indexer used to substitute{}for a missinginputSchema— and{}is not
"we do not know", it is "any arguments at all are valid", published under the
server's name to every caller and every model reading the catalog. MCP
requiresinputSchemaon a tool, so a tool without one is a tool we could not
read, and such an entry now takes the same route as any other unparseable
entry: skipped, logged, costing only itself. A listing in which no tool
parses is treated as a failed listing, so the server's previous tools are kept
rather than reported removed. An explicitly emptyinputSchema: {}is still
accepted — the server said "any arguments", and that is an answer; only the
absence, and any non-object value such asnull, is unreadable. A server that
omitsinputSchematherefore loses that tool from the catalog where it
previously appeared with a permissive schema. (#175)
Fixed
-
derive_npm_flags.py --verifyno longer reports host-enumerated npm config
types as table drift. The npm flag tables are the node-less fallback for
npm package identity, and--verifyis what keeps them honest against a real
npm — but it was green on one machine and red on another with identical npm
and identical source. npm buildslocal-address's declaredtypefrom
os.networkInterfaces(), so its 51 members here are facts about this
machine; and whennetworkInterfaces()throws, npm'sgetLocalAddresses()
catches it and returns exactly[null], which the member rule stripped to
nothing and reported asvalue: --local-address in table, absent from live npm. A drift check with false positives gets ignored, and an ignored check
is how real drift ships.Detection now happens in the node script, the only place the raw members
still exist — the serializer maps every string member to'<literal>', so no
Python-side predicate could tell 51 addresses fromloglevel's 8 fixed
words. It uses npm's owntypeDescription === 'IP Address'label (the one
signal that survives the[null]case) with anet.isIPmember scan as an
independent backstop, andclassify()then returnsvalueregardless of the
members. The flag is not exempted from the comparison: skipping it would
blind the check to a real arity change on the flag most likely to drift, so
--verifyreports which flags it normalised instead — without printing the
member count, which is the host fact. The committed tables are unchanged;
this fixes the comparison, not the data.Scope, so the next red
--verifyis not waved off as another false
positive: this makes the comparison logic host-independent, not the
tables' freshness. Version skew — tables derived from one npm, checked
against a newer one — still turns--verifyred, correctly and by design.
That is the signal the check exists to produce. What is gone is only the
redness that two machines running the same npm could disagree about. A CI
test now also holds the recorded schema fixture and the committed tables to
each other, so regenerating one without the other cannot pass silently.
(#193) -
_index_tools/_index_resources/_index_promptsno longer overstate the
catalog._index_resourcesdocumented that "the count returned is what was
actually indexed, not what was offered", while all three returned the length
of the parsed list. Two entries sharing an identity are two list items and one
catalog key, so the count was wrong by exactly the number of collisions. The
count is now of entries that actually landed, and each collision is logged at
DEBUG naming the id. (#175) -
adopt_processnow clears the server's catalog entries before indexing,
like every other path into the indexers. Adopting a server previously indexed
under the same name left the earlier listing's tools in the catalog beside the
new ones — entries the adopted process does not serve, still routable. (#175) -
A
max_tools_per_serverof0is no longer reported as a malformed
listing. A zero limit empties the parse result before any entry is examined,
so reconciliation announced "Every tools entry in the listing was
unparseable" — blaming the downstream for a decision the gateway's own policy
file made. Both that message and the parser's truncation warning now name the
limit. Deliberately not fixed by adding a schema bound:LimitsPolicystill
accepts0, because at the time policy auto-discovery swallowed validation
errors and fell back to an allow-all default, so rejecting the value would
have silently discarded the operator's entire policy file. #202 — see the
entry above, which ships in this same release — has since made that case
fatal, soField(ge=1)is now safe to add; it is left to a follow-up rather
than folded in here. (#175)