Skip to content

fix(mcp): every tool shares one persistent session per endpoint (#451) - #452

Merged
lxsaah merged 2 commits into
mainfrom
451-mcp-tools-share-persistent-session
Nov 9, 2026
Merged

lxsaah merged 2 commits into
mainfrom
451-mcp-tools-share-persistent-session

Conversation

@lxsaah

@lxsaah lxsaah commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Description

Most aimdb-mcp tools opened a new AimX connection on every call. Over serial that breaks the session get_record and drain_record share. A UART never closes, so a board learns of a new client only from a hello, and a hello resets its session. Each call either failed (the tty was already open) or reset the board and discarded the drain cursor. #450 fixed this for get_record and drain_record; this PR finishes it for everything else.

aimdb-mcp

  • Every tool uses the persistent per-endpoint client: list_records, set_record, get_instance_info, graph_nodes, graph_edges, graph_topo_order, get_buffer_metrics, reset_buffer_metrics, get_stage_profiling, reset_stage_profiling, and the records resource. Each takes the per-client lock for one call.
  • No second connection path remains. get_connection(), invalidate_connection(), invalidate_drain_client() and the per-tool fallbacks are removed. A tool called before the pool is initialized now returns an error, as get_record and drain_record already did.
  • get_drain_client() is now get_client(), since it is no longer drain-specific.
  • The rules from fix: record.get answers at once and no longer drains; MCP get/drain share one session (#444) #450 still apply: an error reply keeps the session, and the client is replaced only once its engine has stopped.

Not in the issue: parallel first calls

get_drain_client() released its lock while connecting, so two first calls on one endpoint both connected. On serial the loser found the tty busy, which becomes more likely once every tool shares this path. Each endpoint now has one connect in flight and the others wait for it, without holding up other endpoints.

aimdb-uds-connector

  • Startup warning for max_connections(1). A persistent client such as aimdb-mcp can occupy the only slot, and instance discovery's separate connection then fails. The default limit of 16 is not affected. Checked with a throwaway program and a counting logger: one warning at a limit of 1 (through the builder or from_config), none at 2 or the default. There is no regression test in the repo.

Not in this PR

  • get_buffer_metrics matches only the Rust type name, although its parameter is record_key; get_stage_profiling matches the name or the key. This is existing behaviour, and the new test passes a type name for that reason.

Tests

Test What it checks
tools/aimdb-mcp/tests/serial_session.rs: every_tool_shares_the_drain_session_over_serial A SerialServer board behind a pty. Open a ring's drain cursor, publish events, call each tool once, including error replies, then drain. Every call succeeds, the drain returns the queued events, and the board receives exactly one hello.
serial_session.rs: concurrent_first_calls_open_one_session Four parallel first calls all succeed, with one hello.
tools/aimdb-mcp/tests/persistent_client.rs: the_records_resource_uses_the_pool The records resource gets its client from the pool.

serial_session.rs runs on Linux only, so it compiles to zero tests on macOS. CI runs it through make test.

Each test fails when its bug is put back (Linux container, Rust 1.98):

Bug put back Result
list_records opens its own connection Tty busy
drain_record drops its client on an error reply Cursor lost: drain returns nothing
Connect not serialized per endpoint A parallel first call fails
Pool ignores engine state Stopped client is reused
Records resource connects directly Pool has no client

Related Issue

Checklist

  • I have read the CONTRIBUTING.md document.
  • My code follows the project's coding standards.
  • I have added tests to cover my changes.
  • All new and existing tests passed (make check). Left to CI. Run locally:
    • aimdb-mcp tests on macOS, and fmt and clippy for aimdb-mcp with transport-serial on macOS and Linux;
    • the reintroduced-bug runs above on Linux.
    • A complete run of the final tree on Linux was not finished locally: CI is the first one.
  • I have updated the documentation accordingly (doc comments, changelogs).

🤖 Generated with Claude Code

lxsaah and others added 2 commits November 9, 2026 21:31
Most aimdb-mcp tools opened a new AimX connection per call. Over serial that
either failed (the tty is already open) or sent a new hello, which reset the
board's session and discarded the drain cursor drain_record reads.

- All tools and the records resource go through ConnectionPool::get_client
  (renamed from get_drain_client) and hold the per-client lock for one call.
- get_connection, invalidate_connection, invalidate_drain_client and the
  per-tool fallbacks are gone, so no path opens a second connection.
- Each endpoint has one connect in flight: parallel first calls wait for it
  instead of racing, where the loser found the tty busy.

Tests: serial_session calls every tool between opening a ring's drain cursor
and draining it, and checks the queued events and exactly one hello. New tests
cover parallel first calls and the records resource.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A persistent client such as aimdb-mcp can occupy the only slot, and instance
discovery's separate connection then fails. The default limit of 16 is not
affected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@lxsaah
lxsaah merged commit d9c7be0 into main Nov 9, 2026
10 checks passed
@lxsaah
lxsaah deleted the 451-mcp-tools-share-persistent-session branch November 9, 2026 21:11
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.

aimdb-mcp tools open a new AimX connection per call, which breaks the serial session

1 participant