Skip to content

Sentry and Google Calendar plugins, user logins in chat - #736

Merged
Nash0x7E2 merged 14 commits into
acceleratefrom
cursor/sentry-calendar-mcp-add9
Oct 3, 2026
Merged

Nash0x7E2 merged 14 commits into
acceleratefrom
cursor/sentry-calendar-mcp-add9

Conversation

@tschellenbach

@tschellenbach tschellenbach commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Why

Sprint 2 of .factory/features/mcp_plugins_and_connectors.md: an agent reads the company's Sentry (one login per app) and the end user's own Google Calendar (one login per user). Both are set up in agent.yaml. The dashboard reminds the app to finish the company login, and the user's login is asked for in the chat as a custom attachment. Following review: a login the provider later refuses is asked for again, and the config and plugin operations no longer live in legacy.yaml (the rest of it is ported in the stacked #739).

How it works

flowchart LR
  yaml["agent.yaml<br/>plugins: sentry<br/>user_plugins: google_calendar"] -->|syncAgent| config[(agent_configs<br/>plugins, user_plugins)]
  config -->|listConfigPlugins| dash["Dashboard<br/>sentry: not_connected"]
  dash -->|authorizePlugin, once| sentryLogin[(app login<br/>user_id empty)]
  config --> session[Session]
  sentryLogin -->|sentry__* tools| session
  session -->|"google_calendar__list_tools<br/>google_calendar__call_tool"| runner{caller has a login<br/>the provider accepts?}
  runner -->|no, or 401| oauth["start OAuth, store pending login<br/>for this user"] --> attach["reply carries<br/>plugin_authorization attachment"]
  runner -->|yes| mcp["MCP with the user's token"]
Loading
sequenceDiagram
  participant U as End user
  participant R as Router
  participant M as Model
  participant G as Google
  U->>R: When am I free today?
  M->>R: call google_calendar__list_tools
  R->>G: RFC 9728 and 8414 discovery
  R-->>U: reply + plugin_authorization attachment (button)
  U->>G: consent
  G->>R: /v1/agents/plugins/callback, login stored for this user only
  M->>R: google_calendar__call_tool
  R->>G: Calendar MCP with the user's token
  G-->>R: 401 (revoked later)
  R-->>U: plugin_authorization again, refused login forgotten
