Skip to content

Conversation

@jamadeo
Copy link
Collaborator

@jamadeo jamadeo commented May 21, 2025

This adds handling for server-initiated notification messages. Currently it supports logs and progress messages. This involved a decent amount of work up and down the MCP service stack, so it's a hefty change. Notifications means the services need to support listening for more messages besides a single request response.

The change also adds a tiny bit of refactoring to support it:

  • moves request/response matching into the service layer out of the transport layer
  • simplifies client construction a bit, using the tower service object as an implementation within the client (if need be, we can still expose the whole middleware bit, but right now we're just using it to add timeouts)
Screen.Recording.2025-05-22.at.11.44.07.AM.mov
spinner.mov

@michaelneale michaelneale requested a review from baxen May 21, 2025 23:15
@michaelneale
Copy link
Collaborator

nice ...

image

image

so this does subjectively give nice feedback. Need to think a bit on the design how to present it. I think both in GUI and CLI it should be some sort of spinner like activity - which just shows the message (subtle) which changes as new one comes in (instead of printing out). That would feel like a lot more fine grained progress, but the bones of it are here! really cool!

@jamadeo
Copy link
Collaborator Author

jamadeo commented May 22, 2025

@michaelneale I like it. Here's what it looks like as a spinner:

spinner.mov

still very informative of progress without taking more than a line of screen real estate

@jamadeo
Copy link
Collaborator Author

jamadeo commented May 27, 2025

Here's the example MCP server that emits log and progress messages:

import asyncio
from fastmcp import FastMCP, Context

mcp = FastMCP("Demo 🚀")


@mcp.tool()
async def count(ctx: Context) -> int:
    """Counts and streams results"""
    await ctx.info("Counting...")

    until = 10
    for i in range(1, until + 1):
        await ctx.report_progress(i, until)
        await asyncio.sleep(0.5)

    await ctx.info("Finished counting")
    return until


if __name__ == "__main__":
    mcp.run()

@michaelneale
Copy link
Collaborator

.bundle

@github-actions
Copy link
Contributor

macOS ARM64 Desktop App (Apple Silicon)

📱 Download macOS Desktop App (arm64, signed)

Instructions:
After downloading, unzip the file and drag the Goose.app to your Applications folder. The app is signed and notarized for macOS.

This link is provided by nightly.link and will work even if you're not logged into GitHub.

Copy link
Collaborator

@baxen baxen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great feature!

We should make sure we test thoroughly with existing mcps and SSE version (i don't see any examples with SSE here) because so much surface area is updated. We'll likely need to update post switching to rmcp so i'm less concerned with backwards compatibility in the mcp crates here as long as integration tests are passing

let output_str = output_task
.await
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: repeated map?

T: TransportHandle + Send + Sync + 'static,
{
service: Mutex<S>,
service: Mutex<tower::timeout::Timeout<McpService<T>>>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's less important that we hardcode for now because we have rmcp to migrate to anyways, but why do we need to force the timeout wrapped service?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that McpClient is built with a Transport instead of a Service, the service itself is really just an implementation detail for the McpClient. In fact we're really only using the tower::Service in order to use the timeout wrapper.

It could be made generic here again but makes things decently more complicated and since it isn't part of the public API it feels less useful to do so

@jamadeo jamadeo force-pushed the jackamadeo/notifications branch from 4bae700 to 82921d7 Compare May 30, 2025 02:16
@jamadeo
Copy link
Collaborator Author

jamadeo commented May 30, 2025

thanks @baxen for review! I created an integration test that covers the new functionality over both SSE and STDIO: https://github.com/block/goose/blob/b896dee383268021b9bd4bce2897035a2f72afc7/crates/mcp-client/examples/integration_test.rs

Also did a bunch of manual testing with some of the bundled MCPs: google drive, memory, developer of course. Everything LGTM.

@jamadeo jamadeo merged commit 03e5549 into main May 30, 2025
7 checks passed
@jamadeo jamadeo deleted the jackamadeo/notifications branch May 30, 2025 15:50
GitMurf pushed a commit to GitMurf/goose that referenced this pull request May 30, 2025
Co-authored-by: Michael Neale <michael.neale@gmail.com>
@GitMurf
Copy link
Contributor

GitMurf commented Jun 3, 2025

@jamadeo / @michaelneale please see this gh issue I just opened: #2767

This PR "broke" developer extension shell tool calls in goose cli. It seems it is not properly waiting for the tool output to complete (e.g., shell command output for an ls command) before responding to the LLM. The details are in the gh issue. Let me know if you have any questions over there.

cbruyndoncx pushed a commit to cbruyndoncx/goose that referenced this pull request Jul 20, 2025
Co-authored-by: Michael Neale <michael.neale@gmail.com>
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.

5 participants