Skip to content
Draft
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
50 changes: 35 additions & 15 deletions apiclient/types/mcpserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,21 @@ type ContainerizedRuntimeConfig struct {

// RemoteRuntimeConfig represents configuration for remote runtime (External MCP servers)
type RemoteRuntimeConfig struct {
URL string `json:"url"` // Required: Full URL to remote MCP server
IsTemplate bool `json:"isTemplate"` // Optional: Whether the URL is a template
URLTemplate string `json:"urlTemplate,omitempty"` // URL template for user URLs
Hostname string `json:"hostname,omitempty"` // Optional: Hostname constraint the URL conforms to
Headers []MCPHeader `json:"headers,omitempty"` // Optional
URL string `json:"url"` // Required: Full URL to remote MCP server
IsTemplate bool `json:"isTemplate"` // Optional: Whether the URL is a template
URLTemplate string `json:"urlTemplate,omitempty"` // URL template for user URLs
Hostname string `json:"hostname,omitempty"` // Optional: Hostname constraint the URL conforms to
Headers []MCPHeader `json:"headers,omitempty"` // Optional
AuthorizationServerURL string `json:"authorizationServerURL,omitempty"` // OAuth authorization server URL (indicates static OAuth required)
}

// RemoteCatalogConfig represents template configuration for remote servers in catalog entries
type RemoteCatalogConfig struct {
FixedURL string `json:"fixedURL,omitempty"` // Fixed URL for all instances
URLTemplate string `json:"urlTemplate,omitempty"` // URL template for user URLs
Hostname string `json:"hostname,omitempty"` // Required hostname for user URLs
Headers []MCPHeader `json:"headers,omitempty"` // Optional
FixedURL string `json:"fixedURL,omitempty"` // Fixed URL for all instances
URLTemplate string `json:"urlTemplate,omitempty"` // URL template for user URLs
Hostname string `json:"hostname,omitempty"` // Required hostname for user URLs
Headers []MCPHeader `json:"headers,omitempty"` // Optional
AuthorizationServerURL string `json:"authorizationServerURL,omitempty"` // OAuth authorization server URL (indicates static OAuth required)
}

// CompositeCatalogConfig represents configuration for composite servers in catalog entries.
Expand Down Expand Up @@ -222,11 +224,12 @@ type MCPServer struct {

// Alias is a user-defined alias for the MCP server.
// This may only be set for single user and remote MCP servers (i.e. where `MCPCatalogID` is "").
Alias string `json:"alias,omitempty"`
UserID string `json:"userID"`
Configured bool `json:"configured"`
MissingRequiredEnvVars []string `json:"missingRequiredEnvVars,omitempty"`
MissingRequiredHeaders []string `json:"missingRequiredHeader,omitempty"`
Alias string `json:"alias,omitempty"`
UserID string `json:"userID"`
Configured bool `json:"configured"`
MissingRequiredEnvVars []string `json:"missingRequiredEnvVars,omitempty"`
MissingRequiredHeaders []string `json:"missingRequiredHeader,omitempty"`
MissingOAuthCredentials bool `json:"missingOAuthCredentials,omitempty"`
CatalogEntryID string `json:"catalogEntryID"`
PowerUserWorkspaceID string `json:"powerUserWorkspaceID"`
MCPCatalogID string `json:"mcpCatalogID,omitempty"`
Expand Down Expand Up @@ -465,8 +468,9 @@ func MapCatalogEntryToServer(catalogEntry MCPServerCatalogEntryManifest, userURL
}
}

// Copy headers from catalog entry
// Copy headers and authorization server URL from catalog entry
remoteConfig.Headers = catalogEntry.RemoteConfig.Headers
remoteConfig.AuthorizationServerURL = catalogEntry.RemoteConfig.AuthorizationServerURL
serverManifest.RemoteConfig = remoteConfig
default:
return serverManifest, RuntimeValidationError{
Expand Down Expand Up @@ -535,3 +539,19 @@ func ValidateURLHostname(u string, hostname string) error {
}
return nil
}

// MCPServerOAuthCredentialRequest represents a request to set OAuth credentials for an MCP server
type MCPServerOAuthCredentialRequest struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
}

