Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional SSE middleware to obtain Grafana info from headers #21

Closed
wants to merge 8 commits into from

Conversation

sd2k
Copy link
Collaborator

@sd2k sd2k commented Feb 6, 2025

This PR adds a flag to the CLI which, when combined with the
SSE transport, allows the client to include information about
the Grafana instance to use in headers.

This is a non-standard way to use the MCP server, but I'm not sure any
of the other accepted auth proposals meet our use case.

Unfortunately it involves changing some of the callsites because the
Grafana client is no longer a singleton (since it can be changed).
Instead, settings live in a contextvar and the client is recreated
for each request.

sd2k added 2 commits February 6, 2025 15:57
This PR adds a flag to the CLI which, when combined with the
SSE transport, allows the client to include information about
the Grafana instance to use in headers.

This is a non-standard way to use the MCP server, but I'm not sure any
of the other accepted auth proposals meet our use case.

Unfortunately it involves changing some of the callsites because the
Grafana client is no longer a singleton (since it can be changed).
Instead, settings live in a contextvar and the client is recreated
for each request.
This avoids the need to create a new client for each tool
use, and hopefully lets us reuse connections.
@sd2k sd2k requested a review from a team as a code owner February 6, 2025 16:35
@sd2k
Copy link
Collaborator Author

sd2k commented Feb 6, 2025

This would definitely benefit from some tests. I think it'll be a bit complex though, we'll need to:

  • set up a couple of mock Grafana servers on different ports
  • start the MCP server using our customised run_sse_async method
  • start one SSE session with headers pointing to the first server, then send a message to use tool A
  • start a second SSE session pointing to the second server, then use a different tool
  • assert that the right servers get the right calls

I would really love to have some kind of load test as well to make sure there's nothing funky going on with the settings being accidentally shared between SSE sessions...

Copy link
Contributor

@annanay25 annanay25 left a comment

Choose a reason for hiding this comment

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

Nice, clever design of handling Grafana auth headers using ContextVars and a special middleware. +1 on the load tests, it would be nice knowing tokens aren't leaking between requests.

sd2k added 4 commits February 10, 2025 10:04
This commit adds a test for the new GrafanaMiddleware class.
It is a bit heavy-handed due to the fact that it requires quite
a lot of moving parts. There are quite a few comments in the
test file which hopefully explains what is going on.
@sd2k
Copy link
Collaborator Author

sd2k commented Feb 10, 2025

Alright, I've added a test which starts up some separate Grafana servers and makes requests with different headers to hit each one. It seems to work fine!

One thing I noticed is that after the SSE request is set up, anyone can send a JSONRPC request to /messages?<uuid> if they can guess the UUID, without any auth, and it will cause that message to be handled by the SSE handler. All output goes to the SSE handler rather than the client calling /messages though so I don't think this is a big deal, especially given how it's nearly impossible to guess that UUID.

@@ -7,21 +7,21 @@ async def list_datasources() -> bytes:
"""
List datasources in the Grafana instance.
"""
return await grafana_client.list_datasources()
return await grafana_client.get().list_datasources()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I would love to do away with all of these annoying .get() calls, tools definitely shouldn't be able to modify this contextvar...

@sd2k sd2k force-pushed the grafana-auth-middleware branch from f205487 to 762c60c Compare February 10, 2025 16:43
Copy link
Contributor

@annanay25 annanay25 left a comment

Choose a reason for hiding this comment

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

Also, cloud tests seem to have failed with

=========================== short test summary info ============================
FAILED tests/tools/incident_test.py::test_list_incidents - mcp_grafana.client.GrafanaError: {"code":"Loading","message":"Your instance is loading, and will be ready shortly."}
=================== 1 failed, 11 passed, 15 skipped in 3.38s ===================

🫠

@sd2k
Copy link
Collaborator Author

sd2k commented Feb 12, 2025

Also, cloud tests seem to have failed with

Yeah this is really annoying, I can't figure out how to keep that cloud instance online rather than pausing all the time... I'll dig into it a bit more!

Copy link
Contributor

@annanay25 annanay25 left a comment

Choose a reason for hiding this comment

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

lgtm!

@sd2k
Copy link
Collaborator Author

sd2k commented Feb 12, 2025

There was a good suggestion by @breitreiter that we could use a custom transport for this. In reality we're just piggy-backing on the SSE transport but it's an interesting idea to make it an explicitly different transport, not just a hack on top of SSE 😅

@sd2k sd2k marked this pull request as draft February 13, 2025 09:03
@sd2k
Copy link
Collaborator Author

sd2k commented Feb 13, 2025

Marking as draft while we rethink some of the details.

@annanay25
Copy link
Contributor

annanay25 commented Feb 13, 2025

I'd hedge towards merging this (we can always rerun tests if the cloud instance is down) and handling that in a different PR, but will leave it to you!

Nevermind, you marked draft because of the alternate transport consideration.

sd2k added a commit that referenced this pull request Feb 13, 2025
This is an implementation of a stateless transport over HTTP
for MCP. It's not part of the official spec, but something
similar will probably be added in the future.

Functionally, it just presents a single HTTP endpoint
(conventionally at `/mcp`) that accepts JSONRPC messages
as POST bodies, and returns JSONRPC messages as responses.

Each inbound message is validated and gets its own 'server'
instance. The handler then takes care of initializing the
server (which in a stateless protocol is the responsibility
of the client), then forwards the inbound message to the
server, and returns the server's response.

We could easily add middleware for things like authentication,
similar to in #21.

This also comes with a 'client' implementation which is similar
to the stdio and sse clients in the official SDK and returns a
pair of read/write streams for use with a `ClientSession`. This
is useful for testing or writing example MCP clients.

The idea is that this transport will be compatible with the
[HTTP transport in the unofficial Go SDK][go-sdk], so we can
start building examples of usage. Notably this client also
supports custom headers, which will be useful for authentication.

[go-sdk]: https://github.com/metoro-io/mcp-golang/blob/main/examples/http_example/README.md
@sd2k sd2k closed this Mar 12, 2025
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.

2 participants