A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
A host can now contribute tools for one session through
McpAttachOptions.dynamicTools, consulted once per attach() with that
session's ctx.
It belongs here rather than in the host calling server.registerTool()
itself, because attach() is where the onInvoke seam fires. Anything
registered outside it is invisible to the tool-call audit trail, and
handlers that run as the calling user are the last thing that should sit
in an audit blind spot.
The source is synchronous: attach() runs inside the initialize request,
so an awaited source would put a third party's round trip in front of
every session creation.
Two properties carry the weight, both security ones:
- Names are reserved against every registered entry, not against what
this session was granted. Reserving against the session would mean a
token scoped to trips:read has no create_trip attached, so a
contributed tool could take that name for exactly the callers holding
no scope for it.
- Nothing a contributor does can make attach() throw. Hosts call it
outside their request try block, so an escape is a 500 on every
initialize and one bad contributor takes MCP down instance-wide. The
source is contained once, each entry again. Both live throw sites are
covered: the SDK duplicate-name throw, and the policy-less declarative
access throw that registered entries only avoid because validate()
pre-checks them at boot.
ToolOptions.inputSchema widens to ZodRawShape | McpZodSchema. The SDK has
accepted either since it normalises both to one object schema, and a
whole schema is what lets a host advertise one it did not author (via
.meta()) without converting it to Zod first. The arms are disjoint, so
nothing else moves; one test cast in decorators.test.ts records which arm
that decorator declared.
DYNTOOL-001 to DYNTOOL-014.