From e162d0b5d0f14f322e5419b5f11fc7925a4e828d Mon Sep 17 00:00:00 2001 From: David Gageot Date: Mon, 13 Oct 2025 12:10:40 +0200 Subject: [PATCH] Better support remote MCP servers references by their name in the MCP Catalog Signed-off-by: David Gageot --- examples/apify.yaml | 14 ++++++++++++++ pkg/gateway/catalog.go | 22 +++++++++++++++++++--- pkg/gateway/catalog_test.go | 25 ++++++++++++++++++++++++- pkg/gateway/types.go | 7 +++++++ pkg/teamloader/teamloader.go | 10 ++++++++++ pkg/tools/mcp/remote.go | 2 +- 6 files changed, 75 insertions(+), 5 deletions(-) create mode 100755 examples/apify.yaml diff --git a/examples/apify.yaml b/examples/apify.yaml new file mode 100755 index 000000000..2cc118a19 --- /dev/null +++ b/examples/apify.yaml @@ -0,0 +1,14 @@ +#!/usr/bin/env cagent run +version: "2" + +agents: + root: + model: openai/gpt-4o + description: Agent knowledgeable in Apify. + instruction: | + You are an AI assistant with a deep understanding of Apify. + Your responses should be clear, concise, and focused on providing accurate information about + Apify concepts, best practices, and usage. + toolsets: + - type: mcp + ref: docker:apify diff --git a/pkg/gateway/catalog.go b/pkg/gateway/catalog.go index 290635dae..a7612b2bc 100644 --- a/pkg/gateway/catalog.go +++ b/pkg/gateway/catalog.go @@ -13,17 +13,33 @@ import ( const DockerCatalogURL = "https://desktop.docker.com/mcp/catalog/v3/catalog.yaml" func RequiredEnvVars(ctx context.Context, serverName string) ([]Secret, error) { + server, err := ServerSpec(ctx, serverName) + if err != nil { + return nil, err + } + + // TODO(dga): until the MCP Gateway supports oauth with cagent, + // we ignore every secret listed on `remote` servers and assume + // we can use oauth by connecting directly to the server's url. + if server.Type == "remote" { + return nil, nil + } + + return server.Secrets, nil +} + +func ServerSpec(ctx context.Context, serverName string) (Server, error) { catalog, err := readCatalogOnce() if err != nil { - return nil, fmt.Errorf("failed to fetch MCP catalog: %w", err) + return Server{}, fmt.Errorf("failed to fetch MCP catalog: %w", err) } server, ok := catalog[serverName] if !ok { - return nil, fmt.Errorf("MCP server %q not found in MCP catalog", serverName) + return Server{}, fmt.Errorf("MCP server %q not found in MCP catalog", serverName) } - return server.Secrets, nil + return server, nil } // Read the MCP Catalog only once and cache the result. diff --git a/pkg/gateway/catalog_test.go b/pkg/gateway/catalog_test.go index 1319290f2..c30341adb 100644 --- a/pkg/gateway/catalog_test.go +++ b/pkg/gateway/catalog_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestRequiredEnvVars(t *testing.T) { +func TestRequiredEnvVars_local(t *testing.T) { secrets, err := RequiredEnvVars(t.Context(), "github-official") require.NoError(t, err) @@ -15,3 +15,26 @@ func TestRequiredEnvVars(t *testing.T) { assert.Equal(t, "GITHUB_PERSONAL_ACCESS_TOKEN", secrets[0].Env) assert.Equal(t, "github.personal_access_token", secrets[0].Name) } + +func TestRequiredEnvVars_remote(t *testing.T) { + secrets, err := RequiredEnvVars(t.Context(), "apify") + require.NoError(t, err) + + assert.Empty(t, secrets) +} + +func TestServerSpec_local(t *testing.T) { + server, err := ServerSpec(t.Context(), "fetch") + require.NoError(t, err) + + assert.Equal(t, "server", server.Type) +} + +func TestServerSpec_remote(t *testing.T) { + server, err := ServerSpec(t.Context(), "apify") + require.NoError(t, err) + + assert.Equal(t, "remote", server.Type) + assert.Equal(t, "https://mcp.apify.com", server.Remote.URL) + assert.Equal(t, "streamable-http", server.Remote.TransportType) +} diff --git a/pkg/gateway/types.go b/pkg/gateway/types.go index 44d178e42..51c1dbe24 100644 --- a/pkg/gateway/types.go +++ b/pkg/gateway/types.go @@ -7,7 +7,14 @@ type topLevel struct { type Catalog map[string]Server type Server struct { + Type string `json:"type"` Secrets []Secret `json:"secrets,omitempty"` + Remote Remote `json:"remote,omitempty"` +} + +type Remote struct { + URL string `json:"url"` + TransportType string `json:"transport_type"` } type Secret struct { diff --git a/pkg/teamloader/teamloader.go b/pkg/teamloader/teamloader.go index d3bbf52c1..e14869773 100644 --- a/pkg/teamloader/teamloader.go +++ b/pkg/teamloader/teamloader.go @@ -346,6 +346,16 @@ func createTool(ctx context.Context, toolset latest.Toolset, a *latest.AgentConf case toolset.Type == "mcp" && toolset.Ref != "": mcpServerName := gateway.ParseServerRef(toolset.Ref) + serverSpec, err := gateway.ServerSpec(ctx, mcpServerName) + if err != nil { + return nil, fmt.Errorf("fetching MCP server spec for %q: %w", mcpServerName, err) + } + + // TODO(dga): until the MCP Gateway supports oauth with cagent, we fetch the remote url and directly connect to it. + if serverSpec.Type == "remote" { + return mcp.NewRemoteToolset(serverSpec.Remote.URL, serverSpec.Remote.TransportType, nil, toolset.Tools, runtimeConfig.RedirectURI) + } + return mcp.NewGatewayToolset(mcpServerName, toolset.Config, toolset.Tools, envProvider), nil case toolset.Type == "mcp" && toolset.Command != "": diff --git a/pkg/tools/mcp/remote.go b/pkg/tools/mcp/remote.go index c47bb5df6..72d294197 100644 --- a/pkg/tools/mcp/remote.go +++ b/pkg/tools/mcp/remote.go @@ -92,7 +92,7 @@ func (c *remoteMCPClient) Initialize(ctx context.Context, request *mcp.Initializ Endpoint: c.url, HTTPClient: httpClient, } - case "streamable": + case "streamable", "streamable-http": transport = &mcp.StreamableClientTransport{ Endpoint: c.url, HTTPClient: httpClient,