// MCPServerOAuthCredentialStatus represents the status of OAuth credentials for an MCP server
type MCPServerOAuthCredentialStatus struct {
// Configured is true if OAuth credentials have been set
Configured bool `json:"configured"`
// ClientID is the configured client ID (never includes secret)
ClientID string `json:"clientID,omitempty"`
// AuthorizationServerURL is the authorization server URL from the server config
AuthorizationServerURL string `json:"authorizationServerURL,omitempty"`
}
30 changes: 30 additions & 0 deletions apiclient/types/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/obot-platform/obot
go 1.25.5

replace (
github.com/nanobot-ai/nanobot => ../nanobot
github.com/obot-platform/obot/apiclient => ./apiclient
github.com/obot-platform/obot/logger => ./logger
)
Expand Down Expand Up @@ -250,7 +251,7 @@ require (
github.com/nwaples/rardecode/v2 v2.2.1 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/obot-platform/mcp-oauth-proxy v0.0.3-0.20251219153209-05acb93b2b5c // indirect
github.com/obot-platform/mcp-oauth-proxy v0.0.3-0.20260106135339-3745d9b14a30 // indirect
github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 // indirect
github.com/onsi/ginkgo/v2 v2.20.2
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nanobot-ai/nanobot v0.0.47 h1:S2iNJYzOWuJsne1pl8hsfVlESOvROMMibw759P1T7bQ=
github.com/nanobot-ai/nanobot v0.0.47/go.mod h1:4e8Nx3KqzSaWm2EYz1vSrRQ6PvpFXltZCQ5X9oFI/t4=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA=
Expand All @@ -582,8 +580,8 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/obot-platform/kinm v0.0.0-20250905213846-3c65d6845f83 h1:f38gPMZxtqsvmusPBUgU8ZNmGsEU/lw4qpCruiStmu8=
github.com/obot-platform/kinm v0.0.0-20250905213846-3c65d6845f83/go.mod h1:uIt/q4bSrRWxjnX9wGn9i+LfBvN04g0b/c4h5imPCOI=
github.com/obot-platform/mcp-oauth-proxy v0.0.3-0.20251219153209-05acb93b2b5c h1:kSoTjmtlQU9+Gfsuh11hzG4F6L04+Ixs7YwNT0xG6Mk=
github.com/obot-platform/mcp-oauth-proxy v0.0.3-0.20251219153209-05acb93b2b5c/go.mod h1:8I+MeGRPsv42hk7/MCSqWHvz4p7xvIR0CIh1GG3vtXM=
github.com/obot-platform/mcp-oauth-proxy v0.0.3-0.20260106135339-3745d9b14a30 h1:PbvvLXjoUHQGpQ4ZzV9Aj8n8y0evpZljPlpDu96lFC4=
github.com/obot-platform/mcp-oauth-proxy v0.0.3-0.20260106135339-3745d9b14a30/go.mod h1:8I+MeGRPsv42hk7/MCSqWHvz4p7xvIR0CIh1GG3vtXM=
github.com/obot-platform/nah v0.0.0-20250418220644-1b9278409317 h1:yUEiybfv/VpPf4Ct73cIBIDi++V0x5vzFH79pAXWgL0=
github.com/obot-platform/nah v0.0.0-20250418220644-1b9278409317/go.mod h1:AUP8EhWbz+tBQn1QMYcBY8EPFN8LFNZvKhOxpt4hafA=
github.com/obot-platform/namegenerator v0.0.0-20241217121223-fc58bdb7dca2 h1:jiyBM/TYxU6UNVS9ff8Y8n55DOKDYohKkIZjfHpjfTY=
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/authz/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ var apiResources = map[string][]string{
"GET /api/workspaces/{workspace_id}/entries/{entry_id}/servers/{mcpserver_id}/logs",
"POST /api/workspaces/{workspace_id}/entries/{entry_id}/servers/{mcpserver_id}/restart",
"POST /api/workspaces/{workspace_id}/entries/{entry_id}/servers/{mcpserver_id}/trigger-update",
"GET /api/workspaces/{workspace_id}/entries/{entry_id}/oauth-credentials",
"POST /api/workspaces/{workspace_id}/entries/{entry_id}/oauth-credentials",
"DELETE /api/workspaces/{workspace_id}/entries/{entry_id}/oauth-credentials",
},
types.GroupPowerUserPlus: {
"GET /api/workspaces/{workspace_id}/servers",
Expand Down
58 changes: 42 additions & 16 deletions pkg/api/handlers/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ func (m *MCPHandler) ListEntriesFromAllSources(req api.Context) error {
}

if hasAccess {
// Hide entries that require OAuth credentials that haven't been configured (non-admins only).
// Workspace owners can always see their own entries (they need to configure the OAuth credentials).
if !req.UserIsAdmin() && entryRequiresUnconfiguredOAuth(req.Context(), req.GPTClient, entry) {
// Check if this is a workspace entry owned by the current user
if entry.Spec.PowerUserWorkspaceID == "" || entry.Spec.PowerUserWorkspaceID != system.GetPowerUserWorkspaceID(req.User.GetUID()) {
// Either the entry is not in a workspace, or it's in a workspace not owned by the user. Omit it.
continue
}
}
entries = append(entries, convertEntry(entry))
}
}
Expand Down Expand Up @@ -274,7 +283,7 @@ func (m *MCPHandler) ListServer(req api.Context) error {
return err
}
}
converted := ConvertMCPServer(server, credMap[server.Name], m.serverURL, slug, components...)
converted := ConvertMCPServer(req.Context(), req.GPTClient, server, credMap[server.Name], m.serverURL, slug, components...)
items = append(items, converted)
}

Expand Down Expand Up @@ -343,7 +352,7 @@ func (m *MCPHandler) GetServer(req api.Context) error {
return err
}
}
converted := ConvertMCPServer(server, cred.Env, m.serverURL, slug, components...)
converted := ConvertMCPServer(req.Context(), req.GPTClient, server, cred.Env, m.serverURL, slug, components...)
return req.Write(converted)
}

Expand Down Expand Up @@ -398,7 +407,7 @@ func (m *MCPHandler) DeleteServer(req api.Context) error {
return err
}

return req.Write(ConvertMCPServer(server, nil, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, server, nil, m.serverURL, slug))
}

// compositeDeletionDependency represents a composite MCP server or catalog entry that depends
Expand Down Expand Up @@ -1577,6 +1586,11 @@ func (m *MCPHandler) CreateServer(req api.Context) error {
return types.NewErrForbidden("user does not have access to MCP server catalog entry")
}

// Block server creation if OAuth is required but not configured
if entryRequiresUnconfiguredOAuth(req.Context(), req.GPTClient, catalogEntry) {
return types.NewErrBadRequest("catalog entry requires OAuth configuration by an administrator before it can be used")
}

manifest, err := serverManifestFromCatalogEntryManifest(req.UserIsAdmin(), false, catalogEntry.Spec.Manifest, input.MCPServerManifest)
if err != nil {
return err
Expand Down Expand Up @@ -1621,7 +1635,7 @@ func (m *MCPHandler) CreateServer(req api.Context) error {
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.WriteCreated(ConvertMCPServer(server, cred.Env, m.serverURL, slug))
return req.WriteCreated(ConvertMCPServer(req.Context(), req.GPTClient, server, cred.Env, m.serverURL, slug))
}

// UpdateServer updates the manifest of an MCPServer.
Expand Down Expand Up @@ -1698,7 +1712,7 @@ func (m *MCPHandler) UpdateServer(req api.Context) error {
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(existing, cred.Env, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, existing, cred.Env, m.serverURL, slug))
}

func (m *MCPHandler) UpdateServerAlias(req api.Context) error {
Expand Down Expand Up @@ -1836,7 +1850,7 @@ func (m *MCPHandler) ConfigureServer(req api.Context) error {
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(mcpServer, envVars, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, mcpServer, envVars, m.serverURL, slug))
}

func (m *MCPHandler) configureCompositeServer(req api.Context, compositeServer v1.MCPServer) error {
Expand Down Expand Up @@ -1997,7 +2011,7 @@ func (m *MCPHandler) configureCompositeServer(req api.Context, compositeServer v
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(compositeServer, nil, m.serverURL, slug, components...))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, compositeServer, nil, m.serverURL, slug, components...))
}

// applyURLTemplate applies a URL template with environment variables
Expand Down Expand Up @@ -2055,7 +2069,7 @@ func (m *MCPHandler) DeconfigureServer(req api.Context) error {
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(mcpServer, nil, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, mcpServer, nil, m.serverURL, slug))
}

func (m *MCPHandler) deconfigureCompositeServer(req api.Context, compositeServer v1.MCPServer) error {
Expand Down Expand Up @@ -2104,7 +2118,7 @@ func (m *MCPHandler) deconfigureCompositeServer(req api.Context, compositeServer
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(compositeServer, nil, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, compositeServer, nil, m.serverURL, slug))
}

func (m *MCPHandler) Reveal(req api.Context) error {
Expand Down Expand Up @@ -2404,7 +2418,7 @@ func addExtractedEnvVarsToCatalogEntry(entry *v1.MCPServerCatalogEntry) {
}
}

func ConvertMCPServer(server v1.MCPServer, credEnv map[string]string, serverURL, slug string, components ...types.MCPServer) types.MCPServer {
func ConvertMCPServer(ctx context.Context, gptClient *gptscript.GPTScript, server v1.MCPServer, credEnv map[string]string, serverURL, slug string, components ...types.MCPServer) types.MCPServer {
var missingEnvVars, missingHeaders []string

// Check for missing required env vars
Expand All @@ -2431,6 +2445,17 @@ func ConvertMCPServer(server v1.MCPServer, credEnv map[string]string, serverURL,
}
}

// Check if OAuth credentials are required but missing
missingOAuth := false
if server.Spec.Manifest.RemoteConfig != nil &&
server.Spec.Manifest.RemoteConfig.AuthorizationServerURL != "" &&
server.Spec.MCPServerCatalogEntryName != "" {
// Look up OAuth credentials for the catalog entry
credName := system.MCPOAuthCredentialName(server.Spec.MCPServerCatalogEntryName)
_, err := gptClient.RevealCredential(ctx, []string{credName}, "oauth")
missingOAuth = (err != nil)
}

var connectURL string
// Only single-user servers get a connect URL.
// Multi-user servers have connect URLs on the MCPServerInstances instead.
Expand All @@ -2455,8 +2480,9 @@ func ConvertMCPServer(server v1.MCPServer, credEnv map[string]string, serverURL,
Alias: server.Spec.Alias,
MissingRequiredEnvVars: missingEnvVars,
MissingRequiredHeaders: missingHeaders,
MissingOAuthCredentials: missingOAuth,
UserID: server.Spec.UserID,
Configured: len(missingEnvVars) == 0 && len(missingHeaders) == 0 && !server.Spec.NeedsURL,
Configured: len(missingEnvVars) == 0 && len(missingHeaders) == 0 && !server.Spec.NeedsURL && !missingOAuth,
MCPServerManifest: server.Spec.Manifest,
CatalogEntryID: server.Spec.MCPServerCatalogEntryName,
PowerUserWorkspaceID: server.Spec.PowerUserWorkspaceID,
Expand Down Expand Up @@ -2555,7 +2581,7 @@ func resolveCompositeComponents(req api.Context, composite v1.MCPServer) ([]type

addExtractedEnvVars(&component)
// No slug/URL needed; only Configured/NeedsURL are used from the component
convertedComponents = append(convertedComponents, ConvertMCPServer(component, cred.Env, "", ""))
convertedComponents = append(convertedComponents, ConvertMCPServer(req.Context(), req.GPTClient, component, cred.Env, "", ""))
}

return convertedComponents, nil
Expand Down Expand Up @@ -2662,7 +2688,7 @@ func (m *MCPHandler) ListServersFromAllSources(req api.Context) error {
return err
}
}
parent := ConvertMCPServer(server, credMap[server.Name], m.serverURL, slug, components...)
parent := ConvertMCPServer(req.Context(), req.GPTClient, server, credMap[server.Name], m.serverURL, slug, components...)
mcpServers = append(mcpServers, parent)
}

Expand Down Expand Up @@ -2736,7 +2762,7 @@ func (m *MCPHandler) GetServerFromAllSources(req api.Context) error {
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(server, cred.Env, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, server, cred.Env, m.serverURL, slug))
}

func (m *MCPHandler) ClearOAuthCredentials(req api.Context) error {
Expand Down Expand Up @@ -3067,7 +3093,7 @@ func (m *MCPHandler) RedeployWithK8sSettings(req api.Context) error {
}

// Return updated server
return req.Write(ConvertMCPServer(server, cred.Env, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, server, cred.Env, m.serverURL, slug))
}

// ListServersNeedingK8sUpdateInCatalog lists all servers in a catalog that need redeployment with new K8s settings
Expand Down Expand Up @@ -3322,7 +3348,7 @@ func (m *MCPHandler) UpdateURL(req api.Context) error {
return fmt.Errorf("failed to generate slug: %w", err)
}

return req.Write(ConvertMCPServer(mcpServer, nil, m.serverURL, slug))
return req.Write(ConvertMCPServer(req.Context(), req.GPTClient, mcpServer, nil, m.serverURL, slug))
}

func (m *MCPHandler) TriggerUpdate(req api.Context) error {
Expand Down
Loading
Loading