Skip to content

v2.7.0

Choose a tag to compare

@github-actions github-actions released this 29 Aug 19:26
· 98 commits to main since this release
v2.7.0
4fdae8e

Added

  • The truncation boundary at max_tools_per_server is now pinned by tests at
    limit - 1, limit and limit + 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 of tests/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 default GatewayPolicy(), and that default is allow-all — every field
    is a default_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.loads returned 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 .json at 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_POLICY is unchanged — it was already fatal for every mode. (#202)
  • The default policy search paths now follow the working directory. The two
    project-local entries in DEFAULT_POLICY_PATHS were joined with Path.cwd()
    at module import, freezing the directory as of first import; a gateway that
    changed directory before constructing its PolicyManager looked 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 inputSchema is now skipped instead of
    indexed.
    This is a behaviour change, and the only one in this set. The
    indexer used to substitute {} for a missing inputSchema — 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
    requires inputSchema on 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 empty inputSchema: {} is still
    accepted — the server said "any arguments", and that is an answer; only the
    absence, and any non-object value such as null, is unreadable. A server that
    omits inputSchema therefore loses that tool from the catalog where it
    previously appeared with a permissive schema. (#175)

Fixed

  • derive_npm_flags.py --verify no longer reports host-enumerated npm config
    types as table drift.
    The npm flag tables are the node-less fallback for
    npm package identity, and --verify is 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 builds local-address's declared type from
    os.networkInterfaces(), so its 51 members here are facts about this
    machine; and when networkInterfaces() throws, npm's getLocalAddresses()
    catches it and returns exactly [null], which the member rule stripped to
    nothing and reported as value: --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 from loglevel's 8 fixed
    words. It uses npm's own typeDescription === 'IP Address' label (the one
    signal that survives the [null] case) with a net.isIP member scan as an
    independent backstop, and classify() then returns value regardless 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
    --verify reports 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 --verify is 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 --verify red, 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_prompts no longer overstate the
    catalog.
    _index_resources documented 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_process now 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_server of 0 is 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: LimitsPolicy still
    accepts 0, 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, so Field(ge=1) is now safe to add; it is left to a follow-up rather
    than folded in here. (#175)