Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
128 changes: 128 additions & 0 deletions examples/miro-expert.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 4 additions & 6 deletions pkg/tools/mcp/oauth_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, " ")
Expand Down
34 changes: 34 additions & 0 deletions pkg/tools/mcp/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(&registrationBody); 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.
Expand Down
Loading