From 28e81ea75c17675cb2118eb0f2c42e92a9e7745b Mon Sep 17 00:00:00 2001 From: Chris Freeman Date: Wed, 4 Mar 2026 10:57:37 -0700 Subject: [PATCH 1/2] feat: add GLEAN_SERVER_URL support with highest priority --- packages/local-mcp-server/README.md | 11 +++-- .../local-mcp-server/src/common/client.ts | 4 +- packages/local-mcp-server/src/index.ts | 10 +++- packages/local-mcp-server/src/server.ts | 5 ++ .../local-mcp-server/src/test/server.test.ts | 2 + packages/mcp-server-utils/src/config/index.ts | 10 +++- .../src/test/config/config.test.ts | 47 +++++++++++++++++++ .../mcp-server-utils/src/util/preflight.ts | 47 ++++++++++++++++++- 8 files changed, 125 insertions(+), 11 deletions(-) diff --git a/packages/local-mcp-server/README.md b/packages/local-mcp-server/README.md index 2401c9dc..10018584 100644 --- a/packages/local-mcp-server/README.md +++ b/packages/local-mcp-server/README.md @@ -60,7 +60,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 +98,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 +118,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 +132,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 (alternative to `GLEAN_SERVER_URL`) - `GLEAN_API_TOKEN` (required): Your Glean API token ### Troubleshooting @@ -146,7 +147,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..c0144565 100644 --- a/packages/local-mcp-server/src/index.ts +++ b/packages/local-mcp-server/src/index.ts @@ -24,6 +24,7 @@ async function main() { $ npx @gleanwork/local-mcp-server [options] Options + --server-url, -s Glean server URL (e.g. https://my-company-be.glean.com) --instance, -i Glean instance name --token, -t Glean API token --help, -h Show this help message @@ -31,6 +32,7 @@ async function main() { Examples $ npx @gleanwork/local-mcp-server + $ 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 Version: v${VERSION} @@ -38,6 +40,10 @@ async function main() { { 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..fa7881d6 100644 --- a/packages/local-mcp-server/src/server.ts +++ b/packages/local-mcp-server/src/server.ts @@ -240,10 +240,15 @@ 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; + } + 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; + } +} From f95369ca58c89e42ba5d80ba378e3a1ffb8c90b2 Mon Sep 17 00:00:00 2001 From: Chris Freeman Date: Wed, 4 Mar 2026 14:54:12 -0700 Subject: [PATCH 2/2] docs: use fully qualified URLs and mark instance as deprecated --- packages/local-mcp-server/README.md | 9 ++++++--- packages/local-mcp-server/src/index.ts | 4 ++-- packages/local-mcp-server/src/server.ts | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/local-mcp-server/README.md b/packages/local-mcp-server/README.md index 10018584..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). @@ -133,7 +136,7 @@ If your MCP client doesn't pass the `env` block to Docker, use `-e` flags in the ### Environment Variables - `GLEAN_SERVER_URL` (recommended): Your Glean server URL (e.g. `https://your-instance-be.glean.com`) -- `GLEAN_INSTANCE`: Your Glean instance name (alternative to `GLEAN_SERVER_URL`) +- `GLEAN_INSTANCE`: Your Glean instance name (deprecated alternative to `GLEAN_SERVER_URL`) - `GLEAN_API_TOKEN` (required): Your Glean API token ### Troubleshooting diff --git a/packages/local-mcp-server/src/index.ts b/packages/local-mcp-server/src/index.ts index c0144565..5ada2faa 100644 --- a/packages/local-mcp-server/src/index.ts +++ b/packages/local-mcp-server/src/index.ts @@ -25,7 +25,7 @@ async function main() { Options --server-url, -s Glean server URL (e.g. https://my-company-be.glean.com) - --instance, -i Glean instance name + --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 @@ -33,7 +33,7 @@ async function main() { Examples $ npx @gleanwork/local-mcp-server $ 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 + $ npx @gleanwork/local-mcp-server --instance my-company --token glean_api_xyz # deprecated, use --server-url Version: v${VERSION} `, diff --git a/packages/local-mcp-server/src/server.ts b/packages/local-mcp-server/src/server.ts index fa7881d6..d37535bc 100644 --- a/packages/local-mcp-server/src/server.ts +++ b/packages/local-mcp-server/src/server.ts @@ -249,6 +249,7 @@ export async function runServer(options?: { 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; }