diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5b42458 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Octopus Deploy Configuration for Testing +# Copy this file to .env and fill in your actual values + +# Your Octopus Deploy instance URL +OCTOPUS_SERVER_URL=https://your-octopus-instance.octopus.app + +# Your Octopus Deploy API key +OCTOPUS_API_KEY=API-XXXXXXXXXXXXXXXXXXXXXXXXXX + +# Or use an access token (Bearer token) instead of an API key +# OCTOPUS_ACCESS_TOKEN=your-access-token-here + +# Space name to use for testing (optional, defaults to "Default") +TEST_SPACE_NAME=Default \ No newline at end of file diff --git a/README.md b/README.md index 0317767..7a3093d 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ We are planning to release a native ARM build shortly so that those arguments wi #### Requirements - Node.js >= v20.0.0 - Octopus Deploy instance that can be accessed by the MCP server via HTTPS -- Octopus Deploy API Key +- Octopus Deploy API Key or Access Token (see [Authentication](#authentication) below) #### Configuration @@ -118,6 +118,49 @@ Or with configuration supplied via the command line: npx -y @octopusdeploy/mcp-server --server-url https://your-octopus.com --api-key YOUR_API_KEY ``` +### Authentication + +The MCP server supports two authentication methods: + +#### API Key (recommended for interactive use) + +API keys are the standard authentication method for Octopus Deploy. You can generate one from your Octopus Deploy user profile. + +```bash +# Via environment variable +OCTOPUS_API_KEY=API-XXXXXXXXXXXXXXXXXXXXXXXXXX + +# Via command line argument +npx -y @octopusdeploy/mcp-server --api-key YOUR_API_KEY --server-url https://your-octopus.com +``` + +#### Access Token / Bearer Token (automated scenarios only) + +The server also supports short-lived access tokens (Bearer tokens) as an alternative to API keys. This authentication method is intended **only for automated scenarios** where an external system issues a short-lived token to the MCP server (e.g., CI/CD pipelines, automated orchestration, or machine-to-machine workflows). Do not use long-lived Bearer tokens — use API keys instead for interactive or long-running sessions. + +```bash +# Via environment variable +OCTOPUS_ACCESS_TOKEN=your-short-lived-token + +# Via command line argument +npx -y @octopusdeploy/mcp-server --access-token YOUR_TOKEN --server-url https://your-octopus.com +``` + +Full example configuration with an access token: +```json +{ + "mcpServers": { + "octopusdeploy": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@octopusdeploy/mcp-server", "--access-token", "YOUR_TOKEN", "--server-url", "https://your-octopus.com"] + } + } +} +``` + +If both an API key and an access token are provided, the access token takes precedence. + ### Configuration Options The Octopus MCP Server supports several command-line options to customize which tools are available. diff --git a/src/helpers/errorHandling.ts b/src/helpers/errorHandling.ts index 57e1dd1..a590db9 100644 --- a/src/helpers/errorHandling.ts +++ b/src/helpers/errorHandling.ts @@ -58,7 +58,7 @@ export function handleOctopusApiError( isErrorWithMessage(error, "provide a valid API key") ) { throw new Error( - "Authentication failed. Ensure OCTOPUS_API_KEY environment variable is set with a valid API key. " + + "Authentication failed. Ensure a valid API key (OCTOPUS_API_KEY) or access token (OCTOPUS_ACCESS_TOKEN) is provided. " + "You can generate an API key from your Octopus Deploy user profile.", ); } diff --git a/src/helpers/getClientConfigurationFromEnvironment.ts b/src/helpers/getClientConfigurationFromEnvironment.ts index 34f7608..2bd4a62 100644 --- a/src/helpers/getClientConfigurationFromEnvironment.ts +++ b/src/helpers/getClientConfigurationFromEnvironment.ts @@ -8,6 +8,7 @@ const USER_AGENT_NAME = "octopus-mcp-server"; export interface ConfigurationOptions { instanceURL?: string; apiKey?: string; + accessToken?: string; } function isEmpty(value: string | undefined): value is undefined | "" { @@ -22,14 +23,31 @@ function constructUserAgent(): string { } function getClientConfiguration(options: ConfigurationOptions = {}): ClientConfiguration { - if (isEmpty(options.instanceURL) || isEmpty(options.apiKey)) { + const hasApiKey = !isEmpty(options.apiKey); + const hasAccessToken = !isEmpty(options.accessToken); + + if (isEmpty(options.instanceURL)) { throw new Error( - "Octopus server URL and API key must be provided either via command line arguments (--server-url, --api-key) or environment variables (OCTOPUS_SERVER_URL, OCTOPUS_API_KEY)." + "Octopus server URL must be provided either via command line argument (--server-url) or environment variable (OCTOPUS_SERVER_URL)." + ); + } + + if (!hasApiKey && !hasAccessToken) { + throw new Error( + "Octopus authentication must be provided. Supply either an API key (--api-key or OCTOPUS_API_KEY) or an access token (--access-token or OCTOPUS_ACCESS_TOKEN)." ); } const userAgent = constructUserAgent(); + if (hasAccessToken) { + return { + userAgentApp: userAgent, + instanceURL: options.instanceURL, + accessToken: options.accessToken, + }; + } + return { userAgentApp: userAgent, instanceURL: options.instanceURL, @@ -41,5 +59,6 @@ export function getClientConfigurationFromEnvironment(): ClientConfiguration { return getClientConfiguration({ instanceURL: env["CLI_SERVER_URL"] || env["OCTOPUS_SERVER_URL"], apiKey: env["CLI_API_KEY"] || env["OCTOPUS_API_KEY"], + accessToken: env["CLI_ACCESS_TOKEN"] || env["OCTOPUS_ACCESS_TOKEN"], }); } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 99abcb4..6d96023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ program .version(SEMVER_VERSION) .option("-s, --server-url ", "Octopus server URL") .option("-k, --api-key ", "Octopus API key") + .option("-t, --access-token ", "Octopus access token (Bearer token)") .option( "--toolsets ", `Comma-separated list of toolsets to enable, or "all" (default: all). Available toolsets: ${DEFAULT_TOOLSETS.join(", ")}`, @@ -90,6 +91,9 @@ if (options.serverUrl) { if (options.apiKey) { process.env.CLI_API_KEY = options.apiKey; } +if (options.accessToken) { + process.env.CLI_ACCESS_TOKEN = options.accessToken; +} // Set up initialization callback to capture client info server.server.oninitialized = () => { diff --git a/src/tools/__tests__/testSetup.ts b/src/tools/__tests__/testSetup.ts index 4d321aa..4f39c68 100644 --- a/src/tools/__tests__/testSetup.ts +++ b/src/tools/__tests__/testSetup.ts @@ -7,6 +7,7 @@ config(); export const testConfig = { octopusServerUrl: process.env.OCTOPUS_SERVER_URL || process.env.CLI_SERVER_URL, octopusApiKey: process.env.OCTOPUS_API_KEY || process.env.CLI_API_KEY, + octopusAccessToken: process.env.OCTOPUS_ACCESS_TOKEN || process.env.CLI_ACCESS_TOKEN, testSpaceName: process.env.TEST_SPACE_NAME || "Default", timeout: 30000, // 30 seconds }; @@ -18,8 +19,8 @@ export function validateTestEnvironment(): void { missing.push("OCTOPUS_SERVER_URL (or CLI_SERVER_URL)"); } - if (!testConfig.octopusApiKey) { - missing.push("OCTOPUS_API_KEY (or CLI_API_KEY)"); + if (!testConfig.octopusApiKey && !testConfig.octopusAccessToken) { + missing.push("OCTOPUS_API_KEY (or CLI_API_KEY) or OCTOPUS_ACCESS_TOKEN (or CLI_ACCESS_TOKEN)"); } if (missing.length > 0) {