Loading
Change Result Proven by
sentry, google_calendar in plugins.yaml; discovery at the endpoint's path (RFC 9728 §3.1), catalog scopes and authorize_params, client secret for the deployment's own client Both providers' real metadata is found. Google gets calendar.readonly, offline access and consent TestMetadataAtTheEndpointsPathIsFoundFirst, TestTheDeploymentsOwnClientSendsItsSecret, live run
Mcp-Session-Id kept after initialize Streamable HTTP servers that hand out sessions work TestTheSessionTheServerGivesIsSentBack
user_plugins on config, sync, patch, session spec; agent_plugin_connections.user_id (migration 20261003200000) A user's login is theirs alone and never listed or used as the app's TestAnEndUsersLoginIsTheirsAloneAndSendsThemBackToTheConversation
listConfigPlugins adds named, unconnected app plugins as not_connected What the dashboard reminder reads TestAPluginTheAgentNamesThatNobodyConnectedIsLeftToRemindAbout
<id>__list_tools / <id>__call_tool per user plugin; first call without a login answers authorization_required Works with tools fixed at session open, and mid-conversation UserPluginsSuite, live run
An MCP 401 is plugins.ErrUnauthorized; the runner drops the refused login and asks again Expired or revoked logins re-prompt instead of erroring, at open or mid-conversation TestALoginTheProviderRefusesIsAskedForAgain, TestALoginRevokedMidConversationIsAskedForAgain, TestALoginTheServerRefusesIsToldApart
plugin_authorization Chat attachment on the reply (shown whether or not the tool is visible, only from the plugin's own tool) Button in chat, restored with history TestALoginAPluginAsksForIsAttachedToTheReplyAndRestored, TestOnlyThePluginsOwnToolMayAskForItsLogin
Config CRUD and the plugin operations move from legacy.yaml to Huma, so user_plugins and not_connected are declared in Go Rendered spec unchanged apart from Huma's 400/500 on listPlugins spec comparison, PluginsSuite, ConfigsSuite
Huma middleware answers a request with no credential 401 before its body is read Same 401 the handlers gave, not a 400 about the body TestACallerWithNoCredentialIsToldToAuthenticateFirst
Python: Accelerated(user_id=), authorization_required event, respond numbered for a user's kept conversation, ask() follows pending_work The example works against a real router test_accelerated.py, test_a_reply_that_runs_tools_is_followed_until_it_answers
Go SDK reads user_plugins; clients regenerated TestTheDeclarationSaysWhoConnectsEachPlugin
examples/text_agents/on_call live run

Live run

Local router in proxy mode with Postgres, Redis, real LLMs and real Stream Chat, plus uv run on_call.py as user alice. Google Calendar used a placeholder client id.

  • The model called google_calendar__list_tools. The router discovered accounts.google.com through calendarmcp's path-suffixed metadata, stored a pending login for alice, and the example printed the consent URL. The URL had scope=…calendar.readonly, access_type=offline, prompt=consent, PKCE S256 and resource.
  • The reply, read back from Stream Chat, carries {"type":"plugin_authorization","plugin_id":"google_calendar","title":"Connect Google Calendar","authorize_url":…}.
  • GET /v1/agents/configs/{id}/plugins answered [{"plugin_id":"sentry","status":"not_connected",…}].

Checks: go test ./...; the internal/api, internal/store and internal/session integration suites; dev.py check run with UV_FROZEN=1, because plain uv run fails to resolve on accelerate (roboflow vs moondream pillow pins, pre-existing). Ruff and both mypy passes are clean, and unit tests pass (1652, 0 failed); the 44 errors are Docker-only fixtures. Known unrelated failures: TestPhoneSuite with vendor credentials in the environment, and a flaky TestAUserRenamesASessionThatEnded that also fails on accelerate.

Not done / unverified

  • Volt dashboard UI (separate repo): render the reminder for status: not_connected, and a chat button for plugin_authorization attachments.
  • A real consent for Sentry or Google was not completed (no accounts). Whether Google accepts the RFC 8707 resource parameter on consent is unverified.
  • A refused login is asked for again without first trying its refresh token.
  • .NET, Ruby, Rust and PHP folder readers still need user_plugins (noted in the sdk skill).
Open in Web Open in Cursor 

cursoragent and others added 6 commits October 3, 2026 19:35
…chat

Sentry and Google Calendar join the plugin catalog. OAuth discovery now
reads RFC 9728 metadata at the endpoint's path, asks for the catalog's
scopes and extra authorize params, and authenticates a deployment's own
client with its secret, which Google needs.

An agent config names user_plugins beside plugins. Each end user reaches
those with their own login: the model gets <id>__list_tools and
<id>__call_tool, and the first call without a login starts one and puts
a plugin_authorization attachment on the reply. listConfigPlugins lists
plugins the config names that the app has not connected as not_connected,
for the dashboard to remind about.

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
agent.yaml declares user_plugins beside plugins and sync carries them.
Accelerated takes the end user a conversation is for, and a plugin tool
answering authorization_required becomes an authorization_required
event with the URL to open.

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
cursoragent and others added 8 commits October 3, 2026 19:55
…tachment

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
…ssages

A model that calls a tool first answers with nothing but the call, and
ask() ended there, closing the conversation before the tool came back.
responded carries pending_work, and ask() follows on until a reply has
nothing pending.

A conversation the router keeps for an end user takes each message by a
command id, so respond sends one when Accelerated acts for a user.

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
…s their login

An MCP server answering 401 is ErrUnauthorized. A user plugin's tool that
gets one, opening the session or mid-conversation, drops the refused login
and answers authorization_required again, so the reply carries a fresh
plugin_authorization attachment instead of an error.

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
The config CRUD and the plugin catalog, logins and authorize operations
move from legacy.yaml to Huma, with AgentConfig, AgentConfigRequest and
the plugin schemas as Go types documented by their tags, so user_plugins
and not_connected no longer live in legacy.yaml. The rendered spec is the
same apart from Huma's 400 and 500 on listPlugins.

Huma reads a request before its handler runs, so a caller with no
credential is now refused by middleware with the 401 the handlers gave,
rather than told what is wrong with its body.

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
…o Go

Co-authored-by: Thierry Schellenbach <thierryschellenbach@gmail.com>
@tschellenbach
tschellenbach marked this pull request as ready for review October 3, 2026 22:47
@Nash0x7E2
Nash0x7E2 merged commit 1c156a3 into accelerate Oct 3, 2026
17 checks passed
@Nash0x7E2
Nash0x7E2 deleted the cursor/sentry-calendar-mcp-add9 branch October 3, 2026 22:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants