Skip to content

v2.2.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 14:35
· 60 commits to dev since this release

Added

  • RoselineMCP:ConfirmDestructiveWritesTimeout (default 300000 ms, 5 minutes) — bounds how
    long the write-confirmation elicitation waits for the client's answer. Set it via
    appsettings.json or ROSELINE_RoselineMCP__ConfirmDestructiveWritesTimeout=<ms>; 0 or less
    removes the bound and restores the previous, indefinite wait. This changes default behavior:
    a client that advertises elicitation support, accepts the confirmation request and then never
    answers — a CI job, a headless agent, a human who walked away — used to block apply_fixes /
    edit_member / rename_symbol forever, because RoselineMCP:DefaultTimeout is an analysis
    budget and by construction does not apply to the human round-trip. The server now stops
    waiting after the timeout — note that a client whose elicitation handler never returns may still
    not read the response, since the SDK's client dispatches server-initiated requests on its read
    loop; the bound is on RoselineMCP's side of the wire. It returns a preview, not a write:
    silence is not consent, so previewOnly
    comes back true and notes[] explains that the confirmation timed out. Writing without a human
    remains an explicit operator decision (ConfirmDestructiveWrites=false), so the security posture
    is no weaker than before — only the hang is gone. The clock is deliberately separate from
    DefaultTimeout: a human reading a real diff may legitimately exceed an analysis budget.
  • RoselineMCP:ConfirmDestructiveWrites (default true) — an operator switch that turns off
    the write-confirmation elicitation. The write tools (ApplyFixes, EditMember, RenameSymbol)
    ask the connected client to confirm before writing when the caller passed previewOnly: false;
    on an unattended host that prompt is not a second guard but a stop, because MCP elicitation is a
    separate channel from tool permissions and no client-side setting can pre-answer it — so
    claude -p runs, CI jobs and overnight agent loops blocked on a human keypress. Set it via
    appsettings.json or ROSELINE_RoselineMCP__ConfirmDestructiveWrites=false and no elicitation
    is sent at all (rather than one being auto-accepted); the explicit previewOnly: false opt-in
    then stands as the only guard before a write — see SECURITY.md. Interactive installs are
    unaffected: leave it at its default and behavior is unchanged. The server logs a warning at
    startup
    when the switch is off, so a gate-off deployment is identifiable from its stderr rather
    than being indistinguishable from a confirmed one.

Fixed

  • A write confirmation that times out can no longer be read as consent when the SDK reports the
    abandoned prompt as something other than a cancellation.
    The confirmation gate downgraded to a
    preview only on OperationCanceledException; every other exception fell through to a catch-all
    meaning "this client cannot elicit — honor the explicit opt-in" and returned proceed. Cancelling
    an in-flight JSON-RPC request is not guaranteed to surface as an OCE, so a transport or protocol
    exception raised by our own deadline would have written to disk on a confirmation nobody
    answered
    — the exact inversion of the gate. The timeout branch now filters on the deadline
    rather than on the exception type, so any failure caused by it downgrades to a preview; genuine
    caller cancellations and broken sessions still propagate unchanged.
  • The RoselineMCP:DefaultTimeout clock no longer runs while a human is being asked to confirm a
    write.
    It was armed before the confirmation elicitation, so its 120 s budget was spent on
    think-time — the very thing the confirmation's separate clock exists to prevent. Two consequences,
    both gone: a human who approved a previewOnly: false call more than 120 s after it started got
    {"ok": false, "error": {"type": "TimeoutError"}} instead of the write they had just authorized;
    and with the new ConfirmDestructiveWritesTimeout default (300 s) exceeding DefaultTimeout,
    the timeout path could never have delivered the documented preview-and-note either. The analysis
    budget now starts once the confirmation resolves, so DefaultTimeout measures analysis — as
    documented — rather than analysis plus however long the human took.
  • The EditMember write-confirmation prompt no longer asks the human to approve a write in ''
    when project was omitted (the documented auto-discovery default) — it now names "the
    auto-discovered project", matching ApplyFixes. RenameSymbol's prompt likewise names the
    project it resolved, instead of describing a solution-wide rename without saying which solution.

Changed

  • Upgraded the MCP SDK from ModelContextProtocol 1.4.1 to 2.2.0, which negotiates protocol
    revision 2026-07-28 by default. No tool's wire shape, parameters, or response envelope
    changes.
  • Client-side log forwarding is now inert for clients on protocol 2026-07-28 or later.
    SEP-2577
    deprecated the MCP Logging feature in that revision: logging/setLevel is rejected, and a server
    must not emit notifications/message for a request that did not carry an
    io.modelcontextprotocol/logLevel _meta field — which the SDK's own McpClient provides no way
    to set (on such a session it injects per-request _meta and strips that key). So the tool-failure
    log notifications RoselineMCP sends via AsClientLoggerProvider() are delivered only to clients
    that negotiate 2025-11-25 or earlier; the code is kept for them and stays a no-op otherwise.
    Nothing is lost for anyone: the correlation ID that those notifications carried is still in
    every error envelope (error.correlationId) and in the server's own stderr log, which is exactly
    what SEP-2577 names as the replacement. Deprecated features remain in the spec for at least twelve
    months.
  • Releases now publish to NuGet.org via Trusted Publishing instead of a long-lived
    NUGET_API_KEY secret: publish-nuget.yml exchanges the GitHub OIDC token for a key valid
    ~1 hour, the same way the registry publish already proved this repo's identity. The only
    remaining secret is NUGET_USER, the nuget.org profile name. Both prerequisites are in place as
    of 2026-07-27: the Trusted Publishing policy is registered on nuget.org (package owner
    phmatray, naming this repository and publish-nuget.yml) and NUGET_USER is set. The
    long-lived NUGET_API_KEY repository secret is deliberately kept until one real release has been
    published and verified — see PUBLISH.md.
  • The write-confirmation gate now lives in one place. apply_fixes, edit_member and
    rename_symbol each carried their own copy of the block deciding whether to ask, what a declined
    or unanswered prompt means for the call, what to log, and which note to attach — one policy with
    three edit sites. All three now call a single ToolExecutionHelper.ResolveWriteModeAsync, and the
    one part of the prompt that had drifted — how the target project is named — is single-sourced
    through ToolExecutionHelper.DescribeWriteTarget. No behavior change: no tool's parameters,
    response shape, prompt wording or notes text differ. This removes the cause of the three
    divergent confirmation messages already fixed under Fixed above, rather than fixing them
    again; ElicitationTests now pins all three prompts (and edit_member's decline path, which had
    no end-to-end cover) so the same divergence cannot re-form.

Documentation

  • RoselineMCP:RunAnalyzers = false no longer claims to stop all analyzer-assembly execution —
    source generators run regardless, and the docs now say so.
    SECURITY.md promised the switch
    "disables all analyzer execution (bundled and project-referenced alike)", and CLAUDE.md and
    README.md repeated it. Source generators ship through the same AnalyzerReferences and are
    equally arbitrary in-process code from the analyzed repository, but they run as part of building
    any compilation rather than as part of the diagnostics pass — which RunAnalyzers is the only
    thing gating. Every semantic path therefore executes them: all seven navigation tools (via
    SymbolResolver), ApplyFixes (via CodeFixService) and AnalyzeSolution (via
    SolutionAnalyzerService). Verified on Roslyn 5.6.0 / .NET SDK 10.0.302 — a project whose only
    content is a [GeneratedRegex] partial method compiles through MSBuildWorkspace with the
    generated implementation bound and zero errors, which is only possible if the generator ran.
    The consequence for an operator is the point: someone who set RunAnalyzers=false before
    pointing RoselineMCP at an untrusted repository believed they had closed a code-execution
    surface that was still fully open. Suppressing generators is not offered because it would not be
    honest — stripping AnalyzerReferences removes the generated types too, so every symbol
    resolving through generated code would be reported as a compile error. The switch narrows the
    surface; isolation, not configuration, is the mitigation, and the operator recommendations now
    lead with that. No behavior changed — only the guarantee the documentation advertised.
  • RoselineMCP:WorkspaceCache = false is documented as what it is: an isolation/debugging
    switch, never a way to save memory.
    The docs described it only as "loads a fresh workspace on
    every call", which reads as the memory-frugal option; measured, it is the opposite — disposing
    the workspace after every call costs +26 % resident memory (~374 MB vs ~296 MB after two
    calls) and ~45× second-call latency (0.92 s vs 0.02 s), because a disposed workspace's memory
    is never returned to the OS and the reload allocates on top of it. An operator who set it to
    reduce a server's footprint was getting a regression.
  • docs/ARCHITECTURE.md gains the measured memory profile behind that correction (~78 MB before
    any workspace exists — runtime plus the Roslyn/MSBuild/Roslynator assembly set, not the cache;
    ~300 MB once real work has been served; disposal plus a forced compacting GC moves the working
    set 276 MB → 276 MB), so the cache's 4-entry LRU bound is visible as the only lever that affects
    it. Releasing cached workspaces on idle was evaluated against these numbers and rejected.
  • docs/ARCHITECTURE.md now states that the stdio transport, not UseConsoleLifetime, is what
    stops the host when a client closes stdin, with the four measured EOF paths (after handshake,
    before handshake, mid-tool-call, and stdin held open). UseConsoleLifetime handles only
    SIGINT/SIGTERM, and reading it as the whole lifetime story suggests a stranded-server bug that
    does not exist.
  • Dependency versions asserted in prose corrected against RoselineMCP.csproj, which the README
    already names as the source of truth: ModelContextProtocol 1.4.0 → 1.4.1 (README tech stack
    and tool-annotations section, CLAUDE.md), MSBuild 18.7.1 → 18.8.2, and
    Microsoft.Extensions.Hosting 10.0.9 → 10.0.10.