Skip to content
Merged
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
18 changes: 11 additions & 7 deletions packages/local-mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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 instance name>",
"GLEAN_SERVER_URL": "<glean server URL>",
"GLEAN_API_TOKEN": "<glean api token>"
}
}
Expand Down Expand Up @@ -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"
}
}
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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:**

Expand Down
4 changes: 3 additions & 1 deletion packages/local-mcp-server/src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
14 changes: 10 additions & 4 deletions packages/local-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
});
Expand Down
6 changes: 6 additions & 0 deletions packages/local-mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/local-mcp-server/src/test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
10 changes: 9 additions & 1 deletion packages/mcp-server-utils/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,17 @@ export async function getConfig(): Promise<GleanConfig> {
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;

Expand Down
47 changes: 47 additions & 0 deletions packages/mcp-server-utils/src/test/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
47 changes: 45 additions & 2 deletions packages/mcp-server-utils/src/util/preflight.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
// 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;
Expand Down Expand Up @@ -40,3 +47,39 @@ export async function validateInstance(instance: string): Promise<boolean> {
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<boolean> {
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;
}
}