diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 06efc8d5afd64..a55bd137f7994 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -340,6 +340,11 @@ jobs: - name: Disable Spotlight Indexing if: runner.os == 'macOS' run: | + enabled=$(sudo mdutil -a -s | grep "Indexing enabled" | wc -l) + if [ $enabled -eq 0 ]; then + echo "Spotlight indexing is already disabled" + exit 0 + fi sudo mdutil -a -i off sudo mdutil -X / sudo launchctl bootout system /System/Library/LaunchDaemons/com.apple.metadata.mds.plist diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9feaf72b938ff..6ea28ad87a90c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -60,7 +60,7 @@ jobs: - name: Switch XCode Version uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0 with: - xcode-version: "16.0.0" + xcode-version: "16.1.0" - name: Setup Go uses: ./.github/actions/setup-go @@ -655,7 +655,7 @@ jobs: detached_signature="${binary}.asc" gcloud storage cp "./site/out/bin/${binary}" "gs://releases.coder.com/coder-cli/${version}/${binary}" gcloud storage cp "./site/out/bin/${detached_signature}" "gs://releases.coder.com/coder-cli/${version}/${detached_signature}" - done + done - name: Publish release run: | diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index c8cba67cea73f..dce0fb27bfc25 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -77,7 +77,8 @@ type API struct { subAgentURL string subAgentEnv []string - projectDiscovery bool // If we should perform project discovery or not. + projectDiscovery bool // If we should perform project discovery or not. + discoveryAutostart bool // If we should autostart discovered projects. ownerName string workspaceName string @@ -144,7 +145,8 @@ func WithCommandEnv(ce CommandEnv) Option { strings.HasPrefix(s, "CODER_AGENT_TOKEN=") || strings.HasPrefix(s, "CODER_AGENT_AUTH=") || strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_ENABLE=") || - strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") || + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=") }) return shell, dir, env, nil } @@ -287,6 +289,14 @@ func WithProjectDiscovery(projectDiscovery bool) Option { } } +// WithDiscoveryAutostart sets if the API should attempt to autostart +// projects that have been discovered +func WithDiscoveryAutostart(discoveryAutostart bool) Option { + return func(api *API) { + api.discoveryAutostart = discoveryAutostart + } +} + // ScriptLogger is an interface for sending devcontainer logs to the // controlplane. type ScriptLogger interface { @@ -542,11 +552,13 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { Container: nil, } - config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{}) - if err != nil { - logger.Error(api.ctx, "read project configuration", slog.Error(err)) - } else if config.Configuration.Customizations.Coder.AutoStart { - dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting + if api.discoveryAutostart { + config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{}) + if err != nil { + logger.Error(api.ctx, "read project configuration", slog.Error(err)) + } else if config.Configuration.Customizations.Coder.AutoStart { + dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting + } } api.knownDevcontainers[workspaceFolder] = dc diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index e6e25ed92558e..76e0fca6714d5 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3792,6 +3792,7 @@ func TestDevcontainerDiscovery(t *testing.T) { agentcontainers.WithContainerCLI(&fakeContainerCLI{}), agentcontainers.WithDevcontainerCLI(mDCCLI), agentcontainers.WithProjectDiscovery(true), + agentcontainers.WithDiscoveryAutostart(true), ) api.Start() defer api.Close() @@ -3813,5 +3814,74 @@ func TestDevcontainerDiscovery(t *testing.T) { // Then: We expect the mock infra to not fail. }) } + + t.Run("Disabled", func(t *testing.T) { + t.Parallel() + var ( + ctx = testutil.Context(t, testutil.WaitShort) + logger = testutil.Logger(t) + mClock = quartz.NewMock(t) + mDCCLI = acmock.NewMockDevcontainerCLI(gomock.NewController(t)) + + fs = map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + } + + r = chi.NewRouter() + ) + + // We expect that neither `ReadConfig`, nor `Up` are called as we + // have explicitly disabled the agentcontainers API from attempting + // to autostart devcontainers that it discovers. + mDCCLI.EXPECT().ReadConfig(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + []string{}, + ).Return(agentcontainers.DevcontainerConfig{ + Configuration: agentcontainers.DevcontainerConfiguration{ + Customizations: agentcontainers.DevcontainerCustomizations{ + Coder: agentcontainers.CoderCustomization{ + AutoStart: true, + }, + }, + }, + }, nil).Times(0) + + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil).Times(0) + + api := agentcontainers.NewAPI(logger, + agentcontainers.WithClock(mClock), + agentcontainers.WithWatcher(watcher.NewNoop()), + agentcontainers.WithFileSystem(initFS(t, fs)), + agentcontainers.WithManifestInfo("owner", "workspace", "parent-agent", "/home/coder"), + agentcontainers.WithContainerCLI(&fakeContainerCLI{}), + agentcontainers.WithDevcontainerCLI(mDCCLI), + agentcontainers.WithProjectDiscovery(true), + agentcontainers.WithDiscoveryAutostart(false), + ) + api.Start() + defer api.Close() + r.Mount("/", api.Routes()) + + // When: All expected dev containers have been found. + require.Eventuallyf(t, func() bool { + req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + got := codersdk.WorkspaceAgentListContainersResponse{} + err := json.NewDecoder(rec.Body).Decode(&got) + require.NoError(t, err) + + return len(got.Devcontainers) >= 1 + }, testutil.WaitShort, testutil.IntervalFast, "dev containers never found") + + // Then: We expect the mock infra to not fail. + }) }) } diff --git a/cli/agent.go b/cli/agent.go index 4f50fbfe88942..c192d4429ccaf 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -40,23 +40,24 @@ import ( func (r *RootCmd) workspaceAgent() *serpent.Command { var ( - auth string - logDir string - scriptDataDir string - pprofAddress string - noReap bool - sshMaxTimeout time.Duration - tailnetListenPort int64 - prometheusAddress string - debugAddress string - slogHumanPath string - slogJSONPath string - slogStackdriverPath string - blockFileTransfer bool - agentHeaderCommand string - agentHeader []string - devcontainers bool - devcontainerProjectDiscovery bool + auth string + logDir string + scriptDataDir string + pprofAddress string + noReap bool + sshMaxTimeout time.Duration + tailnetListenPort int64 + prometheusAddress string + debugAddress string + slogHumanPath string + slogJSONPath string + slogStackdriverPath string + blockFileTransfer bool + agentHeaderCommand string + agentHeader []string + devcontainers bool + devcontainerProjectDiscovery bool + devcontainerDiscoveryAutostart bool ) cmd := &serpent.Command{ Use: "agent", @@ -366,6 +367,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { DevcontainerAPIOptions: []agentcontainers.Option{ agentcontainers.WithSubAgentURL(r.agentURL.String()), agentcontainers.WithProjectDiscovery(devcontainerProjectDiscovery), + agentcontainers.WithDiscoveryAutostart(devcontainerDiscoveryAutostart), }, }) @@ -519,6 +521,13 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { Description: "Allow the agent to search the filesystem for devcontainer projects.", Value: serpent.BoolOf(&devcontainerProjectDiscovery), }, + { + Flag: "devcontainers-discovery-autostart-enable", + Default: "false", + Env: "CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE", + Description: "Allow the agent to autostart devcontainer projects it discovers based on their configuration.", + Value: serpent.BoolOf(&devcontainerDiscoveryAutostart), + }, } return cmd diff --git a/cli/testdata/coder_agent_--help.golden b/cli/testdata/coder_agent_--help.golden index 0627016855e08..c6d75705a6eb4 100644 --- a/cli/testdata/coder_agent_--help.golden +++ b/cli/testdata/coder_agent_--help.golden @@ -33,6 +33,10 @@ OPTIONS: --debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113) The bind address to serve a debug HTTP server. + --devcontainers-discovery-autostart-enable bool, $CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE (default: false) + Allow the agent to autostart devcontainer projects it discovers based + on their configuration. + --devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: true) Allow the agent to automatically detect running devcontainers. diff --git a/coderd/dynamicparameters/error.go b/coderd/dynamicparameters/error.go index 4c27905bfa832..ae2217936b9dd 100644 --- a/coderd/dynamicparameters/error.go +++ b/coderd/dynamicparameters/error.go @@ -26,6 +26,14 @@ func tagValidationError(diags hcl.Diagnostics) *DiagnosticError { } } +func presetValidationError(diags hcl.Diagnostics) *DiagnosticError { + return &DiagnosticError{ + Message: "Unable to validate presets", + Diagnostics: diags, + KeyedDiagnostics: make(map[string]hcl.Diagnostics), + } +} + type DiagnosticError struct { // Message is the human-readable message that will be returned to the user. Message string diff --git a/coderd/dynamicparameters/presets.go b/coderd/dynamicparameters/presets.go new file mode 100644 index 0000000000000..24974962e029f --- /dev/null +++ b/coderd/dynamicparameters/presets.go @@ -0,0 +1,28 @@ +package dynamicparameters + +import ( + "github.com/hashicorp/hcl/v2" + + "github.com/coder/preview" +) + +// CheckPresets extracts the preset related diagnostics from a template version preset +func CheckPresets(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { + de := presetValidationError(diags) + if output == nil { + return de + } + + presets := output.Presets + for _, preset := range presets { + if hcl.Diagnostics(preset.Diagnostics).HasErrors() { + de.Extend(preset.Name, hcl.Diagnostics(preset.Diagnostics)) + } + } + + if de.HasError() { + return de + } + + return nil +} diff --git a/coderd/dynamicparameters/tags.go b/coderd/dynamicparameters/tags.go index 38a9bf4691571..d9037db5dd909 100644 --- a/coderd/dynamicparameters/tags.go +++ b/coderd/dynamicparameters/tags.go @@ -11,6 +11,10 @@ import ( func CheckTags(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { de := tagValidationError(diags) + if output == nil { + return de + } + failedTags := output.WorkspaceTags.UnusableTags() if len(failedTags) == 0 && !de.HasError() { return nil // No errors, all is good! diff --git a/coderd/templateversions.go b/coderd/templateversions.go index cc106b390f73c..2a6e09d94978e 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1822,6 +1822,14 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response return nil, false } + // Fails early if presets are invalid to prevent downstream workspace creation errors + presetErr := dynamicparameters.CheckPresets(output, nil) + if presetErr != nil { + code, resp := presetErr.Response() + httpapi.Write(ctx, rw, code, resp) + return nil, false + } + return output.WorkspaceTags.Tags(), true } diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 912bca1c5fec1..0b5bf6fcf2302 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -620,6 +620,119 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { }) } }) + + t.Run("Presets", func(t *testing.T) { + t.Parallel() + store, ps := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: store, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + for _, tt := range []struct { + name string + files map[string]string + expectError string + }{ + { + name: "valid preset", + files: map[string]string{ + `main.tf`: ` + terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.8.0" + } + } + } + data "coder_parameter" "valid_parameter" { + name = "valid_parameter_name" + default = "valid_option_value" + option { + name = "valid_option_name" + value = "valid_option_value" + } + } + data "coder_workspace_preset" "valid_preset" { + name = "valid_preset" + parameters = { + "valid_parameter_name" = "valid_option_value" + } + } + `, + }, + }, + { + name: "invalid preset", + files: map[string]string{ + `main.tf`: ` + terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.8.0" + } + } + } + data "coder_parameter" "valid_parameter" { + name = "valid_parameter_name" + default = "valid_option_value" + option { + name = "valid_option_name" + value = "valid_option_value" + } + } + data "coder_workspace_preset" "invalid_parameter_name" { + name = "invalid_parameter_name" + parameters = { + "invalid_parameter_name" = "irrelevant_value" + } + } + `, + }, + expectError: "Undefined Parameter", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + // Create an archive from the files provided in the test case. + tarFile := testutil.CreateTar(t, tt.files) + + // Post the archive file + fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile)) + require.NoError(t, err) + + // Create a template version from the archive + tvName := testutil.GetRandomNameHyphenated(t) + tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: tvName, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + FileID: fi.ID, + }) + + if tt.expectError == "" { + require.NoError(t, err) + // Assert the expected provisioner job is created from the template version import + pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID) + require.NoError(t, err) + require.NotNil(t, pj) + // Also assert that we get the expected information back from the API endpoint + require.Zero(t, tv.MatchedProvisioners.Count) + require.Zero(t, tv.MatchedProvisioners.Available) + require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time) + } else { + require.ErrorContains(t, err, tt.expectError) + require.Equal(t, tv.Job.ID, uuid.Nil) + } + }) + } + }) } func TestPatchCancelTemplateVersion(t *testing.T) { diff --git a/docs/admin/integrations/oauth2-provider.md b/docs/admin/integrations/oauth2-provider.md new file mode 100644 index 0000000000000..e5264904293f7 --- /dev/null +++ b/docs/admin/integrations/oauth2-provider.md @@ -0,0 +1,236 @@ +# OAuth2 Provider (Experimental) + +> [!WARNING] +> The OAuth2 provider functionality is currently **experimental and unstable**. This feature: +> +> - Is subject to breaking changes without notice +> - May have incomplete functionality +> - Is not recommended for production use +> - Requires the `oauth2` experiment flag to be enabled +> +> Use this feature for development and testing purposes only. + +Coder can act as an OAuth2 authorization server, allowing third-party applications to authenticate users through Coder and access the Coder API on their behalf. This enables integrations where external applications can leverage Coder's authentication and user management. + +## Requirements + +- Admin privileges in Coder +- OAuth2 experiment flag enabled +- HTTPS recommended for production deployments + +## Enable OAuth2 Provider + +Add the `oauth2` experiment flag to your Coder server: + +```bash +coder server --experiments oauth2 +``` + +Or set the environment variable: + +```env +CODER_EXPERIMENTS=oauth2 +``` + +## Creating OAuth2 Applications + +### Method 1: Web UI + +1. Navigate to **Deployment Settings** → **OAuth2 Applications** +2. Click **Create Application** +3. Fill in the application details: + - **Name**: Your application name + - **Callback URL**: `https://yourapp.example.com/callback` + - **Icon**: Optional icon URL + +### Method 2: Management API + +Create an application using the Coder API: + +```bash +curl -X POST \ + -H "Authorization: Bearer $CODER_SESSION_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My Application", + "callback_url": "https://myapp.example.com/callback", + "icon": "https://myapp.example.com/icon.png" + }' \ + "$CODER_URL/api/v2/oauth2-provider/apps" +``` + +Generate a client secret: + +```bash +curl -X POST \ + -H "Authorization: Bearer $CODER_SESSION_TOKEN" \ + "$CODER_URL/api/v2/oauth2-provider/apps/$APP_ID/secrets" +``` + +## Integration Patterns + +### Standard OAuth2 Flow + +1. **Authorization Request**: Redirect users to Coder's authorization endpoint: + + ```url + https://coder.example.com/oauth2/authorize? + client_id=your-client-id& + response_type=code& + redirect_uri=https://yourapp.example.com/callback& + state=random-string + ``` + +2. **Token Exchange**: Exchange the authorization code for an access token: + + ```bash + curl -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code" \ + -d "code=$AUTH_CODE" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + -d "redirect_uri=https://yourapp.example.com/callback" \ + "$CODER_URL/oauth2/tokens" + ``` + +3. **API Access**: Use the access token to call Coder's API: + + ```bash + curl -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$CODER_URL/api/v2/users/me" + ``` + +### PKCE Flow (Public Clients) + +For mobile apps and single-page applications, use PKCE for enhanced security: + +1. Generate a code verifier and challenge: + + ```bash + CODE_VERIFIER=$(openssl rand -base64 96 | tr -d "=+/" | cut -c1-128) + CODE_CHALLENGE=$(echo -n $CODE_VERIFIER | openssl dgst -sha256 -binary | base64 | tr -d "=+/" | cut -c1-43) + ``` + +2. Include PKCE parameters in the authorization request: + + ```url + https://coder.example.com/oauth2/authorize? + client_id=your-client-id& + response_type=code& + code_challenge=$CODE_CHALLENGE& + code_challenge_method=S256& + redirect_uri=https://yourapp.example.com/callback + ``` + +3. Include the code verifier in the token exchange: + + ```bash + curl -X POST \ + -d "grant_type=authorization_code" \ + -d "code=$AUTH_CODE" \ + -d "client_id=$CLIENT_ID" \ + -d "code_verifier=$CODE_VERIFIER" \ + "$CODER_URL/oauth2/tokens" + ``` + +## Discovery Endpoints + +Coder provides OAuth2 discovery endpoints for programmatic integration: + +- **Authorization Server Metadata**: `GET /.well-known/oauth-authorization-server` +- **Protected Resource Metadata**: `GET /.well-known/oauth-protected-resource` + +These endpoints return server capabilities and endpoint URLs according to [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) and [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728). + +## Token Management + +### Refresh Tokens + +Refresh an expired access token: + +```bash +curl -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=refresh_token" \ + -d "refresh_token=$REFRESH_TOKEN" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + "$CODER_URL/oauth2/tokens" +``` + +### Revoke Access + +Revoke all tokens for an application: + +```bash +curl -X DELETE \ + -H "Authorization: Bearer $CODER_SESSION_TOKEN" \ + "$CODER_URL/oauth2/tokens?client_id=$CLIENT_ID" +``` + +## Testing and Development + +Coder provides comprehensive test scripts for OAuth2 development: + +```bash +# Navigate to the OAuth2 test scripts +cd scripts/oauth2/ + +# Run the full automated test suite +./test-mcp-oauth2.sh + +# Create a test application for manual testing +eval $(./setup-test-app.sh) + +# Run an interactive browser-based test +./test-manual-flow.sh + +# Clean up when done +./cleanup-test-app.sh +``` + +For more details on testing, see the [OAuth2 test scripts README](../../../scripts/oauth2/README.md). + +## Common Issues + +### "OAuth2 experiment not enabled" + +Add `oauth2` to your experiment flags: `coder server --experiments oauth2` + +### "Invalid redirect_uri" + +Ensure the redirect URI in your request exactly matches the one registered for your application. + +### "PKCE verification failed" + +Verify that the `code_verifier` used in the token request matches the one used to generate the `code_challenge`. + +## Security Considerations + +- **Use HTTPS**: Always use HTTPS in production to protect tokens in transit +- **Implement PKCE**: Use PKCE for all public clients (mobile apps, SPAs) +- **Validate redirect URLs**: Only register trusted redirect URIs for your applications +- **Rotate secrets**: Periodically rotate client secrets using the management API + +## Limitations + +As an experimental feature, the current implementation has limitations: + +- No scope system - all tokens have full API access +- No client credentials grant support +- Limited to opaque access tokens (no JWT support) + +## Standards Compliance + +This implementation follows established OAuth2 standards including [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) (OAuth2 core), [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) (PKCE), and related specifications for discovery and client registration. + +## Next Steps + +- Review the [API Reference](../../reference/api/index.md) for complete endpoint documentation +- Check [External Authentication](../external-auth/index.md) for configuring Coder as an OAuth2 client +- See [Security Best Practices](../security/index.md) for deployment security guidance + +## Feedback + +This is an experimental feature under active development. Please report issues and feedback through [GitHub Issues](https://github.com/coder/coder/issues) with the `oauth2` label. diff --git a/docs/ai-coder/mcp-server.md b/docs/ai-coder/mcp-server.md index 29d602030ab58..fdfadb4117d36 100644 --- a/docs/ai-coder/mcp-server.md +++ b/docs/ai-coder/mcp-server.md @@ -1,6 +1,6 @@ # MCP Server -Power users can configure Claude Desktop, Cursor, or other external agents to interact with Coder in order to: +Power users can configure [claude.ai](https://claude.ai), Claude Desktop, Cursor, or other external agents to interact with Coder in order to: - List workspaces - Create/start/stop workspaces @@ -12,6 +12,8 @@ Power users can configure Claude Desktop, Cursor, or other external agents to in In this model, any custom agent could interact with a remote Coder workspace, or Coder can be used in a remote pipeline or a larger workflow. +## Local MCP server + The Coder CLI has options to automatically configure MCP servers for you. On your local machine, run the following command: ```sh @@ -30,4 +32,27 @@ coder exp mcp server ``` > [!NOTE] -> The MCP server is authenticated with the same identity as your Coder CLI and can perform any action on the user's behalf. Fine-grained permissions and a remote MCP server are in development. [Contact us](https://coder.com/contact) if this use case is important to you. +> The MCP server is authenticated with the same identity as your Coder CLI and can perform any action on the user's behalf. Fine-grained permissions are in development. [Contact us](https://coder.com/contact) if this use case is important to you. + +## Remote MCP server + +Coder can expose an MCP server via HTTP. This is useful for connecting web-based agents, like https://claude.ai/, to Coder. This is an experimental feature and is subject to change. + +To enable this feature, activate the `oauth2` and `mcp-server-http` experiments using an environment variable or a CLI flag: + +```sh +CODER_EXPERIMENTS="oauth2,mcp-server-http" coder server +# or +coder server --experiments=oauth2,mcp-server-http +``` + +The Coder server will expose the MCP server at: + +```txt +https://coder.example.com/api/experimental/mcp/http +``` + +> [!NOTE] +> At this time, the remote MCP server is not compatible with web-based ChatGPT. + +Users can authenticate applications to use the remote MCP server with [OAuth2](../admin/integrations/oauth2-provider.md). An authenticated application can perform any action on the user's behalf. Fine-grained permissions are in development. diff --git a/docs/manifest.json b/docs/manifest.json index 0305105c029fd..9959fab8f79c0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -718,6 +718,11 @@ "title": "Hashicorp Vault", "description": "Integrate Coder with Hashicorp Vault", "path": "./admin/integrations/vault.md" + }, + { + "title": "OAuth2 Provider", + "description": "Use Coder as an OAuth2 provider", + "path": "./admin/integrations/oauth2-provider.md" } ] },
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: