From 5a8fe452f76830c9829ba3d10bc5cb62a52b55d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20He=CC=81ritier?= Date: Sat, 20 Jun 2026 19:15:31 +0200 Subject: [PATCH 1/2] fix: advertise refresh_token grant in OAuth dynamic client registration docker-agent's RFC 7591 dynamic client registration only advertised the authorization_code grant. Strict authorization servers that require clients to declare every grant they use (e.g. Miro's hosted MCP server at mcp.miro.com) reject the registration with: invalid_client_metadata: grant_types must be authorization_code and refresh_token docker-agent already uses the refresh_token grant via RefreshAccessToken, so declaring it at registration time is correct and backwards-compatible with lenient servers (Notion, Atlassian). Fixes #3191 --- pkg/tools/mcp/oauth_helpers.go | 10 ++++------ pkg/tools/mcp/oauth_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/pkg/tools/mcp/oauth_helpers.go b/pkg/tools/mcp/oauth_helpers.go index 5bb499b7a..501c4a5c8 100644 --- a/pkg/tools/mcp/oauth_helpers.go +++ b/pkg/tools/mcp/oauth_helpers.go @@ -333,12 +333,10 @@ func registerClient(ctx context.Context, client *http.Client, authMetadata *Auth } reqBody := map[string]any{ - "redirect_uris": []string{redirectURI}, - "client_name": "docker-agent", - "grant_types": []string{"authorization_code"}, - "response_types": []string{ - "code", - }, + "redirect_uris": []string{redirectURI}, + "client_name": "docker-agent", + "grant_types": []string{"authorization_code", "refresh_token"}, + "response_types": []string{"code"}, } if len(scopes) > 0 { reqBody["scope"] = strings.Join(scopes, " ") diff --git a/pkg/tools/mcp/oauth_test.go b/pkg/tools/mcp/oauth_test.go index 93a74ca2e..3b3b1f196 100644 --- a/pkg/tools/mcp/oauth_test.go +++ b/pkg/tools/mcp/oauth_test.go @@ -2002,6 +2002,40 @@ func TestUnmanagedOAuthFlow_DriveFlow_TimesOutWhenNoReplyArrives(t *testing.T) { "token endpoint must NOT be hit on timeout") } +// TestRegisterClient_GrantTypesIncludeRefreshToken verifies that dynamic +// client registration (RFC 7591) advertises both grant types the client +// uses. Strict authorization servers like Miro reject registrations that +// omit refresh_token. +func TestRegisterClient_GrantTypesIncludeRefreshToken(t *testing.T) { + var registrationBody map[string]any + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(®istrationBody); err != nil { + t.Fatalf("failed to decode registration request body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{"client_id":"test-client","client_secret":"test-secret"}`)) + })) + defer srv.Close() + + meta := &AuthorizationServerMetadata{ + RegistrationEndpoint: srv.URL, + } + clientID, clientSecret, err := RegisterClient(t.Context(), meta, "https://example.test/oauth/cb", nil) + require.NoError(t, err) + assert.Equal(t, "test-client", clientID) + assert.Equal(t, "test-secret", clientSecret) + + grantTypes, _ := registrationBody["grant_types"].([]any) + require.Len(t, grantTypes, 2, "grant_types must list both grants the client uses") + assert.Contains(t, grantTypes, "authorization_code", "grant_types must include authorization_code") + assert.Contains(t, grantTypes, "refresh_token", "grant_types must include refresh_token (RFC 7591; required by strict servers such as Miro)") + + responseTypes, _ := registrationBody["response_types"].([]any) + assert.Contains(t, responseTypes, "code", "response_types must include code") +} + // TestUnmanagedRedirectURI_PerToolsetTakesPrecedence verifies the precedence // order: per-toolset RemoteOAuthConfig.CallbackRedirectURL overrides the // runtime-wide --mcp-oauth-redirect-uri. From 874bc26e8f2144d43f6f38170e6f2a80b83638e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20He=CC=81ritier?= Date: Sat, 20 Jun 2026 19:15:38 +0200 Subject: [PATCH 2/2] docs: add Miro hosted MCP example with inline board skills Add examples/miro-expert.yaml demonstrating Miro's hosted MCP server (https://mcp.miro.com/) over streamable HTTP with OAuth 2.1 Dynamic Client Registration. Includes four inline board skills (browse, diagram, doc, table) adapted from Miro's own skill set, plus a link to the official docs and notes on the Enterprise-plan requirement. Registers the example in examples/README.md. --- examples/README.md | 1 + examples/miro-expert.yaml | 128 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 examples/miro-expert.yaml diff --git a/examples/README.md b/examples/README.md index fa12ebceb..caf1efb81 100644 --- a/examples/README.md +++ b/examples/README.md @@ -140,6 +140,7 @@ remote MCP endpoints. | File | What it shows | |------|---------------| | [`notion-expert.yaml`](notion-expert.yaml) | Remote MCP server with OAuth Dynamic Client Registration. | +| [`miro-expert.yaml`](miro-expert.yaml) | Miro's hosted MCP server (`mcp.miro.com`) over streamable HTTP with OAuth 2.1 DCR, plus four inline board skills (browse / diagram / doc / table). | | [`remote_mcp_oauth.yaml`](remote_mcp_oauth.yaml) | Remote MCP server with explicit OAuth credentials (Slack/GitHub-style). | | [`remote_mcp_oauth_callback_redirect.yaml`](remote_mcp_oauth_callback_redirect.yaml) | OAuth flow with a public redirect URL bouncing back to localhost. | | [`websocket_transport.yaml`](websocket_transport.yaml) | OpenAI Responses API streaming over WebSocket instead of SSE. | diff --git a/examples/miro-expert.yaml b/examples/miro-expert.yaml new file mode 100644 index 000000000..fadd67e8f --- /dev/null +++ b/examples/miro-expert.yaml @@ -0,0 +1,128 @@ +# Example: Miro's hosted MCP server (https://miro.com/ai/mcp/) with skills. +# +# Miro exposes a remote, streamable-HTTP MCP server at https://mcp.miro.com/. +# It authenticates with OAuth 2.1 and supports Dynamic Client Registration, so +# no clientId/clientSecret is required: docker-agent opens the browser for the +# OAuth flow on first run. +# +# Official docs: https://developers.miro.com/docs/miro-mcp +# +# - The MCP server is Enterprise-plan only. Your organization's admin must +# enable it before you can connect (see the docs above). +# - The MCP server is the source of truth for the exact tools available; the +# skills below are intentionally thin shortcuts that route the agent to the +# right family of tools rather than hard-coding tool names or schemas. +# +# The four inline skills are adapted from Miro's own skill set +# (https://github.com/miroapp/miro-ai/tree/main/skills) — one read-oriented +# (browse) and three authoring ones (diagram, doc, table). They are defined +# inline so this example runs without any external skill source. + +models: + sonnet: + provider: anthropic + model: claude-sonnet-4-6 + +agents: + root: + model: sonnet + description: Miro Expert — search, summarize, and build on Miro boards. + instruction: | + You are a Miro board expert with access to Miro's hosted MCP server. + + You can explore and summarize boards, and author structured content: + frames, sticky notes, shapes, diagrams, documents, and tables. + + Operating rules: + - Always work against a specific board. If the user hasn't given a board + URL, ask for one before calling a tool. Preserve any item target + (e.g. ?moveTowidget=...) in the URL so tools scope correctly. + - The MCP server is the source of truth for which tools exist and their + parameter schemas. Inspect the live tool descriptions and follow them; + never invent tool names, diagram types, or column types. + - Match the user's intent to the right skill (browse / diagram / doc / + table), then call the corresponding MCP tool per its schema. + - For authoring, restate what you're about to create and where before + creating it, then report back a link to the board or frame. + - Respect existing board permissions and access controls at all times. + skills: + - name: miro-browse + description: Use when the user wants to explore, list, summarize, or inspect items on a Miro board. + instructions: | + # Miro Browse + + Shortcut to the Miro MCP browsing and context tools. The MCP server + is the source of truth for which tools exist (board-level overview, + item-level content, item listing/filtering, image and asset + retrieval), which to pick, how to chain them, and all parameters. + + ## Workflow + + 1. Identify the **board URL**. If the user's URL targets a specific + item (frame, document, prototype screen, etc.), preserve it so the + tools scope their response to that target. + 2. Identify **what the user wants to learn**: a high-level overview + of the whole board, a filtered listing of items of a certain type, + the contents of one specific item, or a downloadable asset. Ask if + unclear. + 3. Pick the appropriate browsing or context tool and call it per its + description. For a board summary, start with the high-level + overview tool and then drill into individual items with the + item-level retrieval tool as questions get more specific. + + - name: miro-diagram + description: Use when the user wants to create or update a diagram on a Miro board. + instructions: | + # Miro Diagram + + Shortcut to the Miro MCP diagramming tools. The MCP server is the + source of truth for which diagram types and inputs are supported, + which tool to pick, the order in which tools must be called, and all + placement parameters. + + ## Workflow + + 1. Identify the **board URL**. If missing, ask. + 2. Identify **what to diagram**. Ask if unclear. + 3. Pick the appropriate diagramming tool and call it according to its + description and parameter schema. + + - name: miro-doc + description: Use when the user wants to create or edit a Google-Docs-style markdown document on a Miro board. + instructions: | + # Miro Doc + + Shortcut to the Miro MCP document tools. The MCP server is the source + of truth for supported markdown, which tool to pick, the order in + which tools must be called, and all placement parameters. + + ## Workflow + + 1. Identify the **board URL**. If missing, ask. + 2. Identify **what document the user wants** (provided content or a + topic to generate from). Ask if unclear. + 3. Pick the appropriate document tool and call it according to its + description and parameter schema. + + - name: miro-table + description: Use when the user wants to create or update a structured table on a Miro board. + instructions: | + # Miro Table + + Shortcut to the Miro MCP table tools. The MCP server is the source of + truth for supported column types and option shape, which tool to + pick, the order in which tools must be called, and all placement + parameters. + + ## Workflow + + 1. Identify the **board URL**. If missing, ask. + 2. Identify **what table the user wants** (title and columns, or a + topic to propose a column set from). Ask if unclear. + 3. Pick the appropriate table tool and call it according to its + description and parameter schema. + toolsets: + - type: mcp + remote: + url: https://mcp.miro.com/ + transport_type: streamable