diff --git a/packages/local-mcp-server/README.md b/packages/local-mcp-server/README.md index 2401c9dc..78bc95b8 100644 --- a/packages/local-mcp-server/README.md +++ b/packages/local-mcp-server/README.md @@ -41,10 +41,13 @@ To configure this MCP server in your MCP client (such as Claude Desktop, Windsur ```bash # Configure for Cursor -npx @gleanwork/configure-mcp-server --client cursor --token your_api_token --instance instance_name +npx @gleanwork/configure-mcp-server --client cursor --token your_api_token --server-url https://your-company-be.glean.com # Configure for Claude Desktop -npx @gleanwork/configure-mcp-server --client claude --token your_api_token --instance instance_name +npx @gleanwork/configure-mcp-server --client claude --token your_api_token --server-url https://your-company-be.glean.com + +# Using deprecated --instance flag (use --server-url instead) +# npx @gleanwork/configure-mcp-server --client cursor --token your_api_token --instance instance_name ``` For more details see: [@gleanwork/configure-mcp-server](https://github.com/gleanwork/configure-mcp-server). @@ -60,7 +63,7 @@ To manually configure an MCP client (such as Claude Desktop, Windsurf, Cursor, e "command": "npx", "args": ["-y", "@gleanwork/local-mcp-server"], "env": { - "GLEAN_INSTANCE": "", + "GLEAN_SERVER_URL": "", "GLEAN_API_TOKEN": "" } } @@ -98,7 +101,7 @@ Configure your MCP client to use the Docker image. Most MCP clients support pass "ghcr.io/gleanwork/local-mcp-server:latest" ], "env": { - "GLEAN_INSTANCE": "your-instance", + "GLEAN_SERVER_URL": "https://your-instance-be.glean.com", "GLEAN_API_TOKEN": "your-token" } } @@ -118,7 +121,7 @@ If your MCP client doesn't pass the `env` block to Docker, use `-e` flags in the "-i", "--rm", "-e", - "GLEAN_INSTANCE=your-instance", + "GLEAN_SERVER_URL=https://your-instance-be.glean.com", "-e", "GLEAN_API_TOKEN=your-token", "ghcr.io/gleanwork/local-mcp-server:latest" @@ -132,7 +135,8 @@ If your MCP client doesn't pass the `env` block to Docker, use `-e` flags in the ### Environment Variables -- `GLEAN_INSTANCE` (required): Your Glean instance name +- `GLEAN_SERVER_URL` (recommended): Your Glean server URL (e.g. `https://your-instance-be.glean.com`) +- `GLEAN_INSTANCE`: Your Glean instance name (deprecated alternative to `GLEAN_SERVER_URL`) - `GLEAN_API_TOKEN` (required): Your Glean API token ### Troubleshooting @@ -146,7 +150,7 @@ If your MCP client doesn't pass the `env` block to Docker, use `-e` flags in the **Permission or authentication errors:** - Verify your `GLEAN_API_TOKEN` is valid -- Check your `GLEAN_INSTANCE` matches your Glean deployment +- Check your `GLEAN_SERVER_URL` or `GLEAN_INSTANCE` matches your Glean deployment **MCP client can't connect:** diff --git a/packages/local-mcp-server/src/common/client.ts b/packages/local-mcp-server/src/common/client.ts index 6df7e4b8..cc845e50 100644 --- a/packages/local-mcp-server/src/common/client.ts +++ b/packages/local-mcp-server/src/common/client.ts @@ -3,7 +3,9 @@ * * This module provides a client for interacting with the Glean API. * - * Required environment variables: + * Required environment variables (one of): + * - GLEAN_SERVER_URL: Full Glean server URL (recommended, highest priority) + * - GLEAN_URL: Full Glean URL (alternative) * - GLEAN_INSTANCE or GLEAN_SUBDOMAIN: Name of the Glean instance * - GLEAN_API_TOKEN: API token for authentication * diff --git a/packages/local-mcp-server/src/index.ts b/packages/local-mcp-server/src/index.ts index 32ee8a46..5ada2faa 100644 --- a/packages/local-mcp-server/src/index.ts +++ b/packages/local-mcp-server/src/index.ts @@ -24,20 +24,26 @@ async function main() { $ npx @gleanwork/local-mcp-server [options] Options - --instance, -i Glean instance name + --server-url, -s Glean server URL (e.g. https://my-company-be.glean.com) + --instance, -i Glean instance name (deprecated, use --server-url instead) --token, -t Glean API token --help, -h Show this help message --trace Enable trace logging Examples $ npx @gleanwork/local-mcp-server - $ npx @gleanwork/local-mcp-server --instance my-company --token glean_api_xyz + $ npx @gleanwork/local-mcp-server --server-url https://my-company-be.glean.com --token glean_api_xyz + $ npx @gleanwork/local-mcp-server --instance my-company --token glean_api_xyz # deprecated, use --server-url Version: v${VERSION} `, { importMeta: import.meta, flags: { + serverUrl: { + type: 'string', + shortFlag: 's', + }, token: { type: 'string', shortFlag: 't', @@ -66,8 +72,8 @@ async function main() { await checkAndOpenLaunchWarning(VERSION); - const { instance, token } = cli.flags; - runServer({ instance, token }).catch((error) => { + const { serverUrl, instance, token } = cli.flags; + runServer({ serverUrl, instance, token }).catch((error) => { console.error('Error starting MCP server:', error); process.exit(1); }); diff --git a/packages/local-mcp-server/src/server.ts b/packages/local-mcp-server/src/server.ts index 9e8a43a1..d37535bc 100644 --- a/packages/local-mcp-server/src/server.ts +++ b/packages/local-mcp-server/src/server.ts @@ -240,10 +240,16 @@ server.setRequestHandler(CallToolRequestSchema, callToolHandler); * @throws {Error} If server initialization or connection fails */ export async function runServer(options?: { + serverUrl?: string; instance?: string; token?: string; }) { // Set environment variables from command line args if provided + if (options?.serverUrl) { + process.env.GLEAN_SERVER_URL = options.serverUrl; + } + + // GLEAN_INSTANCE is deprecated; prefer GLEAN_SERVER_URL / --server-url if (options?.instance) { process.env.GLEAN_INSTANCE = options.instance; } diff --git a/packages/local-mcp-server/src/test/server.test.ts b/packages/local-mcp-server/src/test/server.test.ts index ad334dbe..54f617de 100644 --- a/packages/local-mcp-server/src/test/server.test.ts +++ b/packages/local-mcp-server/src/test/server.test.ts @@ -5,12 +5,14 @@ import '@gleanwork/mcp-test-utils/mocks/setup'; describe('MCP Server Handlers (integration)', () => { beforeEach(() => { + delete process.env.GLEAN_SERVER_URL; delete process.env.GLEAN_URL; process.env.GLEAN_INSTANCE = 'test'; process.env.GLEAN_API_TOKEN = 'test-token'; }); afterEach(() => { + delete process.env.GLEAN_SERVER_URL; delete process.env.GLEAN_INSTANCE; delete process.env.GLEAN_API_TOKEN; }); diff --git a/packages/mcp-server-utils/src/config/index.ts b/packages/mcp-server-utils/src/config/index.ts index f165f847..cddc8754 100644 --- a/packages/mcp-server-utils/src/config/index.ts +++ b/packages/mcp-server-utils/src/config/index.ts @@ -54,9 +54,17 @@ export async function getConfig(): Promise { return getLocalConfig(); } +function normalizeUrl(url: string): string { + if (!/^https?:\/\//i.test(url)) { + return `https://${url}`; + } + return url; +} + function getLocalConfig(): GleanConfig { const instance = process.env.GLEAN_INSTANCE || process.env.GLEAN_SUBDOMAIN; - const baseUrl = process.env.GLEAN_URL; + const serverUrl = process.env.GLEAN_SERVER_URL; + const baseUrl = serverUrl ? normalizeUrl(serverUrl) : process.env.GLEAN_URL; const token = process.env.GLEAN_API_TOKEN; const actAs = process.env.GLEAN_ACT_AS; diff --git a/packages/mcp-server-utils/src/test/config/config.test.ts b/packages/mcp-server-utils/src/test/config/config.test.ts index 20b5aae2..9a6939cd 100644 --- a/packages/mcp-server-utils/src/test/config/config.test.ts +++ b/packages/mcp-server-utils/src/test/config/config.test.ts @@ -105,4 +105,51 @@ describe('getConfig', () => { expect(config.baseUrl).toBe('https://test-subdomain-be.glean.com/'); }); + + it('uses GLEAN_SERVER_URL when provided', async () => { + process.env.GLEAN_SERVER_URL = 'https://custom-be.glean.com/'; + process.env.GLEAN_API_TOKEN = 'test-token'; + + const config = await getConfig(); + + expect(config.baseUrl).toBe('https://custom-be.glean.com/'); + }); + + it('GLEAN_SERVER_URL takes precedence over GLEAN_URL', async () => { + process.env.GLEAN_SERVER_URL = 'https://server-url-be.glean.com/'; + process.env.GLEAN_URL = 'https://glean-url-be.glean.com/'; + process.env.GLEAN_API_TOKEN = 'test-token'; + + const config = await getConfig(); + + expect(config.baseUrl).toBe('https://server-url-be.glean.com/'); + }); + + it('GLEAN_SERVER_URL takes precedence over GLEAN_INSTANCE', async () => { + process.env.GLEAN_SERVER_URL = 'https://server-url-be.glean.com/'; + process.env.GLEAN_INSTANCE = 'test-company'; + process.env.GLEAN_API_TOKEN = 'test-token'; + + const config = await getConfig(); + + expect(config.baseUrl).toBe('https://server-url-be.glean.com/'); + }); + + it('normalizes schemeless GLEAN_SERVER_URL by adding https://', async () => { + process.env.GLEAN_SERVER_URL = 'acme-be.glean.com'; + process.env.GLEAN_API_TOKEN = 'test-token'; + + const config = await getConfig(); + + expect(config.baseUrl).toBe('https://acme-be.glean.com'); + }); + + it('preserves GLEAN_SERVER_URL that already has https://', async () => { + process.env.GLEAN_SERVER_URL = 'https://acme-be.glean.com'; + process.env.GLEAN_API_TOKEN = 'test-token'; + + const config = await getConfig(); + + expect(config.baseUrl).toBe('https://acme-be.glean.com'); + }); }); diff --git a/packages/mcp-server-utils/src/util/preflight.ts b/packages/mcp-server-utils/src/util/preflight.ts index 0d8f10bf..8ad2bc65 100644 --- a/packages/mcp-server-utils/src/util/preflight.ts +++ b/packages/mcp-server-utils/src/util/preflight.ts @@ -1,13 +1,20 @@ import { trace, error } from '../log/logger.js'; /** - * Validates that the given instance name is valid by checking its liveness endpoint. - * Makes a fetch request to https://{instance}-be.glean.com/liveness_check + * Validates that the given instance name or server URL is valid by checking its liveness endpoint. + * When GLEAN_SERVER_URL is set, validates using that URL directly. + * Otherwise, makes a fetch request to https://{instance}-be.glean.com/liveness_check * * @param instance - The instance name to validate * @returns A Promise that resolves to true if the instance is valid */ export async function validateInstance(instance: string): Promise { + // If GLEAN_SERVER_URL is set, skip instance name validation and validate the server URL directly + const serverUrl = process.env.GLEAN_SERVER_URL; + if (serverUrl) { + return validateServerUrl(serverUrl); + } + if (!instance) { trace('No instance provided for validation'); return false; @@ -40,3 +47,39 @@ export async function validateInstance(instance: string): Promise { return false; } } + +/** + * Validates a server URL by checking its liveness endpoint. + * + * @param serverUrl - The full server URL to validate + * @returns A Promise that resolves to true if the server is reachable + */ +async function validateServerUrl(serverUrl: string): Promise { + try { + const normalizedUrl = /^https?:\/\//i.test(serverUrl) + ? serverUrl + : `https://${serverUrl}`; + const url = `${normalizedUrl.replace(/\/+$/, '')}/liveness_check`; + trace(`Checking server URL validity with: ${url}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + error( + `Server URL validation failed for ${serverUrl}: ${response.status} ${response.statusText}`, + ); + return false; + } + + return true; + } catch (err) { + const cause = err instanceof Error ? err : new Error(String(err)); + error(`Server URL validation failed: ${cause.message}`); + return false; + } +}